--- a/attr/_make.py +++ b/attr/_make.py @@ -4,7 +4,6 @@ import linecache import sys import threading -import uuid import warnings from operator import itemgetter @@ -1426,34 +1425,54 @@ return tuple(getattr(obj, a.name) for a in attrs) -def _generate_unique_filename(cls, func_name): +def _compile_and_eval(script, globs, locs=None, filename=""): """ - Create a "filename" suitable for a function being generated. + "Exec" the script with the given global (globs) and local (locs) variables. """ - unique_id = uuid.uuid4() - extra = "" - count = 1 + bytecode = compile(script, filename, "exec") + eval(bytecode, globs, locs) + + +def _make_method(name, script, filename, globs=None): + """ + Create the method with the script given and return the method object. + """ + locs = {} + if globs is None: + globs = {} + # Add generated source to linecache so debuggers can display it. Reuse an + # existing entry when both the generated filename and source are equal. + count = 1 + base_filename = filename while True: - unique_filename = "".format( - func_name, - cls.__module__, - getattr(cls, "__qualname__", cls.__name__), - extra, - ) - # To handle concurrency we essentially "reserve" our spot in - # the linecache with a dummy line. The caller can then - # set this value correctly. - cache_line = (1, None, (str(unique_id),), unique_filename) - if ( - linecache.cache.setdefault(unique_filename, cache_line) - == cache_line - ): - return unique_filename + linecache_tuple = ( + len(script), + None, + script.splitlines(True), + filename, + ) + old_val = linecache.cache.setdefault(filename, linecache_tuple) + if old_val == linecache_tuple: + break - # Looks like this spot is taken. Try again. + filename = "{}-{}>".format(base_filename[:-1], count) count += 1 - extra = "-{0}".format(count) + + _compile_and_eval(script, globs, locs, filename) + return locs[name] + + +def _generate_unique_filename(cls, func_name): + """ + Create a "filename" suitable for a function being generated. + """ + unique_filename = "".format( + func_name, + cls.__module__, + getattr(cls, "__qualname__", cls.__name__), + ) + return unique_filename def _make_hash(cls, attrs, frozen, cache_hash): @@ -1519,21 +1538,7 @@ append_hash_computation_lines("return ", tab) script = "\n".join(method_lines) - globs = {} - locs = {} - bytecode = compile(script, unique_filename, "exec") - eval(bytecode, globs, locs) - - # In order of debuggers like PDB being able to step through the code, - # we add a fake linecache entry. - linecache.cache[unique_filename] = ( - len(script), - None, - script.splitlines(True), - unique_filename, - ) - - return locs["__hash__"] + return _make_method("__hash__", script, unique_filename) def _add_hash(cls, attrs): @@ -1589,20 +1594,7 @@ lines.append(" return True") script = "\n".join(lines) - globs = {} - locs = {} - bytecode = compile(script, unique_filename, "exec") - eval(bytecode, globs, locs) - - # In order of debuggers like PDB being able to step through the code, - # we add a fake linecache entry. - linecache.cache[unique_filename] = ( - len(script), - None, - script.splitlines(True), - unique_filename, - ) - return locs["__eq__"] + return _make_method("__eq__", script, unique_filename) def _make_order(cls, attrs): @@ -1873,8 +1865,6 @@ needs_cached_setattr, has_global_on_setattr, ) - locs = {} - bytecode = compile(script, unique_filename, "exec") globs.update({"NOTHING": NOTHING, "attr_dict": attr_dict}) if needs_cached_setattr: @@ -1882,18 +1872,13 @@ # setattr hooks. globs["_cached_setattr"] = _obj_setattr - eval(bytecode, globs, locs) - - # In order of debuggers like PDB being able to step through the code, - # we add a fake linecache entry. - linecache.cache[unique_filename] = ( - len(script), - None, - script.splitlines(True), + __init__ = _make_method( + "__init__", + script, unique_filename, + globs, ) - __init__ = locs["__init__"] __init__.__annotations__ = annotations return __init__