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.
Files changed (28) hide show
  1. {mpytool-2.2.1 → mpytool-2.2.2}/PKG-INFO +1 -1
  2. {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/logger.py +11 -0
  3. {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/mpy_comm.py +29 -0
  4. {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/mpytool.py +50 -20
  5. {mpytool-2.2.1 → mpytool-2.2.2}/mpytool.egg-info/PKG-INFO +1 -1
  6. {mpytool-2.2.1 → mpytool-2.2.2}/pyproject.toml +1 -1
  7. {mpytool-2.2.1 → mpytool-2.2.2}/tests/test_errors.py +31 -0
  8. {mpytool-2.2.1 → mpytool-2.2.2}/LICENSE +0 -0
  9. {mpytool-2.2.1 → mpytool-2.2.2}/README.md +0 -0
  10. {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/__init__.py +0 -0
  11. {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/conn.py +0 -0
  12. {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/conn_serial.py +0 -0
  13. {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/conn_socket.py +0 -0
  14. {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/mpy.py +0 -0
  15. {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/terminal.py +0 -0
  16. {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/terminal_unix.py +0 -0
  17. {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/terminal_win.py +0 -0
  18. {mpytool-2.2.1 → mpytool-2.2.2}/mpytool/utils.py +0 -0
  19. {mpytool-2.2.1 → mpytool-2.2.2}/mpytool.egg-info/SOURCES.txt +0 -0
  20. {mpytool-2.2.1 → mpytool-2.2.2}/mpytool.egg-info/dependency_links.txt +0 -0
  21. {mpytool-2.2.1 → mpytool-2.2.2}/mpytool.egg-info/entry_points.txt +0 -0
  22. {mpytool-2.2.1 → mpytool-2.2.2}/mpytool.egg-info/requires.txt +0 -0
  23. {mpytool-2.2.1 → mpytool-2.2.2}/mpytool.egg-info/top_level.txt +0 -0
  24. {mpytool-2.2.1 → mpytool-2.2.2}/setup.cfg +0 -0
  25. {mpytool-2.2.1 → mpytool-2.2.2}/tests/test_integration.py +0 -0
  26. {mpytool-2.2.1 → mpytool-2.2.2}/tests/test_mpy.py +0 -0
  27. {mpytool-2.2.1 → mpytool-2.2.2}/tests/test_mpytool.py +0 -0
  28. {mpytool-2.2.1 → mpytool-2.2.2}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mpytool
3
- Version: 2.2.1
3
+ Version: 2.2.2
4
4
  Summary: MPY tool - manage files on devices running MicroPython
5
5
  Author-email: Pavel Revak <pavel.revak@gmail.com>
6
6
  License-Expression: MIT
@@ -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.verbose(f"COPY (chunk: {chunk_str}, compress: {compress_str})", 1)
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 progress/skip line: [2/5] 100% 24.1K source -> dest (base64)"""
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
- line = self._format_progress_line(percent, total)
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
- line = self._format_progress_line(100, total, encodings)
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[:] = [d for d in dirs if not self._is_excluded(d)]
271
- filenames = [f for f in filenames if not self._is_excluded(f)]
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} skipped"
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" {summary} ({file_info})", color='green')
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 >= 1:
525
- self._progress_current_file += 1
526
- self._set_progress_info(src_path, dst_path, False, True)
527
- self.verbose(self._format_skip_line(file_size), color='yellow')
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[:] = [d for d in dirs if not self._is_excluded(d)]
561
- files = [f for f in files if not self._is_excluded(f)]
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mpytool
3
- Version: 2.2.1
3
+ Version: 2.2.2
4
4
  Summary: MPY tool - manage files on devices running MicroPython
5
5
  Author-email: Pavel Revak <pavel.revak@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mpytool"
7
- version = "2.2.1"
7
+ version = "2.2.2"
8
8
  description = "MPY tool - manage files on devices running MicroPython"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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