Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 34 additions & 15 deletions IPython/extensions/deduperreload/deduperreload.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,12 +445,10 @@ def _patch_namespace_inner(
to_patch_to = namespace_to_check.__dict__[name]
if isinstance(to_patch_to, (staticmethod, classmethod)):
to_patch_to = to_patch_to.__func__
# exec new source code using old function's (obj) globals environment.
func_code = textwrap.dedent(ast.unparse(new_ast_def))
if is_method := (len(prefixes) > 0):
func_code = "class __autoreload_class__:\n" + textwrap.indent(
func_code, " "
)
# Exec the new definition using the old function's globals. Keep
# the source locations from the parsed AST so tracebacks continue
# to point at the original file after a reload.
is_method = len(prefixes) > 0
global_env = ns.__dict__
if not isinstance(global_env, dict):
global_env = dict(global_env)
Expand All @@ -460,16 +458,37 @@ def _patch_namespace_inner(
and to_patch_to.__code__.co_filename
or "<string>"
)
func_asts = [ast.parse(func_code)]
if len(cast(ast.FunctionDef, func_asts[0].body[0]).decorator_list) > 0:
without_decorator_list = pickle.loads(pickle.dumps(func_asts[0]))
cast(
ast.FunctionDef, without_decorator_list.body[0]
).decorator_list = []
func_asts.insert(0, without_decorator_list)
for func_ast in func_asts:
if is_method or new_ast_def.decorator_list:
# Keep source reconstruction for methods and decorated
# functions, whose wrapper/decorator handling relies on it.
func_code = textwrap.dedent(ast.unparse(new_ast_def))
if is_method:
func_code = "class __autoreload_class__:\n" + textwrap.indent(
func_code, " "
)
function_asts = [ast.parse(func_code)]
if (
len(
cast(
ast.FunctionDef, function_asts[0].body[0]
).decorator_list
)
> 0
):
without_decorator_list = pickle.loads(
pickle.dumps(function_asts[0])
)
cast(
ast.FunctionDef, without_decorator_list.body[0]
).decorator_list = []
function_asts.insert(0, without_decorator_list)
else:
# Compiling the original AST preserves source line metadata
# so tracebacks continue to point at the reloaded file.
function_asts = [ast.Module(body=[new_ast_def], type_ignores=[])]
for function_ast in function_asts:
compiled_code = compile(
func_ast, filename, mode="exec", dont_inherit=True
function_ast, filename, mode="exec", dont_inherit=True
)
exec(compiled_code, global_env, local_env)
# local_env contains the function exec'd from new version of function
Expand Down
14 changes: 14 additions & 0 deletions IPython/extensions/tests/test_deduperreload.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,20 @@ def foo(n):
assert mod.foo(2) == 2


def test_patching_preserves_traceback_line_number(deduperreloader):
code1 = "def foo():\n return 1\n"
code2 = "\n\n\n\ndef foo():\n raise RuntimeError\n"
deduperreloader._to_autoreload.defs_to_reload = [
(("foo",), ast.parse(code2).body[0])
]
mod = ModuleType("mod")
exec(code1, mod.__dict__)

deduperreloader._patch_namespace(mod)

assert mod.foo.__code__.co_firstlineno == 5


def test_add_function(deduperreloader):
code1 = squish_text(
"""
Expand Down
Loading