Added validation for template string options
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user