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
This commit is contained in:
Rhet Turnbull 2023-08-05 11:11:47 -04:00 committed by GitHub
parent 8fb47d9c40
commit 2875b45d6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 98 additions and 22 deletions

View File

@ -1,5 +1,7 @@
"""export command for osxphotos CLI""" """export command for osxphotos CLI"""
from __future__ import annotations
import atexit import atexit
import inspect import inspect
import os import os
@ -9,7 +11,7 @@ import shlex
import subprocess import subprocess
import sys import sys
import time import time
from typing import Iterable, List, Optional, Tuple from typing import Any, Callable, Iterable, List, Literal, Optional, Tuple
import click import click
@ -647,7 +649,7 @@ from .verbose import get_verbose_console, verbose_print
"If present, this file will be read after the export is completed and any rules found in the file " "If present, this file will be read after the export is completed and any rules found in the file "
"will be added to the list of rules to keep. " "will be added to the list of rules to keep. "
"This file uses the same format as a .gitignore file and should contain one rule per line; " "This file uses the same format as a .gitignore file and should contain one rule per line; "
"lines starting with a `#` will be ignored. " "lines starting with a `#` will be ignored. ",
) )
@click.option( @click.option(
"--add-exported-to-album", "--add-exported-to-album",
@ -683,11 +685,22 @@ from .verbose import get_verbose_console, verbose_print
"COMMAND is an osxphotos template string, for example: '--post-command exported \"echo {filepath|shell_quote} >> {export_dir}/exported.txt\"', " "COMMAND is an osxphotos template string, for example: '--post-command exported \"echo {filepath|shell_quote} >> {export_dir}/exported.txt\"', "
"which appends the full path of all exported files to the file 'exported.txt'. " "which appends the full path of all exported files to the file 'exported.txt'. "
"You can run more than one command by repeating the '--post-command' option with different arguments. " "You can run more than one command by repeating the '--post-command' option with different arguments. "
"See also --post-command-error and --post-function."
"See Post Command below.", "See Post Command below.",
type=click.Tuple( type=click.Tuple(
[click.Choice(POST_COMMAND_CATEGORIES, case_sensitive=False), TemplateString()] [click.Choice(POST_COMMAND_CATEGORIES, case_sensitive=False), TemplateString()]
), ),
) )
@click.option(
"--post-command-error",
metavar="ACTION",
help="Specify either `continue` or `break` for ACTION to control behavior when a post-command fails. "
"If `continue`, osxphotos will log the error and continue processing. "
"If `break`, osxphotos will stop processing any additional --post-command commands for the current photo "
"but will continue with the export. "
"Without --post-command-error, osxphotos will abort the export if a post-command encounters an error. ",
type=click.Choice(["continue", "break"], case_sensitive=False),
)
@click.option( @click.option(
"--post-function", "--post-function",
metavar="filename.py::function", metavar="filename.py::function",
@ -910,6 +923,7 @@ def export(
place, place,
portrait, portrait,
post_command, post_command,
post_command_error,
post_function, post_function,
preview, preview,
preview_if_missing, preview_if_missing,
@ -1138,6 +1152,7 @@ def export(
place = cfg.place place = cfg.place
portrait = cfg.portrait portrait = cfg.portrait
post_command = cfg.post_command post_command = cfg.post_command
post_command_error = cfg.post_command_error
post_function = cfg.post_function post_function = cfg.post_function
preview = cfg.preview preview = cfg.preview
preview_if_missing = cfg.preview_if_missing preview_if_missing = cfg.preview_if_missing
@ -1575,7 +1590,7 @@ def export(
export_dir=dest, export_dir=dest,
dry_run=dry_run, dry_run=dry_run,
exiftool_path=exiftool_path, exiftool_path=exiftool_path,
export_db=export_db, on_error=post_command_error,
verbose=verbose, verbose=verbose,
) )
@ -2590,7 +2605,7 @@ def collect_files_to_keep(
KEEP_RULEs = [] KEEP_RULEs = []
# parse .osxphotos_keep file if it exists # parse .osxphotos_keep file if it exists
keep_file : pathlib.Path = export_dir / ".osxphotos_keep" keep_file: pathlib.Path = export_dir / ".osxphotos_keep"
if keep_file.is_file(): if keep_file.is_file():
for line in keep_file.read_text().splitlines(): for line in keep_file.read_text().splitlines():
line = line.rstrip("\r\n") line = line.rstrip("\r\n")
@ -2604,10 +2619,10 @@ def collect_files_to_keep(
KEEP_RULEs.append(k.replace(export_dir_str, "")) KEEP_RULEs.append(k.replace(export_dir_str, ""))
else: else:
KEEP_RULEs.append(k) KEEP_RULEs.append(k)
if not KEEP_RULEs: if not KEEP_RULEs:
return [], [] return [], []
# have some rules to apply # have some rules to apply
matcher = osxphotos.gitignorefile.parse_pattern_list(KEEP_RULEs, export_dir) matcher = osxphotos.gitignorefile.parse_pattern_list(KEEP_RULEs, export_dir)
keepers = [] keepers = []
@ -2841,16 +2856,18 @@ def write_extended_attributes(
def run_post_command( def run_post_command(
photo, photo: osxphotos.PhotoInfo,
post_command, post_command: tuple[tuple[str, str]],
export_results, export_results: ExportResults,
export_dir, export_dir: str | pathlib.Path,
dry_run, dry_run: bool,
exiftool_path, exiftool_path: str,
export_db, on_error: Literal["break", "continue"] | None,
verbose, verbose: Callable[[Any], None],
): ):
"""Run --post-command commands"""
# todo: pass in RenderOptions from export? (e.g. so it contains strip, etc?) # todo: pass in RenderOptions from export? (e.g. so it contains strip, etc?)
for category, command_template in post_command: for category, command_template in post_command:
files = getattr(export_results, category) files = getattr(export_results, category)
for f in files: for f in files:
@ -2864,7 +2881,6 @@ def run_post_command(
if command: if command:
verbose(f'Running command: "{command}"') verbose(f'Running command: "{command}"')
if not dry_run: if not dry_run:
args = shlex.split(command)
cwd = pathlib.Path(f).parent cwd = pathlib.Path(f).parent
run_error = None run_error = None
run_results = None run_results = None
@ -2873,11 +2889,18 @@ def run_post_command(
except Exception as e: except Exception as e:
run_error = e run_error = e
finally: finally:
run_error = run_error or run_results.returncode returncode = run_results.returncode if run_results else None
if run_error: if run_error or returncode:
rich_echo_error( # there was an error running the command
f'[error]Error running command "{command}": {run_error}' error_str = f'Error running command "{command}": return code: {returncode}, exception: {run_error}'
) rich_echo_error(f"[error]{error_str}[/]")
if not on_error:
# no error handling specified, raise exception
raise RuntimeError(error_str)
if on_error == "break":
# break out of loop and return
return
# else on_error must be continue
def render_and_validate_report(report: str, exiftool_path: str, export_dir: str) -> str: def render_and_validate_report(report: str, exiftool_path: str, export_dir: str) -> str:

View File

@ -8644,14 +8644,67 @@ def test_export_post_command_bad_command():
".", ".",
"--post-command", "--post-command",
"exported", "exported",
"foobar {filepath.name|shell_quote} >> {export_dir}/exported.txt", "false",
"--name", "--name",
"Park", "Park",
"--skip-original-if-edited", "--skip-original-if-edited",
], ],
) )
assert result.exit_code != 0
def test_export_post_command_bad_command_continue():
"""Test --post-command with bad command with --post-command-error=continue"""
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
cli_main,
[
"export",
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
".",
"--post-command",
"exported",
"false",
"--post-command-error",
"continue",
"--name",
"wedding",
],
)
assert result.exit_code == 0 assert result.exit_code == 0
assert 'Error running command "foobar' in result.output assert result.output.count("Error running command") == 2
def test_export_post_command_bad_command_break():
"""Test --post-command with bad command with --post-command-error=break"""
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
cli_main,
[
"export",
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
".",
"--post-command",
"exported",
"false",
"--post-command-error",
"break",
"--name",
"wedding",
],
)
assert result.exit_code == 0
assert result.output.count("Error running command") == 1
def test_export_post_command_bad_option_1(): def test_export_post_command_bad_option_1():