Skip to content

Commit 1b72427

Browse files
committed
Fix bug in colon seeker for multiline docstrings in Py < 3.8
Multiline docstrings in Python versions prior to 3.8 require rewinding through the body to seek the opening of the multiline docstring, which is the line after the close of the function def. The previous logic contained an indexing bug where it rewound prematurely at the beginning. This wasn't a problem for multiline docstrings with more than one line of content, but if the multiline docstring is empty or contained only one file then this jumped over the opening of the docstring and just keeps rewinding until an unrelated triple quote is found (or even all the way around to the starting point). This indexing has been adjusted & the comments changed to better explain the flow of the seeker. Off-by-one errors, you'll never be safe.
1 parent 94d61fc commit 1b72427

File tree

3 files changed

+23
-8
lines changed

3 files changed

+23
-8
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html) (`<ma
66
* #98 Add `--dispatch-decorators` to support suppression of all errors from functions decorated by decorators such as `functools.singledispatch` and `functools.singledispatchmethod`.
77
* #99 Add `--overload-decorators` to support generic aliasing of the `typing.overload` decorator.
88

9+
### Fixed
10+
* #106 Fix incorrect parsing of multiline docstrings with less than two lines of content, causing incorrect line numbers for yielded errors in Python versions prior to 3.8
11+
912
## [v2.5.0]
1013
### Added
1114
* #103 Add `--allow-untyped-nested` to suppress all errors from dynamically typted nested functions. A function is considered dynamically typed if it does not contain any type hints.

flake8_annotations/__init__.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -329,25 +329,25 @@ def colon_seeker(node: AST_FUNCTION_TYPES, lines: List[str]) -> Tuple[int, int]:
329329
if node.lineno == node.body[0].lineno:
330330
return Function._single_line_colon_seeker(node, lines[node.lineno - 1])
331331

332-
def_end_lineno = node.body[0].lineno - 1
333-
334332
# With Python < 3.8, the function node includes the docstring & the body does not, so
335333
# we have rewind through any docstrings, if present, before looking for the def colon
334+
# We should end up with lines[def_end_lineno - 1] having the colon
335+
def_end_lineno = node.body[0].lineno
336336
if not PY_GTE_38:
337-
# This list index is a little funky, since we've already subtracted 1 outside of this
338-
# context, we can leave it as-is since it will index the list to the line prior to where
339-
# the function node's body begins.
340337
# If the docstring is on one line then no rewinding is necessary.
341-
n_triple_quotes = lines[def_end_lineno].count('"""')
338+
n_triple_quotes = lines[def_end_lineno - 1].count('"""')
342339
if n_triple_quotes == 1: # pragma: no branch
343340
# Docstring closure, rewind until the opening is found & take the line prior
344341
while True:
345342
def_end_lineno -= 1
346343
if '"""' in lines[def_end_lineno - 1]:
347344
# Docstring has closed
348-
def_end_lineno -= 1
349345
break
350346

347+
# Once we've gotten here, we've found the line where the docstring begins, so we have
348+
# to step up one more line to get to the close of the def
349+
def_end_lineno -= 1
350+
351351
# Use str.rfind() to account for annotations on the same line, definition closure should
352352
# be the last : on the line
353353
def_end_col_offset = lines[def_end_lineno - 1].rfind(":") + 1

testing/test_cases/column_line_numbers_test_cases.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def foo(): # 1
139139
),
140140
error_locations=((1, 10),),
141141
),
142-
"multiline_docstring_only_summary": ParserTestCase(
142+
"multiline_docstring_summary_at_open": ParserTestCase(
143143
src=dedent(
144144
"""\
145145
def foo(): # 1
@@ -150,4 +150,16 @@ def foo(): # 1
150150
),
151151
error_locations=((1, 10),),
152152
),
153+
"multiline_docstring_single_line_summary": ParserTestCase(
154+
src=dedent(
155+
"""\
156+
def foo(): # 1
157+
\"\"\" # 2
158+
Some docstring. # 3
159+
\"\"\" # 4
160+
... # 5
161+
"""
162+
),
163+
error_locations=((1, 10),),
164+
),
153165
}

0 commit comments

Comments
 (0)