Bug fix for template functions #477

This commit is contained in:
Rhet Turnbull 2021-06-23 22:36:58 -07:00
parent 5ea01df69b
commit 49317582c4
8 changed files with 99 additions and 45 deletions

View File

@ -1,3 +1,3 @@
""" version info """
__version__ = "0.42.45"
__version__ = "0.42.46"

View File

@ -57,7 +57,7 @@ from .photokit import check_photokit_authorization, request_photokit_authorizati
from .photosalbum import PhotosAlbum
from .phototemplate import PhotoTemplate, RenderOptions
from .queryoptions import QueryOptions
from .utils import get_preferred_uti_extension, load_function
from .utils import get_preferred_uti_extension, load_function, expand_and_validate_filepath
# global variable to control verbose output
# set via --verbose/-V
@ -172,13 +172,14 @@ class FunctionCall(click.ParamType):
filename, funcname = value.split("::")
if not pathlib.Path(filename).is_file():
filename_validated = expand_and_validate_filepath(filename)
if not filename_validated:
self.fail(f"'{filename}' does not appear to be a file")
try:
function = load_function(filename, funcname)
function = load_function(filename_validated, funcname)
except Exception as e:
self.fail(f"Could not load function {funcname} from {filename}")
self.fail(f"Could not load function {funcname} from {filename_validated}")
return (function, value)

View File

@ -4,8 +4,10 @@ import datetime
import locale
import os
import pathlib
import sys
import shlex
import sys
from dataclasses import dataclass
from typing import Optional
from textx import TextXSyntaxError, metamodel_from_file
@ -14,9 +16,7 @@ from ._version import __version__
from .datetime_formatter import DateTimeFormatter
from .exiftool import ExifToolCaching
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
from .utils import load_function
from dataclasses import dataclass
from typing import Optional
from .utils import expand_and_validate_filepath, load_function
# TODO: a lot of values are passed from function to function like path_sep--make these all class properties
@ -1177,10 +1177,11 @@ class PhotoTemplate:
filename, funcname = subfield.split("::")
if not pathlib.Path(filename).is_file():
filename_validated = expand_and_validate_filepath(filename)
if not filename_validated:
raise ValueError(f"'{filename}' does not appear to be a file")
template_func = load_function(filename, funcname)
template_func = load_function(filename_validated, funcname)
values = template_func(self.photo)
if not isinstance(values, (str, list)):
@ -1211,10 +1212,11 @@ class PhotoTemplate:
filename, funcname = filter_.split("::")
if not pathlib.Path(filename).is_file():
filename_validated = expand_and_validate_filepath(filename)
if not filename_validated:
raise ValueError(f"'{filename}' does not appear to be a file")
template_func = load_function(filename, funcname)
template_func = load_function(filename_validated, funcname)
if not isinstance(values, (list, tuple)):
values = [values]

View File

@ -63,7 +63,8 @@ SubField:
;
SUBFIELD_WORD:
/[\.\w:\/]+/
/[\.\w:\/\-\~\'\"\%\@\#\^\]+/
/\\\s/?
;
Filter:

View File

@ -46,7 +46,7 @@ def _get_logger():
def _set_debug(debug):
""" Enable or disable debug logging """
"""Enable or disable debug logging"""
global _DEBUG
_DEBUG = debug
if debug:
@ -56,18 +56,18 @@ def _set_debug(debug):
def _debug():
""" returns True if debugging turned on (via _set_debug), otherwise, false """
"""returns True if debugging turned on (via _set_debug), otherwise, false"""
return _DEBUG
def noop(*args, **kwargs):
""" do nothing (no operation) """
"""do nothing (no operation)"""
pass
def lineno(filename):
""" Returns string with filename and current line number in caller as '(filename): line_num'
Will trim filename to just the name, dropping path, if any. """
"""Returns string with filename and current line number in caller as '(filename): line_num'
Will trim filename to just the name, dropping path, if any."""
line = inspect.currentframe().f_back.f_lineno
filename = pathlib.Path(filename).name
return f"{filename}: {line}"
@ -92,14 +92,14 @@ def _get_os_version():
def _check_file_exists(filename):
""" returns true if file exists and is not a directory
otherwise returns false """
"""returns true if file exists and is not a directory
otherwise returns false"""
filename = os.path.abspath(filename)
return os.path.exists(filename) and not os.path.isdir(filename)
def _get_resource_loc(model_id):
""" returns folder_id and file_id needed to find location of edited photo """
"""returns folder_id and file_id needed to find location of edited photo"""
""" and live photos for version <= Photos 4.0 """
# determine folder where Photos stores edited version
# edited images are stored in:
@ -117,7 +117,7 @@ def _get_resource_loc(model_id):
def _dd_to_dms(dd):
""" convert lat or lon in decimal degrees (dd) to degrees, minutes, seconds """
"""convert lat or lon in decimal degrees (dd) to degrees, minutes, seconds"""
""" return tuple of int(deg), int(min), float(sec) """
dd = float(dd)
negative = dd < 0
@ -136,7 +136,7 @@ def _dd_to_dms(dd):
def dd_to_dms_str(lat, lon):
""" convert latitude, longitude in degrees to degrees, minutes, seconds as string """
"""convert latitude, longitude in degrees to degrees, minutes, seconds as string"""
""" lat: latitude in degrees """
""" lon: longitude in degrees """
""" returns: string tuple in format ("51 deg 30' 12.86\" N", "0 deg 7' 54.50\" W") """
@ -165,7 +165,7 @@ def dd_to_dms_str(lat, lon):
def get_system_library_path():
""" return the path to the system Photos library as string """
"""return the path to the system Photos library as string"""
""" only works on MacOS 10.15 """
""" on earlier versions, returns None """
_, major, _ = _get_os_version()
@ -190,8 +190,8 @@ def get_system_library_path():
def get_last_library_path():
""" returns the path to the last opened Photos library
If a library has never been opened, returns None """
"""returns the path to the last opened Photos library
If a library has never been opened, returns None"""
plist_file = pathlib.Path(
str(pathlib.Path.home())
+ "/Library/Containers/com.apple.Photos/Data/Library/Preferences/com.apple.Photos.plist"
@ -241,7 +241,7 @@ def get_last_library_path():
def list_photo_libraries():
""" returns list of Photos libraries found on the system """
"""returns list of Photos libraries found on the system"""
""" on MacOS < 10.15, this may omit some libraries """
# On 10.15, mdfind appears to find all libraries
@ -266,9 +266,9 @@ def list_photo_libraries():
def get_preferred_uti_extension(uti):
""" get preferred extension for a UTI type
uti: UTI str, e.g. 'public.jpeg'
returns: preferred extension as str or None if cannot be determined """
"""get preferred extension for a UTI type
uti: UTI str, e.g. 'public.jpeg'
returns: preferred extension as str or None if cannot be determined"""
# reference: https://developer.apple.com/documentation/coreservices/1442744-uttypecopypreferredtagwithclass?language=objc
with objc.autorelease_pool():
@ -288,8 +288,8 @@ def get_preferred_uti_extension(uti):
def findfiles(pattern, path_):
"""Returns list of filenames from path_ matched by pattern
shell pattern. Matching is case-insensitive.
If 'path_' is invalid/doesn't exist, returns []."""
shell pattern. Matching is case-insensitive.
If 'path_' is invalid/doesn't exist, returns []."""
if not os.path.isdir(path_):
return []
# See: https://gist.github.com/techtonik/5694830
@ -316,8 +316,8 @@ def findfiles(pattern, path_):
def _open_sql_file(dbname):
""" opens sqlite file dbname in read-only mode
returns tuple of (connection, cursor) """
"""opens sqlite file dbname in read-only mode
returns tuple of (connection, cursor)"""
try:
dbpath = pathlib.Path(dbname).resolve()
conn = sqlite3.connect(f"{dbpath.as_uri()}?mode=ro", timeout=1, uri=True)
@ -328,9 +328,9 @@ def _open_sql_file(dbname):
def _db_is_locked(dbname):
""" check to see if a sqlite3 db is locked
returns True if database is locked, otherwise False
dbname: name of database to test """
"""check to see if a sqlite3 db is locked
returns True if database is locked, otherwise False
dbname: name of database to test"""
# first, check to see if lock file exists, if so, assume the file is locked
lock_name = f"{dbname}.lock"
@ -381,7 +381,7 @@ def _db_is_locked(dbname):
def normalize_unicode(value):
""" normalize unicode data """
"""normalize unicode data"""
if value is not None:
if isinstance(value, (tuple, list)):
return tuple(unicodedata.normalize(UNICODE_FORMAT, v) for v in value)
@ -394,7 +394,7 @@ def normalize_unicode(value):
def increment_filename(filepath):
""" Return filename (1).ext, etc if filename.ext exists
"""Return filename (1).ext, etc if filename.ext exists
If file exists in filename's parent folder with same stem as filename,
add (1), (2), etc. until a non-existing filename is found.
@ -419,8 +419,22 @@ def increment_filename(filepath):
return str(dest)
def expand_and_validate_filepath(path: str) -> str:
"""validate and expand ~ in filepath, also un-escapes "\ "
Returns:
expanded path if path is valid file, else None
"""
path = re.sub(r"\\ ", " ", path)
path = pathlib.Path(path).expanduser()
if path.is_file():
return str(path)
return None
def load_function(pyfile: str, function_name: str) -> Callable:
""" Load function_name from python file pyfile """
"""Load function_name from python file pyfile"""
module_file = pathlib.Path(pyfile)
if not module_file.is_file():
raise FileNotFoundError(f"module {pyfile} does not appear to exist")

View File

@ -0,0 +1,3 @@
# Contents
This directory used by test_template.py for testing {function} templates with hyphenated directory names

View File

@ -0,0 +1,20 @@
""" Example showing how to use a custom function for osxphotos {function} template """
import pathlib
from typing import List, Union
import osxphotos
def foo(photo: osxphotos.PhotoInfo, **kwargs) -> Union[List, str]:
""" example function for {function} template
Args:
photo: osxphotos.PhotoInfo object
**kwargs: not currently used, placeholder to keep functions compatible with possible changes to {function}
Returns:
str or list of str of values that should be substituted for the {function} template
"""
return photo.original_filename + "-FOO"

View File

@ -82,7 +82,9 @@ TEMPLATE_VALUES_TITLE = {
"{title|titlecase}": ["Tulips Tied Together At A Flower Shop"],
"{title|upper}": ["TULIPS TIED TOGETHER AT A FLOWER SHOP"],
"{title|titlecase|lower|upper}": ["TULIPS TIED TOGETHER AT A FLOWER SHOP"],
"{title|titlecase|lower|upper|shell_quote}": ["'TULIPS TIED TOGETHER AT A FLOWER SHOP'"],
"{title|titlecase|lower|upper|shell_quote}": [
"'TULIPS TIED TOGETHER AT A FLOWER SHOP'"
],
"{title|upper|titlecase}": ["Tulips Tied Together At A Flower Shop"],
"{title|capitalize}": ["Tulips tied together at a flower shop"],
"{title[ ,_]}": ["Tulips_tied_together_at_a_flower_shop"],
@ -388,7 +390,9 @@ def test_lookup_multi(photosdb_places):
lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1)
if subst in ["{exiftool}", "{photo}", "{function}"]:
continue
lookup = template.get_template_value_multi(lookup_str, path_sep=os.path.sep, default=[])
lookup = template.get_template_value_multi(
lookup_str, path_sep=os.path.sep, default=[]
)
assert isinstance(lookup, list)
@ -975,6 +979,15 @@ def test_conditional(photosdb):
assert sorted(rendered) == sorted(UUID_CONDITIONAL[uuid][template])
def test_function_hyphen_dir(photosdb):
"""Test {function} with a hyphenated directory (#477)"""
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
rendered, _ = photo.render_template(
"{function:tests/hyphen-dir/template_function.py::foo}"
)
assert rendered == [f"{photo.original_filename}-FOO"]
def test_function(photosdb):
"""Test {function}"""
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)