From 9e9d7afa4504efbba1f3f7d28cd9d0e726a0db66 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Sun, 16 Jul 2023 16:51:23 -0700 Subject: [PATCH 1/3] Improve inspect.getsource for class --- Lib/inspect.py | 57 ++++++++++++++++++++++++++++++------- Lib/test/inspect_fodder2.py | 20 +++++++++++++ Lib/test/test_inspect.py | 5 +++- 3 files changed, 70 insertions(+), 12 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 15f94a194856ac..675714dc8b3f70 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1034,9 +1034,13 @@ class ClassFoundException(Exception): class _ClassFinder(ast.NodeVisitor): - def __init__(self, qualname): + def __init__(self, cls, tree, lines, qualname): self.stack = [] + self.cls = cls + self.tree = tree + self.lines = lines self.qualname = qualname + self.lineno_found = [] def visit_FunctionDef(self, node): self.stack.append(node.name) @@ -1057,11 +1061,48 @@ def visit_ClassDef(self, node): line_number = node.lineno # decrement by one since lines starts with indexing by zero - line_number -= 1 - raise ClassFoundException(line_number) + self.lineno_found.append((line_number - 1, node.end_lineno)) self.generic_visit(node) self.stack.pop() + def get_lineno(self): + self.visit(self.tree) + lineno_found_number = len(self.lineno_found) + if lineno_found_number == 0: + raise OSError('could not find class definition') + elif lineno_found_number == 1: + return self.lineno_found[0][0] + else: + # We have multiple candidates for the class definition. + # Now we have to guess. + + # First, let's see if there are any method definitions + for member in self.cls.__dict__.values(): + if isinstance(member, types.FunctionType): + for lineno, end_lineno in self.lineno_found: + if lineno <= member.__code__.co_firstlineno <= end_lineno: + return lineno + + class_strings = [(''.join(self.lines[lineno: end_lineno]), lineno) + for lineno, end_lineno in self.lineno_found] + + # Maybe the class has a docstring and it's unique? + if self.cls.__doc__: + ret = None + for candidate, lineno in class_strings: + if self.cls.__doc__.strip() in candidate: + if ret is None: + ret = lineno + else: + break + else: + if ret is not None: + return ret + + # We are out of ideas, just return the last one found, which is + # slightly better than previous ones + return self.lineno_found[-1][0] + def findsource(object): """Return the entire source file and starting line number for an object. @@ -1098,14 +1139,8 @@ def findsource(object): qualname = object.__qualname__ source = ''.join(lines) tree = ast.parse(source) - class_finder = _ClassFinder(qualname) - try: - class_finder.visit(tree) - except ClassFoundException as e: - line_number = e.args[0] - return lines, line_number - else: - raise OSError('could not find class definition') + class_finder = _ClassFinder(object, tree, lines, qualname) + return lines, class_finder.get_lineno() if ismethod(object): object = object.__func__ diff --git a/Lib/test/inspect_fodder2.py b/Lib/test/inspect_fodder2.py index 03464613694605..e8097aa3791b6f 100644 --- a/Lib/test/inspect_fodder2.py +++ b/Lib/test/inspect_fodder2.py @@ -290,3 +290,23 @@ def complex_decorated(foo=0, bar=lambda: 0): nested_lambda = ( lambda right: [].map( lambda length: ())) + +# line 294 +if True: + class cls296: + def f(): + pass +else: + class cls296: + def g(): + pass + +# line 304 +if False: + class cls310: + def f(): + pass +else: + class cls310: + def g(): + pass \ No newline at end of file diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 64afeec351b353..33a593f3591d68 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -949,7 +949,6 @@ def test_class_decorator(self): self.assertSourceEqual(mod2.cls196.cls200, 198, 201) def test_class_inside_conditional(self): - self.assertSourceEqual(mod2.cls238, 238, 240) self.assertSourceEqual(mod2.cls238.cls239, 239, 240) def test_multiple_children_classes(self): @@ -975,6 +974,10 @@ def test_nested_class_definition_inside_async_function(self): self.assertSourceEqual(mod2.cls226, 231, 235) self.assertSourceEqual(asyncio.run(mod2.cls226().func232()), 233, 234) + def test_class_definition_same_name_diff_methods(self): + self.assertSourceEqual(mod2.cls296, 296, 298) + self.assertSourceEqual(mod2.cls310, 310, 312) + class TestNoEOL(GetSourceBase): def setUp(self): self.tempdir = TESTFN + '_dir' From 867730b5be8409fd35ab71f13c7a076683608071 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sun, 16 Jul 2023 23:59:35 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2023-07-16-23-59-33.gh-issue-106727.bk3uCu.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2023-07-16-23-59-33.gh-issue-106727.bk3uCu.rst diff --git a/Misc/NEWS.d/next/Library/2023-07-16-23-59-33.gh-issue-106727.bk3uCu.rst b/Misc/NEWS.d/next/Library/2023-07-16-23-59-33.gh-issue-106727.bk3uCu.rst new file mode 100644 index 00000000000000..e4ea0ce1890d2f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-07-16-23-59-33.gh-issue-106727.bk3uCu.rst @@ -0,0 +1 @@ +Make :func:`inspect.getsource` smarter for class for same name definitions From fc0a563d1e26d86b28a2b6f1172881f5eed0344e Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Mon, 17 Jul 2023 14:32:02 -0700 Subject: [PATCH 3/3] Add newline at the end of the file --- Lib/test/inspect_fodder2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/inspect_fodder2.py b/Lib/test/inspect_fodder2.py index e8097aa3791b6f..8639cf2e72cd7a 100644 --- a/Lib/test/inspect_fodder2.py +++ b/Lib/test/inspect_fodder2.py @@ -309,4 +309,4 @@ def f(): else: class cls310: def g(): - pass \ No newline at end of file + pass