Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 696136b

Browse files
authored
bpo-35113: Fix inspect.getsource to return correct source for inner classes (#10307)
* Use ast module to find class definition * Add NEWS entry * Fix class with multiple children and move decorator code to the method * Fix PR comments 1. Use node.decorator_list to select decorators 2. Remove unwanted variables in ClassVisitor 3. Simplify stack management as per review * Add test for nested functions and async calls * Fix pydoc test since comments are returned now correctly * Set event loop policy as None to fix environment related change * Refactor visit_AsyncFunctionDef and tests * Refactor to use local variables and fix tests * Add patch attribution * Use self.addCleanup for asyncio * Rename ClassVisitor to ClassFinder and fix asyncio cleanup * Return first class inside conditional in case of multiple definitions. Remove decorator for class source. * Add docstring to make the test correct * Modify NEWS entry regarding decorators * Return decorators too for bpo-15856 * Move ast and the class source code to top. Use proper Exception.
1 parent ce57883 commit 696136b

File tree

5 files changed

+200
-23
lines changed

5 files changed

+200
-23
lines changed

Lib/inspect.py

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
'Yury Selivanov <[email protected]>')
3333

3434
import abc
35+
import ast
3536
import dis
3637
import collections.abc
3738
import enum
@@ -770,6 +771,42 @@ def getmodule(object, _filename=None):
770771
if builtinobject is object:
771772
return builtin
772773

774+
775+
class ClassFoundException(Exception):
776+
pass
777+
778+
779+
class _ClassFinder(ast.NodeVisitor):
780+
781+
def __init__(self, qualname):
782+
self.stack = []
783+
self.qualname = qualname
784+
785+
def visit_FunctionDef(self, node):
786+
self.stack.append(node.name)
787+
self.stack.append('<locals>')
788+
self.generic_visit(node)
789+
self.stack.pop()
790+
self.stack.pop()
791+
792+
visit_AsyncFunctionDef = visit_FunctionDef
793+
794+
def visit_ClassDef(self, node):
795+
self.stack.append(node.name)
796+
if self.qualname == '.'.join(self.stack):
797+
# Return the decorator for the class if present
798+
if node.decorator_list:
799+
line_number = node.decorator_list[0].lineno
800+
else:
801+
line_number = node.lineno
802+
803+
# decrement by one since lines starts with indexing by zero
804+
line_number -= 1
805+
raise ClassFoundException(line_number)
806+
self.generic_visit(node)
807+
self.stack.pop()
808+
809+
773810
def findsource(object):
774811
"""Return the entire source file and starting line number for an object.
775812
@@ -802,25 +839,15 @@ def findsource(object):
802839
return lines, 0
803840

804841
if isclass(object):
805-
name = object.__name__
806-
pat = re.compile(r'^(\s*)class\s*' + name + r'\b')
807-
# make some effort to find the best matching class definition:
808-
# use the one with the least indentation, which is the one
809-
# that's most probably not inside a function definition.
810-
candidates = []
811-
for i in range(len(lines)):
812-
match = pat.match(lines[i])
813-
if match:
814-
# if it's at toplevel, it's already the best one
815-
if lines[i][0] == 'c':
816-
return lines, i
817-
# else add whitespace to candidate list
818-
candidates.append((match.group(1), i))
819-
if candidates:
820-
# this will sort by whitespace, and by line number,
821-
# less whitespace first
822-
candidates.sort()
823-
return lines, candidates[0][1]
842+
qualname = object.__qualname__
843+
source = ''.join(lines)
844+
tree = ast.parse(source)
845+
class_finder = _ClassFinder(qualname)
846+
try:
847+
class_finder.visit(tree)
848+
except ClassFoundException as e:
849+
line_number = e.args[0]
850+
return lines, line_number
824851
else:
825852
raise OSError('could not find class definition')
826853

Lib/test/inspect_fodder2.py

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,18 +138,124 @@ def func137():
138138
never_reached1
139139
never_reached2
140140

141-
#line 141
141+
# line 141
142+
class cls142:
143+
a = """
144+
class cls149:
145+
...
146+
"""
147+
148+
# line 148
149+
class cls149:
150+
151+
def func151(self):
152+
pass
153+
154+
'''
155+
class cls160:
156+
pass
157+
'''
158+
159+
# line 159
160+
class cls160:
161+
162+
def func162(self):
163+
pass
164+
165+
# line 165
166+
class cls166:
167+
a = '''
168+
class cls175:
169+
...
170+
'''
171+
172+
# line 172
173+
class cls173:
174+
175+
class cls175:
176+
pass
177+
178+
# line 178
179+
class cls179:
180+
pass
181+
182+
# line 182
183+
class cls183:
184+
185+
class cls185:
186+
187+
def func186(self):
188+
pass
189+
190+
def class_decorator(cls):
191+
return cls
192+
193+
# line 193
194+
@class_decorator
195+
@class_decorator
196+
class cls196:
197+
198+
@class_decorator
199+
@class_decorator
200+
class cls200:
201+
pass
202+
203+
class cls203:
204+
class cls204:
205+
class cls205:
206+
pass
207+
class cls207:
208+
class cls205:
209+
pass
210+
211+
# line 211
212+
def func212():
213+
class cls213:
214+
pass
215+
return cls213
216+
217+
# line 217
218+
class cls213:
219+
def func219(self):
220+
class cls220:
221+
pass
222+
return cls220
223+
224+
# line 224
225+
async def func225():
226+
class cls226:
227+
pass
228+
return cls226
229+
230+
# line 230
231+
class cls226:
232+
async def func232(self):
233+
class cls233:
234+
pass
235+
return cls233
236+
237+
if True:
238+
class cls238:
239+
class cls239:
240+
'''if clause cls239'''
241+
else:
242+
class cls238:
243+
class cls239:
244+
'''else clause 239'''
245+
pass
246+
247+
#line 247
142248
def positional_only_arg(a, /):
143249
pass
144250

145-
#line 145
251+
#line 251
146252
def all_markers(a, b, /, c, d, *, e, f):
147253
pass
148254

149-
# line 149
255+
# line 255
150256
def all_markers_with_args_and_kwargs(a, b, /, c, d, *args, e, f, **kwargs):
151257
pass
152258

153-
#line 153
259+
#line 259
154260
def all_markers_with_defaults(a, b=1, /, c=2, d=3, *, e=4, f=5):
155261
pass

Lib/test/test_inspect.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,7 @@ def test_cleandoc(self):
473473
def test_getcomments(self):
474474
self.assertEqual(inspect.getcomments(mod), '# line 1\n')
475475
self.assertEqual(inspect.getcomments(mod.StupidGit), '# line 20\n')
476+
self.assertEqual(inspect.getcomments(mod2.cls160), '# line 159\n')
476477
# If the object source file is not available, return None.
477478
co = compile('x=1', '_non_existing_filename.py', 'exec')
478479
self.assertIsNone(inspect.getcomments(co))
@@ -709,6 +710,45 @@ def test_getsource_on_method(self):
709710
def test_nested_func(self):
710711
self.assertSourceEqual(mod2.cls135.func136, 136, 139)
711712

713+
def test_class_definition_in_multiline_string_definition(self):
714+
self.assertSourceEqual(mod2.cls149, 149, 152)
715+
716+
def test_class_definition_in_multiline_comment(self):
717+
self.assertSourceEqual(mod2.cls160, 160, 163)
718+
719+
def test_nested_class_definition_indented_string(self):
720+
self.assertSourceEqual(mod2.cls173.cls175, 175, 176)
721+
722+
def test_nested_class_definition(self):
723+
self.assertSourceEqual(mod2.cls183, 183, 188)
724+
self.assertSourceEqual(mod2.cls183.cls185, 185, 188)
725+
726+
def test_class_decorator(self):
727+
self.assertSourceEqual(mod2.cls196, 194, 201)
728+
self.assertSourceEqual(mod2.cls196.cls200, 198, 201)
729+
730+
def test_class_inside_conditional(self):
731+
self.assertSourceEqual(mod2.cls238, 238, 240)
732+
self.assertSourceEqual(mod2.cls238.cls239, 239, 240)
733+
734+
def test_multiple_children_classes(self):
735+
self.assertSourceEqual(mod2.cls203, 203, 209)
736+
self.assertSourceEqual(mod2.cls203.cls204, 204, 206)
737+
self.assertSourceEqual(mod2.cls203.cls204.cls205, 205, 206)
738+
self.assertSourceEqual(mod2.cls203.cls207, 207, 209)
739+
self.assertSourceEqual(mod2.cls203.cls207.cls205, 208, 209)
740+
741+
def test_nested_class_definition_inside_function(self):
742+
self.assertSourceEqual(mod2.func212(), 213, 214)
743+
self.assertSourceEqual(mod2.cls213, 218, 222)
744+
self.assertSourceEqual(mod2.cls213().func219(), 220, 221)
745+
746+
def test_nested_class_definition_inside_async_function(self):
747+
import asyncio
748+
self.addCleanup(asyncio.set_event_loop_policy, None)
749+
self.assertSourceEqual(asyncio.run(mod2.func225()), 226, 227)
750+
self.assertSourceEqual(mod2.cls226, 231, 235)
751+
self.assertSourceEqual(asyncio.run(mod2.cls226().func232()), 233, 234)
712752

713753
class TestNoEOL(GetSourceBase):
714754
def setUp(self):

Lib/test/test_pydoc.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@ def test_getpager_with_stdin_none(self):
476476
def test_non_str_name(self):
477477
# issue14638
478478
# Treat illegal (non-str) name like no name
479+
479480
class A:
480481
__name__ = 42
481482
class B:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:meth:`inspect.getsource` now returns correct source code for inner class
2+
with same name as module level class. Decorators are also returned as part
3+
of source of the class. Patch by Karthikeyan Singaravelan.

0 commit comments

Comments
 (0)