More work on phototemplate.py to add inline expansion

This commit is contained in:
Rhet Turnbull
2020-11-08 09:10:09 -08:00
parent 8c36c6712a
commit a6231e29ff
4 changed files with 133 additions and 55 deletions

View File

@@ -158,20 +158,21 @@ class ExportCommand(click.Command):
formatter.write("\n\n")
formatter.write_text("** Templating System **")
formatter.write("\n")
formatter.write_text("Several options, such as --directory, allow you to specify a template "
+ "which will be rendered to substitute template fields with values from the photo. "
+ "For example, '{created.month}' would be replaced with the month name of the photo creation date. "
+ "e.g. 'November'. "
+ "The general format for a template is '{TEMPLATE_FIELD[,[DEFAULT]]}'. "
+ "The ',' and DEFAULT value are optional. "
+ "If TEMPLATE_FIELD results in a null (empty) value, the default is '_'. "
+ "You may specify an alternate default value by appending ',DEFAULT' after template_field. "
+ "e.g. '{title,no_title}' would result in 'no_title' if the photo had no title. "
+ "You may include other text in the template string outside the {} and use more than "
+ "one template field, e.g. '{created.year} - {created.month}' (e.g. '2020 - November'). "
+ "Some template fields such as 'hdr' are boolean and resolve to True or False. "
+ "These take the form: '{TEMPLATE_FIELD?VALUE_IF_TRUE,VALUE_IF_FALSE}', e.g. "
+ "'{hdr?is_hdr,not_hdr}'."
formatter.write_text(
"Several options, such as --directory, allow you to specify a template "
+ "which will be rendered to substitute template fields with values from the photo. "
+ "For example, '{created.month}' would be replaced with the month name of the photo creation date. "
+ "e.g. 'November'. "
+ "The general format for a template is '{TEMPLATE_FIELD[,[DEFAULT]]}'. "
+ "The ',' and DEFAULT value are optional. "
+ "If TEMPLATE_FIELD results in a null (empty) value, the default is '_'. "
+ "You may specify an alternate default value by appending ',DEFAULT' after template_field. "
+ "e.g. '{title,no_title}' would result in 'no_title' if the photo had no title. "
+ "You may include other text in the template string outside the {} and use more than "
+ "one template field, e.g. '{created.year} - {created.month}' (e.g. '2020 - November'). "
+ "Some template fields such as 'hdr' are boolean and resolve to True or False. "
+ "These take the form: '{TEMPLATE_FIELD?VALUE_IF_TRUE,VALUE_IF_FALSE}', e.g. "
+ "'{hdr?is_hdr,not_hdr}'."
)
formatter.write("\n")
formatter.write_text(
@@ -2330,7 +2331,9 @@ def export_photo(
# requested edited version but it's missing, download original
export_original = True
export_edited = False
verbose(f"Edited file for {photo.original_filename} is missing, exporting original")
verbose(
f"Edited file for {photo.original_filename} is missing, exporting original"
)
filenames = get_filenames_from_template(photo, filename_template, original_name)
for filename in filenames:

View File

@@ -1,4 +1,4 @@
""" version info """
__version__ = "0.36.11"
__version__ = "0.36.12"

View File

@@ -198,7 +198,7 @@ class PhotoTemplate:
# regex to find {template_field,optional_default} in strings
# for explanation of regex see https://regex101.com/r/YFpWsn/1
# pylint: disable=anomalous-backslash-in-string
regex = r"(?<!\{)\{([^}]*\+)?([^\\,}+\?]+)(\?[^\\,}]*)?(,{0,1}([\w\=\;\-\%. ]+)?)(?=\}(?!\}))\}"
regex = r"(?<!\{)\{([^}]*\+)?([^\\,}+\?]+)(\?[^\\,}]*)?(,[\w\=\;\-\%. ]*)?(?=\}(?!\}))\}"
if type(template) is not str:
raise TypeError(f"template must be type str, not {type(template)}")
@@ -219,18 +219,21 @@ class PhotoTemplate:
# closure to capture photo, none_str, filename, dirname in subst
def subst(matchobj):
groups = len(matchobj.groups())
if groups == 5:
if groups == 4:
delim = matchobj.group(1)
field = matchobj.group(2)
bool_val = matchobj.group(3)
default = matchobj.group(4)
default_val = matchobj.group(5)
if bool_val is not None:
# drop the ?
bool_val = bool_val[1:]
# drop the comma on default
default_val = default[1:] if default is not None else None
# drop the '+' on delim
delim = delim[:-1] if delim is not None else None
# drop the ? on bool_val
bool_val = bool_val[1:] if bool_val is not None else None
try:
val = get_func(field, default_val, bool_val)
val = get_func(field, default_val, bool_val, delim)
except ValueError:
return matchobj.group(0)
@@ -281,9 +284,12 @@ class PhotoTemplate:
for field in MULTI_VALUE_SUBSTITUTIONS:
# Build a regex that matches only the field being processed
re_str = (
r"(?<!\{)\{([^}]*\+)?("
+ field
+ r")(\?[^\\,}]*)?(,{0,1}([\w\=\;\-\%. ]+)?)(?=\}(?!\}))\}"
r"(?<!\{)\{"
+ r"([^}]*\+)?" # group 1, optional delim/expand in place
+ r"("
+ field # group 2 (field name)
+ r")"
+ r"(\?[^\\,}]*)?(,[\w\=\;\-\%. ]*)?(?=\}(?!\}))\}"
)
regex_multi = re.compile(re_str)
@@ -291,7 +297,8 @@ class PhotoTemplate:
new_strings = {}
for str_template in rendered_strings:
if regex_multi.search(str_template):
matches = regex_multi.search(str_template)
if matches:
values = self.get_template_value_multi(
field,
path_sep,
@@ -299,12 +306,15 @@ class PhotoTemplate:
dirname=dirname,
replacement=replacement,
)
if expand_inplace:
if expand_inplace or matches.group(1) is not None:
delim = (
matches.group(1)[:-1]
if matches.group(1) is not None
else inplace_sep
)
# instead of returning multiple strings, join values into a single string
val = (
inplace_sep.join(sorted(values))
if values and values[0]
else None
delim.join(sorted(values)) if values and values[0] else None
)
def lookup_template_value_multi(lookup_value, *_):
@@ -374,7 +384,14 @@ class PhotoTemplate:
return rendered_strings, unmatched
def get_template_value(
self, field, default, bool_val=None, filename=False, dirname=False, replacement=":"
self,
field,
default,
bool_val=None,
delim=None,
filename=False,
dirname=False,
replacement=":",
):
"""lookup value for template field (single-value template substitutions)
@@ -741,13 +758,13 @@ class PhotoTemplate:
else:
return default_dict["photo"]
def get_photo_hdr(self, default, bool_val):
if self.photo.hdr:
return bool_val
else:
return default
def parse_default_kv(default, default_dict):
""" parse a string in form key1=value1;key2=value2,... as used for some template fields

View File

@@ -4,8 +4,7 @@ import pytest
PHOTOS_DB_PLACES = (
"./tests/Test-Places-Catalina-10_15_7.photoslibrary/database/photos.db"
)
PHOTOS_DB_15_1 = "./tests/Test-10.15.1.photoslibrary/database/photos.db"
PHOTOS_DB_15_4 = "./tests/Test-10.15.4.photoslibrary/database/photos.db"
PHOTOS_DB_15_7 = "./tests/Test-10.15.7.photoslibrary/database/photos.db"
PHOTOS_DB_14_6 = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
PHOTOS_DB_COMMENTS = "tests/Test-Cloud-10.15.6.photoslibrary"
PHOTOS_DB_CLOUD = "./tests/Test-Cloud-10.15.6.photoslibrary/database/photos.db"
@@ -35,6 +34,23 @@ UUID_MEDIA_TYPE = {
"burst": None,
}
# multi keywords
UUID_MULTI_KEYWORDS = "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4"
TEMPLATE_VALUES_MULTI_KEYWORDS = {
"{keyword}": ["flowers", "wedding"],
"{+keyword}": ["flowerswedding"],
"{;+keyword}": ["flowers;wedding"],
"{; +keyword}": ["flowers; wedding"],
}
UUID_TITLE = "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4"
TEMPLATE_VALUES_TITLE = {
"{title}": ["Tulips tied together at a flower shop"],
"{+title}": ["Tulips tied together at a flower shop"],
"{,+title}": ["Tulips tied together at a flower shop"],
"{, +title}": ["Tulips tied together at a flower shop"],
}
# Boolean type values that render to True
UUID_BOOL_VALUES = {"hdr": "D11D25FF-5F31-47D2-ABA9-58418878DC15"}
@@ -337,7 +353,7 @@ def test_subst_multi_1_1_2():
# one album, one keyword, two persons
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_7)
photo = photosdb.photos(uuid=[UUID_DICT["1_1_2"]])[0]
template = "{created.year}/{album}/{keyword}/{person}"
@@ -351,16 +367,12 @@ def test_subst_multi_2_1_1():
# 2 albums, 1 keyword, 1 person
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_7)
# one album, one keyword, two persons
photo = photosdb.photos(uuid=[UUID_DICT["2_1_1"]])[0]
template = "{created.year}/{album}/{keyword}/{person}"
expected = [
"2018/Pumpkin Farm/Kids/Katie",
"2018/Test Album/Kids/Katie",
"2018/Multi Keyword/Kids/Katie",
]
expected = ["2018/Pumpkin Farm/Kids/Katie", "2018/Test Album/Kids/Katie"]
rendered, _ = photo.render_template(template)
assert sorted(rendered) == sorted(expected)
@@ -370,7 +382,7 @@ def test_subst_multi_2_1_1_single():
# 2 albums, 1 keyword, 1 person but only do keywords
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_7)
# one album, one keyword, two persons
photo = photosdb.photos(uuid=[UUID_DICT["2_1_1"]])[0]
@@ -385,7 +397,7 @@ def test_subst_multi_0_2_0():
# 0 albums, 2 keywords, 0 persons
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_7)
# one album, one keyword, two persons
photo = photosdb.photos(uuid=[UUID_DICT["0_2_0"]])[0]
@@ -400,7 +412,7 @@ def test_subst_multi_0_2_0_single():
# 0 albums, 2 keywords, 0 persons, but only do albums
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_7)
# one album, one keyword, two persons
photo = photosdb.photos(uuid=[UUID_DICT["0_2_0"]])[0]
@@ -415,7 +427,7 @@ def test_subst_multi_0_2_0_default_val():
# 0 albums, 2 keywords, 0 persons, default vals provided
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_7)
# one album, one keyword, two persons
photo = photosdb.photos(uuid=[UUID_DICT["0_2_0"]])[0]
@@ -430,7 +442,7 @@ def test_subst_multi_0_2_0_default_val_unknown_val():
# 0 albums, 2 keywords, 0 persons, default vals provided, unknown val in template
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_7)
# one album, one keyword, two persons
photo = photosdb.photos(uuid=[UUID_DICT["0_2_0"]])[0]
@@ -451,7 +463,7 @@ def test_subst_multi_0_2_0_default_val_unknown_val_2():
# 0 albums, 2 keywords, 0 persons, default vals provided, unknown val in template
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_7)
# one album, one keyword, two persons
photo = photosdb.photos(uuid=[UUID_DICT["0_2_0"]])[0]
@@ -469,12 +481,16 @@ def test_subst_multi_folder_albums_1():
""" Test substitutions for folder_album are correct """
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_4)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_7)
# photo in an album in a folder
photo = photosdb.photos(uuid=[UUID_DICT["folder_album_1"]])[0]
template = "{folder_album}"
expected = ["Folder1/SubFolder2/AlbumInFolder"]
expected = [
"2018-10 - Sponsion, Museum, Frühstück, Römermuseum",
"2019-10/11 Paris Clermont",
"Folder1/SubFolder2/AlbumInFolder",
]
rendered, unknown = photo.render_template(template)
assert sorted(rendered) == sorted(expected)
assert unknown == []
@@ -484,7 +500,7 @@ def test_subst_multi_folder_albums_2():
""" Test substitutions for folder_album are correct """
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_4)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_7)
# photo in an album in a folder
photo = photosdb.photos(uuid=[UUID_DICT["folder_album_no_folder"]])[0]
@@ -530,7 +546,7 @@ 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)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_7)
# one album, one keyword, two persons
photo = photosdb.photos(uuid=[UUID_DICT["1_1_2"]])[0]
@@ -544,7 +560,7 @@ 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)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_7)
# one album, one keyword, two persons
photo = photosdb.photos(uuid=[UUID_DICT["1_1_2"]])[0]
@@ -558,7 +574,7 @@ 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)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_7)
# one album, one keyword, two persons
photo = photosdb.photos(uuid=[UUID_DICT["1_1_2"]])[0]
@@ -630,3 +646,45 @@ def test_bool_values_not():
photo = photosdb.get_photo(uuid)
rendered, _ = photo.render_template("{" + f"{field}" + "?True,False}")
assert rendered[0] == "False"
def test_partial_match():
""" test that template successfully rejects a field that is superset of valid field """
import osxphotos
photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD)
for uuid in COMMENT_UUID_DICT:
photo = photosdb.get_photo(uuid)
rendered, notmatched = photo.render_template("{keywords}")
assert [rendered, notmatched] == [["{keywords}"], ["keywords"]]
rendered, notmatched = photo.render_template("{keywords,}")
assert [rendered, notmatched] == [["{keywords,}"], ["keywords"]]
rendered, notmatched = photo.render_template("{keywords,foo}")
assert [rendered, notmatched] == [["{keywords,foo}"], ["keywords"]]
rendered, notmatched = photo.render_template("{,+keywords,foo}")
assert [rendered, notmatched] == [["{,+keywords,foo}"], ["keywords"]]
def test_expand_in_place_with_delim():
""" Test that substitutions are correct when {DELIM+FIELD} format used """
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_7)
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
for template in TEMPLATE_VALUES_MULTI_KEYWORDS:
rendered, _ = photo.render_template(template)
assert sorted(rendered) == sorted(TEMPLATE_VALUES_MULTI_KEYWORDS[template])
def test_expand_in_place_with_delim_single_value():
""" Test that single-value substitutions are correct when {DELIM+FIELD} format used """
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_7)
photo = photosdb.get_photo(UUID_TITLE)
for template in TEMPLATE_VALUES_TITLE:
rendered, _ = photo.render_template(template)
assert sorted(rendered) == sorted(TEMPLATE_VALUES_TITLE[template])