micropython-stubber 1.23.2__py3-none-any.whl → 1.24.0__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 (70) hide show
  1. {micropython_stubber-1.23.2.dist-info → micropython_stubber-1.24.0.dist-info}/METADATA +30 -12
  2. {micropython_stubber-1.23.2.dist-info → micropython_stubber-1.24.0.dist-info}/RECORD +69 -66
  3. {micropython_stubber-1.23.2.dist-info → micropython_stubber-1.24.0.dist-info}/WHEEL +1 -1
  4. mpflash/README.md +2 -2
  5. mpflash/mpflash/basicgit.py +49 -9
  6. mpflash/mpflash/common.py +23 -16
  7. mpflash/mpflash/downloaded.py +10 -2
  8. mpflash/mpflash/mpboard_id/__init__.py +9 -4
  9. mpflash/mpflash/mpboard_id/add_boards.py +25 -14
  10. mpflash/mpflash/mpboard_id/board.py +2 -2
  11. mpflash/mpflash/mpboard_id/board_id.py +10 -6
  12. mpflash/mpflash/mpboard_id/board_info.zip +0 -0
  13. mpflash/mpflash/mpboard_id/store.py +8 -3
  14. mpflash/mpflash/mpremoteboard/__init__.py +13 -8
  15. mpflash/mpflash/mpremoteboard/mpy_fw_info.py +27 -16
  16. mpflash/mpflash/vendor/board_database.py +185 -0
  17. mpflash/mpflash/vendor/readme.md +10 -1
  18. mpflash/mpflash/versions.py +28 -40
  19. mpflash/poetry.lock +1605 -601
  20. mpflash/pyproject.toml +4 -3
  21. stubber/__init__.py +1 -1
  22. stubber/board/createstubs.py +51 -27
  23. stubber/board/createstubs_db.py +36 -28
  24. stubber/board/createstubs_db_min.py +171 -165
  25. stubber/board/createstubs_db_mpy.mpy +0 -0
  26. stubber/board/createstubs_mem.py +36 -28
  27. stubber/board/createstubs_mem_min.py +184 -178
  28. stubber/board/createstubs_mem_mpy.mpy +0 -0
  29. stubber/board/createstubs_min.py +102 -94
  30. stubber/board/createstubs_mpy.mpy +0 -0
  31. stubber/board/modulelist.txt +16 -0
  32. stubber/codemod/enrich.py +297 -88
  33. stubber/codemod/merge_docstub.py +250 -65
  34. stubber/codemod/test_enrich.py +87 -0
  35. stubber/codemod/visitors/typevars.py +200 -0
  36. stubber/commands/build_cmd.py +16 -3
  37. stubber/commands/clone_cmd.py +3 -3
  38. stubber/commands/config_cmd.py +4 -2
  39. stubber/commands/enrich_folder_cmd.py +33 -21
  40. stubber/commands/get_core_cmd.py +1 -2
  41. stubber/commands/get_docstubs_cmd.py +60 -6
  42. stubber/commands/get_frozen_cmd.py +15 -12
  43. stubber/commands/get_mcu_cmd.py +3 -3
  44. stubber/commands/merge_cmd.py +1 -2
  45. stubber/commands/publish_cmd.py +19 -4
  46. stubber/commands/stub_cmd.py +3 -3
  47. stubber/commands/switch_cmd.py +3 -5
  48. stubber/commands/variants_cmd.py +3 -3
  49. stubber/cst_transformer.py +52 -17
  50. stubber/freeze/common.py +27 -11
  51. stubber/freeze/freeze_manifest_2.py +8 -1
  52. stubber/freeze/get_frozen.py +4 -1
  53. stubber/merge_config.py +111 -0
  54. stubber/minify.py +1 -2
  55. stubber/publish/database.py +51 -10
  56. stubber/publish/merge_docstubs.py +33 -16
  57. stubber/publish/package.py +32 -18
  58. stubber/publish/publish.py +8 -8
  59. stubber/publish/stubpackage.py +110 -47
  60. stubber/rst/lookup.py +205 -43
  61. stubber/rst/reader.py +106 -59
  62. stubber/rst/rst_utils.py +24 -11
  63. stubber/stubber.py +1 -1
  64. stubber/stubs_from_docs.py +31 -13
  65. stubber/update_module_list.py +2 -2
  66. stubber/utils/config.py +33 -13
  67. stubber/utils/post.py +9 -6
  68. stubber/publish/missing_class_methods.py +0 -51
  69. {micropython_stubber-1.23.2.dist-info → micropython_stubber-1.24.0.dist-info}/LICENSE +0 -0
  70. {micropython_stubber-1.23.2.dist-info → micropython_stubber-1.24.0.dist-info}/entry_points.txt +0 -0
stubber/codemod/enrich.py CHANGED
@@ -3,149 +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
 
9
13
  from libcst import ParserSyntaxError
10
14
  from libcst.codemod import CodemodContext, diff_code, exec_transform_with_prettyprint
11
15
  from libcst.tool import _default_config # type: ignore
12
- from mpflash.logger import log
13
16
 
14
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
15
24
  from stubber.utils.post import run_black
16
25
 
26
+
17
27
  ##########################################################################################
18
28
  # # log = logging.getLogger(__name__)
19
29
  # logging.basicConfig(level=logging.INFO)
20
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()
21
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
22
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
+ #########################################################################################
23
162
  def enrich_file(
163
+ source_path: Path,
24
164
  target_path: Path,
25
- docstub_path: Path,
26
165
  diff: bool = False,
27
166
  write_back: bool = False,
28
- package_name="",
29
- ) -> Optional[str]:
167
+ # package_name="", # not used
168
+ params_only: bool = False,
169
+ ) -> Generator[str, None, None]:
30
170
  """
31
171
  Enrich a MCU stubs using the doc-stubs in another folder.
32
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.
33
175
 
34
176
  Parameters:
35
- source_path: the path to the firmware stub to enrich
36
- docstub_path: the path to the folder containing the doc-stubs
177
+ source_path: the path to the firmware stub-file to enrich
178
+ docstub_path: the path to the file containing the doc-stubs
37
179
  diff: if True, return the diff between the original and the enriched source file
38
180
  write_back: if True, write the enriched source file back to the source_path
39
181
 
40
182
  Returns:
41
183
  - None or a string containing the diff between the original and the enriched source file
42
184
  """
43
- config: Dict[str, Any] = _default_config()
44
- context = CodemodContext()
45
- if not package_name:
46
- package_name = (
47
- target_path.stem if target_path.stem != "__init__" else target_path.parent.stem
48
- )
49
185
 
50
- # find a matching doc-stub file in the docstub_path
51
- docstub_file = None
52
- if docstub_path.is_file():
53
- candidates = [docstub_path]
54
- else:
55
- candidates = []
56
- for ext in [".py", ".pyi"]:
57
- candidates = list(docstub_path.rglob(package_name + ext))
58
- if package_name[0].lower() == "u":
59
- # also look for candidates without leading u ( usys.py <- sys.py)
60
- candidates += list(docstub_path.rglob(package_name[1:] + ext))
61
- elif package_name[0] == "_":
62
- # also look for candidates without leading _ ( _rp2.py <- rp2.py )
63
- candidates += list(docstub_path.rglob(package_name[1:] + ext))
64
- else:
65
- # also look for candidates with leading u ( sys.py <- usys.py)
66
- candidates += list(docstub_path.rglob("u" + package_name + ext))
67
-
68
- for docstub_file in candidates:
69
- if docstub_file.exists():
70
- break
71
- else:
72
- docstub_file = None
73
- if not docstub_file:
74
- raise FileNotFoundError(f"No doc-stub file found for {target_path}")
75
-
76
- log.debug(f"Merge {target_path} from {docstub_file}")
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")
77
201
  # read source file
78
- old_code = target_path.read_text(encoding="utf-8")
79
-
80
- codemod_instance = merge_docstub.MergeCommand(context, docstub_file=docstub_file)
81
- if not (
82
- new_code := exec_transform_with_prettyprint(
83
- codemod_instance,
84
- old_code,
85
- # include_generated=False,
86
- generated_code_marker=config["generated_code_marker"],
87
- # format_code=not args.no_format,
88
- formatter_args=config["formatter"],
89
- # python_version=args.python_version,
90
- )
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,
91
213
  ):
92
- return None
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}")
93
219
  if write_back:
94
220
  log.trace(f"Write back enriched file {target_path}")
95
- # write updated code to file
96
- target_path.write_text(new_code, encoding="utf-8")
97
- return diff_code(old_code, new_code, 5, filename=target_path.name) if diff else new_code
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
98
277
 
99
278
 
100
279
  def enrich_folder(
101
- source_path: Path,
102
- docstub_path: Path,
280
+ source_folder: Path,
281
+ target_folder: Path,
103
282
  show_diff: bool = False,
104
283
  write_back: bool = False,
105
284
  require_docstub: bool = False,
106
- package_name: str = "",
285
+ params_only: bool = False,
286
+ ext: Optional[str] = None,
287
+ # package_name: str = "",
107
288
  ) -> int:
108
289
  """\
109
290
  Enrich a folder with containing MCU stubs using the doc-stubs in another folder.
110
291
 
111
292
  Returns the number of files enriched.
112
293
  """
113
- if not source_path.exists():
114
- raise FileNotFoundError(f"Source {source_path} does not exist")
115
- if not docstub_path.exists():
116
- raise FileNotFoundError(f"Docstub {docstub_path} does not exist")
117
- log.debug(f"Enrich folder {source_path}.")
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}")
118
300
  count = 0
119
- # list all the .py and .pyi files in the source folder
120
- if source_path.is_file():
121
- source_files = [source_path]
122
- else:
123
- source_files = sorted(
124
- list(source_path.rglob("**/*.py")) + list(source_path.rglob("**/*.pyi"))
125
- )
126
- 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:
127
308
  try:
128
- diff = enrich_file(
129
- source_file,
130
- docstub_path,
131
- diff=True,
132
- write_back=write_back,
133
- package_name=package_name,
134
- )
135
- if diff:
136
- count += 1
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)
137
322
  if show_diff:
138
- print(diff)
323
+ for d in diff:
324
+ print(d)
139
325
  except FileNotFoundError as e:
140
326
  # no docstub to enrich with
141
327
  if require_docstub:
142
- raise (FileNotFoundError(f"No doc-stub file found for {source_file}")) from e
328
+ raise (
329
+ FileNotFoundError(f"No doc-stub or source file found for {mm.target}")
330
+ ) from e
143
331
  except (Exception, ParserSyntaxError) as e:
144
- log.error(f"Error parsing {source_file}")
332
+ log.error(f"Error parsing {mm.target}")
145
333
  log.exception(e)
146
334
  continue
147
- # run black on the destination folder
148
- run_black(source_path)
149
- # DO NOT run Autoflake as this removes some relevant (unused) imports
150
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
339
+
340
+ if params_only:
341
+ copy_type_modules(source_folder, target_folder, CP_REFERENCE_TO_DOCSTUB)
151
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 ""