micropython-stubber 1.23.1.post1__py3-none-any.whl → 1.23.2__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.23.1.post1.dist-info → micropython_stubber-1.23.2.dist-info}/LICENSE +30 -30
  2. {micropython_stubber-1.23.1.post1.dist-info → micropython_stubber-1.23.2.dist-info}/METADATA +4 -4
  3. micropython_stubber-1.23.2.dist-info/RECORD +158 -0
  4. mpflash/README.md +220 -220
  5. mpflash/libusb_flash.ipynb +203 -203
  6. mpflash/mpflash/add_firmware.py +98 -98
  7. mpflash/mpflash/ask_input.py +236 -236
  8. mpflash/mpflash/basicgit.py +284 -284
  9. mpflash/mpflash/bootloader/__init__.py +2 -2
  10. mpflash/mpflash/bootloader/activate.py +60 -60
  11. mpflash/mpflash/bootloader/detect.py +82 -82
  12. mpflash/mpflash/bootloader/manual.py +101 -101
  13. mpflash/mpflash/bootloader/micropython.py +12 -12
  14. mpflash/mpflash/bootloader/touch1200.py +36 -36
  15. mpflash/mpflash/cli_download.py +129 -129
  16. mpflash/mpflash/cli_flash.py +224 -216
  17. mpflash/mpflash/cli_group.py +111 -111
  18. mpflash/mpflash/cli_list.py +87 -87
  19. mpflash/mpflash/cli_main.py +39 -39
  20. mpflash/mpflash/common.py +210 -166
  21. mpflash/mpflash/config.py +44 -44
  22. mpflash/mpflash/connected.py +96 -77
  23. mpflash/mpflash/download.py +364 -364
  24. mpflash/mpflash/downloaded.py +130 -130
  25. mpflash/mpflash/errors.py +9 -9
  26. mpflash/mpflash/flash/__init__.py +55 -55
  27. mpflash/mpflash/flash/esp.py +59 -59
  28. mpflash/mpflash/flash/stm32.py +19 -19
  29. mpflash/mpflash/flash/stm32_dfu.py +104 -104
  30. mpflash/mpflash/flash/uf2/__init__.py +88 -88
  31. mpflash/mpflash/flash/uf2/boardid.py +15 -15
  32. mpflash/mpflash/flash/uf2/linux.py +136 -130
  33. mpflash/mpflash/flash/uf2/macos.py +42 -42
  34. mpflash/mpflash/flash/uf2/uf2disk.py +12 -12
  35. mpflash/mpflash/flash/uf2/windows.py +43 -43
  36. mpflash/mpflash/flash/worklist.py +170 -170
  37. mpflash/mpflash/list.py +106 -106
  38. mpflash/mpflash/logger.py +41 -41
  39. mpflash/mpflash/mpboard_id/__init__.py +93 -93
  40. mpflash/mpflash/mpboard_id/add_boards.py +251 -251
  41. mpflash/mpflash/mpboard_id/board.py +37 -37
  42. mpflash/mpflash/mpboard_id/board_id.py +86 -86
  43. mpflash/mpflash/mpboard_id/store.py +43 -43
  44. mpflash/mpflash/mpremoteboard/__init__.py +266 -266
  45. mpflash/mpflash/mpremoteboard/mpy_fw_info.py +141 -141
  46. mpflash/mpflash/mpremoteboard/runner.py +140 -140
  47. mpflash/mpflash/vendor/click_aliases.py +91 -91
  48. mpflash/mpflash/vendor/dfu.py +165 -165
  49. mpflash/mpflash/vendor/pydfu.py +605 -605
  50. mpflash/mpflash/vendor/readme.md +2 -2
  51. mpflash/mpflash/versions.py +135 -135
  52. mpflash/poetry.lock +1599 -1599
  53. mpflash/pyproject.toml +65 -65
  54. mpflash/stm32_udev_rules.md +62 -62
  55. stubber/__init__.py +3 -3
  56. stubber/board/board_info.csv +193 -193
  57. stubber/board/boot.py +34 -34
  58. stubber/board/createstubs.py +1004 -986
  59. stubber/board/createstubs_db.py +826 -825
  60. stubber/board/createstubs_db_min.py +332 -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 +767 -766
  65. stubber/board/createstubs_mem_min.py +307 -306
  66. stubber/board/createstubs_mem_mpy.mpy +0 -0
  67. stubber/board/createstubs_min.py +295 -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 +437 -437
  75. stubber/codemod/_partials/__init__.py +48 -48
  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 +151 -145
  83. stubber/codemod/merge_docstub.py +284 -284
  84. stubber/codemod/modify_list.py +54 -54
  85. stubber/codemod/utils.py +56 -56
  86. stubber/commands/build_cmd.py +94 -94
  87. stubber/commands/cli.py +49 -49
  88. stubber/commands/clone_cmd.py +78 -78
  89. stubber/commands/config_cmd.py +29 -29
  90. stubber/commands/enrich_folder_cmd.py +71 -71
  91. stubber/commands/get_core_cmd.py +71 -71
  92. stubber/commands/get_docstubs_cmd.py +92 -92
  93. stubber/commands/get_frozen_cmd.py +117 -117
  94. stubber/commands/get_mcu_cmd.py +102 -102
  95. stubber/commands/merge_cmd.py +66 -66
  96. stubber/commands/publish_cmd.py +118 -118
  97. stubber/commands/stub_cmd.py +31 -31
  98. stubber/commands/switch_cmd.py +62 -62
  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 +37 -37
  107. stubber/freeze/common.py +72 -72
  108. stubber/freeze/freeze_folder.py +69 -69
  109. stubber/freeze/freeze_manifest_2.py +126 -126
  110. stubber/freeze/get_frozen.py +131 -131
  111. stubber/get_cpython.py +112 -112
  112. stubber/get_lobo.py +59 -59
  113. stubber/minify.py +423 -423
  114. stubber/publish/bump.py +86 -86
  115. stubber/publish/candidates.py +275 -275
  116. stubber/publish/database.py +18 -18
  117. stubber/publish/defaults.py +40 -40
  118. stubber/publish/enums.py +24 -24
  119. stubber/publish/helpers.py +29 -29
  120. stubber/publish/merge_docstubs.py +136 -132
  121. stubber/publish/missing_class_methods.py +51 -51
  122. stubber/publish/package.py +150 -150
  123. stubber/publish/pathnames.py +51 -51
  124. stubber/publish/publish.py +120 -120
  125. stubber/publish/pypi.py +42 -42
  126. stubber/publish/stubpackage.py +1055 -1051
  127. stubber/rst/__init__.py +9 -9
  128. stubber/rst/classsort.py +78 -78
  129. stubber/rst/lookup.py +533 -531
  130. stubber/rst/output_dict.py +401 -401
  131. stubber/rst/reader.py +814 -814
  132. stubber/rst/report_return.py +77 -77
  133. stubber/rst/rst_utils.py +541 -541
  134. stubber/stubber.py +38 -38
  135. stubber/stubs_from_docs.py +90 -90
  136. stubber/tools/manifestfile.py +654 -654
  137. stubber/tools/readme.md +6 -6
  138. stubber/update_fallback.py +117 -117
  139. stubber/update_module_list.py +123 -123
  140. stubber/utils/__init__.py +6 -6
  141. stubber/utils/config.py +137 -137
  142. stubber/utils/makeversionhdr.py +54 -54
  143. stubber/utils/manifest.py +90 -90
  144. stubber/utils/post.py +80 -80
  145. stubber/utils/repos.py +156 -156
  146. stubber/utils/stubmaker.py +139 -139
  147. stubber/utils/typed_config_toml.py +80 -80
  148. stubber/variants.py +106 -106
  149. micropython_stubber-1.23.1.post1.dist-info/RECORD +0 -159
  150. mpflash/basicgit.py +0 -288
  151. {micropython_stubber-1.23.1.post1.dist-info → micropython_stubber-1.23.2.dist-info}/WHEEL +0 -0
  152. {micropython_stubber-1.23.1.post1.dist-info → micropython_stubber-1.23.2.dist-info}/entry_points.txt +0 -0
@@ -1,141 +1,141 @@
1
- # %%micropython
2
- import os
3
- import sys
4
-
5
-
6
- def _build(s):
7
- # extract build from sys.version or os.uname().version if available
8
- # sys.version: 'MicroPython v1.23.0-preview.6.g3d0b6276f'
9
- # sys.implementation.version: 'v1.13-103-gb137d064e'
10
- if not s:
11
- return ""
12
- s = s.split(" on ", 1)[0] if " on " in s else s
13
- if s.startswith("v"):
14
- if not "-" in s:
15
- return ""
16
- b = s.split("-")[1]
17
- return b
18
- if not "-preview" in s:
19
- return ""
20
- b = s.split("-preview")[1].split(".")[1]
21
- return b
22
-
23
-
24
- def _version_str(version: tuple): # -> str:
25
- v_str = ".".join([str(n) for n in version[:3]])
26
- if len(version) > 3 and version[3]:
27
- v_str += "-" + version[3]
28
- return v_str
29
-
30
-
31
- def _info(): # type:() -> dict[str, str]
32
- # sourcery skip: use-contextlib-suppress, use-fstring-for-formatting, use-named-expression
33
- info = dict(
34
- {
35
- "family": sys.implementation[0], # type: ignore
36
- "version": "",
37
- "build": "",
38
- "ver": "",
39
- "port": "stm32" if sys.platform.startswith("pyb") else sys.platform, # port: esp32 / win32 / linux / stm32
40
- "board": "GENERIC",
41
- "cpu": "",
42
- "mpy": "",
43
- "arch": "",
44
- }
45
- )
46
- try:
47
- info["version"] = _version_str(sys.implementation.version)
48
- except AttributeError:
49
- pass
50
- try:
51
- machine = sys.implementation._machine if "_machine" in dir(sys.implementation) else os.uname().machine
52
- info["board"] = machine.strip()
53
- info["cpu"] = machine.split("with")[-1].strip() if "with" in machine else ""
54
- info["mpy"] = (
55
- sys.implementation._mpy
56
- if "_mpy" in dir(sys.implementation)
57
- else sys.implementation.mpy if "mpy" in dir(sys.implementation) else ""
58
- )
59
- except (AttributeError, IndexError):
60
- pass
61
-
62
- try:
63
- if hasattr(sys, "version"):
64
- info["build"] = _build(sys.version)
65
- elif hasattr(os, "uname"):
66
- info["build"] = _build(os.uname()[3])
67
- if not info["build"]:
68
- # extract build from uname().release if available
69
- info["build"] = _build(os.uname()[2])
70
- except (AttributeError, IndexError):
71
- pass
72
- # avoid build hashes
73
- if info["build"] and len(info["build"]) > 5:
74
- info["build"] = ""
75
-
76
- if info["version"] == "" and sys.platform not in ("unix", "win32"):
77
- try:
78
- u = os.uname()
79
- info["version"] = u.release
80
- except (IndexError, AttributeError, TypeError):
81
- pass
82
- # detect families
83
- for fam_name, mod_name, mod_thing in [
84
- ("pycopy", "pycopy", "const"),
85
- ("pycom", "pycom", "FAT"),
86
- ("ev3-pybricks", "pybricks.hubs", "EV3Brick"),
87
- ]:
88
- try:
89
- _t = __import__(mod_name, None, None, (mod_thing))
90
- info["family"] = fam_name
91
- del _t
92
- break
93
- except (ImportError, KeyError):
94
- pass
95
-
96
- if info["family"] == "ev3-pybricks":
97
- info["release"] = "2.0.0"
98
-
99
- if info["family"] == "micropython":
100
- if (
101
- info["version"]
102
- and info["version"].endswith(".0")
103
- and info["version"] >= "1.10.0" # versions from 1.10.0 to 1.20.0 do not have a micro .0
104
- and info["version"] <= "1.19.9"
105
- ):
106
- # drop the .0 for newer releases
107
- info["version"] = info["version"][:-2]
108
-
109
- # spell-checker: disable
110
- if "mpy" in info and info["mpy"]: # mpy on some v1.11+ builds
111
- sys_mpy = int(info["mpy"])
112
- # .mpy architecture
113
- arch = [
114
- None,
115
- "x86",
116
- "x64",
117
- "armv6",
118
- "armv6m",
119
- "armv7m",
120
- "armv7em",
121
- "armv7emsp",
122
- "armv7emdp",
123
- "xtensa",
124
- "xtensawin",
125
- ][sys_mpy >> 10]
126
- if arch:
127
- info["arch"] = arch
128
- # .mpy version.minor
129
- info["mpy"] = "v{}.{}".format(sys_mpy & 0xFF, sys_mpy >> 8 & 3)
130
- # simple to use version[-build] string avoiding f-strings for backward compat
131
- info["ver"] = (
132
- "v{version}-{build}".format(version=info["version"], build=info["build"])
133
- if info["build"]
134
- else "v{version}".format(version=info["version"])
135
- )
136
-
137
- return info
138
-
139
-
140
- print(_info())
141
- del _info, _build, _version_str
1
+ # %%micropython
2
+ import os
3
+ import sys
4
+
5
+
6
+ def _build(s):
7
+ # extract build from sys.version or os.uname().version if available
8
+ # sys.version: 'MicroPython v1.23.0-preview.6.g3d0b6276f'
9
+ # sys.implementation.version: 'v1.13-103-gb137d064e'
10
+ if not s:
11
+ return ""
12
+ s = s.split(" on ", 1)[0] if " on " in s else s
13
+ if s.startswith("v"):
14
+ if not "-" in s:
15
+ return ""
16
+ b = s.split("-")[1]
17
+ return b
18
+ if not "-preview" in s:
19
+ return ""
20
+ b = s.split("-preview")[1].split(".")[1]
21
+ return b
22
+
23
+
24
+ def _version_str(version: tuple): # -> str:
25
+ v_str = ".".join([str(n) for n in version[:3]])
26
+ if len(version) > 3 and version[3]:
27
+ v_str += "-" + version[3]
28
+ return v_str
29
+
30
+
31
+ def _info(): # type:() -> dict[str, str]
32
+ # sourcery skip: use-contextlib-suppress, use-fstring-for-formatting, use-named-expression
33
+ info = dict(
34
+ {
35
+ "family": sys.implementation[0], # type: ignore
36
+ "version": "",
37
+ "build": "",
38
+ "ver": "",
39
+ "port": "stm32" if sys.platform.startswith("pyb") else sys.platform, # port: esp32 / win32 / linux / stm32
40
+ "board": "GENERIC",
41
+ "cpu": "",
42
+ "mpy": "",
43
+ "arch": "",
44
+ }
45
+ )
46
+ try:
47
+ info["version"] = _version_str(sys.implementation.version)
48
+ except AttributeError:
49
+ pass
50
+ try:
51
+ machine = sys.implementation._machine if "_machine" in dir(sys.implementation) else os.uname().machine
52
+ info["board"] = machine.strip()
53
+ info["cpu"] = machine.split("with")[-1].strip() if "with" in machine else ""
54
+ info["mpy"] = (
55
+ sys.implementation._mpy
56
+ if "_mpy" in dir(sys.implementation)
57
+ else sys.implementation.mpy if "mpy" in dir(sys.implementation) else ""
58
+ )
59
+ except (AttributeError, IndexError):
60
+ pass
61
+
62
+ try:
63
+ if hasattr(sys, "version"):
64
+ info["build"] = _build(sys.version)
65
+ elif hasattr(os, "uname"):
66
+ info["build"] = _build(os.uname()[3])
67
+ if not info["build"]:
68
+ # extract build from uname().release if available
69
+ info["build"] = _build(os.uname()[2])
70
+ except (AttributeError, IndexError):
71
+ pass
72
+ # avoid build hashes
73
+ if info["build"] and len(info["build"]) > 5:
74
+ info["build"] = ""
75
+
76
+ if info["version"] == "" and sys.platform not in ("unix", "win32"):
77
+ try:
78
+ u = os.uname()
79
+ info["version"] = u.release
80
+ except (IndexError, AttributeError, TypeError):
81
+ pass
82
+ # detect families
83
+ for fam_name, mod_name, mod_thing in [
84
+ ("pycopy", "pycopy", "const"),
85
+ ("pycom", "pycom", "FAT"),
86
+ ("ev3-pybricks", "pybricks.hubs", "EV3Brick"),
87
+ ]:
88
+ try:
89
+ _t = __import__(mod_name, None, None, (mod_thing))
90
+ info["family"] = fam_name
91
+ del _t
92
+ break
93
+ except (ImportError, KeyError):
94
+ pass
95
+
96
+ if info["family"] == "ev3-pybricks":
97
+ info["release"] = "2.0.0"
98
+
99
+ if info["family"] == "micropython":
100
+ if (
101
+ info["version"]
102
+ and info["version"].endswith(".0")
103
+ and info["version"] >= "1.10.0" # versions from 1.10.0 to 1.20.0 do not have a micro .0
104
+ and info["version"] <= "1.19.9"
105
+ ):
106
+ # drop the .0 for newer releases
107
+ info["version"] = info["version"][:-2]
108
+
109
+ # spell-checker: disable
110
+ if "mpy" in info and info["mpy"]: # mpy on some v1.11+ builds
111
+ sys_mpy = int(info["mpy"])
112
+ # .mpy architecture
113
+ arch = [
114
+ None,
115
+ "x86",
116
+ "x64",
117
+ "armv6",
118
+ "armv6m",
119
+ "armv7m",
120
+ "armv7em",
121
+ "armv7emsp",
122
+ "armv7emdp",
123
+ "xtensa",
124
+ "xtensawin",
125
+ ][sys_mpy >> 10]
126
+ if arch:
127
+ info["arch"] = arch
128
+ # .mpy version.minor
129
+ info["mpy"] = "v{}.{}".format(sys_mpy & 0xFF, sys_mpy >> 8 & 3)
130
+ # simple to use version[-build] string avoiding f-strings for backward compat
131
+ info["ver"] = (
132
+ "v{version}-{build}".format(version=info["version"], build=info["build"])
133
+ if info["build"]
134
+ else "v{version}".format(version=info["version"])
135
+ )
136
+
137
+ return info
138
+
139
+
140
+ print(_info())
141
+ del _info, _build, _version_str
@@ -1,140 +1,140 @@
1
- """
2
- Run a command and return the output and return code as a tuple
3
- """
4
-
5
- import subprocess
6
- from dataclasses import dataclass
7
- from threading import Timer
8
- from typing import List, Optional, Tuple
9
-
10
- from loguru import logger as log
11
-
12
- LogTagList = List[str]
13
-
14
-
15
- @dataclass
16
- class LogTags:
17
- reset_tags: LogTagList
18
- error_tags: LogTagList
19
- warning_tags: LogTagList
20
- success_tags: LogTagList
21
- ignore_tags: LogTagList
22
-
23
-
24
- DEFAULT_RESET_TAGS = [
25
- # ESP32 reset causes
26
- "rst cause:1, boot mode:", # 1 -> hardware watch dog reset
27
- "rst cause:2, boot mode:", # 2 -> software watch dog reset (From an exception)
28
- "rst cause:3, boot mode:", # 3 -> software watch dog reset system_restart (Possibly unfed watchdog got angry)
29
- "rst cause:4, boot mode:", # 4 -> soft restart (Possibly with a restart command)
30
- "boot.esp32: PRO CPU has been reset by WDT.",
31
- "rst:0x10 (RTCWDT_RTC_RESET)",
32
- ]
33
-
34
-
35
- def run(
36
- cmd: List[str],
37
- timeout: int = 60,
38
- log_errors: bool = True,
39
- no_info: bool = False,
40
- *,
41
- log_warnings: bool = False,
42
- reset_tags: Optional[LogTagList] = None,
43
- error_tags: Optional[LogTagList] = None,
44
- warning_tags: Optional[LogTagList] = None,
45
- success_tags: Optional[LogTagList] = None,
46
- ignore_tags: Optional[LogTagList] = None,
47
- ) -> Tuple[int, List[str]]:
48
- # sourcery skip: no-long-functions
49
- """
50
- Run a command and return the output and return code as a tuple
51
- Parameters
52
- ----------
53
- cmd : List[str]
54
- The command to run
55
- timeout : int, optional
56
- The timeout in seconds, by default 60
57
- log_errors : bool, optional
58
- If False, don't log errors, Default: true
59
- no_info : bool, optional
60
- If True, don't log info, by default False
61
- error_tags : Optional[LogTagList], optional
62
- A list of strings to look for in the output to log as errors, by default None
63
- warning_tags : Optional[LogTagList], optional
64
- A list of strings to look for in the output to log as warnings, by default None
65
- Returns
66
- -------
67
- Tuple[int, List[str]]
68
- The return code and the output as a list of strings
69
- """
70
- if not reset_tags:
71
- reset_tags = DEFAULT_RESET_TAGS
72
- if not error_tags:
73
- error_tags = ["Traceback ", "Error: ", "Exception: ", "ERROR :", "CRIT :"]
74
- if not warning_tags:
75
- warning_tags = ["WARN :", "TRACE :"]
76
- if not success_tags:
77
- success_tags = []
78
- if not ignore_tags:
79
- ignore_tags = [' File "<stdin>",']
80
-
81
- replace_tags = ["\x1b[1A"]
82
-
83
- output = []
84
- try:
85
- proc = subprocess.Popen(
86
- cmd,
87
- stdout=subprocess.PIPE,
88
- stderr=subprocess.PIPE,
89
- universal_newlines=True,
90
- encoding="utf-8",
91
- )
92
- except FileNotFoundError as e:
93
- raise FileNotFoundError(f"Failed to start {cmd[0]}") from e
94
-
95
- def timed_out():
96
- proc.kill()
97
- if log_warnings:
98
- log.warning(f"Command {cmd} timed out after {timeout} seconds")
99
-
100
- timer = Timer(timeout, timed_out)
101
- try:
102
- timer.start()
103
- # stdout has most of the output, assign log categories based on text tags
104
- if proc.stdout:
105
- for line in proc.stdout:
106
- if not line or not line.strip():
107
- continue
108
- for tag in replace_tags:
109
- line = line.replace(tag, "")
110
- output.append(line) # full output, no trimming
111
- if any(tag in line for tag in reset_tags):
112
- raise RuntimeError("Board reset detected")
113
-
114
- line = line.rstrip("\n")
115
- # if any of the error tags in the line
116
- if any(tag in line for tag in error_tags):
117
- if not log_errors:
118
- continue
119
- log.error(line)
120
- elif any(tag in line for tag in warning_tags):
121
- log.warning(line)
122
- elif any(tag in line for tag in success_tags):
123
- log.success(line)
124
- elif any(tag in line for tag in ignore_tags):
125
- continue
126
- else:
127
- if not no_info:
128
- if line.startswith(("INFO : ", "WARN : ", "ERROR : ")):
129
- line = line[8:].lstrip()
130
- log.info(line)
131
- if proc.stderr and log_errors:
132
- for line in proc.stderr:
133
- log.warning(line)
134
- except UnicodeDecodeError as e:
135
- log.error(f"Failed to decode output: {e}")
136
- finally:
137
- timer.cancel()
138
-
139
- proc.wait(timeout=1)
140
- return proc.returncode or 0, output
1
+ """
2
+ Run a command and return the output and return code as a tuple
3
+ """
4
+
5
+ import subprocess
6
+ from dataclasses import dataclass
7
+ from threading import Timer
8
+ from typing import List, Optional, Tuple
9
+
10
+ from loguru import logger as log
11
+
12
+ LogTagList = List[str]
13
+
14
+
15
+ @dataclass
16
+ class LogTags:
17
+ reset_tags: LogTagList
18
+ error_tags: LogTagList
19
+ warning_tags: LogTagList
20
+ success_tags: LogTagList
21
+ ignore_tags: LogTagList
22
+
23
+
24
+ DEFAULT_RESET_TAGS = [
25
+ # ESP32 reset causes
26
+ "rst cause:1, boot mode:", # 1 -> hardware watch dog reset
27
+ "rst cause:2, boot mode:", # 2 -> software watch dog reset (From an exception)
28
+ "rst cause:3, boot mode:", # 3 -> software watch dog reset system_restart (Possibly unfed watchdog got angry)
29
+ "rst cause:4, boot mode:", # 4 -> soft restart (Possibly with a restart command)
30
+ "boot.esp32: PRO CPU has been reset by WDT.",
31
+ "rst:0x10 (RTCWDT_RTC_RESET)",
32
+ ]
33
+
34
+
35
+ def run(
36
+ cmd: List[str],
37
+ timeout: int = 60,
38
+ log_errors: bool = True,
39
+ no_info: bool = False,
40
+ *,
41
+ log_warnings: bool = False,
42
+ reset_tags: Optional[LogTagList] = None,
43
+ error_tags: Optional[LogTagList] = None,
44
+ warning_tags: Optional[LogTagList] = None,
45
+ success_tags: Optional[LogTagList] = None,
46
+ ignore_tags: Optional[LogTagList] = None,
47
+ ) -> Tuple[int, List[str]]:
48
+ # sourcery skip: no-long-functions
49
+ """
50
+ Run a command and return the output and return code as a tuple
51
+ Parameters
52
+ ----------
53
+ cmd : List[str]
54
+ The command to run
55
+ timeout : int, optional
56
+ The timeout in seconds, by default 60
57
+ log_errors : bool, optional
58
+ If False, don't log errors, Default: true
59
+ no_info : bool, optional
60
+ If True, don't log info, by default False
61
+ error_tags : Optional[LogTagList], optional
62
+ A list of strings to look for in the output to log as errors, by default None
63
+ warning_tags : Optional[LogTagList], optional
64
+ A list of strings to look for in the output to log as warnings, by default None
65
+ Returns
66
+ -------
67
+ Tuple[int, List[str]]
68
+ The return code and the output as a list of strings
69
+ """
70
+ if not reset_tags:
71
+ reset_tags = DEFAULT_RESET_TAGS
72
+ if not error_tags:
73
+ error_tags = ["Traceback ", "Error: ", "Exception: ", "ERROR :", "CRIT :"]
74
+ if not warning_tags:
75
+ warning_tags = ["WARN :", "TRACE :"]
76
+ if not success_tags:
77
+ success_tags = []
78
+ if not ignore_tags:
79
+ ignore_tags = [' File "<stdin>",']
80
+
81
+ replace_tags = ["\x1b[1A"]
82
+
83
+ output = []
84
+ try:
85
+ proc = subprocess.Popen(
86
+ cmd,
87
+ stdout=subprocess.PIPE,
88
+ stderr=subprocess.PIPE,
89
+ universal_newlines=True,
90
+ encoding="utf-8",
91
+ )
92
+ except FileNotFoundError as e:
93
+ raise FileNotFoundError(f"Failed to start {cmd[0]}") from e
94
+
95
+ def timed_out():
96
+ proc.kill()
97
+ if log_warnings:
98
+ log.warning(f"Command {cmd} timed out after {timeout} seconds")
99
+
100
+ timer = Timer(timeout, timed_out)
101
+ try:
102
+ timer.start()
103
+ # stdout has most of the output, assign log categories based on text tags
104
+ if proc.stdout:
105
+ for line in proc.stdout:
106
+ if not line or not line.strip():
107
+ continue
108
+ for tag in replace_tags:
109
+ line = line.replace(tag, "")
110
+ output.append(line) # full output, no trimming
111
+ if any(tag in line for tag in reset_tags):
112
+ raise RuntimeError("Board reset detected")
113
+
114
+ line = line.rstrip("\n")
115
+ # if any of the error tags in the line
116
+ if any(tag in line for tag in error_tags):
117
+ if not log_errors:
118
+ continue
119
+ log.error(line)
120
+ elif any(tag in line for tag in warning_tags):
121
+ log.warning(line)
122
+ elif any(tag in line for tag in success_tags):
123
+ log.success(line)
124
+ elif any(tag in line for tag in ignore_tags):
125
+ continue
126
+ else:
127
+ if not no_info:
128
+ if line.startswith(("INFO : ", "WARN : ", "ERROR : ")):
129
+ line = line[8:].lstrip()
130
+ log.info(line)
131
+ if proc.stderr and log_errors:
132
+ for line in proc.stderr:
133
+ log.warning(line)
134
+ except UnicodeDecodeError as e:
135
+ log.error(f"Failed to decode output: {e}")
136
+ finally:
137
+ timer.cancel()
138
+
139
+ proc.wait(timeout=1)
140
+ return proc.returncode or 0, output