mpytool 2.2.1__tar.gz → 2.2.2__tar.gz
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.
- {mpytool-2.2.1 → mpytool-2.2.2}/PKG-INFO +1 -1
- {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/logger.py +11 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/mpy_comm.py +29 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/mpytool.py +50 -20
- {mpytool-2.2.1 → mpytool-2.2.2}/mpytool.egg-info/PKG-INFO +1 -1
- {mpytool-2.2.1 → mpytool-2.2.2}/pyproject.toml +1 -1
- {mpytool-2.2.1 → mpytool-2.2.2}/tests/test_errors.py +31 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/LICENSE +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/README.md +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/__init__.py +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/conn.py +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/conn_serial.py +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/conn_socket.py +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/mpy.py +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/terminal.py +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/terminal_unix.py +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/terminal_win.py +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/utils.py +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/mpytool.egg-info/SOURCES.txt +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/mpytool.egg-info/dependency_links.txt +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/mpytool.egg-info/entry_points.txt +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/mpytool.egg-info/requires.txt +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/mpytool.egg-info/top_level.txt +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/setup.cfg +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/tests/test_integration.py +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/tests/test_mpy.py +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/tests/test_mpytool.py +0 -0
- {mpytool-2.2.1 → mpytool-2.2.2}/tests/test_utils.py +0 -0
|
@@ -36,8 +36,16 @@ class SimpleColorLogger():
|
|
|
36
36
|
and _os.environ.get('CI') is None
|
|
37
37
|
and (_sys.platform != 'win32' or _os.environ.get('TERM'))
|
|
38
38
|
)
|
|
39
|
+
self._pending_line = False # True when last output had no trailing newline
|
|
40
|
+
|
|
41
|
+
def _clear_pending(self):
|
|
42
|
+
"""End pending progress line before printing a new message"""
|
|
43
|
+
if self._pending_line:
|
|
44
|
+
print(file=_sys.stderr)
|
|
45
|
+
self._pending_line = False
|
|
39
46
|
|
|
40
47
|
def log(self, msg):
|
|
48
|
+
self._clear_pending()
|
|
41
49
|
print(msg, file=_sys.stderr)
|
|
42
50
|
|
|
43
51
|
def error(self, msg, *args):
|
|
@@ -83,7 +91,10 @@ class SimpleColorLogger():
|
|
|
83
91
|
# Skip progress updates (overwrite without newline) in non-TTY mode
|
|
84
92
|
if overwrite and not self._is_tty and end != '\n':
|
|
85
93
|
return
|
|
94
|
+
if not overwrite:
|
|
95
|
+
self._clear_pending()
|
|
86
96
|
color_code = self.COLORS.get(color, self._BOLD_GREEN) if self._color else ''
|
|
87
97
|
reset_code = self._RESET if self._color else ''
|
|
88
98
|
clear = f'\r{self._CLEAR_LINE}' if self._color and overwrite else ('\r' if self._is_tty and overwrite else '')
|
|
89
99
|
print(f"{clear}{color_code}{msg}{reset_code}", end=end, file=_sys.stderr, flush=True)
|
|
100
|
+
self._pending_line = (end != '\n')
|
|
@@ -20,13 +20,42 @@ class MpyError(Exception):
|
|
|
20
20
|
|
|
21
21
|
class CmdError(MpyError):
|
|
22
22
|
"""Command execution error on device"""
|
|
23
|
+
|
|
24
|
+
# Known MicroPython OSError codes
|
|
25
|
+
_OSERROR_MESSAGES = {
|
|
26
|
+
'2': 'No such file or directory',
|
|
27
|
+
'13': 'Permission denied',
|
|
28
|
+
'17': 'File exists',
|
|
29
|
+
'19': 'No such device',
|
|
30
|
+
'21': 'Is a directory',
|
|
31
|
+
'22': 'Invalid argument',
|
|
32
|
+
'28': 'No space left on device',
|
|
33
|
+
'30': 'Read-only filesystem',
|
|
34
|
+
'110': 'Connection timed out',
|
|
35
|
+
'113': 'No route to host',
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
def __init__(self, cmd, result, error):
|
|
24
39
|
self._cmd = cmd
|
|
25
40
|
self._result = result
|
|
26
41
|
self._error = error.decode('utf-8')
|
|
27
42
|
super().__init__(self.__str__())
|
|
28
43
|
|
|
44
|
+
def _friendly_error(self):
|
|
45
|
+
"""Translate known OSError codes to human-readable messages"""
|
|
46
|
+
import re
|
|
47
|
+
match = re.search(r'OSError: (\d+)', self._error)
|
|
48
|
+
if match:
|
|
49
|
+
code = match.group(1)
|
|
50
|
+
msg = self._OSERROR_MESSAGES.get(code)
|
|
51
|
+
if msg:
|
|
52
|
+
return f'OSError: {msg} (errno {code})'
|
|
53
|
+
return None
|
|
54
|
+
|
|
29
55
|
def __str__(self):
|
|
56
|
+
friendly = self._friendly_error()
|
|
57
|
+
if friendly:
|
|
58
|
+
return friendly
|
|
30
59
|
res = f'Command:\n {self._cmd}\n'
|
|
31
60
|
if self._result:
|
|
32
61
|
res += f'Result:\n {self._result}\n'
|
|
@@ -119,7 +119,10 @@ class MpyTool():
|
|
|
119
119
|
chunk_str = f"{chunk // 1024}K" if chunk >= 1024 else str(chunk)
|
|
120
120
|
compress = self._mpy._detect_deflate() if self._compress is None else self._compress
|
|
121
121
|
compress_str = "on" if compress else "off"
|
|
122
|
-
self.
|
|
122
|
+
if self._verbose >= 2:
|
|
123
|
+
self.verbose(f"COPY (chunk: {chunk_str}, compress: {compress_str})")
|
|
124
|
+
else:
|
|
125
|
+
self.verbose("COPY")
|
|
123
126
|
|
|
124
127
|
@staticmethod
|
|
125
128
|
def _format_local_path(path):
|
|
@@ -157,7 +160,7 @@ class MpyTool():
|
|
|
157
160
|
return info
|
|
158
161
|
|
|
159
162
|
def _format_line(self, status, total, encodings=None):
|
|
160
|
-
"""Format
|
|
163
|
+
"""Format verbose line: [2/5] 100% 24.1K source -> dest (base64)"""
|
|
161
164
|
size_str = self.format_size(total)
|
|
162
165
|
multi = self._progress_total_files > 1
|
|
163
166
|
if multi:
|
|
@@ -170,6 +173,22 @@ class MpyTool():
|
|
|
170
173
|
enc = self._format_encoding_info(encodings, pad=multi) if encodings else (" " * self._ENC_WIDTH if multi else "")
|
|
171
174
|
return f"{prefix:>7} {status} {size_str:>5} {self._progress_src:<{src_w}} -> {self._progress_dst:<{dst_w}}{enc}"
|
|
172
175
|
|
|
176
|
+
def _format_compact_progress(self, status, total):
|
|
177
|
+
"""Format compact progress line: [2/5] 45% 24.1K source"""
|
|
178
|
+
size_str = self.format_size(total)
|
|
179
|
+
multi = self._progress_total_files > 1
|
|
180
|
+
if multi:
|
|
181
|
+
width = len(str(self._progress_total_files))
|
|
182
|
+
prefix = f"[{self._progress_current_file:>{width}}/{self._progress_total_files}]"
|
|
183
|
+
else:
|
|
184
|
+
prefix = ""
|
|
185
|
+
return f"{prefix:>7} {status} {size_str:>5} {self._progress_src}"
|
|
186
|
+
|
|
187
|
+
def _format_compact_complete(self, total):
|
|
188
|
+
"""Format compact complete line: 24.1K source -> dest"""
|
|
189
|
+
size_str = self.format_size(total)
|
|
190
|
+
return f" {size_str:>5} {self._progress_src} -> {self._progress_dst}"
|
|
191
|
+
|
|
173
192
|
def _format_progress_line(self, percent, total, encodings=None):
|
|
174
193
|
return self._format_line(f"{percent:3d}%", total, encodings)
|
|
175
194
|
|
|
@@ -179,19 +198,22 @@ class MpyTool():
|
|
|
179
198
|
def _progress_callback(self, transferred, total):
|
|
180
199
|
"""Callback for file transfer progress"""
|
|
181
200
|
percent = (transferred * 100 // total) if total > 0 else 100
|
|
182
|
-
|
|
201
|
+
if self._verbose >= 2:
|
|
202
|
+
line = self._format_progress_line(percent, total)
|
|
203
|
+
else:
|
|
204
|
+
line = self._format_compact_progress(f"{percent:3d}%", total)
|
|
183
205
|
if self._is_debug:
|
|
184
|
-
# Debug mode: always newlines
|
|
185
206
|
self.verbose(line, color='cyan')
|
|
186
207
|
else:
|
|
187
|
-
# Normal mode: overwrite line
|
|
188
208
|
self.verbose(line, color='cyan', end='', overwrite=True)
|
|
189
209
|
|
|
190
210
|
def _progress_complete(self, total, encodings=None):
|
|
191
211
|
"""Mark current file as complete"""
|
|
192
|
-
|
|
212
|
+
if self._verbose >= 2:
|
|
213
|
+
line = self._format_progress_line(100, total, encodings)
|
|
214
|
+
else:
|
|
215
|
+
line = self._format_compact_complete(total)
|
|
193
216
|
if self._is_debug:
|
|
194
|
-
# Already printed with newline in callback
|
|
195
217
|
pass
|
|
196
218
|
else:
|
|
197
219
|
self.verbose(line, color='cyan', overwrite=True)
|
|
@@ -267,8 +289,8 @@ class MpyTool():
|
|
|
267
289
|
if basename:
|
|
268
290
|
dst_path = _join_remote_path(dst_path, basename)
|
|
269
291
|
for root, dirs, filenames in _os.walk(src_path, topdown=True):
|
|
270
|
-
dirs[:] =
|
|
271
|
-
filenames =
|
|
292
|
+
dirs[:] = sorted(d for d in dirs if not self._is_excluded(d))
|
|
293
|
+
filenames = sorted(f for f in filenames if not self._is_excluded(f))
|
|
272
294
|
rel_path = _os.path.relpath(root, src_path).replace(_os.sep, '/')
|
|
273
295
|
if rel_path == '.':
|
|
274
296
|
rel_path = ''
|
|
@@ -467,10 +489,10 @@ class MpyTool():
|
|
|
467
489
|
parts.append(f"speedup {speedup:.1f}x")
|
|
468
490
|
summary = " ".join(parts)
|
|
469
491
|
if self._skipped_files > 0:
|
|
470
|
-
file_info = f"{self._stats_transferred_files} transferred, {self._skipped_files}
|
|
492
|
+
file_info = f"{self._stats_transferred_files} transferred, {self._skipped_files} unchanged"
|
|
471
493
|
else:
|
|
472
494
|
file_info = f"{total_files} files"
|
|
473
|
-
self.verbose(f"
|
|
495
|
+
self.verbose(f" {summary} ({file_info})", color='green')
|
|
474
496
|
|
|
475
497
|
@classmethod
|
|
476
498
|
def print_tree(cls, tree, prefix='', print_size=True, first=True, last=True):
|
|
@@ -519,18 +541,26 @@ class MpyTool():
|
|
|
519
541
|
"""Upload file data to device with stats tracking and progress display"""
|
|
520
542
|
file_size = len(data)
|
|
521
543
|
self._stats_total_bytes += file_size
|
|
544
|
+
if show_progress and self._verbose >= 1:
|
|
545
|
+
self._progress_current_file += 1
|
|
546
|
+
self._set_progress_info(src_path, dst_path, False, True)
|
|
547
|
+
# Show CHCK status during checksum verification
|
|
548
|
+
if self._verbose >= 2:
|
|
549
|
+
line = self._format_line("CHCK", file_size)
|
|
550
|
+
else:
|
|
551
|
+
line = self._format_compact_progress("CHCK", file_size)
|
|
552
|
+
self.verbose(line, color='cyan', end='', overwrite=True)
|
|
522
553
|
if not self._file_needs_update(data, dst_path):
|
|
523
554
|
self._skipped_files += 1
|
|
524
|
-
if show_progress and self._verbose >=
|
|
525
|
-
self.
|
|
526
|
-
|
|
527
|
-
|
|
555
|
+
if show_progress and self._verbose >= 2:
|
|
556
|
+
self.verbose(self._format_skip_line(file_size), color='yellow', overwrite=True)
|
|
557
|
+
elif show_progress and self._verbose >= 1:
|
|
558
|
+
# Erase CHCK line (next file's progress will overwrite)
|
|
559
|
+
self.verbose("", end='', overwrite=True)
|
|
528
560
|
return False # skipped
|
|
529
561
|
self._stats_transferred_bytes += file_size
|
|
530
562
|
self._stats_transferred_files += 1
|
|
531
563
|
if show_progress and self._verbose >= 1:
|
|
532
|
-
self._progress_current_file += 1
|
|
533
|
-
self._set_progress_info(src_path, dst_path, False, True)
|
|
534
564
|
encodings, wire = self._mpy.put(data, dst_path, self._progress_callback, self._compress)
|
|
535
565
|
self._stats_wire_bytes += wire
|
|
536
566
|
self._progress_complete(file_size, encodings)
|
|
@@ -557,8 +587,8 @@ class MpyTool():
|
|
|
557
587
|
self.verbose(f"PUT DIR: {src_path} -> {dst_path}", 2)
|
|
558
588
|
created_dirs = set()
|
|
559
589
|
for path, dirs, files in _os.walk(src_path, topdown=True):
|
|
560
|
-
dirs[:] =
|
|
561
|
-
files =
|
|
590
|
+
dirs[:] = sorted(d for d in dirs if not self._is_excluded(d))
|
|
591
|
+
files = sorted(f for f in files if not self._is_excluded(f))
|
|
562
592
|
if not files:
|
|
563
593
|
continue
|
|
564
594
|
rel_path = _os.path.relpath(path, src_path).replace(_os.sep, '/')
|
|
@@ -598,7 +628,7 @@ class MpyTool():
|
|
|
598
628
|
self._progress_current_file += 1
|
|
599
629
|
self._set_progress_info(src_path, dst_path, True, False)
|
|
600
630
|
data = self._mpy.get(src_path, self._progress_callback)
|
|
601
|
-
self._progress_complete(len(data))
|
|
631
|
+
self._progress_complete(len(data), None)
|
|
602
632
|
else:
|
|
603
633
|
data = self._mpy.get(src_path)
|
|
604
634
|
file_size = len(data)
|
|
@@ -37,6 +37,37 @@ class TestCmdError(unittest.TestCase):
|
|
|
37
37
|
def test_is_mpy_error(self):
|
|
38
38
|
self.assertTrue(issubclass(CmdError, MpyError))
|
|
39
39
|
|
|
40
|
+
def test_friendly_oserror_no_space(self):
|
|
41
|
+
err = CmdError("f.write(b'data')", b"", b"Traceback (most recent call last):\n File \"<stdin>\", line 1, in <module>\nOSError: 28")
|
|
42
|
+
msg = str(err)
|
|
43
|
+
self.assertIn("No space left on device", msg)
|
|
44
|
+
self.assertIn("errno 28", msg)
|
|
45
|
+
self.assertNotIn("Traceback", msg)
|
|
46
|
+
|
|
47
|
+
def test_friendly_oserror_enoent(self):
|
|
48
|
+
err = CmdError("open('x')", b"", b"OSError: 2")
|
|
49
|
+
msg = str(err)
|
|
50
|
+
self.assertIn("No such file or directory", msg)
|
|
51
|
+
self.assertIn("errno 2", msg)
|
|
52
|
+
|
|
53
|
+
def test_friendly_oserror_read_only(self):
|
|
54
|
+
err = CmdError("f.write(b'')", b"", b"OSError: 30")
|
|
55
|
+
msg = str(err)
|
|
56
|
+
self.assertIn("Read-only filesystem", msg)
|
|
57
|
+
|
|
58
|
+
def test_unknown_oserror_shows_full(self):
|
|
59
|
+
err = CmdError("cmd", b"", b"OSError: 999")
|
|
60
|
+
msg = str(err)
|
|
61
|
+
self.assertIn("cmd", msg)
|
|
62
|
+
self.assertIn("OSError: 999", msg)
|
|
63
|
+
|
|
64
|
+
def test_non_oserror_shows_full(self):
|
|
65
|
+
err = CmdError("cmd", b"result", b"ValueError: bad")
|
|
66
|
+
msg = str(err)
|
|
67
|
+
self.assertIn("cmd", msg)
|
|
68
|
+
self.assertIn("result", msg)
|
|
69
|
+
self.assertIn("ValueError: bad", msg)
|
|
70
|
+
|
|
40
71
|
|
|
41
72
|
class TestPathErrors(unittest.TestCase):
|
|
42
73
|
def test_path_not_found(self):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|