mpytool 2.3.1__tar.gz → 2.3.3__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 (36) hide show
  1. {mpytool-2.3.1 → mpytool-2.3.3}/PKG-INFO +38 -5
  2. {mpytool-2.3.1 → mpytool-2.3.3}/README.md +37 -4
  3. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool/cmd_cp.py +6 -6
  4. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool/conn.py +3 -1
  5. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool/conn_serial.py +56 -30
  6. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool/conn_socket.py +2 -4
  7. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool/logger.py +27 -6
  8. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool/mount.py +75 -113
  9. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool/mpy.py +21 -39
  10. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool/mpy_comm.py +20 -33
  11. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool/mpy_cross.py +18 -11
  12. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool/mpytool.py +66 -49
  13. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool/terminal.py +2 -2
  14. mpytool-2.3.3/mpytool/utils.py +154 -0
  15. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool.egg-info/PKG-INFO +38 -5
  16. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool.egg-info/SOURCES.txt +1 -0
  17. {mpytool-2.3.1 → mpytool-2.3.3}/pyproject.toml +1 -1
  18. mpytool-2.3.3/tests/test_cli.py +639 -0
  19. {mpytool-2.3.1 → mpytool-2.3.3}/tests/test_cmd_cp.py +1 -1
  20. {mpytool-2.3.1 → mpytool-2.3.3}/tests/test_integration.py +116 -79
  21. {mpytool-2.3.1 → mpytool-2.3.3}/tests/test_mount.py +11 -3
  22. {mpytool-2.3.1 → mpytool-2.3.3}/tests/test_mpy.py +28 -70
  23. {mpytool-2.3.1 → mpytool-2.3.3}/tests/test_utils.py +81 -36
  24. mpytool-2.3.1/mpytool/utils.py +0 -100
  25. {mpytool-2.3.1 → mpytool-2.3.3}/LICENSE +0 -0
  26. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool/__init__.py +0 -0
  27. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool/speedtest.py +0 -0
  28. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool/terminal_unix.py +0 -0
  29. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool/terminal_win.py +0 -0
  30. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool.egg-info/dependency_links.txt +0 -0
  31. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool.egg-info/entry_points.txt +0 -0
  32. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool.egg-info/requires.txt +0 -0
  33. {mpytool-2.3.1 → mpytool-2.3.3}/mpytool.egg-info/top_level.txt +0 -0
  34. {mpytool-2.3.1 → mpytool-2.3.3}/setup.cfg +0 -0
  35. {mpytool-2.3.1 → mpytool-2.3.3}/tests/test_errors.py +0 -0
  36. {mpytool-2.3.1 → mpytool-2.3.3}/tests/test_mpytool.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mpytool
3
- Version: 2.3.1
3
+ Version: 2.3.3
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
@@ -608,11 +608,12 @@ Scanning...
608
608
 
609
609
  ## Debug output
610
610
 
611
- - `-d` print warnings (yellow)
612
- - `-dd` print info messages (purple)
613
- - `-ddd` print debug messages (blue)
611
+ By default, errors and warnings are shown.
614
612
 
615
- For reporting bugs, please include `-ddd` output in the issue.
613
+ - `-d` also print info messages (purple)
614
+ - `-dd` also print debug messages (blue)
615
+
616
+ For reporting bugs, please include `-dd` output in the issue.
616
617
 
617
618
  ## Performance
618
619
 
@@ -692,6 +693,38 @@ exec bash
692
693
  - Support for `--` command separator
693
694
  - Works with both relative and absolute paths
694
695
 
696
+ ## Development
697
+
698
+ ### Running tests
699
+
700
+ ```bash
701
+ # Unit tests (no device needed)
702
+ python -m unittest discover tests/ -v
703
+
704
+ # Read-only integration tests (safe on production device)
705
+ MPYTOOL_TEST_PORT=/dev/ttyACM0 python -m unittest tests.test_integration -v
706
+
707
+ # All integration tests (dedicated test device)
708
+ MPYTOOL_TEST_PORT_RW=/dev/ttyACM0 python -m unittest tests.test_integration -v
709
+
710
+ # CLI tests (no device needed for basic tests)
711
+ python -m unittest tests.test_cli -v
712
+
713
+ # All CLI tests with device
714
+ MPYTOOL_TEST_PORT_RW=/dev/ttyACM0 python -m unittest tests.test_cli -v
715
+ ```
716
+
717
+ Test categories:
718
+ - **Unit tests** (`test_*.py` except `test_integration.py`, `test_cli.py`) - no device needed
719
+ - **Read-only tests** (`MPYTOOL_TEST_PORT`) - safe on any device, no filesystem changes
720
+ - **All tests** (`MPYTOOL_TEST_PORT_RW`) - includes write and flash operations, use dedicated test device
721
+
722
+ ### Install from source
723
+
724
+ ```bash
725
+ pip install -e .
726
+ ```
727
+
695
728
  ## Requirements
696
729
 
697
730
  Working only with MicroPython boards, not with CircuitPython
@@ -594,11 +594,12 @@ Scanning...
594
594
 
595
595
  ## Debug output
596
596
 
597
- - `-d` print warnings (yellow)
598
- - `-dd` print info messages (purple)
599
- - `-ddd` print debug messages (blue)
597
+ By default, errors and warnings are shown.
600
598
 
601
- For reporting bugs, please include `-ddd` output in the issue.
599
+ - `-d` also print info messages (purple)
600
+ - `-dd` also print debug messages (blue)
601
+
602
+ For reporting bugs, please include `-dd` output in the issue.
602
603
 
603
604
  ## Performance
604
605
 
@@ -678,6 +679,38 @@ exec bash
678
679
  - Support for `--` command separator
679
680
  - Works with both relative and absolute paths
680
681
 
682
+ ## Development
683
+
684
+ ### Running tests
685
+
686
+ ```bash
687
+ # Unit tests (no device needed)
688
+ python -m unittest discover tests/ -v
689
+
690
+ # Read-only integration tests (safe on production device)
691
+ MPYTOOL_TEST_PORT=/dev/ttyACM0 python -m unittest tests.test_integration -v
692
+
693
+ # All integration tests (dedicated test device)
694
+ MPYTOOL_TEST_PORT_RW=/dev/ttyACM0 python -m unittest tests.test_integration -v
695
+
696
+ # CLI tests (no device needed for basic tests)
697
+ python -m unittest tests.test_cli -v
698
+
699
+ # All CLI tests with device
700
+ MPYTOOL_TEST_PORT_RW=/dev/ttyACM0 python -m unittest tests.test_cli -v
701
+ ```
702
+
703
+ Test categories:
704
+ - **Unit tests** (`test_*.py` except `test_integration.py`, `test_cli.py`) - no device needed
705
+ - **Read-only tests** (`MPYTOOL_TEST_PORT`) - safe on any device, no filesystem changes
706
+ - **All tests** (`MPYTOOL_TEST_PORT_RW`) - includes write and flash operations, use dedicated test device
707
+
708
+ ### Install from source
709
+
710
+ ```bash
711
+ pip install -e .
712
+ ```
713
+
681
714
  ## Requirements
682
715
 
683
716
  Working only with MicroPython boards, not with CircuitPython
@@ -79,8 +79,8 @@ class CopyCommand:
79
79
  # Cache
80
80
  self._remote_file_cache = {}
81
81
 
82
- # Debug mode
83
- self._is_debug = getattr(self._log, 'loglevel', 1) >= 4
82
+ # Info mode disables progress overwrite (each update on new line)
83
+ self._is_info = self._log.is_info
84
84
 
85
85
  @property
86
86
  def _verbose(self):
@@ -196,7 +196,7 @@ class CopyCommand:
196
196
  line = self._format_progress_line(percent, total)
197
197
  else:
198
198
  line = self._format_compact_progress(f"{percent:3d}%", total)
199
- if self._is_debug:
199
+ if self._is_info:
200
200
  self.verbose(line, color='cyan')
201
201
  else:
202
202
  self.verbose(line, color='cyan', end='', overwrite=True)
@@ -207,7 +207,7 @@ class CopyCommand:
207
207
  line = self._format_progress_line(100, total, encodings)
208
208
  else:
209
209
  line = self._format_compact_complete(total)
210
- self.verbose(line, color='cyan', overwrite=not self._is_debug)
210
+ self.verbose(line, color='cyan', overwrite=not self._is_info)
211
211
 
212
212
  def _set_progress_info(self, src, dst, is_src_remote, is_dst_remote):
213
213
  """Set progress source and destination paths"""
@@ -673,7 +673,7 @@ class CopyCommand:
673
673
  self._force = force
674
674
  self._compress = compress
675
675
  if mpy:
676
- self._mpy_cross = MpyCross(self._log, self.verbose)
676
+ self._mpy_cross = MpyCross(self._log)
677
677
 
678
678
  try:
679
679
  self._run_impl(paths)
@@ -831,7 +831,7 @@ class CopyCommand:
831
831
 
832
832
  def print_transfer_info(self):
833
833
  """Print transfer settings (chunk size and compression)"""
834
- chunk = self.mpy.detect_chunk_size()
834
+ chunk = self.mpy.chunk_size
835
835
  chunk_str = f"{chunk // 1024}K" if chunk >= 1024 else str(chunk)
836
836
  if self._compress is None:
837
837
  compress = self.mpy.detect_deflate()
@@ -3,6 +3,8 @@
3
3
  import time as _time
4
4
  import select as _select
5
5
 
6
+ from mpytool.logger import SimpleColorLogger as _SimpleColorLogger
7
+
6
8
  # VFS escape byte - signals start of VFS command from device
7
9
  ESCAPE = 0x18
8
10
 
@@ -17,7 +19,7 @@ class Timeout(ConnError):
17
19
 
18
20
  class Conn():
19
21
  def __init__(self, log=None):
20
- self._log = log
22
+ self._log = log if log is not None else _SimpleColorLogger()
21
23
  self._buffer = bytearray()
22
24
  self._escape_handlers = {} # {escape_byte: handler}
23
25
  self._active_handler = None # Currently processing handler
@@ -3,6 +3,7 @@
3
3
  import time as _time
4
4
  import serial as _serial
5
5
  import mpytool.conn as _conn
6
+ import mpytool.utils as _utils
6
7
 
7
8
 
8
9
  class ConnSerial(_conn.Conn):
@@ -11,6 +12,7 @@ class ConnSerial(_conn.Conn):
11
12
  def __init__(self, log=None, **serial_config):
12
13
  super().__init__(log)
13
14
  self._serial_config = serial_config
15
+ self._port_info = _utils.get_port_info(serial_config.get('port', ''))
14
16
  try:
15
17
  self._serial = _serial.Serial(**serial_config)
16
18
  except _serial.serialutil.SerialException as err:
@@ -20,9 +22,27 @@ class ConnSerial(_conn.Conn):
20
22
  # Windows: pyserial has no fd, select() works only with sockets
21
23
  if not hasattr(self._serial, 'fd'):
22
24
  self._has_data = self._has_data_polling
25
+ # Debug info about port type
26
+ port = serial_config.get('port', '')
27
+ if self._port_info and self._port_info.vid:
28
+ vid = self._port_info.vid
29
+ pid = self._port_info.pid
30
+ self._log.info(
31
+ "Connected to %s [%04X:%04X %s]",
32
+ port, vid, pid, self.port_type)
33
+ else:
34
+ self._log.info("Connected to %s [unknown serial port]", port)
23
35
 
24
36
  def close(self):
25
37
  if self._serial:
38
+ # Prevent ESP32 reset when driver clears DTR/RTS on close
39
+ # Only for USB-UART bridges (CP2102, CH340, etc.), not USB-CDC
40
+ if self._is_usb_uart():
41
+ try:
42
+ self._serial.rts = False
43
+ self._serial.dtr = False
44
+ except OSError:
45
+ pass
26
46
  self._serial.close()
27
47
  self._serial = None
28
48
 
@@ -56,7 +76,7 @@ class ConnSerial(_conn.Conn):
56
76
  in_waiting = self._serial.in_waiting
57
77
  if in_waiting > 0:
58
78
  data = self._serial.read(in_waiting)
59
- if data and self._log:
79
+ if data:
60
80
  self._log.debug("RX: %r", data)
61
81
  return data
62
82
  return None
@@ -67,20 +87,43 @@ class ConnSerial(_conn.Conn):
67
87
  """Write data to serial port"""
68
88
  if self._serial is None:
69
89
  raise _conn.ConnError("Not connected")
70
- if self._log:
71
- self._log.debug("TX: %r", data)
90
+ self._log.debug("TX: %r", data)
72
91
  try:
73
92
  return self._serial.write(data)
74
93
  except OSError as err:
75
94
  raise _conn.ConnError(f"Connection lost: {err}") from err
76
95
 
77
- def _is_usb_cdc(self):
78
- """Check if this is a USB-CDC port (native USB on ESP32-S/C)"""
79
- port = self._serial.port or ''
80
- return 'ACM' in port or 'usbmodem' in port
96
+ @property
97
+ def vid(self):
98
+ """USB Vendor ID or None if not available"""
99
+ return self._port_info.vid if self._port_info else None
100
+
101
+ @property
102
+ def pid(self):
103
+ """USB Product ID or None if not available"""
104
+ return self._port_info.pid if self._port_info else None
105
+
106
+ @property
107
+ def port_type(self):
108
+ """Return port type string for debug info"""
109
+ if self._is_usb_uart():
110
+ return "USB-UART"
111
+ if self.vid in _utils._MICROPYTHON_VIDS:
112
+ return "USB-CDC"
113
+ if self.vid:
114
+ return "USB (unknown)"
115
+ return "unknown"
116
+
117
+ def _is_usb_uart(self):
118
+ """Check if this is a USB-UART bridge (CP210x, CH340, FTDI, etc.)"""
119
+ return self.vid in _utils._USB_UART_VIDS
81
120
 
82
121
  def hard_reset(self):
83
- """Hardware reset using DTR/RTS signals"""
122
+ """Hardware reset using DTR/RTS signals (USB-UART only)"""
123
+ if not self._is_usb_uart():
124
+ raise _conn.ConnError(
125
+ f"Hardware reset not supported on {self.port_type}")
126
+ self._log.info("hard_reset: using DTR/RTS")
84
127
  self._serial.setDTR(False) # GPIO0 high (normal boot)
85
128
  self._serial.setRTS(True) # Assert reset
86
129
  _time.sleep(0.1)
@@ -117,28 +160,11 @@ class ConnSerial(_conn.Conn):
117
160
  raise _conn.ConnError(f"Could not reconnect to {port} within {timeout}s")
118
161
 
119
162
  def reset_to_bootloader(self):
120
- """Reset into bootloader mode (auto-detects USB-CDC vs USB-UART)"""
121
- if self._is_usb_cdc():
122
- self._reset_to_bootloader_usb_jtag()
123
- else:
124
- self._reset_to_bootloader_classic()
125
-
126
- def _reset_to_bootloader_usb_jtag(self):
127
- """Bootloader reset for USB-JTAG-Serial (esptool sequence)"""
128
- self._serial.setRTS(False)
129
- self._serial.setDTR(False)
130
- _time.sleep(0.1)
131
- self._serial.setDTR(True)
132
- self._serial.setRTS(True)
133
- _time.sleep(0.1)
134
- self._serial.setDTR(False)
135
- self._serial.setRTS(True)
136
- _time.sleep(0.1)
137
- self._serial.setDTR(False)
138
- self._serial.setRTS(False)
139
-
140
- def _reset_to_bootloader_classic(self):
141
- """Bootloader reset for USB-UART (classic DTR/RTS circuit)"""
163
+ """Reset into bootloader mode (USB-UART only)"""
164
+ if not self._is_usb_uart():
165
+ raise _conn.ConnError(
166
+ f"Bootloader reset not supported on {self.port_type}")
167
+ self._log.info("reset_to_bootloader: using DTR/RTS")
142
168
  self._serial.setDTR(False) # GPIO0 high
143
169
  self._serial.setRTS(True) # Assert reset
144
170
  _time.sleep(0.1)
@@ -49,8 +49,7 @@ class ConnSocket(_conn.Conn):
49
49
  try:
50
50
  data = self._socket.recv(4096)
51
51
  if data:
52
- if self._log:
53
- self._log.debug("RX: %r", data)
52
+ self._log.debug("RX: %r", data)
54
53
  return data
55
54
  except BlockingIOError:
56
55
  pass
@@ -62,8 +61,7 @@ class ConnSocket(_conn.Conn):
62
61
  """Write data to socket"""
63
62
  if self._socket is None:
64
63
  raise _conn.ConnError("Not connected")
65
- if self._log:
66
- self._log.debug("TX: %r", data)
64
+ self._log.debug("TX: %r", data)
67
65
  try:
68
66
  return self._socket.send(data)
69
67
  except OSError as err:
@@ -4,6 +4,12 @@ import os as _os
4
4
  import sys as _sys
5
5
 
6
6
 
7
+ # Log levels (compatible with Python logging)
8
+ DEBUG = 10
9
+ INFO = 20
10
+ WARNING = 30
11
+ ERROR = 40
12
+
7
13
  class SimpleColorLogger():
8
14
  # ANSI color codes
9
15
  _RESET = '\033[0m'
@@ -25,7 +31,7 @@ class SimpleColorLogger():
25
31
  'cyan': _BOLD_CYAN,
26
32
  }
27
33
 
28
- def __init__(self, loglevel=1, verbose_level=0):
34
+ def __init__(self, loglevel=WARNING, verbose_level=0):
29
35
  self._loglevel = loglevel
30
36
  self._verbose_level = verbose_level
31
37
  self._is_tty = _sys.stderr.isatty()
@@ -40,9 +46,24 @@ class SimpleColorLogger():
40
46
 
41
47
  @property
42
48
  def loglevel(self):
43
- """Return current log level (1=error, 2=warning, 3=info, 4=debug)"""
49
+ """Return current log level (DEBUG=10, INFO=20, WARNING=30, ERROR=40)"""
44
50
  return self._loglevel
45
51
 
52
+ @property
53
+ def is_debug(self):
54
+ """True if debug messages are enabled"""
55
+ return self._loglevel <= DEBUG
56
+
57
+ @property
58
+ def is_info(self):
59
+ """True if info messages are enabled"""
60
+ return self._loglevel <= INFO
61
+
62
+ @property
63
+ def is_warning(self):
64
+ """True if warning messages are enabled"""
65
+ return self._loglevel <= WARNING
66
+
46
67
  def colorize(self, text, color):
47
68
  """Return text wrapped in color codes if colors are enabled"""
48
69
  if not self._color:
@@ -67,7 +88,7 @@ class SimpleColorLogger():
67
88
  def error(self, msg, *args):
68
89
  if args:
69
90
  msg = msg % args
70
- if self._loglevel >= 1:
91
+ if self._loglevel <= ERROR:
71
92
  if self._color:
72
93
  self.log(f"{self._BOLD_RED}{msg}{self._RESET}")
73
94
  else:
@@ -76,7 +97,7 @@ class SimpleColorLogger():
76
97
  def warning(self, msg, *args):
77
98
  if args:
78
99
  msg = msg % args
79
- if self._loglevel >= 2:
100
+ if self._loglevel <= WARNING:
80
101
  if self._color:
81
102
  self.log(f"{self._BOLD_YELLOW}{msg}{self._RESET}")
82
103
  else:
@@ -85,7 +106,7 @@ class SimpleColorLogger():
85
106
  def info(self, msg, *args):
86
107
  if args:
87
108
  msg = msg % args
88
- if self._loglevel >= 3:
109
+ if self._loglevel <= INFO:
89
110
  if self._color:
90
111
  self.log(f"{self._BOLD_MAGENTA}{msg}{self._RESET}")
91
112
  else:
@@ -94,7 +115,7 @@ class SimpleColorLogger():
94
115
  def debug(self, msg, *args):
95
116
  if args:
96
117
  msg = msg % args
97
- if self._loglevel >= 4:
118
+ if self._loglevel <= DEBUG:
98
119
  if self._color:
99
120
  self.log(f"{self._BOLD_BLUE}{msg}{self._RESET}")
100
121
  else: