Implemented context manager for ExifTool, closes #250
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.36.6"
|
__version__ = "0.36.7"
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
I rolled my own for following reasons:
|
I rolled my own for following reasons:
|
||||||
1. I wanted something under MIT license (best alternative was licensed under GPL/BSD)
|
1. I wanted something under MIT license (best alternative was licensed under GPL/BSD)
|
||||||
2. I wanted singleton behavior so only a single exiftool process was ever running
|
2. I wanted singleton behavior so only a single exiftool process was ever running
|
||||||
|
3. When used as a context manager, I wanted the operations to batch until exiting the context (improved performance)
|
||||||
If these aren't important to you, I highly recommend you use Sven Marnach's excellent
|
If these aren't important to you, I highly recommend you use Sven Marnach's excellent
|
||||||
pyexiftool: https://github.com/smarnach/pyexiftool which provides more functionality """
|
pyexiftool: https://github.com/smarnach/pyexiftool which provides more functionality """
|
||||||
|
|
||||||
@@ -10,10 +11,8 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
|
||||||
from functools import lru_cache # pylint: disable=syntax-error
|
from functools import lru_cache # pylint: disable=syntax-error
|
||||||
|
|
||||||
from .utils import _debug
|
|
||||||
|
|
||||||
# exiftool -stay_open commands outputs this EOF marker after command is run
|
# exiftool -stay_open commands outputs this EOF marker after command is run
|
||||||
EXIFTOOL_STAYOPEN_EOF = "{ready}"
|
EXIFTOOL_STAYOPEN_EOF = "{ready}"
|
||||||
@@ -23,9 +22,7 @@ EXIFTOOL_STAYOPEN_EOF_LEN = len(EXIFTOOL_STAYOPEN_EOF)
|
|||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def get_exiftool_path():
|
def get_exiftool_path():
|
||||||
""" return path of exiftool, cache result """
|
""" return path of exiftool, cache result """
|
||||||
exiftool_path = shutil.which('exiftool')
|
exiftool_path = shutil.which("exiftool")
|
||||||
if _debug():
|
|
||||||
logging.debug("exiftool path = %s" % (exiftool_path))
|
|
||||||
if exiftool_path:
|
if exiftool_path:
|
||||||
return exiftool_path.rstrip()
|
return exiftool_path.rstrip()
|
||||||
else:
|
else:
|
||||||
@@ -59,7 +56,7 @@ class _ExifToolProc:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
self._exiftool = exiftool if exiftool else get_exiftool_path()
|
self._exiftool = exiftool or get_exiftool_path()
|
||||||
self._process_running = False
|
self._process_running = False
|
||||||
self._start_proc()
|
self._start_proc()
|
||||||
|
|
||||||
@@ -98,7 +95,7 @@ class _ExifToolProc:
|
|||||||
"-", # read from stdin
|
"-", # read from stdin
|
||||||
"-common_args", # specifies args common to all commands subsequently run
|
"-common_args", # specifies args common to all commands subsequently run
|
||||||
"-n", # no print conversion (e.g. print tag values in machine readable format)
|
"-n", # no print conversion (e.g. print tag values in machine readable format)
|
||||||
"-P", # Preserve file modification date/time (possible interfere w/ --touch-file)
|
"-P", # Preserve file modification date/time
|
||||||
"-G", # print group name for each tag
|
"-G", # print group name for each tag
|
||||||
],
|
],
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
@@ -143,6 +140,8 @@ class ExifTool:
|
|||||||
self.file = filepath
|
self.file = filepath
|
||||||
self.overwrite = overwrite
|
self.overwrite = overwrite
|
||||||
self.data = {}
|
self.data = {}
|
||||||
|
# if running as a context manager, self._context_mgr will be True
|
||||||
|
self._context_mgr = False
|
||||||
self._exiftoolproc = _ExifToolProc(exiftool=exiftool)
|
self._exiftoolproc = _ExifToolProc(exiftool=exiftool)
|
||||||
self._process = self._exiftoolproc.process
|
self._process = self._exiftoolproc.process
|
||||||
self._read_exif()
|
self._read_exif()
|
||||||
@@ -154,8 +153,11 @@ class ExifTool:
|
|||||||
if value is None:
|
if value is None:
|
||||||
value = ""
|
value = ""
|
||||||
command = [f"-{tag}={value}"]
|
command = [f"-{tag}={value}"]
|
||||||
if self.overwrite:
|
if self.overwrite and not self._context_mgr:
|
||||||
command.append("-overwrite_original")
|
command.append("-overwrite_original")
|
||||||
|
if self._context_mgr:
|
||||||
|
self._commands.extend(command)
|
||||||
|
else:
|
||||||
self.run_commands(*command)
|
self.run_commands(*command)
|
||||||
|
|
||||||
def addvalues(self, tag, *values):
|
def addvalues(self, tag, *values):
|
||||||
@@ -178,10 +180,12 @@ class ExifTool:
|
|||||||
raise ValueError("Can't add None value to tag")
|
raise ValueError("Can't add None value to tag")
|
||||||
command.append(f"-{tag}+={value}")
|
command.append(f"-{tag}+={value}")
|
||||||
|
|
||||||
if self.overwrite:
|
if self.overwrite and not self._context_mgr:
|
||||||
command.append("-overwrite_original")
|
command.append("-overwrite_original")
|
||||||
|
|
||||||
if command:
|
if self._context_mgr:
|
||||||
|
self._commands.extend(command)
|
||||||
|
else:
|
||||||
self.run_commands(*command)
|
self.run_commands(*command)
|
||||||
|
|
||||||
def run_commands(self, *commands, no_file=False):
|
def run_commands(self, *commands, no_file=False):
|
||||||
@@ -195,6 +199,10 @@ class ExifTool:
|
|||||||
if not commands:
|
if not commands:
|
||||||
raise TypeError("must provide one or more command to run")
|
raise TypeError("must provide one or more command to run")
|
||||||
|
|
||||||
|
if self._context_mgr and self.overwrite:
|
||||||
|
commands = list(commands)
|
||||||
|
commands.append("-overwrite_original")
|
||||||
|
|
||||||
filename = os.fsencode(self.file) if not no_file else b""
|
filename = os.fsencode(self.file) if not no_file else b""
|
||||||
command_str = (
|
command_str = (
|
||||||
b"\n".join([c.encode("utf-8") for c in commands])
|
b"\n".join([c.encode("utf-8") for c in commands])
|
||||||
@@ -204,9 +212,6 @@ class ExifTool:
|
|||||||
+ b"-execute\n"
|
+ b"-execute\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
if _debug():
|
|
||||||
logging.debug(command_str)
|
|
||||||
|
|
||||||
# send the command
|
# send the command
|
||||||
self._process.stdin.write(command_str)
|
self._process.stdin.write(command_str)
|
||||||
self._process.stdin.flush()
|
self._process.stdin.flush()
|
||||||
@@ -250,3 +255,14 @@ class ExifTool:
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"file: {self.file}\nexiftool: {self._exiftoolproc._exiftool}"
|
return f"file: {self.file}\nexiftool: {self._exiftoolproc._exiftool}"
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self._context_mgr = True
|
||||||
|
self._commands = []
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
if exc_type:
|
||||||
|
return False
|
||||||
|
elif self._commands:
|
||||||
|
self.run_commands(*self._commands)
|
||||||
|
|||||||
@@ -1019,7 +1019,6 @@ def _write_exif_data(
|
|||||||
"""
|
"""
|
||||||
if not os.path.exists(filepath):
|
if not os.path.exists(filepath):
|
||||||
raise FileNotFoundError(f"Could not find file {filepath}")
|
raise FileNotFoundError(f"Could not find file {filepath}")
|
||||||
exiftool = ExifTool(filepath)
|
|
||||||
exif_info = self._exiftool_dict(
|
exif_info = self._exiftool_dict(
|
||||||
use_albums_as_keywords=use_albums_as_keywords,
|
use_albums_as_keywords=use_albums_as_keywords,
|
||||||
use_persons_as_keywords=use_persons_as_keywords,
|
use_persons_as_keywords=use_persons_as_keywords,
|
||||||
@@ -1027,15 +1026,14 @@ def _write_exif_data(
|
|||||||
description_template=description_template,
|
description_template=description_template,
|
||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with ExifTool(filepath) as exiftool:
|
||||||
for exiftag, val in exif_info.items():
|
for exiftag, val in exif_info.items():
|
||||||
if exiftag == "_CreatedBy":
|
if exiftag == "_CreatedBy":
|
||||||
continue
|
continue
|
||||||
if type(val) == list:
|
elif type(val) == list:
|
||||||
# more than one, set first value the add additional values
|
for v in val:
|
||||||
exiftool.setvalue(exiftag, val.pop(0))
|
exiftool.setvalue(exiftag, v)
|
||||||
if val:
|
|
||||||
# add any remaining items
|
|
||||||
exiftool.addvalues(exiftag, *val)
|
|
||||||
else:
|
else:
|
||||||
exiftool.setvalue(exiftag, val)
|
exiftool.setvalue(exiftag, val)
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,30 @@ def test_setvalue_1():
|
|||||||
assert exif.data["IPTC:Keywords"] == "test"
|
assert exif.data["IPTC:Keywords"] == "test"
|
||||||
|
|
||||||
|
|
||||||
|
def test_setvalue_context_manager():
|
||||||
|
# test setting a tag value as context manager
|
||||||
|
import os.path
|
||||||
|
import tempfile
|
||||||
|
import osxphotos.exiftool
|
||||||
|
from osxphotos.fileutil import FileUtil
|
||||||
|
|
||||||
|
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||||
|
tempfile = os.path.join(tempdir.name, os.path.basename(TEST_FILE_ONE_KEYWORD))
|
||||||
|
FileUtil.copy(TEST_FILE_ONE_KEYWORD, tempfile)
|
||||||
|
|
||||||
|
with osxphotos.exiftool.ExifTool(tempfile) as exif:
|
||||||
|
exif.setvalue("IPTC:Keywords", "test1")
|
||||||
|
exif.setvalue("IPTC:Keywords", "test2")
|
||||||
|
exif.setvalue("XMP:Title", "title")
|
||||||
|
exif.setvalue("XMP:Subject", "subject")
|
||||||
|
|
||||||
|
exif2 = osxphotos.exiftool.ExifTool(tempfile)
|
||||||
|
exif2._read_exif()
|
||||||
|
assert sorted(exif2.data["IPTC:Keywords"]) == ["test1", "test2"]
|
||||||
|
assert exif2.data["XMP:Title"] == "title"
|
||||||
|
assert exif2.data["XMP:Subject"] == "subject"
|
||||||
|
|
||||||
|
|
||||||
def test_clear_value():
|
def test_clear_value():
|
||||||
# test clearing a tag value
|
# test clearing a tag value
|
||||||
import os.path
|
import os.path
|
||||||
|
|||||||
Reference in New Issue
Block a user