micropython-stubber 1.20.4__py3-none-any.whl → 1.20.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. {micropython_stubber-1.20.4.dist-info → micropython_stubber-1.20.6.dist-info}/LICENSE +30 -30
  2. {micropython_stubber-1.20.4.dist-info → micropython_stubber-1.20.6.dist-info}/METADATA +4 -4
  3. micropython_stubber-1.20.6.dist-info/RECORD +159 -0
  4. mpflash/README.md +184 -184
  5. mpflash/libusb_flash.ipynb +203 -203
  6. mpflash/mpflash/add_firmware.py +98 -98
  7. mpflash/mpflash/ask_input.py +236 -226
  8. mpflash/mpflash/bootloader/__init__.py +37 -0
  9. mpflash/mpflash/bootloader/manual.py +102 -0
  10. mpflash/mpflash/bootloader/micropython.py +10 -0
  11. mpflash/mpflash/bootloader/touch1200.py +45 -0
  12. mpflash/mpflash/cli_download.py +129 -128
  13. mpflash/mpflash/cli_flash.py +219 -212
  14. mpflash/mpflash/cli_group.py +98 -92
  15. mpflash/mpflash/cli_list.py +81 -77
  16. mpflash/mpflash/cli_main.py +41 -38
  17. mpflash/mpflash/common.py +164 -151
  18. mpflash/mpflash/config.py +47 -31
  19. mpflash/mpflash/connected.py +74 -74
  20. mpflash/mpflash/download.py +360 -361
  21. mpflash/mpflash/downloaded.py +129 -129
  22. mpflash/mpflash/errors.py +9 -5
  23. mpflash/mpflash/flash.py +52 -69
  24. mpflash/mpflash/flash_esp.py +59 -59
  25. mpflash/mpflash/flash_stm32.py +24 -24
  26. mpflash/mpflash/flash_stm32_cube.py +111 -111
  27. mpflash/mpflash/flash_stm32_dfu.py +101 -101
  28. mpflash/mpflash/flash_uf2.py +67 -67
  29. mpflash/mpflash/flash_uf2_boardid.py +15 -15
  30. mpflash/mpflash/flash_uf2_linux.py +123 -123
  31. mpflash/mpflash/flash_uf2_macos.py +34 -37
  32. mpflash/mpflash/flash_uf2_windows.py +34 -34
  33. mpflash/mpflash/list.py +89 -89
  34. mpflash/mpflash/logger.py +41 -41
  35. mpflash/mpflash/mpboard_id/__init__.py +93 -93
  36. mpflash/mpflash/mpboard_id/add_boards.py +255 -255
  37. mpflash/mpflash/mpboard_id/board.py +37 -37
  38. mpflash/mpflash/mpboard_id/board_id.py +86 -86
  39. mpflash/mpflash/mpboard_id/store.py +43 -43
  40. mpflash/mpflash/mpremoteboard/__init__.py +221 -221
  41. mpflash/mpflash/mpremoteboard/mpy_fw_info.py +141 -141
  42. mpflash/mpflash/mpremoteboard/runner.py +140 -140
  43. mpflash/mpflash/uf2disk.py +12 -12
  44. mpflash/mpflash/vendor/basicgit.py +288 -288
  45. mpflash/mpflash/vendor/click_aliases.py +91 -0
  46. mpflash/mpflash/vendor/dfu.py +165 -165
  47. mpflash/mpflash/vendor/pydfu.py +605 -605
  48. mpflash/mpflash/vendor/readme.md +2 -2
  49. mpflash/mpflash/vendor/versions.py +119 -117
  50. mpflash/mpflash/worklist.py +170 -170
  51. mpflash/poetry.lock +1588 -1623
  52. mpflash/pyproject.toml +60 -60
  53. mpflash/stm32_udev_rules.md +62 -62
  54. stubber/__init__.py +3 -3
  55. stubber/basicgit.py +294 -288
  56. stubber/board/board_info.csv +193 -193
  57. stubber/board/boot.py +34 -34
  58. stubber/board/createstubs.py +986 -987
  59. stubber/board/createstubs_db.py +825 -826
  60. stubber/board/createstubs_db_min.py +331 -331
  61. stubber/board/createstubs_db_mpy.mpy +0 -0
  62. stubber/board/createstubs_lvgl.py +741 -741
  63. stubber/board/createstubs_lvgl_min.py +741 -741
  64. stubber/board/createstubs_mem.py +766 -767
  65. stubber/board/createstubs_mem_min.py +306 -306
  66. stubber/board/createstubs_mem_mpy.mpy +0 -0
  67. stubber/board/createstubs_min.py +294 -294
  68. stubber/board/createstubs_mpy.mpy +0 -0
  69. stubber/board/fw_info.py +141 -141
  70. stubber/board/info.py +183 -183
  71. stubber/board/main.py +19 -19
  72. stubber/board/modulelist.txt +247 -247
  73. stubber/board/pyrightconfig.json +34 -34
  74. stubber/bulk/mcu_stubber.py +454 -455
  75. stubber/codemod/_partials/__init__.py +48 -50
  76. stubber/codemod/_partials/db_main.py +147 -147
  77. stubber/codemod/_partials/lvgl_main.py +77 -77
  78. stubber/codemod/_partials/modules_reader.py +80 -80
  79. stubber/codemod/add_comment.py +53 -53
  80. stubber/codemod/add_method.py +65 -65
  81. stubber/codemod/board.py +317 -317
  82. stubber/codemod/enrich.py +145 -145
  83. stubber/codemod/merge_docstub.py +284 -284
  84. stubber/codemod/modify_list.py +54 -54
  85. stubber/codemod/utils.py +57 -57
  86. stubber/commands/build_cmd.py +94 -94
  87. stubber/commands/cli.py +51 -51
  88. stubber/commands/clone_cmd.py +66 -66
  89. stubber/commands/config_cmd.py +29 -29
  90. stubber/commands/enrich_folder_cmd.py +70 -70
  91. stubber/commands/get_core_cmd.py +69 -69
  92. stubber/commands/get_docstubs_cmd.py +87 -87
  93. stubber/commands/get_frozen_cmd.py +112 -112
  94. stubber/commands/get_mcu_cmd.py +56 -56
  95. stubber/commands/merge_cmd.py +66 -66
  96. stubber/commands/publish_cmd.py +119 -119
  97. stubber/commands/stub_cmd.py +30 -30
  98. stubber/commands/switch_cmd.py +54 -54
  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 +36 -36
  107. stubber/freeze/common.py +68 -68
  108. stubber/freeze/freeze_folder.py +69 -69
  109. stubber/freeze/freeze_manifest_2.py +113 -113
  110. stubber/freeze/get_frozen.py +127 -127
  111. stubber/get_cpython.py +101 -101
  112. stubber/get_lobo.py +59 -59
  113. stubber/minify.py +418 -418
  114. stubber/publish/bump.py +86 -86
  115. stubber/publish/candidates.py +262 -283
  116. stubber/publish/database.py +18 -18
  117. stubber/publish/defaults.py +45 -45
  118. stubber/publish/enums.py +24 -30
  119. stubber/publish/helpers.py +29 -29
  120. stubber/publish/merge_docstubs.py +130 -130
  121. stubber/publish/missing_class_methods.py +49 -49
  122. stubber/publish/package.py +146 -177
  123. stubber/publish/pathnames.py +51 -51
  124. stubber/publish/publish.py +120 -121
  125. stubber/publish/pypi.py +38 -38
  126. stubber/publish/stubpackage.py +1029 -1029
  127. stubber/rst/__init__.py +9 -9
  128. stubber/rst/classsort.py +77 -77
  129. stubber/rst/lookup.py +530 -530
  130. stubber/rst/output_dict.py +401 -401
  131. stubber/rst/reader.py +822 -823
  132. stubber/rst/report_return.py +69 -69
  133. stubber/rst/rst_utils.py +540 -540
  134. stubber/stubber.py +38 -38
  135. stubber/stubs_from_docs.py +90 -90
  136. stubber/tools/manifestfile.py +610 -610
  137. stubber/tools/readme.md +5 -5
  138. stubber/update_fallback.py +117 -117
  139. stubber/update_module_list.py +123 -125
  140. stubber/utils/__init__.py +5 -5
  141. stubber/utils/config.py +127 -127
  142. stubber/utils/makeversionhdr.py +54 -54
  143. stubber/utils/manifest.py +92 -92
  144. stubber/utils/post.py +79 -79
  145. stubber/utils/repos.py +157 -154
  146. stubber/utils/stubmaker.py +139 -139
  147. stubber/utils/typed_config_toml.py +77 -77
  148. stubber/utils/versions.py +128 -120
  149. stubber/variants.py +106 -106
  150. micropython_stubber-1.20.4.dist-info/RECORD +0 -154
  151. {micropython_stubber-1.20.4.dist-info → micropython_stubber-1.20.6.dist-info}/WHEEL +0 -0
  152. {micropython_stubber-1.20.4.dist-info → micropython_stubber-1.20.6.dist-info}/entry_points.txt +0 -0
@@ -1,767 +1,766 @@
1
- """Create stubs for (all) modules on a MicroPython board.
2
-
3
- This variant of the createstubs.py script is optimised for use on low-memory devices, and reads the list of modules from a text file
4
- `modulelist.txt` in the root or `libs` folder that should be uploaded to the device.
5
- If that cannot be found then only a single module (micropython) is stubbed.
6
- In order to run this on low-memory devices two additional steps are recommended:
7
- - minifification, using python-minifier
8
- to reduce overall size, and remove logging overhead.
9
- - cross compilation, using mpy-cross,
10
- to avoid the compilation step on the micropython device
11
-
12
- This variant was generated from createstubs.py by micropython-stubber v1.20.4
13
- """
14
-
15
- # Copyright (c) 2019-2024 Jos Verlinde
16
-
17
- import gc
18
- import os
19
- import sys
20
- from time import sleep
21
-
22
- try:
23
- from ujson import dumps
24
- except:
25
- from json import dumps
26
-
27
- try:
28
- from machine import reset # type: ignore
29
- except ImportError:
30
- pass
31
-
32
- try:
33
- from collections import OrderedDict
34
- except ImportError:
35
- from ucollections import OrderedDict # type: ignore
36
-
37
- __version__ = "v1.20.4"
38
- ENOENT = 2
39
- _MAX_CLASS_LEVEL = 2 # Max class nesting
40
- LIBS = ["lib", "/lib", "/sd/lib", "/flash/lib", "."]
41
-
42
-
43
- # our own logging module to avoid dependency on and interfering with logging module
44
- class logging:
45
- # DEBUG = 10
46
- INFO = 20
47
- WARNING = 30
48
- ERROR = 40
49
- level = INFO
50
- prnt = print
51
-
52
- @staticmethod
53
- def getLogger(name):
54
- return logging()
55
-
56
- @classmethod
57
- def basicConfig(cls, level):
58
- cls.level = level
59
-
60
- # def debug(self, msg):
61
- # if self.level <= logging.DEBUG:
62
- # self.prnt("DEBUG :", msg)
63
-
64
- def info(self, msg):
65
- if self.level <= logging.INFO:
66
- self.prnt("INFO :", msg)
67
-
68
- def warning(self, msg):
69
- if self.level <= logging.WARNING:
70
- self.prnt("WARN :", msg)
71
-
72
- def error(self, msg):
73
- if self.level <= logging.ERROR:
74
- self.prnt("ERROR :", msg)
75
-
76
-
77
- log = logging.getLogger("stubber")
78
- logging.basicConfig(level=logging.INFO)
79
- # logging.basicConfig(level=logging.DEBUG)
80
-
81
-
82
- class Stubber:
83
- "Generate stubs for modules in firmware"
84
-
85
- def __init__(self, path: str = None, firmware_id: str = None): # type: ignore
86
- try:
87
- if os.uname().release == "1.13.0" and os.uname().version < "v1.13-103": # type: ignore
88
- raise NotImplementedError("MicroPython 1.13.0 cannot be stubbed")
89
- except AttributeError:
90
- pass # Allow testing on CPython 3.11
91
- self.info = _info()
92
- log.info("Port: {}".format(self.info["port"]))
93
- log.info("Board: {}".format(self.info["board"]))
94
- gc.collect()
95
- if firmware_id:
96
- self._fwid = firmware_id.lower()
97
- else:
98
- if self.info["family"] == "micropython":
99
- self._fwid = "{family}-v{version}-{port}-{board}".format(**self.info).rstrip("-")
100
- else:
101
- self._fwid = "{family}-v{version}-{port}".format(**self.info)
102
- self._start_free = gc.mem_free() # type: ignore
103
-
104
- if path:
105
- if path.endswith("/"):
106
- path = path[:-1]
107
- else:
108
- path = get_root()
109
-
110
- self.path = "{}/stubs/{}".format(path, self.flat_fwid).replace("//", "/")
111
- # log.debug(self.path)
112
- try:
113
- ensure_folder(path + "/")
114
- except OSError:
115
- log.error("error creating stub folder {}".format(path))
116
- self.problematic = [
117
- "upip",
118
- "upysh",
119
- "webrepl_setup",
120
- "http_client",
121
- "http_client_ssl",
122
- "http_server",
123
- "http_server_ssl",
124
- ]
125
- self.excluded = [
126
- "webrepl",
127
- "_webrepl",
128
- "port_diag",
129
- "example_sub_led.py",
130
- "example_pub_button.py",
131
- ]
132
- # there is no option to discover modules from micropython, list is read from an external file.
133
- self.modules = [] # type: list[str]
134
- self._json_name = None
135
- self._json_first = False
136
-
137
- def get_obj_attributes(self, item_instance: object):
138
- "extract information of the objects members and attributes"
139
- # name_, repr_(value), type as text, item_instance
140
- _result = []
141
- _errors = []
142
- # log.debug("get attributes {} {}".format(repr(item_instance), item_instance))
143
- for name in dir(item_instance):
144
- if name.startswith("__") and not name in self.modules:
145
- continue
146
- # log.debug("get attribute {}".format(name))
147
- try:
148
- val = getattr(item_instance, name)
149
- # name , item_repr(value) , type as text, item_instance, order
150
- # log.debug("attribute {}:{}".format(name, val))
151
- try:
152
- type_text = repr(type(val)).split("'")[1]
153
- except IndexError:
154
- type_text = ""
155
- if type_text in {"int", "float", "str", "bool", "tuple", "list", "dict"}:
156
- order = 1
157
- elif type_text in {"function", "method"}:
158
- order = 2
159
- elif type_text in ("class"):
160
- order = 3
161
- else:
162
- order = 4
163
- _result.append((name, repr(val), repr(type(val)), val, order))
164
- except AttributeError as e:
165
- _errors.append("Couldn't get attribute '{}' from object '{}', Err: {}".format(name, item_instance, e))
166
- except MemoryError as e:
167
- print("MemoryError: {}".format(e))
168
- sleep(1)
169
- reset()
170
-
171
- # remove internal __
172
- # _result = sorted([i for i in _result if not (i[0].startswith("_"))], key=lambda x: x[4])
173
- _result = sorted([i for i in _result if not (i[0].startswith("__"))], key=lambda x: x[4])
174
- gc.collect()
175
- return _result, _errors
176
-
177
- def add_modules(self, modules):
178
- "Add additional modules to be exported"
179
- self.modules = sorted(set(self.modules) | set(modules))
180
-
181
- def create_all_stubs(self):
182
- "Create stubs for all configured modules"
183
- log.info("Start micropython-stubber {} on {}".format(__version__, self._fwid))
184
- self.report_start()
185
- gc.collect()
186
- for module_name in self.modules:
187
- self.create_one_stub(module_name)
188
- self.report_end()
189
- log.info("Finally done")
190
-
191
- def create_one_stub(self, module_name: str):
192
- if module_name in self.problematic:
193
- log.warning("Skip module: {:<25} : Known problematic".format(module_name))
194
- return False
195
- if module_name in self.excluded:
196
- log.warning("Skip module: {:<25} : Excluded".format(module_name))
197
- return False
198
-
199
- file_name = "{}/{}.pyi".format(self.path, module_name.replace(".", "/"))
200
- gc.collect()
201
- result = False
202
- try:
203
- result = self.create_module_stub(module_name, file_name)
204
- except OSError:
205
- return False
206
- gc.collect()
207
- return result
208
-
209
- def create_module_stub(self, module_name: str, file_name: str = None) -> bool: # type: ignore
210
- """Create a Stub of a single python module
211
-
212
- Args:
213
- - module_name (str): name of the module to document. This module will be imported.
214
- - file_name (Optional[str]): the 'path/filename.pyi' to write to. If omitted will be created based on the module name.
215
- """
216
- if file_name is None:
217
- fname = module_name.replace(".", "_") + ".pyi"
218
- file_name = self.path + "/" + fname
219
- else:
220
- fname = file_name.split("/")[-1]
221
-
222
- if "/" in module_name:
223
- # for nested modules
224
- module_name = module_name.replace("/", ".")
225
-
226
- # import the module (as new_module) to examine it
227
- new_module = None
228
- try:
229
- new_module = __import__(module_name, None, None, ("*"))
230
- m1 = gc.mem_free() # type: ignore
231
- log.info("Stub module: {:<25} to file: {:<70} mem:{:>5}".format(module_name, fname, m1))
232
-
233
- except ImportError:
234
- # log.debug("Skip module: {:<25} {:<79}".format(module_name, "Module not found."))
235
- return False
236
-
237
- # Start a new file
238
- ensure_folder(file_name)
239
- with open(file_name, "w") as fp:
240
- # todo: improve header
241
- info_ = str(self.info).replace("OrderedDict(", "").replace("})", "}")
242
- s = '"""\nModule: \'{0}\' on {1}\n"""\n# MCU: {2}\n# Stubber: {3}\n'.format(module_name, self._fwid, info_, __version__)
243
- fp.write(s)
244
- fp.write("from __future__ import annotations\nfrom typing import Any, Generator\nfrom _typeshed import Incomplete\n\n")
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(self, fp, object_expr: object, obj_name: str, indent: str, in_class: int = 0):
260
- "Write a module/object stub to an open file. Can be called recursive."
261
- gc.collect()
262
- if object_expr in self.problematic:
263
- log.warning("SKIPPING problematic module:{}".format(object_expr))
264
- return
265
-
266
- # # log.debug("DUMP : {}".format(object_expr))
267
- items, errors = self.get_obj_attributes(object_expr)
268
-
269
- if errors:
270
- log.error(errors)
271
-
272
- for item_name, item_repr, item_type_txt, item_instance, _ in items:
273
- # name_, repr_(value), type as text, item_instance, order
274
- if item_name in ["classmethod", "staticmethod", "BaseException", "Exception"]:
275
- # do not create stubs for these primitives
276
- continue
277
- if item_name[0].isdigit():
278
- log.warning("NameError: invalid name {}".format(item_name))
279
- continue
280
- # Class expansion only on first 3 levels (bit of a hack)
281
- if (
282
- item_type_txt == "<class 'type'>"
283
- and len(indent) <= _MAX_CLASS_LEVEL * 4
284
- # and not obj_name.endswith(".Pin")
285
- # avoid expansion of Pin.cpu / Pin.board to avoid crashes on most platforms
286
- ):
287
- # log.debug("{0}class {1}:".format(indent, item_name))
288
- superclass = ""
289
- is_exception = (
290
- item_name.endswith("Exception")
291
- or item_name.endswith("Error")
292
- or item_name
293
- in [
294
- "KeyboardInterrupt",
295
- "StopIteration",
296
- "SystemExit",
297
- ]
298
- )
299
- if is_exception:
300
- superclass = "Exception"
301
- s = "\n{}class {}({}):\n".format(indent, item_name, superclass)
302
- # s += indent + " ''\n"
303
- if is_exception:
304
- s += indent + " ...\n"
305
- fp.write(s)
306
- continue
307
- # write classdef
308
- fp.write(s)
309
- # first write the class literals and methods
310
- # log.debug("# recursion over class {0}".format(item_name))
311
- self.write_object_stub(
312
- fp,
313
- item_instance,
314
- "{0}.{1}".format(obj_name, item_name),
315
- indent + " ",
316
- in_class + 1,
317
- )
318
- # end with the __init__ method to make sure that the literals are defined
319
- # Add __init__
320
- s = indent + " def __init__(self, *argv, **kwargs) -> None:\n"
321
- s += indent + " ...\n\n"
322
- fp.write(s)
323
- elif any(word in item_type_txt for word in ["method", "function", "closure"]):
324
- # log.debug("# def {1} function/method/closure, type = '{0}'".format(item_type_txt, item_name))
325
- # module Function or class method
326
- # will accept any number of params
327
- # return type Any/Incomplete
328
- ret = "Incomplete"
329
- first = ""
330
- # Self parameter only on class methods/functions
331
- if in_class > 0:
332
- first = "self, "
333
- # class method - add function decoration
334
- if "bound_method" in item_type_txt or "bound_method" in item_repr:
335
- s = "{}@classmethod\n".format(indent) + "{}def {}(cls, *args, **kwargs) -> {}:\n".format(indent, item_name, ret)
336
- else:
337
- s = "{}def {}({}*args, **kwargs) -> {}:\n".format(indent, item_name, first, ret)
338
- s += indent + " ...\n\n"
339
- fp.write(s)
340
- # log.debug("\n" + s)
341
- elif item_type_txt == "<class 'module'>":
342
- # Skip imported modules
343
- # fp.write("# import {}\n".format(item_name))
344
- pass
345
-
346
- elif item_type_txt.startswith("<class '"):
347
- t = item_type_txt[8:-2]
348
- s = ""
349
-
350
- if t in ("str", "int", "float", "bool", "bytearray", "bytes"):
351
- # known type: use actual value
352
- # s = "{0}{1} = {2} # type: {3}\n".format(indent, item_name, item_repr, t)
353
- s = "{0}{1}: {3} = {2}\n".format(indent, item_name, item_repr, t)
354
- elif t in ("dict", "list", "tuple"):
355
- # dict, list , tuple: use empty value
356
- ev = {"dict": "{}", "list": "[]", "tuple": "()"}
357
- # s = "{0}{1} = {2} # type: {3}\n".format(indent, item_name, ev[t], t)
358
- s = "{0}{1}: {3} = {2}\n".format(indent, item_name, ev[t], t)
359
- else:
360
- # something else
361
- if t in ("object", "set", "frozenset", "Pin", "generator"): # "FileIO"
362
- # https://docs.python.org/3/tutorial/classes.html#item_instance-objects
363
- # use these types for the attribute
364
- if t == "generator":
365
- t = "Generator"
366
- s = "{0}{1}: {2} ## = {4}\n".format(indent, item_name, t, item_type_txt, item_repr)
367
- else:
368
- # Requires Python 3.6 syntax, which is OK for the stubs/pyi
369
- t = "Incomplete"
370
- if " at " in item_repr:
371
- item_repr = item_repr.split(" at ")[0] + " at ...>"
372
- if " at " in item_repr:
373
- item_repr = item_repr.split(" at ")[0] + " at ...>"
374
- s = "{0}{1}: {2} ## {3} = {4}\n".format(indent, item_name, t, item_type_txt, item_repr)
375
- fp.write(s)
376
- # log.debug("\n" + s)
377
- else:
378
- # keep only the name
379
- # log.debug("# all other, type = '{0}'".format(item_type_txt))
380
- fp.write("# all other, type = '{0}'\n".format(item_type_txt))
381
-
382
- fp.write(indent + item_name + " # type: Incomplete\n")
383
-
384
- # del items
385
- # del errors
386
- # try:
387
- # del item_name, item_repr, item_type_txt, item_instance # type: ignore
388
- # except (OSError, KeyError, NameError):
389
- # pass
390
-
391
- @property
392
- def flat_fwid(self):
393
- "Turn _fwid from 'v1.2.3' into '1_2_3' to be used in filename"
394
- s = self._fwid
395
- # path name restrictions
396
- chars = " .()/\\:$"
397
- for c in chars:
398
- s = s.replace(c, "_")
399
- return s
400
-
401
- def clean(self, path: str = None): # type: ignore
402
- "Remove all files from the stub folder"
403
- if path is None:
404
- path = self.path
405
- log.info("Clean/remove files in folder: {}".format(path))
406
- try:
407
- os.stat(path) # TEMP workaround mpremote listdir bug -
408
- items = os.listdir(path)
409
- except (OSError, AttributeError):
410
- # os.listdir fails on unix
411
- return
412
- for fn in items:
413
- item = "{}/{}".format(path, fn)
414
- try:
415
- os.remove(item)
416
- except OSError:
417
- try: # folder
418
- self.clean(item)
419
- os.rmdir(item)
420
- except OSError:
421
- pass
422
-
423
- def report_start(self, filename: str = "modules.json"):
424
- """Start a report of the modules that have been stubbed
425
- "create json with list of exported modules"""
426
- self._json_name = "{}/{}".format(self.path, filename)
427
- self._json_first = True
428
- ensure_folder(self._json_name)
429
- log.info("Report file: {}".format(self._json_name))
430
- gc.collect()
431
- try:
432
- # write json by node to reduce memory requirements
433
- with open(self._json_name, "w") as f:
434
- f.write("{")
435
- f.write(dumps({"firmware": self.info})[1:-1])
436
- f.write(",\n")
437
- f.write(dumps({"stubber": {"version": __version__}, "stubtype": "firmware"})[1:-1])
438
- f.write(",\n")
439
- f.write('"modules" :[\n')
440
-
441
- except OSError as e:
442
- log.error("Failed to create the report.")
443
- self._json_name = None
444
- raise e
445
-
446
- def report_add(self, module_name: str, stub_file: str):
447
- "Add a module to the report"
448
- # write json by node to reduce memory requirements
449
- if not self._json_name:
450
- raise Exception("No report file")
451
- try:
452
- with open(self._json_name, "a") as f:
453
- if not self._json_first:
454
- f.write(",\n")
455
- else:
456
- self._json_first = False
457
- line = '{{"module": "{}", "file": "{}"}}'.format(module_name, stub_file.replace("\\", "/"))
458
- f.write(line)
459
-
460
- except OSError:
461
- log.error("Failed to create the report.")
462
-
463
- def report_end(self):
464
- if not self._json_name:
465
- raise Exception("No report file")
466
- with open(self._json_name, "a") as f:
467
- f.write("\n]}")
468
- # is used as sucess indicator
469
- log.info("Path: {}".format(self.path))
470
-
471
-
472
- def ensure_folder(path: str):
473
- "Create nested folders if needed"
474
- i = start = 0
475
- while i != -1:
476
- i = path.find("/", start)
477
- if i != -1:
478
- p = path[0] if i == 0 else path[:i]
479
- # p = partial folder
480
- try:
481
- _ = os.stat(p)
482
- except OSError as e:
483
- # folder does not exist
484
- if e.args[0] == ENOENT:
485
- try:
486
- os.mkdir(p)
487
- except OSError as e2:
488
- log.error("failed to create folder {}".format(p))
489
- raise e2
490
- # next level deep
491
- start = i + 1
492
-
493
-
494
- def _build(s):
495
- # extract build from sys.version or os.uname().version if available
496
- # sys.version: 'MicroPython v1.23.0-preview.6.g3d0b6276f'
497
- # sys.implementation.version: 'v1.13-103-gb137d064e'
498
- if not s:
499
- return ""
500
- s = s.split(" on ", 1)[0] if " on " in s else s
501
- if s.startswith("v"):
502
- if not "-" in s:
503
- return ""
504
- b = s.split("-")[1]
505
- return b
506
- if not "-preview" in s:
507
- return ""
508
- b = s.split("-preview")[1].split(".")[1]
509
- return b
510
-
511
-
512
- def _info(): # type:() -> dict[str, str]
513
- try:
514
- fam = sys.implementation[0] # type: ignore
515
- except TypeError:
516
- # testing on CPython 3.11
517
- fam = sys.implementation.name
518
-
519
- info = OrderedDict(
520
- {
521
- "family": fam,
522
- "version": "",
523
- "build": "",
524
- "ver": "",
525
- "port": sys.platform, # port: esp32 / win32 / linux / stm32
526
- "board": "UNKNOWN",
527
- "cpu": "",
528
- "mpy": "",
529
- "arch": "",
530
- }
531
- )
532
- # change port names to be consistent with the repo
533
- if info["port"].startswith("pyb"):
534
- info["port"] = "stm32"
535
- elif info["port"] == "win32":
536
- info["port"] = "windows"
537
- elif info["port"] == "linux":
538
- info["port"] = "unix"
539
- try:
540
- info["version"] = version_str(sys.implementation.version) # type: ignore
541
- except AttributeError:
542
- pass
543
- try:
544
- _machine = sys.implementation._machine if "_machine" in dir(sys.implementation) else os.uname().machine # type: ignore
545
- # info["board"] = "with".join(_machine.split("with")[:-1]).strip()
546
- info["board"] = _machine
547
- info["cpu"] = _machine.split("with")[-1].strip()
548
- info["mpy"] = (
549
- sys.implementation._mpy # type: ignore
550
- if "_mpy" in dir(sys.implementation)
551
- else sys.implementation.mpy if "mpy" in dir(sys.implementation) else "" # type: ignore
552
- )
553
- except (AttributeError, IndexError):
554
- pass
555
- info["board"] = get_boardname()
556
-
557
- try:
558
- if "uname" in dir(os): # old
559
- # extract build from uname().version if available
560
- info["build"] = _build(os.uname()[3]) # type: ignore
561
- if not info["build"]:
562
- # extract build from uname().release if available
563
- info["build"] = _build(os.uname()[2]) # type: ignore
564
- elif "version" in dir(sys): # new
565
- # extract build from sys.version if available
566
- info["build"] = _build(sys.version)
567
- except (AttributeError, IndexError, TypeError):
568
- pass
569
- # avoid build hashes
570
- # if info["build"] and len(info["build"]) > 5:
571
- # info["build"] = ""
572
-
573
- if info["version"] == "" and sys.platform not in ("unix", "win32"):
574
- try:
575
- u = os.uname() # type: ignore
576
- info["version"] = u.release
577
- except (IndexError, AttributeError, TypeError):
578
- pass
579
- # detect families
580
- for fam_name, mod_name, mod_thing in [
581
- ("pycopy", "pycopy", "const"),
582
- ("pycom", "pycom", "FAT"),
583
- ("ev3-pybricks", "pybricks.hubs", "EV3Brick"),
584
- ]:
585
- try:
586
- _t = __import__(mod_name, None, None, (mod_thing))
587
- info["family"] = fam_name
588
- del _t
589
- break
590
- except (ImportError, KeyError):
591
- pass
592
-
593
- if info["family"] == "ev3-pybricks":
594
- info["release"] = "2.0.0"
595
-
596
- if info["family"] == "micropython":
597
- info["version"]
598
- if (
599
- info["version"]
600
- and info["version"].endswith(".0")
601
- and info["version"] >= "1.10.0" # versions from 1.10.0 to 1.20.4 do not have a micro .0
602
- and info["version"] <= "1.19.9"
603
- ):
604
- # versions from 1.10.0 to 1.20.4 do not have a micro .0
605
- info["version"] = info["version"][:-2]
606
-
607
- # spell-checker: disable
608
- if "mpy" in info and info["mpy"]: # mpy on some v1.11+ builds
609
- sys_mpy = int(info["mpy"])
610
- # .mpy architecture
611
- arch = [
612
- None,
613
- "x86",
614
- "x64",
615
- "armv6",
616
- "armv6m",
617
- "armv7m",
618
- "armv7em",
619
- "armv7emsp",
620
- "armv7emdp",
621
- "xtensa",
622
- "xtensawin",
623
- ][sys_mpy >> 10]
624
- if arch:
625
- info["arch"] = arch
626
- # .mpy version.minor
627
- info["mpy"] = "v{}.{}".format(sys_mpy & 0xFF, sys_mpy >> 8 & 3)
628
- if info["build"] and not info["version"].endswith("-preview"):
629
- info["version"] = info["version"] + "-preview"
630
- # simple to use version[-build] string
631
- info["ver"] = f"{info['version']}-{info['build']}" if info["build"] else f"{info['version']}"
632
-
633
- return info
634
-
635
-
636
- def version_str(version: tuple): # -> str:
637
- v_str = ".".join([str(n) for n in version[:3]])
638
- if len(version) > 3 and version[3]:
639
- v_str += "-" + version[3]
640
- return v_str
641
-
642
-
643
- def get_boardname() -> str:
644
- "Read the board name from the boardname.py file that may have been created upfront"
645
- try:
646
- from boardname import BOARDNAME # type: ignore
647
-
648
- log.info("Found BOARDNAME: {}".format(BOARDNAME))
649
- except ImportError:
650
- log.warning("BOARDNAME not found")
651
- BOARDNAME = ""
652
- return BOARDNAME
653
-
654
-
655
- def get_root() -> str: # sourcery skip: use-assigned-variable
656
- "Determine the root folder of the device"
657
- try:
658
- c = os.getcwd()
659
- except (OSError, AttributeError):
660
- # unix port
661
- c = "."
662
- r = c
663
- for r in [c, "/sd", "/flash", "/", "."]:
664
- try:
665
- _ = os.stat(r)
666
- break
667
- except OSError:
668
- continue
669
- return r
670
-
671
-
672
- def file_exists(filename: str):
673
- try:
674
- if os.stat(filename)[0] >> 14:
675
- return True
676
- return False
677
- except OSError:
678
- return False
679
-
680
-
681
- def show_help():
682
- print("-p, --path path to store the stubs in, defaults to '.'")
683
- sys.exit(1)
684
-
685
-
686
- def read_path() -> str:
687
- "get --path from cmdline. [unix/win]"
688
- path = ""
689
- if len(sys.argv) == 3:
690
- cmd = (sys.argv[1]).lower()
691
- if cmd in ("--path", "-p"):
692
- path = sys.argv[2]
693
- else:
694
- show_help()
695
- elif len(sys.argv) == 2:
696
- show_help()
697
- return path
698
-
699
-
700
- def is_micropython() -> bool:
701
- "runtime test to determine full or micropython"
702
- # pylint: disable=unused-variable,eval-used
703
- try:
704
- # either test should fail on micropython
705
-
706
- # b) https://docs.micropython.org/en/latest/genrst/builtin_types.html#bytes-with-keywords-not-implemented
707
- # Micropython: NotImplementedError
708
- b = bytes("abc", encoding="utf8") # type: ignore # lgtm [py/unused-local-variable]
709
-
710
- # c) https://docs.micropython.org/en/latest/genrst/core_language.html#function-objects-do-not-have-the-module-attribute
711
- # Micropython: AttributeError
712
- c = is_micropython.__module__ # type: ignore # lgtm [py/unused-local-variable]
713
- return False
714
- except (NotImplementedError, AttributeError):
715
- return True
716
-
717
-
718
- def main():
719
- stubber = Stubber(path=read_path())
720
- # stubber = Stubber(path="/sd")
721
- # Option: Specify a firmware name & version
722
- # stubber = Stubber(firmware_id='HoverBot v1.2.1')
723
- stubber.clean()
724
-
725
- # Read stubs from modulelist in the current folder or in /libs
726
- # fall back to default modules
727
- def get_modulelist(stubber):
728
- # new
729
- gc.collect()
730
- stubber.modules = [] # avoid duplicates
731
- for p in LIBS:
732
- fname = p + "/modulelist.txt"
733
- if not file_exists(fname):
734
- continue
735
- with open(fname) as f:
736
- # print("DEBUG: list of modules: " + p + "/modulelist.txt")
737
- while True:
738
- line = f.readline().strip()
739
- if not line:
740
- break
741
- if len(line) > 0 and line[0] != "#":
742
- stubber.modules.append(line)
743
- gc.collect()
744
- print("BREAK")
745
- break
746
-
747
- if not stubber.modules:
748
- stubber.modules = ["micropython"]
749
- # _log.warn("Could not find modulelist.txt, using default modules")
750
- gc.collect()
751
-
752
- stubber.modules = [] # avoid duplicates
753
- get_modulelist(stubber)
754
-
755
- gc.collect()
756
-
757
- stubber.create_all_stubs()
758
-
759
-
760
- if __name__ == "__main__" or is_micropython():
761
- if not file_exists("no_auto_stubber.txt"):
762
- try:
763
- gc.threshold(4 * 1024) # type: ignore
764
- gc.enable()
765
- except BaseException:
766
- pass
767
- main()
1
+ """Create stubs for (all) modules on a MicroPython board.
2
+
3
+ This variant of the createstubs.py script is optimised for use on low-memory devices, and reads the list of modules from a text file
4
+ `modulelist.txt` in the root or `libs` folder that should be uploaded to the device.
5
+ If that cannot be found then only a single module (micropython) is stubbed.
6
+ In order to run this on low-memory devices two additional steps are recommended:
7
+ - minifification, using python-minifier
8
+ to reduce overall size, and remove logging overhead.
9
+ - cross compilation, using mpy-cross,
10
+ to avoid the compilation step on the micropython device
11
+
12
+ This variant was generated from createstubs.py by micropython-stubber v1.20.6
13
+ """
14
+
15
+ # Copyright (c) 2019-2024 Jos Verlinde
16
+
17
+ import gc
18
+ import os
19
+ import sys
20
+ from time import sleep
21
+
22
+ try:
23
+ from ujson import dumps
24
+ except:
25
+ from json import dumps
26
+
27
+ try:
28
+ from machine import reset # type: ignore
29
+ except ImportError:
30
+ pass
31
+
32
+ try:
33
+ from collections import OrderedDict
34
+ except ImportError:
35
+ from ucollections import OrderedDict # type: ignore
36
+
37
+ __version__ = "v1.20.6"
38
+ ENOENT = 2
39
+ _MAX_CLASS_LEVEL = 2 # Max class nesting
40
+ LIBS = ["lib", "/lib", "/sd/lib", "/flash/lib", "."]
41
+
42
+
43
+ # our own logging module to avoid dependency on and interfering with logging module
44
+ class logging:
45
+ # DEBUG = 10
46
+ INFO = 20
47
+ WARNING = 30
48
+ ERROR = 40
49
+ level = INFO
50
+ prnt = print
51
+
52
+ @staticmethod
53
+ def getLogger(name):
54
+ return logging()
55
+
56
+ @classmethod
57
+ def basicConfig(cls, level):
58
+ cls.level = level
59
+
60
+ # def debug(self, msg):
61
+ # if self.level <= logging.DEBUG:
62
+ # self.prnt("DEBUG :", msg)
63
+
64
+ def info(self, msg):
65
+ if self.level <= logging.INFO:
66
+ self.prnt("INFO :", msg)
67
+
68
+ def warning(self, msg):
69
+ if self.level <= logging.WARNING:
70
+ self.prnt("WARN :", msg)
71
+
72
+ def error(self, msg):
73
+ if self.level <= logging.ERROR:
74
+ self.prnt("ERROR :", msg)
75
+
76
+
77
+ log = logging.getLogger("stubber")
78
+ logging.basicConfig(level=logging.INFO)
79
+ # logging.basicConfig(level=logging.DEBUG)
80
+
81
+
82
+ class Stubber:
83
+ "Generate stubs for modules in firmware"
84
+
85
+ def __init__(self, path: str = None, firmware_id: str = None): # type: ignore
86
+ try:
87
+ if os.uname().release == "1.13.0" and os.uname().version < "v1.13-103": # type: ignore
88
+ raise NotImplementedError("MicroPython 1.13.0 cannot be stubbed")
89
+ except AttributeError:
90
+ pass # Allow testing on CPython 3.11
91
+ self.info = _info()
92
+ log.info("Port: {}".format(self.info["port"]))
93
+ log.info("Board: {}".format(self.info["board"]))
94
+ gc.collect()
95
+ if firmware_id:
96
+ self._fwid = firmware_id.lower()
97
+ else:
98
+ if self.info["family"] == "micropython":
99
+ self._fwid = "{family}-v{version}-{port}-{board}".format(**self.info).rstrip("-")
100
+ else:
101
+ self._fwid = "{family}-v{version}-{port}".format(**self.info)
102
+ self._start_free = gc.mem_free() # type: ignore
103
+
104
+ if path:
105
+ if path.endswith("/"):
106
+ path = path[:-1]
107
+ else:
108
+ path = get_root()
109
+
110
+ self.path = "{}/stubs/{}".format(path, self.flat_fwid).replace("//", "/")
111
+ # log.debug(self.path)
112
+ try:
113
+ ensure_folder(path + "/")
114
+ except OSError:
115
+ log.error("error creating stub folder {}".format(path))
116
+ self.problematic = [
117
+ "upip",
118
+ "upysh",
119
+ "webrepl_setup",
120
+ "http_client",
121
+ "http_client_ssl",
122
+ "http_server",
123
+ "http_server_ssl",
124
+ ]
125
+ self.excluded = [
126
+ "webrepl",
127
+ "_webrepl",
128
+ "port_diag",
129
+ "example_sub_led.py",
130
+ "example_pub_button.py",
131
+ ]
132
+ # there is no option to discover modules from micropython, list is read from an external file.
133
+ self.modules = [] # type: list[str]
134
+ self._json_name = None
135
+ self._json_first = False
136
+
137
+ def get_obj_attributes(self, item_instance: object):
138
+ "extract information of the objects members and attributes"
139
+ # name_, repr_(value), type as text, item_instance
140
+ _result = []
141
+ _errors = []
142
+ # log.debug("get attributes {} {}".format(repr(item_instance), item_instance))
143
+ for name in dir(item_instance):
144
+ if name.startswith("__") and not name in self.modules:
145
+ continue
146
+ # log.debug("get attribute {}".format(name))
147
+ try:
148
+ val = getattr(item_instance, name)
149
+ # name , item_repr(value) , type as text, item_instance, order
150
+ # log.debug("attribute {}:{}".format(name, val))
151
+ try:
152
+ type_text = repr(type(val)).split("'")[1]
153
+ except IndexError:
154
+ type_text = ""
155
+ if type_text in {"int", "float", "str", "bool", "tuple", "list", "dict"}:
156
+ order = 1
157
+ elif type_text in {"function", "method"}:
158
+ order = 2
159
+ elif type_text in ("class"):
160
+ order = 3
161
+ else:
162
+ order = 4
163
+ _result.append((name, repr(val), repr(type(val)), val, order))
164
+ except AttributeError as e:
165
+ _errors.append("Couldn't get attribute '{}' from object '{}', Err: {}".format(name, item_instance, e))
166
+ except MemoryError as e:
167
+ print("MemoryError: {}".format(e))
168
+ sleep(1)
169
+ reset()
170
+
171
+ # remove internal __
172
+ # _result = sorted([i for i in _result if not (i[0].startswith("_"))], key=lambda x: x[4])
173
+ _result = sorted([i for i in _result if not (i[0].startswith("__"))], key=lambda x: x[4])
174
+ gc.collect()
175
+ return _result, _errors
176
+
177
+ def add_modules(self, modules):
178
+ "Add additional modules to be exported"
179
+ self.modules = sorted(set(self.modules) | set(modules))
180
+
181
+ def create_all_stubs(self):
182
+ "Create stubs for all configured modules"
183
+ log.info("Start micropython-stubber {} on {}".format(__version__, self._fwid))
184
+ self.report_start()
185
+ gc.collect()
186
+ for module_name in self.modules:
187
+ self.create_one_stub(module_name)
188
+ self.report_end()
189
+ log.info("Finally done")
190
+
191
+ def create_one_stub(self, module_name: str):
192
+ if module_name in self.problematic:
193
+ log.warning("Skip module: {:<25} : Known problematic".format(module_name))
194
+ return False
195
+ if module_name in self.excluded:
196
+ log.warning("Skip module: {:<25} : Excluded".format(module_name))
197
+ return False
198
+
199
+ file_name = "{}/{}.pyi".format(self.path, module_name.replace(".", "/"))
200
+ gc.collect()
201
+ result = False
202
+ try:
203
+ result = self.create_module_stub(module_name, file_name)
204
+ except OSError:
205
+ return False
206
+ gc.collect()
207
+ return result
208
+
209
+ def create_module_stub(self, module_name: str, file_name: str = None) -> bool: # type: ignore
210
+ """Create a Stub of a single python module
211
+
212
+ Args:
213
+ - module_name (str): name of the module to document. This module will be imported.
214
+ - file_name (Optional[str]): the 'path/filename.pyi' to write to. If omitted will be created based on the module name.
215
+ """
216
+ if file_name is None:
217
+ fname = module_name.replace(".", "_") + ".pyi"
218
+ file_name = self.path + "/" + fname
219
+ else:
220
+ fname = file_name.split("/")[-1]
221
+
222
+ if "/" in module_name:
223
+ # for nested modules
224
+ module_name = module_name.replace("/", ".")
225
+
226
+ # import the module (as new_module) to examine it
227
+ new_module = None
228
+ try:
229
+ new_module = __import__(module_name, None, None, ("*"))
230
+ m1 = gc.mem_free() # type: ignore
231
+ log.info("Stub module: {:<25} to file: {:<70} mem:{:>5}".format(module_name, fname, m1))
232
+
233
+ except ImportError:
234
+ # log.debug("Skip module: {:<25} {:<79}".format(module_name, "Module not found."))
235
+ return False
236
+
237
+ # Start a new file
238
+ ensure_folder(file_name)
239
+ with open(file_name, "w") as fp:
240
+ info_ = str(self.info).replace("OrderedDict(", "").replace("})", "}")
241
+ s = '"""\nModule: \'{0}\' on {1}\n"""\n# MCU: {2}\n# Stubber: {3}\n'.format(module_name, self._fwid, info_, __version__)
242
+ fp.write(s)
243
+ fp.write("from __future__ import annotations\nfrom typing import Any, Generator\nfrom _typeshed import Incomplete\n\n")
244
+ self.write_object_stub(fp, new_module, module_name, "")
245
+
246
+ self.report_add(module_name, file_name)
247
+
248
+ if module_name not in {"os", "sys", "logging", "gc"}:
249
+ # try to unload the module unless we use it
250
+ try:
251
+ del new_module
252
+ except (OSError, KeyError): # lgtm [py/unreachable-statement]
253
+ log.warning("could not del new_module")
254
+ # do not try to delete from sys.modules - most times it does not work anyway
255
+ gc.collect()
256
+ return True
257
+
258
+ def write_object_stub(self, fp, object_expr: object, obj_name: str, indent: str, in_class: int = 0):
259
+ "Write a module/object stub to an open file. Can be called recursive."
260
+ gc.collect()
261
+ if object_expr in self.problematic:
262
+ log.warning("SKIPPING problematic module:{}".format(object_expr))
263
+ return
264
+
265
+ # # log.debug("DUMP : {}".format(object_expr))
266
+ items, errors = self.get_obj_attributes(object_expr)
267
+
268
+ if errors:
269
+ log.error(errors)
270
+
271
+ for item_name, item_repr, item_type_txt, item_instance, _ in items:
272
+ # name_, repr_(value), type as text, item_instance, order
273
+ if item_name in ["classmethod", "staticmethod", "BaseException", "Exception"]:
274
+ # do not create stubs for these primitives
275
+ continue
276
+ if item_name[0].isdigit():
277
+ log.warning("NameError: invalid name {}".format(item_name))
278
+ continue
279
+ # Class expansion only on first 3 levels (bit of a hack)
280
+ if (
281
+ item_type_txt == "<class 'type'>"
282
+ and len(indent) <= _MAX_CLASS_LEVEL * 4
283
+ # and not obj_name.endswith(".Pin")
284
+ # avoid expansion of Pin.cpu / Pin.board to avoid crashes on most platforms
285
+ ):
286
+ # log.debug("{0}class {1}:".format(indent, item_name))
287
+ superclass = ""
288
+ is_exception = (
289
+ item_name.endswith("Exception")
290
+ or item_name.endswith("Error")
291
+ or item_name
292
+ in [
293
+ "KeyboardInterrupt",
294
+ "StopIteration",
295
+ "SystemExit",
296
+ ]
297
+ )
298
+ if is_exception:
299
+ superclass = "Exception"
300
+ s = "\n{}class {}({}):\n".format(indent, item_name, superclass)
301
+ # s += indent + " ''\n"
302
+ if is_exception:
303
+ s += indent + " ...\n"
304
+ fp.write(s)
305
+ continue
306
+ # write classdef
307
+ fp.write(s)
308
+ # first write the class literals and methods
309
+ # log.debug("# recursion over class {0}".format(item_name))
310
+ self.write_object_stub(
311
+ fp,
312
+ item_instance,
313
+ "{0}.{1}".format(obj_name, item_name),
314
+ indent + " ",
315
+ in_class + 1,
316
+ )
317
+ # end with the __init__ method to make sure that the literals are defined
318
+ # Add __init__
319
+ s = indent + " def __init__(self, *argv, **kwargs) -> None:\n"
320
+ s += indent + " ...\n\n"
321
+ fp.write(s)
322
+ elif any(word in item_type_txt for word in ["method", "function", "closure"]):
323
+ # log.debug("# def {1} function/method/closure, type = '{0}'".format(item_type_txt, item_name))
324
+ # module Function or class method
325
+ # will accept any number of params
326
+ # return type Any/Incomplete
327
+ ret = "Incomplete"
328
+ first = ""
329
+ # Self parameter only on class methods/functions
330
+ if in_class > 0:
331
+ first = "self, "
332
+ # class method - add function decoration
333
+ if "bound_method" in item_type_txt or "bound_method" in item_repr:
334
+ s = "{}@classmethod\n".format(indent) + "{}def {}(cls, *args, **kwargs) -> {}:\n".format(indent, item_name, ret)
335
+ else:
336
+ s = "{}def {}({}*args, **kwargs) -> {}:\n".format(indent, item_name, first, ret)
337
+ s += indent + " ...\n\n"
338
+ fp.write(s)
339
+ # log.debug("\n" + s)
340
+ elif item_type_txt == "<class 'module'>":
341
+ # Skip imported modules
342
+ # fp.write("# import {}\n".format(item_name))
343
+ pass
344
+
345
+ elif item_type_txt.startswith("<class '"):
346
+ t = item_type_txt[8:-2]
347
+ s = ""
348
+
349
+ if t in ("str", "int", "float", "bool", "bytearray", "bytes"):
350
+ # known type: use actual value
351
+ # s = "{0}{1} = {2} # type: {3}\n".format(indent, item_name, item_repr, t)
352
+ s = "{0}{1}: {3} = {2}\n".format(indent, item_name, item_repr, t)
353
+ elif t in ("dict", "list", "tuple"):
354
+ # dict, list , tuple: use empty value
355
+ ev = {"dict": "{}", "list": "[]", "tuple": "()"}
356
+ # s = "{0}{1} = {2} # type: {3}\n".format(indent, item_name, ev[t], t)
357
+ s = "{0}{1}: {3} = {2}\n".format(indent, item_name, ev[t], t)
358
+ else:
359
+ # something else
360
+ if t in ("object", "set", "frozenset", "Pin", "generator"): # "FileIO"
361
+ # https://docs.python.org/3/tutorial/classes.html#item_instance-objects
362
+ # use these types for the attribute
363
+ if t == "generator":
364
+ t = "Generator"
365
+ s = "{0}{1}: {2} ## = {4}\n".format(indent, item_name, t, item_type_txt, item_repr)
366
+ else:
367
+ # Requires Python 3.6 syntax, which is OK for the stubs/pyi
368
+ t = "Incomplete"
369
+ if " at " in item_repr:
370
+ item_repr = item_repr.split(" at ")[0] + " at ...>"
371
+ if " at " in item_repr:
372
+ item_repr = item_repr.split(" at ")[0] + " at ...>"
373
+ s = "{0}{1}: {2} ## {3} = {4}\n".format(indent, item_name, t, item_type_txt, item_repr)
374
+ fp.write(s)
375
+ # log.debug("\n" + s)
376
+ else:
377
+ # keep only the name
378
+ # log.debug("# all other, type = '{0}'".format(item_type_txt))
379
+ fp.write("# all other, type = '{0}'\n".format(item_type_txt))
380
+
381
+ fp.write(indent + item_name + " # type: Incomplete\n")
382
+
383
+ # del items
384
+ # del errors
385
+ # try:
386
+ # del item_name, item_repr, item_type_txt, item_instance # type: ignore
387
+ # except (OSError, KeyError, NameError):
388
+ # pass
389
+
390
+ @property
391
+ def flat_fwid(self):
392
+ "Turn _fwid from 'v1.2.3' into '1_2_3' to be used in filename"
393
+ s = self._fwid
394
+ # path name restrictions
395
+ chars = " .()/\\:$"
396
+ for c in chars:
397
+ s = s.replace(c, "_")
398
+ return s
399
+
400
+ def clean(self, path: str = None): # type: ignore
401
+ "Remove all files from the stub folder"
402
+ if path is None:
403
+ path = self.path
404
+ log.info("Clean/remove files in folder: {}".format(path))
405
+ try:
406
+ os.stat(path) # TEMP workaround mpremote listdir bug -
407
+ items = os.listdir(path)
408
+ except (OSError, AttributeError):
409
+ # os.listdir fails on unix
410
+ return
411
+ for fn in items:
412
+ item = "{}/{}".format(path, fn)
413
+ try:
414
+ os.remove(item)
415
+ except OSError:
416
+ try: # folder
417
+ self.clean(item)
418
+ os.rmdir(item)
419
+ except OSError:
420
+ pass
421
+
422
+ def report_start(self, filename: str = "modules.json"):
423
+ """Start a report of the modules that have been stubbed
424
+ "create json with list of exported modules"""
425
+ self._json_name = "{}/{}".format(self.path, filename)
426
+ self._json_first = True
427
+ ensure_folder(self._json_name)
428
+ log.info("Report file: {}".format(self._json_name))
429
+ gc.collect()
430
+ try:
431
+ # write json by node to reduce memory requirements
432
+ with open(self._json_name, "w") as f:
433
+ f.write("{")
434
+ f.write(dumps({"firmware": self.info})[1:-1])
435
+ f.write(",\n")
436
+ f.write(dumps({"stubber": {"version": __version__}, "stubtype": "firmware"})[1:-1])
437
+ f.write(",\n")
438
+ f.write('"modules" :[\n')
439
+
440
+ except OSError as e:
441
+ log.error("Failed to create the report.")
442
+ self._json_name = None
443
+ raise e
444
+
445
+ def report_add(self, module_name: str, stub_file: str):
446
+ "Add a module to the report"
447
+ # write json by node to reduce memory requirements
448
+ if not self._json_name:
449
+ raise Exception("No report file")
450
+ try:
451
+ with open(self._json_name, "a") as f:
452
+ if not self._json_first:
453
+ f.write(",\n")
454
+ else:
455
+ self._json_first = False
456
+ line = '{{"module": "{}", "file": "{}"}}'.format(module_name, stub_file.replace("\\", "/"))
457
+ f.write(line)
458
+
459
+ except OSError:
460
+ log.error("Failed to create the report.")
461
+
462
+ def report_end(self):
463
+ if not self._json_name:
464
+ raise Exception("No report file")
465
+ with open(self._json_name, "a") as f:
466
+ f.write("\n]}")
467
+ # is used as sucess indicator
468
+ log.info("Path: {}".format(self.path))
469
+
470
+
471
+ def ensure_folder(path: str):
472
+ "Create nested folders if needed"
473
+ i = start = 0
474
+ while i != -1:
475
+ i = path.find("/", start)
476
+ if i != -1:
477
+ p = path[0] if i == 0 else path[:i]
478
+ # p = partial folder
479
+ try:
480
+ _ = os.stat(p)
481
+ except OSError as e:
482
+ # folder does not exist
483
+ if e.args[0] == ENOENT:
484
+ try:
485
+ os.mkdir(p)
486
+ except OSError as e2:
487
+ log.error("failed to create folder {}".format(p))
488
+ raise e2
489
+ # next level deep
490
+ start = i + 1
491
+
492
+
493
+ def _build(s):
494
+ # extract build from sys.version or os.uname().version if available
495
+ # sys.version: 'MicroPython v1.23.0-preview.6.g3d0b6276f'
496
+ # sys.implementation.version: 'v1.13-103-gb137d064e'
497
+ if not s:
498
+ return ""
499
+ s = s.split(" on ", 1)[0] if " on " in s else s
500
+ if s.startswith("v"):
501
+ if not "-" in s:
502
+ return ""
503
+ b = s.split("-")[1]
504
+ return b
505
+ if not "-preview" in s:
506
+ return ""
507
+ b = s.split("-preview")[1].split(".")[1]
508
+ return b
509
+
510
+
511
+ def _info(): # type:() -> dict[str, str]
512
+ try:
513
+ fam = sys.implementation[0] # type: ignore
514
+ except TypeError:
515
+ # testing on CPython 3.11
516
+ fam = sys.implementation.name
517
+
518
+ info = OrderedDict(
519
+ {
520
+ "family": fam,
521
+ "version": "",
522
+ "build": "",
523
+ "ver": "",
524
+ "port": sys.platform, # port: esp32 / win32 / linux / stm32
525
+ "board": "UNKNOWN",
526
+ "cpu": "",
527
+ "mpy": "",
528
+ "arch": "",
529
+ }
530
+ )
531
+ # change port names to be consistent with the repo
532
+ if info["port"].startswith("pyb"):
533
+ info["port"] = "stm32"
534
+ elif info["port"] == "win32":
535
+ info["port"] = "windows"
536
+ elif info["port"] == "linux":
537
+ info["port"] = "unix"
538
+ try:
539
+ info["version"] = version_str(sys.implementation.version) # type: ignore
540
+ except AttributeError:
541
+ pass
542
+ try:
543
+ _machine = sys.implementation._machine if "_machine" in dir(sys.implementation) else os.uname().machine # type: ignore
544
+ # info["board"] = "with".join(_machine.split("with")[:-1]).strip()
545
+ info["board"] = _machine
546
+ info["cpu"] = _machine.split("with")[-1].strip()
547
+ info["mpy"] = (
548
+ sys.implementation._mpy # type: ignore
549
+ if "_mpy" in dir(sys.implementation)
550
+ else sys.implementation.mpy if "mpy" in dir(sys.implementation) else "" # type: ignore
551
+ )
552
+ except (AttributeError, IndexError):
553
+ pass
554
+ info["board"] = get_boardname()
555
+
556
+ try:
557
+ if "uname" in dir(os): # old
558
+ # extract build from uname().version if available
559
+ info["build"] = _build(os.uname()[3]) # type: ignore
560
+ if not info["build"]:
561
+ # extract build from uname().release if available
562
+ info["build"] = _build(os.uname()[2]) # type: ignore
563
+ elif "version" in dir(sys): # new
564
+ # extract build from sys.version if available
565
+ info["build"] = _build(sys.version)
566
+ except (AttributeError, IndexError, TypeError):
567
+ pass
568
+ # avoid build hashes
569
+ # if info["build"] and len(info["build"]) > 5:
570
+ # info["build"] = ""
571
+
572
+ if info["version"] == "" and sys.platform not in ("unix", "win32"):
573
+ try:
574
+ u = os.uname() # type: ignore
575
+ info["version"] = u.release
576
+ except (IndexError, AttributeError, TypeError):
577
+ pass
578
+ # detect families
579
+ for fam_name, mod_name, mod_thing in [
580
+ ("pycopy", "pycopy", "const"),
581
+ ("pycom", "pycom", "FAT"),
582
+ ("ev3-pybricks", "pybricks.hubs", "EV3Brick"),
583
+ ]:
584
+ try:
585
+ _t = __import__(mod_name, None, None, (mod_thing))
586
+ info["family"] = fam_name
587
+ del _t
588
+ break
589
+ except (ImportError, KeyError):
590
+ pass
591
+
592
+ if info["family"] == "ev3-pybricks":
593
+ info["release"] = "2.0.0"
594
+
595
+ if info["family"] == "micropython":
596
+ info["version"]
597
+ if (
598
+ info["version"]
599
+ and info["version"].endswith(".0")
600
+ and info["version"] >= "1.10.0" # versions from 1.10.0 to 1.20.6 do not have a micro .0
601
+ and info["version"] <= "1.19.9"
602
+ ):
603
+ # versions from 1.10.0 to 1.20.6 do not have a micro .0
604
+ info["version"] = info["version"][:-2]
605
+
606
+ # spell-checker: disable
607
+ if "mpy" in info and info["mpy"]: # mpy on some v1.11+ builds
608
+ sys_mpy = int(info["mpy"])
609
+ # .mpy architecture
610
+ arch = [
611
+ None,
612
+ "x86",
613
+ "x64",
614
+ "armv6",
615
+ "armv6m",
616
+ "armv7m",
617
+ "armv7em",
618
+ "armv7emsp",
619
+ "armv7emdp",
620
+ "xtensa",
621
+ "xtensawin",
622
+ ][sys_mpy >> 10]
623
+ if arch:
624
+ info["arch"] = arch
625
+ # .mpy version.minor
626
+ info["mpy"] = "v{}.{}".format(sys_mpy & 0xFF, sys_mpy >> 8 & 3)
627
+ if info["build"] and not info["version"].endswith("-preview"):
628
+ info["version"] = info["version"] + "-preview"
629
+ # simple to use version[-build] string
630
+ info["ver"] = f"{info['version']}-{info['build']}" if info["build"] else f"{info['version']}"
631
+
632
+ return info
633
+
634
+
635
+ def version_str(version: tuple): # -> str:
636
+ v_str = ".".join([str(n) for n in version[:3]])
637
+ if len(version) > 3 and version[3]:
638
+ v_str += "-" + version[3]
639
+ return v_str
640
+
641
+
642
+ def get_boardname() -> str:
643
+ "Read the board name from the boardname.py file that may have been created upfront"
644
+ try:
645
+ from boardname import BOARDNAME # type: ignore
646
+
647
+ log.info("Found BOARDNAME: {}".format(BOARDNAME))
648
+ except ImportError:
649
+ log.warning("BOARDNAME not found")
650
+ BOARDNAME = ""
651
+ return BOARDNAME
652
+
653
+
654
+ def get_root() -> str: # sourcery skip: use-assigned-variable
655
+ "Determine the root folder of the device"
656
+ try:
657
+ c = os.getcwd()
658
+ except (OSError, AttributeError):
659
+ # unix port
660
+ c = "."
661
+ r = c
662
+ for r in [c, "/sd", "/flash", "/", "."]:
663
+ try:
664
+ _ = os.stat(r)
665
+ break
666
+ except OSError:
667
+ continue
668
+ return r
669
+
670
+
671
+ def file_exists(filename: str):
672
+ try:
673
+ if os.stat(filename)[0] >> 14:
674
+ return True
675
+ return False
676
+ except OSError:
677
+ return False
678
+
679
+
680
+ def show_help():
681
+ print("-p, --path path to store the stubs in, defaults to '.'")
682
+ sys.exit(1)
683
+
684
+
685
+ def read_path() -> str:
686
+ "get --path from cmdline. [unix/win]"
687
+ path = ""
688
+ if len(sys.argv) == 3:
689
+ cmd = (sys.argv[1]).lower()
690
+ if cmd in ("--path", "-p"):
691
+ path = sys.argv[2]
692
+ else:
693
+ show_help()
694
+ elif len(sys.argv) == 2:
695
+ show_help()
696
+ return path
697
+
698
+
699
+ def is_micropython() -> bool:
700
+ "runtime test to determine full or micropython"
701
+ # pylint: disable=unused-variable,eval-used
702
+ try:
703
+ # either test should fail on micropython
704
+
705
+ # b) https://docs.micropython.org/en/latest/genrst/builtin_types.html#bytes-with-keywords-not-implemented
706
+ # Micropython: NotImplementedError
707
+ b = bytes("abc", encoding="utf8") # type: ignore # lgtm [py/unused-local-variable]
708
+
709
+ # c) https://docs.micropython.org/en/latest/genrst/core_language.html#function-objects-do-not-have-the-module-attribute
710
+ # Micropython: AttributeError
711
+ c = is_micropython.__module__ # type: ignore # lgtm [py/unused-local-variable]
712
+ return False
713
+ except (NotImplementedError, AttributeError):
714
+ return True
715
+
716
+
717
+ def main():
718
+ stubber = Stubber(path=read_path())
719
+ # stubber = Stubber(path="/sd")
720
+ # Option: Specify a firmware name & version
721
+ # stubber = Stubber(firmware_id='HoverBot v1.2.1')
722
+ stubber.clean()
723
+
724
+ # Read stubs from modulelist in the current folder or in /libs
725
+ # fall back to default modules
726
+ def get_modulelist(stubber):
727
+ # new
728
+ gc.collect()
729
+ stubber.modules = [] # avoid duplicates
730
+ for p in LIBS:
731
+ fname = p + "/modulelist.txt"
732
+ if not file_exists(fname):
733
+ continue
734
+ with open(fname) as f:
735
+ # print("DEBUG: list of modules: " + p + "/modulelist.txt")
736
+ while True:
737
+ line = f.readline().strip()
738
+ if not line:
739
+ break
740
+ if len(line) > 0 and line[0] != "#":
741
+ stubber.modules.append(line)
742
+ gc.collect()
743
+ print("BREAK")
744
+ break
745
+
746
+ if not stubber.modules:
747
+ stubber.modules = ["micropython"]
748
+ # _log.warn("Could not find modulelist.txt, using default modules")
749
+ gc.collect()
750
+
751
+ stubber.modules = [] # avoid duplicates
752
+ get_modulelist(stubber)
753
+
754
+ gc.collect()
755
+
756
+ stubber.create_all_stubs()
757
+
758
+
759
+ if __name__ == "__main__" or is_micropython():
760
+ if not file_exists("no_auto_stubber.txt"):
761
+ try:
762
+ gc.threshold(4 * 1024) # type: ignore
763
+ gc.enable()
764
+ except BaseException:
765
+ pass
766
+ main()