Replaced template renderer with regex-based renderer

This commit is contained in:
Rhet Turnbull
2020-03-28 07:58:50 -07:00
parent f0d200435a
commit 427c4c0bc4
2 changed files with 167 additions and 101 deletions

View File

@@ -21,7 +21,7 @@ import osxphotos
from ._constants import _EXIF_TOOL_URL, _PHOTOS_5_VERSION from ._constants import _EXIF_TOOL_URL, _PHOTOS_5_VERSION
from ._version import __version__ from ._version import __version__
from .exiftool import get_exiftool_path from .exiftool import get_exiftool_path
from .template import render_filename_template, TEMPLATE_SUBSTITUTIONS from .template import render_filepath_template, TEMPLATE_SUBSTITUTIONS
from .utils import _copy_file, create_path_by_date from .utils import _copy_file, create_path_by_date
@@ -1403,7 +1403,7 @@ def export_photo(
date_created = photo.date.timetuple() date_created = photo.date.timetuple()
dest = create_path_by_date(dest, date_created) dest = create_path_by_date(dest, date_created)
elif directory: elif directory:
dirname, unmatched = render_filename_template(directory, photo) dirname, unmatched = render_filepath_template(directory, photo)
if unmatched: if unmatched:
click.echo( click.echo(
f"Possible unmatched substitution in template: {unmatched}", err=True f"Possible unmatched substitution in template: {unmatched}", err=True

View File

@@ -39,125 +39,185 @@ TEMPLATE_SUBSTITUTIONS = {
} }
def render_filename_template( def get_template_value(lookup, photo):
""" lookup: value to find a match for
photo: PhotoInfo object whose data will be used for value substitutions
returns: either the matching template value (which may be None)
raises: KeyError if no rule exists for lookup """
# must be a valid keyword
if lookup == "name":
return pathlib.Path(photo.filename).stem
if lookup == "original_name":
return pathlib.Path(photo.original_filename).stem
if lookup == "title":
return photo.title
if lookup == "descr":
return photo.description
if lookup == "created.date":
return DateTimeFormatter(photo.date).date
if lookup == "created.year":
return DateTimeFormatter(photo.date).year
if lookup == "created.yy":
return DateTimeFormatter(photo.date).yy
if lookup == "created.mm":
return DateTimeFormatter(photo.date).mm
if lookup == "created.month":
return DateTimeFormatter(photo.date).month
if lookup == "created.mon":
return DateTimeFormatter(photo.date).mon
if lookup == "created.doy":
return DateTimeFormatter(photo.date).doy
if lookup == "modified.date":
return (
DateTimeFormatter(photo.date_modified).date if photo.date_modified else None
)
if lookup == "modified.year":
return (
DateTimeFormatter(photo.date_modified).year if photo.date_modified else None
)
if lookup == "modified.yy":
return (
DateTimeFormatter(photo.date_modified).yy if photo.date_modified else None
)
if lookup == "modified.mm":
return (
DateTimeFormatter(photo.date_modified).mm if photo.date_modified else None
)
if lookup == "modified.month":
return (
DateTimeFormatter(photo.date_modified).month
if photo.date_modified
else None
)
if lookup == "modified.mon":
return (
DateTimeFormatter(photo.date_modified).mon if photo.date_modified else None
)
if lookup == "modified.doy":
return (
DateTimeFormatter(photo.date_modified).doy if photo.date_modified else None
)
if lookup == "place.name":
return photo.place.name if photo.place else None
if lookup == "place.names.country":
return (
photo.place.names.country[0]
if photo.place and photo.place.names.country
else None
)
if lookup == "place.names.state_province":
return (
photo.place.names.state_province[0]
if photo.place and photo.place.names.state_province
else None
)
if lookup == "place.names.city":
return (
photo.place.names.city[0]
if photo.place and photo.place.names.city
else None
)
if lookup == "place.names.area_of_interest":
return (
photo.place.names.area_of_interest[0]
if photo.place and photo.place.names.area_of_interest
else None
)
# if here, didn't get a match
raise KeyError(f"No rule for processing {lookup}")
def render_filepath_template(
template: str, photo: PhotoInfo, none_str: str = "_" template: str, photo: PhotoInfo, none_str: str = "_"
) -> Tuple[str, list]: ) -> Tuple[str, list]:
""" render a filename or directory template """ """ render a filename or directory template """
# pylint: disable=anomalous-backslash-in-string
regex = r"""(?<!\\)\{([^\\,}]+)(,{0,1}(([\w\-. ]+))?)\}"""
# pylint: disable=anomalous-backslash-in-string
unmatched_regex = r"(?<!\\)(\{[^\\,}]+\})"
# Explanation for regex:
# (?<!\\) Negative Lookbehind to skip escaped braces
# assert regex following does not match "\" preceeding "{"
# \{ Match the opening brace
# 1st Capturing Group ([^\\,}]+) Don't match "\", ",", or "}"
# 2nd Capturing Group (,?(([\w\-. ]+))?)
# ,{0,1} optional ","
# 3rd Capturing Group (([\w\-. ]+))?
# Matches the comma and any word characters after
# 4th Capturing Group ([\w\-. ]+)
# Matches just the characters after the comma
# \} Matches the closing brace
if type(template) is not str: if type(template) is not str:
raise TypeError(f"template must be type str, not {type(template)}") raise TypeError(f"template must be type str, not {type(template)}")
if type(photo) is not PhotoInfo: if type(photo) is not PhotoInfo:
raise TypeError(f"photo must be type osxphotos.PhotoInfo, not {type(photo)}") raise TypeError(f"photo must be type osxphotos.PhotoInfo, not {type(photo)}")
rendered = template def make_subst_function(photo, none_str):
original_name = pathlib.Path(photo.original_filename).stem """ returns: substitution function for use in re.sub """
current_name = pathlib.Path(photo.filename).stem # closure to capture photo, none_str in subst
created = DateTimeFormatter(photo.date) def subst(matchobj):
if photo.date_modified: groups = len(matchobj.groups())
modified = DateTimeFormatter(photo.date_modified) if groups == 4:
else: try:
modified = None val = get_template_value(matchobj.group(1), photo)
except KeyError:
return matchobj.group(0)
# make substitutions if val is None:
rendered = rendered.replace("{name}", current_name) return (
rendered = rendered.replace("{original_name}", original_name) matchobj.group(3) if matchobj.group(3) is not None else none_str
)
else:
return val
else:
raise ValueError(
f"Unexpected number of groups: expected 4, got {groups}"
)
title = photo.title if photo.title is not None else none_str return subst
rendered = rendered.replace("{title}", f"{title}")
descr = photo.description if photo.description is not None else none_str subst_func = make_subst_function(photo, none_str)
rendered = rendered.replace("{descr}", f"{descr}")
rendered = rendered.replace("{created.date}", photo.date.date().isoformat()) # do the replacements
rendered = rendered.replace("{created.year}", created.year) rendered = re.sub(regex, subst_func, template)
rendered = rendered.replace("{created.yy}", created.yy)
rendered = rendered.replace("{created.mm}", created.mm)
rendered = rendered.replace("{created.month}", created.month)
rendered = rendered.replace("{created.mon}", created.mon)
rendered = rendered.replace("{created.doy}", created.doy)
if modified is not None: # find any {words} that weren't replaced
rendered = rendered.replace( unmatched = re.findall(unmatched_regex, rendered)
"{modified.date}", photo.date_modified.date().isoformat()
)
rendered = rendered.replace("{modified.year}", modified.year)
rendered = rendered.replace("{modified.yy}", modified.yy)
rendered = rendered.replace("{modified.mm}", modified.mm)
rendered = rendered.replace("{modified.month}", modified.month)
rendered = rendered.replace("{modified.mon}", modified.mon)
rendered = rendered.replace("{modified.doy}", modified.doy)
else:
rendered = rendered.replace("{modified.year}", none_str)
rendered = rendered.replace("{modified.yy}", none_str)
rendered = rendered.replace("{modified.mm}", none_str)
rendered = rendered.replace("{modified.month}", none_str)
rendered = rendered.replace("{modified.mon}", none_str)
rendered = rendered.replace("{modified.doy}", none_str)
place_name = photo.place.name if photo.place and photo.place.name else none_str
rendered = rendered.replace("{place.name}", place_name)
# place_names = (
# "_".join(photo.place.names) if photo.place and photo.place.names else none_str
# )
# rendered = rendered.replace("{place.names}", place_names)
address = (
photo.place.address_str if photo.place and photo.place.address_str else none_str
)
rendered = rendered.replace("{place.address}", address)
street = (
photo.place.address.street
if photo.place and photo.place.address.street
else none_str
)
rendered = rendered.replace("{place.street}", street)
city = (
photo.place.address.city
if photo.place and photo.place.address.city
else none_str
)
rendered = rendered.replace("{place.city}", city)
state_province = (
photo.place.address.state_province
if photo.place and photo.place.address.state_province
else none_str
)
rendered = rendered.replace("{place.state_province}", state_province)
postal_code = (
photo.place.address.postal_code
if photo.place and photo.place.address.postal_code
else none_str
)
rendered = rendered.replace("{place.postal_code}", postal_code)
country = (
photo.place.address.country
if photo.place and photo.place.address.country
else none_str
)
rendered = rendered.replace("{place.country}", country)
country_code = (
photo.place.country_code
if photo.place and photo.place.country_code
else none_str
)
rendered = rendered.replace("{place.country_code}", country_code)
# fix any escaped curly braces # fix any escaped curly braces
rendered = re.sub(r"\\{", "{", rendered) rendered = re.sub(r"\\{", "{", rendered)
rendered = re.sub(r"\\}", "}", rendered) rendered = re.sub(r"\\}", "}", rendered)
# find any {words} that weren't replaced return rendered, unmatched
unmatched = re.findall(r"{\w+}", rendered)
return (rendered, unmatched)
class DateTimeFormatter: class DateTimeFormatter:
@@ -166,6 +226,12 @@ class DateTimeFormatter:
def __init__(self, dt: datetime.datetime): def __init__(self, dt: datetime.datetime):
self.dt = dt self.dt = dt
@property
def date(self):
""" ISO date in form 2020-03-22 """
date = self.dt.date().isoformat()
return date
@property @property
def year(self): def year(self):
""" 4 digit year """ """ 4 digit year """