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,610 +1,610 @@
1
- #!/usr/bin/env python3
2
- #
3
- # This file is part of the MicroPython project, http://micropython.org/
4
- #
5
- # The MIT License (MIT)
6
- #
7
- # Copyright (c) 2022 Jim Mussared
8
- # Copyright (c) 2019 Damien P. George
9
- #
10
- # Permission is hereby granted, free of charge, to any person obtaining a copy
11
- # of this software and associated documentation files (the "Software"), to deal
12
- # in the Software without restriction, including without limitation the rights
13
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- # copies of the Software, and to permit persons to whom the Software is
15
- # furnished to do so, subject to the following conditions:
16
- #
17
- # The above copyright notice and this permission notice shall be included in
18
- # all copies or substantial portions of the Software.
19
- #
20
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26
- # THE SOFTWARE.
27
-
28
- from __future__ import print_function
29
-
30
- import contextlib
31
- import os
32
- import sys
33
- import tempfile
34
- from collections import namedtuple
35
-
36
- __all__ = ["ManifestFileError", "ManifestFile"]
37
-
38
- # Allow freeze*() etc.
39
- MODE_FREEZE = 1
40
- # Only allow include/require/module/package.
41
- MODE_COMPILE = 2
42
- # Same as compile, but handles require(..., pypi="name") as a requirements.txt entry.
43
- MODE_PYPROJECT = 3
44
-
45
- # In compile mode, .py -> KIND_COMPILE_AS_MPY
46
- # In freeze mode, .py -> KIND_FREEZE_AS_MPY, .mpy->KIND_FREEZE_MPY
47
- KIND_AUTO = 1
48
- # Freeze-mode only, .py -> KIND_FREEZE_AS_MPY, .mpy->KIND_FREEZE_MPY
49
- KIND_FREEZE_AUTO = 2
50
-
51
- # Freeze-mode only, The .py file will be frozen as text.
52
- KIND_FREEZE_AS_STR = 3
53
- # Freeze-mode only, The .py file will be compiled and frozen as bytecode.
54
- KIND_FREEZE_AS_MPY = 4
55
- # Freeze-mode only, The .mpy file will be frozen directly.
56
- KIND_FREEZE_MPY = 5
57
- # Compile mode only, the .py file should be compiled to .mpy.
58
- KIND_COMPILE_AS_MPY = 6
59
-
60
- # File on the local filesystem.
61
- FILE_TYPE_LOCAL = 1
62
- # URL to file. (TODO)
63
- FILE_TYPE_HTTP = 2
64
-
65
-
66
- class ManifestFileError(Exception):
67
- pass
68
-
69
-
70
- class ManifestIgnoreException(Exception):
71
- pass
72
-
73
-
74
- class ManifestUsePyPIException(Exception):
75
- def __init__(self, pypi_name):
76
- self.pypi_name = pypi_name
77
-
78
-
79
- # The set of files that this manifest references.
80
- ManifestOutput = namedtuple(
81
- "ManifestOutput",
82
- [
83
- "file_type", # FILE_TYPE_*.
84
- "full_path", # The input file full path.
85
- "target_path", # The target path on the device.
86
- "timestamp", # Last modified date of the input file.
87
- "kind", # KIND_*.
88
- "metadata", # Metadata for the containing package.
89
- "opt", # Optimisation level (or None).
90
- ],
91
- )
92
-
93
-
94
- # Represents the metadata for a package.
95
- class ManifestPackageMetadata:
96
- def __init__(self, is_require=False):
97
- self._is_require = is_require
98
- self._initialised = False
99
-
100
- self.version = None
101
- self.description = None
102
- self.license = None
103
- self.author = None
104
-
105
- # Annotate a package as being from the python standard library.
106
- self.stdlib = False
107
-
108
- # Allows a python-ecosys package to be annotated with the
109
- # corresponding name in PyPI. e.g. micropython-lib/urequests is based
110
- # on pypi/requests.
111
- self.pypi = None
112
- # For a micropython package, this is the name that we will publish it
113
- # to PyPI as. e.g. micropython-lib/senml publishes as
114
- # pypi/micropython-senml.
115
- self.pypi_publish = None
116
-
117
- def update(
118
- self,
119
- mode,
120
- description=None,
121
- version=None,
122
- license=None,
123
- author=None,
124
- stdlib=False,
125
- pypi=None,
126
- pypi_publish=None,
127
- ):
128
- if self._initialised:
129
- raise ManifestFileError("Duplicate call to metadata().")
130
-
131
- # In MODE_PYPROJECT, if this manifest is being evaluated as a result
132
- # of a require(), then figure out if it should be replaced by a PyPI
133
- # dependency instead.
134
- if mode == MODE_PYPROJECT and self._is_require:
135
- if stdlib:
136
- # No dependency required at all for CPython.
137
- raise ManifestIgnoreException
138
- if pypi_publish or pypi:
139
- # In the case where a package is both based on a PyPI package and
140
- # provides one, preference depending on the published one.
141
- # (This should be pretty rare).
142
- raise ManifestUsePyPIException(pypi_publish or pypi)
143
-
144
- self.description = description
145
- self.version = version
146
- self.license = version
147
- self.author = author
148
- self.pypi = pypi
149
- self.pypi_publish = pypi_publish
150
- self._initialised = True
151
-
152
- def check_initialised(self, mode):
153
- # Ensure that metadata() is the first thing a manifest.py does.
154
- # This is to ensure that we early-exit if it should be replaced by a pypi dependency.
155
- if mode in (MODE_COMPILE, MODE_PYPROJECT):
156
- if not self._initialised:
157
- raise ManifestFileError("metadata() must be the first command in a manifest file.")
158
-
159
- def __str__(self):
160
- return "version={} description={} license={} author={} pypi={} pypi_publish={}".format(
161
- self.version, self.description, self.license, self.author, self.pypi, self.pypi_publish
162
- )
163
-
164
-
165
- # Turns a dict of options into a object with attributes used to turn the
166
- # kwargs passed to include() and require into the "options" global in the
167
- # included manifest.
168
- # options = IncludeOptions(foo="bar", blah="stuff")
169
- # options.foo # "bar"
170
- # options.blah # "stuff"
171
- class IncludeOptions:
172
- def __init__(self, **kwargs):
173
- self._kwargs = kwargs
174
- self._defaults = {}
175
-
176
- def defaults(self, **kwargs):
177
- self._defaults = kwargs
178
-
179
- def __getattr__(self, name):
180
- return self._kwargs.get(name, self._defaults.get(name, None))
181
-
182
-
183
- class ManifestFile:
184
- def __init__(self, mode, path_vars=None):
185
- # See MODE_* constants above.
186
- self._mode = mode
187
- # Path substitution variables.
188
- self._path_vars = path_vars or {}
189
- # List of files (as ManifestFileResult) references by this manifest.
190
- self._manifest_files = []
191
- # List of PyPI dependencies (when mode=MODE_PYPROJECT).
192
- self._pypi_dependencies = []
193
- # Don't allow including the same file twice.
194
- self._visited = set()
195
- # Stack of metadata for each level.
196
- self._metadata = [ManifestPackageMetadata()]
197
-
198
- def _resolve_path(self, path):
199
- # Convert path to an absolute path, applying variable substitutions.
200
- for name, value in self._path_vars.items():
201
- if value is not None:
202
- path = path.replace("$({})".format(name), value)
203
- return os.path.abspath(path)
204
-
205
- def _manifest_globals(self, kwargs):
206
- # This is the "API" available to a manifest file.
207
- g = {
208
- "metadata": self.metadata,
209
- "include": self.include,
210
- "require": self.require,
211
- "package": self.package,
212
- "module": self.module,
213
- "options": IncludeOptions(**kwargs),
214
- }
215
-
216
- # Extra legacy functions only for freeze mode.
217
- if self._mode == MODE_FREEZE:
218
- g.update(
219
- {
220
- "freeze": self.freeze,
221
- "freeze_as_str": self.freeze_as_str,
222
- "freeze_as_mpy": self.freeze_as_mpy,
223
- "freeze_mpy": self.freeze_mpy,
224
- }
225
- )
226
-
227
- return g
228
-
229
- def files(self):
230
- return self._manifest_files
231
-
232
- def pypi_dependencies(self):
233
- # In MODE_PYPROJECT, this will return a list suitable for requirements.txt.
234
- return self._pypi_dependencies
235
-
236
- def execute(self, manifest_file):
237
- if manifest_file.endswith(".py"):
238
- # Execute file from filesystem.
239
- self.include(manifest_file)
240
- else:
241
- # Execute manifest code snippet.
242
- try:
243
- exec(manifest_file, self._manifest_globals({}))
244
- except Exception as er:
245
- raise ManifestFileError("Error in manifest: {}".format(er))
246
-
247
- def _add_file(self, full_path, target_path, kind=KIND_AUTO, opt=None):
248
- # Check file exists and get timestamp.
249
- try:
250
- stat = os.stat(full_path)
251
- timestamp = stat.st_mtime
252
- except OSError:
253
- raise ManifestFileError("Cannot stat {}".format(full_path))
254
-
255
- # Map the AUTO kinds to their actual kind based on mode and extension.
256
- _, ext = os.path.splitext(full_path)
257
- if self._mode == MODE_FREEZE:
258
- if kind in (
259
- KIND_AUTO,
260
- KIND_FREEZE_AUTO,
261
- ):
262
- if ext.lower() == ".py":
263
- kind = KIND_FREEZE_AS_MPY
264
- elif ext.lower() == ".mpy":
265
- kind = KIND_FREEZE_MPY
266
- else:
267
- if kind != KIND_AUTO:
268
- raise ManifestFileError("Not in freeze mode")
269
- if ext.lower() != ".py":
270
- raise ManifestFileError("Expected .py file")
271
- kind = KIND_COMPILE_AS_MPY
272
-
273
- self._manifest_files.append(
274
- ManifestOutput(FILE_TYPE_LOCAL, full_path, target_path, timestamp, kind, self._metadata[-1], opt)
275
- )
276
-
277
- def _search(self, base_path, package_path, files, exts, kind, opt=None, strict=False):
278
- base_path = self._resolve_path(base_path)
279
-
280
- if files:
281
- # Use explicit list of files (relative to package_path).
282
- for file in files:
283
- if package_path:
284
- file = os.path.join(package_path, file)
285
- self._add_file(os.path.join(base_path, file), file, kind=kind, opt=opt)
286
- else:
287
- if base_path:
288
- prev_cwd = os.getcwd()
289
- os.chdir(self._resolve_path(base_path))
290
-
291
- # Find all candidate files.
292
- for dirpath, _, filenames in os.walk(package_path or ".", followlinks=True):
293
- for file in filenames:
294
- file = os.path.relpath(os.path.join(dirpath, file), ".")
295
- _, ext = os.path.splitext(file)
296
- if ext.lower() in exts:
297
- self._add_file(
298
- os.path.join(base_path, file),
299
- file,
300
- kind=kind,
301
- opt=opt,
302
- )
303
- elif strict:
304
- raise ManifestFileError("Unexpected file type")
305
-
306
- if base_path:
307
- os.chdir(prev_cwd)
308
-
309
- def metadata(self, **kwargs):
310
- """
311
- From within a manifest file, use this to set the metadata for the
312
- package described by current manifest.
313
-
314
- After executing a manifest file (via execute()), call this
315
- to obtain the metadata for the top-level manifest file.
316
-
317
- See ManifestPackageMetadata.update() for valid kwargs.
318
- """
319
- if kwargs:
320
- self._metadata[-1].update(self._mode, **kwargs)
321
- return self._metadata[-1]
322
-
323
- def include(self, manifest_path, is_require=False, **kwargs):
324
- """
325
- Include another manifest.
326
-
327
- The manifest argument can be a string (filename) or an iterable of
328
- strings.
329
-
330
- Relative paths are resolved with respect to the current manifest file.
331
-
332
- If the path is to a directory, then it implicitly includes the
333
- manifest.py file inside that directory.
334
-
335
- Optional kwargs can be provided which will be available to the
336
- included script via the `options` variable.
337
-
338
- e.g. include("path.py", extra_features=True)
339
-
340
- in path.py:
341
- options.defaults(standard_features=True)
342
-
343
- # freeze minimal modules.
344
- if options.standard_features:
345
- # freeze standard modules.
346
- if options.extra_features:
347
- # freeze extra modules.
348
- """
349
- if is_require:
350
- self._metadata[-1].check_initialised(self._mode)
351
-
352
- if not isinstance(manifest_path, str):
353
- for m in manifest_path:
354
- self.include(m, **kwargs)
355
- else:
356
- manifest_path = self._resolve_path(manifest_path)
357
- # Including a directory grabs the manifest.py inside it.
358
- if os.path.isdir(manifest_path):
359
- manifest_path = os.path.join(manifest_path, "manifest.py")
360
- if manifest_path in self._visited:
361
- return
362
- self._visited.add(manifest_path)
363
- if is_require:
364
- # This include is the result of require("name"), so push a new
365
- # package metadata onto the stack.
366
- self._metadata.append(ManifestPackageMetadata(is_require=True))
367
- try:
368
- with open(manifest_path) as f:
369
- # Make paths relative to this manifest file while processing it.
370
- # Applies to includes and input files.
371
- prev_cwd = os.getcwd()
372
- os.chdir(os.path.dirname(manifest_path))
373
- try:
374
- exec(f.read(), self._manifest_globals(kwargs))
375
- finally:
376
- os.chdir(prev_cwd)
377
- except ManifestIgnoreException:
378
- # e.g. MODE_PYPROJECT and this was a stdlib dependency. No-op.
379
- pass
380
- except ManifestUsePyPIException as e:
381
- # e.g. MODE_PYPROJECT and this was a package from
382
- # python-ecosys. Add PyPI dependency instead.
383
- self._pypi_dependencies.append(e.pypi_name)
384
- except Exception as e:
385
- raise ManifestFileError("Error in manifest file: {}: {}".format(manifest_path, e))
386
- if is_require:
387
- self._metadata.pop()
388
-
389
- def require(self, name, version=None, unix_ffi=False, pypi=None, **kwargs):
390
- """
391
- Require a module by name from micropython-lib.
392
-
393
- Optionally specify unix_ffi=True to use a module from the unix-ffi directory.
394
-
395
- Optionally specify pipy="package-name" to indicate that this should
396
- use the named package from PyPI when building for CPython.
397
- """
398
- self._metadata[-1].check_initialised(self._mode)
399
-
400
- if self._mode == MODE_PYPROJECT and pypi:
401
- # In PYPROJECT mode, allow overriding the PyPI dependency name
402
- # explicitly. Otherwise if the dependent package has metadata
403
- # (pypi_publish) or metadata(pypi) we will use that.
404
- self._pypi_dependencies.append(pypi)
405
- return
406
-
407
- if self._path_vars["MPY_LIB_DIR"]:
408
- lib_dirs = ["micropython", "python-stdlib", "python-ecosys"]
409
- if unix_ffi:
410
- # Search unix-ffi only if unix_ffi=True, and make unix-ffi modules
411
- # take precedence.
412
- lib_dirs = ["unix-ffi"] + lib_dirs
413
-
414
- for lib_dir in lib_dirs:
415
- # Search for {lib_dir}/**/{name}/manifest.py.
416
- for root, _, filenames in os.walk(os.path.join(self._path_vars["MPY_LIB_DIR"], lib_dir)):
417
- if os.path.basename(root) == name and "manifest.py" in filenames:
418
- self.include(root, is_require=True, **kwargs)
419
- return
420
-
421
- raise ValueError("Library not found in local micropython-lib: {}".format(name))
422
- else:
423
- # TODO: HTTP request to obtain URLs from manifest.json.
424
- raise ValueError("micropython-lib not available for require('{}').", name)
425
-
426
- def package(self, package_path, files=None, base_path=".", opt=None):
427
- """
428
- Define a package, optionally restricting to a set of files.
429
-
430
- Simple case, a package in the current directory:
431
- package("foo")
432
- will include all .py files in foo, and will be stored as foo/bar/baz.py.
433
-
434
- If the package isn't in the current directory, use base_path:
435
- package("foo", base_path="src")
436
-
437
- To restrict to certain files in the package use files (note: paths should be relative to the package):
438
- package("foo", files=["bar/baz.py"])
439
- """
440
- self._metadata[-1].check_initialised(self._mode)
441
-
442
- # Include "base_path/package_path/**/*.py" --> "package_path/**/*.py"
443
- self._search(base_path, package_path, files, exts=(".py",), kind=KIND_AUTO, opt=opt)
444
-
445
- def module(self, module_path, base_path=".", opt=None):
446
- """
447
- Include a single Python file as a module.
448
-
449
- If the file is in the current directory:
450
- module("foo.py")
451
-
452
- Otherwise use base_path to locate the file:
453
- module("foo.py", "src/drivers")
454
- """
455
- self._metadata[-1].check_initialised(self._mode)
456
-
457
- # Include "base_path/module_path" --> "module_path"
458
- base_path = self._resolve_path(base_path)
459
- _, ext = os.path.splitext(module_path)
460
- if ext.lower() != ".py":
461
- raise ManifestFileError("module must be .py file")
462
- # TODO: version None
463
- self._add_file(os.path.join(base_path, module_path), module_path, opt=opt)
464
-
465
- def _freeze_internal(self, path, script, exts, kind, opt):
466
- if script is None:
467
- self._search(path, None, None, exts=exts, kind=kind, opt=opt)
468
- elif isinstance(script, str) and os.path.isdir(os.path.join(path, script)):
469
- self._search(path, script, None, exts=exts, kind=kind, opt=opt)
470
- elif not isinstance(script, str):
471
- self._search(path, None, script, exts=exts, kind=kind, opt=opt)
472
- else:
473
- self._search(path, None, (script,), exts=exts, kind=kind, opt=opt)
474
-
475
- def freeze(self, path, script=None, opt=None):
476
- """
477
- Freeze the input, automatically determining its type. A .py script
478
- will be compiled to a .mpy first then frozen, and a .mpy file will be
479
- frozen directly.
480
-
481
- `path` must be a directory, which is the base directory to _search for
482
- files from. When importing the resulting frozen modules, the name of
483
- the module will start after `path`, ie `path` is excluded from the
484
- module name.
485
-
486
- If `path` is relative, it is resolved to the current manifest.py.
487
- Use $(MPY_DIR), $(MPY_LIB_DIR), $(PORT_DIR), $(BOARD_DIR) if you need
488
- to access specific paths.
489
-
490
- If `script` is None all files in `path` will be frozen.
491
-
492
- If `script` is an iterable then freeze() is called on all items of the
493
- iterable (with the same `path` and `opt` passed through).
494
-
495
- If `script` is a string then it specifies the file or directory to
496
- freeze, and can include extra directories before the file or last
497
- directory. The file or directory will be _searched for in `path`. If
498
- `script` is a directory then all files in that directory will be frozen.
499
-
500
- `opt` is the optimisation level to pass to mpy-cross when compiling .py
501
- to .mpy.
502
- """
503
- self._freeze_internal(
504
- path,
505
- script,
506
- exts=(
507
- ".py",
508
- ".mpy",
509
- ),
510
- kind=KIND_FREEZE_AUTO,
511
- opt=opt,
512
- )
513
-
514
- def freeze_as_str(self, path):
515
- """
516
- Freeze the given `path` and all .py scripts within it as a string,
517
- which will be compiled upon import.
518
- """
519
- self._search(path, None, None, exts=(".py",), kind=KIND_FREEZE_AS_STR)
520
-
521
- def freeze_as_mpy(self, path, script=None, opt=None):
522
- """
523
- Freeze the input (see above) by first compiling the .py scripts to
524
- .mpy files, then freezing the resulting .mpy files.
525
- """
526
- self._freeze_internal(path, script, exts=(".py",), kind=KIND_FREEZE_AS_MPY, opt=opt)
527
-
528
- def freeze_mpy(self, path, script=None, opt=None):
529
- """
530
- Freeze the input (see above), which must be .mpy files that are
531
- frozen directly.
532
- """
533
- self._freeze_internal(path, script, exts=(".mpy",), kind=KIND_FREEZE_MPY, opt=opt)
534
-
535
-
536
- # Generate a temporary file with a line appended to the end that adds __version__.
537
- @contextlib.contextmanager
538
- def tagged_py_file(path, metadata):
539
- dest_fd, dest_path = tempfile.mkstemp(suffix=".py", text=True)
540
- try:
541
- with os.fdopen(dest_fd, "w") as dest:
542
- with open(path, "r") as src:
543
- contents = src.read()
544
- dest.write(contents)
545
-
546
- # Don't overwrite a version definition if the file already has one in it.
547
- if metadata.version and "__version__ =" not in contents:
548
- dest.write("\n\n__version__ = {}\n".format(repr(metadata.version)))
549
- yield dest_path
550
- finally:
551
- os.unlink(dest_path)
552
-
553
-
554
- def main():
555
- import argparse
556
-
557
- cmd_parser = argparse.ArgumentParser(description="List the files referenced by a manifest.")
558
- cmd_parser.add_argument("--freeze", action="store_true", help="freeze mode")
559
- cmd_parser.add_argument("--compile", action="store_true", help="compile mode")
560
- cmd_parser.add_argument("--pyproject", action="store_true", help="pyproject mode")
561
- cmd_parser.add_argument(
562
- "--lib",
563
- default=os.path.join(os.path.dirname(__file__), "../lib/micropython-lib"),
564
- help="path to micropython-lib repo",
565
- )
566
- cmd_parser.add_argument("--port", default=None, help="path to port dir")
567
- cmd_parser.add_argument("--board", default=None, help="path to board dir")
568
- cmd_parser.add_argument(
569
- "--top",
570
- default=os.path.join(os.path.dirname(__file__), ".."),
571
- help="path to micropython repo",
572
- )
573
- cmd_parser.add_argument("files", nargs="+", help="input manifest.py")
574
- args = cmd_parser.parse_args()
575
-
576
- path_vars = {
577
- "MPY_DIR": os.path.abspath(args.top) if args.top else None,
578
- "BOARD_DIR": os.path.abspath(args.board) if args.board else None,
579
- "PORT_DIR": os.path.abspath(args.port) if args.port else None,
580
- "MPY_LIB_DIR": os.path.abspath(args.lib) if args.lib else None,
581
- }
582
-
583
- mode = None
584
- if args.freeze:
585
- mode = MODE_FREEZE
586
- elif args.compile:
587
- mode = MODE_COMPILE
588
- elif args.pyproject:
589
- mode = MODE_PYPROJECT
590
- else:
591
- print("Error: No mode specified.", file=sys.stderr)
592
- exit(1)
593
-
594
- m = ManifestFile(mode, path_vars)
595
- for manifest_file in args.files:
596
- try:
597
- m.execute(manifest_file)
598
- except ManifestFileError as er:
599
- print(er, file=sys.stderr)
600
- exit(1)
601
- print(m.metadata())
602
- for f in m.files():
603
- print(f)
604
- if mode == MODE_PYPROJECT:
605
- for r in m.pypi_dependencies():
606
- print("pypi-require:", r)
607
-
608
-
609
- if __name__ == "__main__":
610
- main()
1
+ #!/usr/bin/env python3
2
+ #
3
+ # This file is part of the MicroPython project, http://micropython.org/
4
+ #
5
+ # The MIT License (MIT)
6
+ #
7
+ # Copyright (c) 2022 Jim Mussared
8
+ # Copyright (c) 2019 Damien P. George
9
+ #
10
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ # of this software and associated documentation files (the "Software"), to deal
12
+ # in the Software without restriction, including without limitation the rights
13
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ # copies of the Software, and to permit persons to whom the Software is
15
+ # furnished to do so, subject to the following conditions:
16
+ #
17
+ # The above copyright notice and this permission notice shall be included in
18
+ # all copies or substantial portions of the Software.
19
+ #
20
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26
+ # THE SOFTWARE.
27
+
28
+ from __future__ import print_function
29
+
30
+ import contextlib
31
+ import os
32
+ import sys
33
+ import tempfile
34
+ from collections import namedtuple
35
+
36
+ __all__ = ["ManifestFileError", "ManifestFile"]
37
+
38
+ # Allow freeze*() etc.
39
+ MODE_FREEZE = 1
40
+ # Only allow include/require/module/package.
41
+ MODE_COMPILE = 2
42
+ # Same as compile, but handles require(..., pypi="name") as a requirements.txt entry.
43
+ MODE_PYPROJECT = 3
44
+
45
+ # In compile mode, .py -> KIND_COMPILE_AS_MPY
46
+ # In freeze mode, .py -> KIND_FREEZE_AS_MPY, .mpy->KIND_FREEZE_MPY
47
+ KIND_AUTO = 1
48
+ # Freeze-mode only, .py -> KIND_FREEZE_AS_MPY, .mpy->KIND_FREEZE_MPY
49
+ KIND_FREEZE_AUTO = 2
50
+
51
+ # Freeze-mode only, The .py file will be frozen as text.
52
+ KIND_FREEZE_AS_STR = 3
53
+ # Freeze-mode only, The .py file will be compiled and frozen as bytecode.
54
+ KIND_FREEZE_AS_MPY = 4
55
+ # Freeze-mode only, The .mpy file will be frozen directly.
56
+ KIND_FREEZE_MPY = 5
57
+ # Compile mode only, the .py file should be compiled to .mpy.
58
+ KIND_COMPILE_AS_MPY = 6
59
+
60
+ # File on the local filesystem.
61
+ FILE_TYPE_LOCAL = 1
62
+ # URL to file. (TODO)
63
+ FILE_TYPE_HTTP = 2
64
+
65
+
66
+ class ManifestFileError(Exception):
67
+ pass
68
+
69
+
70
+ class ManifestIgnoreException(Exception):
71
+ pass
72
+
73
+
74
+ class ManifestUsePyPIException(Exception):
75
+ def __init__(self, pypi_name):
76
+ self.pypi_name = pypi_name
77
+
78
+
79
+ # The set of files that this manifest references.
80
+ ManifestOutput = namedtuple(
81
+ "ManifestOutput",
82
+ [
83
+ "file_type", # FILE_TYPE_*.
84
+ "full_path", # The input file full path.
85
+ "target_path", # The target path on the device.
86
+ "timestamp", # Last modified date of the input file.
87
+ "kind", # KIND_*.
88
+ "metadata", # Metadata for the containing package.
89
+ "opt", # Optimisation level (or None).
90
+ ],
91
+ )
92
+
93
+
94
+ # Represents the metadata for a package.
95
+ class ManifestPackageMetadata:
96
+ def __init__(self, is_require=False):
97
+ self._is_require = is_require
98
+ self._initialised = False
99
+
100
+ self.version = None
101
+ self.description = None
102
+ self.license = None
103
+ self.author = None
104
+
105
+ # Annotate a package as being from the python standard library.
106
+ self.stdlib = False
107
+
108
+ # Allows a python-ecosys package to be annotated with the
109
+ # corresponding name in PyPI. e.g. micropython-lib/urequests is based
110
+ # on pypi/requests.
111
+ self.pypi = None
112
+ # For a micropython package, this is the name that we will publish it
113
+ # to PyPI as. e.g. micropython-lib/senml publishes as
114
+ # pypi/micropython-senml.
115
+ self.pypi_publish = None
116
+
117
+ def update(
118
+ self,
119
+ mode,
120
+ description=None,
121
+ version=None,
122
+ license=None,
123
+ author=None,
124
+ stdlib=False,
125
+ pypi=None,
126
+ pypi_publish=None,
127
+ ):
128
+ if self._initialised:
129
+ raise ManifestFileError("Duplicate call to metadata().")
130
+
131
+ # In MODE_PYPROJECT, if this manifest is being evaluated as a result
132
+ # of a require(), then figure out if it should be replaced by a PyPI
133
+ # dependency instead.
134
+ if mode == MODE_PYPROJECT and self._is_require:
135
+ if stdlib:
136
+ # No dependency required at all for CPython.
137
+ raise ManifestIgnoreException
138
+ if pypi_publish or pypi:
139
+ # In the case where a package is both based on a PyPI package and
140
+ # provides one, preference depending on the published one.
141
+ # (This should be pretty rare).
142
+ raise ManifestUsePyPIException(pypi_publish or pypi)
143
+
144
+ self.description = description
145
+ self.version = version
146
+ self.license = version
147
+ self.author = author
148
+ self.pypi = pypi
149
+ self.pypi_publish = pypi_publish
150
+ self._initialised = True
151
+
152
+ def check_initialised(self, mode):
153
+ # Ensure that metadata() is the first thing a manifest.py does.
154
+ # This is to ensure that we early-exit if it should be replaced by a pypi dependency.
155
+ if mode in (MODE_COMPILE, MODE_PYPROJECT):
156
+ if not self._initialised:
157
+ raise ManifestFileError("metadata() must be the first command in a manifest file.")
158
+
159
+ def __str__(self):
160
+ return "version={} description={} license={} author={} pypi={} pypi_publish={}".format(
161
+ self.version, self.description, self.license, self.author, self.pypi, self.pypi_publish
162
+ )
163
+
164
+
165
+ # Turns a dict of options into a object with attributes used to turn the
166
+ # kwargs passed to include() and require into the "options" global in the
167
+ # included manifest.
168
+ # options = IncludeOptions(foo="bar", blah="stuff")
169
+ # options.foo # "bar"
170
+ # options.blah # "stuff"
171
+ class IncludeOptions:
172
+ def __init__(self, **kwargs):
173
+ self._kwargs = kwargs
174
+ self._defaults = {}
175
+
176
+ def defaults(self, **kwargs):
177
+ self._defaults = kwargs
178
+
179
+ def __getattr__(self, name):
180
+ return self._kwargs.get(name, self._defaults.get(name, None))
181
+
182
+
183
+ class ManifestFile:
184
+ def __init__(self, mode, path_vars=None):
185
+ # See MODE_* constants above.
186
+ self._mode = mode
187
+ # Path substitution variables.
188
+ self._path_vars = path_vars or {}
189
+ # List of files (as ManifestFileResult) references by this manifest.
190
+ self._manifest_files = []
191
+ # List of PyPI dependencies (when mode=MODE_PYPROJECT).
192
+ self._pypi_dependencies = []
193
+ # Don't allow including the same file twice.
194
+ self._visited = set()
195
+ # Stack of metadata for each level.
196
+ self._metadata = [ManifestPackageMetadata()]
197
+
198
+ def _resolve_path(self, path):
199
+ # Convert path to an absolute path, applying variable substitutions.
200
+ for name, value in self._path_vars.items():
201
+ if value is not None:
202
+ path = path.replace("$({})".format(name), value)
203
+ return os.path.abspath(path)
204
+
205
+ def _manifest_globals(self, kwargs):
206
+ # This is the "API" available to a manifest file.
207
+ g = {
208
+ "metadata": self.metadata,
209
+ "include": self.include,
210
+ "require": self.require,
211
+ "package": self.package,
212
+ "module": self.module,
213
+ "options": IncludeOptions(**kwargs),
214
+ }
215
+
216
+ # Extra legacy functions only for freeze mode.
217
+ if self._mode == MODE_FREEZE:
218
+ g.update(
219
+ {
220
+ "freeze": self.freeze,
221
+ "freeze_as_str": self.freeze_as_str,
222
+ "freeze_as_mpy": self.freeze_as_mpy,
223
+ "freeze_mpy": self.freeze_mpy,
224
+ }
225
+ )
226
+
227
+ return g
228
+
229
+ def files(self):
230
+ return self._manifest_files
231
+
232
+ def pypi_dependencies(self):
233
+ # In MODE_PYPROJECT, this will return a list suitable for requirements.txt.
234
+ return self._pypi_dependencies
235
+
236
+ def execute(self, manifest_file):
237
+ if manifest_file.endswith(".py"):
238
+ # Execute file from filesystem.
239
+ self.include(manifest_file)
240
+ else:
241
+ # Execute manifest code snippet.
242
+ try:
243
+ exec(manifest_file, self._manifest_globals({}))
244
+ except Exception as er:
245
+ raise ManifestFileError("Error in manifest: {}".format(er))
246
+
247
+ def _add_file(self, full_path, target_path, kind=KIND_AUTO, opt=None):
248
+ # Check file exists and get timestamp.
249
+ try:
250
+ stat = os.stat(full_path)
251
+ timestamp = stat.st_mtime
252
+ except OSError:
253
+ raise ManifestFileError("Cannot stat {}".format(full_path))
254
+
255
+ # Map the AUTO kinds to their actual kind based on mode and extension.
256
+ _, ext = os.path.splitext(full_path)
257
+ if self._mode == MODE_FREEZE:
258
+ if kind in (
259
+ KIND_AUTO,
260
+ KIND_FREEZE_AUTO,
261
+ ):
262
+ if ext.lower() == ".py":
263
+ kind = KIND_FREEZE_AS_MPY
264
+ elif ext.lower() == ".mpy":
265
+ kind = KIND_FREEZE_MPY
266
+ else:
267
+ if kind != KIND_AUTO:
268
+ raise ManifestFileError("Not in freeze mode")
269
+ if ext.lower() != ".py":
270
+ raise ManifestFileError("Expected .py file")
271
+ kind = KIND_COMPILE_AS_MPY
272
+
273
+ self._manifest_files.append(
274
+ ManifestOutput(FILE_TYPE_LOCAL, full_path, target_path, timestamp, kind, self._metadata[-1], opt)
275
+ )
276
+
277
+ def _search(self, base_path, package_path, files, exts, kind, opt=None, strict=False):
278
+ base_path = self._resolve_path(base_path)
279
+
280
+ if files:
281
+ # Use explicit list of files (relative to package_path).
282
+ for file in files:
283
+ if package_path:
284
+ file = os.path.join(package_path, file)
285
+ self._add_file(os.path.join(base_path, file), file, kind=kind, opt=opt)
286
+ else:
287
+ if base_path:
288
+ prev_cwd = os.getcwd()
289
+ os.chdir(self._resolve_path(base_path))
290
+
291
+ # Find all candidate files.
292
+ for dirpath, _, filenames in os.walk(package_path or ".", followlinks=True):
293
+ for file in filenames:
294
+ file = os.path.relpath(os.path.join(dirpath, file), ".")
295
+ _, ext = os.path.splitext(file)
296
+ if ext.lower() in exts:
297
+ self._add_file(
298
+ os.path.join(base_path, file),
299
+ file,
300
+ kind=kind,
301
+ opt=opt,
302
+ )
303
+ elif strict:
304
+ raise ManifestFileError("Unexpected file type")
305
+
306
+ if base_path:
307
+ os.chdir(prev_cwd)
308
+
309
+ def metadata(self, **kwargs):
310
+ """
311
+ From within a manifest file, use this to set the metadata for the
312
+ package described by current manifest.
313
+
314
+ After executing a manifest file (via execute()), call this
315
+ to obtain the metadata for the top-level manifest file.
316
+
317
+ See ManifestPackageMetadata.update() for valid kwargs.
318
+ """
319
+ if kwargs:
320
+ self._metadata[-1].update(self._mode, **kwargs)
321
+ return self._metadata[-1]
322
+
323
+ def include(self, manifest_path, is_require=False, **kwargs):
324
+ """
325
+ Include another manifest.
326
+
327
+ The manifest argument can be a string (filename) or an iterable of
328
+ strings.
329
+
330
+ Relative paths are resolved with respect to the current manifest file.
331
+
332
+ If the path is to a directory, then it implicitly includes the
333
+ manifest.py file inside that directory.
334
+
335
+ Optional kwargs can be provided which will be available to the
336
+ included script via the `options` variable.
337
+
338
+ e.g. include("path.py", extra_features=True)
339
+
340
+ in path.py:
341
+ options.defaults(standard_features=True)
342
+
343
+ # freeze minimal modules.
344
+ if options.standard_features:
345
+ # freeze standard modules.
346
+ if options.extra_features:
347
+ # freeze extra modules.
348
+ """
349
+ if is_require:
350
+ self._metadata[-1].check_initialised(self._mode)
351
+
352
+ if not isinstance(manifest_path, str):
353
+ for m in manifest_path:
354
+ self.include(m, **kwargs)
355
+ else:
356
+ manifest_path = self._resolve_path(manifest_path)
357
+ # Including a directory grabs the manifest.py inside it.
358
+ if os.path.isdir(manifest_path):
359
+ manifest_path = os.path.join(manifest_path, "manifest.py")
360
+ if manifest_path in self._visited:
361
+ return
362
+ self._visited.add(manifest_path)
363
+ if is_require:
364
+ # This include is the result of require("name"), so push a new
365
+ # package metadata onto the stack.
366
+ self._metadata.append(ManifestPackageMetadata(is_require=True))
367
+ try:
368
+ with open(manifest_path) as f:
369
+ # Make paths relative to this manifest file while processing it.
370
+ # Applies to includes and input files.
371
+ prev_cwd = os.getcwd()
372
+ os.chdir(os.path.dirname(manifest_path))
373
+ try:
374
+ exec(f.read(), self._manifest_globals(kwargs))
375
+ finally:
376
+ os.chdir(prev_cwd)
377
+ except ManifestIgnoreException:
378
+ # e.g. MODE_PYPROJECT and this was a stdlib dependency. No-op.
379
+ pass
380
+ except ManifestUsePyPIException as e:
381
+ # e.g. MODE_PYPROJECT and this was a package from
382
+ # python-ecosys. Add PyPI dependency instead.
383
+ self._pypi_dependencies.append(e.pypi_name)
384
+ except Exception as e:
385
+ raise ManifestFileError("Error in manifest file: {}: {}".format(manifest_path, e))
386
+ if is_require:
387
+ self._metadata.pop()
388
+
389
+ def require(self, name, version=None, unix_ffi=False, pypi=None, **kwargs):
390
+ """
391
+ Require a module by name from micropython-lib.
392
+
393
+ Optionally specify unix_ffi=True to use a module from the unix-ffi directory.
394
+
395
+ Optionally specify pipy="package-name" to indicate that this should
396
+ use the named package from PyPI when building for CPython.
397
+ """
398
+ self._metadata[-1].check_initialised(self._mode)
399
+
400
+ if self._mode == MODE_PYPROJECT and pypi:
401
+ # In PYPROJECT mode, allow overriding the PyPI dependency name
402
+ # explicitly. Otherwise if the dependent package has metadata
403
+ # (pypi_publish) or metadata(pypi) we will use that.
404
+ self._pypi_dependencies.append(pypi)
405
+ return
406
+
407
+ if self._path_vars["MPY_LIB_DIR"]:
408
+ lib_dirs = ["micropython", "python-stdlib", "python-ecosys"]
409
+ if unix_ffi:
410
+ # Search unix-ffi only if unix_ffi=True, and make unix-ffi modules
411
+ # take precedence.
412
+ lib_dirs = ["unix-ffi"] + lib_dirs
413
+
414
+ for lib_dir in lib_dirs:
415
+ # Search for {lib_dir}/**/{name}/manifest.py.
416
+ for root, _, filenames in os.walk(os.path.join(self._path_vars["MPY_LIB_DIR"], lib_dir)):
417
+ if os.path.basename(root) == name and "manifest.py" in filenames:
418
+ self.include(root, is_require=True, **kwargs)
419
+ return
420
+
421
+ raise ValueError("Library not found in local micropython-lib: {}".format(name))
422
+ else:
423
+ # TODO: HTTP request to obtain URLs from manifest.json.
424
+ raise ValueError("micropython-lib not available for require('{}').", name)
425
+
426
+ def package(self, package_path, files=None, base_path=".", opt=None):
427
+ """
428
+ Define a package, optionally restricting to a set of files.
429
+
430
+ Simple case, a package in the current directory:
431
+ package("foo")
432
+ will include all .py files in foo, and will be stored as foo/bar/baz.py.
433
+
434
+ If the package isn't in the current directory, use base_path:
435
+ package("foo", base_path="src")
436
+
437
+ To restrict to certain files in the package use files (note: paths should be relative to the package):
438
+ package("foo", files=["bar/baz.py"])
439
+ """
440
+ self._metadata[-1].check_initialised(self._mode)
441
+
442
+ # Include "base_path/package_path/**/*.py" --> "package_path/**/*.py"
443
+ self._search(base_path, package_path, files, exts=(".py",), kind=KIND_AUTO, opt=opt)
444
+
445
+ def module(self, module_path, base_path=".", opt=None):
446
+ """
447
+ Include a single Python file as a module.
448
+
449
+ If the file is in the current directory:
450
+ module("foo.py")
451
+
452
+ Otherwise use base_path to locate the file:
453
+ module("foo.py", "src/drivers")
454
+ """
455
+ self._metadata[-1].check_initialised(self._mode)
456
+
457
+ # Include "base_path/module_path" --> "module_path"
458
+ base_path = self._resolve_path(base_path)
459
+ _, ext = os.path.splitext(module_path)
460
+ if ext.lower() != ".py":
461
+ raise ManifestFileError("module must be .py file")
462
+ # TODO: version None
463
+ self._add_file(os.path.join(base_path, module_path), module_path, opt=opt)
464
+
465
+ def _freeze_internal(self, path, script, exts, kind, opt):
466
+ if script is None:
467
+ self._search(path, None, None, exts=exts, kind=kind, opt=opt)
468
+ elif isinstance(script, str) and os.path.isdir(os.path.join(path, script)):
469
+ self._search(path, script, None, exts=exts, kind=kind, opt=opt)
470
+ elif not isinstance(script, str):
471
+ self._search(path, None, script, exts=exts, kind=kind, opt=opt)
472
+ else:
473
+ self._search(path, None, (script,), exts=exts, kind=kind, opt=opt)
474
+
475
+ def freeze(self, path, script=None, opt=None):
476
+ """
477
+ Freeze the input, automatically determining its type. A .py script
478
+ will be compiled to a .mpy first then frozen, and a .mpy file will be
479
+ frozen directly.
480
+
481
+ `path` must be a directory, which is the base directory to _search for
482
+ files from. When importing the resulting frozen modules, the name of
483
+ the module will start after `path`, ie `path` is excluded from the
484
+ module name.
485
+
486
+ If `path` is relative, it is resolved to the current manifest.py.
487
+ Use $(MPY_DIR), $(MPY_LIB_DIR), $(PORT_DIR), $(BOARD_DIR) if you need
488
+ to access specific paths.
489
+
490
+ If `script` is None all files in `path` will be frozen.
491
+
492
+ If `script` is an iterable then freeze() is called on all items of the
493
+ iterable (with the same `path` and `opt` passed through).
494
+
495
+ If `script` is a string then it specifies the file or directory to
496
+ freeze, and can include extra directories before the file or last
497
+ directory. The file or directory will be _searched for in `path`. If
498
+ `script` is a directory then all files in that directory will be frozen.
499
+
500
+ `opt` is the optimisation level to pass to mpy-cross when compiling .py
501
+ to .mpy.
502
+ """
503
+ self._freeze_internal(
504
+ path,
505
+ script,
506
+ exts=(
507
+ ".py",
508
+ ".mpy",
509
+ ),
510
+ kind=KIND_FREEZE_AUTO,
511
+ opt=opt,
512
+ )
513
+
514
+ def freeze_as_str(self, path):
515
+ """
516
+ Freeze the given `path` and all .py scripts within it as a string,
517
+ which will be compiled upon import.
518
+ """
519
+ self._search(path, None, None, exts=(".py",), kind=KIND_FREEZE_AS_STR)
520
+
521
+ def freeze_as_mpy(self, path, script=None, opt=None):
522
+ """
523
+ Freeze the input (see above) by first compiling the .py scripts to
524
+ .mpy files, then freezing the resulting .mpy files.
525
+ """
526
+ self._freeze_internal(path, script, exts=(".py",), kind=KIND_FREEZE_AS_MPY, opt=opt)
527
+
528
+ def freeze_mpy(self, path, script=None, opt=None):
529
+ """
530
+ Freeze the input (see above), which must be .mpy files that are
531
+ frozen directly.
532
+ """
533
+ self._freeze_internal(path, script, exts=(".mpy",), kind=KIND_FREEZE_MPY, opt=opt)
534
+
535
+
536
+ # Generate a temporary file with a line appended to the end that adds __version__.
537
+ @contextlib.contextmanager
538
+ def tagged_py_file(path, metadata):
539
+ dest_fd, dest_path = tempfile.mkstemp(suffix=".py", text=True)
540
+ try:
541
+ with os.fdopen(dest_fd, "w") as dest:
542
+ with open(path, "r") as src:
543
+ contents = src.read()
544
+ dest.write(contents)
545
+
546
+ # Don't overwrite a version definition if the file already has one in it.
547
+ if metadata.version and "__version__ =" not in contents:
548
+ dest.write("\n\n__version__ = {}\n".format(repr(metadata.version)))
549
+ yield dest_path
550
+ finally:
551
+ os.unlink(dest_path)
552
+
553
+
554
+ def main():
555
+ import argparse
556
+
557
+ cmd_parser = argparse.ArgumentParser(description="List the files referenced by a manifest.")
558
+ cmd_parser.add_argument("--freeze", action="store_true", help="freeze mode")
559
+ cmd_parser.add_argument("--compile", action="store_true", help="compile mode")
560
+ cmd_parser.add_argument("--pyproject", action="store_true", help="pyproject mode")
561
+ cmd_parser.add_argument(
562
+ "--lib",
563
+ default=os.path.join(os.path.dirname(__file__), "../lib/micropython-lib"),
564
+ help="path to micropython-lib repo",
565
+ )
566
+ cmd_parser.add_argument("--port", default=None, help="path to port dir")
567
+ cmd_parser.add_argument("--board", default=None, help="path to board dir")
568
+ cmd_parser.add_argument(
569
+ "--top",
570
+ default=os.path.join(os.path.dirname(__file__), ".."),
571
+ help="path to micropython repo",
572
+ )
573
+ cmd_parser.add_argument("files", nargs="+", help="input manifest.py")
574
+ args = cmd_parser.parse_args()
575
+
576
+ path_vars = {
577
+ "MPY_DIR": os.path.abspath(args.top) if args.top else None,
578
+ "BOARD_DIR": os.path.abspath(args.board) if args.board else None,
579
+ "PORT_DIR": os.path.abspath(args.port) if args.port else None,
580
+ "MPY_LIB_DIR": os.path.abspath(args.lib) if args.lib else None,
581
+ }
582
+
583
+ mode = None
584
+ if args.freeze:
585
+ mode = MODE_FREEZE
586
+ elif args.compile:
587
+ mode = MODE_COMPILE
588
+ elif args.pyproject:
589
+ mode = MODE_PYPROJECT
590
+ else:
591
+ print("Error: No mode specified.", file=sys.stderr)
592
+ exit(1)
593
+
594
+ m = ManifestFile(mode, path_vars)
595
+ for manifest_file in args.files:
596
+ try:
597
+ m.execute(manifest_file)
598
+ except ManifestFileError as er:
599
+ print(er, file=sys.stderr)
600
+ exit(1)
601
+ print(m.metadata())
602
+ for f in m.files():
603
+ print(f)
604
+ if mode == MODE_PYPROJECT:
605
+ for r in m.pypi_dependencies():
606
+ print("pypi-require:", r)
607
+
608
+
609
+ if __name__ == "__main__":
610
+ main()