mpflash 1.24.7__py3-none-any.whl → 1.25.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 (49) hide show
  1. mpflash/ask_input.py +7 -7
  2. mpflash/basicgit.py +26 -59
  3. mpflash/bootloader/__init__.py +0 -2
  4. mpflash/bootloader/detect.py +1 -2
  5. mpflash/bootloader/manual.py +0 -1
  6. mpflash/bootloader/touch1200.py +2 -2
  7. mpflash/cli_flash.py +28 -5
  8. mpflash/cli_group.py +1 -0
  9. mpflash/cli_list.py +7 -8
  10. mpflash/cli_main.py +2 -2
  11. mpflash/common.py +6 -14
  12. mpflash/config.py +30 -6
  13. mpflash/connected.py +6 -14
  14. mpflash/db/boards.py +63 -0
  15. mpflash/db/downloads.py +87 -0
  16. mpflash/download/__init__.py +221 -0
  17. mpflash/download/from_web.py +204 -0
  18. mpflash/downloaded.py +9 -34
  19. mpflash/flash/__init__.py +33 -18
  20. mpflash/flash/esp.py +39 -8
  21. mpflash/flash/uf2/linux.py +4 -9
  22. mpflash/flash/uf2/macos.py +1 -1
  23. mpflash/flash/uf2/windows.py +1 -1
  24. mpflash/flash/worklist.py +10 -5
  25. mpflash/list.py +17 -6
  26. mpflash/logger.py +1 -3
  27. mpflash/mpboard_id/__init__.py +6 -87
  28. mpflash/mpboard_id/add_boards.py +3 -8
  29. mpflash/mpboard_id/board.py +5 -2
  30. mpflash/mpboard_id/board_id.py +67 -7
  31. mpflash/mpboard_id/board_info.json +30974 -0
  32. mpflash/mpboard_id/board_info.zip +0 -0
  33. mpflash/mpboard_id/known.py +108 -0
  34. mpflash/mpboard_id/store.py +2 -3
  35. mpflash/mpremoteboard/__init__.py +85 -17
  36. mpflash/mpremoteboard/mpy_fw_info.py +23 -22
  37. mpflash/py.typed +0 -0
  38. mpflash/vendor/board_database.py +86 -1
  39. mpflash/vendor/click_aliases.py +64 -0
  40. mpflash/vendor/dfu.py +2 -8
  41. mpflash/vendor/pydfu.py +3 -14
  42. mpflash/versions.py +16 -6
  43. {mpflash-1.24.7.dist-info → mpflash-1.25.0.dist-info}/METADATA +71 -13
  44. mpflash-1.25.0.dist-info/RECORD +62 -0
  45. {mpflash-1.24.7.dist-info → mpflash-1.25.0.dist-info}/WHEEL +1 -1
  46. mpflash/download.py +0 -364
  47. mpflash-1.24.7.dist-info/RECORD +0 -56
  48. {mpflash-1.24.7.dist-info → mpflash-1.25.0.dist-info}/LICENSE +0 -0
  49. {mpflash-1.24.7.dist-info → mpflash-1.25.0.dist-info}/entry_points.txt +0 -0
Binary file
@@ -0,0 +1,108 @@
1
+ """
2
+ KNOWN ports and boards are sourced from the micropython repo,
3
+ this info is stored in the board_info.json file
4
+ and is used to identify the board and port for flashing.
5
+ This module provides access to the board info and the known ports and boards."""
6
+
7
+ from functools import lru_cache
8
+ from typing import List, Optional, Tuple
9
+
10
+ from mpflash.db.boards import find_board_id, find_board_info
11
+ from mpflash.errors import MPFlashError
12
+ from mpflash.versions import clean_version
13
+ from mpflash.logger import log
14
+
15
+ from .board import Board
16
+ from .store import read_known_boardinfo
17
+
18
+
19
+ def get_known_ports() -> List[str]:
20
+ # TODO: Filter for Version
21
+ log.warning("get_known_ports() is deprecated")
22
+ mp_boards = read_known_boardinfo()
23
+ # select the unique ports from info
24
+ ports = set({board.port for board in mp_boards if board.port})
25
+ return sorted(list(ports))
26
+
27
+
28
+ def get_known_boards_for_port(port: Optional[str] = "", versions: Optional[List[str]] = None) -> List[Board]:
29
+ """
30
+ Returns a list of boards for the given port and version(s)
31
+
32
+ port: The Micropython port to filter for
33
+ versions: Optional, The Micropython versions to filter for (actual versions required)
34
+ """
35
+ mp_boards = read_known_boardinfo()
36
+ if versions:
37
+ preview_or_stable = "preview" in versions or "stable" in versions
38
+ else:
39
+ preview_or_stable = False
40
+
41
+ # filter for 'preview' as they are not in the board_info.json
42
+ # instead use stable version
43
+ versions = versions or []
44
+ if "preview" in versions:
45
+ versions.remove("preview")
46
+ versions.append("stable")
47
+ # filter for the port
48
+ if port:
49
+ mp_boards = [board for board in mp_boards if board.port == port]
50
+ if versions:
51
+ # make sure of the v prefix
52
+ versions = [clean_version(v) for v in versions]
53
+ # filter for the version(s)
54
+ mp_boards = [board for board in mp_boards if board.version in versions]
55
+ if not mp_boards and preview_or_stable:
56
+ # nothing found - perhaps there is a newer version for which we do not have the board info yet
57
+ # use the latest known version from the board info
58
+ mp_boards = read_known_boardinfo()
59
+ last_known_version = sorted({b.version for b in mp_boards})[-1]
60
+ mp_boards = [board for board in mp_boards if board.version == last_known_version]
61
+
62
+ return mp_boards
63
+
64
+
65
+ def known_stored_boards(port: str, versions: Optional[List[str]] = None) -> List[Tuple[str, str]]:
66
+ """
67
+ Returns a list of tuples with the description and board name for the given port and version
68
+
69
+ port : str : The Micropython port to filter for
70
+ versions : List[str] : The Micropython versions to filter for (actual versions required)
71
+ """
72
+ mp_boards = get_known_boards_for_port(port, versions)
73
+
74
+ boards = set({(f"{board.version} {board.description}", board.board_id) for board in mp_boards})
75
+ return sorted(list(boards))
76
+
77
+
78
+ @lru_cache(maxsize=20)
79
+ def find_known_board(board_id: str, version ="") -> Board:
80
+ """Find the board for the given BOARD_ID or 'board description' and return the board info as a Board object"""
81
+ # Some functional overlap with:
82
+ # mpboard_id\board_id.py _find_board_id_by_description
83
+ # TODO: Refactor to search the SQLite DB instead of the JSON file
84
+ board_ids = find_board_id(board_id = board_id, version = version or "%")
85
+ boards = []
86
+ for board_id in board_ids:
87
+ # if we have a board_id, use it to find the board info
88
+ boards += [Board.from_dict(dict(r)) for r in find_board_info(board_id = board_id)]
89
+
90
+
91
+ # if board_ids:
92
+ # # if we have a board_id, use it to find the board info
93
+ # board_id = board_ids[0]
94
+ # info = read_known_boardinfo()
95
+ # for board_info in info:
96
+ # if board_id in (
97
+ # board_info.board_id,
98
+ # board_info.description,
99
+ # ) or board_info.description.startswith(board_id):
100
+ # if not board_info.cpu:
101
+ # # failsafe for older board_info.json files
102
+ # print(f"Board {board_id} has no CPU info, using port as CPU")
103
+ # if " with " in board_info.description:
104
+ # board_info.cpu = board_info.description.split(" with ")[-1]
105
+ # else:
106
+ # board_info.cpu = board_info.port
107
+ # return board_info
108
+ raise MPFlashError(f"Board {board_id} not found")
@@ -2,6 +2,7 @@ import functools
2
2
  import zipfile
3
3
  from pathlib import Path
4
4
  from typing import Final, List, Optional
5
+ from mpflash.logger import log
5
6
 
6
7
  import jsons
7
8
 
@@ -19,7 +20,6 @@ def write_boardinfo_json(board_list: List[Board], *, folder: Optional[Path] = No
19
20
  board_list (List[Board]): The list of Board objects.
20
21
  folder (Path): The folder where the compressed JSON file will be saved.
21
22
  """
22
- import zipfile
23
23
 
24
24
  if not folder:
25
25
  folder = HERE
@@ -33,8 +33,7 @@ def write_boardinfo_json(board_list: List[Board], *, folder: Optional[Path] = No
33
33
  @functools.lru_cache(maxsize=20)
34
34
  def read_known_boardinfo(board_info: Optional[Path] = None) -> List[Board]:
35
35
  """Reads the board information from a JSON file in a zip file."""
36
-
37
- import zipfile
36
+ log.warning("read_known_boardinfo() is deprecated")
38
37
 
39
38
  if not board_info:
40
39
  board_info = HERE / "board_info.zip"
@@ -2,6 +2,7 @@
2
2
  Module to run mpremote commands, and retry on failure or timeout
3
3
  """
4
4
 
5
+ import contextlib
5
6
  import sys
6
7
  import time
7
8
  from pathlib import Path
@@ -33,7 +34,9 @@ RETRIES = 3
33
34
  class MPRemoteBoard:
34
35
  """Class to run mpremote commands"""
35
36
 
36
- def __init__(self, serialport: str = "", update: bool = False, *, location: str = ""):
37
+ def __init__(
38
+ self, serialport: str = "", update: bool = False, *, location: str = ""
39
+ ):
37
40
  """
38
41
  Initialize MPRemoteBoard object.
39
42
 
@@ -41,6 +44,8 @@ class MPRemoteBoard:
41
44
  - serialport (str): The serial port to connect to. Default is an empty string.
42
45
  - update (bool): Whether to update the MCU information. Default is False.
43
46
  """
47
+ self._board_id = ""
48
+
44
49
  self.serialport: str = serialport
45
50
  self.firmware = {}
46
51
 
@@ -50,7 +55,6 @@ class MPRemoteBoard:
50
55
  self.description = ""
51
56
  self.version = ""
52
57
  self.port = ""
53
- self.board = ""
54
58
  self.cpu = ""
55
59
  self.arch = ""
56
60
  self.mpy = ""
@@ -60,17 +64,46 @@ class MPRemoteBoard:
60
64
  if update:
61
65
  self.get_mcu_info()
62
66
 
67
+ ###################################
68
+ # board_id := board[-variant]
69
+ @property
70
+ def board_id(self) -> str:
71
+ return self._board_id
72
+
73
+ @board_id.setter
74
+ def board_id(self, value: str) -> None:
75
+ self._board_id = value.rstrip("-")
76
+
77
+ @property
78
+ def board(self) -> str:
79
+ return self._board_id.split("-")[0]
80
+
81
+ @board.setter
82
+ def board(self, value: str) -> None:
83
+ self.board_id = f"{value}-{self.variant}" if self.variant else value
84
+
85
+ @property
86
+ def variant(self) -> str:
87
+ return self._board_id.split("-")[1] if "-" in self._board_id else ""
88
+
89
+ @variant.setter
90
+ def variant(self, value: str) -> None:
91
+ self.board_id = f"{self.board}-{value}"
92
+
93
+ ###################################
63
94
  def __str__(self):
64
95
  """
65
96
  Return a string representation of the MPRemoteBoard object.
66
97
 
67
98
  Returns:
68
- - str: The string representation of the object.
99
+ - str: A human readable representation of the MCU.
69
100
  """
70
- return f"MPRemoteBoard({self.serialport}, {self.family} {self.port}, {self.board}, {self.version})"
101
+ return f"MPRemoteBoard({self.serialport}, {self.family} {self.port}, {self.board}{f'-{self.variant}' if self.variant else ''}, {self.version})"
71
102
 
72
103
  @staticmethod
73
- def connected_boards(bluetooth: bool = False, description: bool = False) -> List[str]:
104
+ def connected_boards(
105
+ bluetooth: bool = False, description: bool = False
106
+ ) -> List[str]:
74
107
  # TODO: rename to connected_comports
75
108
  """
76
109
  Get a list of connected comports.
@@ -98,7 +131,10 @@ class MPRemoteBoard:
98
131
  if sys.platform == "win32":
99
132
  # Windows sort of comports by number - but fallback to device name
100
133
  return sorted(
101
- output, key=lambda x: int(x.split()[0][3:]) if x.split()[0][3:].isdigit() else x
134
+ output,
135
+ key=lambda x: int(x.split()[0][3:])
136
+ if x.split()[0][3:].isdigit()
137
+ else x,
102
138
  )
103
139
  # sort by device name
104
140
  return sorted(output)
@@ -120,11 +156,11 @@ class MPRemoteBoard:
120
156
  timeout=timeout,
121
157
  resume=False, # Avoid restarts
122
158
  )
123
- if rc:
159
+ if rc not in (0, 1): ## WORKAROUND - SUDDEN RETURN OF 1 on success
124
160
  log.debug(f"rc: {rc}, result: {result}")
125
161
  raise ConnectionError(f"Failed to get mcu_info for {self.serialport}")
126
162
  # Ok we have the info, now parse it
127
- raw_info = result[0].strip()
163
+ raw_info = result[0].strip() if result else ""
128
164
  if raw_info.startswith("{") and raw_info.endswith("}"):
129
165
  info = eval(raw_info)
130
166
  self.family = info["family"]
@@ -137,14 +173,19 @@ class MPRemoteBoard:
137
173
  self.description = descr = info["board"]
138
174
  pos = descr.rfind(" with")
139
175
  short_descr = descr[:pos].strip() if pos != -1 else ""
140
- if board_name := find_board_id_by_description(
141
- descr, short_descr, version=self.version
142
- ):
143
- self.board = board_name
176
+ if info.get("board_id", None):
177
+ # we have a board_id - so use that to get the board name
178
+ self.board_id = info["board_id"]
144
179
  else:
145
- self.board = "UNKNOWN_BOARD"
180
+ self.board_id = f"{info['board']}-{info.get('variant', '')}"
181
+ board_name = find_board_id_by_description(
182
+ descr, short_descr, version=self.version
183
+ )
184
+ self.board_id = board_name or "UNKNOWN_BOARD"
185
+ # TODO: Get the variant as well
146
186
  # get the board_info.toml
147
187
  self.get_board_info_toml()
188
+ # TODO: get board_id from the toml file if it exists
148
189
  # now we know the board is connected
149
190
  self.connected = True
150
191
 
@@ -168,7 +209,9 @@ class MPRemoteBoard:
168
209
  log_errors=False,
169
210
  )
170
211
  except Exception as e:
171
- raise ConnectionError(f"Failed to get board_info.toml for {self.serialport}: {e}")
212
+ raise ConnectionError(
213
+ f"Failed to get board_info.toml for {self.serialport}:"
214
+ ) from e
172
215
  # this is optional - so only parse if we got the file
173
216
  self.toml = {}
174
217
  if rc in [OK]: # sometimes we get an -9 ???
@@ -264,8 +307,33 @@ class MPRemoteBoard:
264
307
  total=timeout,
265
308
  ):
266
309
  time.sleep(1)
267
- try:
310
+ with contextlib.suppress(ConnectionError, MPFlashError):
268
311
  self.get_mcu_info()
269
312
  break
270
- except (ConnectionError, MPFlashError):
271
- pass
313
+
314
+ def to_dict(self) -> dict:
315
+ """
316
+ Serialize the MPRemoteBoard object to JSON, including all attributes and readable properties.
317
+
318
+ Returns:
319
+ - str: A JSON string representation of the object.
320
+ """
321
+
322
+ def get_properties(obj):
323
+ """Helper function to get all readable properties."""
324
+ return {
325
+ name: getattr(obj, name)
326
+ for name in dir(obj.__class__)
327
+ if isinstance(getattr(obj.__class__, name, None), property)
328
+ }
329
+
330
+ # Combine instance attributes, readable properties, and private attributes
331
+ data = {**self.__dict__, **get_properties(self)}
332
+
333
+ # remove the path and firmware attibutes from the json output as they are always empty
334
+ del data["_board_id"] # dup of board_id
335
+ del data["connected"]
336
+ del data["path"]
337
+ del data["firmware"]
338
+
339
+ return data
@@ -1,9 +1,9 @@
1
- # %%micropython
1
+ # pragma: no cover
2
2
  import os
3
3
  import sys
4
4
 
5
5
 
6
- def _build(s):
6
+ def get_build(s):
7
7
  # extract build from sys.version or os.uname().version if available
8
8
  # sys.version: 'MicroPython v1.23.0-preview.6.g3d0b6276f'
9
9
  # sys.implementation.version: 'v1.13-103-gb137d064e'
@@ -11,11 +11,11 @@ def _build(s):
11
11
  return ""
12
12
  s = s.split(" on ", 1)[0] if " on " in s else s
13
13
  if s.startswith("v"):
14
- if not "-" in s:
14
+ if "-" not in s:
15
15
  return ""
16
16
  b = s.split("-")[1]
17
17
  return b
18
- if not "-preview" in s:
18
+ if "-preview" not in s:
19
19
  return ""
20
20
  b = s.split("-preview")[1].split(".")[1]
21
21
  return b
@@ -36,10 +36,9 @@ def _info(): # type:() -> dict[str, str]
36
36
  "version": "",
37
37
  "build": "",
38
38
  "ver": "",
39
- "port": (
40
- "stm32" if sys.platform.startswith("pyb") else sys.platform
41
- ), # port: esp32 / win32 / linux / stm32
39
+ "port": ("stm32" if sys.platform.startswith("pyb") else sys.platform), # port: esp32 / win32 / linux / stm32
42
40
  "board": "GENERIC",
41
+ "_build": "",
43
42
  "cpu": "",
44
43
  "mpy": "",
45
44
  "arch": "",
@@ -50,29 +49,32 @@ def _info(): # type:() -> dict[str, str]
50
49
  except AttributeError:
51
50
  pass
52
51
  try:
53
- machine = (
54
- sys.implementation._machine
55
- if "_machine" in dir(sys.implementation)
56
- else os.uname().machine
57
- )
58
- info["board"] = machine.strip()
59
- info["cpu"] = machine.split("with")[-1].strip() if "with" in machine else ""
52
+ _machine = sys.implementation._machine if "_machine" in dir(sys.implementation) else os.uname().machine # type: ignore
53
+ info["board"] = _machine.strip()
54
+ si_build = sys.implementation._build if "_build" in dir(sys.implementation) else ""
55
+ if si_build:
56
+ info["board"] = si_build.split("-")[0]
57
+ info["variant"] = si_build.split("-")[1] if "-" in si_build else ""
58
+ info["board_id"] = si_build
59
+ info["cpu"] = _machine.split("with")[-1].strip() if "with" in _machine else ""
60
60
  info["mpy"] = (
61
61
  sys.implementation._mpy
62
62
  if "_mpy" in dir(sys.implementation)
63
- else sys.implementation.mpy if "mpy" in dir(sys.implementation) else ""
63
+ else sys.implementation.mpy
64
+ if "mpy" in dir(sys.implementation)
65
+ else ""
64
66
  )
65
67
  except (AttributeError, IndexError):
66
68
  pass
67
69
 
68
70
  try:
69
71
  if hasattr(sys, "version"):
70
- info["build"] = _build(sys.version)
72
+ info["build"] = get_build(sys.version)
71
73
  elif hasattr(os, "uname"):
72
- info["build"] = _build(os.uname()[3])
74
+ info["build"] = get_build(os.uname()[3]) # type: ignore
73
75
  if not info["build"]:
74
76
  # extract build from uname().release if available
75
- info["build"] = _build(os.uname()[2])
77
+ info["build"] = get_build(os.uname()[2]) # type: ignore
76
78
  except (AttributeError, IndexError):
77
79
  pass
78
80
  # avoid build hashes
@@ -81,7 +83,7 @@ def _info(): # type:() -> dict[str, str]
81
83
 
82
84
  if info["version"] == "" and sys.platform not in ("unix", "win32"):
83
85
  try:
84
- u = os.uname()
86
+ u = os.uname() # type: ignore
85
87
  info["version"] = u.release
86
88
  except (IndexError, AttributeError, TypeError):
87
89
  pass
@@ -106,8 +108,7 @@ def _info(): # type:() -> dict[str, str]
106
108
  if (
107
109
  info["version"]
108
110
  and info["version"].endswith(".0")
109
- and info["version"]
110
- >= "1.10.0" # versions from 1.10.0 to 1.20.0 do not have a micro .0
111
+ and info["version"] >= "1.10.0" # versions from 1.10.0 to 1.20.0 do not have a micro .0
111
112
  and info["version"] <= "1.19.9"
112
113
  ):
113
114
  # drop the .0 for newer releases
@@ -149,4 +150,4 @@ def _info(): # type:() -> dict[str, str]
149
150
 
150
151
 
151
152
  print(_info())
152
- del _info, _build, _version_str
153
+ del _info, get_build, _version_str
mpflash/py.typed ADDED
File without changes
@@ -37,7 +37,9 @@ from __future__ import annotations
37
37
  import json
38
38
  from dataclasses import dataclass, field
39
39
  from glob import glob
40
+ from os import path
40
41
  from pathlib import Path
42
+ import re
41
43
 
42
44
 
43
45
  @dataclass(order=True)
@@ -52,6 +54,15 @@ class Variant:
52
54
  """
53
55
  board: Board = field(repr=False)
54
56
 
57
+ @property
58
+ def description(self) -> str:
59
+ """
60
+ Description of the board, if available.
61
+ Example: "Pyboard v1.1 with STM32F4"
62
+ """
63
+ return description_from_source(self.board.path, self.name) or self.board.description
64
+ # f"{self.board.description}-{self.name}"
65
+
55
66
 
56
67
  @dataclass(order=True)
57
68
  class Board:
@@ -93,6 +104,12 @@ class Board:
93
104
  """
94
105
  port: Port | None = field(default=None, compare=False)
95
106
 
107
+ path: str = ""
108
+ """
109
+ the relative path to the boards files.
110
+ Example: "ports/stm32/boards/PYBV11"
111
+ """
112
+
96
113
  @staticmethod
97
114
  def factory(filename_json: Path) -> Board:
98
115
  with filename_json.open() as f:
@@ -101,18 +118,27 @@ class Board:
101
118
  board = Board(
102
119
  name=filename_json.parent.name,
103
120
  variants=[],
104
- url=board_json["url"],
121
+ url=board_json["url"] if "url" in board_json else "", # fix missing url
105
122
  mcu=board_json["mcu"],
106
123
  product=board_json["product"],
107
124
  vendor=board_json["vendor"],
108
125
  images=board_json["images"],
109
126
  deploy=board_json["deploy"],
127
+ path=filename_json.parent.as_posix(),
110
128
  )
111
129
  board.variants.extend(
112
130
  sorted([Variant(*v, board) for v in board_json.get("variants", {}).items()]) # type: ignore
113
131
  )
114
132
  return board
115
133
 
134
+ @property
135
+ def description(self) -> str:
136
+ """
137
+ Description of the board, if available.
138
+ Example: "Pyboard v1.1 with STM32F4"
139
+ """
140
+ return description_from_source(self.path, "") or self.name
141
+
116
142
 
117
143
  @dataclass(order=True)
118
144
  class Port:
@@ -140,6 +166,8 @@ class Database:
140
166
 
141
167
  def __post_init__(self) -> None:
142
168
  mpy_dir = self.mpy_root_directory
169
+ if not mpy_dir.is_dir():
170
+ raise ValueError(f"Invalid path to micropython directory: {mpy_dir}")
143
171
  # Take care to avoid using Path.glob! Performance was 15x slower.
144
172
  for p in glob(f"{mpy_dir}/ports/**/boards/**/board.json"):
145
173
  filename_json = Path(p)
@@ -176,6 +204,7 @@ class Database:
176
204
  "",
177
205
  [],
178
206
  [],
207
+ path=path.as_posix(),
179
208
  )
180
209
  board.variants = [Variant(v, "", board) for v in variant_names]
181
210
  port = Port(special_port_name, {special_port_name: board})
@@ -183,3 +212,59 @@ class Database:
183
212
 
184
213
  self.ports[special_port_name] = port
185
214
  self.boards[board.name] = board
215
+
216
+
217
+ # look for all mpconfigboard.h files and extract the board name
218
+ # from the #define MICROPY_HW_BOARD_NAME "PYBD_SF6"
219
+ # and the #define MICROPY_HW_MCU_NAME "STM32F767xx"
220
+ RE_H_MICROPY_HW_BOARD_NAME = re.compile(r"#define\s+MICROPY_HW_BOARD_NAME\s+\"(.+)\"")
221
+ RE_H_MICROPY_HW_MCU_NAME = re.compile(r"#define\s+MICROPY_HW_MCU_NAME\s+\"(.+)\"")
222
+ # find boards and variants in the mpconfigboard*.cmake files
223
+ RE_CMAKE_MICROPY_HW_BOARD_NAME = re.compile(r"MICROPY_HW_BOARD_NAME\s?=\s?\"(?P<variant>[\w\s\S]*)\"")
224
+ RE_CMAKE_MICROPY_HW_MCU_NAME = re.compile(r"MICROPY_HW_MCU_NAME\s?=\s?\"(?P<variant>[\w\s\S]*)\"")
225
+
226
+
227
+ def description_from_source(board_path: str | Path, variant: str = "") -> str:
228
+ """Get the board's description from the header or make files."""
229
+ return description_from_header(board_path, variant) or description_from_cmake(board_path, variant)
230
+
231
+
232
+ def description_from_header(board_path: str | Path, variant: str = "") -> str:
233
+ """Get the board's description from the mpconfigboard.h file."""
234
+
235
+ mpconfig_path = path.join(board_path, f"mpconfigboard_{variant}.h" if variant else "mpconfigboard.h")
236
+ if not path.exists(mpconfig_path):
237
+ return f""
238
+
239
+ with open(mpconfig_path, "r") as f:
240
+ board_name = mcu_name = "-"
241
+ found = 0
242
+ for line in f:
243
+ if match := RE_H_MICROPY_HW_BOARD_NAME.match(line):
244
+ board_name = match[1]
245
+ found += 1
246
+ elif match := RE_H_MICROPY_HW_MCU_NAME.match(line):
247
+ mcu_name = match[1]
248
+ found += 1
249
+ if found == 2:
250
+ return f"{board_name} with {mcu_name}" if mcu_name != "-" else board_name
251
+ return board_name if found == 1 else ""
252
+
253
+
254
+ def description_from_cmake(board_path: str | Path, variant: str = "") -> str:
255
+ """Get the board's description from the mpconfig[board|variant].cmake file."""
256
+
257
+ cmake_path = path.join(board_path, f"mpconfigvariant_{variant}.cmake" if variant else "mpconfigboard.cmake")
258
+ if not path.exists(cmake_path):
259
+ return f""
260
+ with open(cmake_path, "r") as f:
261
+ board_name = mcu_name = "-"
262
+ for line in f:
263
+ line = line.strip()
264
+ if match := RE_CMAKE_MICROPY_HW_BOARD_NAME.match(line):
265
+ description = match["variant"]
266
+ return description
267
+ elif match := RE_CMAKE_MICROPY_HW_MCU_NAME.match(line):
268
+ description = match["variant"]
269
+ return description
270
+ return ""
@@ -10,6 +10,7 @@
10
10
  # The above copyright notice and this permission notice shall be included in all
11
11
  # copies or substantial portions of the Software.
12
12
  # ------------------------------------------------------------------------------------
13
+ # Jos Verlinde - 2024
13
14
  # modified to avoid conflcts with rich_click
14
15
 
15
16
  # sourcery skip: assign-if-exp, use-named-expression
@@ -20,12 +21,36 @@ _click7 = click.__version__[0] >= "7"
20
21
 
21
22
 
22
23
  class ClickAliasedGroup(click.RichGroup):
24
+ """
25
+ A subclass of click.RichGroup that adds support for command aliases.
26
+
27
+ This class allows defining aliases for commands and groups, enabling users
28
+ to invoke commands using alternative names.
29
+ """
30
+
23
31
  def __init__(self, *args, **kwargs):
32
+ """
33
+ Initialize the ClickAliasedGroup instance.
34
+
35
+ Args:
36
+ *args: Positional arguments passed to the superclass.
37
+ **kwargs: Keyword arguments passed to the superclass.
38
+ """
24
39
  super().__init__(*args, **kwargs)
25
40
  self._commands = {}
26
41
  self._aliases = {}
27
42
 
28
43
  def add_command(self, *args, **kwargs):
44
+ """
45
+ Add a command to the group, optionally with aliases.
46
+
47
+ Args:
48
+ *args: Positional arguments, typically the command instance and optionally its name.
49
+ **kwargs: Keyword arguments, may include 'aliases' as a list of alternative names.
50
+
51
+ Raises:
52
+ TypeError: If the command has no name.
53
+ """
29
54
  aliases = kwargs.pop("aliases", [])
30
55
  super().add_command(*args, **kwargs)
31
56
  if aliases:
@@ -40,6 +65,16 @@ class ClickAliasedGroup(click.RichGroup):
40
65
  self._aliases[alias] = cmd.name
41
66
 
42
67
  def command(self, *args, **kwargs):
68
+ """
69
+ Decorator to define a new command with optional aliases.
70
+
71
+ Args:
72
+ *args: Positional arguments passed to the superclass decorator.
73
+ **kwargs: Keyword arguments, may include 'aliases' as a list of alternative names.
74
+
75
+ Returns:
76
+ Callable: A decorator function that registers the command and its aliases.
77
+ """
43
78
  aliases = kwargs.pop("aliases", [])
44
79
  decorator = super().command(*args, **kwargs)
45
80
  if not aliases:
@@ -56,6 +91,16 @@ class ClickAliasedGroup(click.RichGroup):
56
91
  return _decorator
57
92
 
58
93
  def group(self, *args, **kwargs):
94
+ """
95
+ Decorator to define a new command group with optional aliases.
96
+
97
+ Args:
98
+ *args: Positional arguments passed to the superclass decorator.
99
+ **kwargs: Keyword arguments, may include 'aliases' as a list of alternative names.
100
+
101
+ Returns:
102
+ Callable: A decorator function that registers the group and its aliases.
103
+ """
59
104
  aliases = kwargs.pop("aliases", [])
60
105
  decorator = super().group(*args, **kwargs)
61
106
  if not aliases:
@@ -72,11 +117,30 @@ class ClickAliasedGroup(click.RichGroup):
72
117
  return _decorator
73
118
 
74
119
  def resolve_alias(self, cmd_name):
120
+ """
121
+ Resolve a command alias to its original command name.
122
+
123
+ Args:
124
+ cmd_name (str): The command name or alias to resolve.
125
+
126
+ Returns:
127
+ str: The original command name if an alias is provided; otherwise, the input name.
128
+ """
75
129
  if cmd_name in self._aliases:
76
130
  return self._aliases[cmd_name]
77
131
  return cmd_name
78
132
 
79
133
  def get_command(self, ctx, cmd_name):
134
+ """
135
+ Retrieve a command by name or alias.
136
+
137
+ Args:
138
+ ctx (click.Context): The Click context object.
139
+ cmd_name (str): The command name or alias to retrieve.
140
+
141
+ Returns:
142
+ click.Command or None: The command object if found; otherwise, None.
143
+ """
80
144
  cmd_name = self.resolve_alias(cmd_name)
81
145
  command = super().get_command(ctx, cmd_name)
82
146
  if command: