From ff0328785f3ea14b1c8ae2b7d1a9b07e8aef0777 Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Sun, 28 Jun 2020 13:46:35 -0700 Subject: [PATCH] Added expand_inplace to PhotoTemplate.render --- osxphotos/photoinfo/photoinfo.py | 21 ++++++++++-- osxphotos/photosdb/photosdb.py | 3 +- osxphotos/phototemplate.py | 59 +++++++++++++++++++++++++++----- tests/test_mojave_10_14_6.py | 4 +-- tests/test_template.py | 45 ++++++++++++++++++++++++ 5 files changed, 118 insertions(+), 14 deletions(-) diff --git a/osxphotos/photoinfo/photoinfo.py b/osxphotos/photoinfo/photoinfo.py index 52b93cf0..4e8eac1b 100644 --- a/osxphotos/photoinfo/photoinfo.py +++ b/osxphotos/photoinfo/photoinfo.py @@ -642,7 +642,14 @@ class PhotoInfo: otherwise returns False """ return self._info["raw_is_original"] - def render_template(self, template_str, none_str="_", path_sep=None): + def render_template( + self, + template_str, + none_str="_", + path_sep=None, + expand_inplace=False, + inplace_sep=None, + ): """Renders a template string for PhotoInfo instance using PhotoTemplate Args: @@ -650,12 +657,22 @@ class PhotoInfo: none_str: a str to use if template field renders to None, default is "_". path_sep: a single character str to use as path separator when joining fields like folder_album; if not provided, defaults to os.path.sep + expand_inplace: expand multi-valued substitutions in-place as a single string + instead of returning individual strings + inplace_sep: optional string to use as separator between multi-valued keywords + with expand_inplace; default is ',' Returns: ([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values """ template = PhotoTemplate(self) - return template.render(template_str, none_str, path_sep) + return template.render( + template_str, + none_str=none_str, + path_sep=path_sep, + expand_inplace=expand_inplace, + inplace_sep=inplace_sep, + ) @property def _longitude(self): diff --git a/osxphotos/photosdb/photosdb.py b/osxphotos/photosdb/photosdb.py index 9d2c3585..41cac1b7 100644 --- a/osxphotos/photosdb/photosdb.py +++ b/osxphotos/photosdb/photosdb.py @@ -2232,8 +2232,7 @@ class PhotosDB: logging.debug(f"Could not find person '{person}' in database") photos_sets.append(person_set) - # sourcery off - if from_date or to_date: + if from_date or to_date: # sourcery off dsel = self._dbphotos if from_date: dsel = { diff --git a/osxphotos/phototemplate.py b/osxphotos/phototemplate.py index ac4bc95f..dd71ef27 100644 --- a/osxphotos/phototemplate.py +++ b/osxphotos/phototemplate.py @@ -124,13 +124,24 @@ class PhotoTemplate: # gets initialized in get_template_value self.today = None - def render(self, template, none_str="_", path_sep=None): + def render( + self, + template, + none_str="_", + path_sep=None, + expand_inplace=False, + inplace_sep=None, + ): """ Render a filename or directory template Args: template: str template none_str: str to use default for None values, default is '_' path_sep: optional character to use as path separator, default is os.path.sep + expand_inplace: expand multi-valued substitutions in-place as a single string + instead of returning individual strings + inplace_sep: optional string to use as separator between multi-valued keywords + with expand_inplace; default is ',' Returns: ([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values @@ -141,6 +152,9 @@ class PhotoTemplate: elif path_sep is not None and len(path_sep) != 1: raise ValueError(f"path_sep must be single character: {path_sep}") + if inplace_sep is None: + inplace_sep = "," + # the rendering happens in two phases: # phase 1: handle all the single-value template substitutions # results in a single string with all the template fields replaced @@ -226,13 +240,19 @@ class PhotoTemplate: for str_template in rendered_strings: if regex_multi.search(str_template): values = self.get_template_value_multi(field, path_sep) - for val in values: + if expand_inplace: + # instead of returning multiple strings, join values into a single string + val = ( + inplace_sep.join(sorted(values)) + if values and values[0] + else None + ) def lookup_template_value_multi(lookup_value, default): """ Closure passed to make_subst_function get_func - Capture val and field in the closure - Allows make_subst_function to be re-used w/o modification - default is not used but required so signature matches get_template_value """ + Capture val and field in the closure + Allows make_subst_function to be re-used w/o modification + default is not used but required so signature matches get_template_value """ if lookup_value == field: return val else: @@ -242,10 +262,33 @@ class PhotoTemplate: self, none_str, get_func=lookup_template_value_multi ) new_string = regex_multi.sub(subst, str_template) - new_strings.add(new_string) - # update rendered_strings for the next field to process - rendered_strings = new_strings + # update rendered_strings for the next field to process + rendered_strings = {new_string} + else: + # create a new template string for each value + for val in values: + + def lookup_template_value_multi(lookup_value, default): + """ Closure passed to make_subst_function get_func + Capture val and field in the closure + Allows make_subst_function to be re-used w/o modification + default is not used but required so signature matches get_template_value """ + if lookup_value == field: + return val + else: + raise ValueError( + f"Unexpected value: {lookup_value}" + ) + + subst = make_subst_function( + self, none_str, get_func=lookup_template_value_multi + ) + new_string = regex_multi.sub(subst, str_template) + new_strings.add(new_string) + + # update rendered_strings for the next field to process + rendered_strings = new_strings # find any {fields} that weren't replaced unmatched = [] diff --git a/tests/test_mojave_10_14_6.py b/tests/test_mojave_10_14_6.py index c7b8cfb4..90da828e 100644 --- a/tests/test_mojave_10_14_6.py +++ b/tests/test_mojave_10_14_6.py @@ -168,8 +168,8 @@ def test_missing(): photos = photosdb.photos(uuid=["od0fmC7NQx+ayVr+%i06XA"]) assert len(photos) == 1 p = photos[0] - assert p.path == None - assert p.ismissing == True + assert p.path is None + assert p.ismissing def test_favorite(): diff --git a/tests/test_template.py b/tests/test_template.py index 8c64b15b..1c634ccc 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -458,3 +458,48 @@ def test_subst_strftime(): rendered, unmatched = photo.render_template("{created.strftime}") assert rendered[0] == "_" + + +def test_subst_expand_inplace_1(): + """ Test that substitutions are correct when expand_inplace=True """ + import osxphotos + + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1) + # one album, one keyword, two persons + photo = photosdb.photos(uuid=[UUID_DICT["1_1_2"]])[0] + + template = "{person}" + expected = ["Katie,Suzy"] + rendered, unknown = photo.render_template(template, expand_inplace=True) + assert sorted(rendered) == sorted(expected) + + +def test_subst_expand_inplace_2(): + """ Test that substitutions are correct when expand_inplace=True """ + import osxphotos + + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1) + # one album, one keyword, two persons + photo = photosdb.photos(uuid=[UUID_DICT["1_1_2"]])[0] + + template = "{person}-{keyword}" + expected = ["Katie,Suzy-Kids"] + rendered, unknown = photo.render_template(template, expand_inplace=True) + assert sorted(rendered) == sorted(expected) + + +def test_subst_expand_inplace_3(): + """ Test that substitutions are correct when expand_inplace=True and inplace_sep specified""" + import osxphotos + + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1) + # one album, one keyword, two persons + photo = photosdb.photos(uuid=[UUID_DICT["1_1_2"]])[0] + + template = "{person}-{keyword}" + expected = ["Katie; Suzy-Kids"] + rendered, unknown = photo.render_template( + template, expand_inplace=True, inplace_sep="; " + ) + assert sorted(rendered) == sorted(expected) +