Added {function} template, #419

This commit is contained in:
Rhet Turnbull
2021-04-14 22:00:04 -07:00
parent eff8e7a63f
commit 21dc0d388f
20 changed files with 149 additions and 22 deletions

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.42.00"
__version__ = "0.42.1"

View File

@@ -60,7 +60,7 @@ e.g. If Photo is in `Album1` in `Folder1`:
- `"{folder_album(>)}"` renders to `["Folder1>Album1"]`
- `"{folder_album()}"` renders to `["Folder1Album1"]`
`[find|replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
`[find,replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
`conditional`: optional conditional expression that is evaluated as boolean (True/False) for use with the `?bool_value` modifier. Conditional expressions take the form '` not operator value`' where `not` is an optional modifier that negates the `operator`. Note: the space before the conditional expression is required if you use a conditional expression. Valid comparison operators are:

View File

@@ -11,6 +11,7 @@ from ._constants import _UNKNOWN_PERSON
from .datetime_formatter import DateTimeFormatter
from .exiftool import ExifTool
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
from .utils import load_function
# ensure locale set to user's locale
locale.setlocale(locale.LC_ALL, "")
@@ -152,6 +153,10 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
+ "For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. "
+ "'{photo}' provides access to properties that are not available as separate template fields but it assumes some knowledge of "
+ "the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.",
"{function}": "Execute a python function from an external file and use return value as template substitution. "
+ "Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. "
+ "The function will be passed the PhotoInfo object for the photo. "
+ "See https://github.com/RhetTbull/osxphotos/blob/master/examples/template_function.py for an example of how to implement a template function.",
}
FILTER_VALUES = {
@@ -463,6 +468,14 @@ class PhotoTemplate:
vals = self.get_template_value_exiftool(
subfield, filename=filename, dirname=dirname
)
elif field == "function":
if subfield is None:
raise ValueError(
"SyntaxError: filename and function must not be null with {function::filename.py:function_name}"
)
vals = self.get_template_value_function(
subfield, filename=filename, dirname=dirname
)
elif field in MULTI_VALUE_SUBSTITUTIONS or field.startswith("photo"):
vals = self.get_template_value_multi(
field, path_sep=path_sep, filename=filename, dirname=dirname
@@ -1064,6 +1077,39 @@ class PhotoTemplate:
return values
def get_template_value_function(self, subfield, filename=None, dirname=None):
"""Get template value from external function """
if "::" not in subfield:
raise ValueError(
f"SyntaxError: could not parse function name from '{subfield}'"
)
filename, funcname = subfield.split("::")
print(filename, funcname)
if not pathlib.Path(filename).is_file():
raise ValueError(f"'{filename}' does not appear to be a file")
template_func = load_function(filename, funcname)
values = template_func(self.photo)
if not isinstance(values, (str, list)):
raise TypeError(
f"Invalid return type for function {funcname}: expected str or list"
)
if type(values) == str:
values = [values]
# sanitize directory names if needed
if filename:
values = [sanitize_pathpart(value) for value in values]
elif dirname:
values = [sanitize_dirname(value) for value in values]
return values
def get_photo_video_type(self, default):
""" return media type, e.g. photo or video """
default_dict = parse_default_kv(default, PHOTO_VIDEO_TYPE_DEFAULTS)

View File

@@ -51,6 +51,10 @@ Field:
FIELD_WORD+
;
FIELD_WORD:
/[\.\w]+/
;
SubField:
(
":"-
@@ -58,12 +62,8 @@ SubField:
)?
;
FIELD_WORD:
/[\.\w]+/
;
SUBFIELD_WORD:
/[\.\w:]+/
/[\.\w:\/]+/
;
Filter:

View File

@@ -1,5 +1,8 @@
""" Utility functions used in osxphotos """
import fnmatch
import glob
import importlib
import inspect
import logging
import os
@@ -13,6 +16,7 @@ import sys
import unicodedata
import urllib.parse
from plistlib import load as plistload
from typing import Callable
import CoreFoundation
import CoreServices
@@ -401,3 +405,28 @@ def increment_filename(filepath):
count += 1
dest = dest.parent / f"{dest_new}{dest.suffix}"
return str(dest)
def load_function(pyfile: str, function_name: str) -> Callable:
""" Load function_name from python file pyfile """
module_file = pathlib.Path(pyfile)
if not module_file.is_file():
raise FileNotFoundError(f"module {pyfile} does not appear to exist")
module_dir = module_file.parent or pathlib.Path(os.getcwd())
module_name = module_file.stem
# store old sys.path and ensure module_dir at beginning of path
syspath = sys.path
sys.path = [str(module_dir)] + syspath
module = importlib.import_module(module_name)
try:
func = getattr(module, function_name)
except AttributeError:
raise ValueError(f"'{function_name}' not found in module '{module_name}'")
finally:
# restore sys.path
sys.path = syspath
return func