Skip to content

Commit ce92190

Browse files
committed
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.<locals>.subclass.method"` and chokes over `.<locals>.`. 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 `.<locals>.`), to help with introspection?
1 parent ffb543d commit ce92190

4 files changed

Lines changed: 31 additions & 3 deletions

File tree

Lib/inspect.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,12 @@ def indentsize(line):
697697
return len(expline) - len(expline.lstrip())
698698

699699
def _findclass(func):
700+
try:
701+
idx = func.__code__.co_freevars.index('__class__')
702+
except ValueError:
703+
pass
704+
else:
705+
return func.__closure__[0].cell_contents
700706
cls = sys.modules.get(func.__module__)
701707
if cls is None:
702708
return None

Lib/test/test_inspect/inspect_fodder.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,15 @@ async def asyncf(self):
118118
# a closing parenthesis with the opening paren being in another line
119119
(
120120
); after_closing = lambda: 1
121+
122+
def gen_subclass(abuse_msg):
123+
124+
class subclass(StupidGit):
125+
def abuse(self, a, b, c):
126+
print(abuse_msg)
127+
super().abuse(a, b, c)
128+
129+
return subclass
130+
131+
DynamicSubclass = gen_subclass("some message")
132+
del gen_subclass

Lib/test/test_inspect/test_inspect.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -629,7 +629,8 @@ class TestRetrievingSourceCode(GetSourceBase):
629629
def test_getclasses(self):
630630
classes = inspect.getmembers(mod, inspect.isclass)
631631
self.assertEqual(classes,
632-
[('FesteringGob', mod.FesteringGob),
632+
[('DynamicSubclass', mod.DynamicSubclass),
633+
('FesteringGob', mod.FesteringGob),
633634
('MalodorousPervert', mod.MalodorousPervert),
634635
('ParrotDroppings', mod.ParrotDroppings),
635636
('StupidGit', mod.StupidGit),
@@ -649,7 +650,8 @@ def test_getclasses(self):
649650
mod.ParrotDroppings))
650651
]
651652
],
652-
(mod.WhichComments, (object,),)
653+
(mod.WhichComments, (object,),),
654+
(mod.DynamicSubclass, (mod.StupidGit,)),
653655
]
654656
])
655657
tree = inspect.getclasstree([cls[1] for cls in classes], True)
@@ -662,7 +664,8 @@ def test_getclasses(self):
662664
mod.ParrotDroppings))
663665
]
664666
],
665-
(mod.WhichComments, (object,),)
667+
(mod.WhichComments, (object,),),
668+
(mod.DynamicSubclass, (mod.StupidGit,)),
666669
]
667670
])
668671

@@ -697,6 +700,10 @@ def test_getdoc_inherited(self):
697700
'Another\n\ndocstring\n\ncontaining\n\ntabs')
698701
self.assertEqual(inspect.getdoc(mod.FesteringGob.contradiction),
699702
'The automatic gainsaying.')
703+
self.assertEqual(inspect.getdoc(mod.DynamicSubclass.abuse),
704+
'Another\n\ndocstring\n\ncontaining\n\ntabs')
705+
self.assertEqual(inspect.getdoc(mod.DynamicSubclass().abuse),
706+
'Another\n\ndocstring\n\ncontaining\n\ntabs')
700707

701708
@unittest.skipIf(sys.flags.optimize >= 2,
702709
"Docstrings are omitted with -O2 and above")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
`inspect.getdoc` now correctly inherits docstrings in dynamically generated
2+
subclasses for methods that rely on `super()` (as the owning class can be
3+
retrieved by inspecting the `__class__` cell).

0 commit comments

Comments
 (0)