Compare commits

...

2 Commits

Author SHA1 Message Date
Miss Islington (bot) a4e7d5f750
bpo-17735: inspect.findsource now raises OSError when co_lineno is out of range (GH-23633)
This can happen when a file was edited after it was imported.
(cherry picked from commit 2e0760bb2e)

Co-authored-by: Irit Katriel <iritkatriel@yahoo.com>
2020-12-04 13:44:53 -08:00
Miss Islington (bot) 3b14f18205
bpo-42116: Fix inspect.getsource handling of trailing comments (GH-23630)
(cherry picked from commit 6e1eec71f5)

Co-authored-by: Irit Katriel <iritkatriel@yahoo.com>
2020-12-04 12:20:09 -08:00
5 changed files with 70 additions and 5 deletions

View File

@ -837,7 +837,12 @@ def findsource(object):
lnum = object.co_firstlineno - 1
pat = re.compile(r'^(\s*def\s)|(\s*async\s+def\s)|(.*(?<!\w)lambda(:|\s))|^(\s*@)')
while lnum > 0:
if pat.match(lines[lnum]): break
try:
line = lines[lnum]
except IndexError:
raise OSError('lineno is out of bounds')
if pat.match(line):
break
lnum = lnum - 1
return lines, lnum
raise OSError('could not find code object')
@ -899,6 +904,7 @@ class BlockFinder:
self.indecorator = False
self.decoratorhasargs = False
self.last = 1
self.body_col0 = None
def tokeneater(self, type, token, srowcol, erowcol, line):
if not self.started and not self.indecorator:
@ -930,6 +936,8 @@ class BlockFinder:
elif self.passline:
pass
elif type == tokenize.INDENT:
if self.body_col0 is None and self.started:
self.body_col0 = erowcol[1]
self.indent = self.indent + 1
self.passline = True
elif type == tokenize.DEDENT:
@ -939,6 +947,10 @@ class BlockFinder:
# not e.g. for "if: else:" or "try: finally:" blocks)
if self.indent <= 0:
raise EndOfBlock
elif type == tokenize.COMMENT:
if self.body_col0 is not None and srowcol[1] >= self.body_col0:
# Include comments if indented at least as much as the block
self.last = srowcol[0]
elif self.indent == 0 and type not in (tokenize.COMMENT, tokenize.NL):
# any other token on the same indentation level end the previous
# block as well, except the pseudo-tokens COMMENT and NL.

View File

@ -91,3 +91,25 @@ class Callable:
custom_method = Callable().as_method_of(42)
del Callable
# line 95
class WhichComments:
# line 97
# before f
def f(self):
# line 100
# start f
return 1
# line 103
# end f
# line 105
# after f
# before asyncf - line 108
async def asyncf(self):
# start asyncf
return 2
# end asyncf
# after asyncf - line 113
# end of WhichComments - line 114
# after WhichComments - line 115

View File

@ -392,6 +392,7 @@ class TestRetrievingSourceCode(GetSourceBase):
('ParrotDroppings', mod.ParrotDroppings),
('StupidGit', mod.StupidGit),
('Tit', mod.MalodorousPervert),
('WhichComments', mod.WhichComments),
])
tree = inspect.getclasstree([cls[1] for cls in classes])
self.assertEqual(tree,
@ -405,7 +406,8 @@ class TestRetrievingSourceCode(GetSourceBase):
[(mod.FesteringGob, (mod.MalodorousPervert,
mod.ParrotDroppings))
]
]
],
(mod.WhichComments, (object,),)
]
])
tree = inspect.getclasstree([cls[1] for cls in classes], True)
@ -417,7 +419,8 @@ class TestRetrievingSourceCode(GetSourceBase):
[(mod.FesteringGob, (mod.MalodorousPervert,
mod.ParrotDroppings))
]
]
],
(mod.WhichComments, (object,),)
]
])
@ -647,6 +650,18 @@ class TestOneliners(GetSourceBase):
# as argument to another function.
self.assertSourceEqual(mod2.anonymous, 55, 55)
class TestBlockComments(GetSourceBase):
fodderModule = mod
def test_toplevel_class(self):
self.assertSourceEqual(mod.WhichComments, 96, 114)
def test_class_method(self):
self.assertSourceEqual(mod.WhichComments.f, 99, 104)
def test_class_async_method(self):
self.assertSourceEqual(mod.WhichComments.asyncf, 109, 112)
class TestBuggyCases(GetSourceBase):
fodderModule = mod2
@ -698,6 +713,17 @@ class TestBuggyCases(GetSourceBase):
self.assertRaises(IOError, inspect.findsource, co)
self.assertRaises(IOError, inspect.getsource, co)
def test_findsource_with_out_of_bounds_lineno(self):
mod_len = len(inspect.getsource(mod))
src = '\n' * 2* mod_len + "def f(): pass"
co = compile(src, mod.__file__, "exec")
g, l = {}, {}
eval(co, g, l)
func = l['f']
self.assertEqual(func.__code__.co_firstlineno, 1+2*mod_len)
with self.assertRaisesRegex(IOError, "lineno is out of bounds"):
inspect.findsource(func)
def test_getsource_on_method(self):
self.assertSourceEqual(mod2.ClassWithMethod.method, 118, 119)
@ -3970,8 +3996,8 @@ def foo():
def test_main():
run_unittest(
TestDecorators, TestRetrievingSourceCode, TestOneliners, TestBuggyCases,
TestInterpreterStack, TestClassesAndFunctions, TestPredicates,
TestDecorators, TestRetrievingSourceCode, TestOneliners, TestBlockComments,
TestBuggyCases, TestInterpreterStack, TestClassesAndFunctions, TestPredicates,
TestGetcallargsFunctions, TestGetcallargsMethods,
TestGetcallargsUnboundMethods, TestGetattrStatic, TestGetGeneratorState,
TestNoEOL, TestSignatureObject, TestSignatureBind, TestParameterObject,

View File

@ -0,0 +1 @@
Fix handling of trailing comments by :func:`inspect.getsource`.

View File

@ -0,0 +1,4 @@
:func:`inspect.findsource` now raises :exc:`OSError` instead of
:exc:`IndexError` when :attr:`co_lineno` of a code object is greater than the
file length. This can happen, for example, when a file is edited after it was
imported. PR by Irit Katriel.