diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cd1c9c2b..2b6c7acf 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. * ENH: Highlight final summary using rich if enabled 4.2.0 diff --git a/line_profiler/_line_profiler.pyx b/line_profiler/_line_profiler.pyx index ebb6d715..13ac480b 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 @@ -155,14 +159,21 @@ else: def label(code): """ - Return a (filename, first_lineno, func_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. - This is the same 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) else: - return (code.co_filename, code.co_firstlineno, code.co_name) + if HAS_CO_QUALNAME: + return (code.co_filename, code.co_firstlineno, code.co_qualname) + else: + return (code.co_filename, code.co_firstlineno, code.co_name) cpdef _code_replace(func, co_code): diff --git a/tests/test_line_profiler.py b/tests/test_line_profiler.py index 46322dbc..3075189a 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