-
Notifications
You must be signed in to change notification settings - Fork 0
/
stubdoc.py
513 lines (454 loc) · 15.5 KB
/
stubdoc.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
"""The module that implements core functions.
"""
import sys
import os
import re
import inspect
import importlib
from types import ModuleType
from typing import Any, Callable, List, Optional, Tuple
def add_docstring_to_stubfile(
original_module_path: str, stub_file_path: str) -> None:
"""
Add docstring to a specified stub file.
Notes
-----
Currently only applied top level function or top level class
methods. Not to be applied to nested function.
Parameters
----------
original_module_path : str
The path of stub file's original module.
stub_file_path : str
Target stub file path.
"""
module = _read_module(module_path=original_module_path)
stub_str: str = _read_txt(file_path=stub_file_path)
callable_names: List[str] = _get_callable_names_from_module(
module=module)
callable_names = _remove_doc_not_existing_func_from_callable_names(
callable_names=callable_names, module=module)
for callable_name in callable_names:
if '.' not in callable_name:
stub_str = _add_doctring_to_target_function(
stub_str=stub_str,
function_name=callable_name,
module=module,
)
continue
stub_str = _add_docstring_to_class_method(
stub_str=stub_str,
method_name=callable_name,
module=module,
)
if not stub_str.endswith('\n'):
stub_str += '\n'
with open(stub_file_path, 'w') as f:
f.write(stub_str)
class _ClassScopeLineRange:
_class_name: str
_stub_str: str
start_line: int
end_line: int
def __init__(self, class_name: str, stub_str: str) -> None:
"""
The class that stores specified class's scope line range
in stub string.
e.g., if that class scope is starting at line 10, then
start_line attribute will set to 10. end_line attribute is
also same.
Parameters
----------
class_name : str
Target class name that defined in stub string.
stub_str : str
Overall stub string.
Raises
------
Exception
If specified class name not found in the stub string.
"""
self._class_name = class_name
self._stub_str = stub_str
pattern = r'^class ' + class_name + r'[\(:].*$'
stub_lines: List[str] = stub_str.splitlines()
start_line: Optional[int] = None
end_line: Optional[int] = None
last_line: int = 1
for i, stub_line in enumerate(stub_lines):
last_line = i + 1
if start_line is None:
match: Optional[re.Match] = re.search(
pattern=pattern, string=stub_line)
if match is None:
continue
start_line = i + 1
continue
if stub_line == '' or stub_line == ' ':
continue
if not stub_line.startswith(' '):
end_line = i
break
if start_line is not None and end_line is None:
end_line = last_line
if start_line is None or end_line is None:
raise Exception(f'Target class name not found: {class_name}')
self.start_line = start_line
self.end_line = end_line
def _add_docstring_to_class_method(
stub_str: str, method_name: str, module: ModuleType) -> str:
"""
Add docstring to a specified class method.
Parameters
----------
stub_str : str
Target stub file's string.
method_name : str
Target method name (top-level class method only).
Class name and method name need to be concatenated by comma.
e.g. `ClassName.method_name`
module: ModuleType
Stub file's original module.
Returns
-------
result_stub_str : str
Stub file's string after docstring added.
"""
class_name: str = method_name.split('.')[0]
method_name = method_name.split('.')[1]
line_range: _ClassScopeLineRange = _ClassScopeLineRange(
class_name=class_name, stub_str=stub_str)
stub_lines: List[str] = stub_str.splitlines()
result_stub_str: str = ''
pattern = re.compile(pattern=r' def ' + method_name + r'\(.+$')
for i, stub_line in enumerate(stub_lines):
if result_stub_str != '':
result_stub_str += '\n'
line_num: int = i + 1
if line_num < line_range.start_line or line_range.end_line < line_num:
result_stub_str += stub_line
continue
match: Optional[re.Match] = pattern.search(string=stub_line)
if match is None:
result_stub_str += stub_line
continue
docstring: str = _get_docstring_from_top_level_class_method(
class_name=class_name,
method_name=method_name,
module=module,
)
stub_line = _remove_line_end_ellipsis_or_pass_keyword(line=stub_line)
stub_line = _add_docstring_to_top_level_class_method(
line=stub_line, docstring=docstring)
result_stub_str += stub_line
return result_stub_str
def _add_docstring_to_top_level_class_method(
line: str, docstring: str) -> str:
"""
Add docstring to the line string of top-level class's method.
Parameters
----------
line : str
Target class's method line string.
e.g., ` def sample_method(self) -> None:`
docstring : str
A doctring to add.
Returns
-------
line : str
Docstring added line str.
"""
eight_tabs: str = ' '
line += f'\n{eight_tabs}"""'
docstring_lines: List[str] = docstring.splitlines()
for docstring_line in docstring_lines:
if docstring_line == '':
line += '\n'
continue
if not docstring_line.startswith(eight_tabs):
docstring_line = f'{eight_tabs}{docstring_line}'
line += f'\n{docstring_line}'
line = line.rstrip()
line += f'\n{eight_tabs}"""'
return line
def _get_docstring_from_top_level_class_method(
class_name: str, method_name: str, module: ModuleType) -> str:
"""
Get docstring from method of top-level class.
Parameters
----------
class_name : str
Target class name.
method_name : str
Target class's method name.
module : ModuleType
Stub file's original module.
Returns
-------
docstring : str
Class method's docstring.
"""
members: List[Tuple[str, Any]] = inspect.getmembers(
module, predicate=inspect.isclass)
target_class: Optional[type] = None
for member_name, member_val in members:
if member_name != class_name:
continue
target_class = member_val
members = inspect.getmembers(target_class)
for member_name, member_val in members:
if member_name != method_name:
continue
target_method: Callable = member_val
if target_method.__doc__ is None:
return ''
docstring: str = target_method.__doc__
docstring = docstring.strip()
return docstring
return ''
def _remove_doc_not_existing_func_from_callable_names(
callable_names: List[str], module: ModuleType) -> List[str]:
"""
Remove top-level function that docstring not existing from callable
names list.
Parameters
----------
callable_names : list of str
Callable names list to check.
module : ModuleType
The module that specified callables are defined.
Returns
-------
remove_callable_names : list of str
The list that removed docstring not existing functions.
"""
remove_callable_names: List[str] = []
for callable_name in callable_names:
if '.' in callable_name:
remove_callable_names.append(callable_name)
continue
docstring: str = _get_docstring_from_top_level_func(
function_name=callable_name,
module=module)
if docstring == '':
continue
remove_callable_names.append(callable_name)
return remove_callable_names
def _add_doctring_to_target_function(
stub_str: str, function_name: str,
module: ModuleType) -> str:
"""
Add doctring to a specified function.
Parameters
----------
stub_str : str
Target stub file's string.
function_name : str
Target function name (top-level function only).
module: ModuleType
Stub file's original module.
Returns
-------
result_stub_str : str
Stub file's string after docstring added.
"""
result_stub_str: str = ''
lines: List[str] = stub_str.splitlines()
pattern = re.compile(pattern=r'^def ' + function_name + r'\(.+$')
for line in lines:
if result_stub_str != '':
result_stub_str += '\n'
match: Optional[re.Match] = pattern.search(string=line)
if match is None:
result_stub_str += line
continue
docstring: str = _get_docstring_from_top_level_func(
function_name=function_name,
module=module,
)
line = _remove_line_end_ellipsis_or_pass_keyword(line=line)
line = _add_docstring_to_top_level_func(
line=line, docstring=docstring)
result_stub_str += line
return result_stub_str
def _add_docstring_to_top_level_func(line: str, docstring: str) -> str:
"""
Add docstring to the top-level function line string.
Parameters
----------
line : str
Target function line string.
e.g., `def sample_func(a: int) -> None:`
docstring : str
A doctring to add.
Returns
-------
line : str
Docstring added line str.
"""
line += '\n """'
docstring_lines: List[str] = docstring.splitlines()
for docstring_line in docstring_lines:
if docstring_line == '':
line += '\n'
continue
if not docstring_line.startswith(' '):
docstring_line = f' {docstring_line}'
line += f'\n{docstring_line}'
line += '\n """'
return line
def _remove_line_end_ellipsis_or_pass_keyword(line: str) -> str:
"""
Remove ellipsis or pass keyword from end of line
(e.g., `def sample_func(): ...` or `def sample_func(): pass`).
Parameters
----------
line : str
Target line string.
Returns
-------
result_line : str
Line string that removed ellipsis or pass keyword string.
"""
if line.endswith(' ...'):
line = re.sub(pattern=r' ...$', repl='', string=line)
return line
if line.endswith(' pass'):
line = re.sub(pattern=r' pass$', repl='', string=line)
return line
return line
def _get_docstring_from_top_level_func(
function_name: str, module: ModuleType) -> str:
"""
Get docstring of the specified top-level function name.
Parameters
----------
function_name : str
Target function name.
module : ModuleType
Target module that specified function exists.
Returns
-------
docstring : str
Specified function's docstring.
"""
members: List[Tuple[str, Any]] = inspect.getmembers(module)
for member_name, member_val in members:
if member_name != function_name:
continue
target_function: Callable = member_val
if target_function.__doc__ is None:
return ''
docstring : str = target_function.__doc__
docstring = docstring.strip()
return docstring
return ''
def _read_module(module_path: str) -> ModuleType:
"""
Read specified path's module.
Parameters
----------
module_path : str
Target module path to read.
Returns
-------
module : ModuleType
Read module.
"""
file_name: str = os.path.basename(module_path)
dir_path: str = module_path.replace(file_name, '', 1)
sys.path.append(dir_path)
package_name: str = ''
all_suffixes: List[str] = importlib.machinery.all_suffixes() # type: ignore
for ending in all_suffixes:
if module_path.endswith(ending):
package_name = module_path[:-len(ending)]
break
package_name = package_name.replace('/', '.')
package_name = package_name.replace('\\', '.')
while package_name.startswith('.'):
package_name = package_name.replace('.', '', 1)
try:
module: ModuleType = importlib.import_module(package_name)
except Exception:
raise Exception(
'Specified module import failed. Please check specified path'
' is not a upper level directory or root directory (need to be'
f' able to import by package path style): {package_name}')
return module
def _get_callable_names_from_module(module: ModuleType) -> List[str]:
"""
Get callable names defined in specified module.
Parameters
----------
module : ModuleType
Target module.
Returns
-------
callable_names : list of str
Result callable names in module str.
If class method exists, name will be concatenated by comma.
e.g., `_read_txt`, `SampleClass._read_text`.
Nested function will not be included.
"""
callable_names: List[str] = []
members: List[Tuple[str, Any]] = inspect.getmembers(module)
for member_name, member_val in members:
if not hasattr(member_val, '__module__'):
continue
if member_val.__module__ != module.__name__:
continue
if inspect.isfunction(member_val):
callable_names.append(member_name)
continue
if inspect.isclass(member_val):
_append_class_callable_names_to_list(
callable_names= callable_names,
class_name=member_name,
class_val=member_val)
continue
return callable_names
def _append_class_callable_names_to_list(
callable_names: List[str], class_name: str,
class_val: type) -> None:
"""
Append class's member method names to list.
Name will be added as following format:
<class_name>.<method_name>
Parameters
----------
callable_names : list of str
The list that append names to.
class_name : str
Target Class name.
class_val : type
Target class.
"""
members: List[Tuple[str, Any]] = inspect.getmembers(
class_val,
)
for member_name, member_val in members:
if (not isinstance(member_val, Callable)
and not isinstance(member_val, property)):
continue
if (member_name.startswith('__') and member_name != '__init__'):
continue
if inspect.isclass(member_val):
continue
name: str = f'{class_name}.{member_name}'
callable_names.append(name)
def _read_txt(file_path: str) -> str:
"""
Read specified file path's text.
Parameters
----------
file_path : str
Target file path to read.
Returns
-------
txt : str
Read txt.
"""
with open(file_path) as f:
txt: str = f.read()
return txt