From ed1486fd4b153baa97c029b5a294705953005a9c Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Sat, 29 Jul 2023 08:54:07 -0700 Subject: [PATCH] Refactored --sidecar-template options (#1137) --- osxphotos/cli/export.py | 26 ++--- osxphotos/cli/param_types.py | 31 ++++++ osxphotos/cli/sidecar.py | 10 +- tests/test_cli_export_sidecar_template.py | 113 ++++++++++++++-------- 4 files changed, 121 insertions(+), 59 deletions(-) diff --git a/osxphotos/cli/export.py b/osxphotos/cli/export.py index 7aab7dd0..22906726 100644 --- a/osxphotos/cli/export.py +++ b/osxphotos/cli/export.py @@ -88,7 +88,7 @@ from .common import ( ) from .help import ExportCommand, get_help_msg from .list import _list_libraries -from .param_types import BooleanString, ExportDBType, FunctionCall, TemplateString +from .param_types import CSVOptions, ExportDBType, FunctionCall, TemplateString from .report_writer import ReportWriterNoOp, export_report_writer_factory from .rich_progress import rich_progress from .sidecar import generate_user_sidecar @@ -342,15 +342,13 @@ from .verbose import get_verbose_console, verbose_print ) @click.option( "--sidecar-template", - metavar="MAKO_TEMPLATE_FILE SIDECAR_FILENAME_TEMPLATE WRITE_SKIPPED STRIP_WHITESPACE STRIP_LINES", + metavar="MAKO_TEMPLATE_FILE SIDECAR_FILENAME_TEMPLATE OPTIONS", multiple=True, type=click.Tuple( [ click.Path(dir_okay=False, file_okay=True, exists=True), TemplateString(), - BooleanString(), - BooleanString(), - BooleanString(), + CSVOptions(["write_skipped", "strip_whitespace", "strip_lines", "none"]) ] ), help="Create a custom sidecar file for each photo exported with user provided Mako template (MAKO_TEMPLATE_FILE). " @@ -362,17 +360,19 @@ from .verbose import get_verbose_console, verbose_print "which will be rendered to generate the filename of the sidecar file. " "The `{filepath}` template variable may be used in the SIDECAR_FILENAME_TEMPLATE to refer to the filename of the " "photo being exported. " - "WRITE_SKIPPED is a boolean value (true/false, yes/no, 1/0 are all valid values) and indicates whether or not " - "write the sidecar file even if the photo is skipped during export. " - "If WRITE_SKIPPED is false, the sidecar file will not be written if the photo is skipped during export. " - "If WRITE_SKIPPED is true, the sidecar file will be written even if the photo is skipped during export. " - "STRIP_WHITESPACE and STRIP_LINES are boolean values (true/false, yes/no, 1/0 are all valid values) " - "and indicate whether or not to strip whitespace and blank lines from the resulting sidecar file. " + "OPTIONS is a comma-separated list of strings providing additional options to the template. " + "Valid options are: write_skipped, strip_whitespace, strip_lines, none. " + "write_skipped will cause the sidecar file to be written even if the photo is skipped during export. " + "If write_skipped is not passed as an option, the sidecar file will not be written if the photo is skipped during export. " + "strip_whitespace and strip_lines indicate whether or not to strip whitespace and blank lines, respectively, " + "from the resulting sidecar file. " "For example, to create a sidecar file with extension .xmp using a template file named 'sidecar.mako' " "and write a sidecar for skipped photos and strip blank lines but not whitespace: " - "`--sidecar-template sidecar.mako '{filepath}.xmp' yes no yes`. " + "`--sidecar-template sidecar.mako '{filepath}.xmp' write_skipped,strip_lines`. " "To do the same but to drop the photo extension from the sidecar filename: " - "`--sidecar-template sidecar.mako '{filepath.parent}/{filepath.stem}.xmp' yes no yes --sidecar-drop-ext`. " + "`--sidecar-template sidecar.mako '{filepath.parent}/{filepath.stem}.xmp' write_skipped,strip_lines`. " + "If you are not passing any options, you must pass 'none' as the last argument to --sidecar-template: " + "`--sidecar-template sidecar.mako '{filepath}.xmp' none`. " "For an example Mako file see https://raw.githubusercontent.com/RhetTbull/osxphotos/main/examples/custom_sidecar.mako", ) @click.option( diff --git a/osxphotos/cli/param_types.py b/osxphotos/cli/param_types.py index 54c5477a..d9e70ee4 100644 --- a/osxphotos/cli/param_types.py +++ b/osxphotos/cli/param_types.py @@ -1,4 +1,7 @@ """Click parameter types for osxphotos CLI""" + +from __future__ import annotations + import datetime import os import pathlib @@ -19,6 +22,7 @@ from osxphotos.utils import expand_and_validate_filepath, load_function __all__ = [ "BitMathSize", "BooleanString", + "CSVOptions", "DateOffset", "DateTimeISO8601", "DeprecatedPath", @@ -319,3 +323,30 @@ class BooleanString(click.ParamType): self.fail( f"Invalid boolean string {value}. Must be one of True/False, Yes/No, T/F, Y/N, 1/0 (case insensitive)." ) + + +class CSVOptions(click.ParamType): + """A comma-separated list of option values, not case sensitive""" + + name = "CSVOptions" + + def __init__(self, options: list[str]): + """Initialize CSVOptions + + Args: + options: list of valid options as str + + Note: + The convert method returns a tuple[str, ...] of the options selected + """ + self._csv_options = options + + def convert(self, value, param, ctx) -> tuple[str, ...]: + values = value.split(",") + values = [v.lower().strip() for v in values] + for v in values: + if v not in self._csv_options: + self.fail( + f"Invalid option {v}. Must be one of {','.join(self._csv_options)}" + ) + return tuple(values) diff --git a/osxphotos/cli/sidecar.py b/osxphotos/cli/sidecar.py index 854aad2a..3b6d6f39 100644 --- a/osxphotos/cli/sidecar.py +++ b/osxphotos/cli/sidecar.py @@ -23,7 +23,7 @@ def get_template(template: str) -> Template: def generate_user_sidecar( photo: PhotoInfo, export_results: ExportResults, - sidecar_template: tuple[tuple[str, str, bool, bool]], + sidecar_template: tuple[tuple[str, str, tuple[str, ...]], ...], exiftool_path: str, export_dir: str, dry_run: bool, @@ -48,10 +48,12 @@ def generate_user_sidecar( for ( template_file, filename_template, - write_skipped, - strip_whitespace, - strip_lines, + options, ) in sidecar_template: + strip_whitespace = "strip_whitespace" in options + strip_lines = "strip_lines" in options + write_skipped = "write_skipped" in options + if not write_skipped: # skip writing sidecar if photo not exported # but if run with --update and --cleanup, a sidecar file may have been written diff --git a/tests/test_cli_export_sidecar_template.py b/tests/test_cli_export_sidecar_template.py index 8a66faa2..c228bb67 100644 --- a/tests/test_cli_export_sidecar_template.py +++ b/tests/test_cli_export_sidecar_template.py @@ -52,9 +52,7 @@ def test_export_sidecar_template_1(): "--sidecar-template", os.path.join(cwd, "tests", "custom_sidecar.mako"), "{filepath}.txt", - "no", - "no", - "no", + "none", ], ) assert result.exit_code == 0 @@ -63,6 +61,34 @@ def test_export_sidecar_template_1(): sidecar_data = sidecar_file.read_text() assert sidecar_data == SIDECAR_DATA +def test_export_sidecar_template_option_case(): + """test basic export with --sidecar-template and option case insensitivity""" + runner = CliRunner() + cwd = os.getcwd() + # pylint: disable=not-context-manager + with runner.isolated_filesystem(): + result = runner.invoke( + export, + [ + "--library", + os.path.join(cwd, PHOTOS_DB), + ".", + "-V", + "--uuid", + PHOTO_UUID, + "--sidecar-template", + os.path.join(cwd, "tests", "custom_sidecar.mako"), + "{filepath}.txt", + "None", + ], + ) + assert result.exit_code == 0 + sidecar_file = pathlib.Path(SIDECAR_FILENAME) + assert sidecar_file.exists() + sidecar_data = sidecar_file.read_text() + assert sidecar_data == SIDECAR_DATA + + def test_export_sidecar_template_strip_whitespace(): """test basic export with --sidecar-template and STRIP_WHITESPACE = True""" @@ -82,9 +108,7 @@ def test_export_sidecar_template_strip_whitespace(): "--sidecar-template", os.path.join(cwd, "tests", "custom_sidecar.mako"), "{filepath}.txt", - "no", - "yes", - "no", + "strip_whitespace", ], ) assert result.exit_code == 0 @@ -115,9 +139,7 @@ def test_export_sidecar_template_strip_lines(): "--sidecar-template", os.path.join(cwd, "tests", "custom_sidecar.mako"), "{filepath}.txt", - "no", - "no", - "yes", + "strip_lines", ], ) assert result.exit_code == 0 @@ -148,9 +170,7 @@ def test_export_sidecar_template_strip_lines_strip_whitespace(): "--sidecar-template", os.path.join(cwd, "tests", "custom_sidecar.mako"), "{filepath}.txt", - "no", - "yes", - "yes", + "strip_whitespace,strip_lines", ], ) assert result.exit_code == 0 @@ -162,6 +182,35 @@ def test_export_sidecar_template_strip_lines_strip_whitespace(): ) assert sidecar_data == sidecar_expected +def test_export_sidecar_template_strip_lines_strip_whitespace_option_space(): + """test basic export with --sidecar-template and STRIP_LINES = True and STRIP_WHITESPACE = True with space in option""" + runner = CliRunner() + cwd = os.getcwd() + # pylint: disable=not-context-manager + with runner.isolated_filesystem(): + result = runner.invoke( + export, + [ + "--library", + os.path.join(cwd, PHOTOS_DB), + ".", + "-V", + "--uuid", + PHOTO_UUID, + "--sidecar-template", + os.path.join(cwd, "tests", "custom_sidecar.mako"), + "{filepath}.txt", + "strip_whitespace, strip_lines", + ], + ) + assert result.exit_code == 0 + sidecar_file = pathlib.Path(SIDECAR_FILENAME) + assert sidecar_file.exists() + sidecar_data = sidecar_file.read_text() + sidecar_expected = "\n".join( + line.strip() for line in SIDECAR_DATA.splitlines() if line.strip() + ) + assert sidecar_data == sidecar_expected def test_export_sidecar_template_update_no(): """test basic export with --sidecar-template and WRITE_SKIPPED = False, also test --cleanup""" @@ -181,9 +230,7 @@ def test_export_sidecar_template_update_no(): "--sidecar-template", os.path.join(cwd, "tests", "custom_sidecar.mako"), "{filepath}.txt", - "no", - "no", - "no", + "none", ], ) @@ -200,9 +247,7 @@ def test_export_sidecar_template_update_no(): "--sidecar-template", os.path.join(cwd, "tests", "custom_sidecar.mako"), "{filepath}.txt", - "no", - "no", - "no", + "none", "--update", "--cleanup", ], @@ -237,9 +282,7 @@ def test_export_sidecar_template_update_ues(): "--sidecar-template", os.path.join(cwd, "tests", "custom_sidecar.mako"), "{filepath}.txt", - "no", - "no", - "no", + "none", ], ) @@ -256,9 +299,7 @@ def test_export_sidecar_template_update_ues(): "--sidecar-template", os.path.join(cwd, "tests", "custom_sidecar.mako"), "{filepath}.txt", - "yes", - "no", - "no", + "write_skipped", "--update", "--cleanup", ], @@ -294,9 +335,7 @@ def test_export_sidecar_template_report_csv(): "--sidecar-template", os.path.join(cwd, "tests", "custom_sidecar.mako"), "{filepath}.txt", - "no", - "no", - "no", + "none", "--report", "report.csv", ], @@ -340,9 +379,7 @@ def test_export_sidecar_template_report_json(): "--sidecar-template", os.path.join(cwd, "tests", "custom_sidecar.mako"), "{filepath}.txt", - "no", - "no", - "no", + "none", "--report", "report.json", ], @@ -385,9 +422,7 @@ def test_export_sidecar_template_report_db(): "--sidecar-template", os.path.join(cwd, "tests", "custom_sidecar.mako"), "{filepath}.txt", - "no", - "no", - "no", + "none", "--report", "report.db", ], @@ -430,15 +465,11 @@ def test_export_sidecar_template_multiple(): "--sidecar-template", os.path.join(cwd, "tests", "custom_sidecar.mako"), "{filepath}.txt", - "no", - "no", - "no", + "none", "--sidecar-template", os.path.join(cwd, "tests", "custom_sidecar.mako"), "{filepath}.sidecar", - "no", - "no", - "no", + "none", ], ) assert result.exit_code == 0 @@ -470,9 +501,7 @@ def test_export_sidecar_template_full_library(): "--sidecar-template", os.path.join(cwd, "tests", "custom_sidecar.mako"), "{filepath}.txt", - "no", - "no", - "no", + "none", ], ) assert result.exit_code == 0