From 5b6b7b28b34f884715d649fda00fc8cf77fe1a5f Mon Sep 17 00:00:00 2001 From: joncrall Date: Fri, 23 May 2025 09:07:24 -0400 Subject: [PATCH 1/5] Use co_qualname in reporting when possible --- CHANGELOG.rst | 1 + line_profiler/_line_profiler.pyx | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7d2f1809..889e82be 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,7 @@ Changes * FIX: Fixed namespace bug when running ``kernprof -m`` on certain modules (e.g. ``calendar`` on Python 3.12+). * FIX: Fixed ``@contextlib.contextmanager`` bug where the cleanup code (e.g. restoration of ``sys`` attributes) is not run if exceptions occurred inside the context * ENH: Added CLI arguments ``-c`` to ``kernprof`` for (auto-)profiling module/package/inline-script execution instead of that of script files; passing ``'-'`` as the script-file name now also reads from and profiles ``stdin`` +* ENH: In Python >=3.11, profiled objects are reported using their qualified name. 4.2.0 ~~~~~ diff --git a/line_profiler/_line_profiler.pyx b/line_profiler/_line_profiler.pyx index ebb6d715..cc79cb70 100644 --- a/line_profiler/_line_profiler.pyx +++ b/line_profiler/_line_profiler.pyx @@ -155,14 +155,18 @@ else: def label(code): """ - Return a (filename, first_lineno, func_name) tuple for a given code object. + Return a (filename, first_lineno, qual_name) tuple for a given code object. - This is the same labelling as used by the cProfile module in Python 2.5. + This is the similar labelling as used by the cProfile module in Python 2.5. """ if isinstance(code, str): return ('~', 0, code) # built-in functions ('~' sorts at the end) else: - return (code.co_filename, code.co_firstlineno, code.co_name) + try: + # Python >=3.11a1 + return (code.co_filename, code.co_firstlineno, code.co_qualname) + except AttributeError: + return (code.co_filename, code.co_firstlineno, code.co_name) cpdef _code_replace(func, co_code): From c25ff359a337e4760ec7e4e9ae0fdde6409b4ffc Mon Sep 17 00:00:00 2001 From: joncrall Date: Fri, 23 May 2025 09:28:35 -0400 Subject: [PATCH 2/5] fix tests --- tests/test_line_profiler.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/test_line_profiler.py b/tests/test_line_profiler.py index 85860b2a..b8a4866c 100644 --- a/tests/test_line_profiler.py +++ b/tests/test_line_profiler.py @@ -257,7 +257,7 @@ def foo(cls) -> str: output = strip(sio.getvalue()) print(output) # Check that we have profiled `Object.foo()` - assert output.endswith('- foo') + assert output.endswith('foo') line, = (line for line in output.splitlines() if line.endswith('* 2')) # Check that it has been run twice assert int(line.split()[1]) == 2 @@ -291,7 +291,7 @@ def foo(x: int) -> int: output = strip(sio.getvalue()) print(output) # Check that we have profiled `Object.foo()` - assert output.endswith('- foo') + assert output.endswith('foo') line, = (line for line in output.splitlines() if line.endswith('* 2')) # Check that it has been run twice assert int(line.split()[1]) == 2 @@ -332,7 +332,7 @@ def foo(self, x: int) -> int: output = strip(sio.getvalue()) print(output) # Check that we have profiled `Object.foo()` - assert output.endswith('- foo') + assert output.endswith('foo') line, = (line for line in output.splitlines() if line.endswith('* x')) # Check that the wrapped methods has been run twice in total assert int(line.split()[1]) == 2 @@ -368,7 +368,7 @@ def foo(self, x: int) -> int: output = strip(sio.getvalue()) print(output) # Check that we have profiled `Object.foo()` (via `.bar()`) - assert output.endswith('- foo') + assert output.endswith('foo') line, = (line for line in output.splitlines() if line.endswith('* x')) # Check that the wrapped method has been run once assert int(line.split()[1]) == 1 @@ -408,7 +408,7 @@ def foo(x: int, y: int) -> int: output = strip(sio.getvalue()) print(output) # Check that we have profiled `foo()` - assert output.endswith('- foo') + assert output.endswith('foo') line, = (line for line in output.splitlines() if line.endswith('x + y')) # Check that the wrapped partials has been run twice in total assert int(line.split()[1]) == 2 @@ -457,7 +457,7 @@ def foo(self, foo) -> None: output = strip(sio.getvalue()) print(output) # Check that we have profiled `Object.foo` - assert output.endswith('- foo') + assert output.endswith('foo') getter_line, = (line for line in output.splitlines() if line.endswith('* 2')) setter_line, = (line for line in output.splitlines() @@ -498,9 +498,8 @@ def foo(self) -> int: with io.StringIO() as sio: profile.print_stats(stream=sio, summarize=True) output = strip(sio.getvalue()) - print(output) # Check that we have profiled `Object.foo` - assert output.endswith('- foo') + assert output.endswith('foo') line, = (line for line in output.splitlines() if line.endswith('* 2')) # Check that the getter has been run once assert int(line.split()[1]) == 1 From 52cbfcda5645e96dc3b7bbd1c6bef930a4d9b0a7 Mon Sep 17 00:00:00 2001 From: joncrall Date: Fri, 23 May 2025 09:32:51 -0400 Subject: [PATCH 3/5] Corrected docstring --- line_profiler/_line_profiler.pyx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/line_profiler/_line_profiler.pyx b/line_profiler/_line_profiler.pyx index cc79cb70..9c5294f9 100644 --- a/line_profiler/_line_profiler.pyx +++ b/line_profiler/_line_profiler.pyx @@ -155,9 +155,13 @@ else: def label(code): """ - Return a (filename, first_lineno, qual_name) tuple for a given code object. + Return a (filename, first_lineno, _name) tuple for a given code object. This is the similar labelling as used by the cProfile module in Python 2.5. + + Note: + In Python >=3.11 we use we return qualname for _name. + In older versions of Python we just return name. """ if isinstance(code, str): return ('~', 0, code) # built-in functions ('~' sorts at the end) From 885dfab3a3d0292efddfa8f37e5afa3db8fa557b Mon Sep 17 00:00:00 2001 From: joncrall Date: Fri, 23 May 2025 13:47:16 -0400 Subject: [PATCH 4/5] check for co_qualname once --- line_profiler/_line_profiler.pyx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/line_profiler/_line_profiler.pyx b/line_profiler/_line_profiler.pyx index 9c5294f9..188d23be 100644 --- a/line_profiler/_line_profiler.pyx +++ b/line_profiler/_line_profiler.pyx @@ -24,6 +24,7 @@ from libc.stdint cimport int64_t from libcpp.unordered_map cimport unordered_map import threading import opcode +import types NOP_VALUE: int = opcode.opmap['NOP'] @@ -32,6 +33,9 @@ NOP_VALUE: int = opcode.opmap['NOP'] # if sys.version_info[0:2] >= (3, 11): NOP_BYTES: bytes = NOP_VALUE.to_bytes(2, byteorder=byteorder) +# This should be true for Python >=3.11a1 +HAS_CO_QUALNAME: bool = hasattr(types.CodeType, 'co_qualname')) + # long long int is at least 64 bytes assuming c99 ctypedef unsigned long long int uint64 ctypedef long long int int64 @@ -166,10 +170,9 @@ def label(code): if isinstance(code, str): return ('~', 0, code) # built-in functions ('~' sorts at the end) else: - try: - # Python >=3.11a1 + if HAS_CO_QUALNAME: return (code.co_filename, code.co_firstlineno, code.co_qualname) - except AttributeError: + else: return (code.co_filename, code.co_firstlineno, code.co_name) From 6bc8b32c41f9e07c8ab80d8bda9a908ff5a6133c Mon Sep 17 00:00:00 2001 From: joncrall Date: Fri, 23 May 2025 14:39:47 -0400 Subject: [PATCH 5/5] fix syntax error --- line_profiler/_line_profiler.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/line_profiler/_line_profiler.pyx b/line_profiler/_line_profiler.pyx index 188d23be..13ac480b 100644 --- a/line_profiler/_line_profiler.pyx +++ b/line_profiler/_line_profiler.pyx @@ -34,7 +34,7 @@ NOP_VALUE: int = opcode.opmap['NOP'] NOP_BYTES: bytes = NOP_VALUE.to_bytes(2, byteorder=byteorder) # This should be true for Python >=3.11a1 -HAS_CO_QUALNAME: bool = hasattr(types.CodeType, 'co_qualname')) +HAS_CO_QUALNAME: bool = hasattr(types.CodeType, 'co_qualname') # long long int is at least 64 bytes assuming c99 ctypedef unsigned long long int uint64