From a8723a02ea109beabe2dfe1bbe18958afc5b7af9 Mon Sep 17 00:00:00 2001 From: Antoine Pitrou Date: Wed, 15 Apr 2015 00:41:29 +0200 Subject: [PATCH] Issue #21217: inspect.getsourcelines() now tries to compute the start and end lines from the code object, fixing an issue when a lambda function is used as decorator argument. Patch by Thomas Ballinger. --- Lib/inspect.py | 37 +++++++++++++++++++++++-------------- Lib/test/inspect_fodder2.py | 10 +++++++++- Lib/test/test_inspect.py | 11 ++++++++++- Misc/ACKS | 1 + Misc/NEWS | 7 ++++++- 5 files changed, 49 insertions(+), 17 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 81b1ce87098..60890f2eec0 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -32,6 +32,7 @@ __author__ = ('Ka-Ping Yee ', 'Yury Selivanov ') import ast +import dis import enum import importlib.machinery import itertools @@ -49,18 +50,10 @@ from operator import attrgetter from collections import namedtuple, OrderedDict # Create constants for the compiler flags in Include/code.h -# We try to get them from dis to avoid duplication, but fall -# back to hard-coding so the dependency is optional -try: - from dis import COMPILER_FLAG_NAMES as _flag_names -except ImportError: - CO_OPTIMIZED, CO_NEWLOCALS = 0x1, 0x2 - CO_VARARGS, CO_VARKEYWORDS = 0x4, 0x8 - CO_NESTED, CO_GENERATOR, CO_NOFREE = 0x10, 0x20, 0x40 -else: - mod_dict = globals() - for k, v in _flag_names.items(): - mod_dict["CO_" + v] = k +# We try to get them from dis to avoid duplication +mod_dict = globals() +for k, v in dis.COMPILER_FLAG_NAMES.items(): + mod_dict["CO_" + v] = k # See Include/object.h TPFLAGS_IS_ABSTRACT = 1 << 20 @@ -888,6 +881,14 @@ def getblock(lines): pass return lines[:blockfinder.last] +def _line_number_helper(code_obj, lines, lnum): + """Return a list of source lines and starting line number for a code object. + + The arguments must be a code object with lines and lnum from findsource. + """ + _, end_line = list(dis.findlinestarts(code_obj))[-1] + return lines[lnum:end_line], lnum + 1 + def getsourcelines(object): """Return a list of source lines and starting line number for an object. @@ -899,8 +900,16 @@ def getsourcelines(object): object = unwrap(object) lines, lnum = findsource(object) - if ismodule(object): return lines, 0 - else: return getblock(lines[lnum:]), lnum + 1 + if ismodule(object): + return lines, 0 + elif iscode(object): + return _line_number_helper(object, lines, lnum) + elif isfunction(object): + return _line_number_helper(object.__code__, lines, lnum) + elif ismethod(object): + return _line_number_helper(object.__func__.__code__, lines, lnum) + else: + return getblock(lines[lnum:]), lnum + 1 def getsource(object): """Return the text of the source code for an object. diff --git a/Lib/test/inspect_fodder2.py b/Lib/test/inspect_fodder2.py index e452235cd8e..ab1cd9f5002 100644 --- a/Lib/test/inspect_fodder2.py +++ b/Lib/test/inspect_fodder2.py @@ -110,6 +110,14 @@ def annotated(arg1: list): def keyword_only_arg(*, arg): pass +@wrap(lambda: None) +def func114(): + return 115 + +class ClassWithMethod: + def method(self): + pass + from functools import wraps def decorator(func): @@ -118,7 +126,7 @@ def decorator(func): return 42 return fake -#line 121 +#line 129 @decorator def real(): return 20 diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 76f2b474012..9e1f546be79 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -392,6 +392,9 @@ class TestRetrievingSourceCode(GetSourceBase): finally: linecache.getlines = getlines + def test_getsource_on_code_object(self): + self.assertSourceEqual(mod.eggs.__code__, 12, 18) + class TestDecorators(GetSourceBase): fodderModule = mod2 @@ -402,7 +405,10 @@ class TestDecorators(GetSourceBase): self.assertSourceEqual(mod2.gone, 9, 10) def test_getsource_unwrap(self): - self.assertSourceEqual(mod2.real, 122, 124) + self.assertSourceEqual(mod2.real, 130, 132) + + def test_decorator_with_lambda(self): + self.assertSourceEqual(mod2.func114, 113, 115) class TestOneliners(GetSourceBase): fodderModule = mod2 @@ -497,6 +503,9 @@ class TestBuggyCases(GetSourceBase): self.assertRaises(IOError, inspect.findsource, co) self.assertRaises(IOError, inspect.getsource, co) + def test_getsource_on_method(self): + self.assertSourceEqual(mod2.ClassWithMethod.method, 118, 119) + class TestNoEOL(GetSourceBase): def __init__(self, *args, **kwargs): self.tempdir = TESTFN + '_dir' diff --git a/Misc/ACKS b/Misc/ACKS index 75a34d4a73b..c26ecf4995a 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -72,6 +72,7 @@ Dwayne Bailey Stig Bakken Greg Ball Luigi Ballabio +Thomas Ballinger Jeff Balogh Manuel Balsera Matt Bandy diff --git a/Misc/NEWS b/Misc/NEWS index ff7e543577e..e3083c1e278 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -1,4 +1,4 @@ -+++++++++++ ++++++++++++ Python News +++++++++++ @@ -9,6 +9,7 @@ Release date: XXX Core and Builtins ----------------- + - Issue #22631: Added Linux-specific socket constant CAN_RAW_FD_FRAMES. Patch courtesy of Joe Jevnik. @@ -31,6 +32,10 @@ Core and Builtins Library ------- +- Issue #21217: inspect.getsourcelines() now tries to compute the start and + end lines from the code object, fixing an issue when a lambda function is + used as decorator argument. Patch by Thomas Ballinger. + - Issue #23811: Add missing newline to the PyCompileError error message. Patch by Alex Shkop.