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
@@ -1,986 +1,1004 @@
1
- """
2
- Create stubs for (all) modules on a MicroPython board
3
- """
4
-
5
- # Copyright (c) 2019-2024 Jos Verlinde
6
-
7
- import gc
8
- import os
9
- import sys
10
- from time import sleep
11
-
12
- try:
13
- from ujson import dumps
14
- except:
15
- from json import dumps
16
-
17
- try:
18
- from machine import reset # type: ignore
19
- except ImportError:
20
- pass
21
-
22
- try:
23
- from collections import OrderedDict
24
- except ImportError:
25
- from ucollections import OrderedDict # type: ignore
26
-
27
- __version__ = "v1.23.1"
28
- ENOENT = 2
29
- _MAX_CLASS_LEVEL = 2 # Max class nesting
30
- LIBS = ["lib", "/lib", "/sd/lib", "/flash/lib", "."]
31
-
32
-
33
- # our own logging module to avoid dependency on and interfering with logging module
34
- class logging:
35
- # DEBUG = 10
36
- INFO = 20
37
- WARNING = 30
38
- ERROR = 40
39
- level = INFO
40
- prnt = print
41
-
42
- @staticmethod
43
- def getLogger(name):
44
- return logging()
45
-
46
- @classmethod
47
- def basicConfig(cls, level):
48
- cls.level = level
49
-
50
- # def debug(self, msg):
51
- # if self.level <= logging.DEBUG:
52
- # self.prnt("DEBUG :", msg)
53
-
54
- def info(self, msg):
55
- if self.level <= logging.INFO:
56
- self.prnt("INFO :", msg)
57
-
58
- def warning(self, msg):
59
- if self.level <= logging.WARNING:
60
- self.prnt("WARN :", msg)
61
-
62
- def error(self, msg):
63
- if self.level <= logging.ERROR:
64
- self.prnt("ERROR :", msg)
65
-
66
-
67
- log = logging.getLogger("stubber")
68
- logging.basicConfig(level=logging.INFO)
69
- # logging.basicConfig(level=logging.DEBUG)
70
-
71
-
72
- class Stubber:
73
- "Generate stubs for modules in firmware"
74
-
75
- def __init__(self, path: str = None, firmware_id: str = None): # type: ignore
76
- try:
77
- if os.uname().release == "1.13.0" and os.uname().version < "v1.13-103": # type: ignore
78
- raise NotImplementedError("MicroPython 1.13.0 cannot be stubbed")
79
- except AttributeError:
80
- pass # Allow testing on CPython 3.11
81
- self.info = _info()
82
- log.info("Port: {}".format(self.info["port"]))
83
- log.info("Board: {}".format(self.info["board"]))
84
- gc.collect()
85
- if firmware_id:
86
- self._fwid = firmware_id.lower()
87
- else:
88
- if self.info["family"] == "micropython":
89
- self._fwid = "{family}-v{version}-{port}-{board}".format(**self.info).rstrip("-")
90
- else:
91
- self._fwid = "{family}-v{version}-{port}".format(**self.info)
92
- self._start_free = gc.mem_free() # type: ignore
93
-
94
- if path:
95
- if path.endswith("/"):
96
- path = path[:-1]
97
- else:
98
- path = get_root()
99
-
100
- self.path = "{}/stubs/{}".format(path, self.flat_fwid).replace("//", "/")
101
- # log.debug(self.path)
102
- try:
103
- ensure_folder(path + "/")
104
- except OSError:
105
- log.error("error creating stub folder {}".format(path))
106
- self.problematic = [
107
- "upip",
108
- "upysh",
109
- "webrepl_setup",
110
- "http_client",
111
- "http_client_ssl",
112
- "http_server",
113
- "http_server_ssl",
114
- ]
115
- self.excluded = [
116
- "webrepl",
117
- "_webrepl",
118
- "port_diag",
119
- "example_sub_led.py",
120
- "example_pub_button.py",
121
- ]
122
- # there is no option to discover modules from micropython, list is read from an external file.
123
- self.modules = [] # type: list[str]
124
- self._json_name = None
125
- self._json_first = False
126
-
127
- def get_obj_attributes(self, item_instance: object):
128
- "extract information of the objects members and attributes"
129
- # name_, repr_(value), type as text, item_instance
130
- _result = []
131
- _errors = []
132
- # log.debug("get attributes {} {}".format(repr(item_instance), item_instance))
133
- for name in dir(item_instance):
134
- if name.startswith("__") and not name in self.modules:
135
- continue
136
- # log.debug("get attribute {}".format(name))
137
- try:
138
- val = getattr(item_instance, name)
139
- # name , item_repr(value) , type as text, item_instance, order
140
- # log.debug("attribute {}:{}".format(name, val))
141
- try:
142
- type_text = repr(type(val)).split("'")[1]
143
- except IndexError:
144
- type_text = ""
145
- if type_text in {"int", "float", "str", "bool", "tuple", "list", "dict"}:
146
- order = 1
147
- elif type_text in {"function", "method"}:
148
- order = 2
149
- elif type_text in ("class"):
150
- order = 3
151
- else:
152
- order = 4
153
- _result.append((name, repr(val), repr(type(val)), val, order))
154
- except AttributeError as e:
155
- _errors.append("Couldn't get attribute '{}' from object '{}', Err: {}".format(name, item_instance, e))
156
- except MemoryError as e:
157
- print("MemoryError: {}".format(e))
158
- sleep(1)
159
- reset()
160
-
161
- # remove internal __
162
- # _result = sorted([i for i in _result if not (i[0].startswith("_"))], key=lambda x: x[4])
163
- _result = sorted([i for i in _result if not (i[0].startswith("__"))], key=lambda x: x[4])
164
- gc.collect()
165
- return _result, _errors
166
-
167
- def add_modules(self, modules):
168
- "Add additional modules to be exported"
169
- self.modules = sorted(set(self.modules) | set(modules))
170
-
171
- def create_all_stubs(self):
172
- "Create stubs for all configured modules"
173
- log.info("Start micropython-stubber {} on {}".format(__version__, self._fwid))
174
- self.report_start()
175
- gc.collect()
176
- for module_name in self.modules:
177
- self.create_one_stub(module_name)
178
- self.report_end()
179
- log.info("Finally done")
180
-
181
- def create_one_stub(self, module_name: str):
182
- if module_name in self.problematic:
183
- log.warning("Skip module: {:<25} : Known problematic".format(module_name))
184
- return False
185
- if module_name in self.excluded:
186
- log.warning("Skip module: {:<25} : Excluded".format(module_name))
187
- return False
188
-
189
- file_name = "{}/{}.pyi".format(self.path, module_name.replace(".", "/"))
190
- gc.collect()
191
- result = False
192
- try:
193
- result = self.create_module_stub(module_name, file_name)
194
- except OSError:
195
- return False
196
- gc.collect()
197
- return result
198
-
199
- def create_module_stub(self, module_name: str, file_name: str = None) -> bool: # type: ignore
200
- """Create a Stub of a single python module
201
-
202
- Args:
203
- - module_name (str): name of the module to document. This module will be imported.
204
- - file_name (Optional[str]): the 'path/filename.pyi' to write to. If omitted will be created based on the module name.
205
- """
206
- if file_name is None:
207
- fname = module_name.replace(".", "_") + ".pyi"
208
- file_name = self.path + "/" + fname
209
- else:
210
- fname = file_name.split("/")[-1]
211
-
212
- if "/" in module_name:
213
- # for nested modules
214
- module_name = module_name.replace("/", ".")
215
-
216
- # import the module (as new_module) to examine it
217
- new_module = None
218
- try:
219
- new_module = __import__(module_name, None, None, ("*"))
220
- m1 = gc.mem_free() # type: ignore
221
- log.info("Stub module: {:<25} to file: {:<70} mem:{:>5}".format(module_name, fname, m1))
222
-
223
- except ImportError:
224
- # log.debug("Skip module: {:<25} {:<79}".format(module_name, "Module not found."))
225
- return False
226
-
227
- # Start a new file
228
- ensure_folder(file_name)
229
- with open(file_name, "w") as fp:
230
- info_ = str(self.info).replace("OrderedDict(", "").replace("})", "}")
231
- s = '"""\nModule: \'{0}\' on {1}\n"""\n# MCU: {2}\n# Stubber: {3}\n'.format(
232
- module_name, self._fwid, info_, __version__
233
- )
234
- fp.write(s)
235
- fp.write(
236
- "from __future__ import annotations\nfrom typing import Any, Generator\nfrom _typeshed import Incomplete\n\n"
237
- )
238
- self.write_object_stub(fp, new_module, module_name, "")
239
-
240
- self.report_add(module_name, file_name)
241
-
242
- if module_name not in {"os", "sys", "logging", "gc"}:
243
- # try to unload the module unless we use it
244
- try:
245
- del new_module
246
- except (OSError, KeyError): # lgtm [py/unreachable-statement]
247
- log.warning("could not del new_module")
248
- # do not try to delete from sys.modules - most times it does not work anyway
249
- gc.collect()
250
- return True
251
-
252
- def write_object_stub(self, fp, object_expr: object, obj_name: str, indent: str, in_class: int = 0):
253
- "Write a module/object stub to an open file. Can be called recursive."
254
- gc.collect()
255
- if object_expr in self.problematic:
256
- log.warning("SKIPPING problematic module:{}".format(object_expr))
257
- return
258
-
259
- # # log.debug("DUMP : {}".format(object_expr))
260
- items, errors = self.get_obj_attributes(object_expr)
261
-
262
- if errors:
263
- log.error(errors)
264
-
265
- for item_name, item_repr, item_type_txt, item_instance, _ in items:
266
- # name_, repr_(value), type as text, item_instance, order
267
- if item_name in ["classmethod", "staticmethod", "BaseException", "Exception"]:
268
- # do not create stubs for these primitives
269
- continue
270
- if item_name[0].isdigit():
271
- log.warning("NameError: invalid name {}".format(item_name))
272
- continue
273
- # Class expansion only on first 3 levels (bit of a hack)
274
- if (
275
- item_type_txt == "<class 'type'>"
276
- and len(indent) <= _MAX_CLASS_LEVEL * 4
277
- # and not obj_name.endswith(".Pin")
278
- # avoid expansion of Pin.cpu / Pin.board to avoid crashes on most platforms
279
- ):
280
- # log.debug("{0}class {1}:".format(indent, item_name))
281
- superclass = ""
282
- is_exception = (
283
- item_name.endswith("Exception")
284
- or item_name.endswith("Error")
285
- or item_name
286
- in [
287
- "KeyboardInterrupt",
288
- "StopIteration",
289
- "SystemExit",
290
- ]
291
- )
292
- if is_exception:
293
- superclass = "Exception"
294
- s = "\n{}class {}({}):\n".format(indent, item_name, superclass)
295
- # s += indent + " ''\n"
296
- if is_exception:
297
- s += indent + " ...\n"
298
- fp.write(s)
299
- continue
300
- # write classdef
301
- fp.write(s)
302
- # first write the class literals and methods
303
- # log.debug("# recursion over class {0}".format(item_name))
304
- self.write_object_stub(
305
- fp,
306
- item_instance,
307
- "{0}.{1}".format(obj_name, item_name),
308
- indent + " ",
309
- in_class + 1,
310
- )
311
- # end with the __init__ method to make sure that the literals are defined
312
- # Add __init__
313
- s = indent + " def __init__(self, *argv, **kwargs) -> None:\n"
314
- s += indent + " ...\n\n"
315
- fp.write(s)
316
- elif any(word in item_type_txt for word in ["method", "function", "closure"]):
317
- # log.debug("# def {1} function/method/closure, type = '{0}'".format(item_type_txt, item_name))
318
- # module Function or class method
319
- # will accept any number of params
320
- # return type Any/Incomplete
321
- ret = "Incomplete"
322
- first = ""
323
- # Self parameter only on class methods/functions
324
- if in_class > 0:
325
- first = "self, "
326
- # class method - add function decoration
327
- if "bound_method" in item_type_txt or "bound_method" in item_repr:
328
- s = "{}@classmethod\n".format(indent) + "{}def {}(cls, *args, **kwargs) -> {}:\n".format(
329
- indent, item_name, ret
330
- )
331
- else:
332
- s = "{}def {}({}*args, **kwargs) -> {}:\n".format(indent, item_name, first, ret)
333
- s += indent + " ...\n\n"
334
- fp.write(s)
335
- # log.debug("\n" + s)
336
- elif item_type_txt == "<class 'module'>":
337
- # Skip imported modules
338
- # fp.write("# import {}\n".format(item_name))
339
- pass
340
-
341
- elif item_type_txt.startswith("<class '"):
342
- t = item_type_txt[8:-2]
343
- s = ""
344
-
345
- if t in ("str", "int", "float", "bool", "bytearray", "bytes"):
346
- # known type: use actual value
347
- # s = "{0}{1} = {2} # type: {3}\n".format(indent, item_name, item_repr, t)
348
- s = "{0}{1}: {3} = {2}\n".format(indent, item_name, item_repr, t)
349
- elif t in ("dict", "list", "tuple"):
350
- # dict, list , tuple: use empty value
351
- ev = {"dict": "{}", "list": "[]", "tuple": "()"}
352
- # s = "{0}{1} = {2} # type: {3}\n".format(indent, item_name, ev[t], t)
353
- s = "{0}{1}: {3} = {2}\n".format(indent, item_name, ev[t], t)
354
- else:
355
- # something else
356
- if t in ("object", "set", "frozenset", "Pin", "generator"): # "FileIO"
357
- # https://docs.python.org/3/tutorial/classes.html#item_instance-objects
358
- # use these types for the attribute
359
- if t == "generator":
360
- t = "Generator"
361
- s = "{0}{1}: {2} ## = {4}\n".format(indent, item_name, t, item_type_txt, item_repr)
362
- else:
363
- # Requires Python 3.6 syntax, which is OK for the stubs/pyi
364
- t = "Incomplete"
365
- if " at " in item_repr:
366
- item_repr = item_repr.split(" at ")[0] + " at ...>"
367
- if " at " in item_repr:
368
- item_repr = item_repr.split(" at ")[0] + " at ...>"
369
- s = "{0}{1}: {2} ## {3} = {4}\n".format(indent, item_name, t, item_type_txt, item_repr)
370
- fp.write(s)
371
- # log.debug("\n" + s)
372
- else:
373
- # keep only the name
374
- # log.debug("# all other, type = '{0}'".format(item_type_txt))
375
- fp.write("# all other, type = '{0}'\n".format(item_type_txt))
376
-
377
- fp.write(indent + item_name + " # type: Incomplete\n")
378
-
379
- # del items
380
- # del errors
381
- # try:
382
- # del item_name, item_repr, item_type_txt, item_instance # type: ignore
383
- # except (OSError, KeyError, NameError):
384
- # pass
385
-
386
- @property
387
- def flat_fwid(self):
388
- "Turn _fwid from 'v1.2.3' into '1_2_3' to be used in filename"
389
- s = self._fwid
390
- # path name restrictions
391
- chars = " .()/\\:$"
392
- for c in chars:
393
- s = s.replace(c, "_")
394
- return s
395
-
396
- def clean(self, path: str = None): # type: ignore
397
- "Remove all files from the stub folder"
398
- if path is None:
399
- path = self.path
400
- log.info("Clean/remove files in folder: {}".format(path))
401
- try:
402
- os.stat(path) # TEMP workaround mpremote listdir bug -
403
- items = os.listdir(path)
404
- except (OSError, AttributeError):
405
- # os.listdir fails on unix
406
- return
407
- for fn in items:
408
- item = "{}/{}".format(path, fn)
409
- try:
410
- os.remove(item)
411
- except OSError:
412
- try: # folder
413
- self.clean(item)
414
- os.rmdir(item)
415
- except OSError:
416
- pass
417
-
418
- def report_start(self, filename: str = "modules.json"):
419
- """Start a report of the modules that have been stubbed
420
- "create json with list of exported modules"""
421
- self._json_name = "{}/{}".format(self.path, filename)
422
- self._json_first = True
423
- ensure_folder(self._json_name)
424
- log.info("Report file: {}".format(self._json_name))
425
- gc.collect()
426
- try:
427
- # write json by node to reduce memory requirements
428
- with open(self._json_name, "w") as f:
429
- f.write("{")
430
- f.write(dumps({"firmware": self.info})[1:-1])
431
- f.write(",\n")
432
- f.write(dumps({"stubber": {"version": __version__}, "stubtype": "firmware"})[1:-1])
433
- f.write(",\n")
434
- f.write('"modules" :[\n')
435
-
436
- except OSError as e:
437
- log.error("Failed to create the report.")
438
- self._json_name = None
439
- raise e
440
-
441
- def report_add(self, module_name: str, stub_file: str):
442
- "Add a module to the report"
443
- # write json by node to reduce memory requirements
444
- if not self._json_name:
445
- raise Exception("No report file")
446
- try:
447
- with open(self._json_name, "a") as f:
448
- if not self._json_first:
449
- f.write(",\n")
450
- else:
451
- self._json_first = False
452
- line = '{{"module": "{}", "file": "{}"}}'.format(module_name, stub_file.replace("\\", "/"))
453
- f.write(line)
454
-
455
- except OSError:
456
- log.error("Failed to create the report.")
457
-
458
- def report_end(self):
459
- if not self._json_name:
460
- raise Exception("No report file")
461
- with open(self._json_name, "a") as f:
462
- f.write("\n]}")
463
- # is used as sucess indicator
464
- log.info("Path: {}".format(self.path))
465
-
466
-
467
- def ensure_folder(path: str):
468
- "Create nested folders if needed"
469
- i = start = 0
470
- while i != -1:
471
- i = path.find("/", start)
472
- if i != -1:
473
- p = path[0] if i == 0 else path[:i]
474
- # p = partial folder
475
- try:
476
- _ = os.stat(p)
477
- except OSError as e:
478
- # folder does not exist
479
- if e.args[0] == ENOENT:
480
- try:
481
- os.mkdir(p)
482
- except OSError as e2:
483
- log.error("failed to create folder {}".format(p))
484
- raise e2
485
- # next level deep
486
- start = i + 1
487
-
488
-
489
- def _build(s):
490
- # extract build from sys.version or os.uname().version if available
491
- # sys.version: 'MicroPython v1.23.1-preview.6.g3d0b6276f'
492
- # sys.implementation.version: 'v1.13-103-gb137d064e'
493
- if not s:
494
- return ""
495
- s = s.split(" on ", 1)[0] if " on " in s else s
496
- if s.startswith("v"):
497
- if not "-" in s:
498
- return ""
499
- b = s.split("-")[1]
500
- return b
501
- if not "-preview" in s:
502
- return ""
503
- b = s.split("-preview")[1].split(".")[1]
504
- return b
505
-
506
-
507
- def _info(): # type:() -> dict[str, str]
508
- try:
509
- fam = sys.implementation[0] # type: ignore
510
- except TypeError:
511
- # testing on CPython 3.11
512
- fam = sys.implementation.name
513
-
514
- info = OrderedDict(
515
- {
516
- "family": fam,
517
- "version": "",
518
- "build": "",
519
- "ver": "",
520
- "port": sys.platform, # port: esp32 / win32 / linux / stm32
521
- "board": "UNKNOWN",
522
- "cpu": "",
523
- "mpy": "",
524
- "arch": "",
525
- }
526
- )
527
- # change port names to be consistent with the repo
528
- if info["port"].startswith("pyb"):
529
- info["port"] = "stm32"
530
- elif info["port"] == "win32":
531
- info["port"] = "windows"
532
- elif info["port"] == "linux":
533
- info["port"] = "unix"
534
- try:
535
- info["version"] = version_str(sys.implementation.version) # type: ignore
536
- except AttributeError:
537
- pass
538
- try:
539
- _machine = (
540
- sys.implementation._machine if "_machine" in dir(sys.implementation) else os.uname().machine # type: ignore
541
- )
542
- # info["board"] = "with".join(_machine.split("with")[:-1]).strip()
543
- info["board"] = _machine
544
- info["cpu"] = _machine.split("with")[-1].strip()
545
- info["mpy"] = (
546
- sys.implementation._mpy # type: ignore
547
- if "_mpy" in dir(sys.implementation)
548
- else sys.implementation.mpy if "mpy" in dir(sys.implementation) else "" # type: ignore
549
- )
550
- except (AttributeError, IndexError):
551
- pass
552
- info["board"] = get_boardname()
553
-
554
- try:
555
- if "uname" in dir(os): # old
556
- # extract build from uname().version if available
557
- info["build"] = _build(os.uname()[3]) # type: ignore
558
- if not info["build"]:
559
- # extract build from uname().release if available
560
- info["build"] = _build(os.uname()[2]) # type: ignore
561
- elif "version" in dir(sys): # new
562
- # extract build from sys.version if available
563
- info["build"] = _build(sys.version)
564
- except (AttributeError, IndexError, TypeError):
565
- pass
566
- # avoid build hashes
567
- # if info["build"] and len(info["build"]) > 5:
568
- # info["build"] = ""
569
-
570
- if info["version"] == "" and sys.platform not in ("unix", "win32"):
571
- try:
572
- u = os.uname() # type: ignore
573
- info["version"] = u.release
574
- except (IndexError, AttributeError, TypeError):
575
- pass
576
- # detect families
577
- for fam_name, mod_name, mod_thing in [
578
- ("pycopy", "pycopy", "const"),
579
- ("pycom", "pycom", "FAT"),
580
- ("ev3-pybricks", "pybricks.hubs", "EV3Brick"),
581
- ]:
582
- try:
583
- _t = __import__(mod_name, None, None, (mod_thing))
584
- info["family"] = fam_name
585
- del _t
586
- break
587
- except (ImportError, KeyError):
588
- pass
589
-
590
- if info["family"] == "ev3-pybricks":
591
- info["release"] = "2.0.0"
592
-
593
- if info["family"] == "micropython":
594
- info["version"]
595
- if (
596
- info["version"]
597
- and info["version"].endswith(".0")
598
- and info["version"] >= "1.10.0" # versions from 1.10.0 to 1.23.1 do not have a micro .0
599
- and info["version"] <= "1.19.9"
600
- ):
601
- # versions from 1.10.0 to 1.23.1 do not have a micro .0
602
- info["version"] = info["version"][:-2]
603
-
604
- # spell-checker: disable
605
- if "mpy" in info and info["mpy"]: # mpy on some v1.11+ builds
606
- sys_mpy = int(info["mpy"])
607
- # .mpy architecture
608
- arch = [
609
- None,
610
- "x86",
611
- "x64",
612
- "armv6",
613
- "armv6m",
614
- "armv7m",
615
- "armv7em",
616
- "armv7emsp",
617
- "armv7emdp",
618
- "xtensa",
619
- "xtensawin",
620
- ][sys_mpy >> 10]
621
- if arch:
622
- info["arch"] = arch
623
- # .mpy version.minor
624
- info["mpy"] = "v{}.{}".format(sys_mpy & 0xFF, sys_mpy >> 8 & 3)
625
- if info["build"] and not info["version"].endswith("-preview"):
626
- info["version"] = info["version"] + "-preview"
627
- # simple to use version[-build] string
628
- info["ver"] = f"{info['version']}-{info['build']}" if info["build"] else f"{info['version']}"
629
-
630
- return info
631
-
632
-
633
- def version_str(version: tuple): # -> str:
634
- v_str = ".".join([str(n) for n in version[:3]])
635
- if len(version) > 3 and version[3]:
636
- v_str += "-" + version[3]
637
- return v_str
638
-
639
-
640
- def get_boardname() -> str:
641
- "Read the board name from the boardname.py file that may have been created upfront"
642
- try:
643
- from boardname import BOARDNAME # type: ignore
644
-
645
- log.info("Found BOARDNAME: {}".format(BOARDNAME))
646
- except ImportError:
647
- log.warning("BOARDNAME not found")
648
- BOARDNAME = ""
649
- return BOARDNAME
650
-
651
-
652
- def get_root() -> str: # sourcery skip: use-assigned-variable
653
- "Determine the root folder of the device"
654
- try:
655
- c = os.getcwd()
656
- except (OSError, AttributeError):
657
- # unix port
658
- c = "."
659
- r = c
660
- for r in [c, "/sd", "/flash", "/", "."]:
661
- try:
662
- _ = os.stat(r)
663
- break
664
- except OSError:
665
- continue
666
- return r
667
-
668
-
669
- def file_exists(filename: str):
670
- try:
671
- if os.stat(filename)[0] >> 14:
672
- return True
673
- return False
674
- except OSError:
675
- return False
676
-
677
-
678
- def show_help():
679
- print("-p, --path path to store the stubs in, defaults to '.'")
680
- sys.exit(1)
681
-
682
-
683
- def read_path() -> str:
684
- "get --path from cmdline. [unix/win]"
685
- path = ""
686
- if len(sys.argv) == 3:
687
- cmd = (sys.argv[1]).lower()
688
- if cmd in ("--path", "-p"):
689
- path = sys.argv[2]
690
- else:
691
- show_help()
692
- elif len(sys.argv) == 2:
693
- show_help()
694
- return path
695
-
696
-
697
- def is_micropython() -> bool:
698
- "runtime test to determine full or micropython"
699
- # pylint: disable=unused-variable,eval-used
700
- try:
701
- # either test should fail on micropython
702
-
703
- # b) https://docs.micropython.org/en/latest/genrst/builtin_types.html#bytes-with-keywords-not-implemented
704
- # Micropython: NotImplementedError
705
- b = bytes("abc", encoding="utf8") # type: ignore # lgtm [py/unused-local-variable]
706
-
707
- # c) https://docs.micropython.org/en/latest/genrst/core_language.html#function-objects-do-not-have-the-module-attribute
708
- # Micropython: AttributeError
709
- c = is_micropython.__module__ # type: ignore # lgtm [py/unused-local-variable]
710
- return False
711
- except (NotImplementedError, AttributeError):
712
- return True
713
-
714
-
715
- def main():
716
- stubber = Stubber(path=read_path())
717
- # stubber = Stubber(path="/sd")
718
- # Option: Specify a firmware name & version
719
- # stubber = Stubber(firmware_id='HoverBot v1.2.1')
720
- stubber.clean()
721
- # there is no option to discover modules from micropython, need to hardcode
722
- # below contains combined modules from Micropython ESP8622, ESP32, Loboris, Pycom and ulab , lvgl
723
- # spell-checker: disable
724
- # modules to stub : 131
725
- stubber.modules = [
726
- "WM8960",
727
- "_OTA",
728
- "_asyncio",
729
- "_boot_fat",
730
- "_coap",
731
- "_espnow",
732
- "_flash_control_OTA",
733
- "_main_pybytes",
734
- "_mqtt",
735
- "_mqtt_core",
736
- "_msg_handl",
737
- "_onewire",
738
- "_periodical_pin",
739
- "_pybytes",
740
- "_pybytes_ca",
741
- "_pybytes_config",
742
- "_pybytes_config_reader",
743
- "_pybytes_connection",
744
- "_pybytes_constants",
745
- "_pybytes_debug",
746
- "_pybytes_library",
747
- "_pybytes_machine_learning",
748
- "_pybytes_main",
749
- "_pybytes_protocol",
750
- "_pybytes_pyconfig",
751
- "_pybytes_pymesh_config",
752
- "_rp2",
753
- "_terminal",
754
- "_thread",
755
- "_uasyncio",
756
- "_urequest",
757
- "adcfft",
758
- "aioble/__init__",
759
- "aioble/central",
760
- "aioble/client",
761
- "aioble/core",
762
- "aioble/device",
763
- "aioble/l2cap",
764
- "aioble/peripheral",
765
- "aioble/security",
766
- "aioble/server",
767
- "aioespnow",
768
- "ak8963",
769
- "apa102",
770
- "apa106",
771
- "argparse",
772
- "array",
773
- "asyncio/__init__",
774
- "asyncio/core",
775
- "asyncio/event",
776
- "asyncio/funcs",
777
- "asyncio/lock",
778
- "asyncio/stream",
779
- "binascii",
780
- "bluetooth",
781
- "breakout_as7262",
782
- "breakout_bh1745",
783
- "breakout_bme280",
784
- "breakout_bme68x",
785
- "breakout_bmp280",
786
- "breakout_dotmatrix",
787
- "breakout_encoder",
788
- "breakout_icp10125",
789
- "breakout_ioexpander",
790
- "breakout_ltr559",
791
- "breakout_matrix11x7",
792
- "breakout_mics6814",
793
- "breakout_msa301",
794
- "breakout_paa5100",
795
- "breakout_pmw3901",
796
- "breakout_potentiometer",
797
- "breakout_rgbmatrix5x5",
798
- "breakout_rtc",
799
- "breakout_scd41",
800
- "breakout_sgp30",
801
- "breakout_trackball",
802
- "breakout_vl53l5cx",
803
- "btree",
804
- "cmath",
805
- "collections",
806
- "crypto",
807
- "cryptolib",
808
- "curl",
809
- "deflate",
810
- "dht",
811
- "display",
812
- "display_driver_utils",
813
- "ds18x20",
814
- "encoder",
815
- "errno",
816
- "esp",
817
- "esp32",
818
- "espidf",
819
- "espnow",
820
- "ffi",
821
- "flashbdev",
822
- "framebuf",
823
- "freesans20",
824
- "fs_driver",
825
- "functools",
826
- "galactic",
827
- "gc",
828
- "gfx_pack",
829
- "gsm",
830
- "hashlib",
831
- "heapq",
832
- "hub75",
833
- "ili9341",
834
- "ili9XXX",
835
- "imagetools",
836
- "inisetup",
837
- "interstate75",
838
- "io",
839
- "jpegdec",
840
- "js",
841
- "jsffi",
842
- "json",
843
- "lcd160cr",
844
- "lodepng",
845
- "logging",
846
- "lsm6dsox",
847
- "lv_colors",
848
- "lv_utils",
849
- "lvgl",
850
- "lwip",
851
- "machine",
852
- "math",
853
- "microWebSocket",
854
- "microWebSrv",
855
- "microWebTemplate",
856
- "micropython",
857
- "mip",
858
- "mip/__init__",
859
- "mip/__main__",
860
- "motor",
861
- "mpu6500",
862
- "mpu9250",
863
- "neopixel",
864
- "network",
865
- "ntptime",
866
- "onewire",
867
- "openamp",
868
- "os",
869
- "pcf85063a",
870
- "picoexplorer",
871
- "picographics",
872
- "picokeypad",
873
- "picoscroll",
874
- "picounicorn",
875
- "picowireless",
876
- "pimoroni",
877
- "pimoroni_bus",
878
- "pimoroni_i2c",
879
- "plasma",
880
- "platform",
881
- "pyb",
882
- "pycom",
883
- "pye",
884
- "qrcode",
885
- "queue",
886
- "random",
887
- "requests",
888
- "requests/__init__",
889
- "rp2",
890
- "rtch",
891
- "samd",
892
- "select",
893
- "servo",
894
- "socket",
895
- "ssd1306",
896
- "ssh",
897
- "ssl",
898
- "stm",
899
- "struct",
900
- "sys",
901
- "termios",
902
- "time",
903
- "tls",
904
- "tpcalib",
905
- "uarray",
906
- "uasyncio/__init__",
907
- "uasyncio/core",
908
- "uasyncio/event",
909
- "uasyncio/funcs",
910
- "uasyncio/lock",
911
- "uasyncio/stream",
912
- "uasyncio/tasks",
913
- "ubinascii",
914
- "ubluetooth",
915
- "ucollections",
916
- "ucrypto",
917
- "ucryptolib",
918
- "uctypes",
919
- "uerrno",
920
- "uftpd",
921
- "uhashlib",
922
- "uheapq",
923
- "uio",
924
- "ujson",
925
- "ulab",
926
- "ulab/approx",
927
- "ulab/compare",
928
- "ulab/fft",
929
- "ulab/filter",
930
- "ulab/linalg",
931
- "ulab/numerical",
932
- "ulab/poly",
933
- "ulab/user",
934
- "ulab/vector",
935
- "umachine",
936
- "umqtt/__init__",
937
- "umqtt/robust",
938
- "umqtt/simple",
939
- "uos",
940
- "uplatform",
941
- "uqueue",
942
- "urandom",
943
- "ure",
944
- "urequests",
945
- "urllib/urequest",
946
- "usb/device",
947
- "usb/device/cdc",
948
- "usb/device/hid",
949
- "usb/device/keyboard",
950
- "usb/device/midi",
951
- "usb/device/mouse",
952
- "uselect",
953
- "usocket",
954
- "ussl",
955
- "ustruct",
956
- "usys",
957
- "utelnetserver",
958
- "utime",
959
- "utimeq",
960
- "uwebsocket",
961
- "uzlib",
962
- "version",
963
- "vfs",
964
- "websocket",
965
- "websocket_helper",
966
- "wipy",
967
- "writer",
968
- "xpt2046",
969
- "ymodem",
970
- "zephyr",
971
- "zlib",
972
- ] # spell-checker: enable
973
-
974
- gc.collect()
975
-
976
- stubber.create_all_stubs()
977
-
978
-
979
- if __name__ == "__main__" or is_micropython():
980
- if not file_exists("no_auto_stubber.txt"):
981
- try:
982
- gc.threshold(4 * 1024) # type: ignore
983
- gc.enable()
984
- except BaseException:
985
- pass
986
- main()
1
+ """
2
+ Create stubs for (all) modules on a MicroPython board
3
+ """
4
+
5
+ # Copyright (c) 2019-2024 Jos Verlinde
6
+
7
+ import gc
8
+ import os
9
+ import sys
10
+ from time import sleep
11
+
12
+ try:
13
+ from ujson import dumps
14
+ except:
15
+ from json import dumps
16
+
17
+ try:
18
+ from machine import reset # type: ignore
19
+ except ImportError:
20
+ pass
21
+
22
+ try:
23
+ from collections import OrderedDict
24
+ except ImportError:
25
+ from ucollections import OrderedDict # type: ignore
26
+
27
+ __version__ = "v1.23.2"
28
+ version_str = __version__.rsplit(".", 1)[0]
29
+ ENOENT = 2
30
+ _MAX_CLASS_LEVEL = 2 # Max class nesting
31
+ LIBS = ["lib", "/lib", "/sd/lib", "/flash/lib", "."]
32
+
33
+
34
+ # our own logging module to avoid dependency on and interfering with logging module
35
+ class logging:
36
+ # DEBUG = 10
37
+ INFO = 20
38
+ WARNING = 30
39
+ ERROR = 40
40
+ level = INFO
41
+ prnt = print
42
+
43
+ @staticmethod
44
+ def getLogger(name):
45
+ return logging()
46
+
47
+ @classmethod
48
+ def basicConfig(cls, level):
49
+ cls.level = level
50
+
51
+ # def debug(self, msg):
52
+ # if self.level <= logging.DEBUG:
53
+ # self.prnt("DEBUG :", msg)
54
+
55
+ def info(self, msg):
56
+ if self.level <= logging.INFO:
57
+ self.prnt("INFO :", msg)
58
+
59
+ def warning(self, msg):
60
+ if self.level <= logging.WARNING:
61
+ self.prnt("WARN :", msg)
62
+
63
+ def error(self, msg):
64
+ if self.level <= logging.ERROR:
65
+ self.prnt("ERROR :", msg)
66
+
67
+
68
+ log = logging.getLogger("stubber")
69
+ logging.basicConfig(level=logging.INFO)
70
+ # logging.basicConfig(level=logging.DEBUG)
71
+
72
+
73
+ class Stubber:
74
+ "Generate stubs for modules in firmware"
75
+
76
+ def __init__(self, path: str = None, firmware_id: str = None): # type: ignore
77
+ try:
78
+ if os.uname().release == "1.13.0" and os.uname().version < "v1.13-103": # type: ignore
79
+ raise NotImplementedError("MicroPython 1.13.0 cannot be stubbed")
80
+ except AttributeError:
81
+ pass # Allow testing on CPython 3.11
82
+ self.info = _info()
83
+ log.info("Port: {}".format(self.info["port"]))
84
+ log.info("Board: {}".format(self.info["board"]))
85
+ gc.collect()
86
+ if firmware_id:
87
+ self._fwid = firmware_id.lower()
88
+ else:
89
+ if self.info["family"] == "micropython":
90
+ self._fwid = "{family}-v{version}-{port}-{board}".format(**self.info).rstrip("-")
91
+ else:
92
+ self._fwid = "{family}-v{version}-{port}".format(**self.info)
93
+ self._start_free = gc.mem_free() # type: ignore
94
+
95
+ if path:
96
+ if path.endswith("/"):
97
+ path = path[:-1]
98
+ else:
99
+ path = get_root()
100
+
101
+ self.path = "{}/stubs/{}".format(path, self.flat_fwid).replace("//", "/")
102
+ # log.debug(self.path)
103
+ try:
104
+ ensure_folder(path + "/")
105
+ except OSError:
106
+ log.error("error creating stub folder {}".format(path))
107
+ self.problematic = [
108
+ "upip",
109
+ "upysh",
110
+ "webrepl_setup",
111
+ "http_client",
112
+ "http_client_ssl",
113
+ "http_server",
114
+ "http_server_ssl",
115
+ ]
116
+ self.excluded = [
117
+ "webrepl",
118
+ "_webrepl",
119
+ "port_diag",
120
+ "example_sub_led.py",
121
+ "example_pub_button.py",
122
+ ]
123
+ # there is no option to discover modules from micropython, list is read from an external file.
124
+ self.modules = [] # type: list[str]
125
+ self._json_name = None
126
+ self._json_first = False
127
+
128
+ def get_obj_attributes(self, item_instance: object):
129
+ "extract information of the objects members and attributes"
130
+ # name_, repr_(value), type as text, item_instance
131
+ _result = []
132
+ _errors = []
133
+ # log.debug("get attributes {} {}".format(repr(item_instance), item_instance))
134
+ for name in dir(item_instance):
135
+ if name.startswith("__") and not name in self.modules:
136
+ continue
137
+ # log.debug("get attribute {}".format(name))
138
+ try:
139
+ val = getattr(item_instance, name)
140
+ # name , item_repr(value) , type as text, item_instance, order
141
+ # log.debug("attribute {}:{}".format(name, val))
142
+ try:
143
+ type_text = repr(type(val)).split("'")[1]
144
+ except IndexError:
145
+ type_text = ""
146
+ if type_text in {"int", "float", "str", "bool", "tuple", "list", "dict"}:
147
+ order = 1
148
+ elif type_text in {"function", "method"}:
149
+ order = 2
150
+ elif type_text in ("class"):
151
+ order = 3
152
+ else:
153
+ order = 4
154
+ _result.append((name, repr(val), repr(type(val)), val, order))
155
+ except AttributeError as e:
156
+ _errors.append(
157
+ "Couldn't get attribute '{}' from object '{}', Err: {}".format(
158
+ name, item_instance, e
159
+ )
160
+ )
161
+ except MemoryError as e:
162
+ print("MemoryError: {}".format(e))
163
+ sleep(1)
164
+ reset()
165
+
166
+ # remove internal __
167
+ # _result = sorted([i for i in _result if not (i[0].startswith("_"))], key=lambda x: x[4])
168
+ _result = sorted([i for i in _result if not (i[0].startswith("__"))], key=lambda x: x[4])
169
+ gc.collect()
170
+ return _result, _errors
171
+
172
+ def add_modules(self, modules):
173
+ "Add additional modules to be exported"
174
+ self.modules = sorted(set(self.modules) | set(modules))
175
+
176
+ def create_all_stubs(self):
177
+ "Create stubs for all configured modules"
178
+ log.info("Start micropython-stubber {} on {}".format(__version__, self._fwid))
179
+ self.report_start()
180
+ gc.collect()
181
+ for module_name in self.modules:
182
+ self.create_one_stub(module_name)
183
+ self.report_end()
184
+ log.info("Finally done")
185
+
186
+ def create_one_stub(self, module_name: str):
187
+ if module_name in self.problematic:
188
+ log.warning("Skip module: {:<25} : Known problematic".format(module_name))
189
+ return False
190
+ if module_name in self.excluded:
191
+ log.warning("Skip module: {:<25} : Excluded".format(module_name))
192
+ return False
193
+
194
+ file_name = "{}/{}.pyi".format(self.path, module_name.replace(".", "/"))
195
+ gc.collect()
196
+ result = False
197
+ try:
198
+ result = self.create_module_stub(module_name, file_name)
199
+ except OSError:
200
+ return False
201
+ gc.collect()
202
+ return result
203
+
204
+ def create_module_stub(self, module_name: str, file_name: str = None) -> bool: # type: ignore
205
+ """Create a Stub of a single python module
206
+
207
+ Args:
208
+ - module_name (str): name of the module to document. This module will be imported.
209
+ - file_name (Optional[str]): the 'path/filename.pyi' to write to. If omitted will be created based on the module name.
210
+ """
211
+ if file_name is None:
212
+ fname = module_name.replace(".", "_") + ".pyi"
213
+ file_name = self.path + "/" + fname
214
+ else:
215
+ fname = file_name.split("/")[-1]
216
+
217
+ if "/" in module_name:
218
+ # for nested modules
219
+ module_name = module_name.replace("/", ".")
220
+
221
+ # import the module (as new_module) to examine it
222
+ new_module = None
223
+ try:
224
+ new_module = __import__(module_name, None, None, ("*"))
225
+ m1 = gc.mem_free() # type: ignore
226
+ log.info(
227
+ "Stub module: {:<25} to file: {:<70} mem:{:>5}".format(module_name, fname, m1)
228
+ )
229
+
230
+ except ImportError:
231
+ # log.debug("Skip module: {:<25} {:<79}".format(module_name, "Module not found."))
232
+ return False
233
+
234
+ # Start a new file
235
+ ensure_folder(file_name)
236
+ with open(file_name, "w") as fp:
237
+ info_ = str(self.info).replace("OrderedDict(", "").replace("})", "}")
238
+ s = '"""\nModule: \'{0}\' on {1}\n"""\n# MCU: {2}\n# Stubber: {3}\n'.format(
239
+ module_name, self._fwid, info_, version_str
240
+ )
241
+ fp.write(s)
242
+ fp.write(
243
+ "from __future__ import annotations\nfrom typing import Any, Generator\nfrom _typeshed import Incomplete\n\n"
244
+ )
245
+ self.write_object_stub(fp, new_module, module_name, "")
246
+
247
+ self.report_add(module_name, file_name)
248
+
249
+ if module_name not in {"os", "sys", "logging", "gc"}:
250
+ # try to unload the module unless we use it
251
+ try:
252
+ del new_module
253
+ except (OSError, KeyError): # lgtm [py/unreachable-statement]
254
+ log.warning("could not del new_module")
255
+ # do not try to delete from sys.modules - most times it does not work anyway
256
+ gc.collect()
257
+ return True
258
+
259
+ def write_object_stub(
260
+ self, fp, object_expr: object, obj_name: str, indent: str, in_class: int = 0
261
+ ):
262
+ "Write a module/object stub to an open file. Can be called recursive."
263
+ gc.collect()
264
+ if object_expr in self.problematic:
265
+ log.warning("SKIPPING problematic module:{}".format(object_expr))
266
+ return
267
+
268
+ # # log.debug("DUMP : {}".format(object_expr))
269
+ items, errors = self.get_obj_attributes(object_expr)
270
+
271
+ if errors:
272
+ log.error(errors)
273
+
274
+ for item_name, item_repr, item_type_txt, item_instance, _ in items:
275
+ # name_, repr_(value), type as text, item_instance, order
276
+ if item_name in ["classmethod", "staticmethod", "BaseException", "Exception"]:
277
+ # do not create stubs for these primitives
278
+ continue
279
+ if item_name[0].isdigit():
280
+ log.warning("NameError: invalid name {}".format(item_name))
281
+ continue
282
+ # Class expansion only on first 3 levels (bit of a hack)
283
+ if (
284
+ item_type_txt == "<class 'type'>"
285
+ and len(indent) <= _MAX_CLASS_LEVEL * 4
286
+ # and not obj_name.endswith(".Pin")
287
+ # avoid expansion of Pin.cpu / Pin.board to avoid crashes on most platforms
288
+ ):
289
+ # log.debug("{0}class {1}:".format(indent, item_name))
290
+ superclass = ""
291
+ is_exception = (
292
+ item_name.endswith("Exception")
293
+ or item_name.endswith("Error")
294
+ or item_name
295
+ in [
296
+ "KeyboardInterrupt",
297
+ "StopIteration",
298
+ "SystemExit",
299
+ ]
300
+ )
301
+ if is_exception:
302
+ superclass = "Exception"
303
+ s = "\n{}class {}({}):\n".format(indent, item_name, superclass)
304
+ # s += indent + " ''\n"
305
+ if is_exception:
306
+ s += indent + " ...\n"
307
+ fp.write(s)
308
+ continue
309
+ # write classdef
310
+ fp.write(s)
311
+ # first write the class literals and methods
312
+ # log.debug("# recursion over class {0}".format(item_name))
313
+ self.write_object_stub(
314
+ fp,
315
+ item_instance,
316
+ "{0}.{1}".format(obj_name, item_name),
317
+ indent + " ",
318
+ in_class + 1,
319
+ )
320
+ # end with the __init__ method to make sure that the literals are defined
321
+ # Add __init__
322
+ s = indent + " def __init__(self, *argv, **kwargs) -> None:\n"
323
+ s += indent + " ...\n\n"
324
+ fp.write(s)
325
+ elif any(word in item_type_txt for word in ["method", "function", "closure"]):
326
+ # log.debug("# def {1} function/method/closure, type = '{0}'".format(item_type_txt, item_name))
327
+ # module Function or class method
328
+ # will accept any number of params
329
+ # return type Any/Incomplete
330
+ ret = "Incomplete"
331
+ first = ""
332
+ # Self parameter only on class methods/functions
333
+ if in_class > 0:
334
+ first = "self, "
335
+ # class method - add function decoration
336
+ if "bound_method" in item_type_txt or "bound_method" in item_repr:
337
+ s = "{}@classmethod\n".format(
338
+ indent
339
+ ) + "{}def {}(cls, *args, **kwargs) -> {}:\n".format(indent, item_name, ret)
340
+ else:
341
+ s = "{}def {}({}*args, **kwargs) -> {}:\n".format(
342
+ indent, item_name, first, ret
343
+ )
344
+ s += indent + " ...\n\n"
345
+ fp.write(s)
346
+ # log.debug("\n" + s)
347
+ elif item_type_txt == "<class 'module'>":
348
+ # Skip imported modules
349
+ # fp.write("# import {}\n".format(item_name))
350
+ pass
351
+
352
+ elif item_type_txt.startswith("<class '"):
353
+ t = item_type_txt[8:-2]
354
+ s = ""
355
+
356
+ if t in ("str", "int", "float", "bool", "bytearray", "bytes"):
357
+ # known type: use actual value
358
+ # s = "{0}{1} = {2} # type: {3}\n".format(indent, item_name, item_repr, t)
359
+ s = "{0}{1}: {3} = {2}\n".format(indent, item_name, item_repr, t)
360
+ elif t in ("dict", "list", "tuple"):
361
+ # dict, list , tuple: use empty value
362
+ ev = {"dict": "{}", "list": "[]", "tuple": "()"}
363
+ # s = "{0}{1} = {2} # type: {3}\n".format(indent, item_name, ev[t], t)
364
+ s = "{0}{1}: {3} = {2}\n".format(indent, item_name, ev[t], t)
365
+ else:
366
+ # something else
367
+ if t in ("object", "set", "frozenset", "Pin", "generator"): # "FileIO"
368
+ # https://docs.python.org/3/tutorial/classes.html#item_instance-objects
369
+ # use these types for the attribute
370
+ if t == "generator":
371
+ t = "Generator"
372
+ s = "{0}{1}: {2} ## = {4}\n".format(
373
+ indent, item_name, t, item_type_txt, item_repr
374
+ )
375
+ else:
376
+ # Requires Python 3.6 syntax, which is OK for the stubs/pyi
377
+ t = "Incomplete"
378
+ if " at " in item_repr:
379
+ item_repr = item_repr.split(" at ")[0] + " at ...>"
380
+ if " at " in item_repr:
381
+ item_repr = item_repr.split(" at ")[0] + " at ...>"
382
+ s = "{0}{1}: {2} ## {3} = {4}\n".format(
383
+ indent, item_name, t, item_type_txt, item_repr
384
+ )
385
+ fp.write(s)
386
+ # log.debug("\n" + s)
387
+ else:
388
+ # keep only the name
389
+ # log.debug("# all other, type = '{0}'".format(item_type_txt))
390
+ fp.write("# all other, type = '{0}'\n".format(item_type_txt))
391
+
392
+ fp.write(indent + item_name + " # type: Incomplete\n")
393
+
394
+ # del items
395
+ # del errors
396
+ # try:
397
+ # del item_name, item_repr, item_type_txt, item_instance # type: ignore
398
+ # except (OSError, KeyError, NameError):
399
+ # pass
400
+
401
+ @property
402
+ def flat_fwid(self):
403
+ "Turn _fwid from 'v1.2.3' into '1_2_3' to be used in filename"
404
+ s = self._fwid
405
+ # path name restrictions
406
+ chars = " .()/\\:$"
407
+ for c in chars:
408
+ s = s.replace(c, "_")
409
+ return s
410
+
411
+ def clean(self, path: str = None): # type: ignore
412
+ "Remove all files from the stub folder"
413
+ if path is None:
414
+ path = self.path
415
+ log.info("Clean/remove files in folder: {}".format(path))
416
+ try:
417
+ os.stat(path) # TEMP workaround mpremote listdir bug -
418
+ items = os.listdir(path)
419
+ except (OSError, AttributeError):
420
+ # os.listdir fails on unix
421
+ return
422
+ for fn in items:
423
+ item = "{}/{}".format(path, fn)
424
+ try:
425
+ os.remove(item)
426
+ except OSError:
427
+ try: # folder
428
+ self.clean(item)
429
+ os.rmdir(item)
430
+ except OSError:
431
+ pass
432
+
433
+ def report_start(self, filename: str = "modules.json"):
434
+ """Start a report of the modules that have been stubbed
435
+ "create json with list of exported modules"""
436
+ self._json_name = "{}/{}".format(self.path, filename)
437
+ self._json_first = True
438
+ ensure_folder(self._json_name)
439
+ log.info("Report file: {}".format(self._json_name))
440
+ gc.collect()
441
+ try:
442
+ # write json by node to reduce memory requirements
443
+ with open(self._json_name, "w") as f:
444
+ f.write("{")
445
+ f.write(dumps({"firmware": self.info})[1:-1])
446
+ f.write(",\n")
447
+ f.write(dumps({"stubber": {"version": version_str}, "stubtype": "firmware"})[1:-1])
448
+ f.write(",\n")
449
+ f.write('"modules" :[\n')
450
+
451
+ except OSError as e:
452
+ log.error("Failed to create the report.")
453
+ self._json_name = None
454
+ raise e
455
+
456
+ def report_add(self, module_name: str, stub_file: str):
457
+ "Add a module to the report"
458
+ # write json by node to reduce memory requirements
459
+ if not self._json_name:
460
+ raise Exception("No report file")
461
+ try:
462
+ with open(self._json_name, "a") as f:
463
+ if not self._json_first:
464
+ f.write(",\n")
465
+ else:
466
+ self._json_first = False
467
+ line = '{{"module": "{}", "file": "{}"}}'.format(
468
+ module_name, stub_file.replace("\\", "/")
469
+ )
470
+ f.write(line)
471
+
472
+ except OSError:
473
+ log.error("Failed to create the report.")
474
+
475
+ def report_end(self):
476
+ if not self._json_name:
477
+ raise Exception("No report file")
478
+ with open(self._json_name, "a") as f:
479
+ f.write("\n]}")
480
+ # is used as sucess indicator
481
+ log.info("Path: {}".format(self.path))
482
+
483
+
484
+ def ensure_folder(path: str):
485
+ "Create nested folders if needed"
486
+ i = start = 0
487
+ while i != -1:
488
+ i = path.find("/", start)
489
+ if i != -1:
490
+ p = path[0] if i == 0 else path[:i]
491
+ # p = partial folder
492
+ try:
493
+ _ = os.stat(p)
494
+ except OSError as e:
495
+ # folder does not exist
496
+ if e.args[0] == ENOENT:
497
+ try:
498
+ os.mkdir(p)
499
+ except OSError as e2:
500
+ log.error("failed to create folder {}".format(p))
501
+ raise e2
502
+ # next level deep
503
+ start = i + 1
504
+
505
+
506
+ def _build(s):
507
+ # extract build from sys.version or os.uname().version if available
508
+ # sys.version: 'MicroPython v1.23.2-preview.6.g3d0b6276f'
509
+ # sys.implementation.version: 'v1.13-103-gb137d064e'
510
+ if not s:
511
+ return ""
512
+ s = s.split(" on ", 1)[0] if " on " in s else s
513
+ if s.startswith("v"):
514
+ if not "-" in s:
515
+ return ""
516
+ b = s.split("-")[1]
517
+ return b
518
+ if not "-preview" in s:
519
+ return ""
520
+ b = s.split("-preview")[1].split(".")[1]
521
+ return b
522
+
523
+
524
+ def _info(): # type:() -> dict[str, str]
525
+ try:
526
+ fam = sys.implementation[0] # type: ignore
527
+ except TypeError:
528
+ # testing on CPython 3.11
529
+ fam = sys.implementation.name
530
+
531
+ info = OrderedDict(
532
+ {
533
+ "family": fam,
534
+ "version": "",
535
+ "build": "",
536
+ "ver": "",
537
+ "port": sys.platform, # port: esp32 / win32 / linux / stm32
538
+ "board": "UNKNOWN",
539
+ "cpu": "",
540
+ "mpy": "",
541
+ "arch": "",
542
+ }
543
+ )
544
+ # change port names to be consistent with the repo
545
+ if info["port"].startswith("pyb"):
546
+ info["port"] = "stm32"
547
+ elif info["port"] == "win32":
548
+ info["port"] = "windows"
549
+ elif info["port"] == "linux":
550
+ info["port"] = "unix"
551
+ try:
552
+ info["version"] = version_str(sys.implementation.version) # type: ignore
553
+ except AttributeError:
554
+ pass
555
+ try:
556
+ _machine = (
557
+ sys.implementation._machine if "_machine" in dir(sys.implementation) else os.uname().machine # type: ignore
558
+ )
559
+ # info["board"] = "with".join(_machine.split("with")[:-1]).strip()
560
+ info["board"] = _machine
561
+ info["cpu"] = _machine.split("with")[-1].strip()
562
+ info["mpy"] = (
563
+ sys.implementation._mpy # type: ignore
564
+ if "_mpy" in dir(sys.implementation)
565
+ else sys.implementation.mpy if "mpy" in dir(sys.implementation) else "" # type: ignore
566
+ )
567
+ except (AttributeError, IndexError):
568
+ pass
569
+ info["board"] = get_boardname()
570
+
571
+ try:
572
+ if "uname" in dir(os): # old
573
+ # extract build from uname().version if available
574
+ info["build"] = _build(os.uname()[3]) # type: ignore
575
+ if not info["build"]:
576
+ # extract build from uname().release if available
577
+ info["build"] = _build(os.uname()[2]) # type: ignore
578
+ elif "version" in dir(sys): # new
579
+ # extract build from sys.version if available
580
+ info["build"] = _build(sys.version)
581
+ except (AttributeError, IndexError, TypeError):
582
+ pass
583
+ # avoid build hashes
584
+ # if info["build"] and len(info["build"]) > 5:
585
+ # info["build"] = ""
586
+
587
+ if info["version"] == "" and sys.platform not in ("unix", "win32"):
588
+ try:
589
+ u = os.uname() # type: ignore
590
+ info["version"] = u.release
591
+ except (IndexError, AttributeError, TypeError):
592
+ pass
593
+ # detect families
594
+ for fam_name, mod_name, mod_thing in [
595
+ ("pycopy", "pycopy", "const"),
596
+ ("pycom", "pycom", "FAT"),
597
+ ("ev3-pybricks", "pybricks.hubs", "EV3Brick"),
598
+ ]:
599
+ try:
600
+ _t = __import__(mod_name, None, None, (mod_thing))
601
+ info["family"] = fam_name
602
+ del _t
603
+ break
604
+ except (ImportError, KeyError):
605
+ pass
606
+
607
+ if info["family"] == "ev3-pybricks":
608
+ info["release"] = "2.0.0"
609
+
610
+ if info["family"] == "micropython":
611
+ info["version"]
612
+ if (
613
+ info["version"]
614
+ and info["version"].endswith(".0")
615
+ and info["version"]
616
+ >= "1.10.0" # versions from 1.10.0 to 1.23.2 do not have a micro .0
617
+ and info["version"] <= "1.19.9"
618
+ ):
619
+ # versions from 1.10.0 to 1.23.2 do not have a micro .0
620
+ info["version"] = info["version"][:-2]
621
+
622
+ # spell-checker: disable
623
+ if "mpy" in info and info["mpy"]: # mpy on some v1.11+ builds
624
+ sys_mpy = int(info["mpy"])
625
+ # .mpy architecture
626
+ arch = [
627
+ None,
628
+ "x86",
629
+ "x64",
630
+ "armv6",
631
+ "armv6m",
632
+ "armv7m",
633
+ "armv7em",
634
+ "armv7emsp",
635
+ "armv7emdp",
636
+ "xtensa",
637
+ "xtensawin",
638
+ ][sys_mpy >> 10]
639
+ if arch:
640
+ info["arch"] = arch
641
+ # .mpy version.minor
642
+ info["mpy"] = "v{}.{}".format(sys_mpy & 0xFF, sys_mpy >> 8 & 3)
643
+ if info["build"] and not info["version"].endswith("-preview"):
644
+ info["version"] = info["version"] + "-preview"
645
+ # simple to use version[-build] string
646
+ info["ver"] = f"{info['version']}-{info['build']}" if info["build"] else f"{info['version']}"
647
+
648
+ return info
649
+
650
+
651
+ def version_str(version: tuple): # -> str:
652
+ v_str = ".".join([str(n) for n in version[:3]])
653
+ if len(version) > 3 and version[3]:
654
+ v_str += "-" + version[3]
655
+ return v_str
656
+
657
+
658
+ def get_boardname() -> str:
659
+ "Read the board name from the boardname.py file that may have been created upfront"
660
+ try:
661
+ from boardname import BOARDNAME # type: ignore
662
+
663
+ log.info("Found BOARDNAME: {}".format(BOARDNAME))
664
+ except ImportError:
665
+ log.warning("BOARDNAME not found")
666
+ BOARDNAME = ""
667
+ return BOARDNAME
668
+
669
+
670
+ def get_root() -> str: # sourcery skip: use-assigned-variable
671
+ "Determine the root folder of the device"
672
+ try:
673
+ c = os.getcwd()
674
+ except (OSError, AttributeError):
675
+ # unix port
676
+ c = "."
677
+ r = c
678
+ for r in [c, "/sd", "/flash", "/", "."]:
679
+ try:
680
+ _ = os.stat(r)
681
+ break
682
+ except OSError:
683
+ continue
684
+ return r
685
+
686
+
687
+ def file_exists(filename: str):
688
+ try:
689
+ if os.stat(filename)[0] >> 14:
690
+ return True
691
+ return False
692
+ except OSError:
693
+ return False
694
+
695
+
696
+ def show_help():
697
+ print("-p, --path path to store the stubs in, defaults to '.'")
698
+ sys.exit(1)
699
+
700
+
701
+ def read_path() -> str:
702
+ "get --path from cmdline. [unix/win]"
703
+ path = ""
704
+ if len(sys.argv) == 3:
705
+ cmd = (sys.argv[1]).lower()
706
+ if cmd in ("--path", "-p"):
707
+ path = sys.argv[2]
708
+ else:
709
+ show_help()
710
+ elif len(sys.argv) == 2:
711
+ show_help()
712
+ return path
713
+
714
+
715
+ def is_micropython() -> bool:
716
+ "runtime test to determine full or micropython"
717
+ # pylint: disable=unused-variable,eval-used
718
+ try:
719
+ # either test should fail on micropython
720
+
721
+ # b) https://docs.micropython.org/en/latest/genrst/builtin_types.html#bytes-with-keywords-not-implemented
722
+ # Micropython: NotImplementedError
723
+ b = bytes("abc", encoding="utf8") # type: ignore # lgtm [py/unused-local-variable]
724
+
725
+ # c) https://docs.micropython.org/en/latest/genrst/core_language.html#function-objects-do-not-have-the-module-attribute
726
+ # Micropython: AttributeError
727
+ c = is_micropython.__module__ # type: ignore # lgtm [py/unused-local-variable]
728
+ return False
729
+ except (NotImplementedError, AttributeError):
730
+ return True
731
+
732
+
733
+ def main():
734
+ stubber = Stubber(path=read_path())
735
+ # stubber = Stubber(path="/sd")
736
+ # Option: Specify a firmware name & version
737
+ # stubber = Stubber(firmware_id='HoverBot v1.2.1')
738
+ stubber.clean()
739
+ # there is no option to discover modules from micropython, need to hardcode
740
+ # below contains combined modules from Micropython ESP8622, ESP32, Loboris, Pycom and ulab , lvgl
741
+ # spell-checker: disable
742
+ # modules to stub : 131
743
+ stubber.modules = [
744
+ "WM8960",
745
+ "_OTA",
746
+ "_asyncio",
747
+ "_boot_fat",
748
+ "_coap",
749
+ "_espnow",
750
+ "_flash_control_OTA",
751
+ "_main_pybytes",
752
+ "_mqtt",
753
+ "_mqtt_core",
754
+ "_msg_handl",
755
+ "_onewire",
756
+ "_periodical_pin",
757
+ "_pybytes",
758
+ "_pybytes_ca",
759
+ "_pybytes_config",
760
+ "_pybytes_config_reader",
761
+ "_pybytes_connection",
762
+ "_pybytes_constants",
763
+ "_pybytes_debug",
764
+ "_pybytes_library",
765
+ "_pybytes_machine_learning",
766
+ "_pybytes_main",
767
+ "_pybytes_protocol",
768
+ "_pybytes_pyconfig",
769
+ "_pybytes_pymesh_config",
770
+ "_rp2",
771
+ "_terminal",
772
+ "_thread",
773
+ "_uasyncio",
774
+ "_urequest",
775
+ "adcfft",
776
+ "aioble/__init__",
777
+ "aioble/central",
778
+ "aioble/client",
779
+ "aioble/core",
780
+ "aioble/device",
781
+ "aioble/l2cap",
782
+ "aioble/peripheral",
783
+ "aioble/security",
784
+ "aioble/server",
785
+ "aioespnow",
786
+ "ak8963",
787
+ "apa102",
788
+ "apa106",
789
+ "argparse",
790
+ "array",
791
+ "asyncio/__init__",
792
+ "asyncio/core",
793
+ "asyncio/event",
794
+ "asyncio/funcs",
795
+ "asyncio/lock",
796
+ "asyncio/stream",
797
+ "binascii",
798
+ "bluetooth",
799
+ "breakout_as7262",
800
+ "breakout_bh1745",
801
+ "breakout_bme280",
802
+ "breakout_bme68x",
803
+ "breakout_bmp280",
804
+ "breakout_dotmatrix",
805
+ "breakout_encoder",
806
+ "breakout_icp10125",
807
+ "breakout_ioexpander",
808
+ "breakout_ltr559",
809
+ "breakout_matrix11x7",
810
+ "breakout_mics6814",
811
+ "breakout_msa301",
812
+ "breakout_paa5100",
813
+ "breakout_pmw3901",
814
+ "breakout_potentiometer",
815
+ "breakout_rgbmatrix5x5",
816
+ "breakout_rtc",
817
+ "breakout_scd41",
818
+ "breakout_sgp30",
819
+ "breakout_trackball",
820
+ "breakout_vl53l5cx",
821
+ "btree",
822
+ "cmath",
823
+ "collections",
824
+ "crypto",
825
+ "cryptolib",
826
+ "curl",
827
+ "deflate",
828
+ "dht",
829
+ "display",
830
+ "display_driver_utils",
831
+ "ds18x20",
832
+ "encoder",
833
+ "errno",
834
+ "esp",
835
+ "esp32",
836
+ "espidf",
837
+ "espnow",
838
+ "ffi",
839
+ "flashbdev",
840
+ "framebuf",
841
+ "freesans20",
842
+ "fs_driver",
843
+ "functools",
844
+ "galactic",
845
+ "gc",
846
+ "gfx_pack",
847
+ "gsm",
848
+ "hashlib",
849
+ "heapq",
850
+ "hub75",
851
+ "ili9341",
852
+ "ili9XXX",
853
+ "imagetools",
854
+ "inisetup",
855
+ "interstate75",
856
+ "io",
857
+ "jpegdec",
858
+ "js",
859
+ "jsffi",
860
+ "json",
861
+ "lcd160cr",
862
+ "lodepng",
863
+ "logging",
864
+ "lsm6dsox",
865
+ "lv_colors",
866
+ "lv_utils",
867
+ "lvgl",
868
+ "lwip",
869
+ "machine",
870
+ "math",
871
+ "microWebSocket",
872
+ "microWebSrv",
873
+ "microWebTemplate",
874
+ "micropython",
875
+ "mip",
876
+ "mip/__init__",
877
+ "mip/__main__",
878
+ "motor",
879
+ "mpu6500",
880
+ "mpu9250",
881
+ "neopixel",
882
+ "network",
883
+ "ntptime",
884
+ "onewire",
885
+ "openamp",
886
+ "os",
887
+ "pcf85063a",
888
+ "picoexplorer",
889
+ "picographics",
890
+ "picokeypad",
891
+ "picoscroll",
892
+ "picounicorn",
893
+ "picowireless",
894
+ "pimoroni",
895
+ "pimoroni_bus",
896
+ "pimoroni_i2c",
897
+ "plasma",
898
+ "platform",
899
+ "pyb",
900
+ "pycom",
901
+ "pye",
902
+ "qrcode",
903
+ "queue",
904
+ "random",
905
+ "requests",
906
+ "requests/__init__",
907
+ "rp2",
908
+ "rtch",
909
+ "samd",
910
+ "select",
911
+ "servo",
912
+ "socket",
913
+ "ssd1306",
914
+ "ssh",
915
+ "ssl",
916
+ "stm",
917
+ "struct",
918
+ "sys",
919
+ "termios",
920
+ "time",
921
+ "tls",
922
+ "tpcalib",
923
+ "uarray",
924
+ "uasyncio/__init__",
925
+ "uasyncio/core",
926
+ "uasyncio/event",
927
+ "uasyncio/funcs",
928
+ "uasyncio/lock",
929
+ "uasyncio/stream",
930
+ "uasyncio/tasks",
931
+ "ubinascii",
932
+ "ubluetooth",
933
+ "ucollections",
934
+ "ucrypto",
935
+ "ucryptolib",
936
+ "uctypes",
937
+ "uerrno",
938
+ "uftpd",
939
+ "uhashlib",
940
+ "uheapq",
941
+ "uio",
942
+ "ujson",
943
+ "ulab",
944
+ "ulab/approx",
945
+ "ulab/compare",
946
+ "ulab/fft",
947
+ "ulab/filter",
948
+ "ulab/linalg",
949
+ "ulab/numerical",
950
+ "ulab/poly",
951
+ "ulab/user",
952
+ "ulab/vector",
953
+ "umachine",
954
+ "umqtt/__init__",
955
+ "umqtt/robust",
956
+ "umqtt/simple",
957
+ "uos",
958
+ "uplatform",
959
+ "uqueue",
960
+ "urandom",
961
+ "ure",
962
+ "urequests",
963
+ "urllib/urequest",
964
+ "usb/device",
965
+ "usb/device/cdc",
966
+ "usb/device/hid",
967
+ "usb/device/keyboard",
968
+ "usb/device/midi",
969
+ "usb/device/mouse",
970
+ "uselect",
971
+ "usocket",
972
+ "ussl",
973
+ "ustruct",
974
+ "usys",
975
+ "utelnetserver",
976
+ "utime",
977
+ "utimeq",
978
+ "uwebsocket",
979
+ "uzlib",
980
+ "version",
981
+ "vfs",
982
+ "websocket",
983
+ "websocket_helper",
984
+ "wipy",
985
+ "writer",
986
+ "xpt2046",
987
+ "ymodem",
988
+ "zephyr",
989
+ "zlib",
990
+ ] # spell-checker: enable
991
+
992
+ gc.collect()
993
+
994
+ stubber.create_all_stubs()
995
+
996
+
997
+ if __name__ == "__main__" or is_micropython():
998
+ if not file_exists("no_auto_stubber.txt"):
999
+ try:
1000
+ gc.threshold(4 * 1024) # type: ignore
1001
+ gc.enable()
1002
+ except BaseException:
1003
+ pass
1004
+ main()