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: Returns:
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values ([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) template = PhotoTemplate(self, exiftool_path=self._exiftool_path)
return template.render(template_str, options) return template.render(template_str, options)
@ -163,7 +163,7 @@ def render_photo_template(
photoinfo = PhotoInfoFromFile(filepath, exiftool=exiftool_path) photoinfo = PhotoInfoFromFile(filepath, exiftool=exiftool_path)
options = RenderOptions( 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) template_values, _ = photoinfo.render_template(template, options=options)
# filter out empty strings # 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 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 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 quote: quote path templates for execution in the shell
caller: which command is calling the template (e.g. 'export')
""" """
none_str: str = "_" none_str: str = "_"
@ -343,6 +344,7 @@ class RenderOptions:
dest_path: Optional[str] = None dest_path: Optional[str] = None
filepath: Optional[str] = None filepath: Optional[str] = None
quote: bool = False quote: bool = False
caller: str = "export"
class PhotoTemplateParser: class PhotoTemplateParser:
@ -788,7 +790,9 @@ class PhotoTemplate:
raise ValueError( raise ValueError(
"SyntaxError: filename and function must not be null with {function::filename.py:function_name}" "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"): elif field in MULTI_VALUE_SUBSTITUTIONS or field.startswith("photo"):
vals = self.get_template_value_multi( vals = self.get_template_value_multi(
field, subfield, path_sep=field_arg, default=default field, subfield, path_sep=field_arg, default=default
@ -1459,10 +1463,17 @@ class PhotoTemplate:
def get_template_value_function( def get_template_value_function(
self, self,
subfield, subfield: str,
field_arg, 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: if "::" not in subfield:
raise ValueError( raise ValueError(
@ -1481,8 +1492,17 @@ class PhotoTemplate:
# if no uuid, then template is being validated but not actually run # if no uuid, then template is being validated but not actually run
# so don't run the function # so don't run the function
values = [] 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) 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)): if not isinstance(values, (str, list)):
raise TypeError( raise TypeError(

View File

@ -4,7 +4,9 @@ import os
import os.path import os.path
import pathlib import pathlib
import re import re
import shutil
import time import time
from tempfile import TemporaryDirectory
from typing import Dict from typing import Dict
import pytest import pytest
@ -13,7 +15,7 @@ from photoscript import Photo
from pytest import approx from pytest import approx
from osxphotos.cli.import_cli import import_cli 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 from tests.conftest import get_os_version
TERMINAL_WIDTH = 250 TERMINAL_WIDTH = 250
@ -644,3 +646,37 @@ def test_import_check_templates():
for idx, line in enumerate(output): for idx, line in enumerate(output):
assert line == TEST_DATA[TEST_IMAGE_1]["check_templates"][idx] 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"]