From aa1a96d20118916a558b08e7f8ec87c43abf789b Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Sun, 11 Apr 2021 23:36:17 -0700 Subject: [PATCH] Added {photo} template, partial fix for issue #417 --- README.md | 13 +++++++++++++ osxphotos/_version.py | 2 +- osxphotos/phototemplate.py | 35 +++++++++++++++++++++++++++++++++-- tests/test_template.py | 28 +++++++++++++++++++++++++++- 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cfc748a8..10cbf66b 100644 --- a/README.md +++ b/README.md @@ -1213,6 +1213,18 @@ Substitution Description 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms). +{photo} Provides direct access to the PhotoInfo object for + the photo. Must be used in format '{photo.property}' + where 'property' represents a PhotoInfo property. 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. + ``` @@ -2788,6 +2800,7 @@ The following template field substitutions are availabe for use with `PhotoInfo. |{searchinfo.activity}|Activities associated with a photo, e.g. 'Sporting Event'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).| |{searchinfo.venue}|Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).| |{searchinfo.venue_type}|Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).| +|{photo}|Provides direct access to the PhotoInfo object for the photo. Must be used in format '{photo.property}' where 'property' represents a PhotoInfo property. 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.| ### Utility Functions diff --git a/osxphotos/_version.py b/osxphotos/_version.py index 772218c9..9d35c4ab 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,3 +1,3 @@ """ version info """ -__version__ = "0.41.10" +__version__ = "0.41.11" diff --git a/osxphotos/phototemplate.py b/osxphotos/phototemplate.py index d8082ade..de55c75a 100644 --- a/osxphotos/phototemplate.py +++ b/osxphotos/phototemplate.py @@ -146,6 +146,11 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = { "{searchinfo.activity}": "Activities associated with a photo, e.g. 'Sporting Event'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).", "{searchinfo.venue}": "Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).", "{searchinfo.venue_type}": "Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).", + "{photo}": "Provides direct access to the PhotoInfo object for the photo. " + + "Must be used in format '{photo.property}' where 'property' represents a PhotoInfo property. " + + "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.", } FILTER_VALUES = { @@ -363,7 +368,7 @@ class PhotoTemplate: if ts.template: # have a template field to process field = ts.template.field - if field not in FIELD_NAMES: + if field not in FIELD_NAMES and not field.startswith("photo"): unmatched.append(field) return [], unmatched @@ -443,7 +448,7 @@ class PhotoTemplate: vals = self.get_template_value_exiftool( subfield, filename=filename, dirname=dirname ) - elif field in MULTI_VALUE_SUBSTITUTIONS: + 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 ) @@ -894,6 +899,32 @@ class PhotoTemplate: values = ( self.photo.search_info.venue_types if self.photo.search_info else [] ) + elif field.startswith("photo"): + # provide access to PhotoInfo object + properties = field.split(".") + if len(properties) <= 1: + raise ValueError( + "Missing property in {photo} template. Use in form {photo.property}." + ) + obj = self.photo + for i in range(1, len(properties)): + property_ = properties[i] + try: + obj = getattr(obj, property_) + if obj is None: + break + except AttributeError: + raise ValueError( + "Invalid property for {photo} template: " + f"'{property_}'" + ) + if obj is None: + values = [] + elif isinstance(obj, bool): + values = [property_] if obj else [] + elif isinstance(obj, (str, int, float)): + values = [str(obj)] + else: + values = [val for val in obj] else: raise ValueError(f"Unhandled template value: {field}") diff --git a/tests/test_template.py b/tests/test_template.py index 2a19ddd3..ec8fad1d 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -256,6 +256,25 @@ COMMENT_UUID_DICT = { "4E4944A0-3E5C-4028-9600-A8709F2FA1DB": ["None: Nice trophy"], } +UUID_PHOTO = { + "DC99FBDD-7A52-4100-A5BB-344131646C30": { + "{photo.title}": ["St. James's Park"], + "{photo.favorite?FAVORITE,NOTFAVORITE}": ["NOTFAVORITE"], + "{photo.hdr}": ["_"], + "{photo.keywords}": [ + "England", + "London", + "London 2018", + "St. James's Park", + "UK", + "United Kingdom", + ], + }, + "3DD2C897-F19E-4CA6-8C22-B027D5A71907": {"{photo.place.country_code}": ["AU"]}, + "F12384F6-CD17-4151-ACBA-AE0E3688539E": {"{photo.place.name}": ["_"]}, + "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51": {"{photo.favorite}": ["favorite"]}, +} + @pytest.fixture(scope="module") def photosdb_places(): @@ -310,7 +329,7 @@ def test_lookup_multi(photosdb_places): for subst in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED: lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1) - if subst == "{exiftool}": + if subst in ["{exiftool}", "{photo}"]: continue lookup = template.get_template_value_multi(lookup_str, path_sep=os.path.sep) assert isinstance(lookup, list) @@ -879,3 +898,10 @@ def test_punctuation(photosdb): rendered, _ = photo.render_template("{" + punc + "}") assert rendered[0] == PUNCTUATION[punc] + +def test_photo_template(photosdb): + for uuid in UUID_PHOTO: + photo = photosdb.get_photo(uuid) + for template in UUID_PHOTO[uuid]: + rendered, _ = photo.render_template(template) + assert sorted(rendered) == sorted(UUID_PHOTO[uuid][template])