From 1c44ff71ef6ee8e9c68b1e60a90a22f6e4839140 Mon Sep 17 00:00:00 2001 From: Nagaraaa <106997831+Nagaraaa@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:30:12 +0200 Subject: [PATCH] Add opt-in CLI continuation after file errors --- README.rst | 17 +++++++++++++ send2trash/__main__.py | 11 +++++++- tests/test_script_main.py | 53 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 7671414..7bc9247 100644 --- a/README.rst +++ b/README.rst @@ -65,6 +65,23 @@ create a ``.Trash-$UID`` directory. For any other problem, ``OSError`` is raised. +Command-line usage +------------------ + +Use ``send2trash`` or ``python -m send2trash`` to trash files from the command line:: + + send2trash --verbose file1 file2 + +By default, the command stops at the first error. To attempt the remaining files +after an error, use ``--continue-on-error``:: + + send2trash --continue-on-error --verbose file1 file2 + +Errors are written to standard error. The command exits with status 1 if any +file could not be trashed, even when ``--continue-on-error`` is used. Verbose +output lists only successfully trashed files. This option does not change the +Python API. + .. _PyGObject: https://wiki.gnome.org/PyGObject .. _GIO: https://developer.gnome.org/gio/ .. _trash specifications from freedesktop.org: http://freedesktop.org/wiki/Specifications/trash-spec/ diff --git a/send2trash/__main__.py b/send2trash/__main__.py index 5aa3a6e..a10268a 100644 --- a/send2trash/__main__.py +++ b/send2trash/__main__.py @@ -17,8 +17,12 @@ def main(args=None): parser = ArgumentParser(description="Tool to send files to trash") parser.add_argument("files", nargs="+") parser.add_argument("-v", "--verbose", action="store_true", help="Print deleted files") + parser.add_argument( + "--continue-on-error", action="store_true", help="Continue with remaining files after an error (exit status is still 1)" + ) args = parser.parse_args(args) + failed = False for filename in args.files: try: send2trash(filename) @@ -26,7 +30,12 @@ def main(args=None): print("Trashed «" + filename + "»") except OSError as e: print(str(e), file=sys.stderr) - sys.exit(1) + if not args.continue_on_error: + sys.exit(1) + failed = True + + if failed: + sys.exit(1) if __name__ == "__main__": diff --git a/tests/test_script_main.py b/tests/test_script_main.py index ee98ec0..196bf78 100644 --- a/tests/test_script_main.py +++ b/tests/test_script_main.py @@ -13,3 +13,56 @@ def test_no_args(test_file): pytest.raises(SystemExit, trash_main, []) pytest.raises(SystemExit, trash_main, ["-v"]) assert op.exists(test_file) is True + + +@pytest.mark.parametrize("continue_on_error", [False, True]) +def test_batch_success(monkeypatch, capsys, continue_on_error): + calls = [] + monkeypatch.setattr("send2trash.__main__.send2trash", calls.append) + args = ["-v", "first", "second"] + if continue_on_error: + args.insert(0, "--continue-on-error") + + trash_main(args) + + assert calls == ["first", "second"] + captured = capsys.readouterr() + assert captured.out == "Trashed «first»\nTrashed «second»\n" + assert captured.err == "" + + +@pytest.mark.parametrize("continue_on_error", [False, True]) +@pytest.mark.parametrize("failed_files", [("first",), ("second",), ("first", "second")]) +def test_batch_errors(monkeypatch, capsys, continue_on_error, failed_files): + calls = [] + + def trash(filename): + calls.append(filename) + if filename in failed_files: + raise OSError("Cannot trash " + filename) + + monkeypatch.setattr("send2trash.__main__.send2trash", trash) + files = ["first", "second", "third"] + args = ["-v"] + files + if continue_on_error: + args.insert(0, "--continue-on-error") + + with pytest.raises(SystemExit) as exc: + trash_main(args) + + assert exc.value.code == 1 + expected_calls = files if continue_on_error else files[:files.index(failed_files[0]) + 1] + assert calls == expected_calls + captured = capsys.readouterr() + assert captured.out == "".join("Trashed «" + name + "»\n" for name in calls if name not in failed_files) + assert captured.err == "".join("Cannot trash " + name + "\n" for name in calls if name in failed_files) + + +def test_continue_on_error_does_not_hide_unexpected_errors(monkeypatch): + def trash(filename): + raise RuntimeError("unexpected error") + + monkeypatch.setattr("send2trash.__main__.send2trash", trash) + + with pytest.raises(RuntimeError, match="unexpected error"): + trash_main(["--continue-on-error", "first"])