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