micropython-stubber 1.23.3__py3-none-any.whl → 1.24.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {micropython_stubber-1.23.3.dist-info → micropython_stubber-1.24.1.dist-info}/METADATA +29 -11
- {micropython_stubber-1.23.3.dist-info → micropython_stubber-1.24.1.dist-info}/RECORD +68 -65
- {micropython_stubber-1.23.3.dist-info → micropython_stubber-1.24.1.dist-info}/WHEEL +1 -1
- mpflash/README.md +2 -2
- mpflash/mpflash/basicgit.py +22 -2
- mpflash/mpflash/common.py +23 -13
- mpflash/mpflash/downloaded.py +10 -2
- mpflash/mpflash/flash/esp.py +1 -1
- mpflash/mpflash/mpboard_id/__init__.py +9 -4
- mpflash/mpflash/mpboard_id/add_boards.py +25 -14
- mpflash/mpflash/mpboard_id/board.py +2 -2
- mpflash/mpflash/mpboard_id/board_id.py +10 -6
- mpflash/mpflash/mpboard_id/board_info.zip +0 -0
- mpflash/mpflash/mpremoteboard/__init__.py +13 -8
- mpflash/mpflash/vendor/board_database.py +185 -0
- mpflash/mpflash/vendor/readme.md +10 -1
- mpflash/mpflash/versions.py +28 -40
- mpflash/poetry.lock +1147 -231
- mpflash/pyproject.toml +4 -3
- stubber/__init__.py +1 -1
- stubber/board/createstubs.py +76 -34
- stubber/board/createstubs_db.py +34 -25
- stubber/board/createstubs_db_min.py +90 -83
- stubber/board/createstubs_db_mpy.mpy +0 -0
- stubber/board/createstubs_mem.py +34 -25
- stubber/board/createstubs_mem_min.py +123 -116
- stubber/board/createstubs_mem_mpy.mpy +0 -0
- stubber/board/createstubs_min.py +154 -145
- stubber/board/createstubs_mpy.mpy +0 -0
- stubber/board/modulelist.txt +16 -0
- stubber/codemod/enrich.py +301 -86
- stubber/codemod/merge_docstub.py +251 -66
- stubber/codemod/test_enrich.py +87 -0
- stubber/codemod/visitors/type_helpers.py +182 -0
- stubber/commands/build_cmd.py +16 -3
- stubber/commands/clone_cmd.py +3 -3
- stubber/commands/config_cmd.py +4 -2
- stubber/commands/enrich_folder_cmd.py +33 -21
- stubber/commands/get_core_cmd.py +1 -2
- stubber/commands/get_docstubs_cmd.py +60 -6
- stubber/commands/get_frozen_cmd.py +15 -12
- stubber/commands/get_mcu_cmd.py +3 -3
- stubber/commands/merge_cmd.py +1 -2
- stubber/commands/publish_cmd.py +19 -4
- stubber/commands/stub_cmd.py +3 -3
- stubber/commands/switch_cmd.py +3 -5
- stubber/commands/variants_cmd.py +3 -3
- stubber/cst_transformer.py +52 -17
- stubber/freeze/common.py +27 -11
- stubber/freeze/freeze_manifest_2.py +8 -1
- stubber/freeze/get_frozen.py +4 -1
- stubber/merge_config.py +111 -0
- stubber/minify.py +1 -2
- stubber/publish/database.py +51 -10
- stubber/publish/merge_docstubs.py +38 -17
- stubber/publish/package.py +32 -18
- stubber/publish/publish.py +8 -8
- stubber/publish/stubpackage.py +117 -50
- stubber/rst/lookup.py +205 -41
- stubber/rst/reader.py +106 -59
- stubber/rst/rst_utils.py +24 -11
- stubber/stubber.py +1 -1
- stubber/stubs_from_docs.py +31 -13
- stubber/update_module_list.py +2 -2
- stubber/utils/config.py +33 -13
- stubber/utils/post.py +9 -6
- stubber/publish/missing_class_methods.py +0 -51
- {micropython_stubber-1.23.3.dist-info → micropython_stubber-1.24.1.dist-info}/LICENSE +0 -0
- {micropython_stubber-1.23.3.dist-info → micropython_stubber-1.24.1.dist-info}/entry_points.txt +0 -0
stubber/codemod/enrich.py
CHANGED
@@ -3,143 +3,358 @@ Enrich MCU stubs by copying docstrings and parameter information from doc-stubs
|
|
3
3
|
Both (.py or .pyi) files are supported.
|
4
4
|
"""
|
5
5
|
|
6
|
+
import shutil
|
7
|
+
from collections.abc import Generator
|
8
|
+
from dataclasses import dataclass
|
9
|
+
from functools import lru_cache
|
6
10
|
from pathlib import Path
|
7
|
-
from typing import Any, Dict, Optional
|
11
|
+
from typing import Any, Dict, List, Optional, Tuple # noqa: UP035
|
8
12
|
|
13
|
+
from libcst import ParserSyntaxError
|
9
14
|
from libcst.codemod import CodemodContext, diff_code, exec_transform_with_prettyprint
|
10
15
|
from libcst.tool import _default_config # type: ignore
|
11
|
-
from mpflash.logger import log
|
12
16
|
|
13
17
|
import stubber.codemod.merge_docstub as merge_docstub
|
18
|
+
from mpflash.logger import log
|
19
|
+
from stubber.merge_config import (
|
20
|
+
CP_REFERENCE_TO_DOCSTUB,
|
21
|
+
copy_type_modules,
|
22
|
+
)
|
23
|
+
from stubber.rst.lookup import U_MODULES
|
14
24
|
from stubber.utils.post import run_black
|
15
25
|
|
26
|
+
|
16
27
|
##########################################################################################
|
17
28
|
# # log = logging.getLogger(__name__)
|
18
29
|
# logging.basicConfig(level=logging.INFO)
|
19
30
|
#########################################################################################
|
31
|
+
@dataclass
|
32
|
+
class MergeMatch:
|
33
|
+
"""A match between a target and source file to merge docstrings and typehints"""
|
34
|
+
|
35
|
+
target: Path
|
36
|
+
source: Path
|
37
|
+
target_pkg: str
|
38
|
+
source_pkg: str
|
39
|
+
is_match: bool
|
40
|
+
|
41
|
+
|
42
|
+
@lru_cache(maxsize=2500)
|
43
|
+
def package_from_path(target: Path, source: Optional[Path] = None) -> str:
|
44
|
+
"""
|
45
|
+
Given a target and source path, return the package name based on the path.
|
46
|
+
"""
|
47
|
+
# package = None
|
48
|
+
_options = [p for p in [target, source] if p is not None]
|
49
|
+
for p in _options:
|
50
|
+
if not p.exists():
|
51
|
+
raise FileNotFoundError(f"Path {p} does not exist")
|
52
|
+
|
53
|
+
# if either the source or target is a package, use that
|
54
|
+
for p in _options:
|
55
|
+
if p.is_dir() and list(p.glob("__init__.py*")):
|
56
|
+
return p.stem
|
57
|
+
|
58
|
+
# check if there is a __init__.py next to the target
|
59
|
+
for p in _options:
|
60
|
+
if list(p.parent.glob("__init__.py*")):
|
61
|
+
return f"{p.parent.stem}.{p.stem}"
|
62
|
+
# check One level up - just in case
|
63
|
+
for p in _options:
|
64
|
+
if list(p.parent.parent.glob("__init__.py*")):
|
65
|
+
return f"{p.parent.parent.stem}.{p.parent.stem}.{p.stem}"
|
66
|
+
# then use the filename, unless it is a __**__.py
|
67
|
+
return next(
|
68
|
+
(p.stem for p in _options if p.is_file() and not p.stem.startswith("__")),
|
69
|
+
"",
|
70
|
+
)
|
71
|
+
|
72
|
+
|
73
|
+
def upackage_equal(src: str, target: str) -> Tuple[bool, int]:
|
74
|
+
"""
|
75
|
+
Compare package names, return True if they are equal, ignoring an _ or u-prefix and case
|
76
|
+
"""
|
77
|
+
if src.startswith("u") and not target.startswith("u"):
|
78
|
+
# do not allow enriching from u-module to module
|
79
|
+
return False, 0
|
80
|
+
if not src.startswith("u") and target.startswith("u"):
|
81
|
+
# allow enriching from module to u-module
|
82
|
+
target = target[1:]
|
83
|
+
# first check for exact match
|
84
|
+
if src == target or f"u{src}" == target:
|
85
|
+
return True, len(src)
|
86
|
+
|
87
|
+
# traet __init__ as a package
|
88
|
+
if src.endswith(".__init__"):
|
89
|
+
src = src[:-9]
|
90
|
+
if target.endswith(".__init__"):
|
91
|
+
target = target[:-9]
|
92
|
+
#
|
93
|
+
if src and src[0] == "_":
|
94
|
+
src = src[1:]
|
95
|
+
if target and target[0] == "_":
|
96
|
+
target = target[1:]
|
97
|
+
|
98
|
+
src = src.lower()
|
99
|
+
target = target.lower()
|
20
100
|
|
101
|
+
if src == target or f"u{src}" == target:
|
102
|
+
return True, len(src)
|
103
|
+
if "." in src and src.startswith(f"{target}."):
|
104
|
+
return True, len(target)
|
105
|
+
if "." in target and target.startswith(f"{src}."):
|
106
|
+
return True, len(src)
|
107
|
+
return False, 0
|
21
108
|
|
109
|
+
|
110
|
+
def source_target_candidates(
|
111
|
+
source: Path,
|
112
|
+
target: Path,
|
113
|
+
ext: Optional[str] = None,
|
114
|
+
) -> Generator[MergeMatch, None, None]:
|
115
|
+
"""
|
116
|
+
Given a target and source path, return a list of tuples of `(target, source, package name)` that are candidates for merging.
|
117
|
+
Goal is to match the target and source files based on the package name, to avoid mismatched merges of docstrings and typehints
|
118
|
+
|
119
|
+
Returns a generator of tuples of `(target, source, target_package, source_package, is_partial_match)`
|
120
|
+
"""
|
121
|
+
ext = ext or ".py*"
|
122
|
+
# first assumption on targets
|
123
|
+
if target.is_dir():
|
124
|
+
targets = list(target.glob(f"**/*{ext}"))
|
125
|
+
elif target.is_file():
|
126
|
+
targets = [target]
|
127
|
+
else:
|
128
|
+
targets = []
|
129
|
+
|
130
|
+
if source.is_dir():
|
131
|
+
sources = list(source.glob(f"**/*{ext}"))
|
132
|
+
elif source.is_file():
|
133
|
+
sources = [source]
|
134
|
+
else:
|
135
|
+
sources = []
|
136
|
+
# filter down using the package name
|
137
|
+
for s in sources:
|
138
|
+
is_match: bool = False
|
139
|
+
best_match_len = 0
|
140
|
+
mm = None
|
141
|
+
s_pkg = package_from_path(s)
|
142
|
+
for t in targets:
|
143
|
+
# find the best match
|
144
|
+
if t.stem.startswith("u") and t.stem[1:] in U_MODULES:
|
145
|
+
# skip enriching umodule.pyi files
|
146
|
+
# log.trace(f"Skip enriching {t.name}, as it is an u-module")
|
147
|
+
continue
|
148
|
+
t_pkg = package_from_path(t)
|
149
|
+
is_match, match_len = upackage_equal(s_pkg, t_pkg)
|
150
|
+
if "_mpy_shed" in str(s) or "_mpy_shed" in str(t):
|
151
|
+
log.trace(f"Skip _mpy_shed file {s}")
|
152
|
+
continue
|
153
|
+
if is_match and match_len > best_match_len:
|
154
|
+
best_match_len = match_len
|
155
|
+
mm = MergeMatch(t, s, t_pkg, s_pkg, is_match)
|
156
|
+
if not mm:
|
157
|
+
continue
|
158
|
+
yield mm
|
159
|
+
|
160
|
+
|
161
|
+
#########################################################################################
|
22
162
|
def enrich_file(
|
163
|
+
source_path: Path,
|
23
164
|
target_path: Path,
|
24
|
-
docstub_path: Path,
|
25
165
|
diff: bool = False,
|
26
166
|
write_back: bool = False,
|
27
|
-
package_name="",
|
28
|
-
|
167
|
+
# package_name="", # not used
|
168
|
+
params_only: bool = False,
|
169
|
+
) -> Generator[str, None, None]:
|
29
170
|
"""
|
30
171
|
Enrich a MCU stubs using the doc-stubs in another folder.
|
31
172
|
Both (.py or .pyi) files are supported.
|
173
|
+
Both source an target files must exist, and are assumed to match.
|
174
|
+
Any matching of source and target files should be done before calling this function.
|
32
175
|
|
33
176
|
Parameters:
|
34
|
-
source_path: the
|
35
|
-
docstub_path: the path to the
|
177
|
+
source_path: the path to the firmware stub-file to enrich
|
178
|
+
docstub_path: the path to the file containing the doc-stubs
|
36
179
|
diff: if True, return the diff between the original and the enriched source file
|
37
180
|
write_back: if True, write the enriched source file back to the source_path
|
38
181
|
|
39
182
|
Returns:
|
40
183
|
- None or a string containing the diff between the original and the enriched source file
|
41
184
|
"""
|
42
|
-
config: Dict[str, Any] = _default_config()
|
43
|
-
context = CodemodContext()
|
44
|
-
if not package_name:
|
45
|
-
package_name = (
|
46
|
-
target_path.stem if target_path.stem != "__init__" else target_path.parent.stem
|
47
|
-
)
|
48
|
-
|
49
|
-
# find a matching doc-stub file in the docstub_path
|
50
|
-
docstub_file = None
|
51
|
-
if docstub_path.is_file():
|
52
|
-
candidates = [docstub_path]
|
53
|
-
else:
|
54
|
-
candidates = []
|
55
|
-
for ext in [".py", ".pyi"]:
|
56
|
-
candidates = list(docstub_path.rglob(package_name + ext))
|
57
|
-
if package_name[0].lower() == "u":
|
58
|
-
# also look for candidates without leading u ( usys.py <- sys.py)
|
59
|
-
candidates += list(docstub_path.rglob(package_name[1:] + ext))
|
60
|
-
elif package_name[0] == "_":
|
61
|
-
# also look for candidates without leading _ ( _rp2.py <- rp2.py )
|
62
|
-
candidates += list(docstub_path.rglob(package_name[1:] + ext))
|
63
|
-
else:
|
64
|
-
# also look for candidates with leading u ( sys.py <- usys.py)
|
65
|
-
candidates += list(docstub_path.rglob("u" + package_name + ext))
|
66
|
-
|
67
|
-
for docstub_file in candidates:
|
68
|
-
if docstub_file.exists():
|
69
|
-
break
|
70
|
-
else:
|
71
|
-
docstub_file = None
|
72
|
-
if not docstub_file:
|
73
|
-
raise FileNotFoundError(f"No doc-stub file found for {target_path}")
|
74
185
|
|
75
|
-
|
186
|
+
if not source_path.exists() or not target_path.exists():
|
187
|
+
raise FileNotFoundError("Source or target file not found")
|
188
|
+
if not source_path.is_file() or not target_path.is_file():
|
189
|
+
raise FileNotFoundError("Source or target is not a file")
|
190
|
+
log.info(f"Enriching file: {target_path}")
|
191
|
+
config: Dict[str, Any] = _default_config()
|
192
|
+
# fass the filename and module name to the codemod
|
193
|
+
context = CodemodContext(
|
194
|
+
filename=target_path.as_posix(),
|
195
|
+
full_module_name=package_from_path(target_path),
|
196
|
+
)
|
197
|
+
# apply a single codemod to the target file
|
198
|
+
success = False
|
199
|
+
# read target file
|
200
|
+
old_code = current_code = target_path.read_text(encoding="utf-8")
|
76
201
|
# read source file
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
if
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
# python_version=args.python_version,
|
89
|
-
)
|
202
|
+
codemod_instance = merge_docstub.MergeCommand(
|
203
|
+
context, docstub_file=source_path, params_only=params_only
|
204
|
+
)
|
205
|
+
if new_code := exec_transform_with_prettyprint(
|
206
|
+
codemod_instance,
|
207
|
+
current_code,
|
208
|
+
# include_generated=False,
|
209
|
+
generated_code_marker=config["generated_code_marker"],
|
210
|
+
# format_code=not args.no_format,
|
211
|
+
formatter_args=config["formatter"],
|
212
|
+
# python_version=args.python_version,
|
90
213
|
):
|
91
|
-
|
214
|
+
current_code = new_code
|
215
|
+
success = True
|
216
|
+
|
217
|
+
if not success:
|
218
|
+
raise FileNotFoundError(f"No doc-stub file found for {target_path}")
|
92
219
|
if write_back:
|
93
220
|
log.trace(f"Write back enriched file {target_path}")
|
94
|
-
|
95
|
-
|
96
|
-
|
221
|
+
target_path.write_text(current_code, encoding="utf-8")
|
222
|
+
if diff:
|
223
|
+
yield diff_code(old_code, current_code, 5, filename=target_path.name)
|
224
|
+
|
225
|
+
|
226
|
+
def merge_candidates(
|
227
|
+
source_folder: Path,
|
228
|
+
target_folder: Path,
|
229
|
+
) -> List[MergeMatch]:
|
230
|
+
"""
|
231
|
+
Generate a list of merge candidates for the source and target folders.
|
232
|
+
Each target is matched with exactly one source file.
|
233
|
+
"""
|
234
|
+
candidates = list(source_target_candidates(source_folder, target_folder))
|
235
|
+
|
236
|
+
# Create a list of candidate matches for the same target
|
237
|
+
target_dict = {}
|
238
|
+
for candidate in candidates:
|
239
|
+
if candidate.target not in target_dict:
|
240
|
+
target_dict[candidate.target] = []
|
241
|
+
target_dict[candidate.target].append(candidate)
|
242
|
+
|
243
|
+
# first get targets with only one candidate
|
244
|
+
candidates = [v[0] for k, v in target_dict.items() if len(v) == 1]
|
245
|
+
|
246
|
+
# then get the best matching from the d
|
247
|
+
multiple_candidates = {k: v for k, v in target_dict.items() if len(v) > 1}
|
248
|
+
for target in multiple_candidates.keys():
|
249
|
+
|
250
|
+
# if simple module --> complex module : select the best matching or first source
|
251
|
+
perfect = next(
|
252
|
+
(
|
253
|
+
match
|
254
|
+
for match in multiple_candidates[target]
|
255
|
+
if match.target_pkg == match.source_pkg
|
256
|
+
),
|
257
|
+
None,
|
258
|
+
)
|
259
|
+
|
260
|
+
if perfect:
|
261
|
+
candidates.append(perfect)
|
262
|
+
else:
|
263
|
+
close_enough = [
|
264
|
+
match
|
265
|
+
for match in multiple_candidates[target]
|
266
|
+
if match.source_pkg.startswith(f"{match.target_pkg}.")
|
267
|
+
]
|
268
|
+
if close_enough:
|
269
|
+
candidates.extend(close_enough)
|
270
|
+
# else:
|
271
|
+
# # take the first one
|
272
|
+
# candidates.append(multiple_candidates[target][0])
|
273
|
+
|
274
|
+
# sort by target_path , to show diffs
|
275
|
+
candidates = sorted(candidates, key=lambda m: m.target)
|
276
|
+
return candidates
|
97
277
|
|
98
278
|
|
99
279
|
def enrich_folder(
|
100
|
-
|
101
|
-
|
280
|
+
source_folder: Path,
|
281
|
+
target_folder: Path,
|
102
282
|
show_diff: bool = False,
|
103
283
|
write_back: bool = False,
|
104
284
|
require_docstub: bool = False,
|
105
|
-
|
285
|
+
params_only: bool = False,
|
286
|
+
ext: Optional[str] = None,
|
287
|
+
# package_name: str = "",
|
106
288
|
) -> int:
|
107
289
|
"""\
|
108
290
|
Enrich a folder with containing MCU stubs using the doc-stubs in another folder.
|
109
291
|
|
110
292
|
Returns the number of files enriched.
|
111
293
|
"""
|
112
|
-
if not
|
113
|
-
raise FileNotFoundError(f"
|
114
|
-
if not
|
115
|
-
raise FileNotFoundError(f"
|
294
|
+
if not target_folder.exists():
|
295
|
+
raise FileNotFoundError(f"Target {target_folder} does not exist")
|
296
|
+
if not source_folder.exists():
|
297
|
+
raise FileNotFoundError(f"Source {source_folder} does not exist")
|
298
|
+
ext = ext or ".py*"
|
299
|
+
log.info(f"Enrich folder {target_folder}/**/*{ext}")
|
116
300
|
count = 0
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
for source_file in source_files:
|
301
|
+
|
302
|
+
candidates = source_target_candidates(source_folder, target_folder, ext)
|
303
|
+
# sort by target_path , to show diffs
|
304
|
+
candidates = sorted(candidates, key=lambda m: m.target)
|
305
|
+
|
306
|
+
# for target in target_files:
|
307
|
+
for mm in candidates:
|
125
308
|
try:
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
309
|
+
log.debug(f"Enriching {mm.target}")
|
310
|
+
log.debug(f" from {mm.source}")
|
311
|
+
if diff := list(
|
312
|
+
enrich_file(
|
313
|
+
mm.source,
|
314
|
+
mm.target,
|
315
|
+
diff=True,
|
316
|
+
write_back=write_back,
|
317
|
+
# package_name=mm.target_pkg,
|
318
|
+
params_only=params_only,
|
319
|
+
)
|
320
|
+
):
|
321
|
+
count += len(diff)
|
135
322
|
if show_diff:
|
136
|
-
|
323
|
+
for d in diff:
|
324
|
+
print(d)
|
137
325
|
except FileNotFoundError as e:
|
138
326
|
# no docstub to enrich with
|
139
327
|
if require_docstub:
|
140
|
-
raise (
|
141
|
-
|
142
|
-
|
143
|
-
|
328
|
+
raise (
|
329
|
+
FileNotFoundError(f"No doc-stub or source file found for {mm.target}")
|
330
|
+
) from e
|
331
|
+
except (Exception, ParserSyntaxError) as e:
|
332
|
+
log.error(f"Error parsing {mm.target}")
|
333
|
+
log.exception(e)
|
334
|
+
continue
|
335
|
+
|
336
|
+
# run black on the target folder
|
337
|
+
run_black(target_folder)
|
338
|
+
# DO NOT run Autoflake as this removes some relevant (but unused) imports too early
|
144
339
|
|
340
|
+
if params_only:
|
341
|
+
copy_type_modules(source_folder, target_folder, CP_REFERENCE_TO_DOCSTUB)
|
145
342
|
return count
|
343
|
+
|
344
|
+
|
345
|
+
def guess_port_from_path(folder: Path) -> str:
|
346
|
+
"""
|
347
|
+
Guess the port name from the folder contents.
|
348
|
+
( could also be done based on the path name)
|
349
|
+
|
350
|
+
"""
|
351
|
+
for port in ["esp32", "samd", "rp2", "pyb"]:
|
352
|
+
if (folder / port).exists() or (folder / f"{port}.pyi").exists():
|
353
|
+
if port == "pyb":
|
354
|
+
return "stm32"
|
355
|
+
return port
|
356
|
+
|
357
|
+
if (folder / "esp").exists() or (folder / f"esp.pyi").exists():
|
358
|
+
return "esp8266"
|
359
|
+
|
360
|
+
return ""
|