1
1
from argparse import ArgumentParser , ArgumentTypeError
2
+ import ast
2
3
from base64 import b64encode , b64decode
3
4
import copy
4
5
from functools import wraps
10
11
import re
11
12
import subprocess
12
13
import sys
14
+ import textwrap
13
15
import tokenize
14
16
from typing import (
15
17
Any ,
24
26
Union ,
25
27
)
26
28
from typing_inspect import get_args as typing_inspect_get_args , get_origin as typing_inspect_get_origin
29
+ import warnings
27
30
28
31
if sys .version_info >= (3 , 10 ):
29
32
from types import UnionType
@@ -216,6 +219,52 @@ def source_line_to_tokens(obj: object) -> Dict[int, List[Dict[str, Union[str, in
216
219
return line_to_tokens
217
220
218
221
222
+ def get_subsequent_assign_lines (cls : type ) -> set [int ]:
223
+ """For all multiline assign statements, get the line numbers after the first line of the assignment."""
224
+ # Get source code of class
225
+ source = inspect .getsource (cls )
226
+
227
+ # Parse source code using ast (with an if statement to avoid indentation errors)
228
+ source = f"if True:\n { textwrap .indent (source , ' ' )} "
229
+ body = ast .parse (source ).body [0 ]
230
+
231
+ # Set up warning message
232
+ parse_warning = (
233
+ "Could not parse class source code to extract comments. "
234
+ "Comments in the help string may be incorrect."
235
+ )
236
+
237
+ # Check for correct parsing
238
+ if not isinstance (body , ast .If ):
239
+ warnings .warn (parse_warning )
240
+ return set ()
241
+
242
+ # Extract if body
243
+ if_body = body .body
244
+
245
+ # Check for a single body
246
+ if len (if_body ) != 1 :
247
+ warnings .warn (parse_warning )
248
+ return set ()
249
+
250
+ # Extract class body
251
+ cls_body = if_body [0 ]
252
+
253
+ # Check for a single class definition
254
+ if not isinstance (cls_body , ast .ClassDef ):
255
+ warnings .warn (parse_warning )
256
+ return set ()
257
+
258
+ # Get line numbers of assign statements
259
+ assign_lines = set ()
260
+ for node in cls_body .body :
261
+ if isinstance (node , (ast .Assign , ast .AnnAssign )):
262
+ # Get line number of assign statement excluding the first line (and minus 1 for the if statement)
263
+ assign_lines |= set (range (node .lineno , node .end_lineno ))
264
+
265
+ return assign_lines
266
+
267
+
219
268
def get_class_variables (cls : type ) -> Dict [str , Dict [str , str ]]:
220
269
"""Returns a dictionary mapping class variables to their additional information (currently just comments)."""
221
270
# Get mapping from line number to tokens
@@ -224,12 +273,19 @@ def get_class_variables(cls: type) -> Dict[str, Dict[str, str]]:
224
273
# Get class variable column number
225
274
class_variable_column = get_class_column (cls )
226
275
276
+ # For all multiline assign statements, get the line numbers after the first line of the assignment
277
+ # This is used to avoid identifying comments in multiline assign statements
278
+ subsequent_assign_lines = get_subsequent_assign_lines (cls )
279
+
227
280
# Extract class variables
228
281
class_variable = None
229
282
variable_to_comment = {}
230
- for tokens in line_to_tokens .values ():
231
- for i , token in enumerate (tokens ):
283
+ for line , tokens in line_to_tokens .items ():
284
+ # Skip assign lines after the first line of multiline assign statements
285
+ if line in subsequent_assign_lines :
286
+ continue
232
287
288
+ for i , token in enumerate (tokens ):
233
289
# Skip whitespace
234
290
if token ["token" ].strip () == "" :
235
291
continue
0 commit comments