Added {function} template, #419
This commit is contained in:
15
README.md
15
README.md
@@ -858,7 +858,7 @@ e.g. If Photo is in Album1 in Folder1:
|
|||||||
• "{folder_album(>)}" renders to ["Folder1>Album1"]
|
• "{folder_album(>)}" renders to ["Folder1>Album1"]
|
||||||
• "{folder_album()}" renders to ["Folder1Album1"]
|
• "{folder_album()}" renders to ["Folder1Album1"]
|
||||||
|
|
||||||
[find|replace]: optional text replacement to perform on rendered template value.
|
[find,replace]: optional text replacement to perform on rendered template value.
|
||||||
For example, to replace "/" in an album name, you could use the template
|
For example, to replace "/" in an album name, you could use the template
|
||||||
"{album[/,-]}". Multiple replacements can be made by appending "|" and adding
|
"{album[/,-]}". Multiple replacements can be made by appending "|" and adding
|
||||||
another find|replace pair. e.g. to replace both "/" and ":" in album name:
|
another find|replace pair. e.g. to replace both "/" and ":" in album name:
|
||||||
@@ -1281,6 +1281,16 @@ Substitution Description
|
|||||||
https://rhettbull.github.io/osxphotos/ for additional
|
https://rhettbull.github.io/osxphotos/ for additional
|
||||||
documentation on the PhotoInfo class.
|
documentation on the PhotoInfo class.
|
||||||
|
|
||||||
|
{function} Execute a python function from an external file and
|
||||||
|
use return value as template substitution. Use in
|
||||||
|
format: {function:file.py::function_name} where
|
||||||
|
'file.py' is the name of the python file and
|
||||||
|
'function_name' is the name of the function to call.
|
||||||
|
The function will be passed the PhotoInfo object for
|
||||||
|
the photo. See https://github.com/RhetTbull/osxphotos
|
||||||
|
/blob/master/examples/template_function.py for an
|
||||||
|
example of how to implement a template function.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -2213,7 +2223,7 @@ e.g. If Photo is in `Album1` in `Folder1`:
|
|||||||
- `"{folder_album(>)}"` renders to `["Folder1>Album1"]`
|
- `"{folder_album(>)}"` renders to `["Folder1>Album1"]`
|
||||||
- `"{folder_album()}"` renders to `["Folder1Album1"]`
|
- `"{folder_album()}"` renders to `["Folder1Album1"]`
|
||||||
|
|
||||||
`[find|replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
|
`[find,replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
|
||||||
|
|
||||||
`conditional`: optional conditional expression that is evaluated as boolean (True/False) for use with the `?bool_value` modifier. Conditional expressions take the form '` not operator value`' where `not` is an optional modifier that negates the `operator`. Note: the space before the conditional expression is required if you use a conditional expression. Valid comparison operators are:
|
`conditional`: optional conditional expression that is evaluated as boolean (True/False) for use with the `?bool_value` modifier. Conditional expressions take the form '` not operator value`' where `not` is an optional modifier that negates the `operator`. Note: the space before the conditional expression is required if you use a conditional expression. Valid comparison operators are:
|
||||||
|
|
||||||
@@ -2892,6 +2902,7 @@ The following template field substitutions are availabe for use with `PhotoInfo.
|
|||||||
|{searchinfo.venue}|Venues associated with a photo, e.g. name of restaurant; (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).|
|
|{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.|
|
|{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.|
|
||||||
|
|{function}|Execute a python function from an external file and use return value as template substitution. Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. The function will be passed the PhotoInfo object for the photo. See https://github.com/RhetTbull/osxphotos/blob/master/examples/template_function.py for an example of how to implement a template function.|
|
||||||
<!-- OSXPHOTOS-TEMPLATE-TABLE:END -->
|
<!-- OSXPHOTOS-TEMPLATE-TABLE:END -->
|
||||||
|
|
||||||
### Utility Functions
|
### Utility Functions
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Sphinx build info version 1
|
# Sphinx build info version 1
|
||||||
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
|
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||||
config: 3a4409f9ef528dee2e4ce25074f0e795
|
config: 456ab9bf5a36a59a5d7b68a97d5f6583
|
||||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Overview: module code — osxphotos 0.42.00 documentation</title>
|
<title>Overview: module code — osxphotos 0.42.1 documentation</title>
|
||||||
<link rel="stylesheet" href="../_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="../_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="../_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="../_static/alabaster.css" type="text/css" />
|
||||||
<script id="documentation_options" data-url_root="../" src="../_static/documentation_options.js"></script>
|
<script id="documentation_options" data-url_root="../" src="../_static/documentation_options.js"></script>
|
||||||
|
|||||||
2
docs/_static/documentation_options.js
vendored
2
docs/_static/documentation_options.js
vendored
@@ -1,6 +1,6 @@
|
|||||||
var DOCUMENTATION_OPTIONS = {
|
var DOCUMENTATION_OPTIONS = {
|
||||||
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
||||||
VERSION: '0.42.00',
|
VERSION: '0.42.1',
|
||||||
LANGUAGE: 'None',
|
LANGUAGE: 'None',
|
||||||
COLLAPSE_INDEX: false,
|
COLLAPSE_INDEX: false,
|
||||||
BUILDER: 'html',
|
BUILDER: 'html',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>osxphotos command line interface (CLI) — osxphotos 0.42.00 documentation</title>
|
<title>osxphotos command line interface (CLI) — osxphotos 0.42.1 documentation</title>
|
||||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||||
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Index — osxphotos 0.42.00 documentation</title>
|
<title>Index — osxphotos 0.42.1 documentation</title>
|
||||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||||
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Welcome to osxphotos’s documentation! — osxphotos 0.42.00 documentation</title>
|
<title>Welcome to osxphotos’s documentation! — osxphotos 0.42.1 documentation</title>
|
||||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||||
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>osxphotos — osxphotos 0.42.00 documentation</title>
|
<title>osxphotos — osxphotos 0.42.1 documentation</title>
|
||||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||||
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
||||||
|
|||||||
Binary file not shown.
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>osxphotos package — osxphotos 0.42.00 documentation</title>
|
<title>osxphotos package — osxphotos 0.42.1 documentation</title>
|
||||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||||
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Search — osxphotos 0.42.00 documentation</title>
|
<title>Search — osxphotos 0.42.1 documentation</title>
|
||||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
""" Example showing how to use a custom function for osxphotos {function} template """
|
""" Example showing how to use a custom function for osxphotos {function} template
|
||||||
|
Use: osxphotos export /path/to/export --filename "{function:/path/to/template_function.py::example}"
|
||||||
|
|
||||||
|
You may place more than one template function in a single file as each is called by name using the {function:file.py::function_name} format
|
||||||
|
"""
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
from typing import List, Union
|
from typing import List, Union
|
||||||
@@ -7,7 +11,7 @@ import osxphotos
|
|||||||
|
|
||||||
|
|
||||||
def example(photo: osxphotos.PhotoInfo, **kwargs) -> Union[List, str]:
|
def example(photo: osxphotos.PhotoInfo, **kwargs) -> Union[List, str]:
|
||||||
""" example function for {function} template
|
""" example function for {function} template; adds suffix of # if photo has adjustments and ! if photo is a favorite
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
photo: osxphotos.PhotoInfo object
|
photo: osxphotos.PhotoInfo object
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.42.00"
|
__version__ = "0.42.1"
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ e.g. If Photo is in `Album1` in `Folder1`:
|
|||||||
- `"{folder_album(>)}"` renders to `["Folder1>Album1"]`
|
- `"{folder_album(>)}"` renders to `["Folder1>Album1"]`
|
||||||
- `"{folder_album()}"` renders to `["Folder1Album1"]`
|
- `"{folder_album()}"` renders to `["Folder1Album1"]`
|
||||||
|
|
||||||
`[find|replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
|
`[find,replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
|
||||||
|
|
||||||
`conditional`: optional conditional expression that is evaluated as boolean (True/False) for use with the `?bool_value` modifier. Conditional expressions take the form '` not operator value`' where `not` is an optional modifier that negates the `operator`. Note: the space before the conditional expression is required if you use a conditional expression. Valid comparison operators are:
|
`conditional`: optional conditional expression that is evaluated as boolean (True/False) for use with the `?bool_value` modifier. Conditional expressions take the form '` not operator value`' where `not` is an optional modifier that negates the `operator`. Note: the space before the conditional expression is required if you use a conditional expression. Valid comparison operators are:
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from ._constants import _UNKNOWN_PERSON
|
|||||||
from .datetime_formatter import DateTimeFormatter
|
from .datetime_formatter import DateTimeFormatter
|
||||||
from .exiftool import ExifTool
|
from .exiftool import ExifTool
|
||||||
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
|
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
|
||||||
|
from .utils import load_function
|
||||||
|
|
||||||
# ensure locale set to user's locale
|
# ensure locale set to user's locale
|
||||||
locale.setlocale(locale.LC_ALL, "")
|
locale.setlocale(locale.LC_ALL, "")
|
||||||
@@ -152,6 +153,10 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
|||||||
+ "For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. "
|
+ "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 "
|
+ "'{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.",
|
+ "the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.",
|
||||||
|
"{function}": "Execute a python function from an external file and use return value as template substitution. "
|
||||||
|
+ "Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. "
|
||||||
|
+ "The function will be passed the PhotoInfo object for the photo. "
|
||||||
|
+ "See https://github.com/RhetTbull/osxphotos/blob/master/examples/template_function.py for an example of how to implement a template function.",
|
||||||
}
|
}
|
||||||
|
|
||||||
FILTER_VALUES = {
|
FILTER_VALUES = {
|
||||||
@@ -463,6 +468,14 @@ class PhotoTemplate:
|
|||||||
vals = self.get_template_value_exiftool(
|
vals = self.get_template_value_exiftool(
|
||||||
subfield, filename=filename, dirname=dirname
|
subfield, filename=filename, dirname=dirname
|
||||||
)
|
)
|
||||||
|
elif field == "function":
|
||||||
|
if subfield is None:
|
||||||
|
raise ValueError(
|
||||||
|
"SyntaxError: filename and function must not be null with {function::filename.py:function_name}"
|
||||||
|
)
|
||||||
|
vals = self.get_template_value_function(
|
||||||
|
subfield, filename=filename, dirname=dirname
|
||||||
|
)
|
||||||
elif field in MULTI_VALUE_SUBSTITUTIONS or field.startswith("photo"):
|
elif field in MULTI_VALUE_SUBSTITUTIONS or field.startswith("photo"):
|
||||||
vals = self.get_template_value_multi(
|
vals = self.get_template_value_multi(
|
||||||
field, path_sep=path_sep, filename=filename, dirname=dirname
|
field, path_sep=path_sep, filename=filename, dirname=dirname
|
||||||
@@ -1064,6 +1077,39 @@ class PhotoTemplate:
|
|||||||
|
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
def get_template_value_function(self, subfield, filename=None, dirname=None):
|
||||||
|
"""Get template value from external function """
|
||||||
|
|
||||||
|
if "::" not in subfield:
|
||||||
|
raise ValueError(
|
||||||
|
f"SyntaxError: could not parse function name from '{subfield}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
filename, funcname = subfield.split("::")
|
||||||
|
|
||||||
|
print(filename, funcname)
|
||||||
|
|
||||||
|
if not pathlib.Path(filename).is_file():
|
||||||
|
raise ValueError(f"'{filename}' does not appear to be a file")
|
||||||
|
|
||||||
|
template_func = load_function(filename, funcname)
|
||||||
|
values = template_func(self.photo)
|
||||||
|
|
||||||
|
if not isinstance(values, (str, list)):
|
||||||
|
raise TypeError(
|
||||||
|
f"Invalid return type for function {funcname}: expected str or list"
|
||||||
|
)
|
||||||
|
if type(values) == str:
|
||||||
|
values = [values]
|
||||||
|
|
||||||
|
# sanitize directory names if needed
|
||||||
|
if filename:
|
||||||
|
values = [sanitize_pathpart(value) for value in values]
|
||||||
|
elif dirname:
|
||||||
|
values = [sanitize_dirname(value) for value in values]
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
def get_photo_video_type(self, default):
|
def get_photo_video_type(self, default):
|
||||||
""" return media type, e.g. photo or video """
|
""" return media type, e.g. photo or video """
|
||||||
default_dict = parse_default_kv(default, PHOTO_VIDEO_TYPE_DEFAULTS)
|
default_dict = parse_default_kv(default, PHOTO_VIDEO_TYPE_DEFAULTS)
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ Field:
|
|||||||
FIELD_WORD+
|
FIELD_WORD+
|
||||||
;
|
;
|
||||||
|
|
||||||
|
FIELD_WORD:
|
||||||
|
/[\.\w]+/
|
||||||
|
;
|
||||||
|
|
||||||
SubField:
|
SubField:
|
||||||
(
|
(
|
||||||
":"-
|
":"-
|
||||||
@@ -58,12 +62,8 @@ SubField:
|
|||||||
)?
|
)?
|
||||||
;
|
;
|
||||||
|
|
||||||
FIELD_WORD:
|
|
||||||
/[\.\w]+/
|
|
||||||
;
|
|
||||||
|
|
||||||
SUBFIELD_WORD:
|
SUBFIELD_WORD:
|
||||||
/[\.\w:]+/
|
/[\.\w:\/]+/
|
||||||
;
|
;
|
||||||
|
|
||||||
Filter:
|
Filter:
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
""" Utility functions used in osxphotos """
|
||||||
|
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import glob
|
import glob
|
||||||
|
import importlib
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -13,6 +16,7 @@ import sys
|
|||||||
import unicodedata
|
import unicodedata
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from plistlib import load as plistload
|
from plistlib import load as plistload
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
import CoreFoundation
|
import CoreFoundation
|
||||||
import CoreServices
|
import CoreServices
|
||||||
@@ -401,3 +405,28 @@ def increment_filename(filepath):
|
|||||||
count += 1
|
count += 1
|
||||||
dest = dest.parent / f"{dest_new}{dest.suffix}"
|
dest = dest.parent / f"{dest_new}{dest.suffix}"
|
||||||
return str(dest)
|
return str(dest)
|
||||||
|
|
||||||
|
|
||||||
|
def load_function(pyfile: str, function_name: str) -> Callable:
|
||||||
|
""" 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")
|
||||||
|
|
||||||
|
module_dir = module_file.parent or pathlib.Path(os.getcwd())
|
||||||
|
module_name = module_file.stem
|
||||||
|
|
||||||
|
# store old sys.path and ensure module_dir at beginning of path
|
||||||
|
syspath = sys.path
|
||||||
|
sys.path = [str(module_dir)] + syspath
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
func = getattr(module, function_name)
|
||||||
|
except AttributeError:
|
||||||
|
raise ValueError(f"'{function_name}' not found in module '{module_name}'")
|
||||||
|
finally:
|
||||||
|
# restore sys.path
|
||||||
|
sys.path = syspath
|
||||||
|
|
||||||
|
return func
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
20
tests/template_function.py
Normal file
20
tests/template_function.py
Normal 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"
|
||||||
@@ -381,7 +381,7 @@ def test_lookup_multi(photosdb_places):
|
|||||||
|
|
||||||
for subst in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED:
|
for subst in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED:
|
||||||
lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1)
|
lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1)
|
||||||
if subst in ["{exiftool}", "{photo}"]:
|
if subst in ["{exiftool}", "{photo}", "{function}"]:
|
||||||
continue
|
continue
|
||||||
lookup = template.get_template_value_multi(lookup_str, path_sep=os.path.sep)
|
lookup = template.get_template_value_multi(lookup_str, path_sep=os.path.sep)
|
||||||
assert isinstance(lookup, list)
|
assert isinstance(lookup, list)
|
||||||
@@ -965,3 +965,20 @@ def test_conditional(photosdb):
|
|||||||
for template in UUID_CONDITIONAL[uuid]:
|
for template in UUID_CONDITIONAL[uuid]:
|
||||||
rendered, _ = photo.render_template(template)
|
rendered, _ = photo.render_template(template)
|
||||||
assert sorted(rendered) == sorted(UUID_CONDITIONAL[uuid][template])
|
assert sorted(rendered) == sorted(UUID_CONDITIONAL[uuid][template])
|
||||||
|
|
||||||
|
|
||||||
|
def test_function(photosdb):
|
||||||
|
""" Test {function} """
|
||||||
|
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
|
||||||
|
rendered, _ = photo.render_template("{function:tests/template_function.py::foo}")
|
||||||
|
assert rendered == [f"{photo.original_filename}-FOO"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_function_bad(photosdb):
|
||||||
|
""" Test invalid {function} """
|
||||||
|
photo = photosdb.get_photo(UUID_MULTI_KEYWORDS)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
rendered, _ = photo.render_template(
|
||||||
|
"{function:tests/template_function.py::foobar}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user