From dd6d519135ce7d25c00826ea108c1e2ab73ca654 Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Sun, 18 Apr 2021 17:52:14 -0700 Subject: [PATCH] Added function filter to template system, closes #429 --- osxphotos/phototemplate.py | 49 ++++++++++++++++++++++++++++++-------- osxphotos/phototemplate.tx | 2 +- tests/template_filter.py | 17 +++++++++++++ tests/test_template.py | 28 ++++++++++++++++++++++ 4 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 tests/template_filter.py diff --git a/osxphotos/phototemplate.py b/osxphotos/phototemplate.py index 26ba7853..b0168939 100644 --- a/osxphotos/phototemplate.py +++ b/osxphotos/phototemplate.py @@ -913,42 +913,44 @@ class PhotoTemplate: if values and type(values) == list: value = [v.lower() for v in values] else: - value = [values.lower()] + value = [values.lower()] if values else [] elif filter_ == "upper": if values and type(values) == list: value = [v.upper() for v in values] else: - value = [values.upper()] + value = [values.upper()] if values else [] elif filter_ == "strip": if values and type(values) == list: value = [v.strip() for v in values] else: - value = [values.strip()] + value = [values.strip()] if values else [] elif filter_ == "capitalize": if values and type(values) == list: value = [v.capitalize() for v in values] else: - value = [values.capitalize()] + value = [values.capitalize()] if values else [] elif filter_ == "titlecase": if values and type(values) == list: value = [v.title() for v in values] else: - value = [values.title()] + value = [values.title()] if values else [] elif filter_ == "braces": if values and type(values) == list: value = ["{" + v + "}" for v in values] else: - value = ["{" + values + "}"] + value = ["{" + values + "}"] if values else [] elif filter_ == "parens": if values and type(values) == list: value = ["(" + v + ")" for v in values] else: - value = ["(" + values + ")"] + value = ["(" + values + ")"] if values else [] elif filter_ == "brackets": if values and type(values) == list: value = ["[" + v + "]" for v in values] else: - value = ["[" + values + "]"] + value = ["[" + values + "]"] if values else [] + elif filter_.startswith("function:"): + value = self.get_template_value_filter_function(filter_, values) else: value = [] return value @@ -1097,8 +1099,6 @@ class PhotoTemplate: 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") @@ -1120,6 +1120,35 @@ class PhotoTemplate: return values + def get_template_value_filter_function(self, filter_, values): + """Filter template value from external function """ + + filter_ = filter_.replace("function:","") + + if "::" not in filter_: + raise ValueError( + f"SyntaxError: could not parse function name from '{filter_}'" + ) + + filename, funcname = filter_.split("::") + + if not pathlib.Path(filename).is_file(): + raise ValueError(f"'{filename}' does not appear to be a file") + + template_func = load_function(filename, funcname) + + if not isinstance(values, (list, tuple)): + values = [values] + values = template_func(values) + + if not isinstance(values, list): + raise TypeError( + f"Invalid return type for function {funcname}: expected list" + ) + + 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) diff --git a/osxphotos/phototemplate.tx b/osxphotos/phototemplate.tx index d5dd2097..795f4a7a 100644 --- a/osxphotos/phototemplate.tx +++ b/osxphotos/phototemplate.tx @@ -74,7 +74,7 @@ Filter: ; FILTER_WORD: - /[\.\w]+/ + /[\.\w:\/]+/ ; Conditional: diff --git a/tests/template_filter.py b/tests/template_filter.py new file mode 100644 index 00000000..454d345d --- /dev/null +++ b/tests/template_filter.py @@ -0,0 +1,17 @@ +""" Example of using a custom python function as an osxphotos template filter + + Use in formath: + "{template_field|template_filter.py::myfilter}" + + Your filter function will receive a list of strings even if the template renders to a single value. + You should expect a list and return a list and be able to handle multi-value templates like {keyword} + as well as single-value templates like {original_name} +""" + +from typing import List + +def myfilter(values: List[str]) -> List[str]: + """ Custom filter to append "foo-" to template value """ + values = ["foo-" + val for val in values] + return values + diff --git a/tests/test_template.py b/tests/test_template.py index 47c57f2d..d420fe65 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -982,3 +982,31 @@ def test_function_bad(photosdb): "{function:tests/template_function.py::foobar}" ) + +def test_function_filter(photosdb): + """ Test {field|function} filter""" + photo = photosdb.get_photo(UUID_MULTI_KEYWORDS) + + rendered, _ = photo.render_template( + "{photo.original_filename|function:tests/template_filter.py::myfilter}" + ) + assert rendered == [f"foo-{photo.original_filename}"] + + rendered, _ = photo.render_template( + "{photo.original_filename|lower|function:tests/template_filter.py::myfilter}" + ) + assert rendered == [f"foo-{photo.original_filename.lower()}"] + + rendered, _ = photo.render_template( + "{photo.original_filename|function:tests/template_filter.py::myfilter|lower}" + ) + assert rendered == [f"foo-{photo.original_filename.lower()}"] + + +def test_function_filter_bad(photosdb): + """ Test invalid {field|function} filter""" + photo = photosdb.get_photo(UUID_MULTI_KEYWORDS) + with pytest.raises(ValueError): + rendered, _ = photo.render_template( + "{photo.original_filename|function:tests/template_filter.py::foobar}" + )