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.
Files changed (69) hide show
  1. {micropython_stubber-1.23.3.dist-info → micropython_stubber-1.24.1.dist-info}/METADATA +29 -11
  2. {micropython_stubber-1.23.3.dist-info → micropython_stubber-1.24.1.dist-info}/RECORD +68 -65
  3. {micropython_stubber-1.23.3.dist-info → micropython_stubber-1.24.1.dist-info}/WHEEL +1 -1
  4. mpflash/README.md +2 -2
  5. mpflash/mpflash/basicgit.py +22 -2
  6. mpflash/mpflash/common.py +23 -13
  7. mpflash/mpflash/downloaded.py +10 -2
  8. mpflash/mpflash/flash/esp.py +1 -1
  9. mpflash/mpflash/mpboard_id/__init__.py +9 -4
  10. mpflash/mpflash/mpboard_id/add_boards.py +25 -14
  11. mpflash/mpflash/mpboard_id/board.py +2 -2
  12. mpflash/mpflash/mpboard_id/board_id.py +10 -6
  13. mpflash/mpflash/mpboard_id/board_info.zip +0 -0
  14. mpflash/mpflash/mpremoteboard/__init__.py +13 -8
  15. mpflash/mpflash/vendor/board_database.py +185 -0
  16. mpflash/mpflash/vendor/readme.md +10 -1
  17. mpflash/mpflash/versions.py +28 -40
  18. mpflash/poetry.lock +1147 -231
  19. mpflash/pyproject.toml +4 -3
  20. stubber/__init__.py +1 -1
  21. stubber/board/createstubs.py +76 -34
  22. stubber/board/createstubs_db.py +34 -25
  23. stubber/board/createstubs_db_min.py +90 -83
  24. stubber/board/createstubs_db_mpy.mpy +0 -0
  25. stubber/board/createstubs_mem.py +34 -25
  26. stubber/board/createstubs_mem_min.py +123 -116
  27. stubber/board/createstubs_mem_mpy.mpy +0 -0
  28. stubber/board/createstubs_min.py +154 -145
  29. stubber/board/createstubs_mpy.mpy +0 -0
  30. stubber/board/modulelist.txt +16 -0
  31. stubber/codemod/enrich.py +301 -86
  32. stubber/codemod/merge_docstub.py +251 -66
  33. stubber/codemod/test_enrich.py +87 -0
  34. stubber/codemod/visitors/type_helpers.py +182 -0
  35. stubber/commands/build_cmd.py +16 -3
  36. stubber/commands/clone_cmd.py +3 -3
  37. stubber/commands/config_cmd.py +4 -2
  38. stubber/commands/enrich_folder_cmd.py +33 -21
  39. stubber/commands/get_core_cmd.py +1 -2
  40. stubber/commands/get_docstubs_cmd.py +60 -6
  41. stubber/commands/get_frozen_cmd.py +15 -12
  42. stubber/commands/get_mcu_cmd.py +3 -3
  43. stubber/commands/merge_cmd.py +1 -2
  44. stubber/commands/publish_cmd.py +19 -4
  45. stubber/commands/stub_cmd.py +3 -3
  46. stubber/commands/switch_cmd.py +3 -5
  47. stubber/commands/variants_cmd.py +3 -3
  48. stubber/cst_transformer.py +52 -17
  49. stubber/freeze/common.py +27 -11
  50. stubber/freeze/freeze_manifest_2.py +8 -1
  51. stubber/freeze/get_frozen.py +4 -1
  52. stubber/merge_config.py +111 -0
  53. stubber/minify.py +1 -2
  54. stubber/publish/database.py +51 -10
  55. stubber/publish/merge_docstubs.py +38 -17
  56. stubber/publish/package.py +32 -18
  57. stubber/publish/publish.py +8 -8
  58. stubber/publish/stubpackage.py +117 -50
  59. stubber/rst/lookup.py +205 -41
  60. stubber/rst/reader.py +106 -59
  61. stubber/rst/rst_utils.py +24 -11
  62. stubber/stubber.py +1 -1
  63. stubber/stubs_from_docs.py +31 -13
  64. stubber/update_module_list.py +2 -2
  65. stubber/utils/config.py +33 -13
  66. stubber/utils/post.py +9 -6
  67. stubber/publish/missing_class_methods.py +0 -51
  68. {micropython_stubber-1.23.3.dist-info → micropython_stubber-1.24.1.dist-info}/LICENSE +0 -0
  69. {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
- ) -> Optional[str]:
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 path to the firmware stub to enrich
35
- 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
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
- 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")
76
201
  # read source file
77
- old_code = target_path.read_text(encoding="utf-8")
78
-
79
- codemod_instance = merge_docstub.MergeCommand(context, docstub_file=docstub_file)
80
- if not (
81
- new_code := exec_transform_with_prettyprint(
82
- codemod_instance,
83
- old_code,
84
- # include_generated=False,
85
- generated_code_marker=config["generated_code_marker"],
86
- # format_code=not args.no_format,
87
- formatter_args=config["formatter"],
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
- 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}")
92
219
  if write_back:
93
220
  log.trace(f"Write back enriched file {target_path}")
94
- # write updated code to file
95
- target_path.write_text(new_code, encoding="utf-8")
96
- 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
97
277
 
98
278
 
99
279
  def enrich_folder(
100
- source_path: Path,
101
- docstub_path: Path,
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
- package_name: str = "",
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 source_path.exists():
113
- raise FileNotFoundError(f"Source {source_path} does not exist")
114
- if not docstub_path.exists():
115
- raise FileNotFoundError(f"Docstub {docstub_path} does not exist")
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
- # list all the .py and .pyi files in the source folder
118
- if source_path.is_file():
119
- source_files = [source_path]
120
- else:
121
- source_files = sorted(
122
- list(source_path.rglob("**/*.py")) + list(source_path.rglob("**/*.pyi"))
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
- diff = enrich_file(
127
- source_file,
128
- docstub_path,
129
- diff=True,
130
- write_back=write_back,
131
- package_name=package_name,
132
- )
133
- if diff:
134
- 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)
135
322
  if show_diff:
136
- print(diff)
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 (FileNotFoundError(f"No doc-stub file found for {source_file}")) from e
141
- # run black on the destination folder
142
- run_black(source_path)
143
- # DO NOT run Autoflake as this removes some relevant (unused) imports
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 ""