diff --git a/src/core/IronPython/Compiler/Ast/PythonAst.cs b/src/core/IronPython/Compiler/Ast/PythonAst.cs index 383537c33..e95525361 100644 --- a/src/core/IronPython/Compiler/Ast/PythonAst.cs +++ b/src/core/IronPython/Compiler/Ast/PythonAst.cs @@ -369,6 +369,7 @@ internal MSAst.Expression ReduceWorker() { var end = IndexToLocation(ret.Expression.EndIndex); return Ast.Block( + MakeEntrySequencePoint(ret.Expression.StartIndex), Ast.DebugInfo( _document, start.Line, @@ -381,6 +382,10 @@ internal MSAst.Expression ReduceWorker() { } ReadOnlyCollectionBuilder block = new ReadOnlyCollectionBuilder(); + + // Add beginning sequence point: the location of the 'call' trace event, as in FunctionDefinition. + // Without it, the first statement would take that role, and fire no 'line' event of its own. + block.Add(MakeEntrySequencePoint(_body is SuiteStatement { Statements: [var first, ..] } ? first.StartIndex : _body.StartIndex)); AddInitialization(block); string doc = GetDocumentation(_body); @@ -416,6 +421,13 @@ internal MSAst.Expression ReduceWorker() { return body; } + private MSAst.Expression MakeEntrySequencePoint(int index) { + var loc = index >= 0 ? IndexToLocation(index) : new SourceLocation(0, 1, 1); + return AstUtils.AddDebugInfo( + AstUtils.Empty(), _document, new SourceLocation(0, loc.Line, loc.Column), new SourceLocation(0, loc.Line, int.MaxValue)); + } + + private void AddInitialization(ReadOnlyCollectionBuilder block) { if (IsModule) { block.Add(AssignValue(GetVariableExpression(FileVariable), Ast.Constant(ModuleFileName))); diff --git a/tests/suite/modules/system_related/test_sys.py b/tests/suite/modules/system_related/test_sys.py index f70abbe16..fce53209c 100644 --- a/tests/suite/modules/system_related/test_sys.py +++ b/tests/suite/modules/system_related/test_sys.py @@ -155,18 +155,21 @@ def temp_func(*args, **kwargs): sys.settrace(None) self.assertEqual(sys.gettrace(), None) - @unittest.expectedFailure # https://github.com/IronLanguages/main/issues/740 def test_cp24381(self): + # https://github.com/IronLanguages/main/issues/740 CP24381_MESSAGES = [] import sys orig_sys_trace_func = sys.gettrace() - def f(*args): - CP24381_MESSAGES.extend(args[1:]) - return f - cp24381_module = "cp24381_%d" % os.getpid() cp24381_file_name = cp24381_module + ".py" + + def f(frame, event, arg): + # the import machinery (importlib) is Python code too, and is traced as well; keep the events of the module only + if os.path.basename(frame.f_code.co_filename) == cp24381_file_name: + CP24381_MESSAGES.extend((event, arg)) + return f + cp24381_contents = """ print('a') print('b') @@ -185,6 +188,7 @@ def f(): __import__(cp24381_module) finally: sys.settrace(orig_sys_trace_func) + sys.modules.pop(cp24381_module, None) os.unlink(cp24381_file_name) self.assertEqual(CP24381_MESSAGES, @@ -192,6 +196,45 @@ def f(): None, 'line', None, 'call', None, 'line', None, 'return', None, 'return', None]) + def test_settrace_first_line(self): + # The first statement of module code gets a 'line' event of its own, as in a function. + # The 'call' event of module code is on the line of its first statement, as in Python 3.4. + # CPython changed that later: 3.10 puts it always on line 1, and 3.11 on line 0 + check_module_call_line = is_cli or sys.version_info < (3, 10) + + cases = [ + ("x = 1\ny = 2\n", "exec", [('call', 1), ('line', 1), ('line', 2), ('return', 2)]), + ("\n\nx = 1\ny = 2\n", "exec", [('call', 3), ('line', 3), ('line', 4), ('return', 4)]), + ("'doc'\nx = 1\n", "exec", [('call', 1), ('line', 1), ('line', 2), ('return', 2)]), + ("def g(): pass\ng()\n", "exec", [('call', 1), ('line', 1), ('line', 2), ('call', 1), ('line', 1), ('return', 1), ('return', 2)]), + ("1 + 2", "eval", [('call', 1), ('line', 1), ('return', 1)]), + ("x = 1", "single", [('call', 1), ('line', 1), ('return', 1)]), + ] + + orig_sys_trace_func = sys.gettrace() + for source, mode, expected in cases: + events = [] + def f(frame, event, arg): + if frame.f_code.co_filename == '': + module_call = event == 'call' and frame.f_code.co_name == '' + events.append((event, None if module_call and not check_module_call_line else frame.f_lineno)) + return f + + if not check_module_call_line: + expected = [('call', None)] + expected[1:] + + code = compile(source, '', mode) + try: + sys.settrace(f) + if mode == 'eval': + eval(code, {}) + else: + exec(code, {}) + finally: + sys.settrace(orig_sys_trace_func) + + self.assertEqual(events, expected, (source, mode)) + def test_cp30130(self): def f(frame, event, arg): if event == 'exception':