Skip to content

Commit 02f4358

Browse files
committed
Fixing an issue to enable extraction of hashtag comments from the end of multiline assign statements
1 parent b78111e commit 02f4358

File tree

2 files changed

+49
-11
lines changed

2 files changed

+49
-11
lines changed

src/tap/utils.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,12 @@ def source_line_to_tokens(tokens: Iterable[tokenize.TokenInfo]) -> Dict[int, Lis
222222
return line_to_tokens
223223

224224

225-
def get_subsequent_assign_lines(source_cls: str) -> Set[int]:
226-
"""For all multiline assign statements, get the line numbers after the first line in the assignment."""
225+
def get_subsequent_assign_lines(source_cls: str) -> Tuple[Set[int], Set[int]]:
226+
"""For all multiline assign statements, get the line numbers after the first line in the assignment.
227+
228+
:param source_cls: The source code of the class.
229+
:return: A set of intermediate line numbers for multiline assign statements and a set of final line numbers.
230+
"""
227231
# Parse source code using ast (with an if statement to avoid indentation errors)
228232
source = f"if True:\n{textwrap.indent(source_cls, ' ')}"
229233
body = ast.parse(source).body[0]
@@ -236,37 +240,43 @@ def get_subsequent_assign_lines(source_cls: str) -> Set[int]:
236240
# Check for correct parsing
237241
if not isinstance(body, ast.If):
238242
warnings.warn(parse_warning)
239-
return set()
243+
return set(), set()
240244

241245
# Extract if body
242246
if_body = body.body
243247

244248
# Check for a single body
245249
if len(if_body) != 1:
246250
warnings.warn(parse_warning)
247-
return set()
251+
return set(), set()
248252

249253
# Extract class body
250254
cls_body = if_body[0]
251255

252256
# Check for a single class definition
253257
if not isinstance(cls_body, ast.ClassDef):
254258
warnings.warn(parse_warning)
255-
return set()
259+
return set(), set()
256260

257261
# Get line numbers of assign statements
258-
assign_lines = set()
262+
intermediate_assign_lines = set()
263+
final_assign_lines = set()
259264
for node in cls_body.body:
260265
if isinstance(node, (ast.Assign, ast.AnnAssign)):
261266
# Check if the end line number is found
262267
if node.end_lineno is None:
263268
warnings.warn(parse_warning)
264269
continue
265270

266-
# Get line number of assign statement excluding the first line (and minus 1 for the if statement)
267-
assign_lines |= set(range(node.lineno, node.end_lineno))
271+
# Only consider multiline assign statements
272+
if node.end_lineno > node.lineno:
273+
# Get intermediate line number of assign statement excluding the first line (and minus 1 for the if statement)
274+
intermediate_assign_lines |= set(range(node.lineno, node.end_lineno - 1))
275+
276+
# If multiline assign statement, get the line number of the last line (and minus 1 for the if statement)
277+
final_assign_lines.add(node.end_lineno - 1)
268278

269-
return assign_lines
279+
return intermediate_assign_lines, final_assign_lines
270280

271281

272282
def get_class_variables(cls: type) -> Dict[str, Dict[str, str]]:
@@ -283,14 +293,25 @@ def get_class_variables(cls: type) -> Dict[str, Dict[str, str]]:
283293

284294
# For all multiline assign statements, get the line numbers after the first line of the assignment
285295
# This is used to avoid identifying comments in multiline assign statements
286-
subsequent_assign_lines = get_subsequent_assign_lines(source_cls)
296+
intermediate_assign_lines, final_assign_lines = get_subsequent_assign_lines(source_cls)
287297

288298
# Extract class variables
289299
class_variable = None
290300
variable_to_comment = {}
291301
for line, tokens in line_to_tokens.items():
302+
# If this is the final line of a multiline assign, extract any potential comments
303+
if line in final_assign_lines:
304+
# Find the comment (if it exists)
305+
for token in tokens:
306+
print(token)
307+
if token["token_type"] == tokenize.COMMENT:
308+
# Leave out "#" and whitespace from comment
309+
variable_to_comment[class_variable]["comment"] = token["token"][1:].strip()
310+
break
311+
continue
312+
292313
# Skip assign lines after the first line of multiline assign statements
293-
if line in subsequent_assign_lines:
314+
if line in intermediate_assign_lines:
294315
continue
295316

296317
for i, token in enumerate(tokens):

tests/test_utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,23 @@ class MultilineArgument:
332332
class_variables = {"bar": {"comment": "biz baz"}}
333333
self.assertEqual(get_class_variables(MultilineArgument), class_variables)
334334

335+
def test_multiline_argument_with_final_hashtag_comment(self):
336+
class MultilineArgument:
337+
bar: str = (
338+
"This is a multiline argument"
339+
" that should not be included in the docstring"
340+
) # biz baz
341+
barr: str = (
342+
"This is a multiline argument"
343+
" that should not be included in the docstring") # bar baz
344+
barrr: str = ( # meow
345+
"This is a multiline argument" # blah
346+
" that should not be included in the docstring" # grrrr
347+
) # yay!
348+
349+
class_variables = {"bar": {"comment": "biz baz"}, "barr": {"comment": "bar baz"}, "barrr": {"comment": "yay!"}}
350+
self.assertEqual(get_class_variables(MultilineArgument), class_variables)
351+
335352
def test_single_quote_multiline(self):
336353
class SingleQuoteMultiline:
337354
bar: int = 0

0 commit comments

Comments
 (0)