Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,31 @@ def getblock(lines):
pass
return lines[:blockfinder.last]

def _find_string_start(lines, lnum):
"""Return the index of the line where lines[lnum] begins, or the index
of the line where a multiline string that is still open at the start of
lines[lnum] begins.
"""
# A line that starts inside a string must contain the closing quote.
if '"' not in lines[lnum] and "'" not in lines[lnum]:
return lnum
starts = []
try:
for tok in tokenize.generate_tokens(iter(lines[:lnum + 1]).__next__):
if tok.start[0] > lnum:
break
if tok.type in (tokenize.FSTRING_START, tokenize.TSTRING_START):
starts.append(tok.start[0] - 1)
elif tok.type in (tokenize.FSTRING_END, tokenize.TSTRING_END):
starts.pop()
elif (tok.end[0] > lnum and
tok.type in (tokenize.STRING, tokenize.FSTRING_MIDDLE,
tokenize.TSTRING_MIDDLE)):
return starts[0] if starts else tok.start[0] - 1
except (tokenize.TokenError, SyntaxError):
pass
return lnum

def getsourcelines(object):
"""Return a list of source lines and starting line number for an object.

Expand All @@ -1183,6 +1208,17 @@ def getsourcelines(object):
(isframe(object) and object.f_code.co_name == "<module>")):
return lines, 0
else:
# A lambda or generator expression may start in the middle of a
# multiline string, which cannot be tokenized without the line
# where the string begins.
if isframe(object):
object = object.f_code
elif ismethod(object):
object = object.__func__
if isfunction(object):
object = object.__code__
if iscode(object) and object.co_name.startswith('<'):
lnum = _find_string_start(lines, lnum)
return getblock(lines[lnum:]), lnum + 1

def getsource(object):
Expand Down
26 changes: 26 additions & 0 deletions Lib/test/test_inspect/inspect_fodder2.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,29 @@ def func400():
return 401

pass # end of file

# line 405
string_then_lambda = (
"""text
""", lambda: None
)

def after_string_then_lambda():
"""A docstring after the lambda."""

# line 414
string_then_multiline_lambda = (
'''text
''', lambda: [1,
2]
)

string_then_fstring_lambda = (
f"""text
{1}""", lambda: None
)

string_then_genexpr = (
"""text
""", (i for i in range(3))
)
25 changes: 25 additions & 0 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,16 @@ def test_nested_lambda(self):
# Test inspect.getsource with a nested lambda function.
self.assertSourceEqual(mod2.nested_lambda, 291, 292)

def test_lambda_after_multiline_string(self):
# gh-158556: the lambda starts on the line where a multiline string
# ends, and a later docstring must not change the result.
self.assertSourceEqual(mod2.string_then_lambda[1], 407, 408)
self.assertSourceEqual(mod2.string_then_multiline_lambda[1], 416, 418)
self.assertSourceEqual(mod2.string_then_fstring_lambda[1], 422, 423)

def test_genexpr_after_multiline_string(self):
self.assertSourceEqual(mod2.string_then_genexpr[1].gi_code, 427, 428)

def test_onelinefunc(self):
# Test inspect.getsource with a regular one-line function.
self.assertSourceEqual(mod2.onelinefunc, 37, 37)
Expand Down Expand Up @@ -1120,6 +1130,21 @@ def test_dataclass(self):
self.assertRaises(OSError, inspect.getsource, mod2.dc370)
self.assertRaises(OSError, inspect.getsource, mod2.dc371)

def test_lambda_after_multiline_string_end(self):
# gh-158556: a lambda on the same line as the closing delimiter of a
# multiline string must not make getblock() fail while tokenizing.
src = textwrap.dedent('''\
pair = (
"""text
""", lambda: None
)
''')
with ready_to_import('lambda_after_string', src) as (name, path):
module = import_helper.import_module(name)
lines, lnum = inspect.getsourcelines(module.pair[1])
self.assertEqual(lines, [' """text\n', '""", lambda: None\n'])
self.assertEqual(lnum, 2)

class TestBlockComments(GetSourceBase):
fodderModule = mod

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :func:`inspect.getsource` and :func:`inspect.getsourcelines` for a lambda
or generator expression that starts on the line where a multiline string ends.
The returned source now begins on the line where that string starts.