Added validation for template string options

This commit is contained in:
Rhet Turnbull 2022-04-18 10:28:02 -07:00
parent c48887612c
commit afe5ed3dc0
3 changed files with 142 additions and 4 deletions

View File

@ -84,7 +84,7 @@ from .common import (
)
from .help import ExportCommand, get_help_msg
from .list import _list_libraries
from .param_types import ExportDBType, FunctionCall
from .param_types import ExportDBType, FunctionCall, TemplateString
from .rich_progress import rich_progress
from .verbose import get_verbose_console, time_stamp, verbose_print
@ -267,6 +267,7 @@ from .verbose import get_verbose_console, time_stamp, verbose_print
f"would be named 'photoname_low_res.ext'. The default suffix is '{DEFAULT_PREVIEW_SUFFIX}'. "
"Multi-value templates (see Templating System) are not permitted with --preview-suffix. "
"See also --preview and --preview-if-missing.",
type=TemplateString(),
)
@click.option(
"--download-missing",
@ -384,6 +385,7 @@ from .verbose import get_verbose_console, time_stamp, verbose_print
'You may specify more than one template, for example --keyword-template "{folder_album}" '
'--keyword-template "{created.year}". '
"See '--replace-keywords' and Templating System below.",
type=TemplateString(),
)
@click.option(
"--replace-keywords",
@ -404,6 +406,7 @@ from .verbose import get_verbose_console, time_stamp, verbose_print
"'exported with osxphotos on [today's date]' to the description, you could specify "
'--description-template "{descr} exported with osxphotos on {today.date}" '
"See Templating System below.",
type=TemplateString(),
)
@click.option(
"--finder-tag-template",
@ -414,6 +417,7 @@ from .verbose import get_verbose_console, time_stamp, verbose_print
"'tag:tagname' format. For example, '--finder-tag-template \"{label}\"' to set Finder tags to photo labels. "
"You may specify multiple TEMPLATE values by using '--finder-tag-template' multiple times. "
"See also '--finder-tag-keywords and Extended Attributes below.'.",
type=TemplateString(),
)
@click.option(
"--finder-tag-keywords",
@ -431,6 +435,7 @@ from .verbose import get_verbose_console, time_stamp, verbose_print
"For example, to set Finder comment to the photo's title and description: "
'\'--xattr-template findercomment "{title}; {descr}" '
"See Extended Attributes below for additional details on this option.",
type=TemplateString(),
)
@click.option(
"--directory",
@ -438,6 +443,7 @@ from .verbose import get_verbose_console, time_stamp, verbose_print
default=None,
help="Optional template for specifying name of output directory in the form '{name,DEFAULT}'. "
"See below for additional details on templating system.",
type=TemplateString(),
)
@click.option(
"--filename",
@ -447,6 +453,7 @@ from .verbose import get_verbose_console, time_stamp, verbose_print
help="Optional template for specifying name of output file in the form '{name,DEFAULT}'. "
"File extension will be added automatically--do not include an extension in the FILENAME template. "
"See below for additional details on templating system.",
type=TemplateString(),
)
@click.option(
"--jpeg-ext",
@ -473,6 +480,7 @@ from .verbose import get_verbose_console, time_stamp, verbose_print
"'photoname_edited.ext'. For example, with '--edited-suffix _bearbeiten', the edited photo "
f"would be named 'photoname_bearbeiten.ext'. The default suffix is '{DEFAULT_EDITED_SUFFIX}'. "
"Multi-value templates (see Templating System) are not permitted with --edited-suffix.",
type=TemplateString(),
)
@click.option(
"--original-suffix",
@ -481,6 +489,7 @@ from .verbose import get_verbose_console, time_stamp, verbose_print
"'filename.ext'. For example, with '--original-suffix _original', the original photo "
"would be named 'filename_original.ext'. The default suffix is '' (no suffix). "
"Multi-value templates (see Templating System) are not permitted with --original-suffix.",
type=TemplateString(),
)
@click.option(
"--use-photos-export",
@ -537,7 +546,6 @@ from .verbose import get_verbose_console, time_stamp, verbose_print
"--post-command",
metavar="CATEGORY COMMAND",
nargs=2,
type=(click.Choice(POST_COMMAND_CATEGORIES, case_sensitive=False), str),
multiple=True,
help="Run COMMAND on exported files of category CATEGORY. CATEGORY can be one of: "
f"{', '.join(list(POST_COMMAND_CATEGORIES.keys()))}. "
@ -545,6 +553,9 @@ from .verbose import get_verbose_console, time_stamp, verbose_print
"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. "
"See Post Command below.",
type=click.Tuple(
[click.Choice(POST_COMMAND_CATEGORIES, case_sensitive=False), TemplateString()]
),
)
@click.option(
"--post-function",

View File

@ -1,11 +1,14 @@
"""Click parameter types for osxphotos CLI"""
import datetime
import os
import pathlib
import bitmath
import click
from osxphotos.export_db_utils import export_db_get_version
from osxphotos.photoinfo import PhotoInfoNone
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
from osxphotos.utils import expand_and_validate_filepath, load_function
__all__ = [
@ -14,6 +17,7 @@ __all__ = [
"ExportDBType",
"FunctionCall",
"TimeISO8601",
"TemplateString",
]
@ -106,3 +110,22 @@ class ExportDBType(click.ParamType):
return value
except Exception:
self.fail(f"{value} exists but is not a valid osxphotos export database. ")
class TemplateString(click.ParamType):
"""Validate an osxphotos template language (OTL) template string"""
name = "OTL_TEMPLATE"
def convert(self, value, param, ctx):
try:
cwd = os.getcwd()
_, unmatched = PhotoTemplate(photo=PhotoInfoNone()).render(
value,
options=RenderOptions(export_dir=cwd, dest_path=cwd, filepath=cwd),
)
if unmatched:
self.fail(f"Template '{value}' contains unknown field(s): {unmatched}")
return value
except ValueError as e:
self.fail(e)

View File

@ -3405,7 +3405,7 @@ def test_export_directory_template_3():
],
)
assert result.exit_code != 0
assert "Invalid template" in result.output
assert "Invalid value" in result.output
def test_export_directory_template_album_1():
@ -3660,7 +3660,7 @@ def test_export_filename_template_3():
],
)
assert result.exit_code != 0
assert "Invalid template" in result.output
assert "Invalid value" in result.output
def test_export_album():
@ -7150,6 +7150,59 @@ def test_export_post_command_bad_command():
assert 'Error running command "foobar' in result.output
def test_export_post_command_bad_option_1():
"""Test --post-command with bad options"""
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",
"export", # should be "exported"
"foobar {filepath.name|shell_quote} >> {export_dir}/exported.txt",
"--name",
"Park",
"--skip-original-if-edited",
],
)
assert result.exit_code != 0
assert "Invalid value" in result.output
def test_export_post_command_bad_option_2():
"""Test --post-command with bad options"""
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",
# error in template for command (missing closing curly brace)
"foobar {filepath.name|shell_quote >> {export_dir}/exported.txt",
"--name",
"Park",
"--skip-original-if-edited",
],
)
assert result.exit_code != 0
assert "Invalid value" in result.output
def test_export_post_function():
"""Test --post-function"""
@ -7419,3 +7472,54 @@ def test_export_min_size_1():
)
assert result.exit_code == 0
assert "Exporting 4 photos" in result.output
def test_export_validate_template_1():
""" "Test CLI validation of template arguments"""
runner = CliRunner()
cwd = os.getcwd()
with runner.isolated_filesystem():
result = runner.invoke(
export,
[
".",
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
"--filename",
"{original_names}",
],
)
assert result.exit_code != 0
assert "Invalid value" in result.output
def test_export_validate_template_2():
""" "Test CLI validation of template arguments"""
runner = CliRunner()
cwd = os.getcwd()
with runner.isolated_filesystem():
result = runner.invoke(
export,
[
".",
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
"--filename",
"{original_name",
],
)
assert result.exit_code != 0
assert "Invalid value" in result.output
def test_theme_list():
"""Test theme --list command"""
runner = CliRunner()
temp_file = tempfile.TemporaryFile()
with runner.isolated_filesystem():
result = runner.invoke(cli_main, ["theme", "--list"])
assert result.exit_code == 0
assert "Dark" in result.output