Skip to content

Commit 2875b45

Browse files
authored
Feature post command options 1142 (#1145)
* Added --post-command-break/catch * Added --post-command-break/catch * Added --post-command-error and tests * Fixed help text for --post-command-error
1 parent 8fb47d9 commit 2875b45

File tree

2 files changed

+98
-22
lines changed

2 files changed

+98
-22
lines changed

osxphotos/cli/export.py

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""export command for osxphotos CLI"""
22

3+
from __future__ import annotations
4+
35
import atexit
46
import inspect
57
import os
@@ -9,7 +11,7 @@
911
import subprocess
1012
import sys
1113
import time
12-
from typing import Iterable, List, Optional, Tuple
14+
from typing import Any, Callable, Iterable, List, Literal, Optional, Tuple
1315

1416
import click
1517

@@ -647,7 +649,7 @@
647649
"If present, this file will be read after the export is completed and any rules found in the file "
648650
"will be added to the list of rules to keep. "
649651
"This file uses the same format as a .gitignore file and should contain one rule per line; "
650-
"lines starting with a `#` will be ignored. "
652+
"lines starting with a `#` will be ignored. ",
651653
)
652654
@click.option(
653655
"--add-exported-to-album",
@@ -683,11 +685,22 @@
683685
"COMMAND is an osxphotos template string, for example: '--post-command exported \"echo {filepath|shell_quote} >> {export_dir}/exported.txt\"', "
684686
"which appends the full path of all exported files to the file 'exported.txt'. "
685687
"You can run more than one command by repeating the '--post-command' option with different arguments. "
688+
"See also --post-command-error and --post-function."
686689
"See Post Command below.",
687690
type=click.Tuple(
688691
[click.Choice(POST_COMMAND_CATEGORIES, case_sensitive=False), TemplateString()]
689692
),
690693
)
694+
@click.option(
695+
"--post-command-error",
696+
metavar="ACTION",
697+
help="Specify either `continue` or `break` for ACTION to control behavior when a post-command fails. "
698+
"If `continue`, osxphotos will log the error and continue processing. "
699+
"If `break`, osxphotos will stop processing any additional --post-command commands for the current photo "
700+
"but will continue with the export. "
701+
"Without --post-command-error, osxphotos will abort the export if a post-command encounters an error. ",
702+
type=click.Choice(["continue", "break"], case_sensitive=False),
703+
)
691704
@click.option(
692705
"--post-function",
693706
metavar="filename.py::function",
@@ -910,6 +923,7 @@ def export(
910923
place,
911924
portrait,
912925
post_command,
926+
post_command_error,
913927
post_function,
914928
preview,
915929
preview_if_missing,
@@ -1138,6 +1152,7 @@ def export(
11381152
place = cfg.place
11391153
portrait = cfg.portrait
11401154
post_command = cfg.post_command
1155+
post_command_error = cfg.post_command_error
11411156
post_function = cfg.post_function
11421157
preview = cfg.preview
11431158
preview_if_missing = cfg.preview_if_missing
@@ -1575,7 +1590,7 @@ def cleanup_lock_files():
15751590
export_dir=dest,
15761591
dry_run=dry_run,
15771592
exiftool_path=exiftool_path,
1578-
export_db=export_db,
1593+
on_error=post_command_error,
15791594
verbose=verbose,
15801595
)
15811596

@@ -2590,7 +2605,7 @@ def collect_files_to_keep(
25902605
KEEP_RULEs = []
25912606

25922607
# parse .osxphotos_keep file if it exists
2593-
keep_file : pathlib.Path = export_dir / ".osxphotos_keep"
2608+
keep_file: pathlib.Path = export_dir / ".osxphotos_keep"
25942609
if keep_file.is_file():
25952610
for line in keep_file.read_text().splitlines():
25962611
line = line.rstrip("\r\n")
@@ -2604,10 +2619,10 @@ def collect_files_to_keep(
26042619
KEEP_RULEs.append(k.replace(export_dir_str, ""))
26052620
else:
26062621
KEEP_RULEs.append(k)
2607-
2622+
26082623
if not KEEP_RULEs:
26092624
return [], []
2610-
2625+
26112626
# have some rules to apply
26122627
matcher = osxphotos.gitignorefile.parse_pattern_list(KEEP_RULEs, export_dir)
26132628
keepers = []
@@ -2841,16 +2856,18 @@ def write_extended_attributes(
28412856

28422857

28432858
def run_post_command(
2844-
photo,
2845-
post_command,
2846-
export_results,
2847-
export_dir,
2848-
dry_run,
2849-
exiftool_path,
2850-
export_db,
2851-
verbose,
2859+
photo: osxphotos.PhotoInfo,
2860+
post_command: tuple[tuple[str, str]],
2861+
export_results: ExportResults,
2862+
export_dir: str | pathlib.Path,
2863+
dry_run: bool,
2864+
exiftool_path: str,
2865+
on_error: Literal["break", "continue"] | None,
2866+
verbose: Callable[[Any], None],
28522867
):
2868+
"""Run --post-command commands"""
28532869
# todo: pass in RenderOptions from export? (e.g. so it contains strip, etc?)
2870+
28542871
for category, command_template in post_command:
28552872
files = getattr(export_results, category)
28562873
for f in files:
@@ -2864,7 +2881,6 @@ def run_post_command(
28642881
if command:
28652882
verbose(f'Running command: "{command}"')
28662883
if not dry_run:
2867-
args = shlex.split(command)
28682884
cwd = pathlib.Path(f).parent
28692885
run_error = None
28702886
run_results = None
@@ -2873,11 +2889,18 @@ def run_post_command(
28732889
except Exception as e:
28742890
run_error = e
28752891
finally:
2876-
run_error = run_error or run_results.returncode
2877-
if run_error:
2878-
rich_echo_error(
2879-
f'[error]Error running command "{command}": {run_error}'
2880-
)
2892+
returncode = run_results.returncode if run_results else None
2893+
if run_error or returncode:
2894+
# there was an error running the command
2895+
error_str = f'Error running command "{command}": return code: {returncode}, exception: {run_error}'
2896+
rich_echo_error(f"[error]{error_str}[/]")
2897+
if not on_error:
2898+
# no error handling specified, raise exception
2899+
raise RuntimeError(error_str)
2900+
if on_error == "break":
2901+
# break out of loop and return
2902+
return
2903+
# else on_error must be continue
28812904

28822905

28832906
def render_and_validate_report(report: str, exiftool_path: str, export_dir: str) -> str:

tests/test_cli.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8644,14 +8644,67 @@ def test_export_post_command_bad_command():
86448644
".",
86458645
"--post-command",
86468646
"exported",
8647-
"foobar {filepath.name|shell_quote} >> {export_dir}/exported.txt",
8647+
"false",
86488648
"--name",
86498649
"Park",
86508650
"--skip-original-if-edited",
86518651
],
86528652
)
8653+
assert result.exit_code != 0
8654+
8655+
8656+
def test_export_post_command_bad_command_continue():
8657+
"""Test --post-command with bad command with --post-command-error=continue"""
8658+
8659+
runner = CliRunner()
8660+
cwd = os.getcwd()
8661+
# pylint: disable=not-context-manager
8662+
with runner.isolated_filesystem():
8663+
result = runner.invoke(
8664+
cli_main,
8665+
[
8666+
"export",
8667+
"--db",
8668+
os.path.join(cwd, PHOTOS_DB_15_7),
8669+
".",
8670+
"--post-command",
8671+
"exported",
8672+
"false",
8673+
"--post-command-error",
8674+
"continue",
8675+
"--name",
8676+
"wedding",
8677+
],
8678+
)
8679+
assert result.exit_code == 0
8680+
assert result.output.count("Error running command") == 2
8681+
8682+
8683+
def test_export_post_command_bad_command_break():
8684+
"""Test --post-command with bad command with --post-command-error=break"""
8685+
8686+
runner = CliRunner()
8687+
cwd = os.getcwd()
8688+
# pylint: disable=not-context-manager
8689+
with runner.isolated_filesystem():
8690+
result = runner.invoke(
8691+
cli_main,
8692+
[
8693+
"export",
8694+
"--db",
8695+
os.path.join(cwd, PHOTOS_DB_15_7),
8696+
".",
8697+
"--post-command",
8698+
"exported",
8699+
"false",
8700+
"--post-command-error",
8701+
"break",
8702+
"--name",
8703+
"wedding",
8704+
],
8705+
)
86538706
assert result.exit_code == 0
8654-
assert 'Error running command "foobar' in result.output
8707+
assert result.output.count("Error running command") == 1
86558708

86568709

86578710
def test_export_post_command_bad_option_1():

0 commit comments

Comments
 (0)