micropython-stubber 1.23.1__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.
- {micropython_stubber-1.23.1.dist-info → micropython_stubber-1.23.2.dist-info}/LICENSE +30 -30
- {micropython_stubber-1.23.1.dist-info → micropython_stubber-1.23.2.dist-info}/METADATA +32 -15
- micropython_stubber-1.23.2.dist-info/RECORD +158 -0
- micropython_stubber-1.23.2.dist-info/entry_points.txt +5 -0
- mpflash/README.md +220 -194
- mpflash/libusb_flash.ipynb +203 -203
- mpflash/mpflash/add_firmware.py +98 -98
- mpflash/mpflash/ask_input.py +236 -236
- mpflash/mpflash/basicgit.py +284 -284
- mpflash/mpflash/bootloader/__init__.py +2 -2
- mpflash/mpflash/bootloader/activate.py +60 -60
- mpflash/mpflash/bootloader/detect.py +82 -82
- mpflash/mpflash/bootloader/manual.py +101 -101
- mpflash/mpflash/bootloader/micropython.py +12 -12
- mpflash/mpflash/bootloader/touch1200.py +36 -36
- mpflash/mpflash/cli_download.py +129 -129
- mpflash/mpflash/cli_flash.py +224 -219
- mpflash/mpflash/cli_group.py +111 -111
- mpflash/mpflash/cli_list.py +87 -81
- mpflash/mpflash/cli_main.py +39 -39
- mpflash/mpflash/common.py +210 -165
- mpflash/mpflash/config.py +44 -44
- mpflash/mpflash/connected.py +96 -78
- mpflash/mpflash/download.py +364 -364
- mpflash/mpflash/downloaded.py +130 -130
- mpflash/mpflash/errors.py +9 -9
- mpflash/mpflash/flash/__init__.py +55 -55
- mpflash/mpflash/flash/esp.py +59 -59
- mpflash/mpflash/flash/stm32.py +19 -19
- mpflash/mpflash/flash/stm32_dfu.py +104 -104
- mpflash/mpflash/flash/uf2/__init__.py +88 -88
- mpflash/mpflash/flash/uf2/boardid.py +15 -15
- mpflash/mpflash/flash/uf2/linux.py +136 -130
- mpflash/mpflash/flash/uf2/macos.py +42 -42
- mpflash/mpflash/flash/uf2/uf2disk.py +12 -12
- mpflash/mpflash/flash/uf2/windows.py +43 -43
- mpflash/mpflash/flash/worklist.py +170 -170
- mpflash/mpflash/list.py +106 -99
- mpflash/mpflash/logger.py +41 -41
- mpflash/mpflash/mpboard_id/__init__.py +93 -93
- mpflash/mpflash/mpboard_id/add_boards.py +251 -251
- mpflash/mpflash/mpboard_id/board.py +37 -37
- mpflash/mpflash/mpboard_id/board_id.py +86 -86
- mpflash/mpflash/mpboard_id/store.py +43 -43
- mpflash/mpflash/mpremoteboard/__init__.py +266 -222
- mpflash/mpflash/mpremoteboard/mpy_fw_info.py +141 -141
- mpflash/mpflash/mpremoteboard/runner.py +140 -140
- mpflash/mpflash/vendor/click_aliases.py +91 -91
- mpflash/mpflash/vendor/dfu.py +165 -165
- mpflash/mpflash/vendor/pydfu.py +605 -605
- mpflash/mpflash/vendor/readme.md +2 -2
- mpflash/mpflash/versions.py +135 -135
- mpflash/poetry.lock +1599 -1599
- mpflash/pyproject.toml +65 -65
- mpflash/stm32_udev_rules.md +62 -62
- stubber/__init__.py +3 -3
- stubber/board/board_info.csv +193 -193
- stubber/board/boot.py +34 -34
- stubber/board/createstubs.py +1004 -986
- stubber/board/createstubs_db.py +826 -825
- stubber/board/createstubs_db_min.py +332 -331
- stubber/board/createstubs_db_mpy.mpy +0 -0
- stubber/board/createstubs_lvgl.py +741 -741
- stubber/board/createstubs_lvgl_min.py +741 -741
- stubber/board/createstubs_mem.py +767 -766
- stubber/board/createstubs_mem_min.py +307 -306
- stubber/board/createstubs_mem_mpy.mpy +0 -0
- stubber/board/createstubs_min.py +295 -294
- stubber/board/createstubs_mpy.mpy +0 -0
- stubber/board/fw_info.py +141 -141
- stubber/board/info.py +183 -183
- stubber/board/main.py +19 -19
- stubber/board/modulelist.txt +247 -247
- stubber/board/pyrightconfig.json +34 -34
- stubber/bulk/mcu_stubber.py +437 -454
- stubber/codemod/_partials/__init__.py +48 -48
- stubber/codemod/_partials/db_main.py +147 -147
- stubber/codemod/_partials/lvgl_main.py +77 -77
- stubber/codemod/_partials/modules_reader.py +80 -80
- stubber/codemod/add_comment.py +53 -53
- stubber/codemod/add_method.py +65 -65
- stubber/codemod/board.py +317 -317
- stubber/codemod/enrich.py +151 -145
- stubber/codemod/merge_docstub.py +284 -284
- stubber/codemod/modify_list.py +54 -54
- stubber/codemod/utils.py +56 -56
- stubber/commands/build_cmd.py +94 -94
- stubber/commands/cli.py +49 -55
- stubber/commands/clone_cmd.py +78 -78
- stubber/commands/config_cmd.py +29 -29
- stubber/commands/enrich_folder_cmd.py +71 -71
- stubber/commands/get_core_cmd.py +71 -71
- stubber/commands/get_docstubs_cmd.py +92 -89
- stubber/commands/get_frozen_cmd.py +117 -114
- stubber/commands/get_mcu_cmd.py +102 -61
- stubber/commands/merge_cmd.py +66 -66
- stubber/commands/publish_cmd.py +118 -118
- stubber/commands/stub_cmd.py +31 -31
- stubber/commands/switch_cmd.py +62 -62
- stubber/commands/variants_cmd.py +48 -48
- stubber/cst_transformer.py +178 -178
- stubber/data/board_info.csv +193 -193
- stubber/data/board_info.json +1729 -1729
- stubber/data/micropython_tags.csv +15 -15
- stubber/data/requirements-core-micropython.txt +38 -38
- stubber/data/requirements-core-pycopy.txt +39 -39
- stubber/downloader.py +37 -36
- stubber/freeze/common.py +72 -68
- stubber/freeze/freeze_folder.py +69 -69
- stubber/freeze/freeze_manifest_2.py +126 -113
- stubber/freeze/get_frozen.py +131 -127
- stubber/get_cpython.py +112 -101
- stubber/get_lobo.py +59 -59
- stubber/minify.py +423 -419
- stubber/publish/bump.py +86 -86
- stubber/publish/candidates.py +275 -256
- stubber/publish/database.py +18 -18
- stubber/publish/defaults.py +40 -40
- stubber/publish/enums.py +24 -24
- stubber/publish/helpers.py +29 -29
- stubber/publish/merge_docstubs.py +136 -130
- stubber/publish/missing_class_methods.py +51 -49
- stubber/publish/package.py +150 -146
- stubber/publish/pathnames.py +51 -51
- stubber/publish/publish.py +120 -120
- stubber/publish/pypi.py +42 -38
- stubber/publish/stubpackage.py +1055 -1027
- stubber/rst/__init__.py +9 -9
- stubber/rst/classsort.py +78 -77
- stubber/rst/lookup.py +533 -530
- stubber/rst/output_dict.py +401 -401
- stubber/rst/reader.py +814 -814
- stubber/rst/report_return.py +77 -69
- stubber/rst/rst_utils.py +541 -540
- stubber/stubber.py +38 -38
- stubber/stubs_from_docs.py +90 -90
- stubber/tools/manifestfile.py +654 -654
- stubber/tools/readme.md +6 -6
- stubber/update_fallback.py +117 -117
- stubber/update_module_list.py +123 -123
- stubber/utils/__init__.py +6 -6
- stubber/utils/config.py +137 -125
- stubber/utils/makeversionhdr.py +54 -54
- stubber/utils/manifest.py +90 -90
- stubber/utils/post.py +80 -79
- stubber/utils/repos.py +156 -150
- stubber/utils/stubmaker.py +139 -139
- stubber/utils/typed_config_toml.py +80 -77
- stubber/variants.py +106 -106
- micropython_stubber-1.23.1.dist-info/RECORD +0 -159
- micropython_stubber-1.23.1.dist-info/entry_points.txt +0 -3
- mpflash/basicgit.py +0 -288
- {micropython_stubber-1.23.1.dist-info → micropython_stubber-1.23.2.dist-info}/WHEEL +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
|
71
|
-
|
72
|
-
from
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
from stubber.
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
class
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
self.
|
97
|
-
self.
|
98
|
-
self.
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
self.
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
"""
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
newline
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
self.
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
"..
|
169
|
-
".. data::
|
170
|
-
".. data::
|
171
|
-
".. data::
|
172
|
-
"..
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
self.
|
179
|
-
self.
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
#
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
underlined
|
224
|
-
|
225
|
-
|
226
|
-
line
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
#
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
"
|
237
|
-
"
|
238
|
-
"
|
239
|
-
"
|
240
|
-
"
|
241
|
-
"
|
242
|
-
"
|
243
|
-
"
|
244
|
-
|
245
|
-
|
246
|
-
"
|
247
|
-
"
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
self.
|
270
|
-
and not self.
|
271
|
-
|
272
|
-
|
273
|
-
line
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
#
|
292
|
-
|
293
|
-
|
294
|
-
q_char
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
r"\
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
#
|
320
|
-
#
|
321
|
-
|
322
|
-
|
323
|
-
r"
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
#
|
328
|
-
|
329
|
-
|
330
|
-
r"
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
_l = _l.replace("..
|
336
|
-
_l = _l.replace("..
|
337
|
-
_l = _l.replace("
|
338
|
-
|
339
|
-
|
340
|
-
_l = _l.replace(r"
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
m
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
Fix(r"\[
|
371
|
-
Fix(r"\[
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
self.output_dict
|
378
|
-
self.
|
379
|
-
self.
|
380
|
-
self.
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
#
|
406
|
-
#
|
407
|
-
|
408
|
-
params = params.replace("
|
409
|
-
|
410
|
-
|
411
|
-
#
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
# #
|
425
|
-
|
426
|
-
|
427
|
-
params = params.replace("
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
self.
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
parent
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
self.
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
"
|
466
|
-
|
467
|
-
self.
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
self.
|
476
|
-
|
477
|
-
|
478
|
-
self.
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
"
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
self.
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
#
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
self.output_dict.
|
501
|
-
self.output_dict.add_comment(f"#
|
502
|
-
self.output_dict.
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
self.
|
513
|
-
|
514
|
-
|
515
|
-
self.output_dict.
|
516
|
-
self.
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
#
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
#
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
#
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
self.
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
#
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
#
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
self.
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
#
|
638
|
-
# -
|
639
|
-
# -
|
640
|
-
# -
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
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}) ->")
|