Added {filepath} template field in prep for --post-command and other goodies

This commit is contained in:
Rhet Turnbull
2021-06-13 18:40:45 -07:00
parent 2cdec3fc78
commit c0bd0ffc9f
9 changed files with 173 additions and 37 deletions

View File

@@ -1505,7 +1505,7 @@ Substitution Description
{lf} A line feed: '\n', alias for {newline}
{cr} A carriage return: '\r'
{crlf} a carriage return + line feed: '\r\n'
{osxphotos_version} The osxphotos version, e.g. '0.42.31'
{osxphotos_version} The osxphotos version, e.g. '0.42.32'
{osxphotos_cmd_line} The full command line used to run osxphotos
The following substitutions may result in multiple values. Thus if specified for
@@ -1574,19 +1574,26 @@ Substitution Description
/blob/master/examples/template_function.py for an
example of how to implement a template function.
The following substitutions are 'path-like'. You can access various parts of the
path using the following modifiers:
The following substitutions are file or directory paths. You can access various
parts of the path using the following modifiers:
{field.parent}: the parent directory
{field.name}: the name of the file or final sub-directory
{field.stem}: the name of the file without the extension
{field.suffix}: the suffix of the file including the leading '.'
{path.parent}: the parent directory
{path.name}: the name of the file or final sub-directory
{path.stem}: the name of the file without the extension
{path.suffix}: the suffix of the file including the leading '.'
For example, if the field {export_dir} is '/Shared/Backup/Photos',
For example, if the field {export_dir} is '/Shared/Backup/Photos':
{export_dir.parent} is '/Shared/Backup'
If the field {filepath} is '/Shared/Backup/Photos/IMG_1234.JPG':
{filepath.parent} is '/Shared/Backup/Photos'
{filepath.name} is 'IMG_1234.JPG'
{filepath.stem} is 'IMG_1234'
{filepath.suffix} is '.JPG'
Substitution Description
{export_dir} The full path to the export directory
{filepath} The full path to the exported file
```
@@ -3216,7 +3223,7 @@ The following template field substitutions are availabe for use the templating s
|{lf}|A line feed: '\n', alias for {newline}|
|{cr}|A carriage return: '\r'|
|{crlf}|a carriage return + line feed: '\r\n'|
|{osxphotos_version}|The osxphotos version, e.g. '0.42.31'|
|{osxphotos_version}|The osxphotos version, e.g. '0.42.32'|
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|{album}|Album(s) photo is contained in|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.42.32"
__version__ = "0.42.33"

View File

@@ -1630,6 +1630,7 @@ def export(
jpeg_ext=jpeg_ext,
replace_keywords=replace_keywords,
retry=retry,
export_dir=dest,
)
if album_export and export_results.exported:
@@ -2251,6 +2252,7 @@ def export_photo(
jpeg_ext=None,
replace_keywords=False,
retry=0,
export_dir=None,
):
"""Helper function for export that does the actual export
@@ -2291,6 +2293,7 @@ def export_photo(
jpeg_ext: if not None, specify the extension to use for all JPEG images on export
replace_keywords: if True, --keyword-template replaces keywords instead of adding keywords
retry: retry up to retry # of times if there's an error
export_dir: top-level export directory for {export_dir} template
Returns:
list of path(s) of exported photo or None if photo was missing
@@ -2449,6 +2452,7 @@ def export_photo(
jpeg_ext=jpeg_ext,
replace_keywords=replace_keywords,
retry=retry,
export_dir=export_dir,
)
if export_edited and photo.hasadjustments:
@@ -2553,6 +2557,7 @@ def export_photo(
jpeg_ext=jpeg_ext,
replace_keywords=replace_keywords,
retry=retry,
export_dir=export_dir,
)
return results
@@ -2597,6 +2602,7 @@ def export_photo_with_template(
jpeg_ext,
replace_keywords,
retry,
export_dir,
):
"""Evaluate directory template then export photo to each directory"""
@@ -2647,6 +2653,8 @@ def export_photo_with_template(
results.missing.append(str(pathlib.Path(dest_path) / filename))
continue
render_options = RenderOptions(export_dir=export_dir)
tries = 0
while tries <= retry:
tries += 1
@@ -2684,6 +2692,7 @@ def export_photo_with_template(
exiftool_flags=exiftool_option,
jpeg_ext=jpeg_ext,
replace_keywords=replace_keywords,
render_options=render_options,
)
for warning_ in export_results.exiftool_warning:
verbose_(f"exiftool warning for file {warning_[0]}: {warning_[1]}")

View File

@@ -163,21 +163,27 @@ The following attributes may be used with '--xattr-template':
formatter.write("\n")
formatter.write_text(
"The following substitutions are 'path-like'. "
"The following substitutions are file or directory paths. "
+ "You can access various parts of the path using the following modifiers:"
)
formatter.write("\n")
formatter.write("{field.parent}: the parent directory\n")
formatter.write("{field.name}: the name of the file or final sub-directory\n")
formatter.write("{field.stem}: the name of the file without the extension\n")
formatter.write("{path.parent}: the parent directory\n")
formatter.write("{path.name}: the name of the file or final sub-directory\n")
formatter.write("{path.stem}: the name of the file without the extension\n")
formatter.write(
"{field.suffix}: the suffix of the file including the leading '.'\n"
"{path.suffix}: the suffix of the file including the leading '.'\n"
)
formatter.write("\n")
formatter.write_text(
"For example, if the field {export_dir} is '/Shared/Backup/Photos', "
+ "{export_dir.parent} is '/Shared/Backup'"
)
formatter.write(
"For example, if the field {export_dir} is '/Shared/Backup/Photos':\n")
formatter.write("{export_dir.parent} is '/Shared/Backup'\n")
formatter.write("\n")
formatter.write(
"If the field {filepath} is '/Shared/Backup/Photos/IMG_1234.JPG':\n")
formatter.write("{filepath.parent} is '/Shared/Backup/Photos'\n")
formatter.write("{filepath.name} is 'IMG_1234.JPG'\n")
formatter.write("{filepath.stem} is 'IMG_1234'\n")
formatter.write("{filepath.suffix} is '.JPG'\n")
formatter.write("\n")
templ_tuples = [("Substitution", "Description")]
templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS_PATHLIB.items())

View File

@@ -15,6 +15,7 @@
# TODO: should this be its own PhotoExporter class?
# TODO: the various sidecar_json, sidecar_xmp, etc args should all be collapsed to a sidecar param using a bit mask
import dataclasses
import glob
import hashlib
import json
@@ -24,6 +25,7 @@ import pathlib
import re
import tempfile
from collections import namedtuple # pylint: disable=syntax-error
from typing import Optional
import photoscript
from mako.template import Template
@@ -391,7 +393,7 @@ def export(
use_persons_as_keywords=False,
keyword_template=None,
description_template=None,
export_dir=None,
render_options: Optional[RenderOptions] = None,
):
"""export photo
dest: must be valid destination path (or exception raised)
@@ -429,7 +431,7 @@ def export(
when exporting metadata with exiftool or sidecar
keyword_template: (list of strings); list of template strings that will be rendered as used as keywords
description_template: string; optional template string that will be rendered for use as photo description
export_dir: value to use for {export_dir} template
render_options: an optional osxphotos.phototemplate.RenderOptions instance with options to pass to template renderer
Returns: list of photos exported
"""
@@ -461,7 +463,7 @@ def export(
use_persons_as_keywords=use_persons_as_keywords,
keyword_template=keyword_template,
description_template=description_template,
export_dir=export_dir,
render_options = render_options,
)
return results.exported
@@ -504,7 +506,7 @@ def export2(
persons=True,
location=True,
replace_keywords=False,
export_dir=None,
render_options: Optional[RenderOptions] = None
):
"""export photo, like export but with update and dry_run options
dest: must be valid destination path or exception raised
@@ -560,7 +562,7 @@ def export2(
persons: if True, include persons in exported metadata
location: if True, include location in exported metadata
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
export_dir: value to use for {export_dir} template
render_options: optional osxphotos.phototemplate.RenderOptions instance to specify options for rendering templates
Returns: ExportResults class
ExportResults has attributes:
@@ -602,6 +604,8 @@ def export2(
if verbose is None:
verbose = self._verbose
self._render_options = render_options or RenderOptions()
# suffix to add to edited files
# e.g. name will be filename_edited.jpg
edited_identifier = "_edited"
@@ -685,6 +689,7 @@ def export2(
f"destination exists ({dest}); overwrite={overwrite}, increment={increment}"
)
self._render_options.filepath = str(dest)
all_results = ExportResults()
if not use_photos_export:
# find the source file on disk and export
@@ -1598,7 +1603,7 @@ def _exiftool_dict(
)
if description_template is not None:
options = RenderOptions(expand_inplace=True, inplace_sep=", ")
options = dataclasses.replace(self._render_options, expand_inplace=True, inplace_sep=", ")
rendered = self.render_template(description_template, options)[0]
description = " ".join(rendered) if rendered else ""
exif["EXIF:ImageDescription"] = description
@@ -1637,7 +1642,7 @@ def _exiftool_dict(
if keyword_template:
rendered_keywords = []
options = RenderOptions(none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/")
options = dataclasses.replace(self._render_options, none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/")
for template_str in keyword_template:
rendered, unmatched = self.render_template(template_str, options)
if unmatched:
@@ -1915,7 +1920,7 @@ def _xmp_sidecar(
extension = extension.suffix[1:] if extension.suffix else None
if description_template is not None:
options = RenderOptions(expand_inplace=True, inplace_sep=", ")
options = dataclasses.replace(self._render_options, expand_inplace=True, inplace_sep=", ")
rendered = self.render_template(description_template, options)[0]
description = " ".join(rendered) if rendered else ""
else:
@@ -1948,7 +1953,7 @@ def _xmp_sidecar(
if keyword_template:
rendered_keywords = []
options = RenderOptions(none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/")
options = dataclasses.replace(self._render_options, none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/")
for template_str in keyword_template:
rendered, unmatched = self.render_template(template_str, options)
if unmatched:

View File

@@ -78,6 +78,9 @@ class PhotoInfo:
self._db = db
self._verbose = self._db._verbose
# TODO: remove this once refactor of PhotoExporter is done
self._render_options = RenderOptions()
@property
def filename(self):
"""filename of the picture"""

View File

@@ -146,6 +146,7 @@ TEMPLATE_SUBSTITUTIONS = {
TEMPLATE_SUBSTITUTIONS_PATHLIB = {
"{export_dir}": "The full path to the export directory",
"{filepath}": "The full path to the exported file",
}
# Permitted multi-value substitutions (each of these returns None or 1 or more values)
@@ -504,9 +505,7 @@ class PhotoTemplate:
path_sep=path_sep,
)
elif field.split(".")[0] in PATHLIB_SUBSTITUTIONS:
vals = self.get_template_value_pathlib(
field,
)
vals = self.get_template_value_pathlib(field)
else:
unmatched.append(field)
return [], unmatched
@@ -931,10 +930,7 @@ class PhotoTemplate:
return [value]
def get_template_value_pathlib(
self,
field,
):
def get_template_value_pathlib(self, field):
"""lookup value for template pathlib template fields
Args:
@@ -946,10 +942,17 @@ class PhotoTemplate:
Raises:
ValueError if no rule exists for field.
"""
if field.split(".")[0] not in PATHLIB_SUBSTITUTIONS:
field_stem = field.split(".")[0]
if field_stem not in PATHLIB_SUBSTITUTIONS:
raise ValueError(f"SyntaxError: Unknown field: {field}")
value = _get_pathlib_value(field, self.export_dir)
field_value = None
try:
field_value = getattr(self, field_stem)
except AttributeError:
raise ValueError(f"Unknown path-like field: {field_stem}")
value = _get_pathlib_value(field, field_value)
if self.filename:
value = sanitize_pathpart(value)

View File

@@ -473,6 +473,7 @@ CLI_UUID_DICT_15_7 = {
}
CLI_TEMPLATE_SIDECAR_FILENAME = "Pumkins1.jpg.json"
CLI_TEMPLATE_FILENAME = "Pumkins1.jpg"
CLI_UUID_DICT_14_6 = {"intrash": "3tljdX43R8+k6peNHVrJNQ"}
@@ -6286,3 +6287,79 @@ def test_query_regex_4():
json_got = json.loads(result.output)
assert len(json_got) == 2
def test_export_export_dir_template():
"""Test {export_dir} template"""
import json
import os
import os.path
import osxphotos
from osxphotos.cli import cli
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
isolated_cwd = os.getcwd()
result = runner.invoke(
cli,
[
"export",
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
".",
"--sidecar=json",
f"--uuid={CLI_UUID_DICT_15_7['template']}",
"-V",
"--keyword-template",
"{person}",
"--description-template",
"{export_dir}",
],
)
assert result.exit_code == 0
assert os.path.isfile(CLI_TEMPLATE_SIDECAR_FILENAME)
with open(CLI_TEMPLATE_SIDECAR_FILENAME, "r") as jsonfile:
exifdata = json.load(jsonfile)
assert exifdata[0]["XMP:Description"] == isolated_cwd
def test_export_filepath_template():
"""Test {filepath} template"""
import json
import os
import os.path
import osxphotos
from osxphotos.cli import cli
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
isolated_cwd = os.getcwd()
result = runner.invoke(
cli,
[
"export",
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
".",
"--sidecar=json",
f"--uuid={CLI_UUID_DICT_15_7['template']}",
"-V",
"--keyword-template",
"{person}",
"--description-template",
"{filepath}",
],
)
assert result.exit_code == 0
assert os.path.isfile(CLI_TEMPLATE_SIDECAR_FILENAME)
with open(CLI_TEMPLATE_SIDECAR_FILENAME, "r") as jsonfile:
exifdata = json.load(jsonfile)
assert exifdata[0]["XMP:Description"] == os.path.join(
isolated_cwd, CLI_TEMPLATE_FILENAME
)

View File

@@ -1041,3 +1041,29 @@ def test_export_dir():
with pytest.raises(ValueError):
rendered, _ = template.render("{export_dir.foo}", options)
def test_filepath():
"""Test {filepath} template"""
from osxphotos.photoinfo import PhotoInfoNone
from osxphotos.phototemplate import PhotoTemplate
options = RenderOptions(filepath="/foo/bar.jpeg")
template = PhotoTemplate(PhotoInfoNone())
rendered, _ = template.render("{filepath}", options)
assert rendered[0] == "/foo/bar.jpeg"
rendered, _ = template.render("{filepath.name}", options)
assert rendered[0] == "bar.jpeg"
rendered, _ = template.render("{filepath.parent}", options)
assert rendered[0] == "/foo"
rendered, _ = template.render("{filepath.stem}", options)
assert rendered[0] == "bar"
rendered, _ = template.render("{filepath.suffix}", options)
assert rendered[0] == ".jpeg"
with pytest.raises(ValueError):
rendered, _ = template.render("{filepath.foo}", options)