Skip to content

Commit aeaf16c

Browse files
committed
Adjust inspect.getsource to properly extract source for decorated functions
When the arguments to a decorator contain any additional parentheses, ``inspect.BlockFinder`` was misinterpreting the closing paren as the closing paren of the decorator, leading to a clipped result of the extracted source of a function. Instead of assuming the closing paren closes the decorator call itself, we keep track of all the parentheses in a new stack, making sure to unset the decorator context once we consumed all of them.
1 parent 28c9163 commit aeaf16c

File tree

4 files changed

+49
-4
lines changed

4 files changed

+49
-4
lines changed

Lib/inspect.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,7 @@ def __init__(self):
899899
self.indecorator = False
900900
self.decoratorhasargs = False
901901
self.last = 1
902+
self.decoratorparens = []
902903

903904
def tokeneater(self, type, token, srowcol, erowcol, line):
904905
if not self.started and not self.indecorator:
@@ -913,11 +914,18 @@ def tokeneater(self, type, token, srowcol, erowcol, line):
913914
self.passline = True # skip to the end of the line
914915
elif token == "(":
915916
if self.indecorator:
917+
self.decoratorparens.append(token)
916918
self.decoratorhasargs = True
917919
elif token == ")":
918920
if self.indecorator:
919-
self.indecorator = False
920-
self.decoratorhasargs = False
921+
if self.decoratorparens:
922+
if self.decoratorparens[-1] == "(":
923+
self.decoratorparens = self.decoratorparens[:-1]
924+
if not self.decoratorparens:
925+
self.indecorator = False
926+
self.decoratorhasargs = False
927+
else:
928+
self.decoratorparens.append(token)
921929
elif type == tokenize.NEWLINE:
922930
self.passline = False # stop skipping when a NEWLINE is seen
923931
self.last = srowcol[0]

Lib/test/inspect_fodder.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# line 1
22
'A module docstring.'
33

4-
import sys, inspect
4+
import sys, collections, functools, inspect
55
# line 5
66

77
# line 7
@@ -91,3 +91,32 @@ def as_method_of(self, obj):
9191

9292
custom_method = Callable().as_method_of(42)
9393
del Callable
94+
95+
96+
def decorator(*args):
97+
def inner(func):
98+
@functools.wraps(func)
99+
def wrapper():
100+
pass
101+
return wrapper
102+
return inner
103+
104+
105+
@decorator(dict(), 24)
106+
@decorator(dict(), 1)
107+
@decorator(dict(), collections.defaultdict(lambda: 1))
108+
@decorator("string containing )", "other string ()")
109+
@decorator(
110+
(()),
111+
collections.defaultdict(lambda: 1),
112+
[(i, j) for i, j in enumerate(range(5))],
113+
)
114+
def decorated():
115+
local_var = 2
116+
return local_var + 42
117+
118+
119+
@decorator()
120+
def other_decorated():
121+
local_var = 2
122+
return local_var + 42

Lib/test/test_inspect.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,8 +419,11 @@ def test_getclasses(self):
419419

420420
def test_getfunctions(self):
421421
functions = inspect.getmembers(mod, inspect.isfunction)
422-
self.assertEqual(functions, [('eggs', mod.eggs),
422+
self.assertEqual(functions, [('decorated', mod.decorated),
423+
('decorator', mod.decorator),
424+
('eggs', mod.eggs),
423425
('lobbest', mod.lobbest),
426+
('other_decorated', mod.other_decorated),
424427
('spam', mod.spam)])
425428

426429
@unittest.skipIf(sys.flags.optimize >= 2,
@@ -493,6 +496,8 @@ def test_getsource(self):
493496
self.assertSourceEqual(git.abuse, 29, 39)
494497
self.assertSourceEqual(mod.StupidGit, 21, 51)
495498
self.assertSourceEqual(mod.lobbest, 75, 76)
499+
self.assertSourceEqual(mod.decorated, 105, 116)
500+
self.assertSourceEqual(mod.other_decorated, 119, 122)
496501

497502
def test_getsourcefile(self):
498503
self.assertEqual(normcase(inspect.getsourcefile(mod.spam)), modfile)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Properly extract source with ``inspect.getsource`` for decorated functions.
2+
3+
Patch by Claudiu Popa

0 commit comments

Comments
 (0)