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

View File

@@ -1,11 +1,14 @@
"""Click parameter types for osxphotos CLI""" """Click parameter types for osxphotos CLI"""
import datetime import datetime
import os
import pathlib import pathlib
import bitmath import bitmath
import click import click
from osxphotos.export_db_utils import export_db_get_version 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 from osxphotos.utils import expand_and_validate_filepath, load_function
__all__ = [ __all__ = [
@@ -14,6 +17,7 @@ __all__ = [
"ExportDBType", "ExportDBType",
"FunctionCall", "FunctionCall",
"TimeISO8601", "TimeISO8601",
"TemplateString",
] ]
@@ -106,3 +110,22 @@ class ExportDBType(click.ParamType):
return value return value
except Exception: except Exception:
self.fail(f"{value} exists but is not a valid osxphotos export database. ") 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 result.exit_code != 0
assert "Invalid template" in result.output assert "Invalid value" in result.output
def test_export_directory_template_album_1(): def test_export_directory_template_album_1():
@@ -3660,7 +3660,7 @@ def test_export_filename_template_3():
], ],
) )
assert result.exit_code != 0 assert result.exit_code != 0
assert "Invalid template" in result.output assert "Invalid value" in result.output
def test_export_album(): def test_export_album():
@@ -7150,6 +7150,59 @@ def test_export_post_command_bad_command():
assert 'Error running command "foobar' in result.output 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(): def test_export_post_function():
"""Test --post-function""" """Test --post-function"""
@@ -7419,3 +7472,54 @@ def test_export_min_size_1():
) )
assert result.exit_code == 0 assert result.exit_code == 0
assert "Exporting 4 photos" in result.output 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