Implemented context manager for ExifTool, closes #250

This commit is contained in:
Rhet Turnbull
2020-11-03 18:51:09 -08:00
parent c7c5320587
commit cf7fab4c7a
4 changed files with 65 additions and 27 deletions

View File

@@ -1,4 +1,4 @@
""" version info """ """ version info """
__version__ = "0.36.6" __version__ = "0.36.7"

View File

@@ -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)

View File

@@ -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)

View File

@@ -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