micropython-stubber 1.20.5__py3-none-any.whl → 1.23.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. {micropython_stubber-1.20.5.dist-info → micropython_stubber-1.23.0.dist-info}/LICENSE +30 -30
  2. {micropython_stubber-1.20.5.dist-info → micropython_stubber-1.23.0.dist-info}/METADATA +1 -1
  3. micropython_stubber-1.23.0.dist-info/RECORD +159 -0
  4. mpflash/README.md +184 -184
  5. mpflash/libusb_flash.ipynb +203 -203
  6. mpflash/mpflash/add_firmware.py +98 -98
  7. mpflash/mpflash/ask_input.py +236 -236
  8. mpflash/mpflash/bootloader/__init__.py +37 -36
  9. mpflash/mpflash/bootloader/manual.py +102 -102
  10. mpflash/mpflash/bootloader/micropython.py +10 -10
  11. mpflash/mpflash/bootloader/touch1200.py +45 -45
  12. mpflash/mpflash/cli_download.py +129 -129
  13. mpflash/mpflash/cli_flash.py +219 -219
  14. mpflash/mpflash/cli_group.py +98 -98
  15. mpflash/mpflash/cli_list.py +81 -81
  16. mpflash/mpflash/cli_main.py +41 -41
  17. mpflash/mpflash/common.py +164 -164
  18. mpflash/mpflash/config.py +43 -47
  19. mpflash/mpflash/connected.py +74 -74
  20. mpflash/mpflash/download.py +360 -360
  21. mpflash/mpflash/downloaded.py +130 -129
  22. mpflash/mpflash/errors.py +9 -9
  23. mpflash/mpflash/flash.py +55 -52
  24. mpflash/mpflash/flash_esp.py +59 -59
  25. mpflash/mpflash/flash_stm32.py +18 -24
  26. mpflash/mpflash/flash_stm32_cube.py +111 -111
  27. mpflash/mpflash/flash_stm32_dfu.py +104 -101
  28. mpflash/mpflash/flash_uf2.py +89 -67
  29. mpflash/mpflash/flash_uf2_boardid.py +15 -15
  30. mpflash/mpflash/flash_uf2_linux.py +129 -123
  31. mpflash/mpflash/flash_uf2_macos.py +37 -34
  32. mpflash/mpflash/flash_uf2_windows.py +38 -34
  33. mpflash/mpflash/list.py +89 -89
  34. mpflash/mpflash/logger.py +41 -41
  35. mpflash/mpflash/mpboard_id/__init__.py +93 -93
  36. mpflash/mpflash/mpboard_id/add_boards.py +255 -255
  37. mpflash/mpflash/mpboard_id/board.py +37 -37
  38. mpflash/mpflash/mpboard_id/board_id.py +86 -86
  39. mpflash/mpflash/mpboard_id/store.py +43 -43
  40. mpflash/mpflash/mpremoteboard/__init__.py +226 -221
  41. mpflash/mpflash/mpremoteboard/mpy_fw_info.py +141 -141
  42. mpflash/mpflash/mpremoteboard/runner.py +140 -140
  43. mpflash/mpflash/uf2disk.py +12 -12
  44. mpflash/mpflash/vendor/basicgit.py +288 -288
  45. mpflash/mpflash/vendor/click_aliases.py +91 -91
  46. mpflash/mpflash/vendor/dfu.py +165 -165
  47. mpflash/mpflash/vendor/pydfu.py +605 -605
  48. mpflash/mpflash/vendor/readme.md +2 -2
  49. mpflash/mpflash/vendor/versions.py +119 -117
  50. mpflash/mpflash/worklist.py +171 -170
  51. mpflash/poetry.lock +1588 -1588
  52. mpflash/pyproject.toml +64 -60
  53. mpflash/stm32_udev_rules.md +62 -62
  54. stubber/__init__.py +3 -3
  55. stubber/basicgit.py +294 -288
  56. stubber/board/board_info.csv +193 -193
  57. stubber/board/boot.py +34 -34
  58. stubber/board/createstubs.py +986 -986
  59. stubber/board/createstubs_db.py +825 -825
  60. stubber/board/createstubs_db_min.py +331 -331
  61. stubber/board/createstubs_db_mpy.mpy +0 -0
  62. stubber/board/createstubs_lvgl.py +741 -741
  63. stubber/board/createstubs_lvgl_min.py +741 -741
  64. stubber/board/createstubs_mem.py +766 -766
  65. stubber/board/createstubs_mem_min.py +306 -306
  66. stubber/board/createstubs_mem_mpy.mpy +0 -0
  67. stubber/board/createstubs_min.py +294 -294
  68. stubber/board/createstubs_mpy.mpy +0 -0
  69. stubber/board/fw_info.py +141 -141
  70. stubber/board/info.py +183 -183
  71. stubber/board/main.py +19 -19
  72. stubber/board/modulelist.txt +247 -247
  73. stubber/board/pyrightconfig.json +34 -34
  74. stubber/bulk/mcu_stubber.py +454 -454
  75. stubber/codemod/_partials/__init__.py +48 -48
  76. stubber/codemod/_partials/db_main.py +147 -147
  77. stubber/codemod/_partials/lvgl_main.py +77 -77
  78. stubber/codemod/_partials/modules_reader.py +80 -80
  79. stubber/codemod/add_comment.py +53 -53
  80. stubber/codemod/add_method.py +65 -65
  81. stubber/codemod/board.py +317 -317
  82. stubber/codemod/enrich.py +145 -145
  83. stubber/codemod/merge_docstub.py +284 -284
  84. stubber/codemod/modify_list.py +54 -54
  85. stubber/codemod/utils.py +57 -57
  86. stubber/commands/build_cmd.py +94 -94
  87. stubber/commands/cli.py +55 -51
  88. stubber/commands/clone_cmd.py +77 -66
  89. stubber/commands/config_cmd.py +29 -29
  90. stubber/commands/enrich_folder_cmd.py +71 -70
  91. stubber/commands/get_core_cmd.py +71 -69
  92. stubber/commands/get_docstubs_cmd.py +89 -87
  93. stubber/commands/get_frozen_cmd.py +114 -112
  94. stubber/commands/get_mcu_cmd.py +61 -56
  95. stubber/commands/merge_cmd.py +67 -66
  96. stubber/commands/publish_cmd.py +119 -119
  97. stubber/commands/stub_cmd.py +31 -30
  98. stubber/commands/switch_cmd.py +62 -54
  99. stubber/commands/variants_cmd.py +49 -48
  100. stubber/cst_transformer.py +178 -178
  101. stubber/data/board_info.csv +193 -193
  102. stubber/data/board_info.json +1729 -1729
  103. stubber/data/micropython_tags.csv +15 -15
  104. stubber/data/requirements-core-micropython.txt +38 -38
  105. stubber/data/requirements-core-pycopy.txt +39 -39
  106. stubber/downloader.py +36 -36
  107. stubber/freeze/common.py +68 -68
  108. stubber/freeze/freeze_folder.py +69 -69
  109. stubber/freeze/freeze_manifest_2.py +113 -113
  110. stubber/freeze/get_frozen.py +127 -127
  111. stubber/get_cpython.py +101 -101
  112. stubber/get_lobo.py +59 -59
  113. stubber/minify.py +418 -418
  114. stubber/publish/bump.py +86 -86
  115. stubber/publish/candidates.py +262 -262
  116. stubber/publish/database.py +18 -18
  117. stubber/publish/defaults.py +45 -45
  118. stubber/publish/enums.py +24 -24
  119. stubber/publish/helpers.py +29 -29
  120. stubber/publish/merge_docstubs.py +130 -130
  121. stubber/publish/missing_class_methods.py +49 -49
  122. stubber/publish/package.py +146 -146
  123. stubber/publish/pathnames.py +51 -51
  124. stubber/publish/publish.py +120 -120
  125. stubber/publish/pypi.py +38 -38
  126. stubber/publish/stubpackage.py +1029 -1029
  127. stubber/rst/__init__.py +9 -9
  128. stubber/rst/classsort.py +77 -77
  129. stubber/rst/lookup.py +530 -530
  130. stubber/rst/output_dict.py +401 -401
  131. stubber/rst/reader.py +822 -822
  132. stubber/rst/report_return.py +69 -69
  133. stubber/rst/rst_utils.py +540 -540
  134. stubber/stubber.py +38 -38
  135. stubber/stubs_from_docs.py +90 -90
  136. stubber/tools/manifestfile.py +655 -610
  137. stubber/tools/readme.md +7 -6
  138. stubber/update_fallback.py +117 -117
  139. stubber/update_module_list.py +123 -123
  140. stubber/utils/__init__.py +5 -5
  141. stubber/utils/config.py +127 -127
  142. stubber/utils/makeversionhdr.py +54 -54
  143. stubber/utils/manifest.py +92 -92
  144. stubber/utils/post.py +79 -79
  145. stubber/utils/repos.py +157 -154
  146. stubber/utils/stubmaker.py +139 -139
  147. stubber/utils/typed_config_toml.py +77 -77
  148. stubber/utils/versions.py +128 -120
  149. stubber/variants.py +106 -106
  150. micropython_stubber-1.20.5.dist-info/RECORD +0 -159
  151. {micropython_stubber-1.20.5.dist-info → micropython_stubber-1.23.0.dist-info}/WHEEL +0 -0
  152. {micropython_stubber-1.20.5.dist-info → micropython_stubber-1.23.0.dist-info}/entry_points.txt +0 -0
stubber/rst/reader.py CHANGED
@@ -1,822 +1,822 @@
1
- """
2
- Read the Micropython library documentation files and use them to build stubs that can be used for static typechecking
3
- using a custom-built parser to read and process the micropython RST files
4
- - generates:
5
- - modules
6
- - docstrings
7
- - function definitions
8
- - function parameters based on documentation
9
- - docstrings
10
- - classes
11
- - docstrings
12
- - __init__ method
13
- - parameters based on documentation for class
14
- - methods
15
- - parameters based on documentation for the method
16
- - docstrings
17
-
18
- - exceptions
19
-
20
- - Tries to determine the return type by parsing the docstring.
21
- - Imperative verbs used in docstrings have a strong correlation to return -> None
22
- - recognizes documented Generators, Iterators, Callable
23
- - Coroutines are identified based tag "This is a Coroutine". Then if the return type was Foo, it will be transformed to : Coroutine[Foo]
24
- - a static Lookup list is used for a few methods/functions for which the return type cannot be determined from the docstring.
25
- - add NoReturn to a few functions that never return ( stop / deepsleep / reset )
26
- - if no type can be detected the type `Any` or `Incomplete` is used
27
-
28
- The generated stub files are formatted using `black` and checked for validity using `pyright`
29
- Note: black on python 3.7 does not like some function defs
30
- `def sizeof(struct, layout_type=NATIVE, /) -> int:`
31
-
32
- - ordering of inter-dependent classes in the same module
33
-
34
- - Literals / constants
35
- - documentation contains repeated vars with the same indentation
36
- - Module level:
37
- .. code-block::
38
-
39
- .. data:: IPPROTO_UDP
40
- IPPROTO_TCP
41
-
42
- - class level:
43
- .. code-block::
44
-
45
- .. data:: Pin.IRQ_FALLING
46
- Pin.IRQ_RISING
47
- Pin.IRQ_LOW_LEVEL
48
- Pin.IRQ_HIGH_LEVEL
49
-
50
- Selects the IRQ trigger type.
51
-
52
- - literals documented using a wildcard are added as comments only
53
-
54
- - Add GLUE imports to allow specific modules to import specific others.
55
-
56
- - Repeats of definitions in the rst file for similar functions or literals
57
- - CONSTANTS ( module and Class level )
58
- - functions
59
- - methods
60
-
61
- - Child/ Parent classes
62
- are added based on a (manual) lookup table CHILD_PARENT_CLASS
63
-
64
- """
65
-
66
- import re
67
- from pathlib import Path
68
- from typing import List, Optional, Tuple
69
-
70
- from loguru import logger as log
71
-
72
- from stubber.rst import (
73
- CHILD_PARENT_CLASS,
74
- MODULE_GLUE,
75
- PARAM_FIXES,
76
- RST_DOC_FIXES,
77
- TYPING_IMPORT,
78
- ClassSourceDict,
79
- FunctionSourceDict,
80
- ModuleSourceDict,
81
- return_type_from_context,
82
- )
83
- from stubber.rst.lookup import Fix
84
- from stubber.utils.config import CONFIG
85
- from stubber.utils.versions import V_PREVIEW
86
-
87
- SEPERATOR = "::"
88
-
89
-
90
- class FileReadWriter:
91
- """base class for reading rst files"""
92
-
93
- def __init__(self):
94
- self.filename = ""
95
- # input buffer
96
- self.rst_text: List[str] = []
97
- self.max_line = 0
98
- self.line_no: int = 0 # current Linenumber used during parsing.
99
- self.last_line = ""
100
-
101
- # Output buffer
102
- self.output: List[str] = []
103
-
104
- def read_file(self, filename: Path):
105
- log.trace(f"Reading : {filename}")
106
- # ignore Unicode decoding issues
107
- with open(filename, errors="ignore", encoding="utf8") as file:
108
- self.rst_text = file.readlines()
109
- # Replace incorrect definitions in .rst files with better ones
110
- for FIX in RST_DOC_FIXES:
111
- self.rst_text = [line.replace(FIX[0], FIX[1]) for line in self.rst_text]
112
- # some lines now may have \n sin them , so re-join and re-split the lines
113
- self.rst_text = "".join(self.rst_text).splitlines(keepends=True)
114
-
115
- self.filename = filename.as_posix() # use fwd slashes in origin
116
- self.max_line = len(self.rst_text) - 1
117
-
118
- def write_file(self, filename: Path) -> bool:
119
- try:
120
- log.info(f" - Writing to: {filename}")
121
- with open(filename, mode="w", encoding="utf8") as file:
122
- file.writelines(self.output)
123
- except OSError as e:
124
- log.error(e)
125
- return False
126
- return True
127
-
128
- @property
129
- def line(self) -> str:
130
- "get the current line from input, also stores this as last_line to allow for inspection and dumping the json file"
131
- if self.line_no >= 0 and self.line_no <= self.max_line:
132
- self.last_line = self.rst_text[self.line_no]
133
- else:
134
- self.last_line = ""
135
- return self.last_line
136
-
137
- @staticmethod
138
- def is_balanced(s: str) -> bool:
139
- """
140
- Check if a string has balanced parentheses
141
- """
142
- return False if s.count("(") != s.count(")") else s.count("{") == s.count("}")
143
-
144
- def extend_and_balance_line(self) -> str:
145
- """
146
- Append the current line + next line in order to try to balance the parentheses
147
- in order to do this the rst_test array is changed by the function
148
- and max_line is adjusted
149
- """
150
- append = 0
151
- newline = self.rst_text[self.line_no]
152
- while not self.is_balanced(newline) and self.line_no >= 0 and (self.line_no + append + 1) <= self.max_line:
153
- append += 1
154
- # concat the lines
155
- newline += self.rst_text[self.line_no + append]
156
- # only update line if things balanced out correctly
157
- if self.is_balanced(newline):
158
- self.rst_text[self.line_no] = newline
159
- for _ in range(append):
160
- self.rst_text.pop(self.line_no + 1)
161
- self.max_line -= 1
162
- # reprocess line
163
- return self.line
164
-
165
-
166
- class RSTReader(FileReadWriter):
167
- docstring_anchors = [
168
- ".. note::",
169
- ".. data:: Arguments:",
170
- ".. data:: Options:",
171
- ".. data:: Returns:",
172
- ".. data:: Raises:",
173
- ".. admonition::",
174
- ]
175
- # considered part of the docstrings
176
-
177
- def __init__(self):
178
- self.current_module = ""
179
- self.current_class = ""
180
- self.current_function = "" # function & method
181
- super().__init__()
182
-
183
- def read_file(self, filename: Path):
184
- super().read_file(filename)
185
- self.current_module = filename.stem # just to be sure
186
-
187
- @property
188
- def module_names(self) -> List[str]:
189
- "list of possible module names [uname , name] (longest first)"
190
- namelist: List[str] = []
191
- if self.current_module == "":
192
- return namelist
193
- # deal with module names "esp and esp.socket"
194
- if "." in self.current_module:
195
- names = [self.current_module, self.current_module.split(".")[0]]
196
- else:
197
- names = [self.current_module]
198
- # process
199
- for c_mod in names:
200
- if self.current_module[0] != "u":
201
- namelist += [f"u{c_mod}", c_mod]
202
- else:
203
- namelist += [c_mod, c_mod[1:]]
204
- return namelist
205
-
206
- @property
207
- def at_anchor(self) -> bool:
208
- "Stop at anchor '..something' ( however .. note: and ..data:: should be added)"
209
- line = self.rst_text[self.line_no].lstrip()
210
- # anchors that are considered part of the docstring
211
- # Check if the line starts with '..' but not any of the docstring_anchors.
212
- if line.startswith(".."):
213
- return not any(line.startswith(anchor) for anchor in self.docstring_anchors)
214
- return False
215
-
216
- # return _l.startswith("..") and not any(_l.startswith(a) for a in self.docstring_anchors)
217
-
218
- # @property
219
- def at_heading(self, large=False) -> bool:
220
- "stop at heading"
221
- u_line = self.rst_text[min(self.line_no + 1, self.max_line - 1)].rstrip()
222
- # Heading ---, ==, ~~~
223
- underlined = u_line.startswith("---") or u_line.startswith("===") or u_line.startswith("~~~")
224
- if underlined and self.line_no > 0:
225
- # check if previous line is a heading
226
- line = self.rst_text[self.line_no].strip()
227
- if line:
228
- # module docstrings can be a bit larger than normal
229
- if not large and len(line) == len(u_line):
230
- # heading is same length as underlined
231
- # for most docstrings that is a sensible boundary
232
- return True
233
- line = line.split()[0]
234
- # stopwords in headings
235
- return line.lower() in [
236
- "classes",
237
- "functions",
238
- "methods",
239
- "constants",
240
- "exceptions",
241
- "constructors",
242
- "class",
243
- "common",
244
- "general",
245
- # below are tuning based on module level docstrings
246
- "time",
247
- "pio",
248
- "memory",
249
- ]
250
- return False
251
-
252
- def read_docstring(self, large: bool = False) -> List[str]:
253
- """Read a textblock that will be used as a docstring, or used to process a toc tree
254
- The textblock is terminated at the following RST line structures/tags
255
- .. <anchor>
256
- -- Heading
257
- == Heading
258
- ~~ Heading
259
-
260
- The blank lines at the start and end are removed to limit the space the docstring takes up.
261
- """
262
- if self.line_no >= len(self.rst_text):
263
- raise IndexError
264
-
265
- block: List[str] = []
266
- self.line_no += 1 # advance over current line
267
- try:
268
- while (
269
- self.line_no < len(self.rst_text)
270
- and not self.at_anchor # stop at next anchor ( however .. note: and a few other anchors should be added)
271
- and not self.at_heading(large) # stop at next heading
272
- ):
273
- line = self.rst_text[self.line_no]
274
- block.append(line.rstrip())
275
- self.line_no += 1 # advance line
276
- except IndexError:
277
- pass
278
-
279
- # remove empty lines at beginning/end of block
280
- block = self.clean_docstr(block)
281
- # add clickable hyperlinks to CPython docpages
282
- block = self.add_link_to_docsstr(block)
283
- # make sure the first char of the first line is a capital
284
- if len(block) > 0 and len(block[0]) > 0:
285
- block[0] = block[0][0].upper() + block[0][1:]
286
- return block
287
-
288
- @staticmethod
289
- def clean_docstr(block: List[str]):
290
- """Clean up a docstring"""
291
- # if a Quoted Literal Block , then remove the first character of each line
292
- # https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#quoted-literal-blocks
293
- if block and len(block[0]) > 0 and block[0][0] != " ":
294
- q_char = block[0][0]
295
- if all(l.startswith(q_char) for l in block):
296
- # all lines start with the same character, so skip that character
297
- block = [l[1:] for l in block]
298
- # rstrip all lines
299
- block = [l.rstrip() for l in block]
300
- # remove empty lines at beginning/end of block
301
- while len(block) and len(block[0]) == 0:
302
- block = block[1:]
303
- while len(block) and len(block[-1]) == 0:
304
- block = block[:-1]
305
-
306
- # Clean up Synopsis
307
- if len(block) and ":synopsis:" in block[0]:
308
- block[0] = re.sub(
309
- r"\s+:synopsis:\s+(?P<syn>[\w|\s]*)",
310
- r"\g<syn>",
311
- block[0],
312
- )
313
- return block
314
-
315
- @staticmethod
316
- def add_link_to_docsstr(block: List[str]):
317
- """Add clickable hyperlinks to CPython docpages"""
318
- for i in range(len(block)):
319
- # hyperlink to Cpython doc pages
320
- # https://regex101.com/r/5RN8rj/1
321
- # Link to python 3 documentation
322
- _l = re.sub(
323
- r"(\s*\|see_cpython_module\|\s+:mod:`python:(?P<mod>[\w|\s]*)`)[.]?",
324
- r"\g<1> https://docs.python.org/3/library/\g<mod>.html .",
325
- block[i],
326
- )
327
- # RST hyperlink format is not clickable in VSCode so convert to markdown format
328
- # https://regex101.com/r/5RN8rj/1
329
- _l = re.sub(
330
- r"(.*)(?P<url><https://docs\.python\.org/.*>)(`_)",
331
- r"\g<1>`\g<url>",
332
- _l,
333
- )
334
- # Clean up note and other docstring anchors
335
- _l = _l.replace(".. note:: ", "``Note:`` ")
336
- _l = _l.replace(".. data:: ", "")
337
- _l = _l.replace(".. admonition:: ", "")
338
- _l = _l.replace("|see_cpython_module|", "CPython module:")
339
- # clean up unsupported escape sequences in rst
340
- _l = _l.replace(r"\ ", " ")
341
- _l = _l.replace(r"\*", "*")
342
- block[i] = _l
343
- return block
344
-
345
- def get_rst_hint(self):
346
- "parse the '.. <rst hint>:: ' from the current line"
347
- m = re.search(r"\.\.\s?(\w+)\s?::\s?", self.line)
348
- return m[1] if m else ""
349
-
350
- def strip_prefixes(self, name: str, strip_mod: bool = True, strip_class: bool = False):
351
- "Remove the modulename. and or the classname. from the begining of a name"
352
- prefixes = self.module_names if strip_mod else []
353
- if strip_class and self.current_class != "":
354
- prefixes += [self.current_class]
355
- for prefix in prefixes:
356
- if len(prefix) > 1 and prefix + "." in name:
357
- name = name.replace(prefix + ".", "")
358
- return name
359
-
360
-
361
- class RSTParser(RSTReader):
362
- """
363
- Parse the RST file and create a ModuleSourceDict
364
- most methods have side effects
365
- """
366
-
367
- target = ".py" # py/pyi
368
- # TODO: Move to lookup.py
369
- PARAM_RE_FIXES = [
370
- Fix(r"\[angle, time=0\]", "[angle], time=0", is_re=True), # fix: method:: Servo.angle([angle, time=0])
371
- Fix(r"\[speed, time=0\]", "[speed], time=0", is_re=True), # fix: .. method:: Servo.speed([speed, time=0])
372
- Fix(
373
- r"\[service_id, key=None, \*, \.\.\.\]", "[service_id], [key], *, ...", is_re=True
374
- ), # fix: network - AbstractNIC.connect
375
- ]
376
-
377
- def __init__(self, v_tag: str) -> None:
378
- super().__init__()
379
- self.output_dict: ModuleSourceDict = ModuleSourceDict("")
380
- self.output_dict.add_import(TYPING_IMPORT)
381
- self.return_info: List[Tuple] = [] # development aid only
382
- self.source_tag = v_tag
383
- self.source_release = v_tag
384
-
385
- def leave_class(self):
386
- if self.current_class != "":
387
- self.current_class = ""
388
-
389
- def fix_parameters(self, params: str, name: str = "") -> str:
390
- """Patch / correct the documentation parameter notation to a supported format that works for linting.
391
- - params is the string containing the parameters as documented in the rst file
392
- - name is the name of the function or method or Class
393
- """
394
- params = params.strip()
395
- if not params.endswith(")"):
396
- # remove all after the closing bracket
397
- params = params[: params.rfind(")") + 1]
398
-
399
- # Remove modulename. and Classname. from class constant
400
- params = self.strip_prefixes(params, strip_mod=True, strip_class=True)
401
-
402
- ## Deal with SQUARE brackets first ( Documentation meaning := [optional])
403
-
404
- for fix in self.PARAM_RE_FIXES:
405
- params = self.apply_fix(fix, params, name)
406
-
407
- # ###########################################################################################################
408
- # does not look cool, but works really well
409
- # change [x] --> x:Optional[Any]
410
- params = params.replace("[", "")
411
- params = params.replace("]]", "") # Q&D Hack-complex nesting
412
-
413
- # Handle Optional arguments
414
- # Optional step 1: [x] --> x: Optional[Any]=None
415
- params = params.replace("]", ": Optional[Any]=None")
416
- # Optional step 2: x: Optional[Any]=None='abc' --> x: Optional[Any]='abc'
417
- params = re.sub(r": Optional\[Any\]=None\s*=", r": Optional[Any]=", params)
418
- # Optional step 3: fix ...
419
- params = re.sub(r"\.\.\.: Optional\[Any\]=None", r"...", params)
420
- # ###########################################################################################################
421
-
422
- for fix in PARAM_FIXES:
423
- if fix.module == self.current_module or not fix.module:
424
- params = self.apply_fix(fix, params, name)
425
-
426
- # # formatting
427
- # # fixme: ... not allowed in .py
428
- if self.target == ".py":
429
- params = params.replace("*, ...", "*args, **kwargs")
430
- params = params.replace("...", "*args, **kwargs")
431
-
432
- return params.strip()
433
-
434
- @staticmethod
435
- def apply_fix(fix: Fix, params: str, name: str = ""):
436
- if fix.name and fix.name != name:
437
- return params
438
- return re.sub(fix.from_, fix.to, params) if fix.is_re else params.replace(fix.from_, fix.to)
439
-
440
- def create_update_class(self, name: str, params: str, docstr: List[str]):
441
- # a bit of a hack: assume no classes in classes or functions in function
442
- self.leave_class()
443
- if full_name := self.output_dict.find(f"class {name}"):
444
- log.warning(f"TODO: UPDATE EXISTING CLASS : {name}")
445
- class_def = self.output_dict[full_name]
446
- else:
447
- parent = CHILD_PARENT_CLASS[name] if name in CHILD_PARENT_CLASS.keys() else ""
448
- if parent == "" and (name.endswith("Error") or name.endswith("Exception")):
449
- parent = "Exception"
450
- class_def = ClassSourceDict(
451
- f"class {name}({parent}):",
452
- docstr=docstr,
453
- )
454
- if params != "":
455
- method = FunctionSourceDict(
456
- name="__init__",
457
- indent=class_def.indent + 4,
458
- definition=[f"def __init__(self, {params} -> None:"],
459
- docstr=[], # todo: check if twice is needed
460
- )
461
- class_def += method
462
- # Append class to output
463
- self.output_dict += class_def
464
- self.current_class = name
465
-
466
- def parse_toc(self):
467
- "process table of content with additional rst files, and add / include them in the current module"
468
- log.trace(f"# {self.line.rstrip()}")
469
- self.line_no += 1 # skip one line
470
- toctree = self.read_docstring()
471
- # cleanup toctree
472
- toctree = [x.strip() for x in toctree if f"{self.current_module}." in x]
473
- # Now parse all files mentioned in the toc
474
- for file in toctree:
475
- #
476
- file_path = CONFIG.mpy_path / "docs" / "library" / file.strip()
477
- self.read_file(file_path)
478
- self.parse()
479
- # reset this file to done
480
- self.rst_text = []
481
- self.line_no = 1
482
-
483
- def parse_module(self):
484
- "parse a module tag and set the module's docstring"
485
- log.trace(f"# {self.line.rstrip()}")
486
- module_name = self.line.split(SEPERATOR)[-1].strip()
487
-
488
- self.current_module = module_name
489
- self.current_function = self.current_class = ""
490
- # get module docstring
491
- docstr = self.read_docstring(large=True)
492
-
493
- if len(docstr) > 0:
494
- # Add link to online documentation
495
- # https://docs.micropython.org/en/v1.17/library/array.html
496
- if "nightly" in self.source_tag:
497
- version = V_PREVIEW
498
- else:
499
- version = self.source_tag.replace("_", ".") # TODO Use clean_version(self.source_tag)
500
- docstr[0] = (
501
- f"{docstr[0]}.\n\nMicroPython module: https://docs.micropython.org/en/{version}/library/{module_name}.html"
502
- )
503
-
504
- self.output_dict.name = module_name
505
- self.output_dict.add_comment(f"# source version: {self.source_tag}")
506
- self.output_dict.add_comment(f"# origin module:: {self.filename}")
507
- self.output_dict.add_docstr(docstr)
508
- # Add additional imports to allow one module te refer to another
509
- if module_name in MODULE_GLUE.keys():
510
- self.output_dict.add_import(MODULE_GLUE[module_name])
511
-
512
- def parse_current_module(self):
513
- log.trace(f"# {self.line.rstrip()}")
514
- module_name = self.line.split(SEPERATOR)[-1].strip()
515
- mod_comment = f"# + module: {self.current_module}.rst"
516
- self.current_module = module_name
517
- self.current_function = self.current_class = ""
518
- log.debug(mod_comment)
519
- self.output_dict.name = module_name
520
- self.output_dict.add_comment(mod_comment)
521
- self.line_no += 1 # advance as we did not read any docstring
522
-
523
- def parse_function(self):
524
- log.trace(f"# {self.line.rstrip()}")
525
- # this_function = self.line.split(SEPERATOR)[-1].strip()
526
- # name = this_function
527
-
528
- # Get one or more names
529
- function_names = self.parse_names(oneliner=False)
530
- docstr = self.read_docstring()
531
-
532
- for this_function in function_names:
533
- # Parse return type from docstring
534
- ret_type = return_type_from_context(docstring=docstr, signature=this_function, module=self.current_module)
535
-
536
- # defaults
537
- name = params = ""
538
- try:
539
- name, params = this_function.split("(", maxsplit=1)
540
- except ValueError:
541
- log.error(this_function)
542
- self.current_function = name
543
- if name in ("classmethod", "staticmethod"):
544
- # skip the classmethod and static method functions
545
- # no use to create stubs for these
546
- return
547
-
548
- # ussl documentation uses a ssl.foobar prefix
549
- for mod in self.module_names:
550
- if name.startswith(f"{mod}."):
551
- # remove module name from the start of the function name
552
- name = name[len(f"{mod}.") :]
553
- # fixup parameters
554
- params = self.fix_parameters(params, name)
555
- # ASSUME no functions in classes,
556
- # so with ther cursor at a function this probably means that we are no longer in a class
557
- self.leave_class()
558
-
559
- fn_def = FunctionSourceDict(
560
- name=f"def {name}",
561
- definition=[f"def {name}({params} -> {ret_type}:"],
562
- docstr=docstr,
563
- )
564
- self.output_dict += fn_def
565
-
566
- def parse_class(self):
567
- log.trace(f"# {self.line.rstrip()}")
568
- this_class = self.line.split(SEPERATOR)[-1].strip() # raw
569
- if "(" in this_class:
570
- name, params = this_class.split("(", 2)
571
- else:
572
- name = this_class
573
- params = ""
574
- name = self.strip_prefixes(name)
575
- self.current_class = name
576
- self.current_function = ""
577
-
578
- log.trace(f"# class:: {name} - {this_class}")
579
- # fixup parameters
580
- params = self.fix_parameters(params, f"{name}.__init__")
581
- docstr = self.read_docstring()
582
-
583
- if any(":noindex:" in line for line in docstr):
584
- # if the class docstring contains ':noindex:' on any line then skip
585
- log.trace(f"# Skip :noindex: class {name}")
586
- else:
587
- # write a class header
588
- self.create_update_class(name, params, docstr)
589
-
590
- def parse_method(self):
591
- name = ""
592
- this_method = ""
593
- ## py:staticmethod - py:classmethod - py:decorator
594
- # ref: https://sphinx-tutorial.readthedocs.io/cheatsheet/
595
- log.trace(f"# {self.line.rstrip()}")
596
- if not self.is_balanced(self.line):
597
- self.extend_and_balance_line()
598
-
599
- ## rst_hint is used to access the method decorator ( method, staticmethod, staticmethod ... )
600
- rst_hint = self.get_rst_hint()
601
-
602
- method_names = self.parse_names(oneliner=False)
603
- # filter out overloads with 'param=value' description as these can't be output (currently)
604
- method_names = [m for m in method_names if "param=value" not in m]
605
-
606
- docstr = self.read_docstring()
607
- for this_method in method_names:
608
- try:
609
- name, params = this_method.split("(", 1) # split methodname from params
610
- except ValueError:
611
- name = this_method
612
- params = ")"
613
- is_async = "async" in name
614
- self.current_function = name
615
- # self.writeln(f"# method:: {name}")
616
- if "." in name:
617
- # todo deal with longer / deeper classes
618
- class_name = name.split(".")[0]
619
- # ESPnow.rst has a few methods that are written as `async AIOESPNow.__anext__()`
620
- if is_async:
621
- class_name = class_name.replace("async ", "").strip()
622
- # update current for out-of sequence method processing
623
- self.current_class = class_name
624
- else:
625
- # if nothing specified lets assume part of current class
626
- class_name = self.current_class
627
- name = name.split(".")[-1] # Take only the last part from Pin.toggle
628
-
629
- if full_name := self.output_dict.find(f"class {class_name}"):
630
- parent_class = self.output_dict[full_name]
631
- else:
632
- # not found, create and add new class to the output dict
633
- parent_class = ClassSourceDict(f"class {class_name}():")
634
- self.output_dict += parent_class
635
-
636
- # fixup optional [] parameters and other notations
637
- params = self.fix_parameters(params, f"{class_name}.{name}")
638
-
639
- # parse return type from docstring
640
- ret_type = return_type_from_context(
641
- docstring=docstr, signature=f"{class_name}.{name}", module=self.current_module
642
- )
643
- # methods have 4 flavours
644
- # - __init__ (self, <params>) -> None:
645
- # - classmethod (cls, <params>) -> <ret_type>:
646
- # - staticmethod ( <params>) -> <ret_type>:
647
- # - all other methods (self, <params>) -> <ret_type>:
648
- if name == "__init__":
649
- # avoid params starting with `self ,`
650
- params = self.lstrip_self(params)
651
- method = FunctionSourceDict(
652
- name=f"def {name}",
653
- indent=parent_class.indent + 4,
654
- definition=[f"def __init__(self, {params} -> None:"],
655
- docstr=docstr,
656
- )
657
- elif rst_hint == "classmethod":
658
- method = FunctionSourceDict(
659
- decorators=["@classmethod"],
660
- name=f"def {name}",
661
- indent=parent_class.indent + 4,
662
- definition=[f"def {name}(cls, {params} -> {ret_type}:"],
663
- docstr=docstr,
664
- is_async=is_async,
665
- )
666
- elif rst_hint == "staticmethod":
667
- method = FunctionSourceDict(
668
- decorators=["@staticmethod"],
669
- name=f"def {name}",
670
- indent=parent_class.indent + 4,
671
- definition=[f"def {name}({params} -> {ret_type}:"],
672
- docstr=docstr,
673
- is_async=is_async,
674
- )
675
- else: # just plain method
676
- # avoid params starting with `self ,`
677
- params = self.lstrip_self(params)
678
- method = FunctionSourceDict(
679
- name=f"def {name}",
680
- indent=parent_class.indent + 4,
681
- definition=[f"def {name}(self, {params} -> {ret_type}:"],
682
- docstr=docstr,
683
- is_async=is_async,
684
- )
685
-
686
- parent_class += method
687
-
688
- def lstrip_self(self, params):
689
- """
690
- To avoid duplicate selfs,
691
- Remove `self,` from the start of the parameters
692
- """
693
- if params.startswith("self,"):
694
- params = params[6:]
695
- elif params.startswith("self ,"):
696
- params = params[7:]
697
- return params
698
-
699
- def parse_exception(self):
700
- log.trace(f"# {self.line.rstrip()}")
701
- name = self.line.split(SEPERATOR)[1].strip()
702
- if name == "Exception":
703
- # no need to redefine Exception
704
- self.line_no += 1
705
- return
706
- # Take only the last part from module.ExceptionX
707
- if "." in name:
708
- name = name.split(".")[-1]
709
- except_1 = ClassSourceDict(name=f"class {name}(Exception) : ...", docstr=[], init="")
710
- self.output_dict += except_1
711
- # no docstream read (yet) , so need to advance to next line
712
- self.line_no += 1
713
-
714
- def parse_name(self, line: Optional[str] = None):
715
- "get the constant/function/class name from a line with an identifier"
716
- # '.. data:: espnow.MAX_DATA_LEN(=250)\n'
717
- if line:
718
- return line.split(SEPERATOR)[-1].strip()
719
- else:
720
- return self.line.split(SEPERATOR)[-1].strip()
721
-
722
- def parse_names(self, oneliner: bool = True):
723
- """get a list of constant/function/class names from and following a line with an identifier
724
- advances the linecounter
725
-
726
- oneliner : treat a line with commas as multiple names (used for constants)
727
- """
728
- names: List[str] = []
729
- names += self.parse_name().split(",") if oneliner else [self.parse_name()]
730
- m = re.search(r"..\s?\w+\s?::\s?", self.line)
731
- if not m: # pragma: no cover
732
- raise KeyError
733
- col = m.end()
734
- counter = 1
735
- while (
736
- self.line_no + counter <= self.max_line
737
- and self.rst_text[self.line_no + counter].startswith(" " * col)
738
- and not self.rst_text[self.line_no + counter][col + 1].isspace()
739
- ):
740
- log.trace("Sequence detected")
741
- names.append(self.parse_name(self.rst_text[self.line_no + counter]))
742
- counter += 1
743
- # now advance the linecounter
744
- self.line_no += counter - 1
745
- # clean up before returning
746
- names = [n.strip() for n in names if n.strip() != "etc."] # remove etc.
747
- return names
748
-
749
- def parse_data(self):
750
- """process ..data:: lines ( one or more)
751
- Note: some data islands are included in the docstring of the module/class/function as the ESPNow documentation started to use this pattern.
752
- """
753
- log.trace(f"# {self.line.rstrip()}")
754
- # Get one or more names
755
- names = self.parse_names()
756
-
757
- # get module docstring
758
- docstr = self.read_docstring()
759
-
760
- # deal with documentation wildcards
761
- for name in names:
762
- r_type = return_type_from_context(
763
- docstring=docstr, signature=name, module=self.current_module, literal=True
764
- )
765
- if r_type in ["None"]: # None does not make sense
766
- r_type = "Incomplete" # Default to Incomplete/ Unknown / int
767
- name = self.strip_prefixes(name)
768
- self.output_dict.add_constant_smart(name=name, type=r_type, docstr=docstr)
769
-
770
- def parse(self):
771
- self.line_no = 0
772
- while self.line_no < len(self.rst_text):
773
- line = self.line
774
- rst_hint = self.get_rst_hint()
775
- # self.writeln(">"+line)
776
- if rst_hint == "module":
777
- self.parse_module()
778
- elif rst_hint == "currentmodule":
779
- self.parse_current_module()
780
- elif rst_hint == "function":
781
- self.parse_function()
782
- elif rst_hint == "class":
783
- self.parse_class()
784
- elif rst_hint in ["method", "staticmethod", "classmethod"]:
785
- self.parse_method()
786
- elif rst_hint == "exception":
787
- self.parse_exception()
788
- elif rst_hint == "data":
789
- self.parse_data()
790
- elif rst_hint == "toctree":
791
- self.parse_toc()
792
- # Note: this will end the processing of this file.
793
- elif len(rst_hint) > 0:
794
- # something new / not yet parsed/understood
795
- self.line_no += 1
796
- log.trace(f"# {line.rstrip()}")
797
- else:
798
- # NOTHING TO SEE HERE , MOVE ON
799
- self.line_no += 1
800
-
801
-
802
- #################################################################################################################
803
- class RSTWriter(RSTParser):
804
- """
805
- Reads, parses and writes
806
- """
807
-
808
- def __init__(self, v_tag="v1.xx"):
809
- super().__init__(v_tag=v_tag)
810
-
811
- def write_file(self, filename: Path) -> bool:
812
- self.prepare_output()
813
- return super().write_file(filename)
814
-
815
- def prepare_output(self):
816
- "Remove trailing spaces and commas from the output."
817
- lines = str(self.output_dict).splitlines(keepends=True)
818
- self.output = lines
819
- for i in range(len(self.output)):
820
- for name in ("self", "cls"):
821
- if f"({name}, ) ->" in self.output[i]:
822
- self.output[i] = self.output[i].replace(f"({name}, ) ->", f"({name}) ->")
1
+ """
2
+ Read the Micropython library documentation files and use them to build stubs that can be used for static typechecking
3
+ using a custom-built parser to read and process the micropython RST files
4
+ - generates:
5
+ - modules
6
+ - docstrings
7
+ - function definitions
8
+ - function parameters based on documentation
9
+ - docstrings
10
+ - classes
11
+ - docstrings
12
+ - __init__ method
13
+ - parameters based on documentation for class
14
+ - methods
15
+ - parameters based on documentation for the method
16
+ - docstrings
17
+
18
+ - exceptions
19
+
20
+ - Tries to determine the return type by parsing the docstring.
21
+ - Imperative verbs used in docstrings have a strong correlation to return -> None
22
+ - recognizes documented Generators, Iterators, Callable
23
+ - Coroutines are identified based tag "This is a Coroutine". Then if the return type was Foo, it will be transformed to : Coroutine[Foo]
24
+ - a static Lookup list is used for a few methods/functions for which the return type cannot be determined from the docstring.
25
+ - add NoReturn to a few functions that never return ( stop / deepsleep / reset )
26
+ - if no type can be detected the type `Any` or `Incomplete` is used
27
+
28
+ The generated stub files are formatted using `black` and checked for validity using `pyright`
29
+ Note: black on python 3.7 does not like some function defs
30
+ `def sizeof(struct, layout_type=NATIVE, /) -> int:`
31
+
32
+ - ordering of inter-dependent classes in the same module
33
+
34
+ - Literals / constants
35
+ - documentation contains repeated vars with the same indentation
36
+ - Module level:
37
+ .. code-block::
38
+
39
+ .. data:: IPPROTO_UDP
40
+ IPPROTO_TCP
41
+
42
+ - class level:
43
+ .. code-block::
44
+
45
+ .. data:: Pin.IRQ_FALLING
46
+ Pin.IRQ_RISING
47
+ Pin.IRQ_LOW_LEVEL
48
+ Pin.IRQ_HIGH_LEVEL
49
+
50
+ Selects the IRQ trigger type.
51
+
52
+ - literals documented using a wildcard are added as comments only
53
+
54
+ - Add GLUE imports to allow specific modules to import specific others.
55
+
56
+ - Repeats of definitions in the rst file for similar functions or literals
57
+ - CONSTANTS ( module and Class level )
58
+ - functions
59
+ - methods
60
+
61
+ - Child/ Parent classes
62
+ are added based on a (manual) lookup table CHILD_PARENT_CLASS
63
+
64
+ """
65
+
66
+ import re
67
+ from pathlib import Path
68
+ from typing import List, Optional, Tuple
69
+
70
+ from loguru import logger as log
71
+
72
+ from stubber.rst import (
73
+ CHILD_PARENT_CLASS,
74
+ MODULE_GLUE,
75
+ PARAM_FIXES,
76
+ RST_DOC_FIXES,
77
+ TYPING_IMPORT,
78
+ ClassSourceDict,
79
+ FunctionSourceDict,
80
+ ModuleSourceDict,
81
+ return_type_from_context,
82
+ )
83
+ from stubber.rst.lookup import Fix
84
+ from stubber.utils.config import CONFIG
85
+ from stubber.utils.versions import V_PREVIEW
86
+
87
+ SEPERATOR = "::"
88
+
89
+
90
+ class FileReadWriter:
91
+ """base class for reading rst files"""
92
+
93
+ def __init__(self):
94
+ self.filename = ""
95
+ # input buffer
96
+ self.rst_text: List[str] = []
97
+ self.max_line = 0
98
+ self.line_no: int = 0 # current Linenumber used during parsing.
99
+ self.last_line = ""
100
+
101
+ # Output buffer
102
+ self.output: List[str] = []
103
+
104
+ def read_file(self, filename: Path):
105
+ log.trace(f"Reading : {filename}")
106
+ # ignore Unicode decoding issues
107
+ with open(filename, errors="ignore", encoding="utf8") as file:
108
+ self.rst_text = file.readlines()
109
+ # Replace incorrect definitions in .rst files with better ones
110
+ for FIX in RST_DOC_FIXES:
111
+ self.rst_text = [line.replace(FIX[0], FIX[1]) for line in self.rst_text]
112
+ # some lines now may have \n sin them , so re-join and re-split the lines
113
+ self.rst_text = "".join(self.rst_text).splitlines(keepends=True)
114
+
115
+ self.filename = filename.as_posix() # use fwd slashes in origin
116
+ self.max_line = len(self.rst_text) - 1
117
+
118
+ def write_file(self, filename: Path) -> bool:
119
+ try:
120
+ log.info(f" - Writing to: {filename}")
121
+ with open(filename, mode="w", encoding="utf8") as file:
122
+ file.writelines(self.output)
123
+ except OSError as e:
124
+ log.error(e)
125
+ return False
126
+ return True
127
+
128
+ @property
129
+ def line(self) -> str:
130
+ "get the current line from input, also stores this as last_line to allow for inspection and dumping the json file"
131
+ if self.line_no >= 0 and self.line_no <= self.max_line:
132
+ self.last_line = self.rst_text[self.line_no]
133
+ else:
134
+ self.last_line = ""
135
+ return self.last_line
136
+
137
+ @staticmethod
138
+ def is_balanced(s: str) -> bool:
139
+ """
140
+ Check if a string has balanced parentheses
141
+ """
142
+ return False if s.count("(") != s.count(")") else s.count("{") == s.count("}")
143
+
144
+ def extend_and_balance_line(self) -> str:
145
+ """
146
+ Append the current line + next line in order to try to balance the parentheses
147
+ in order to do this the rst_test array is changed by the function
148
+ and max_line is adjusted
149
+ """
150
+ append = 0
151
+ newline = self.rst_text[self.line_no]
152
+ while not self.is_balanced(newline) and self.line_no >= 0 and (self.line_no + append + 1) <= self.max_line:
153
+ append += 1
154
+ # concat the lines
155
+ newline += self.rst_text[self.line_no + append]
156
+ # only update line if things balanced out correctly
157
+ if self.is_balanced(newline):
158
+ self.rst_text[self.line_no] = newline
159
+ for _ in range(append):
160
+ self.rst_text.pop(self.line_no + 1)
161
+ self.max_line -= 1
162
+ # reprocess line
163
+ return self.line
164
+
165
+
166
+ class RSTReader(FileReadWriter):
167
+ docstring_anchors = [
168
+ ".. note::",
169
+ ".. data:: Arguments:",
170
+ ".. data:: Options:",
171
+ ".. data:: Returns:",
172
+ ".. data:: Raises:",
173
+ ".. admonition::",
174
+ ]
175
+ # considered part of the docstrings
176
+
177
+ def __init__(self):
178
+ self.current_module = ""
179
+ self.current_class = ""
180
+ self.current_function = "" # function & method
181
+ super().__init__()
182
+
183
+ def read_file(self, filename: Path):
184
+ super().read_file(filename)
185
+ self.current_module = filename.stem # just to be sure
186
+
187
+ @property
188
+ def module_names(self) -> List[str]:
189
+ "list of possible module names [uname , name] (longest first)"
190
+ namelist: List[str] = []
191
+ if self.current_module == "":
192
+ return namelist
193
+ # deal with module names "esp and esp.socket"
194
+ if "." in self.current_module:
195
+ names = [self.current_module, self.current_module.split(".")[0]]
196
+ else:
197
+ names = [self.current_module]
198
+ # process
199
+ for c_mod in names:
200
+ if self.current_module[0] != "u":
201
+ namelist += [f"u{c_mod}", c_mod]
202
+ else:
203
+ namelist += [c_mod, c_mod[1:]]
204
+ return namelist
205
+
206
+ @property
207
+ def at_anchor(self) -> bool:
208
+ "Stop at anchor '..something' ( however .. note: and ..data:: should be added)"
209
+ line = self.rst_text[self.line_no].lstrip()
210
+ # anchors that are considered part of the docstring
211
+ # Check if the line starts with '..' but not any of the docstring_anchors.
212
+ if line.startswith(".."):
213
+ return not any(line.startswith(anchor) for anchor in self.docstring_anchors)
214
+ return False
215
+
216
+ # return _l.startswith("..") and not any(_l.startswith(a) for a in self.docstring_anchors)
217
+
218
+ # @property
219
+ def at_heading(self, large=False) -> bool:
220
+ "stop at heading"
221
+ u_line = self.rst_text[min(self.line_no + 1, self.max_line - 1)].rstrip()
222
+ # Heading ---, ==, ~~~
223
+ underlined = u_line.startswith("---") or u_line.startswith("===") or u_line.startswith("~~~")
224
+ if underlined and self.line_no > 0:
225
+ # check if previous line is a heading
226
+ line = self.rst_text[self.line_no].strip()
227
+ if line:
228
+ # module docstrings can be a bit larger than normal
229
+ if not large and len(line) == len(u_line):
230
+ # heading is same length as underlined
231
+ # for most docstrings that is a sensible boundary
232
+ return True
233
+ line = line.split()[0]
234
+ # stopwords in headings
235
+ return line.lower() in [
236
+ "classes",
237
+ "functions",
238
+ "methods",
239
+ "constants",
240
+ "exceptions",
241
+ "constructors",
242
+ "class",
243
+ "common",
244
+ "general",
245
+ # below are tuning based on module level docstrings
246
+ "time",
247
+ "pio",
248
+ "memory",
249
+ ]
250
+ return False
251
+
252
+ def read_docstring(self, large: bool = False) -> List[str]:
253
+ """Read a textblock that will be used as a docstring, or used to process a toc tree
254
+ The textblock is terminated at the following RST line structures/tags
255
+ .. <anchor>
256
+ -- Heading
257
+ == Heading
258
+ ~~ Heading
259
+
260
+ The blank lines at the start and end are removed to limit the space the docstring takes up.
261
+ """
262
+ if self.line_no >= len(self.rst_text):
263
+ raise IndexError
264
+
265
+ block: List[str] = []
266
+ self.line_no += 1 # advance over current line
267
+ try:
268
+ while (
269
+ self.line_no < len(self.rst_text)
270
+ and not self.at_anchor # stop at next anchor ( however .. note: and a few other anchors should be added)
271
+ and not self.at_heading(large) # stop at next heading
272
+ ):
273
+ line = self.rst_text[self.line_no]
274
+ block.append(line.rstrip())
275
+ self.line_no += 1 # advance line
276
+ except IndexError:
277
+ pass
278
+
279
+ # remove empty lines at beginning/end of block
280
+ block = self.clean_docstr(block)
281
+ # add clickable hyperlinks to CPython docpages
282
+ block = self.add_link_to_docsstr(block)
283
+ # make sure the first char of the first line is a capital
284
+ if len(block) > 0 and len(block[0]) > 0:
285
+ block[0] = block[0][0].upper() + block[0][1:]
286
+ return block
287
+
288
+ @staticmethod
289
+ def clean_docstr(block: List[str]):
290
+ """Clean up a docstring"""
291
+ # if a Quoted Literal Block , then remove the first character of each line
292
+ # https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#quoted-literal-blocks
293
+ if block and len(block[0]) > 0 and block[0][0] != " ":
294
+ q_char = block[0][0]
295
+ if all(l.startswith(q_char) for l in block):
296
+ # all lines start with the same character, so skip that character
297
+ block = [l[1:] for l in block]
298
+ # rstrip all lines
299
+ block = [l.rstrip() for l in block]
300
+ # remove empty lines at beginning/end of block
301
+ while len(block) and len(block[0]) == 0:
302
+ block = block[1:]
303
+ while len(block) and len(block[-1]) == 0:
304
+ block = block[:-1]
305
+
306
+ # Clean up Synopsis
307
+ if len(block) and ":synopsis:" in block[0]:
308
+ block[0] = re.sub(
309
+ r"\s+:synopsis:\s+(?P<syn>[\w|\s]*)",
310
+ r"\g<syn>",
311
+ block[0],
312
+ )
313
+ return block
314
+
315
+ @staticmethod
316
+ def add_link_to_docsstr(block: List[str]):
317
+ """Add clickable hyperlinks to CPython docpages"""
318
+ for i in range(len(block)):
319
+ # hyperlink to Cpython doc pages
320
+ # https://regex101.com/r/5RN8rj/1
321
+ # Link to python 3 documentation
322
+ _l = re.sub(
323
+ r"(\s*\|see_cpython_module\|\s+:mod:`python:(?P<mod>[\w|\s]*)`)[.]?",
324
+ r"\g<1> https://docs.python.org/3/library/\g<mod>.html .",
325
+ block[i],
326
+ )
327
+ # RST hyperlink format is not clickable in VSCode so convert to markdown format
328
+ # https://regex101.com/r/5RN8rj/1
329
+ _l = re.sub(
330
+ r"(.*)(?P<url><https://docs\.python\.org/.*>)(`_)",
331
+ r"\g<1>`\g<url>",
332
+ _l,
333
+ )
334
+ # Clean up note and other docstring anchors
335
+ _l = _l.replace(".. note:: ", "``Note:`` ")
336
+ _l = _l.replace(".. data:: ", "")
337
+ _l = _l.replace(".. admonition:: ", "")
338
+ _l = _l.replace("|see_cpython_module|", "CPython module:")
339
+ # clean up unsupported escape sequences in rst
340
+ _l = _l.replace(r"\ ", " ")
341
+ _l = _l.replace(r"\*", "*")
342
+ block[i] = _l
343
+ return block
344
+
345
+ def get_rst_hint(self):
346
+ "parse the '.. <rst hint>:: ' from the current line"
347
+ m = re.search(r"\.\.\s?(\w+)\s?::\s?", self.line)
348
+ return m[1] if m else ""
349
+
350
+ def strip_prefixes(self, name: str, strip_mod: bool = True, strip_class: bool = False):
351
+ "Remove the modulename. and or the classname. from the begining of a name"
352
+ prefixes = self.module_names if strip_mod else []
353
+ if strip_class and self.current_class != "":
354
+ prefixes += [self.current_class]
355
+ for prefix in prefixes:
356
+ if len(prefix) > 1 and prefix + "." in name:
357
+ name = name.replace(prefix + ".", "")
358
+ return name
359
+
360
+
361
+ class RSTParser(RSTReader):
362
+ """
363
+ Parse the RST file and create a ModuleSourceDict
364
+ most methods have side effects
365
+ """
366
+
367
+ target = ".py" # py/pyi
368
+ # TODO: Move to lookup.py
369
+ PARAM_RE_FIXES = [
370
+ Fix(r"\[angle, time=0\]", "[angle], time=0", is_re=True), # fix: method:: Servo.angle([angle, time=0])
371
+ Fix(r"\[speed, time=0\]", "[speed], time=0", is_re=True), # fix: .. method:: Servo.speed([speed, time=0])
372
+ Fix(
373
+ r"\[service_id, key=None, \*, \.\.\.\]", "[service_id], [key], *, ...", is_re=True
374
+ ), # fix: network - AbstractNIC.connect
375
+ ]
376
+
377
+ def __init__(self, v_tag: str) -> None:
378
+ super().__init__()
379
+ self.output_dict: ModuleSourceDict = ModuleSourceDict("")
380
+ self.output_dict.add_import(TYPING_IMPORT)
381
+ self.return_info: List[Tuple] = [] # development aid only
382
+ self.source_tag = v_tag
383
+ self.source_release = v_tag
384
+
385
+ def leave_class(self):
386
+ if self.current_class != "":
387
+ self.current_class = ""
388
+
389
+ def fix_parameters(self, params: str, name: str = "") -> str:
390
+ """Patch / correct the documentation parameter notation to a supported format that works for linting.
391
+ - params is the string containing the parameters as documented in the rst file
392
+ - name is the name of the function or method or Class
393
+ """
394
+ params = params.strip()
395
+ if not params.endswith(")"):
396
+ # remove all after the closing bracket
397
+ params = params[: params.rfind(")") + 1]
398
+
399
+ # Remove modulename. and Classname. from class constant
400
+ params = self.strip_prefixes(params, strip_mod=True, strip_class=True)
401
+
402
+ ## Deal with SQUARE brackets first ( Documentation meaning := [optional])
403
+
404
+ for fix in self.PARAM_RE_FIXES:
405
+ params = self.apply_fix(fix, params, name)
406
+
407
+ # ###########################################################################################################
408
+ # does not look cool, but works really well
409
+ # change [x] --> x:Optional[Any]
410
+ params = params.replace("[", "")
411
+ params = params.replace("]]", "") # Q&D Hack-complex nesting
412
+
413
+ # Handle Optional arguments
414
+ # Optional step 1: [x] --> x: Optional[Any]=None
415
+ params = params.replace("]", ": Optional[Any]=None")
416
+ # Optional step 2: x: Optional[Any]=None='abc' --> x: Optional[Any]='abc'
417
+ params = re.sub(r": Optional\[Any\]=None\s*=", r": Optional[Any]=", params)
418
+ # Optional step 3: fix ...
419
+ params = re.sub(r"\.\.\.: Optional\[Any\]=None", r"...", params)
420
+ # ###########################################################################################################
421
+
422
+ for fix in PARAM_FIXES:
423
+ if fix.module == self.current_module or not fix.module:
424
+ params = self.apply_fix(fix, params, name)
425
+
426
+ # # formatting
427
+ # # fixme: ... not allowed in .py
428
+ if self.target == ".py":
429
+ params = params.replace("*, ...", "*args, **kwargs")
430
+ params = params.replace("...", "*args, **kwargs")
431
+
432
+ return params.strip()
433
+
434
+ @staticmethod
435
+ def apply_fix(fix: Fix, params: str, name: str = ""):
436
+ if fix.name and fix.name != name:
437
+ return params
438
+ return re.sub(fix.from_, fix.to, params) if fix.is_re else params.replace(fix.from_, fix.to)
439
+
440
+ def create_update_class(self, name: str, params: str, docstr: List[str]):
441
+ # a bit of a hack: assume no classes in classes or functions in function
442
+ self.leave_class()
443
+ if full_name := self.output_dict.find(f"class {name}"):
444
+ log.warning(f"TODO: UPDATE EXISTING CLASS : {name}")
445
+ class_def = self.output_dict[full_name]
446
+ else:
447
+ parent = CHILD_PARENT_CLASS[name] if name in CHILD_PARENT_CLASS.keys() else ""
448
+ if parent == "" and (name.endswith("Error") or name.endswith("Exception")):
449
+ parent = "Exception"
450
+ class_def = ClassSourceDict(
451
+ f"class {name}({parent}):",
452
+ docstr=docstr,
453
+ )
454
+ if params != "":
455
+ method = FunctionSourceDict(
456
+ name="__init__",
457
+ indent=class_def.indent + 4,
458
+ definition=[f"def __init__(self, {params} -> None:"],
459
+ docstr=[], # todo: check if twice is needed
460
+ )
461
+ class_def += method
462
+ # Append class to output
463
+ self.output_dict += class_def
464
+ self.current_class = name
465
+
466
+ def parse_toc(self):
467
+ "process table of content with additional rst files, and add / include them in the current module"
468
+ log.trace(f"# {self.line.rstrip()}")
469
+ self.line_no += 1 # skip one line
470
+ toctree = self.read_docstring()
471
+ # cleanup toctree
472
+ toctree = [x.strip() for x in toctree if f"{self.current_module}." in x]
473
+ # Now parse all files mentioned in the toc
474
+ for file in toctree:
475
+ #
476
+ file_path = CONFIG.mpy_path / "docs" / "library" / file.strip()
477
+ self.read_file(file_path)
478
+ self.parse()
479
+ # reset this file to done
480
+ self.rst_text = []
481
+ self.line_no = 1
482
+
483
+ def parse_module(self):
484
+ "parse a module tag and set the module's docstring"
485
+ log.trace(f"# {self.line.rstrip()}")
486
+ module_name = self.line.split(SEPERATOR)[-1].strip()
487
+
488
+ self.current_module = module_name
489
+ self.current_function = self.current_class = ""
490
+ # get module docstring
491
+ docstr = self.read_docstring(large=True)
492
+
493
+ if len(docstr) > 0:
494
+ # Add link to online documentation
495
+ # https://docs.micropython.org/en/v1.17/library/array.html
496
+ if "nightly" in self.source_tag:
497
+ version = V_PREVIEW
498
+ else:
499
+ version = self.source_tag.replace("_", ".") # TODO Use clean_version(self.source_tag)
500
+ docstr[0] = (
501
+ f"{docstr[0]}.\n\nMicroPython module: https://docs.micropython.org/en/{version}/library/{module_name}.html"
502
+ )
503
+
504
+ self.output_dict.name = module_name
505
+ self.output_dict.add_comment(f"# source version: {self.source_tag}")
506
+ self.output_dict.add_comment(f"# origin module:: {self.filename}")
507
+ self.output_dict.add_docstr(docstr)
508
+ # Add additional imports to allow one module te refer to another
509
+ if module_name in MODULE_GLUE.keys():
510
+ self.output_dict.add_import(MODULE_GLUE[module_name])
511
+
512
+ def parse_current_module(self):
513
+ log.trace(f"# {self.line.rstrip()}")
514
+ module_name = self.line.split(SEPERATOR)[-1].strip()
515
+ mod_comment = f"# + module: {self.current_module}.rst"
516
+ self.current_module = module_name
517
+ self.current_function = self.current_class = ""
518
+ log.debug(mod_comment)
519
+ self.output_dict.name = module_name
520
+ self.output_dict.add_comment(mod_comment)
521
+ self.line_no += 1 # advance as we did not read any docstring
522
+
523
+ def parse_function(self):
524
+ log.trace(f"# {self.line.rstrip()}")
525
+ # this_function = self.line.split(SEPERATOR)[-1].strip()
526
+ # name = this_function
527
+
528
+ # Get one or more names
529
+ function_names = self.parse_names(oneliner=False)
530
+ docstr = self.read_docstring()
531
+
532
+ for this_function in function_names:
533
+ # Parse return type from docstring
534
+ ret_type = return_type_from_context(docstring=docstr, signature=this_function, module=self.current_module)
535
+
536
+ # defaults
537
+ name = params = ""
538
+ try:
539
+ name, params = this_function.split("(", maxsplit=1)
540
+ except ValueError:
541
+ log.error(this_function)
542
+ self.current_function = name
543
+ if name in ("classmethod", "staticmethod"):
544
+ # skip the classmethod and static method functions
545
+ # no use to create stubs for these
546
+ return
547
+
548
+ # ussl documentation uses a ssl.foobar prefix
549
+ for mod in self.module_names:
550
+ if name.startswith(f"{mod}."):
551
+ # remove module name from the start of the function name
552
+ name = name[len(f"{mod}.") :]
553
+ # fixup parameters
554
+ params = self.fix_parameters(params, name)
555
+ # ASSUME no functions in classes,
556
+ # so with ther cursor at a function this probably means that we are no longer in a class
557
+ self.leave_class()
558
+
559
+ fn_def = FunctionSourceDict(
560
+ name=f"def {name}",
561
+ definition=[f"def {name}({params} -> {ret_type}:"],
562
+ docstr=docstr,
563
+ )
564
+ self.output_dict += fn_def
565
+
566
+ def parse_class(self):
567
+ log.trace(f"# {self.line.rstrip()}")
568
+ this_class = self.line.split(SEPERATOR)[-1].strip() # raw
569
+ if "(" in this_class:
570
+ name, params = this_class.split("(", 2)
571
+ else:
572
+ name = this_class
573
+ params = ""
574
+ name = self.strip_prefixes(name)
575
+ self.current_class = name
576
+ self.current_function = ""
577
+
578
+ log.trace(f"# class:: {name} - {this_class}")
579
+ # fixup parameters
580
+ params = self.fix_parameters(params, f"{name}.__init__")
581
+ docstr = self.read_docstring()
582
+
583
+ if any(":noindex:" in line for line in docstr):
584
+ # if the class docstring contains ':noindex:' on any line then skip
585
+ log.trace(f"# Skip :noindex: class {name}")
586
+ else:
587
+ # write a class header
588
+ self.create_update_class(name, params, docstr)
589
+
590
+ def parse_method(self):
591
+ name = ""
592
+ this_method = ""
593
+ ## py:staticmethod - py:classmethod - py:decorator
594
+ # ref: https://sphinx-tutorial.readthedocs.io/cheatsheet/
595
+ log.trace(f"# {self.line.rstrip()}")
596
+ if not self.is_balanced(self.line):
597
+ self.extend_and_balance_line()
598
+
599
+ ## rst_hint is used to access the method decorator ( method, staticmethod, staticmethod ... )
600
+ rst_hint = self.get_rst_hint()
601
+
602
+ method_names = self.parse_names(oneliner=False)
603
+ # filter out overloads with 'param=value' description as these can't be output (currently)
604
+ method_names = [m for m in method_names if "param=value" not in m]
605
+
606
+ docstr = self.read_docstring()
607
+ for this_method in method_names:
608
+ try:
609
+ name, params = this_method.split("(", 1) # split methodname from params
610
+ except ValueError:
611
+ name = this_method
612
+ params = ")"
613
+ is_async = "async" in name
614
+ self.current_function = name
615
+ # self.writeln(f"# method:: {name}")
616
+ if "." in name:
617
+ # todo deal with longer / deeper classes
618
+ class_name = name.split(".")[0]
619
+ # ESPnow.rst has a few methods that are written as `async AIOESPNow.__anext__()`
620
+ if is_async:
621
+ class_name = class_name.replace("async ", "").strip()
622
+ # update current for out-of sequence method processing
623
+ self.current_class = class_name
624
+ else:
625
+ # if nothing specified lets assume part of current class
626
+ class_name = self.current_class
627
+ name = name.split(".")[-1] # Take only the last part from Pin.toggle
628
+
629
+ if full_name := self.output_dict.find(f"class {class_name}"):
630
+ parent_class = self.output_dict[full_name]
631
+ else:
632
+ # not found, create and add new class to the output dict
633
+ parent_class = ClassSourceDict(f"class {class_name}():")
634
+ self.output_dict += parent_class
635
+
636
+ # fixup optional [] parameters and other notations
637
+ params = self.fix_parameters(params, f"{class_name}.{name}")
638
+
639
+ # parse return type from docstring
640
+ ret_type = return_type_from_context(
641
+ docstring=docstr, signature=f"{class_name}.{name}", module=self.current_module
642
+ )
643
+ # methods have 4 flavours
644
+ # - __init__ (self, <params>) -> None:
645
+ # - classmethod (cls, <params>) -> <ret_type>:
646
+ # - staticmethod ( <params>) -> <ret_type>:
647
+ # - all other methods (self, <params>) -> <ret_type>:
648
+ if name == "__init__":
649
+ # avoid params starting with `self ,`
650
+ params = self.lstrip_self(params)
651
+ method = FunctionSourceDict(
652
+ name=f"def {name}",
653
+ indent=parent_class.indent + 4,
654
+ definition=[f"def __init__(self, {params} -> None:"],
655
+ docstr=docstr,
656
+ )
657
+ elif rst_hint == "classmethod":
658
+ method = FunctionSourceDict(
659
+ decorators=["@classmethod"],
660
+ name=f"def {name}",
661
+ indent=parent_class.indent + 4,
662
+ definition=[f"def {name}(cls, {params} -> {ret_type}:"],
663
+ docstr=docstr,
664
+ is_async=is_async,
665
+ )
666
+ elif rst_hint == "staticmethod":
667
+ method = FunctionSourceDict(
668
+ decorators=["@staticmethod"],
669
+ name=f"def {name}",
670
+ indent=parent_class.indent + 4,
671
+ definition=[f"def {name}({params} -> {ret_type}:"],
672
+ docstr=docstr,
673
+ is_async=is_async,
674
+ )
675
+ else: # just plain method
676
+ # avoid params starting with `self ,`
677
+ params = self.lstrip_self(params)
678
+ method = FunctionSourceDict(
679
+ name=f"def {name}",
680
+ indent=parent_class.indent + 4,
681
+ definition=[f"def {name}(self, {params} -> {ret_type}:"],
682
+ docstr=docstr,
683
+ is_async=is_async,
684
+ )
685
+
686
+ parent_class += method
687
+
688
+ def lstrip_self(self, params):
689
+ """
690
+ To avoid duplicate selfs,
691
+ Remove `self,` from the start of the parameters
692
+ """
693
+ if params.startswith("self,"):
694
+ params = params[6:]
695
+ elif params.startswith("self ,"):
696
+ params = params[7:]
697
+ return params
698
+
699
+ def parse_exception(self):
700
+ log.trace(f"# {self.line.rstrip()}")
701
+ name = self.line.split(SEPERATOR)[1].strip()
702
+ if name == "Exception":
703
+ # no need to redefine Exception
704
+ self.line_no += 1
705
+ return
706
+ # Take only the last part from module.ExceptionX
707
+ if "." in name:
708
+ name = name.split(".")[-1]
709
+ except_1 = ClassSourceDict(name=f"class {name}(Exception) : ...", docstr=[], init="")
710
+ self.output_dict += except_1
711
+ # no docstream read (yet) , so need to advance to next line
712
+ self.line_no += 1
713
+
714
+ def parse_name(self, line: Optional[str] = None):
715
+ "get the constant/function/class name from a line with an identifier"
716
+ # '.. data:: espnow.MAX_DATA_LEN(=250)\n'
717
+ if line:
718
+ return line.split(SEPERATOR)[-1].strip()
719
+ else:
720
+ return self.line.split(SEPERATOR)[-1].strip()
721
+
722
+ def parse_names(self, oneliner: bool = True):
723
+ """get a list of constant/function/class names from and following a line with an identifier
724
+ advances the linecounter
725
+
726
+ oneliner : treat a line with commas as multiple names (used for constants)
727
+ """
728
+ names: List[str] = []
729
+ names += self.parse_name().split(",") if oneliner else [self.parse_name()]
730
+ m = re.search(r"..\s?\w+\s?::\s?", self.line)
731
+ if not m: # pragma: no cover
732
+ raise KeyError
733
+ col = m.end()
734
+ counter = 1
735
+ while (
736
+ self.line_no + counter <= self.max_line
737
+ and self.rst_text[self.line_no + counter].startswith(" " * col)
738
+ and not self.rst_text[self.line_no + counter][col + 1].isspace()
739
+ ):
740
+ log.trace("Sequence detected")
741
+ names.append(self.parse_name(self.rst_text[self.line_no + counter]))
742
+ counter += 1
743
+ # now advance the linecounter
744
+ self.line_no += counter - 1
745
+ # clean up before returning
746
+ names = [n.strip() for n in names if n.strip() != "etc."] # remove etc.
747
+ return names
748
+
749
+ def parse_data(self):
750
+ """process ..data:: lines ( one or more)
751
+ Note: some data islands are included in the docstring of the module/class/function as the ESPNow documentation started to use this pattern.
752
+ """
753
+ log.trace(f"# {self.line.rstrip()}")
754
+ # Get one or more names
755
+ names = self.parse_names()
756
+
757
+ # get module docstring
758
+ docstr = self.read_docstring()
759
+
760
+ # deal with documentation wildcards
761
+ for name in names:
762
+ r_type = return_type_from_context(
763
+ docstring=docstr, signature=name, module=self.current_module, literal=True
764
+ )
765
+ if r_type in ["None"]: # None does not make sense
766
+ r_type = "Incomplete" # Default to Incomplete/ Unknown / int
767
+ name = self.strip_prefixes(name)
768
+ self.output_dict.add_constant_smart(name=name, type=r_type, docstr=docstr)
769
+
770
+ def parse(self):
771
+ self.line_no = 0
772
+ while self.line_no < len(self.rst_text):
773
+ line = self.line
774
+ rst_hint = self.get_rst_hint()
775
+ # self.writeln(">"+line)
776
+ if rst_hint == "module":
777
+ self.parse_module()
778
+ elif rst_hint == "currentmodule":
779
+ self.parse_current_module()
780
+ elif rst_hint == "function":
781
+ self.parse_function()
782
+ elif rst_hint == "class":
783
+ self.parse_class()
784
+ elif rst_hint in ["method", "staticmethod", "classmethod"]:
785
+ self.parse_method()
786
+ elif rst_hint == "exception":
787
+ self.parse_exception()
788
+ elif rst_hint == "data":
789
+ self.parse_data()
790
+ elif rst_hint == "toctree":
791
+ self.parse_toc()
792
+ # Note: this will end the processing of this file.
793
+ elif len(rst_hint) > 0:
794
+ # something new / not yet parsed/understood
795
+ self.line_no += 1
796
+ log.trace(f"# {line.rstrip()}")
797
+ else:
798
+ # NOTHING TO SEE HERE , MOVE ON
799
+ self.line_no += 1
800
+
801
+
802
+ #################################################################################################################
803
+ class RSTWriter(RSTParser):
804
+ """
805
+ Reads, parses and writes
806
+ """
807
+
808
+ def __init__(self, v_tag="v1.xx"):
809
+ super().__init__(v_tag=v_tag)
810
+
811
+ def write_file(self, filename: Path) -> bool:
812
+ self.prepare_output()
813
+ return super().write_file(filename)
814
+
815
+ def prepare_output(self):
816
+ "Remove trailing spaces and commas from the output."
817
+ lines = str(self.output_dict).splitlines(keepends=True)
818
+ self.output = lines
819
+ for i in range(len(self.output)):
820
+ for name in ("self", "cls"):
821
+ if f"({name}, ) ->" in self.output[i]:
822
+ self.output[i] = self.output[i].replace(f"({name}, ) ->", f"({name}) ->")