Fixed template function to work with import command (#765)

This commit is contained in:
Rhet Turnbull 2022-08-21 14:33:18 -07:00 committed by GitHub
parent 08b806ff7d
commit 3c98906158
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 91 additions and 8 deletions

View File

@ -0,0 +1,27 @@
""" Example showing how to use a custom function for osxphotos {function} template with the `osxphotos import` command
Use: osxphotos import /path/to/import/*.jpg --album "{function:/path/to/template_function_import.py::example}"
You may place more than one template function in a single file as each is called by name using the {function:file.py::function_name} format
"""
import pathlib
from typing import List, Optional, Union
def example(
filepath: pathlib.Path, args: Optional[str] = None, **kwargs
) -> Union[List, str]:
"""example function for {function} template for use with `osxphotos import`
This example parses filenames in format album_img_123.jpg and returns the album name
Args:
filepath: pathlib.Path object of file being imported
args: optional str of arguments passed to template function
**kwargs: not currently used, placeholder to keep functions compatible with possible changes to {function}
Returns:
str or list of str of values that should be substituted for the {function} template
"""
filename = filepath.stem
fields = filename.split("_")
return fields[0] if len(fields) > 1 else ""

View File

@ -119,7 +119,7 @@ class PhotoInfoFromFile:
Returns:
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
"""
options = options or RenderOptions()
options = options or RenderOptions(caller="import")
template = PhotoTemplate(self, exiftool_path=self._exiftool_path)
return template.render(template_str, options)
@ -163,7 +163,7 @@ def render_photo_template(
photoinfo = PhotoInfoFromFile(filepath, exiftool=exiftool_path)
options = RenderOptions(
none_str=_OSXPHOTOS_NONE_SENTINEL, filepath=relative_filepath
none_str=_OSXPHOTOS_NONE_SENTINEL, filepath=relative_filepath, caller="import"
)
template_values, _ = photoinfo.render_template(template, options=options)
# filter out empty strings

View File

@ -329,6 +329,7 @@ class RenderOptions:
dest_path: set to the destination path of the photo (for use by {function} template), only valid with --filename
filepath: set to value for filepath of the exported photo if you want to evaluate {filepath} template
quote: quote path templates for execution in the shell
caller: which command is calling the template (e.g. 'export')
"""
none_str: str = "_"
@ -343,6 +344,7 @@ class RenderOptions:
dest_path: Optional[str] = None
filepath: Optional[str] = None
quote: bool = False
caller: str = "export"
class PhotoTemplateParser:
@ -788,7 +790,9 @@ class PhotoTemplate:
raise ValueError(
"SyntaxError: filename and function must not be null with {function::filename.py:function_name}"
)
vals = self.get_template_value_function(subfield, field_arg)
vals = self.get_template_value_function(
subfield, field_arg, self.options.caller
)
elif field in MULTI_VALUE_SUBSTITUTIONS or field.startswith("photo"):
vals = self.get_template_value_multi(
field, subfield, path_sep=field_arg, default=default
@ -1459,10 +1463,17 @@ class PhotoTemplate:
def get_template_value_function(
self,
subfield,
field_arg,
subfield: str,
field_arg: Optional[str],
caller: str,
):
"""Get template value from external function"""
"""Get template value from external function
Args:
subfield: the filename and function name in for filename.py::function
field_arg: the argument to pass to the function
caller: the calling source of the template ('export' or 'import')
"""
if "::" not in subfield:
raise ValueError(
@ -1481,8 +1492,17 @@ class PhotoTemplate:
# if no uuid, then template is being validated but not actually run
# so don't run the function
values = []
else:
elif caller == "export":
# function signature is:
# def example(photo: PhotoInfo, options: ExportOptions, args: Optional[str] = None, **kwargs) -> Union[List, str]:
values = template_func(self.photo, options=self.options, args=field_arg)
elif caller == "import":
# function signature is:
# def example(filepath: pathlib.Path, args: Optional[str] = None, **kwargs) -> Union[List, str]:
# the PhotoInfoFromFile class used by import sets `path` to the path of the file being imported
values = template_func(pathlib.Path(self.photo.path), args=field_arg)
else:
raise ValueError(f"Unhandled caller: {caller}")
if not isinstance(values, (str, list)):
raise TypeError(

View File

@ -4,7 +4,9 @@ import os
import os.path
import pathlib
import re
import shutil
import time
from tempfile import TemporaryDirectory
from typing import Dict
import pytest
@ -13,7 +15,7 @@ from photoscript import Photo
from pytest import approx
from osxphotos.cli.import_cli import import_cli
from osxphotos.exiftool import ExifTool, get_exiftool_path
from osxphotos.exiftool import get_exiftool_path
from tests.conftest import get_os_version
TERMINAL_WIDTH = 250
@ -644,3 +646,37 @@ def test_import_check_templates():
for idx, line in enumerate(output):
assert line == TEST_DATA[TEST_IMAGE_1]["check_templates"][idx]
@pytest.mark.test_import
def test_import_function_template():
"""Test import with a function template"""
cwd = os.getcwd()
test_image_1 = os.path.join(cwd, TEST_IMAGE_1)
function = os.path.join(cwd, "examples/template_function_import.py")
with TemporaryDirectory() as tempdir:
test_image = shutil.copy(
test_image_1, os.path.join(tempdir, "MyAlbum_IMG_0001.jpg")
)
runner = CliRunner()
result = runner.invoke(
import_cli,
[
"--verbose",
"--album",
"{function:" + function + "::example}",
test_image,
],
terminal_width=TERMINAL_WIDTH,
)
assert result.exit_code == 0
import_data = parse_import_output(result.output)
file_1 = pathlib.Path(test_image).name
uuid_1 = import_data[file_1]
photo_1 = Photo(uuid_1)
assert photo_1.filename == file_1
albums = [a.title for a in photo_1.albums]
assert albums == ["MyAlbum"]