Skip to content

Commit ef032e4

Browse files
curiouswalaclaude
andcommitted
feat: multi-file get/rm, progress bar for put, env var prefix, binascii fallback
- PR #111: fall back from ubinascii to binascii in the MicroPython-side get command, for forward compatibility with firmware that removes ubinascii - PR #109: add auto_envvar_prefix='AMPY' to the Click group so any future option automatically gets an AMPY_* env var without explicit envvar= params - PR #107: get now accepts multiple remote files via nargs=-1 and saves them to a local directory with --output/-o; rm now accepts multiple files - PR #93: put shows a click progress bar by default; suppress with --no-progress. Progress callback added to files.Files.put() for library users Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8f2a460 commit ef032e4

2 files changed

Lines changed: 88 additions & 32 deletions

File tree

ampy/cli.py

Lines changed: 78 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
# SOFTWARE.
2222
from __future__ import print_function
2323
import os
24+
import pathlib
2425
import platform
2526
import posixpath
2627
import re
@@ -70,7 +71,7 @@ def windows_full_port_name(portname):
7071
return "\\\\.\\{0}".format(portname)
7172

7273

73-
@click.group(cls=AmypGroup)
74+
@click.group(cls=AmypGroup, context_settings={"auto_envvar_prefix": "AMPY"})
7475
@click.option(
7576
"--port",
7677
"-p",
@@ -115,35 +116,54 @@ def cli(port, baud, delay):
115116

116117

117118
@cli.command()
118-
@click.argument("remote_file")
119-
@click.argument("local_file", type=click.File("wb"), required=False)
120-
def get(remote_file, local_file):
119+
@click.argument("remote_files", nargs=-1, required=True)
120+
@click.option(
121+
"--output",
122+
"-o",
123+
type=click.Path(),
124+
default=None,
125+
help="Local file or directory to save to. Defaults to printing to stdout.",
126+
)
127+
def get(remote_files, output):
121128
"""
122129
Retrieve a file from the board.
123130
124131
Get will download a file from the board and print its contents or save it
125132
locally. You must pass at least one argument which is the path to the file
126-
to download from the board. If you don't specify a second argument then
127-
the file contents will be printed to standard output. However if you pass
128-
a file name as the second argument then the contents of the downloaded file
129-
will be saved to that file (overwriting anything inside it!).
133+
to download from the board. If you don't specify --output then the file
134+
contents will be printed to standard output. If you pass --output with a
135+
file path the downloaded contents will be saved there. When downloading
136+
multiple files, --output must point to an existing local directory.
130137
131138
For example to retrieve the boot.py and print it out run:
132139
133140
ampy --port /board/serial/port get boot.py
134141
135142
Or to get main.py and save it as main.py locally run:
136143
137-
ampy --port /board/serial/port get main.py main.py
144+
ampy --port /board/serial/port get main.py --output main.py
145+
146+
Or to download multiple files into a local directory:
147+
148+
ampy --port /board/serial/port get boot.py main.py --output ./local_dir/
138149
"""
139-
# Get the file contents.
140150
board_files = files.Files(_board)
141-
contents = board_files.get(remote_file)
142-
# Print the file out if no local file was provided, otherwise save it.
143-
if local_file is None:
144-
print(contents.decode("utf-8"))
151+
if len(remote_files) > 1:
152+
if output is None or not pathlib.Path(output).is_dir():
153+
raise click.UsageError(
154+
"A local directory (--output) must be provided when downloading multiple files."
155+
)
156+
dest_dir = pathlib.Path(output)
157+
for remote_file in remote_files:
158+
contents = board_files.get(remote_file)
159+
dest = dest_dir / pathlib.PurePosixPath(remote_file).name
160+
dest.write_bytes(contents)
145161
else:
146-
local_file.write(contents)
162+
contents = board_files.get(remote_files[0])
163+
if output is None:
164+
print(contents.decode("utf-8"))
165+
else:
166+
pathlib.Path(output).write_bytes(contents)
147167

148168

149169
@cli.command()
@@ -228,7 +248,13 @@ def ls(directory, long_format, recursive):
228248
@cli.command()
229249
@click.argument("local", type=click.Path(exists=True))
230250
@click.argument("remote", required=False)
231-
def put(local, remote):
251+
@click.option(
252+
"--no-progress",
253+
is_flag=True,
254+
default=False,
255+
help="Disable progress bar during file upload.",
256+
)
257+
def put(local, remote, no_progress):
232258
"""Put a file or folder and its contents on the board.
233259
234260
Put will upload a local file or folder to the board. If the file already
@@ -281,36 +307,58 @@ def put(local, remote):
281307
pass
282308
# Loop through all the files and put them on the board too.
283309
for filename in child_files:
284-
with open(os.path.join(parent, filename), "rb") as infile:
285-
remote_filename = posixpath.join(remote_parent, filename)
286-
board_files.put(remote_filename, infile.read())
287-
310+
local_path = os.path.join(parent, filename)
311+
remote_filename = posixpath.join(remote_parent, filename)
312+
with open(local_path, "rb") as infile:
313+
data = infile.read()
314+
if no_progress:
315+
board_files.put(remote_filename, data)
316+
else:
317+
label = remote_filename
318+
with click.progressbar(
319+
length=len(data), label=label, width=0
320+
) as bar:
321+
def _cb(written, total, bar=bar, prev=[0]):
322+
bar.update(written - prev[0])
323+
prev[0] = written
324+
board_files.put(remote_filename, data, progress_callback=_cb)
288325

289326
else:
290327
# File copy, open the file and copy its contents to the board.
291-
# Put the file on the board.
292328
with open(local, "rb") as infile:
293-
board_files = files.Files(_board)
294-
board_files.put(remote, infile.read())
329+
data = infile.read()
330+
board_files = files.Files(_board)
331+
if no_progress:
332+
board_files.put(remote, data)
333+
else:
334+
with click.progressbar(length=len(data), label=remote, width=0) as bar:
335+
def _cb(written, total, bar=bar, prev=[0]):
336+
bar.update(written - prev[0])
337+
prev[0] = written
338+
board_files.put(remote, data, progress_callback=_cb)
295339

296340

297341
@cli.command()
298-
@click.argument("remote_file")
299-
def rm(remote_file):
342+
@click.argument("remote_files", nargs=-1, required=True)
343+
def rm(remote_files):
300344
"""Remove a file from the board.
301345
302-
Remove the specified file from the board's filesystem. Must specify one
303-
argument which is the path to the file to delete. Note that this can't
304-
delete directories which have files inside them, but can delete empty
346+
Remove the specified file(s) from the board's filesystem. Must specify at
347+
least one argument which is the path to the file to delete. Note that this
348+
can't delete directories which have files inside them, but can delete empty
305349
directories.
306350
307351
For example to delete main.py from the root of a board run:
308352
309353
ampy --port /board/serial/port rm main.py
354+
355+
Or to delete multiple files at once:
356+
357+
ampy --port /board/serial/port rm main.py boot.py
310358
"""
311-
# Delete the provided file/directory on the board.
312359
board_files = files.Files(_board)
313-
board_files.rm(remote_file)
360+
for remote_file in remote_files:
361+
board_files.rm(remote_file)
314362

315363

316364
@cli.command()

ampy/files.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ def get(self, filename):
6060
# expects string data.
6161
command = """
6262
import sys
63-
import ubinascii
63+
try:
64+
import ubinascii
65+
except ImportError:
66+
import binascii as ubinascii
6467
with open('{0}', 'rb') as infile:
6568
while True:
6669
result = infile.read({1})
@@ -213,8 +216,11 @@ def mkdir(self, directory, exists_okay=False):
213216
raise ex
214217
self._pyboard.exit_raw_repl()
215218

216-
def put(self, filename, data):
219+
def put(self, filename, data, progress_callback=None):
217220
"""Create or update the specified file with the provided data.
221+
222+
progress_callback, if provided, is called with (bytes_written, total_bytes)
223+
after each chunk is written.
218224
"""
219225
# Open the file for writing on the board and write chunks of data.
220226
self._pyboard.enter_raw_repl()
@@ -228,6 +234,8 @@ def put(self, filename, data):
228234
if not chunk.startswith("b"):
229235
chunk = "b" + chunk
230236
self._pyboard.exec_("f.write({0})".format(chunk))
237+
if progress_callback:
238+
progress_callback(min(i + BUFFER_SIZE, size), size)
231239
self._pyboard.exec_("f.close()")
232240
self._pyboard.exit_raw_repl()
233241

0 commit comments

Comments
 (0)