From ce92190455c50034638944df38f997f96e23bbc2 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 27 Oct 2019 16:02:53 +0100 Subject: [PATCH] bpo-38603: Inherit docstrings in dynamically generated subclasses if possible. Currently, `inspect.getdoc()` fails to inherit docstrings in dynamically generated subclasses, such as ``` class Base: def method(self): "some docstring" def make_subclass(): class subclass(Base): def method(self): return super().method() return subclass subclass = make_subclass() inspect.getdoc(subclass.method) # => returns None ``` because `inspect._findclass()` tries to find the base class by parsing `subclass.method.__qualname__` which is `"make_subclass..subclass.method"` and chokes over `..`. In the case where the method does rely on `super()`, there is another way we can go back to the "owning" class of the method: by looking up the contents of the `__class__` cell (which is set up to make 0-arg super()). This approach is implemented by this PR. Perhaps a `__class__` cell could even be set up (in a separate patch) for *all* methods defined in dynamically created subclasses (i.e. whose `__qualname__` includes `..`), to help with introspection? --- Lib/inspect.py | 6 ++++++ Lib/test/test_inspect/inspect_fodder.py | 12 ++++++++++++ Lib/test/test_inspect/test_inspect.py | 13 ++++++++++--- .../2019-11-24-21-58-04.bpo-38603.2RMn1t.rst | 3 +++ 4 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2019-11-24-21-58-04.bpo-38603.2RMn1t.rst diff --git a/Lib/inspect.py b/Lib/inspect.py index 9eb87b0d277918..0a1bc8ccc311b0 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -697,6 +697,12 @@ def indentsize(line): return len(expline) - len(expline.lstrip()) def _findclass(func): + try: + idx = func.__code__.co_freevars.index('__class__') + except ValueError: + pass + else: + return func.__closure__[0].cell_contents cls = sys.modules.get(func.__module__) if cls is None: return None diff --git a/Lib/test/test_inspect/inspect_fodder.py b/Lib/test/test_inspect/inspect_fodder.py index febd54c86fe1d1..aeced786130c52 100644 --- a/Lib/test/test_inspect/inspect_fodder.py +++ b/Lib/test/test_inspect/inspect_fodder.py @@ -118,3 +118,15 @@ async def asyncf(self): # a closing parenthesis with the opening paren being in another line ( ); after_closing = lambda: 1 + +def gen_subclass(abuse_msg): + + class subclass(StupidGit): + def abuse(self, a, b, c): + print(abuse_msg) + super().abuse(a, b, c) + + return subclass + +DynamicSubclass = gen_subclass("some message") +del gen_subclass diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index efe9d27e3407ff..66ac65d71e6b49 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -629,7 +629,8 @@ class TestRetrievingSourceCode(GetSourceBase): def test_getclasses(self): classes = inspect.getmembers(mod, inspect.isclass) self.assertEqual(classes, - [('FesteringGob', mod.FesteringGob), + [('DynamicSubclass', mod.DynamicSubclass), + ('FesteringGob', mod.FesteringGob), ('MalodorousPervert', mod.MalodorousPervert), ('ParrotDroppings', mod.ParrotDroppings), ('StupidGit', mod.StupidGit), @@ -649,7 +650,8 @@ def test_getclasses(self): mod.ParrotDroppings)) ] ], - (mod.WhichComments, (object,),) + (mod.WhichComments, (object,),), + (mod.DynamicSubclass, (mod.StupidGit,)), ] ]) tree = inspect.getclasstree([cls[1] for cls in classes], True) @@ -662,7 +664,8 @@ def test_getclasses(self): mod.ParrotDroppings)) ] ], - (mod.WhichComments, (object,),) + (mod.WhichComments, (object,),), + (mod.DynamicSubclass, (mod.StupidGit,)), ] ]) @@ -697,6 +700,10 @@ def test_getdoc_inherited(self): 'Another\n\ndocstring\n\ncontaining\n\ntabs') self.assertEqual(inspect.getdoc(mod.FesteringGob.contradiction), 'The automatic gainsaying.') + self.assertEqual(inspect.getdoc(mod.DynamicSubclass.abuse), + 'Another\n\ndocstring\n\ncontaining\n\ntabs') + self.assertEqual(inspect.getdoc(mod.DynamicSubclass().abuse), + 'Another\n\ndocstring\n\ncontaining\n\ntabs') @unittest.skipIf(sys.flags.optimize >= 2, "Docstrings are omitted with -O2 and above") diff --git a/Misc/NEWS.d/next/Library/2019-11-24-21-58-04.bpo-38603.2RMn1t.rst b/Misc/NEWS.d/next/Library/2019-11-24-21-58-04.bpo-38603.2RMn1t.rst new file mode 100644 index 00000000000000..85f0694ef83051 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-11-24-21-58-04.bpo-38603.2RMn1t.rst @@ -0,0 +1,3 @@ +`inspect.getdoc` now correctly inherits docstrings in dynamically generated +subclasses for methods that rely on `super()` (as the owning class can be +retrieved by inspecting the `__class__` cell).