Port to non-MacOS platforms (#1026)

* Port to non-MacOS platforms

* Keep NFD normalization on macOS

* Update locale_util.py

Fix lint error from ruff (runs in CI)

* Update query.py

click.Option first arg needs to be a list (different than click.option)

* Dynamically normalize Unicode paths in test

* Fix missing import

---------

Co-authored-by: Rhet Turnbull <rturnbull@gmail.com>
This commit is contained in:
dvdkon 2023-05-07 15:55:56 +02:00 committed by GitHub
parent 0c85298c03
commit ca3da647f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 726 additions and 360 deletions

View File

@ -16,7 +16,6 @@ from .momentinfo import MomentInfo
from .personinfo import PersonInfo from .personinfo import PersonInfo
from .photoexporter import ExportOptions, ExportResults, PhotoExporter from .photoexporter import ExportOptions, ExportResults, PhotoExporter
from .photoinfo import PhotoInfo from .photoinfo import PhotoInfo
from .photosalbum import PhotosAlbum, PhotosAlbumPhotoScript
from .photosdb import PhotosDB from .photosdb import PhotosDB
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
from .phototables import PhotoTables from .phototables import PhotoTables
@ -25,6 +24,10 @@ from .placeinfo import PlaceInfo
from .queryoptions import QueryOptions from .queryoptions import QueryOptions
from .scoreinfo import ScoreInfo from .scoreinfo import ScoreInfo
from .searchinfo import SearchInfo from .searchinfo import SearchInfo
from .utils import is_macos
if is_macos:
from .photosalbum import PhotosAlbum, PhotosAlbumPhotoScript
# configure logging; every module in osxphotos should use this logger # configure logging; every module in osxphotos should use this logger
logging.basicConfig( logging.basicConfig(

View File

@ -5,6 +5,7 @@ import sys
from rich import print from rich import print
from rich.traceback import install as install_traceback from rich.traceback import install as install_traceback
from osxphotos.utils import is_macos
from osxphotos.debug import ( from osxphotos.debug import (
debug_breakpoint, debug_breakpoint,
debug_watch, debug_watch,
@ -44,9 +45,7 @@ if args.get("--debug", False):
print("Debugging enabled", file=sys.stderr) print("Debugging enabled", file=sys.stderr)
from .about import about from .about import about
from .add_locations import add_locations
from .albums import albums from .albums import albums
from .batch_edit import batch_edit
from .cli import cli_main from .cli import cli_main
from .cli_commands import ( from .cli_commands import (
abort, abort,
@ -67,7 +66,6 @@ from .export import export
from .exportdb import exportdb from .exportdb import exportdb
from .grep import grep from .grep import grep
from .help import help from .help import help
from .import_cli import import_cli
from .info import info from .info import info
from .install_uninstall_run import install, run, uninstall from .install_uninstall_run import install, run, uninstall
from .keywords import keywords from .keywords import keywords
@ -76,19 +74,24 @@ from .labels import labels
from .list import _list_libraries, list_libraries from .list import _list_libraries, list_libraries
from .orphans import orphans from .orphans import orphans
from .persons import persons from .persons import persons
from .photo_inspect import photo_inspect
from .places import places from .places import places
from .query import query from .query import query
from .repl import repl from .repl import repl
from .show_command import show
from .snap_diff import diff, snap from .snap_diff import diff, snap
from .sync import sync
from .theme import theme from .theme import theme
from .timewarp import timewarp
from .tutorial import tutorial from .tutorial import tutorial
from .uuid import uuid
from .version import version from .version import version
if is_macos:
from .add_locations import add_locations
from .batch_edit import batch_edit
from .import_cli import import_cli
from .photo_inspect import photo_inspect
from .show_command import show
from .sync import sync
from .timewarp import timewarp
from .uuid import uuid
install_traceback() install_traceback()
__all__ = [ __all__ = [

View File

@ -5,11 +5,10 @@ from __future__ import annotations
import datetime import datetime
import click import click
import photoscript
import osxphotos import osxphotos
from osxphotos.queryoptions import IncompatibleQueryOptions, query_options_from_kwargs from osxphotos.queryoptions import IncompatibleQueryOptions, query_options_from_kwargs
from osxphotos.utils import pluralize from osxphotos.utils import assert_macos, pluralize
from .cli_params import QUERY_OPTIONS, THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION from .cli_params import QUERY_OPTIONS, THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
from .click_rich_echo import rich_click_echo as echo from .click_rich_echo import rich_click_echo as echo
@ -18,6 +17,10 @@ from .param_types import TimeOffset
from .rich_progress import rich_progress from .rich_progress import rich_progress
from .verbose import get_verbose_console, verbose_print from .verbose import get_verbose_console, verbose_print
assert_macos()
import photoscript
def get_location( def get_location(
photos: list[osxphotos.PhotoInfo], idx: int, window: datetime.timedelta photos: list[osxphotos.PhotoInfo], idx: int, window: datetime.timedelta

View File

@ -9,11 +9,15 @@ import json
import sys import sys
import click import click
import photoscript
import osxphotos import osxphotos
from osxphotos.phototemplate import RenderOptions from osxphotos.phototemplate import RenderOptions
from osxphotos.sqlitekvstore import SQLiteKVStore from osxphotos.sqlitekvstore import SQLiteKVStore
from osxphotos.utils import assert_macos
assert_macos()
import photoscript
from .cli_commands import echo, echo_error, selection_command, verbose from .cli_commands import echo, echo_error, selection_command, verbose
from .kvstore import kvstore from .kvstore import kvstore

View File

@ -9,11 +9,10 @@ import click
from osxphotos._constants import PROFILE_SORT_KEYS from osxphotos._constants import PROFILE_SORT_KEYS
from osxphotos._version import __version__ from osxphotos._version import __version__
from osxphotos.utils import is_macos
from .about import about from .about import about
from .add_locations import add_locations
from .albums import albums from .albums import albums
from .batch_edit import batch_edit
from .cli_params import DB_OPTION, DEBUG_OPTIONS, JSON_OPTION, VERSION_OPTION from .cli_params import DB_OPTION, DEBUG_OPTIONS, JSON_OPTION, VERSION_OPTION
from .common import OSXPHOTOS_HIDDEN from .common import OSXPHOTOS_HIDDEN
from .debug_dump import debug_dump from .debug_dump import debug_dump
@ -24,7 +23,6 @@ from .export import export
from .exportdb import exportdb from .exportdb import exportdb
from .grep import grep from .grep import grep
from .help import help from .help import help
from .import_cli import import_cli
from .info import info from .info import info
from .install_uninstall_run import install, run, uninstall from .install_uninstall_run import install, run, uninstall
from .keywords import keywords from .keywords import keywords
@ -32,19 +30,24 @@ from .labels import labels
from .list import list_libraries from .list import list_libraries
from .orphans import orphans from .orphans import orphans
from .persons import persons from .persons import persons
from .photo_inspect import photo_inspect
from .places import places from .places import places
from .query import query from .query import query
from .repl import repl from .repl import repl
from .show_command import show
from .snap_diff import diff, snap from .snap_diff import diff, snap
from .sync import sync
from .theme import theme from .theme import theme
from .timewarp import timewarp
from .tutorial import tutorial from .tutorial import tutorial
from .uuid import uuid
from .version import version from .version import version
if is_macos:
from .add_locations import add_locations
from .batch_edit import batch_edit
from .import_cli import import_cli
from .photo_inspect import photo_inspect
from .show_command import show
from .sync import sync
from .timewarp import timewarp
from .uuid import uuid
# Click CLI object & context settings # Click CLI object & context settings
class CLI_Obj: class CLI_Obj:
@ -106,11 +109,9 @@ def cli_main(ctx, db, json_, profile, profile_sort, **kwargs):
# install CLI commands # install CLI commands
for command in [ commands = [
about, about,
add_locations,
albums, albums,
batch_edit,
debug_dump, debug_dump,
diff, diff,
docs_command, docs_command,
@ -120,7 +121,6 @@ for command in [
exportdb, exportdb,
grep, grep,
help, help,
import_cli,
info, info,
install, install,
keywords, keywords,
@ -128,19 +128,28 @@ for command in [
list_libraries, list_libraries,
orphans, orphans,
persons, persons,
photo_inspect,
places, places,
query, query,
repl, repl,
run, run,
show,
snap, snap,
sync,
theme, theme,
timewarp,
tutorial, tutorial,
uninstall, uninstall,
uuid,
version, version,
]: ]
if is_macos:
commands += [
add_locations,
batch_edit,
import_cli,
photo_inspect,
show,
sync,
timewarp,
uuid,
]
for command in commands:
cli_main.add_command(command) cli_main.add_command(command)

View File

@ -8,6 +8,8 @@ from typing import Any, Callable
import click import click
import contextlib import contextlib
from textwrap import dedent from textwrap import dedent
from ..utils import is_macos
from .common import OSXPHOTOS_HIDDEN, print_version from .common import OSXPHOTOS_HIDDEN, print_version
from .param_types import * from .param_types import *
@ -642,6 +644,9 @@ _QUERY_PARAMETERS_DICT = {
), ),
} }
if not is_macos:
del _QUERY_PARAMETERS_DICT["--selected"]
def QUERY_OPTIONS( def QUERY_OPTIONS(
wrapped=None, *, exclude: list[str] | None = None wrapped=None, *, exclude: list[str] | None = None

View File

@ -1,19 +1,34 @@
"""Detect dark mode on MacOS >= 10.14""" """Detect dark mode on MacOS >= 10.14 or fake it elsewhere"""
import objc from osxphotos.utils import is_macos
import Foundation
def theme(): if is_macos:
with objc.autorelease_pool(): import objc
user_defaults = Foundation.NSUserDefaults.standardUserDefaults() import Foundation
system_theme = user_defaults.stringForKey_("AppleInterfaceStyle")
return "dark" if system_theme == "Dark" else "light"
def is_dark_mode(): def theme():
return theme() == "dark" with objc.autorelease_pool():
user_defaults = Foundation.NSUserDefaults.standardUserDefaults()
system_theme = user_defaults.stringForKey_("AppleInterfaceStyle")
return "dark" if system_theme == "Dark" else "light"
def is_light_mode(): def is_dark_mode():
return theme() == "light" return theme() == "dark"
def is_light_mode():
return theme() == "light"
else:
def theme():
return "light"
def is_dark_mode():
return theme() == "dark"
def is_light_mode():
return theme() == "light"

View File

@ -12,13 +12,6 @@ import time
from typing import Iterable, List, Optional, Tuple from typing import Iterable, List, Optional, Tuple
import click import click
from osxmetadata import (
MDITEM_ATTRIBUTE_DATA,
MDITEM_ATTRIBUTE_SHORT_NAMES,
OSXMetaData,
Tag,
)
from osxmetadata.constants import _TAGS_NAMES
import osxphotos import osxphotos
from osxphotos._constants import ( from osxphotos._constants import (
@ -47,26 +40,37 @@ from osxphotos.datetime_formatter import DateTimeFormatter
from osxphotos.debug import is_debug from osxphotos.debug import is_debug
from osxphotos.exiftool import get_exiftool_path from osxphotos.exiftool import get_exiftool_path
from osxphotos.export_db import ExportDB, ExportDBInMemory from osxphotos.export_db import ExportDB, ExportDBInMemory
from osxphotos.fileutil import FileUtil, FileUtilNoOp, FileUtilShUtil from osxphotos.fileutil import FileUtilMacOS, FileUtilNoOp, FileUtilShUtil
from osxphotos.path_utils import is_valid_filepath, sanitize_filename, sanitize_filepath from osxphotos.path_utils import is_valid_filepath, sanitize_filename, sanitize_filepath
from osxphotos.photoexporter import ExportOptions, ExportResults, PhotoExporter from osxphotos.photoexporter import ExportOptions, ExportResults, PhotoExporter
from osxphotos.photoinfo import PhotoInfoNone from osxphotos.photoinfo import PhotoInfoNone
from osxphotos.photokit import (
check_photokit_authorization,
request_photokit_authorization,
)
from osxphotos.photosalbum import PhotosAlbum
from osxphotos.phototemplate import PhotoTemplate, RenderOptions from osxphotos.phototemplate import PhotoTemplate, RenderOptions
from osxphotos.queryoptions import load_uuid_from_file, query_options_from_kwargs from osxphotos.queryoptions import load_uuid_from_file, query_options_from_kwargs
from osxphotos.uti import get_preferred_uti_extension from osxphotos.uti import get_preferred_uti_extension
from osxphotos.utils import ( from osxphotos.utils import (
format_sec_to_hhmmss, format_sec_to_hhmmss,
get_macos_version, get_macos_version,
is_macos,
normalize_fs_path, normalize_fs_path,
pluralize, pluralize,
under_test, under_test,
) )
if is_macos:
from osxmetadata import (
MDITEM_ATTRIBUTE_DATA,
MDITEM_ATTRIBUTE_SHORT_NAMES,
OSXMetaData,
Tag,
)
from osxmetadata.constants import _TAGS_NAMES
from osxphotos.photokit import (
check_photokit_authorization,
request_photokit_authorization,
)
from osxphotos.photosalbum import PhotosAlbum
from .cli_commands import logger from .cli_commands import logger
from .cli_params import ( from .cli_params import (
DB_ARGUMENT, DB_ARGUMENT,
@ -851,7 +855,6 @@ def export(
retry, retry,
save_config, save_config,
screenshot, screenshot,
selected,
selfie, selfie,
shared, shared,
sidecar, sidecar,
@ -883,6 +886,7 @@ def export(
verbose_flag, verbose_flag,
xattr_template, xattr_template,
year, year,
selected=False, # Isn't provided on unsupported platforms
# debug, # debug, watch, breakpoint handled in cli/__init__.py # debug, # debug, watch, breakpoint handled in cli/__init__.py
# watch, # watch,
# breakpoint, # breakpoint,
@ -1111,7 +1115,10 @@ def export(
verbose(f"osxphotos version: {__version__}") verbose(f"osxphotos version: {__version__}")
verbose(f"Python version: {sys.version}") verbose(f"Python version: {sys.version}")
verbose(f"Platform: {platform.platform()}, {'.'.join(get_macos_version())}") if is_macos:
verbose(f"Platform: {platform.platform()}, {'.'.join(get_macos_version())}")
else:
verbose(f"Platform: {platform.platform()}")
verbose(f"Verbose level: {verbose_flag}") verbose(f"Verbose level: {verbose_flag}")
# validate options # validate options
@ -1325,7 +1332,7 @@ def export(
if ramdb if ramdb
else ExportDB(dbfile=export_db_path, export_dir=dest) else ExportDB(dbfile=export_db_path, export_dir=dest)
) )
fileutil = FileUtilShUtil if alt_copy else FileUtil fileutil = FileUtilShUtil if alt_copy or not is_macos else FileUtilMacOS
if verbose: if verbose:
if export_db.was_created: if export_db.was_created:
@ -1713,7 +1720,7 @@ def export_photo(
keyword_template=None, keyword_template=None,
description_template=None, description_template=None,
export_db=None, export_db=None,
fileutil=FileUtil, fileutil=FileUtilShUtil,
dry_run=None, dry_run=None,
touch_file=None, touch_file=None,
edited_suffix="_edited", edited_suffix="_edited",

View File

@ -5,7 +5,6 @@ import re
import typing as t import typing as t
import click import click
from osxmetadata import MDITEM_ATTRIBUTE_DATA, MDITEM_ATTRIBUTE_SHORT_NAMES
from rich.console import Console from rich.console import Console
from rich.markdown import Markdown from rich.markdown import Markdown
@ -21,6 +20,10 @@ from osxphotos.phototemplate import (
TEMPLATE_SUBSTITUTIONS_PATHLIB, TEMPLATE_SUBSTITUTIONS_PATHLIB,
get_template_help, get_template_help,
) )
from osxphotos.utils import is_macos
if is_macos:
from osxmetadata import MDITEM_ATTRIBUTE_DATA, MDITEM_ATTRIBUTE_SHORT_NAMES
from .click_rich_echo import rich_echo_via_pager from .click_rich_echo import rich_echo_via_pager
from .color_themes import get_theme from .color_themes import get_theme
@ -249,68 +252,71 @@ class ExportCommand(click.Command):
+ f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database." + f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database."
) )
formatter.write("\n") formatter.write("\n")
formatter.write(
rich_text("## Extended Attributes", width=formatter.width, markdown=True)
)
formatter.write("\n")
formatter.write_text(
"""
Some options (currently '--finder-tag-template', '--finder-tag-keywords', '-xattr-template') write
additional metadata accessible by Spotlight to facilitate searching.
For example, --finder-tag-keyword writes all keywords (including any specified by '--keyword-template'
or other options) to Finder tags that are searchable in Spotlight using the syntax: 'tag:tagname'.
For example, if you have images with keyword "Travel" then using '--finder-tag-keywords' you could quickly
find those images in the Finder by typing 'tag:Travel' in the Spotlight search bar.
Finder tags are written to the 'com.apple.metadata:_kMDItemUserTags' extended attribute.
Unlike EXIF metadata, extended attributes do not modify the actual file;
the metadata is written to extended attributes associated with the file and the Spotlight metadata database.
Most cloud storage services do not synch extended attributes.
Dropbox does sync them and any changes to a file's extended attributes
will cause Dropbox to re-sync the files.
The following attributes may be used with '--xattr-template': if is_macos:
formatter.write(
""" rich_text("## Extended Attributes", width=formatter.width, markdown=True)
)
# build help text from all the attribute names
# passed to click.HelpFormatter.write_dl for formatting
attr_tuples = [
(
rich_text("[bold]Attribute[/bold]", width=formatter.width),
rich_text("[bold]Description[/bold]", width=formatter.width),
) )
] formatter.write("\n")
for attr_key in sorted(EXTENDED_ATTRIBUTE_NAMES): formatter.write_text(
# get short and long name """
attr = MDITEM_ATTRIBUTE_SHORT_NAMES[attr_key] Some options (currently '--finder-tag-template', '--finder-tag-keywords', '-xattr-template') write
short_name = MDITEM_ATTRIBUTE_DATA[attr]["short_name"] additional metadata accessible by Spotlight to facilitate searching.
long_name = MDITEM_ATTRIBUTE_DATA[attr]["name"] For example, --finder-tag-keyword writes all keywords (including any specified by '--keyword-template'
constant = MDITEM_ATTRIBUTE_DATA[attr]["xattr_constant"] or other options) to Finder tags that are searchable in Spotlight using the syntax: 'tag:tagname'.
For example, if you have images with keyword "Travel" then using '--finder-tag-keywords' you could quickly
find those images in the Finder by typing 'tag:Travel' in the Spotlight search bar.
Finder tags are written to the 'com.apple.metadata:_kMDItemUserTags' extended attribute.
Unlike EXIF metadata, extended attributes do not modify the actual file;
the metadata is written to extended attributes associated with the file and the Spotlight metadata database.
Most cloud storage services do not synch extended attributes.
Dropbox does sync them and any changes to a file's extended attributes
will cause Dropbox to re-sync the files.
# get help text The following attributes may be used with '--xattr-template':
description = MDITEM_ATTRIBUTE_DATA[attr]["description"]
type_ = MDITEM_ATTRIBUTE_DATA[attr]["help_type"]
attr_help = f"{long_name}; {constant}; {description}; {type_}"
# add to list """
attr_tuples.append((short_name, attr_help)) )
formatter.write_dl(attr_tuples) # build help text from all the attribute names
formatter.write("\n") # passed to click.HelpFormatter.write_dl for formatting
formatter.write_text( attr_tuples = [
"For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys" (
) rich_text("[bold]Attribute[/bold]", width=formatter.width),
formatter.write("\n") rich_text("[bold]Description[/bold]", width=formatter.width),
formatter.write( )
rich_text("## Templating System", width=formatter.width, markdown=True) ]
) for attr_key in sorted(EXTENDED_ATTRIBUTE_NAMES):
formatter.write("\n") # get short and long name
help_text += formatter.getvalue() attr = MDITEM_ATTRIBUTE_SHORT_NAMES[attr_key]
help_text += template_help(width=formatter.width) short_name = MDITEM_ATTRIBUTE_DATA[attr]["short_name"]
formatter = click.HelpFormatter(width=HELP_WIDTH) long_name = MDITEM_ATTRIBUTE_DATA[attr]["name"]
constant = MDITEM_ATTRIBUTE_DATA[attr]["xattr_constant"]
# get help text
description = MDITEM_ATTRIBUTE_DATA[attr]["description"]
type_ = MDITEM_ATTRIBUTE_DATA[attr]["help_type"]
attr_help = f"{long_name}; {constant}; {description}; {type_}"
# add to list
attr_tuples.append((short_name, attr_help))
formatter.write_dl(attr_tuples)
formatter.write("\n")
formatter.write_text(
"For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys"
)
formatter.write("\n")
formatter.write(
rich_text("## Templating System", width=formatter.width, markdown=True)
)
formatter.write("\n")
help_text += formatter.getvalue()
help_text += template_help(width=formatter.width)
formatter = click.HelpFormatter(width=HELP_WIDTH)
formatter.write("\n")
formatter.write("\n")
formatter.write_text( formatter.write_text(
"With the --directory and --filename options you may specify a template for the " "With the --directory and --filename options you may specify a template for the "
+ "export directory or filename, respectively. " + "export directory or filename, respectively. "

View File

@ -20,7 +20,6 @@ from textwrap import dedent
from typing import Callable, Dict, List, Optional, Tuple, Union from typing import Callable, Dict, List, Optional, Tuple, Union
import click import click
from photoscript import Photo, PhotosLibrary
from rich.console import Console from rich.console import Console
from rich.markdown import Markdown from rich.markdown import Markdown
from strpdatetime import strpdatetime from strpdatetime import strpdatetime
@ -43,7 +42,11 @@ from osxphotos.photoinfo import PhotoInfoNone
from osxphotos.photosalbum import PhotosAlbumPhotoScript from osxphotos.photosalbum import PhotosAlbumPhotoScript
from osxphotos.phototemplate import PhotoTemplate, RenderOptions from osxphotos.phototemplate import PhotoTemplate, RenderOptions
from osxphotos.sqlitekvstore import SQLiteKVStore from osxphotos.sqlitekvstore import SQLiteKVStore
from osxphotos.utils import pluralize from osxphotos.utils import assert_macos, pluralize
assert_macos()
from photoscript import Photo, PhotosLibrary
from .cli_params import THEME_OPTION from .cli_params import THEME_OPTION
from .click_rich_echo import rich_click_echo, rich_echo_error from .click_rich_echo import rich_click_echo, rich_echo_error

View File

@ -13,8 +13,6 @@ from typing import Generator, List, Optional, Tuple
import bitmath import bitmath
import click import click
from applescript import ScriptError
from photoscript import PhotosLibrary
from rich.console import Console from rich.console import Console
from rich.layout import Layout from rich.layout import Layout
from rich.live import Live from rich.live import Live
@ -23,8 +21,14 @@ from rich.panel import Panel
from osxphotos import PhotoInfo, PhotosDB from osxphotos import PhotoInfo, PhotosDB
from osxphotos._constants import _UNKNOWN_PERSON, search_category_factory from osxphotos._constants import _UNKNOWN_PERSON, search_category_factory
from osxphotos.rich_utils import add_rich_markup_tag from osxphotos.rich_utils import add_rich_markup_tag
from osxphotos.utils import assert_macos, dd_to_dms_str
assert_macos()
from applescript import ScriptError
from photoscript import PhotosLibrary
from osxphotos.text_detection import detect_text as detect_text_in_photo from osxphotos.text_detection import detect_text as detect_text_in_photo
from osxphotos.utils import dd_to_dms_str
from .cli_params import DB_OPTION, THEME_OPTION from .cli_params import DB_OPTION, THEME_OPTION
from .color_themes import get_theme from .color_themes import get_theme

View File

@ -1,5 +1,6 @@
"""query command for osxphotos CLI""" """query command for osxphotos CLI"""
import sys
import click import click
import osxphotos import osxphotos
@ -9,9 +10,12 @@ from osxphotos.cli.click_rich_echo import (
set_rich_theme, set_rich_theme,
) )
from osxphotos.debug import set_debug from osxphotos.debug import set_debug
from osxphotos.photosalbum import PhotosAlbum
from osxphotos.phototemplate import RenderOptions from osxphotos.phototemplate import RenderOptions
from osxphotos.queryoptions import query_options_from_kwargs from osxphotos.queryoptions import query_options_from_kwargs
from osxphotos.utils import assert_macos, is_macos
if is_macos:
from osxphotos.photosalbum import PhotosAlbum
from .cli_params import ( from .cli_params import (
DB_ARGUMENT, DB_ARGUMENT,
@ -20,6 +24,7 @@ from .cli_params import (
FIELD_OPTION, FIELD_OPTION,
JSON_OPTION, JSON_OPTION,
QUERY_OPTIONS, QUERY_OPTIONS,
make_click_option_decorator,
) )
from .color_themes import get_default_theme from .color_themes import get_default_theme
from .common import CLI_COLOR_ERROR, CLI_COLOR_WARNING, OSXPHOTOS_HIDDEN, get_photos_db from .common import CLI_COLOR_ERROR, CLI_COLOR_WARNING, OSXPHOTOS_HIDDEN, get_photos_db
@ -27,20 +32,23 @@ from .list import _list_libraries
from .print_photo_info import print_photo_fields, print_photo_info from .print_photo_info import print_photo_fields, print_photo_info
from .verbose import get_verbose_console from .verbose import get_verbose_console
MACOS_OPTIONS = make_click_option_decorator(*[
click.Option(
["--add-to-album"],
metavar="ALBUM",
help="Add all photos from query to album ALBUM in Photos. Album ALBUM will be created "
"if it doesn't exist. All photos in the query results will be added to this album. "
"This only works if the Photos library being queried is the last-opened (default) library in Photos. "
"This feature is currently experimental. I don't know how well it will work on large query sets.",
),
] if is_macos else [])
@click.command() @click.command()
@DB_OPTION @DB_OPTION
@JSON_OPTION @JSON_OPTION
@QUERY_OPTIONS @QUERY_OPTIONS
@DELETED_OPTIONS @DELETED_OPTIONS
@click.option( @MACOS_OPTIONS
"--add-to-album",
metavar="ALBUM",
help="Add all photos from query to album ALBUM in Photos. Album ALBUM will be created "
"if it doesn't exist. All photos in the query results will be added to this album. "
"This only works if the Photos library being queried is the last-opened (default) library in Photos. "
"This feature is currently experimental. I don't know how well it will work on large query sets.",
)
@click.option( @click.option(
"--quiet", "--quiet",
is_flag=True, is_flag=True,
@ -70,8 +78,8 @@ def query(
json_, json_,
print_template, print_template,
quiet, quiet,
add_to_album,
photos_library, photos_library,
add_to_album=False,
**kwargs, **kwargs,
): ):
"""Query the Photos database using 1 or more search options; """Query the Photos database using 1 or more search options;
@ -124,6 +132,8 @@ def query(
cli_json = cli_obj.json if cli_obj is not None else None cli_json = cli_obj.json if cli_obj is not None else None
if add_to_album and photos: if add_to_album and photos:
assert_macos()
album_query = PhotosAlbum(add_to_album, verbose=None) album_query = PhotosAlbum(add_to_album, verbose=None)
photo_len = len(photos) photo_len = len(photos)
photo_word = "photos" if photo_len > 1 else "photo" photo_word = "photos" if photo_len > 1 else "photo"

View File

@ -10,8 +10,6 @@ import time
from typing import List from typing import List
import click import click
import photoscript
from applescript import ScriptError
from rich import pretty, print from rich import pretty, print
import osxphotos import osxphotos
@ -25,6 +23,11 @@ from osxphotos.queryoptions import (
QueryOptions, QueryOptions,
query_options_from_kwargs, query_options_from_kwargs,
) )
from osxphotos.utils import assert_macos, is_macos
if is_macos:
import photoscript
from applescript import ScriptError
from .cli_params import DB_ARGUMENT, DB_OPTION, DELETED_OPTIONS, QUERY_OPTIONS from .cli_params import DB_ARGUMENT, DB_OPTION, DELETED_OPTIONS, QUERY_OPTIONS
from .common import get_photos_db from .common import get_photos_db
@ -55,7 +58,8 @@ def repl(ctx, cli_obj, db, emacs, beta, **kwargs):
import logging import logging
from objexplore import explore from objexplore import explore
from photoscript import Album, Photo, PhotosLibrary if is_macos:
from photoscript import Album, Photo, PhotosLibrary
from rich import inspect as _inspect from rich import inspect as _inspect
from osxphotos import ExifTool, PhotoInfo, PhotosDB from osxphotos import ExifTool, PhotoInfo, PhotosDB
@ -194,6 +198,7 @@ def _get_selected(photosdb):
"""get list of PhotoInfo objects for photos selected in Photos""" """get list of PhotoInfo objects for photos selected in Photos"""
def get_selected(): def get_selected():
assert_macos()
try: try:
selected = photoscript.PhotosLibrary().selection selected = photoscript.PhotosLibrary().selection
except ScriptError as e: except ScriptError as e:
@ -209,6 +214,7 @@ def _get_selected(photosdb):
def _spotlight_photo(photo: PhotoInfo): def _spotlight_photo(photo: PhotoInfo):
assert_macos()
photo_ = photoscript.Photo(photo.uuid) photo_ = photoscript.Photo(photo.uuid)
photo_.spotlight() photo_.spotlight()

View File

@ -7,12 +7,15 @@ import click
from osxphotos._constants import UUID_PATTERN from osxphotos._constants import UUID_PATTERN
from osxphotos.export_db_utils import get_uuid_for_filepath from osxphotos.export_db_utils import get_uuid_for_filepath
from osxphotos.photosdb.photosdb_utils import get_photos_library_version
from osxphotos.utils import get_last_library_path, assert_macos
assert_macos()
from osxphotos.photoscript_utils import ( from osxphotos.photoscript_utils import (
photoscript_object_from_name, photoscript_object_from_name,
photoscript_object_from_uuid, photoscript_object_from_uuid,
) )
from osxphotos.photosdb.photosdb_utils import get_photos_library_version
from osxphotos.utils import get_last_library_path
from .cli_commands import echo, echo_error from .cli_commands import echo, echo_error
from .cli_params import DB_OPTION from .cli_params import DB_OPTION

View File

@ -9,7 +9,6 @@ import pathlib
from typing import Any, Callable, Literal from typing import Any, Callable, Literal
import click import click
import photoscript
from osxphotos import PhotoInfo, PhotosDB, __version__ from osxphotos import PhotoInfo, PhotosDB, __version__
from osxphotos.photoinfo import PhotoInfoNone from osxphotos.photoinfo import PhotoInfoNone
@ -22,7 +21,11 @@ from osxphotos.queryoptions import (
query_options_from_kwargs, query_options_from_kwargs,
) )
from osxphotos.sqlitekvstore import SQLiteKVStore from osxphotos.sqlitekvstore import SQLiteKVStore
from osxphotos.utils import pluralize from osxphotos.utils import assert_macos, pluralize
assert_macos()
import photoscript
from .cli_params import ( from .cli_params import (
DB_OPTION, DB_OPTION,

View File

@ -7,7 +7,6 @@ from functools import partial
from textwrap import dedent from textwrap import dedent
import click import click
from photoscript import PhotosLibrary
from rich.console import Console from rich.console import Console
from osxphotos._constants import APP_NAME from osxphotos._constants import APP_NAME
@ -25,9 +24,13 @@ from osxphotos.photodates import (
update_photo_from_function, update_photo_from_function,
update_photo_time_for_new_timezone, update_photo_time_for_new_timezone,
) )
from osxphotos.photosalbum import PhotosAlbumPhotoScript
from osxphotos.phototz import PhotoTimeZone, PhotoTimeZoneUpdater from osxphotos.phototz import PhotoTimeZone, PhotoTimeZoneUpdater
from osxphotos.utils import noop, pluralize from osxphotos.utils import assert_macos, noop, pluralize
assert_macos()
from photoscript import PhotosLibrary
from osxphotos.photosalbum import PhotosAlbumPhotoScript
from .cli_params import THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION from .cli_params import THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
from .click_rich_echo import rich_click_echo as echo from .click_rich_echo import rich_click_echo as echo

View File

@ -1,6 +1,11 @@
"""uuid command for osxphotos CLI""" """uuid command for osxphotos CLI"""
import click import click
from osxphotos.utils import assert_macos
assert_macos()
import photoscript import photoscript

View File

@ -5,12 +5,15 @@ from typing import Callable, List, Optional, Tuple
from osxphotos import PhotosDB from osxphotos import PhotosDB
from osxphotos.exiftool import ExifTool from osxphotos.exiftool import ExifTool
from photoscript import Photo
from .datetime_utils import datetime_naive_to_local, datetime_to_new_tz from .datetime_utils import datetime_naive_to_local, datetime_to_new_tz
from .utils import assert_macos, noop
assert_macos()
from photoscript import Photo
from .exif_datetime_updater import get_exif_date_time_offset from .exif_datetime_updater import get_exif_date_time_offset
from .phototz import PhotoTimeZone from .phototz import PhotoTimeZone
from .utils import noop
ExifDiff = namedtuple( ExifDiff = namedtuple(
"ExifDiff", "ExifDiff",

View File

@ -9,9 +9,11 @@ import typing as t
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
import Foundation
from .imageconverter import ImageConverter from .imageconverter import ImageConverter
from .utils import is_macos, normalize_fs_path
if is_macos:
import Foundation
__all__ = ["FileUtilABC", "FileUtilMacOS", "FileUtilShUtil", "FileUtil", "FileUtilNoOp"] __all__ = ["FileUtilABC", "FileUtilMacOS", "FileUtilShUtil", "FileUtil", "FileUtilNoOp"]
@ -90,6 +92,9 @@ class FileUtilMacOS(FileUtilABC):
if src is None or dest is None: if src is None or dest is None:
raise ValueError("src and dest must not be None", src, dest) raise ValueError("src and dest must not be None", src, dest)
src = normalize_fs_path(src)
dest = normalize_fs_path(dest)
if not os.path.isfile(src): if not os.path.isfile(src):
raise FileNotFoundError("src file does not appear to exist", src) raise FileNotFoundError("src file does not appear to exist", src)
@ -115,6 +120,9 @@ class FileUtilMacOS(FileUtilABC):
OSError if copy fails OSError if copy fails
TypeError if either path is None TypeError if either path is None
""" """
src = normalize_fs_path(src)
dest = normalize_fs_path(dest)
if not isinstance(src, pathlib.Path): if not isinstance(src, pathlib.Path):
src = pathlib.Path(src) src = pathlib.Path(src)
@ -135,6 +143,7 @@ class FileUtilMacOS(FileUtilABC):
@classmethod @classmethod
def unlink(cls, filepath): def unlink(cls, filepath):
"""unlink filepath; if it's pathlib.Path, use Path.unlink, otherwise use os.unlink""" """unlink filepath; if it's pathlib.Path, use Path.unlink, otherwise use os.unlink"""
filepath = normalize_fs_path(filepath)
if isinstance(filepath, pathlib.Path): if isinstance(filepath, pathlib.Path):
filepath.unlink() filepath.unlink()
else: else:
@ -143,6 +152,7 @@ class FileUtilMacOS(FileUtilABC):
@classmethod @classmethod
def rmdir(cls, dirpath): def rmdir(cls, dirpath):
"""remove directory filepath; dirpath must be empty""" """remove directory filepath; dirpath must be empty"""
dirpath = normalize_fs_path(dirpath)
if isinstance(dirpath, pathlib.Path): if isinstance(dirpath, pathlib.Path):
dirpath.rmdir() dirpath.rmdir()
else: else:
@ -151,6 +161,7 @@ class FileUtilMacOS(FileUtilABC):
@classmethod @classmethod
def utime(cls, path, times): def utime(cls, path, times):
"""Set the access and modified time of path.""" """Set the access and modified time of path."""
path = normalize_fs_path(path)
os.utime(path, times=times) os.utime(path, times=times)
@classmethod @classmethod
@ -166,6 +177,9 @@ class FileUtilMacOS(FileUtilABC):
Does not do a byte-by-byte comparison. Does not do a byte-by-byte comparison.
""" """
f1 = normalize_fs_path(f1)
f2 = normalize_fs_path(f2)
s1 = cls._sig(os.stat(f1)) s1 = cls._sig(os.stat(f1))
if mtime1 is not None: if mtime1 is not None:
s1 = (s1[0], s1[1], int(mtime1)) s1 = (s1[0], s1[1], int(mtime1))
@ -188,6 +202,7 @@ class FileUtilMacOS(FileUtilABC):
if not s2: if not s2:
return False return False
f1 = normalize_fs_path(f1)
s1 = cls._sig(os.stat(f1)) s1 = cls._sig(os.stat(f1))
if s1[0] != stat.S_IFREG or s2[0] != stat.S_IFREG: if s1[0] != stat.S_IFREG or s2[0] != stat.S_IFREG:
return False return False
@ -196,6 +211,7 @@ class FileUtilMacOS(FileUtilABC):
@classmethod @classmethod
def file_sig(cls, f1): def file_sig(cls, f1):
"""return os.stat signature for file f1 as tuple of (mode, size, mtime)""" """return os.stat signature for file f1 as tuple of (mode, size, mtime)"""
f1 = normalize_fs_path(f1)
return cls._sig(os.stat(f1)) return cls._sig(os.stat(f1))
@classmethod @classmethod
@ -210,6 +226,8 @@ class FileUtilMacOS(FileUtilABC):
Returns: Returns:
True if success, otherwise False True if success, otherwise False
""" """
src_file = normalize_fs_path(src_file)
dest_file = normalize_fs_path(dest_file)
converter = ImageConverter() converter = ImageConverter()
return converter.write_jpeg( return converter.write_jpeg(
src_file, dest_file, compression_quality=compression_quality src_file, dest_file, compression_quality=compression_quality
@ -227,6 +245,8 @@ class FileUtilMacOS(FileUtilABC):
Name of renamed file (dest) Name of renamed file (dest)
""" """
src = normalize_fs_path(src)
dest = normalize_fs_path(dest)
os.rename(str(src), str(dest)) os.rename(str(src), str(dest))
return dest return dest
@ -271,6 +291,9 @@ class FileUtilShUtil(FileUtilMacOS):
OSError if copy fails OSError if copy fails
TypeError if either path is None TypeError if either path is None
""" """
src = normalize_fs_path(src)
dest = normalize_fs_path(dest)
if not isinstance(src, pathlib.Path): if not isinstance(src, pathlib.Path):
src = pathlib.Path(src) src = pathlib.Path(src)
@ -288,7 +311,7 @@ class FileUtilShUtil(FileUtilMacOS):
return True return True
class FileUtil(FileUtilMacOS): class FileUtil(FileUtilShUtil):
"""Various file utilities""" """Various file utilities"""
pass pass

View File

@ -5,16 +5,20 @@
# reference: https://stackoverflow.com/questions/59330149/coreimage-ciimage-write-jpg-is-shifting-colors-macos/59334308#59334308 # reference: https://stackoverflow.com/questions/59330149/coreimage-ciimage-write-jpg-is-shifting-colors-macos/59334308#59334308
import pathlib import pathlib
import sys
import objc
import Metal
import Quartz
from Cocoa import NSURL
from Foundation import NSDictionary
# needed to capture system-level stderr # needed to capture system-level stderr
from wurlitzer import pipes from wurlitzer import pipes
from .utils import is_macos
if is_macos:
import objc
import Metal
import Quartz
from Cocoa import NSURL
from Foundation import NSDictionary
__all__ = ["ImageConversionError", "ImageConverter"] __all__ = ["ImageConversionError", "ImageConverter"]

View File

@ -1,7 +1,20 @@
""" utility functions for validating/sanitizing path components """ """ utility functions for validating/sanitizing path components
This module also performs Unicode normalization. For a quick summary, there are
multiple ways to write more complex characters in Unicode. This causes problems
when e.g. checking if a file already exists and you have multiple sources for
the same string with different encodings. This sadly happens in Photos, but
isn't a problem on macOS, since macOS does normalization behind-the-scenes (see
https://eclecticlight.co/2021/05/08/explainer-unicode-normalization-and-apfs/).
This causes problems on other platforms, so we normalize as part of filename
sanitization functions and rely on them being called every time a unique
filename is needed.
"""
import pathvalidate import pathvalidate
from osxphotos.utils import normalize_unicode
from ._constants import MAX_DIRNAME_LEN, MAX_FILENAME_LEN from ._constants import MAX_DIRNAME_LEN, MAX_FILENAME_LEN
__all__ = [ __all__ = [
@ -24,7 +37,7 @@ def is_valid_filepath(filepath):
def sanitize_filename(filename, replacement=":"): def sanitize_filename(filename, replacement=":"):
"""replace any illegal characters in a filename and truncate filename if needed """replace any illegal characters in a filename, truncate filename if needed and normalize Unicode to NFC form
Args: Args:
filename: str, filename to sanitze filename: str, filename to sanitze
@ -35,6 +48,7 @@ def sanitize_filename(filename, replacement=":"):
""" """
if filename: if filename:
filename = normalize_unicode(filename)
filename = filename.replace("/", replacement) filename = filename.replace("/", replacement)
if len(filename) > MAX_FILENAME_LEN: if len(filename) > MAX_FILENAME_LEN:
parts = filename.split(".") parts = filename.split(".")
@ -54,7 +68,7 @@ def sanitize_filename(filename, replacement=":"):
def sanitize_dirname(dirname, replacement=":"): def sanitize_dirname(dirname, replacement=":"):
"""replace any illegal characters in a directory name and truncate directory name if needed """replace any illegal characters in a directory name, truncate directory name if needed, and normalize Unicode to NFC form
Args: Args:
dirname: str, directory name to sanitize dirname: str, directory name to sanitize
@ -69,7 +83,7 @@ def sanitize_dirname(dirname, replacement=":"):
def sanitize_pathpart(pathpart, replacement=":"): def sanitize_pathpart(pathpart, replacement=":"):
"""replace any illegal characters in a path part (either directory or filename without extension) and truncate name if needed """replace any illegal characters in a path part (either directory or filename without extension), truncate name if needed, and normalize Unicode to NFC form
Args: Args:
pathpart: str, path part to sanitize pathpart: str, path part to sanitize
@ -82,6 +96,7 @@ def sanitize_pathpart(pathpart, replacement=":"):
pathpart = ( pathpart = (
pathpart.replace("/", replacement) if replacement is not None else pathpart pathpart.replace("/", replacement) if replacement is not None else pathpart
) )
pathpart = normalize_unicode(pathpart)
if len(pathpart) > MAX_DIRNAME_LEN: if len(pathpart) > MAX_DIRNAME_LEN:
drop = len(pathpart) - MAX_DIRNAME_LEN drop = len(pathpart) - MAX_DIRNAME_LEN
pathpart = pathpart[:-drop] pathpart = pathpart[:-drop]

View File

@ -7,6 +7,7 @@ import logging
import os import os
import pathlib import pathlib
import re import re
import sys
import typing as t import typing as t
from collections import namedtuple # pylint: disable=syntax-error from collections import namedtuple # pylint: disable=syntax-error
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
@ -14,7 +15,6 @@ from datetime import datetime
from enum import Enum from enum import Enum
from types import SimpleNamespace from types import SimpleNamespace
import photoscript
from mako.template import Template from mako.template import Template
from ._constants import ( from ._constants import (
@ -35,26 +35,33 @@ from .datetime_utils import datetime_tz_to_utc
from .exiftool import ExifTool, ExifToolCaching, exiftool_can_write, get_exiftool_path from .exiftool import ExifTool, ExifToolCaching, exiftool_can_write, get_exiftool_path
from .export_db import ExportDB, ExportDBTemp from .export_db import ExportDB, ExportDBTemp
from .fileutil import FileUtil from .fileutil import FileUtil
from .photokit import (
PHOTOS_VERSION_CURRENT,
PHOTOS_VERSION_ORIGINAL,
PHOTOS_VERSION_UNADJUSTED,
PhotoKitFetchFailed,
PhotoLibrary,
)
from .phototemplate import RenderOptions from .phototemplate import RenderOptions
from .rich_utils import add_rich_markup_tag from .rich_utils import add_rich_markup_tag
from .uti import get_preferred_uti_extension from .uti import get_preferred_uti_extension
from .utils import ( from .utils import (
is_macos,
hexdigest, hexdigest,
increment_filename, increment_filename,
increment_filename_with_count, increment_filename_with_count,
lineno, lineno,
list_directory, list_directory,
lock_filename, lock_filename,
normalize_fs_path,
unlock_filename, unlock_filename,
) )
if is_macos:
import photoscript
from .photokit import (
PHOTOS_VERSION_CURRENT,
PHOTOS_VERSION_ORIGINAL,
PHOTOS_VERSION_UNADJUSTED,
PhotoKitFetchFailed,
PhotoLibrary,
)
__all__ = [ __all__ = [
"ExportError", "ExportError",
"ExportOptions", "ExportOptions",
@ -721,7 +728,6 @@ class PhotoExporter:
self, src: pathlib.Path, dest: pathlib.Path, options: ExportOptions self, src: pathlib.Path, dest: pathlib.Path, options: ExportOptions
) -> t.Literal[True, False]: ) -> t.Literal[True, False]:
"""Return True if photo should be updated, else False""" """Return True if photo should be updated, else False"""
# NOTE: The order of certain checks is important # NOTE: The order of certain checks is important
# read the comments below to understand why before changing # read the comments below to understand why before changing
@ -1181,7 +1187,7 @@ class PhotoExporter:
try: try:
fileutil.copy(src, dest_str) fileutil.copy(src, dest_str)
verbose( verbose(
f"Exported {self._filename(self.photo.original_filename)} to {self._filepath(dest_str)}" f"Exported {self._filename(self.photo.original_filename)} to {self._filepath(normalize_fs_path(dest_str))}"
) )
except Exception as e: except Exception as e:
raise ExportError( raise ExportError(

View File

@ -20,7 +20,6 @@ from types import SimpleNamespace
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
import yaml import yaml
from osxmetadata import OSXMetaData
import osxphotos import osxphotos
@ -65,9 +64,12 @@ from .placeinfo import PlaceInfo4, PlaceInfo5
from .query_builder import get_query from .query_builder import get_query
from .scoreinfo import ScoreInfo from .scoreinfo import ScoreInfo
from .searchinfo import SearchInfo from .searchinfo import SearchInfo
from .text_detection import detect_text
from .uti import get_preferred_uti_extension, get_uti_for_extension from .uti import get_preferred_uti_extension, get_uti_for_extension
from .utils import _get_resource_loc, hexdigest, list_directory from .utils import _get_resource_loc, assert_macos, is_macos, hexdigest, list_directory
if is_macos:
from osxmetadata import OSXMetaData
from .text_detection import detect_text
__all__ = ["PhotoInfo", "PhotoInfoNone", "frozen_photoinfo_factory"] __all__ = ["PhotoInfo", "PhotoInfoNone", "frozen_photoinfo_factory"]
@ -1461,6 +1463,8 @@ class PhotoInfo:
def _detected_text(self): def _detected_text(self):
"""detect text in photo, either from cached extended attribute or by attempting text detection""" """detect text in photo, either from cached extended attribute or by attempting text detection"""
assert_macos()
path = ( path = (
self.path_edited if self.hasadjustments and self.path_edited else self.path self.path_edited if self.hasadjustments and self.path_edited else self.path
) )

View File

@ -19,9 +19,12 @@
import copy import copy
import pathlib import pathlib
import sys
import threading import threading
import time import time
assert(sys.platform == "darwin")
import AVFoundation import AVFoundation
import CoreServices import CoreServices
import Foundation import Foundation

View File

@ -2,12 +2,15 @@
from typing import List, Optional from typing import List, Optional
import photoscript
from more_itertools import chunked from more_itertools import chunked
from photoscript import Album, Folder, Photo, PhotosLibrary
from .photoinfo import PhotoInfo from .photoinfo import PhotoInfo
from .utils import noop, pluralize from .utils import assert_macos, noop, pluralize
assert_macos()
import photoscript
from photoscript import Album, Folder, Photo, PhotosLibrary
__all__ = ["PhotosAlbum", "PhotosAlbumPhotoScript"] __all__ = ["PhotosAlbum", "PhotosAlbumPhotoScript"]

View File

@ -4,6 +4,10 @@ from __future__ import annotations
import sqlite3 import sqlite3
from .utils import assert_macos
assert_macos()
import photoscript import photoscript
from ._constants import _DB_TABLE_NAMES, _PHOTOS_5_ALBUM_KIND, _PHOTOS_5_FOLDER_KIND from ._constants import _DB_TABLE_NAMES, _PHOTOS_5_ALBUM_KIND, _PHOTOS_5_FOLDER_KIND

View File

@ -21,7 +21,6 @@ from typing import Any, List, Optional
from unicodedata import normalize from unicodedata import normalize
import bitmath import bitmath
import photoscript
from rich import print from rich import print
from .._constants import ( from .._constants import (
@ -62,6 +61,7 @@ from ..rich_utils import add_rich_markup_tag
from ..sqlite_utils import sqlite_db_is_locked, sqlite_open_ro from ..sqlite_utils import sqlite_db_is_locked, sqlite_open_ro
from ..utils import ( from ..utils import (
_check_file_exists, _check_file_exists,
is_macos,
get_macos_version, get_macos_version,
get_last_library_path, get_last_library_path,
noop, noop,
@ -69,6 +69,9 @@ from ..utils import (
) )
from .photosdb_utils import get_db_model_version, get_db_version from .photosdb_utils import get_db_model_version, get_db_version
if is_macos:
import photoscript
logger = logging.getLogger("osxphotos") logger = logging.getLogger("osxphotos")
__all__ = ["PhotosDB"] __all__ = ["PhotosDB"]
@ -118,8 +121,8 @@ class PhotosDB:
# Check OS version # Check OS version
system = platform.system() system = platform.system()
(ver, major, _) = get_macos_version() (ver, major, _) = get_macos_version() if is_macos else (None, None, None)
if system != "Darwin" or ((ver, major) not in _TESTED_OS_VERSIONS): if system == "Darwin" and ((ver, major) not in _TESTED_OS_VERSIONS):
logging.warning( logging.warning(
f"WARNING: This module has only been tested with macOS versions " f"WARNING: This module has only been tested with macOS versions "
f"[{', '.join(f'{v}.{m}' for (v, m) in _TESTED_OS_VERSIONS)}]: " f"[{', '.join(f'{v}.{m}' for (v, m) in _TESTED_OS_VERSIONS)}]: "

View File

@ -21,7 +21,6 @@ from ._version import __version__
from .datetime_formatter import DateTimeFormatter from .datetime_formatter import DateTimeFormatter
from .exiftool import ExifToolCaching from .exiftool import ExifToolCaching
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
from .text_detection import detect_text
from .utils import expand_and_validate_filepath, load_function, uuid_to_shortuuid from .utils import expand_and_validate_filepath, load_function, uuid_to_shortuuid
__all__ = [ __all__ = [

View File

@ -1,8 +1,13 @@
""" Use Apple's Vision Framework via PyObjC to perform text detection on images (macOS 10.15+ only) """ """ Use Apple's Vision Framework via PyObjC to perform text detection on images (macOS 10.15+ only) """
import logging import logging
import sys
from typing import List, Optional from typing import List, Optional
from .utils import assert_macos, get_macos_version
assert_macos()
import objc import objc
import Quartz import Quartz
from Cocoa import NSURL from Cocoa import NSURL
@ -11,8 +16,6 @@ from Foundation import NSDictionary
# needed to capture system-level stderr # needed to capture system-level stderr
from wurlitzer import pipes from wurlitzer import pipes
from .utils import get_macos_version
__all__ = ["detect_text", "make_request_handler"] __all__ = ["detect_text", "make_request_handler"]
ver, major, minor = get_macos_version() ver, major, minor = get_macos_version()

View File

@ -2,13 +2,7 @@
from typing import Union from typing import Union
import Foundation from .utils import is_macos
import objc
def known_timezone_names():
"""Get list of valid timezones on macOS"""
return Foundation.NSTimeZone.knownTimeZoneNames()
def format_offset_time(offset: int) -> str: def format_offset_time(offset: int) -> str:
@ -19,43 +13,107 @@ def format_offset_time(offset: int) -> str:
return f"{sign}{hours:02d}:{minutes:02d}" return f"{sign}{hours:02d}:{minutes:02d}"
class Timezone: if is_macos:
"""Create Timezone object from either name (str) or offset from GMT (int)""" import Foundation
import objc
def __init__(self, tz: Union[str, int]):
with objc.autorelease_pool(): def known_timezone_names():
"""Get list of valid timezones on macOS"""
return Foundation.NSTimeZone.knownTimeZoneNames()
class Timezone:
"""Create Timezone object from either name (str) or offset from GMT (int)"""
def __init__(self, tz: Union[str, int]):
with objc.autorelease_pool():
if isinstance(tz, str):
self.timezone = Foundation.NSTimeZone.timeZoneWithName_(tz)
self._name = tz
elif isinstance(tz, int):
self.timezone = Foundation.NSTimeZone.timeZoneForSecondsFromGMT_(tz)
self._name = self.timezone.name()
else:
raise TypeError("Timezone must be a string or an int")
@property
def name(self) -> str:
return self._name
@property
def offset(self) -> int:
return self.timezone.secondsFromGMT()
@property
def offset_str(self) -> str:
return format_offset_time(self.offset)
@property
def abbreviation(self) -> str:
return self.timezone.abbreviation()
def __str__(self):
return self.name
def __repr__(self):
return self.name
def __eq__(self, other):
if isinstance(other, Timezone):
return self.timezone == other.timezone
return False
else:
import zoneinfo
from datetime import datetime
def known_timezone_names():
"""Get list of valid timezones"""
return zoneinfo.available_timezones()
class Timezone:
"""Create Timezone object from either name (str) or offset from GMT (int)"""
def __init__(self, tz: Union[str, int]):
if isinstance(tz, str): if isinstance(tz, str):
self.timezone = Foundation.NSTimeZone.timeZoneWithName_(tz) self.timezone = zoneinfo.ZoneInfo(tz)
self._name = tz self._name = tz
elif isinstance(tz, int): elif isinstance(tz, int):
self.timezone = Foundation.NSTimeZone.timeZoneForSecondsFromGMT_(tz) if tz > 0:
self._name = self.timezone.name() name = f"Etc/GMT+{tz // 3600}"
else:
name = f"Etc/GMT-{-tz // 3600}"
self.timezone = zoneinfo.ZoneInfo(name)
self._name = self.timezone.key
else: else:
raise TypeError("Timezone must be a string or an int") raise TypeError("Timezone must be a string or an int")
@property @property
def name(self) -> str: def name(self) -> str:
return self._name return self._name
@property @property
def offset(self) -> int: def offset(self) -> int:
return self.timezone.secondsFromGMT() td = self.timezone.utcoffset(datetime.now())
assert td
return int(td.total_seconds())
@property @property
def offset_str(self) -> str: def offset_str(self) -> str:
return format_offset_time(self.offset) return format_offset_time(self.offset)
@property @property
def abbreviation(self) -> str: def abbreviation(self) -> str:
return self.timezone.abbreviation() return self.timezone.key
def __str__(self): def __str__(self):
return self.name return self.name
def __repr__(self): def __repr__(self):
return self.name return self.name
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, Timezone): if isinstance(other, Timezone):
return self.timezone == other.timezone return self.timezone == other.timezone
return False return False

View File

@ -1,7 +1,5 @@
""" get UTI for a given file extension and the preferred extension for a given UTI """ get UTI for a given file extension and the preferred extension for a given UTI
Implementation note: runs only on macOS
On macOS <= 11 (Big Sur), uses objective C CoreServices methods On macOS <= 11 (Big Sur), uses objective C CoreServices methods
UTTypeCopyPreferredTagWithClass and UTTypeCreatePreferredIdentifierForTag to retrieve the UTTypeCopyPreferredTagWithClass and UTTypeCreatePreferredIdentifierForTag to retrieve the
UTI and the extension. These are deprecated in 10.15 (Catalina) and no longer supported on Monterey. UTI and the extension. These are deprecated in 10.15 (Catalina) and no longer supported on Monterey.
@ -13,6 +11,8 @@ Implementation note: runs only on macOS
works for the extension -> UTI lookup. On Monterey, if there is no cached value for UTI -> extension lookup, works for the extension -> UTI lookup. On Monterey, if there is no cached value for UTI -> extension lookup,
returns None. returns None.
Outside of macOS uses only the hardcoded list of UTIs.
It's a bit hacky but best I can think of to make this robust on different versions of macOS. PRs welcome. It's a bit hacky but best I can think of to make this robust on different versions of macOS. PRs welcome.
""" """
@ -21,12 +21,14 @@ from __future__ import annotations
import csv import csv
import re import re
import subprocess import subprocess
import sys
import tempfile import tempfile
import CoreServices from .utils import assert_macos, is_macos, get_macos_version
import objc
from .utils import get_macos_version if is_macos:
import CoreServices
import objc
__all__ = ["get_preferred_uti_extension", "get_uti_for_extension"] __all__ = ["get_preferred_uti_extension", "get_uti_for_extension"]
@ -518,11 +520,10 @@ def _load_uti_dict():
EXT_UTI_DICT[row["extension"]] = row["UTI"] EXT_UTI_DICT[row["extension"]] = row["UTI"]
UTI_EXT_DICT[row["UTI"]] = row["preferred_extension"] UTI_EXT_DICT[row["UTI"]] = row["preferred_extension"]
_load_uti_dict() _load_uti_dict()
# OS version for determining which methods can be used # OS version for determining which methods can be used
OS_VER, OS_MAJOR, _ = (int(x) for x in get_macos_version()) OS_VER, OS_MAJOR, _ = (int(x) for x in get_macos_version()) if is_macos else (None, None, None)
def _get_uti_from_mdls(extension): def _get_uti_from_mdls(extension):
@ -532,6 +533,9 @@ def _get_uti_from_mdls(extension):
# mdls -name kMDItemContentType foo.3fr # mdls -name kMDItemContentType foo.3fr
# kMDItemContentType = "com.hasselblad.3fr-raw-image" # kMDItemContentType = "com.hasselblad.3fr-raw-image"
if not is_macos:
return None
try: try:
with tempfile.NamedTemporaryFile(suffix="." + extension) as temp: with tempfile.NamedTemporaryFile(suffix="." + extension) as temp:
output = subprocess.check_output( output = subprocess.check_output(
@ -573,7 +577,7 @@ def get_preferred_uti_extension(uti: str) -> str | None:
uti: UTI str, e.g. 'public.jpeg' uti: UTI str, e.g. 'public.jpeg'
returns: preferred extension as str or None if cannot be determined""" returns: preferred extension as str or None if cannot be determined"""
if (OS_VER, OS_MAJOR) <= (10, 16): if is_macos and (OS_VER, OS_MAJOR) <= (10, 16):
# reference: https://developer.apple.com/documentation/coreservices/1442744-uttypecopypreferredtagwithclass?language=objc # reference: https://developer.apple.com/documentation/coreservices/1442744-uttypecopypreferredtagwithclass?language=objc
# deprecated in Catalina+, likely won't work at all on macOS 12 # deprecated in Catalina+, likely won't work at all on macOS 12
with objc.autorelease_pool(): with objc.autorelease_pool():
@ -602,7 +606,7 @@ def get_uti_for_extension(extension):
if extension[0] == ".": if extension[0] == ".":
extension = extension[1:] extension = extension[1:]
if (OS_VER, OS_MAJOR) <= (10, 16): if is_macos and (OS_VER, OS_MAJOR) <= (10, 16):
# https://developer.apple.com/documentation/coreservices/1448939-uttypecreatepreferredidentifierf # https://developer.apple.com/documentation/coreservices/1448939-uttypecreatepreferredidentifierf
with objc.autorelease_pool(): with objc.autorelease_pool():
uti = CoreServices.UTTypeCreatePreferredIdentifierForTag( uti = CoreServices.UTTypeCreatePreferredIdentifierForTag(

View File

@ -16,10 +16,9 @@ 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, List, Optional, Tuple, Union from typing import Any, Callable, List, Optional, Tuple, TypeVar, Union
from uuid import UUID from uuid import UUID
import CoreFoundation
import requests import requests
import shortuuid import shortuuid
@ -28,6 +27,8 @@ from ._constants import UNICODE_FORMAT
logger = logging.getLogger("osxphotos") logger = logging.getLogger("osxphotos")
__all__ = [ __all__ = [
"is_macos",
"assert_macos",
"dd_to_dms_str", "dd_to_dms_str",
"expand_and_validate_filepath", "expand_and_validate_filepath",
"get_last_library_path", "get_last_library_path",
@ -53,6 +54,16 @@ __all__ = [
VERSION_INFO_URL = "https://pypi.org/pypi/osxphotos/json" VERSION_INFO_URL = "https://pypi.org/pypi/osxphotos/json"
is_macos = sys.platform == "darwin"
def assert_macos():
assert is_macos, "This feature only runs on macOS"
if is_macos:
import CoreFoundation
def noop(*args, **kwargs): def noop(*args, **kwargs):
"""do nothing (no operation)""" """do nothing (no operation)"""
pass pass
@ -67,6 +78,7 @@ def lineno(filename):
def get_macos_version(): def get_macos_version():
assert_macos()
# returns tuple of str containing OS version # returns tuple of str containing OS version
# e.g. 10.13.6 = ("10", "13", "6") # e.g. 10.13.6 = ("10", "13", "6")
version = platform.mac_ver()[0].split(".") version = platform.mac_ver()[0].split(".")
@ -166,6 +178,8 @@ 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 """ """ only works on MacOS 10.15 """
""" on earlier versions, returns None """ """ on earlier versions, returns None """
if not is_macos:
return None
_, major, _ = get_macos_version() _, major, _ = get_macos_version()
if int(major) < 15: if int(major) < 15:
logger.debug( logger.debug(
@ -241,6 +255,8 @@ def get_last_library_path():
def list_photo_libraries(): 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 MacOS < 10.15, this may omit some libraries """
if not is_macos:
return []
# On 10.15, mdfind appears to find all libraries # On 10.15, mdfind appears to find all libraries
# On older MacOS versions, mdfind appears to ignore some libraries # On older MacOS versions, mdfind appears to ignore some libraries
@ -263,11 +279,14 @@ def list_photo_libraries():
return lib_list return lib_list
def normalize_fs_path(path: str) -> str: T = TypeVar("T", bound=Union[str, pathlib.Path])
def normalize_fs_path(path: T) -> T:
"""Normalize filesystem paths with unicode in them""" """Normalize filesystem paths with unicode in them"""
# macOS HFS+ uses NFD, APFS doesn't normalize but stick with NFD form = "NFD" if is_macos else "NFC"
# ref: https://eclecticlight.co/2021/05/08/explainer-unicode-normalization-and-apfs/ if isinstance(path, pathlib.Path):
return unicodedata.normalize("NFD", path) return pathlib.Path(unicodedata.normalize(form, str(path)))
else:
return unicodedata.normalize(form, path)
# def findfiles(pattern, path): # def findfiles(pattern, path):
@ -356,7 +375,7 @@ def list_directory(
return files return files
def normalize_unicode(value): def normalize_unicode(value) -> Any:
"""normalize unicode data""" """normalize unicode data"""
if value is None: if value is None:
return None return None

View File

@ -5,14 +5,19 @@ import shutil
import tempfile import tempfile
import time import time
import photoscript
import pytest import pytest
from applescript import AppleScript
from photoscript.utils import ditto from osxphotos.utils import is_macos
if is_macos:
import photoscript
from applescript import AppleScript
from photoscript.utils import ditto
from .test_catalina_10_15_7 import UUID_DICT_LOCAL
from osxphotos.exiftool import _ExifToolProc from osxphotos.exiftool import _ExifToolProc
from .test_catalina_10_15_7 import UUID_DICT_LOCAL
# run timewarp tests (configured with --timewarp) # run timewarp tests (configured with --timewarp)
TEST_TIMEWARP = False TEST_TIMEWARP = False
@ -34,6 +39,9 @@ NO_CLEANUP = False
def get_os_version(): def get_os_version():
if not is_macos:
return (None, None, None)
import platform import platform
# returns tuple containing OS version # returns tuple containing OS version
@ -53,7 +61,7 @@ def get_os_version():
return (ver, major, minor) return (ver, major, minor)
OS_VER = get_os_version() OS_VER = get_os_version() if is_macos else [None, None]
if OS_VER[0] == "10" and OS_VER[1] == "15": if OS_VER[0] == "10" and OS_VER[1] == "15":
# Catalina # Catalina
TEST_LIBRARY = "tests/Test-10.15.7.photoslibrary" TEST_LIBRARY = "tests/Test-10.15.7.photoslibrary"
@ -77,28 +85,28 @@ else:
TEST_LIBRARY_ADD_LOCATIONS = None TEST_LIBRARY_ADD_LOCATIONS = None
@pytest.fixture(scope="session", autouse=True) @pytest.fixture(scope="session", autouse=is_macos)
def setup_photos_timewarp(): def setup_photos_timewarp():
if not TEST_TIMEWARP: if not TEST_TIMEWARP:
return return
copy_photos_library(TEST_LIBRARY_TIMEWARP, delay=5) copy_photos_library(TEST_LIBRARY_TIMEWARP, delay=5)
@pytest.fixture(scope="session", autouse=True) @pytest.fixture(scope="session", autouse=is_macos)
def setup_photos_import(): def setup_photos_import():
if not TEST_IMPORT: if not TEST_IMPORT:
return return
copy_photos_library(TEST_LIBRARY_IMPORT, delay=10) copy_photos_library(TEST_LIBRARY_IMPORT, delay=10)
@pytest.fixture(scope="session", autouse=True) @pytest.fixture(scope="session", autouse=is_macos)
def setup_photos_sync(): def setup_photos_sync():
if not TEST_SYNC: if not TEST_SYNC:
return return
copy_photos_library(TEST_LIBRARY_SYNC, delay=10) copy_photos_library(TEST_LIBRARY_SYNC, delay=10)
@pytest.fixture(scope="session", autouse=True) @pytest.fixture(scope="session", autouse=is_macos)
def setup_photos_add_locations(): def setup_photos_add_locations():
if not TEST_ADD_LOCATIONS: if not TEST_ADD_LOCATIONS:
return return
@ -312,7 +320,10 @@ def addalbum_library():
def copy_photos_library_to_path(photos_library_path: str, dest_path: str) -> str: def copy_photos_library_to_path(photos_library_path: str, dest_path: str) -> str:
"""Copy a photos library to a folder""" """Copy a photos library to a folder"""
ditto(photos_library_path, dest_path) if is_macos:
ditto(photos_library_path, dest_path)
else:
shutil.copytree(photos_library_path, dest_path)
return dest_path return dest_path

16
tests/locale_util.py Normal file
View File

@ -0,0 +1,16 @@
""" Helpers for running locale-dependent tests """
import contextlib
import locale
import pytest
def setlocale(typ, name):
try:
with contextlib.suppress(Exception):
locale.setlocale(typ, name)
# On Linux UTF-8 locales are separate
locale.setlocale(typ, f"{name}.UTF-8")
except locale.Error:
pytest.skip(f"Locale {name} not available")

View File

@ -15,9 +15,9 @@ import pytest
import osxphotos import osxphotos
from osxphotos._constants import _UNKNOWN_PERSON from osxphotos._constants import _UNKNOWN_PERSON
from osxphotos.photoexporter import PhotoExporter from osxphotos.photoexporter import PhotoExporter
from osxphotos.utils import get_macos_version from osxphotos.utils import is_macos, get_macos_version
OS_VERSION = get_macos_version() OS_VERSION = get_macos_version() if is_macos else (None, None, None)
SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "15" SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "15"
PHOTOS_DB_LOCAL = os.path.expanduser("~/Pictures/Photos Library.photoslibrary") PHOTOS_DB_LOCAL = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
@ -1448,6 +1448,7 @@ def test_multi_uuid(photosdb):
assert len(photos) == 2 assert len(photos) == 2
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
def test_detected_text(photosdb): def test_detected_text(photosdb):
"""test PhotoInfo.detected_text""" """test PhotoInfo.detected_text"""
for uuid, expected_text in UUID_DETECTED_TEXT.items(): for uuid, expected_text in UUID_DETECTED_TEXT.items():

View File

@ -14,10 +14,10 @@ import subprocess
import tempfile import tempfile
import time import time
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from bitmath import contextlib
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
from osxmetadata import OSXMetaData, Tag
import osxphotos import osxphotos
from osxphotos._constants import OSXPHOTOS_EXPORT_DB from osxphotos._constants import OSXPHOTOS_EXPORT_DB
@ -36,9 +36,16 @@ from osxphotos.cli import (
) )
from osxphotos.exiftool import ExifTool, get_exiftool_path from osxphotos.exiftool import ExifTool, get_exiftool_path
from osxphotos.fileutil import FileUtil from osxphotos.fileutil import FileUtil
from osxphotos.utils import noop, normalize_fs_path, normalize_unicode from osxphotos.utils import is_macos, noop, normalize_fs_path, normalize_unicode
if is_macos:
from osxmetadata import OSXMetaData, Tag
from .conftest import copy_photos_library_to_path from .conftest import copy_photos_library_to_path
from .locale_util import setlocale
def _normalize_fs_paths(paths):
"""Small helper to prepare path strings for test"""
return [normalize_fs_path(p) for p in paths]
CLI_PHOTOS_DB = "tests/Test-10.15.7.photoslibrary" CLI_PHOTOS_DB = "tests/Test-10.15.7.photoslibrary"
LIVE_PHOTOS_DB = "tests/Test-Cloud-10.15.1.photoslibrary" LIVE_PHOTOS_DB = "tests/Test-Cloud-10.15.1.photoslibrary"
@ -66,7 +73,7 @@ SKIP_UUID_FILE = "tests/skip_uuid_from_file.txt"
CLI_OUTPUT_QUERY_UUID = '[{"uuid": "D79B8D77-BFFC-460B-9312-034F2877D35B", "filename": "D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg", "original_filename": "Pumkins2.jpg", "date": "2018-09-28T16:07:07-04:00", "description": "Girl holding pumpkin", "title": "I found one!", "keywords": ["Kids"], "albums": ["Pumpkin Farm", "Test Album", "Multi Keyword"], "persons": ["Katie"], "path": "/tests/Test-10.15.7.photoslibrary/originals/D/D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg", "ismissing": false, "hasadjustments": false, "external_edit": false, "favorite": false, "hidden": false, "latitude": 41.256566, "longitude": -95.940257, "path_edited": null, "shared": false, "isphoto": true, "ismovie": false, "uti": "public.jpeg", "burst": false, "live_photo": false, "path_live_photo": null, "iscloudasset": false, "incloud": null}]' CLI_OUTPUT_QUERY_UUID = '[{"uuid": "D79B8D77-BFFC-460B-9312-034F2877D35B", "filename": "D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg", "original_filename": "Pumkins2.jpg", "date": "2018-09-28T16:07:07-04:00", "description": "Girl holding pumpkin", "title": "I found one!", "keywords": ["Kids"], "albums": ["Pumpkin Farm", "Test Album", "Multi Keyword"], "persons": ["Katie"], "path": "/tests/Test-10.15.7.photoslibrary/originals/D/D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg", "ismissing": false, "hasadjustments": false, "external_edit": false, "favorite": false, "hidden": false, "latitude": 41.256566, "longitude": -95.940257, "path_edited": null, "shared": false, "isphoto": true, "ismovie": false, "uti": "public.jpeg", "burst": false, "live_photo": false, "path_live_photo": null, "iscloudasset": false, "incloud": null}]'
CLI_EXPORT_FILENAMES = [ CLI_EXPORT_FILENAMES = _normalize_fs_paths([
"[2020-08-29] AAF035 (1).jpg", "[2020-08-29] AAF035 (1).jpg",
"[2020-08-29] AAF035 (2).jpg", "[2020-08-29] AAF035 (2).jpg",
"[2020-08-29] AAF035 (3).jpg", "[2020-08-29] AAF035 (3).jpg",
@ -100,10 +107,10 @@ CLI_EXPORT_FILENAMES = [
"wedding.jpg", "wedding.jpg",
"winebottle (1).jpeg", "winebottle (1).jpeg",
"winebottle.jpeg", "winebottle.jpeg",
] ])
CLI_EXPORT_FILENAMES_DRY_RUN = [ CLI_EXPORT_FILENAMES_DRY_RUN = _normalize_fs_paths([
"[2020-08-29] AAF035.jpg", "[2020-08-29] AAF035.jpg",
"DSC03584.dng", "DSC03584.dng",
"Frítest_edited.jpeg", "Frítest_edited.jpeg",
@ -130,15 +137,25 @@ CLI_EXPORT_FILENAMES_DRY_RUN = [
"wedding.jpg", "wedding.jpg",
"winebottle.jpeg", "winebottle.jpeg",
"winebottle.jpeg", "winebottle.jpeg",
] ])
CLI_EXPORT_IGNORE_SIGNATURE_FILENAMES = ["Tulips.jpg", "wedding.jpg"] CLI_EXPORT_IGNORE_SIGNATURE_FILENAMES = _normalize_fs_paths([
"Tulips.jpg",
"wedding.jpg"
])
CLI_EXPORT_FILENAMES_ALBUM = ["Pumkins1.jpg", "Pumkins2.jpg", "Pumpkins3.jpg"] CLI_EXPORT_FILENAMES_ALBUM = _normalize_fs_paths([
"Pumkins1.jpg",
"Pumkins2.jpg",
"Pumpkins3.jpg"
])
CLI_EXPORT_FILENAMES_ALBUM_UNICODE = ["IMG_4547.jpg"] CLI_EXPORT_FILENAMES_ALBUM_UNICODE = _normalize_fs_paths(["IMG_4547.jpg"])
CLI_EXPORT_FILENAMES_DELETED_TWIN = ["wedding.jpg", "wedding_edited.jpeg"] CLI_EXPORT_FILENAMES_DELETED_TWIN = _normalize_fs_paths([
"wedding.jpg",
"wedding_edited.jpeg"
])
CLI_EXPORT_EDITED_SUFFIX = "_bearbeiten" CLI_EXPORT_EDITED_SUFFIX = "_bearbeiten"
CLI_EXPORT_EDITED_SUFFIX_TEMPLATE = "{edited?_edited,}" CLI_EXPORT_EDITED_SUFFIX_TEMPLATE = "{edited?_edited,}"
@ -146,7 +163,7 @@ CLI_EXPORT_ORIGINAL_SUFFIX = "_original"
CLI_EXPORT_ORIGINAL_SUFFIX_TEMPLATE = "{edited?_original,}" CLI_EXPORT_ORIGINAL_SUFFIX_TEMPLATE = "{edited?_original,}"
CLI_EXPORT_PREVIEW_SUFFIX = "_lowres" CLI_EXPORT_PREVIEW_SUFFIX = "_lowres"
CLI_EXPORT_FILENAMES_EDITED_SUFFIX = [ CLI_EXPORT_FILENAMES_EDITED_SUFFIX = _normalize_fs_paths([
"[2020-08-29] AAF035 (1).jpg", "[2020-08-29] AAF035 (1).jpg",
"[2020-08-29] AAF035 (2).jpg", "[2020-08-29] AAF035 (2).jpg",
"[2020-08-29] AAF035 (3).jpg", "[2020-08-29] AAF035 (3).jpg",
@ -180,9 +197,9 @@ CLI_EXPORT_FILENAMES_EDITED_SUFFIX = [
"wedding.jpg", "wedding.jpg",
"winebottle (1).jpeg", "winebottle (1).jpeg",
"winebottle.jpeg", "winebottle.jpeg",
] ])
CLI_EXPORT_FILENAMES_EDITED_SUFFIX_TEMPLATE = [ CLI_EXPORT_FILENAMES_EDITED_SUFFIX_TEMPLATE = _normalize_fs_paths([
"[2020-08-29] AAF035 (1).jpg", "[2020-08-29] AAF035 (1).jpg",
"[2020-08-29] AAF035 (2).jpg", "[2020-08-29] AAF035 (2).jpg",
"[2020-08-29] AAF035 (3).jpg", "[2020-08-29] AAF035 (3).jpg",
@ -216,9 +233,9 @@ CLI_EXPORT_FILENAMES_EDITED_SUFFIX_TEMPLATE = [
"wedding.jpg", "wedding.jpg",
"winebottle (1).jpeg", "winebottle (1).jpeg",
"winebottle.jpeg", "winebottle.jpeg",
] ])
CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX = [ CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX = _normalize_fs_paths([
"[2020-08-29] AAF035_original (1).jpg", "[2020-08-29] AAF035_original (1).jpg",
"[2020-08-29] AAF035_original (2).jpg", "[2020-08-29] AAF035_original (2).jpg",
"[2020-08-29] AAF035_original (3).jpg", "[2020-08-29] AAF035_original (3).jpg",
@ -252,9 +269,9 @@ CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX = [
"wedding_original.jpg", "wedding_original.jpg",
"winebottle_original (1).jpeg", "winebottle_original (1).jpeg",
"winebottle_original.jpeg", "winebottle_original.jpeg",
] ])
CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX_TEMPLATE = [ CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX_TEMPLATE = _normalize_fs_paths([
"[2020-08-29] AAF035 (1).jpg", "[2020-08-29] AAF035 (1).jpg",
"[2020-08-29] AAF035 (2).jpg", "[2020-08-29] AAF035 (2).jpg",
"[2020-08-29] AAF035 (3).jpg", "[2020-08-29] AAF035 (3).jpg",
@ -288,7 +305,7 @@ CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX_TEMPLATE = [
"wedding_original.jpg", "wedding_original.jpg",
"winebottle (1).jpeg", "winebottle (1).jpeg",
"winebottle.jpeg", "winebottle.jpeg",
] ])
CLI_EXPORT_FILENAMES_CURRENT = [ CLI_EXPORT_FILENAMES_CURRENT = [
"1793FAAB-DE75-4E25-886C-2BD66C780D6A_edited.jpeg", # Frítest.jpg "1793FAAB-DE75-4E25-886C-2BD66C780D6A_edited.jpeg", # Frítest.jpg
@ -326,7 +343,7 @@ CLI_EXPORT_FILENAMES_CURRENT = [
"F207D5DE-EFAD-4217-8424-0764AAC971D0.jpeg", "F207D5DE-EFAD-4217-8424-0764AAC971D0.jpeg",
] ]
CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG = [ CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG = _normalize_fs_paths([
"[2020-08-29] AAF035 (1).jpg", "[2020-08-29] AAF035 (1).jpg",
"[2020-08-29] AAF035 (2).jpg", "[2020-08-29] AAF035 (2).jpg",
"[2020-08-29] AAF035 (3).jpg", "[2020-08-29] AAF035 (3).jpg",
@ -360,9 +377,9 @@ CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG = [
"wedding.jpg", "wedding.jpg",
"winebottle (1).jpeg", "winebottle (1).jpeg",
"winebottle.jpeg", "winebottle.jpeg",
] ])
CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG_SKIP_RAW = [ CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG_SKIP_RAW = _normalize_fs_paths([
"[2020-08-29] AAF035 (1).jpg", "[2020-08-29] AAF035 (1).jpg",
"[2020-08-29] AAF035 (2).jpg", "[2020-08-29] AAF035 (2).jpg",
"[2020-08-29] AAF035 (3).jpg", "[2020-08-29] AAF035 (3).jpg",
@ -394,26 +411,26 @@ CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG_SKIP_RAW = [
"wedding.jpg", "wedding.jpg",
"winebottle (1).jpeg", "winebottle (1).jpeg",
"winebottle.jpeg", "winebottle.jpeg",
] ])
CLI_EXPORT_CONVERT_TO_JPEG_LARGE_FILE = "DSC03584.jpeg" CLI_EXPORT_CONVERT_TO_JPEG_LARGE_FILE = "DSC03584.jpeg"
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES1 = [ CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES1 = _normalize_fs_paths([
"2019/April/wedding.jpg", "2019/April/wedding.jpg",
"2019/July/Tulips.jpg", "2019/July/Tulips.jpg",
"2018/October/St James Park.jpg", "2018/October/St James Park.jpg",
"2018/September/Pumpkins3.jpg", "2018/September/Pumpkins3.jpg",
"2018/September/Pumkins2.jpg", "2018/September/Pumkins2.jpg",
"2018/September/Pumkins1.jpg", "2018/September/Pumkins1.jpg",
] ])
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_LOCALE = [ CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_LOCALE = _normalize_fs_paths([
"2019/September/IMG_9975.JPEG", "2019/September/IMG_9975.JPEG",
"2020/Februar/IMG_1064.JPEG", "2020/Februar/IMG_1064.JPEG",
"2016/März/IMG_3984.JPEG", "2016/März/IMG_3984.JPEG",
] ])
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM1 = [ CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM1 = _normalize_fs_paths([
"Multi Keyword/wedding.jpg", "Multi Keyword/wedding.jpg",
"_/Tulips.jpg", "_/Tulips.jpg",
"_/St James Park.jpg", "_/St James Park.jpg",
@ -421,9 +438,9 @@ CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM1 = [
"Pumpkin Farm/Pumkins2.jpg", "Pumpkin Farm/Pumkins2.jpg",
"Pumpkin Farm/Pumkins1.jpg", "Pumpkin Farm/Pumkins1.jpg",
"Test Album/Pumkins1.jpg", "Test Album/Pumkins1.jpg",
] ])
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM2 = [ CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM2 = _normalize_fs_paths([
"Multi Keyword/wedding.jpg", "Multi Keyword/wedding.jpg",
"NOALBUM/Tulips.jpg", "NOALBUM/Tulips.jpg",
"NOALBUM/St James Park.jpg", "NOALBUM/St James Park.jpg",
@ -431,28 +448,28 @@ CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM2 = [
"Pumpkin Farm/Pumkins2.jpg", "Pumpkin Farm/Pumkins2.jpg",
"Pumpkin Farm/Pumkins1.jpg", "Pumpkin Farm/Pumkins1.jpg",
"Test Album/Pumkins1.jpg", "Test Album/Pumkins1.jpg",
] ])
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES2 = [ CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES2 = _normalize_fs_paths([
"St James's Park, Great Britain, Westminster, England, United Kingdom/St James Park.jpg", "St James's Park, Great Britain, Westminster, England, United Kingdom/St James Park.jpg",
"_/Pumpkins3.jpg", "_/Pumpkins3.jpg",
"Omaha, Nebraska, United States/Pumkins2.jpg", "Omaha, Nebraska, United States/Pumkins2.jpg",
"_/Pumkins1.jpg", "_/Pumkins1.jpg",
"_/Tulips.jpg", "_/Tulips.jpg",
"_/wedding.jpg", "_/wedding.jpg",
] ])
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES3 = [ CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES3 = _normalize_fs_paths([
"2019/{foo}/wedding.jpg", "2019/{foo}/wedding.jpg",
"2019/{foo}/Tulips.jpg", "2019/{foo}/Tulips.jpg",
"2018/{foo}/St James Park.jpg", "2018/{foo}/St James Park.jpg",
"2018/{foo}/Pumpkins3.jpg", "2018/{foo}/Pumpkins3.jpg",
"2018/{foo}/Pumkins2.jpg", "2018/{foo}/Pumkins2.jpg",
"2018/{foo}/Pumkins1.jpg", "2018/{foo}/Pumkins1.jpg",
] ])
CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES1 = [ CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES1 = _normalize_fs_paths([
"2019-wedding.jpg", "2019-wedding.jpg",
"2019-wedding_edited.jpeg", "2019-wedding_edited.jpeg",
"2019-Tulips.jpg", "2019-Tulips.jpg",
@ -461,9 +478,9 @@ CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES1 = [
"2018-Pumpkins3.jpg", "2018-Pumpkins3.jpg",
"2018-Pumkins2.jpg", "2018-Pumkins2.jpg",
"2018-Pumkins1.jpg", "2018-Pumkins1.jpg",
] ])
CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES2 = [ CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES2 = _normalize_fs_paths([
"Folder1_SubFolder2_AlbumInFolder-IMG_4547.jpg", "Folder1_SubFolder2_AlbumInFolder-IMG_4547.jpg",
"Folder1_SubFolder2_AlbumInFolder-wedding.jpg", "Folder1_SubFolder2_AlbumInFolder-wedding.jpg",
"Folder1_SubFolder2_AlbumInFolder-wedding_edited.jpeg", "Folder1_SubFolder2_AlbumInFolder-wedding_edited.jpeg",
@ -484,18 +501,18 @@ CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES2 = [
"None-IMG_1693.tif", "None-IMG_1693.tif",
"I have a deleted twin-wedding.jpg", "I have a deleted twin-wedding.jpg",
"I have a deleted twin-wedding_edited.jpeg", "I have a deleted twin-wedding_edited.jpeg",
] ])
CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES_PATHSEP = [ CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES_PATHSEP = _normalize_fs_paths([
"2018-10 - Sponsion, Museum, Frühstück, Römermuseum/IMG_4547.jpg", "2018-10 - Sponsion, Museum, Frühstück, Römermuseum/IMG_4547.jpg",
"Folder1/SubFolder2/AlbumInFolder/IMG_4547.jpg", "Folder1/SubFolder2/AlbumInFolder/IMG_4547.jpg",
"2019-10:11 Paris Clermont/IMG_4547.jpg", "2019-10:11 Paris Clermont/IMG_4547.jpg",
] ])
CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES_KEYWORD_PATHSEP = [ CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES_KEYWORD_PATHSEP = _normalize_fs_paths([
"foo:bar/foo:bar_IMG_3092.heic" "foo:bar/foo:bar_IMG_3092.heic"
] ])
CLI_EXPORTED_FILENAME_TEMPLATE_LONG_DESCRIPTION = [ CLI_EXPORTED_FILENAME_TEMPLATE_LONG_DESCRIPTION = [
"Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo" "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo"
@ -519,46 +536,59 @@ CLI_EXPORT_BY_DATE_TOUCH_UUID = [
"F12384F6-CD17-4151-ACBA-AE0E3688539E", # Pumkins1.jpg "F12384F6-CD17-4151-ACBA-AE0E3688539E", # Pumkins1.jpg
] ]
CLI_EXPORT_BY_DATE_TOUCH_TIMES = [1538165373, 1538163349] CLI_EXPORT_BY_DATE_TOUCH_TIMES = [1538165373, 1538163349]
CLI_EXPORT_BY_DATE_NEED_TOUCH = [ CLI_EXPORT_BY_DATE_NEED_TOUCH = _normalize_fs_paths([
"2018/09/28/Pumkins2.jpg", "2018/09/28/Pumkins2.jpg",
"2018/10/13/St James Park.jpg", "2018/10/13/St James Park.jpg",
] ])
CLI_EXPORT_BY_DATE_NEED_TOUCH_UUID = [ CLI_EXPORT_BY_DATE_NEED_TOUCH_UUID = [
"D79B8D77-BFFC-460B-9312-034F2877D35B", "D79B8D77-BFFC-460B-9312-034F2877D35B",
"DC99FBDD-7A52-4100-A5BB-344131646C30", "DC99FBDD-7A52-4100-A5BB-344131646C30",
] ]
CLI_EXPORT_BY_DATE_NEED_TOUCH_TIMES = [1538165227, 1539436692] CLI_EXPORT_BY_DATE_NEED_TOUCH_TIMES = [1538165227, 1539436692]
CLI_EXPORT_BY_DATE = ["2018/09/28/Pumpkins3.jpg", "2018/09/28/Pumkins1.jpg"] CLI_EXPORT_BY_DATE = _normalize_fs_paths([
"2018/09/28/Pumpkins3.jpg",
"2018/09/28/Pumkins1.jpg",
])
CLI_EXPORT_SIDECAR_FILENAMES = ["Pumkins2.jpg", "Pumkins2.jpg.json", "Pumkins2.jpg.xmp"] CLI_EXPORT_SIDECAR_FILENAMES = _normalize_fs_paths([
CLI_EXPORT_SIDECAR_DROP_EXT_FILENAMES = [ "Pumkins2.jpg",
"Pumkins2.jpg.json",
"Pumkins2.jpg.xmp",
])
CLI_EXPORT_SIDECAR_DROP_EXT_FILENAMES = _normalize_fs_paths([
"Pumkins2.jpg", "Pumkins2.jpg",
"Pumkins2.json", "Pumkins2.json",
"Pumkins2.xmp", "Pumkins2.xmp",
] ])
CLI_EXPORT_LIVE = [ CLI_EXPORT_LIVE = [
"51F2BEF7-431A-4D31-8AC1-3284A57826AE.jpeg", "51F2BEF7-431A-4D31-8AC1-3284A57826AE.jpeg",
"51F2BEF7-431A-4D31-8AC1-3284A57826AE.mov", "51F2BEF7-431A-4D31-8AC1-3284A57826AE.mov",
] ]
CLI_EXPORT_LIVE_ORIGINAL = ["IMG_0728.JPG", "IMG_0728.mov"] CLI_EXPORT_LIVE_ORIGINAL = _normalize_fs_paths([
"IMG_0728.JPG",
"IMG_0728.mov",
])
CLI_EXPORT_RAW = ["441DFE2A-A69B-4C79-A69B-3F51D1B9B29C.cr2"] CLI_EXPORT_RAW = ["441DFE2A-A69B-4C79-A69B-3F51D1B9B29C.cr2"]
CLI_EXPORT_RAW_ORIGINAL = ["IMG_0476_2.CR2"] CLI_EXPORT_RAW_ORIGINAL = _normalize_fs_paths(["IMG_0476_2.CR2"])
CLI_EXPORT_RAW_EDITED = [ CLI_EXPORT_RAW_EDITED = [
"441DFE2A-A69B-4C79-A69B-3F51D1B9B29C.cr2", "441DFE2A-A69B-4C79-A69B-3F51D1B9B29C.cr2",
"441DFE2A-A69B-4C79-A69B-3F51D1B9B29C_edited.jpeg", "441DFE2A-A69B-4C79-A69B-3F51D1B9B29C_edited.jpeg",
] ]
CLI_EXPORT_RAW_EDITED_ORIGINAL = ["IMG_0476_2.CR2", "IMG_0476_2_edited.jpeg"] CLI_EXPORT_RAW_EDITED_ORIGINAL = _normalize_fs_paths([
"IMG_0476_2.CR2",
"IMG_0476_2_edited.jpeg",
])
CLI_UUID_DICT_15_7 = { CLI_UUID_DICT_15_7 = {
"intrash": "71E3E212-00EB-430D-8A63-5E294B268554", "intrash": "71E3E212-00EB-430D-8A63-5E294B268554",
"template": "F12384F6-CD17-4151-ACBA-AE0E3688539E", "template": "F12384F6-CD17-4151-ACBA-AE0E3688539E",
} }
CLI_TEMPLATE_SIDECAR_FILENAME = "Pumkins1.jpg.json" CLI_TEMPLATE_SIDECAR_FILENAME = normalize_fs_path("Pumkins1.jpg.json")
CLI_TEMPLATE_FILENAME = "Pumkins1.jpg" CLI_TEMPLATE_FILENAME = normalize_fs_path("Pumkins1.jpg")
CLI_UUID_DICT_14_6 = {"intrash": "3tljdX43R8+k6peNHVrJNQ"} CLI_UUID_DICT_14_6 = {"intrash": "3tljdX43R8+k6peNHVrJNQ"}
@ -960,12 +990,12 @@ UUID_UNICODE_TITLE = [
"D1D4040D-D141-44E8-93EA-E403D9F63E07", # Frítest.jpg "D1D4040D-D141-44E8-93EA-E403D9F63E07", # Frítest.jpg
] ]
EXPORT_UNICODE_TITLE_FILENAMES = [ EXPORT_UNICODE_TITLE_FILENAMES = _normalize_fs_paths([
"Frítest.jpg", "Frítest.jpg",
"Frítest (1).jpg", "Frítest (1).jpg",
"Frítest (2).jpg", "Frítest (2).jpg",
"Frítest (3).jpg", "Frítest (3).jpg",
] ])
# data for --report # data for --report
UUID_REPORT = [ UUID_REPORT = [
@ -987,12 +1017,12 @@ QUERY_EXIF_DATA_CASE_INSENSITIVE = [
EXPORT_EXIF_DATA = [("EXIF:Make", "FUJIFILM", ["Tulips.jpg", "Tulips_edited.jpeg"])] EXPORT_EXIF_DATA = [("EXIF:Make", "FUJIFILM", ["Tulips.jpg", "Tulips_edited.jpeg"])]
UUID_LIVE_EDITED = "136A78FA-1B90-46CC-88A7-CCA3331F0353" # IMG_4813.HEIC UUID_LIVE_EDITED = "136A78FA-1B90-46CC-88A7-CCA3331F0353" # IMG_4813.HEIC
CLI_EXPORT_LIVE_EDITED = [ CLI_EXPORT_LIVE_EDITED = _normalize_fs_paths([
"IMG_4813.HEIC", "IMG_4813.HEIC",
"IMG_4813.mov", "IMG_4813.mov",
"IMG_4813_edited.jpeg", "IMG_4813_edited.jpeg",
"IMG_4813_edited.mov", "IMG_4813_edited.mov",
] ])
UUID_FAVORITE = "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51" UUID_FAVORITE = "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51"
FILE_FAVORITE = "wedding.jpg" FILE_FAVORITE = "wedding.jpg"
@ -1804,12 +1834,25 @@ def test_export_preview_update():
assert len(files) == 2 # preview + original assert len(files) == 2 # preview + original
@contextlib.contextmanager
def isolated_filesystem_here():
cwd = os.getcwd()
tempdir = tempfile.mkdtemp(dir=cwd) # type: ignore[type-var]
os.chdir(tempdir)
try:
yield tempdir
finally:
os.chdir(cwd)
shutil.rmtree(tempdir)
def test_export_as_hardlink(): def test_export_as_hardlink():
runner = CliRunner() runner = CliRunner()
cwd = os.getcwd() cwd = os.getcwd()
# pylint: disable=not-context-manager # pylint: disable=not-context-manager
with runner.isolated_filesystem(): with isolated_filesystem_here():
result = runner.invoke( result = runner.invoke(
export, export,
[os.path.join(cwd, CLI_PHOTOS_DB), ".", "--export-as-hardlink", "-V"], [os.path.join(cwd, CLI_PHOTOS_DB), ".", "--export-as-hardlink", "-V"],
@ -1829,7 +1872,7 @@ def test_export_as_hardlink_samefile():
photo = photosdb.photos(uuid=[CLI_EXPORT_UUID])[0] photo = photosdb.photos(uuid=[CLI_EXPORT_UUID])[0]
# pylint: disable=not-context-manager # pylint: disable=not-context-manager
with runner.isolated_filesystem(): with isolated_filesystem_here():
result = runner.invoke( result = runner.invoke(
export, export,
[ [
@ -1854,7 +1897,7 @@ def test_export_using_hardlinks_incompat_options():
photo = photosdb.photos(uuid=[CLI_EXPORT_UUID])[0] photo = photosdb.photos(uuid=[CLI_EXPORT_UUID])[0]
# pylint: disable=not-context-manager # pylint: disable=not-context-manager
with runner.isolated_filesystem(): with isolated_filesystem_here():
result = runner.invoke( result = runner.invoke(
export, export,
[ [
@ -3717,7 +3760,7 @@ def test_export_raw_edited_original():
def test_export_directory_template_1(): def test_export_directory_template_1():
# test export using directory template # test export using directory template
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
runner = CliRunner() runner = CliRunner()
cwd = os.getcwd() cwd = os.getcwd()
@ -3837,7 +3880,7 @@ def test_export_directory_template_locale():
with runner.isolated_filesystem(): with runner.isolated_filesystem():
# set locale environment # set locale environment
os.environ["LC_ALL"] = "de_DE.UTF-8" os.environ["LC_ALL"] = "de_DE.UTF-8"
locale.setlocale(locale.LC_ALL, "") setlocale(locale.LC_ALL, "")
result = runner.invoke( result = runner.invoke(
export, export,
[ [
@ -3857,7 +3900,7 @@ def test_export_directory_template_locale():
def test_export_filename_template_1(): def test_export_filename_template_1():
"""export photos using filename template""" """export photos using filename template"""
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
runner = CliRunner() runner = CliRunner()
cwd = os.getcwd() cwd = os.getcwd()
@ -3882,7 +3925,7 @@ def test_export_filename_template_1():
def test_export_filename_template_2(): def test_export_filename_template_2():
"""export photos using filename template with folder_album and path_sep""" """export photos using filename template with folder_album and path_sep"""
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
runner = CliRunner() runner = CliRunner()
cwd = os.getcwd() cwd = os.getcwd()
@ -3907,7 +3950,7 @@ def test_export_filename_template_2():
def test_export_filename_template_strip(): def test_export_filename_template_strip():
"""export photos using filename template with --strip""" """export photos using filename template with --strip"""
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
runner = CliRunner() runner = CliRunner()
cwd = os.getcwd() cwd = os.getcwd()
@ -3933,7 +3976,7 @@ def test_export_filename_template_strip():
def test_export_filename_template_pathsep_in_name_1(): def test_export_filename_template_pathsep_in_name_1():
"""export photos using filename template with folder_album and "/" in album name""" """export photos using filename template with folder_album and "/" in album name"""
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
runner = CliRunner() runner = CliRunner()
cwd = os.getcwd() cwd = os.getcwd()
@ -3960,7 +4003,7 @@ def test_export_filename_template_pathsep_in_name_1():
def test_export_filename_template_pathsep_in_name_2(): def test_export_filename_template_pathsep_in_name_2():
"""export photos using filename template with keyword and "/" in keyword""" """export photos using filename template with keyword and "/" in keyword"""
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
runner = CliRunner() runner = CliRunner()
cwd = os.getcwd() cwd = os.getcwd()
@ -3988,7 +4031,7 @@ def test_export_filename_template_pathsep_in_name_2():
def test_export_filename_template_long_description(): def test_export_filename_template_long_description():
"""export photos using filename template with description that exceeds max length""" """export photos using filename template with description that exceeds max length"""
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
runner = CliRunner() runner = CliRunner()
cwd = os.getcwd() cwd = os.getcwd()
@ -4664,7 +4707,6 @@ def test_export_force_update():
export, [os.path.join(cwd, photos_db_path), ".", "--force-update"] export, [os.path.join(cwd, photos_db_path), ".", "--force-update"]
) )
assert result.exit_code == 0 assert result.exit_code == 0
print(result.output)
assert ( assert (
f"Processed: {PHOTOS_NOT_IN_TRASH_LEN_15_7} photos, exported: 0, updated: 0, skipped: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}, updated EXIF data: 0, missing: 3, error: 0" f"Processed: {PHOTOS_NOT_IN_TRASH_LEN_15_7} photos, exported: 0, updated: 0, skipped: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}, updated EXIF data: 0, missing: 3, error: 0"
in result.output in result.output
@ -4891,7 +4933,7 @@ def test_export_update_hardlink():
runner = CliRunner() runner = CliRunner()
cwd = os.getcwd() cwd = os.getcwd()
# pylint: disable=not-context-manager # pylint: disable=not-context-manager
with runner.isolated_filesystem(): with isolated_filesystem_here():
# basic export # basic export
result = runner.invoke( result = runner.invoke(
export, export,
@ -4924,7 +4966,7 @@ def test_export_update_hardlink_exiftool():
runner = CliRunner() runner = CliRunner()
cwd = os.getcwd() cwd = os.getcwd()
# pylint: disable=not-context-manager # pylint: disable=not-context-manager
with runner.isolated_filesystem(): with isolated_filesystem_here():
# basic export # basic export
result = runner.invoke( result = runner.invoke(
export, export,
@ -5069,7 +5111,7 @@ def test_export_then_hardlink():
runner = CliRunner() runner = CliRunner()
cwd = os.getcwd() cwd = os.getcwd()
# pylint: disable=not-context-manager # pylint: disable=not-context-manager
with runner.isolated_filesystem(): with isolated_filesystem_here():
# basic export # basic export
result = runner.invoke(export, [os.path.join(cwd, CLI_PHOTOS_DB), ".", "-V"]) result = runner.invoke(export, [os.path.join(cwd, CLI_PHOTOS_DB), ".", "-V"])
assert result.exit_code == 0 assert result.exit_code == 0
@ -5177,7 +5219,7 @@ def test_export_update_edits_dry_run():
def test_export_directory_template_1_dry_run(): def test_export_directory_template_1_dry_run():
"""test export using directory template with dry-run flag""" """test export using directory template with dry-run flag"""
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
runner = CliRunner() runner = CliRunner()
cwd = os.getcwd() cwd = os.getcwd()
@ -6052,7 +6094,7 @@ def test_export_as_hardlink_download_missing():
runner = CliRunner() runner = CliRunner()
cwd = os.getcwd() cwd = os.getcwd()
# pylint: disable=not-context-manager # pylint: disable=not-context-manager
with runner.isolated_filesystem(): with isolated_filesystem_here():
result = runner.invoke( result = runner.invoke(
export, export,
[ [
@ -6877,6 +6919,7 @@ def test_export_finder_tag_keywords_dry_run():
assert result.exit_code == 0 assert result.exit_code == 0
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
def test_export_finder_tag_keywords(): def test_export_finder_tag_keywords():
"""test --finder-tag-keywords""" """test --finder-tag-keywords"""
@ -6951,6 +6994,7 @@ def test_export_finder_tag_keywords():
assert sorted(md.tags) == sorted(expected) assert sorted(md.tags) == sorted(expected)
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
def test_export_finder_tag_template(): def test_export_finder_tag_template():
"""test --finder-tag-template""" """test --finder-tag-template"""
@ -7028,6 +7072,7 @@ def test_export_finder_tag_template():
assert sorted(md.tags) == sorted(expected) assert sorted(md.tags) == sorted(expected)
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
def test_export_finder_tag_template_multiple(): def test_export_finder_tag_template_multiple():
"""test --finder-tag-template used more than once""" """test --finder-tag-template used more than once"""
@ -7061,6 +7106,7 @@ def test_export_finder_tag_template_multiple():
assert sorted(md.tags) == sorted(expected) assert sorted(md.tags) == sorted(expected)
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
def test_export_finder_tag_template_keywords(): def test_export_finder_tag_template_keywords():
"""test --finder-tag-template with --finder-tag-keywords""" """test --finder-tag-template with --finder-tag-keywords"""
@ -7093,6 +7139,7 @@ def test_export_finder_tag_template_keywords():
assert sorted(md.tags) == sorted(expected) assert sorted(md.tags) == sorted(expected)
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
def test_export_finder_tag_template_multi_field(): def test_export_finder_tag_template_multi_field():
"""test --finder-tag-template with multiple fields (issue #422)""" """test --finder-tag-template with multiple fields (issue #422)"""
@ -7157,6 +7204,7 @@ def test_export_xattr_template_dry_run():
assert "Writing extended attribute" in result.output assert "Writing extended attribute" in result.output
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
def test_export_xattr_template(): def test_export_xattr_template():
"""test --xattr template""" """test --xattr template"""
@ -7655,14 +7703,14 @@ def test_query_name_unicode():
cwd = os.getcwd() cwd = os.getcwd()
result = runner.invoke( result = runner.invoke(
query, query,
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_7), "--name", "Frtest"], ["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_7), "--name", "Frítest"],
) )
assert result.exit_code == 0 assert result.exit_code == 0
json_got = json.loads(result.output) json_got = json.loads(result.output)
assert len(json_got) == 4 assert len(json_got) == 4
assert normalize_unicode(json_got[0]["original_filename"]).startswith( assert normalize_unicode(json_got[0]["original_filename"]).startswith(
normalize_unicode("Frtest.jpg") normalize_unicode("Frítest.jpg")
) )

View File

@ -1,10 +1,14 @@
"""Test osxphotos add-locations command""" """Test osxphotos add-locations command"""
import photoscript
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
from osxphotos.cli.add_locations import add_locations from osxphotos.utils import is_macos
if is_macos:
import photoscript
from osxphotos.cli.add_locations import add_locations
else:
pytest.skip(allow_module_level=True)
UUID_TEST_PHOTO_1 = "F12384F6-CD17-4151-ACBA-AE0E3688539E" # Pumkins1.jpg UUID_TEST_PHOTO_1 = "F12384F6-CD17-4151-ACBA-AE0E3688539E" # Pumkins1.jpg
UUID_TEST_PHOTO_LOCATION = "D79B8D77-BFFC-460B-9312-034F2877D35B" # Pumkins2.jpg UUID_TEST_PHOTO_LOCATION = "D79B8D77-BFFC-460B-9312-034F2877D35B" # Pumkins2.jpg

View File

@ -2,10 +2,15 @@
import os import os
import photoscript
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
from osxphotos.utils import is_macos
if is_macos:
import photoscript
else:
pytest.skip(allow_module_level=True)
UUID_EXPORT = {"3DD2C897-F19E-4CA6-8C22-B027D5A71907": {"filename": "IMG_4547.jpg"}} UUID_EXPORT = {"3DD2C897-F19E-4CA6-8C22-B027D5A71907": {"filename": "IMG_4547.jpg"}}
UUID_MISSING = { UUID_MISSING = {
"8E1D7BC9-9321-44F9-8CFB-4083F6B9232A": {"filename": "IMG_2000.JPGssss"} "8E1D7BC9-9321-44F9-8CFB-4083F6B9232A": {"filename": "IMG_2000.JPGssss"}

View File

@ -24,7 +24,6 @@ TEST_RUN_SCRIPT = "examples/cli_example_1.py"
def runner() -> CliRunner: def runner() -> CliRunner:
return CliRunner() return CliRunner()
from osxphotos.cli import ( from osxphotos.cli import (
about, about,
albums, albums,
@ -42,10 +41,14 @@ from osxphotos.cli import (
places, places,
theme, theme,
tutorial, tutorial,
uuid,
version, version,
) )
from osxphotos.utils import is_macos
if is_macos:
from osxphotos.cli import uuid
def test_about(runner: CliRunner): def test_about(runner: CliRunner):
with runner.isolated_filesystem(): with runner.isolated_filesystem():
@ -68,9 +71,8 @@ def test_about(runner: CliRunner):
persons, persons,
places, places,
tutorial, tutorial,
uuid,
version, version,
], ] + ([uuid] if is_macos else []),
) )
def test_cli_comands(runner: CliRunner, command: Callable[..., Any]): def test_cli_comands(runner: CliRunner, command: Callable[..., Any]):
with runner.isolated_filesystem(): with runner.isolated_filesystem():

View File

@ -5,12 +5,17 @@ from __future__ import annotations
import os import os
import time import time
import photoscript
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
import osxphotos import osxphotos
from osxphotos.cli.batch_edit import batch_edit from osxphotos.utils import is_macos
if is_macos:
import photoscript
from osxphotos.cli.batch_edit import batch_edit
else:
pytest.skip(allow_module_level=True)
# set timezone to avoid issues with comparing dates # set timezone to avoid issues with comparing dates
os.environ["TZ"] = "US/Pacific" os.environ["TZ"] = "US/Pacific"

View File

@ -14,17 +14,23 @@ from tempfile import TemporaryDirectory
from typing import Dict from typing import Dict
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
from photoscript import Photo
from pytest import MonkeyPatch, approx from pytest import MonkeyPatch, approx
from osxphotos import PhotosDB, QueryOptions from osxphotos import PhotosDB, QueryOptions
from osxphotos._constants import UUID_PATTERN from osxphotos._constants import UUID_PATTERN
from osxphotos.cli.import_cli import import_cli
from osxphotos.datetime_utils import datetime_remove_tz from osxphotos.datetime_utils import datetime_remove_tz
from osxphotos.exiftool import get_exiftool_path from osxphotos.exiftool import get_exiftool_path
from osxphotos.utils import is_macos
from tests.conftest import get_os_version from tests.conftest import get_os_version
if is_macos:
from photoscript import Photo
from osxphotos.cli.import_cli import import_cli
else:
pytest.skip(allow_module_level=True)
TERMINAL_WIDTH = 250 TERMINAL_WIDTH = 250
TEST_IMAGES_DIR = "tests/test-images" TEST_IMAGES_DIR = "tests/test-images"

View File

@ -2,11 +2,15 @@
import os import os
import json import json
import photoscript
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
from osxphotos.cli.sync import sync from osxphotos.utils import is_macos
if is_macos:
import photoscript
from osxphotos.cli.sync import sync
else:
pytest.skip(allow_module_level=True)
UUID_TEST_PHOTO_1 = "D79B8D77-BFFC-460B-9312-034F2877D35B" # Pumkins2.jpg UUID_TEST_PHOTO_1 = "D79B8D77-BFFC-460B-9312-034F2877D35B" # Pumkins2.jpg
UUID_TEST_PHOTO_2 = "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51" # wedding.jpg UUID_TEST_PHOTO_2 = "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51" # wedding.jpg

View File

@ -1,6 +1,8 @@
""" test datetime_formatter.DateTimeFormatter """ """ test datetime_formatter.DateTimeFormatter """
import pytest import pytest
from .locale_util import setlocale
def test_datetime_formatter_1(): def test_datetime_formatter_1():
"""Test DateTimeFormatter """ """Test DateTimeFormatter """
@ -8,7 +10,7 @@ def test_datetime_formatter_1():
import locale import locale
from osxphotos.datetime_formatter import DateTimeFormatter from osxphotos.datetime_formatter import DateTimeFormatter
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
dt = datetime.datetime(2020, 5, 23, 12, 42, 33) dt = datetime.datetime(2020, 5, 23, 12, 42, 33)
dtf = DateTimeFormatter(dt) dtf = DateTimeFormatter(dt)
@ -32,7 +34,7 @@ def test_datetime_formatter_2():
import locale import locale
from osxphotos.datetime_formatter import DateTimeFormatter from osxphotos.datetime_formatter import DateTimeFormatter
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
dt = datetime.datetime(2020, 5, 23, 14, 42, 33) dt = datetime.datetime(2020, 5, 23, 14, 42, 33)
dtf = DateTimeFormatter(dt) dtf = DateTimeFormatter(dt)
@ -56,7 +58,7 @@ def test_datetime_formatter_3():
import locale import locale
from osxphotos.datetime_formatter import DateTimeFormatter from osxphotos.datetime_formatter import DateTimeFormatter
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
dt = datetime.datetime(2020, 5, 2, 9, 3, 6) dt = datetime.datetime(2020, 5, 2, 9, 3, 6)
dtf = DateTimeFormatter(dt) dtf = DateTimeFormatter(dt)

View File

@ -538,13 +538,13 @@ def test_exiftool_terminate():
ps = subprocess.run(["ps"], capture_output=True) ps = subprocess.run(["ps"], capture_output=True)
stdout = ps.stdout.decode("utf-8") stdout = ps.stdout.decode("utf-8")
assert "exiftool -stay_open" in stdout assert "exiftool" in stdout
osxphotos.exiftool.terminate_exiftool() osxphotos.exiftool.terminate_exiftool()
ps = subprocess.run(["ps"], capture_output=True) ps = subprocess.run(["ps"], capture_output=True)
stdout = ps.stdout.decode("utf-8") stdout = ps.stdout.decode("utf-8")
assert "exiftool -stay_open" not in stdout assert "exiftool" not in stdout
# verify we can create a new instance after termination # verify we can create a new instance after termination
exif2 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD) exif2 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)

View File

@ -2,9 +2,9 @@ import os
import pytest import pytest
from osxphotos._constants import _UNKNOWN_PERSON from osxphotos._constants import _UNKNOWN_PERSON
from osxphotos.utils import get_macos_version from osxphotos.utils import is_macos, get_macos_version
OS_VERSION = get_macos_version() OS_VERSION = get_macos_version() if is_macos else (None, None, None)
SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "15" SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "15"
PHOTOS_DB = os.path.expanduser("~/Pictures/Photos Library.photoslibrary") PHOTOS_DB = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
pytestmark = pytest.mark.skipif( pytestmark = pytest.mark.skipif(

View File

@ -74,10 +74,12 @@ def test_hardlink_file_valid():
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_") temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
src = "tests/test-images/wedding.jpg" src = "tests/test-images/wedding.jpg"
src2 = os.path.join(temp_dir.name, "wedding_src.jpg")
dest = os.path.join(temp_dir.name, "wedding.jpg") dest = os.path.join(temp_dir.name, "wedding.jpg")
FileUtil.hardlink(src, dest) FileUtil.copy(src, src2)
FileUtil.hardlink(src2, dest)
assert os.path.isfile(dest) assert os.path.isfile(dest)
assert os.path.samefile(src, dest) assert os.path.samefile(src2, dest)
def test_unlink_file(): def test_unlink_file():

View File

@ -14,9 +14,9 @@ import pytest
import osxphotos import osxphotos
from osxphotos._constants import _UNKNOWN_PERSON from osxphotos._constants import _UNKNOWN_PERSON
from osxphotos.photoexporter import PhotoExporter from osxphotos.photoexporter import PhotoExporter
from osxphotos.utils import get_macos_version from osxphotos.utils import is_macos, get_macos_version
OS_VERSION = get_macos_version() OS_VERSION = get_macos_version() if is_macos else (None, None, None)
# SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "17" # SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "17"
SKIP_TEST = True # don't run any of the local library tests SKIP_TEST = True # don't run any of the local library tests
PHOTOS_DB_LOCAL = os.path.expanduser("~/Pictures/Photos Library.photoslibrary") PHOTOS_DB_LOCAL = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")

View File

@ -6,15 +6,19 @@ import tempfile
import pytest import pytest
from osxphotos.photokit import ( from osxphotos.utils import is_macos
LivePhotoAsset, if is_macos:
PhotoAsset, from osxphotos.photokit import (
PhotoLibrary, LivePhotoAsset,
VideoAsset, PhotoAsset,
PHOTOS_VERSION_CURRENT, PhotoLibrary,
PHOTOS_VERSION_ORIGINAL, VideoAsset,
PHOTOS_VERSION_UNADJUSTED, PHOTOS_VERSION_CURRENT,
) PHOTOS_VERSION_ORIGINAL,
PHOTOS_VERSION_UNADJUSTED,
)
else:
pytest.skip(allow_module_level=True)
skip_test = "OSXPHOTOS_TEST_EXPORT" not in os.environ skip_test = "OSXPHOTOS_TEST_EXPORT" not in os.environ
pytestmark = pytest.mark.skipif( pytestmark = pytest.mark.skipif(

View File

@ -15,7 +15,9 @@ from osxphotos.phototemplate import (
RenderOptions, RenderOptions,
) )
from osxphotos.photoinfo import PhotoInfoNone from osxphotos.photoinfo import PhotoInfoNone
from osxphotos.utils import is_macos
from .photoinfo_mock import PhotoInfoMock from .photoinfo_mock import PhotoInfoMock
from .locale_util import setlocale
try: try:
exiftool = get_exiftool_path() exiftool = get_exiftool_path()
@ -549,6 +551,8 @@ def test_lookup_multi(photosdb_places):
lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1) lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1)
if subst in ["{exiftool}", "{photo}", "{function}", "{format}"]: if subst in ["{exiftool}", "{photo}", "{function}", "{format}"]:
continue continue
if subst == "{detected_text}" and not is_macos:
continue
lookup = template.get_template_value_multi( lookup = template.get_template_value_multi(
lookup_str, lookup_str,
path_sep=os.path.sep, path_sep=os.path.sep,
@ -562,7 +566,7 @@ def test_subst(photosdb_places):
"""Test that substitutions are correct""" """Test that substitutions are correct"""
import locale import locale
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0] photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
for template in TEMPLATE_VALUES: for template in TEMPLATE_VALUES:
@ -574,7 +578,7 @@ def test_subst_date_modified(photosdb_places):
"""Test that substitutions are correct for date modified""" """Test that substitutions are correct for date modified"""
import locale import locale
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
photo = photosdb_places.photos(uuid=[UUID_DICT["date_modified"]])[0] photo = photosdb_places.photos(uuid=[UUID_DICT["date_modified"]])[0]
for template in TEMPLATE_VALUES_DATE_MODIFIED: for template in TEMPLATE_VALUES_DATE_MODIFIED:
@ -586,7 +590,7 @@ def test_subst_date_not_modified(photosdb_places):
"""Test that substitutions are correct for date modified when photo isn't modified""" """Test that substitutions are correct for date modified when photo isn't modified"""
import locale import locale
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
photo = photosdb_places.photos(uuid=[UUID_DICT["date_not_modified"]])[0] photo = photosdb_places.photos(uuid=[UUID_DICT["date_not_modified"]])[0]
for template in TEMPLATE_VALUES_DATE_NOT_MODIFIED: for template in TEMPLATE_VALUES_DATE_NOT_MODIFIED:
@ -600,7 +604,7 @@ def test_subst_locale_1(photosdb_places):
# osxphotos.template sets local on load so set the environment first # osxphotos.template sets local on load so set the environment first
# set locale to DE # set locale to DE
locale.setlocale(locale.LC_ALL, "de_DE.UTF-8") setlocale(locale.LC_ALL, "de_DE.UTF-8")
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0] photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
@ -614,6 +618,9 @@ def test_subst_locale_2(photosdb_places):
import locale import locale
import os import os
# Check if locale is available
setlocale(locale.LC_ALL, "de_DE.UTF-8")
# osxphotos.template sets local on load so set the environment first # osxphotos.template sets local on load so set the environment first
os.environ["LANG"] = "de_DE.UTF-8" os.environ["LANG"] = "de_DE.UTF-8"
os.environ["LC_COLLATE"] = "de_DE.UTF-8" os.environ["LC_COLLATE"] = "de_DE.UTF-8"
@ -634,7 +641,7 @@ def test_subst_default_val(photosdb_places):
"""Test substitution with default value specified""" """Test substitution with default value specified"""
import locale import locale
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0] photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
template = "{place.name.area_of_interest,UNKNOWN}" template = "{place.name.area_of_interest,UNKNOWN}"
@ -646,7 +653,7 @@ def test_subst_default_val_2(photosdb_places):
"""Test substitution with ',' but no default value""" """Test substitution with ',' but no default value"""
import locale import locale
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0] photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
template = "{place.name.area_of_interest,}" template = "{place.name.area_of_interest,}"
@ -658,7 +665,7 @@ def test_subst_unknown_val(photosdb_places):
"""Test substitution with unknown value specified""" """Test substitution with unknown value specified"""
import locale import locale
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0] photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
template = "{created.year}/{foo}" template = "{created.year}/{foo}"
@ -682,7 +689,7 @@ def test_subst_unknown_val_with_default(photosdb_places):
"""Test substitution with unknown value specified""" """Test substitution with unknown value specified"""
import locale import locale
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0] photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
template = "{created.year}/{foo,bar}" template = "{created.year}/{foo,bar}"
@ -929,7 +936,7 @@ def test_subst_strftime(photosdb_places):
"""Test that strftime substitutions are correct""" """Test that strftime substitutions are correct"""
import locale import locale
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0] photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
rendered, unmatched = photo.render_template("{created.strftime,%Y-%m-%d-%H%M%S}") rendered, unmatched = photo.render_template("{created.strftime,%Y-%m-%d-%H%M%S}")
@ -1287,6 +1294,7 @@ def test_album_seq(photosdb):
assert rendered[0] == value assert rendered[0] == value
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
def test_detected_text(photosdb): def test_detected_text(photosdb):
"""Test {detected_text} template""" """Test {detected_text} template"""
photo = photosdb.get_photo(UUID_DETECTED_TEXT) photo = photosdb.get_photo(UUID_DETECTED_TEXT)

View File

@ -5,6 +5,8 @@ import pytest
import osxphotos import osxphotos
from osxphotos.phototemplate import RenderOptions from osxphotos.phototemplate import RenderOptions
from .locale_util import setlocale
PHOTOS_DB_PLACES = ( PHOTOS_DB_PLACES = (
"./tests/Test-Places-Catalina-10_15_1.photoslibrary/database/photos.db" "./tests/Test-Places-Catalina-10_15_1.photoslibrary/database/photos.db"
) )
@ -48,7 +50,7 @@ def test_subst_today(photosdb):
"""Test that substitutions are correct for {today.x}""" """Test that substitutions are correct for {today.x}"""
import locale import locale
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0] photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
photo_template = osxphotos.PhotoTemplate(photo) photo_template = osxphotos.PhotoTemplate(photo)
@ -64,7 +66,7 @@ def test_subst_strftime_today(photosdb):
"""Test that strftime substitutions are correct for {today.strftime}""" """Test that strftime substitutions are correct for {today.strftime}"""
import locale import locale
locale.setlocale(locale.LC_ALL, "en_US") setlocale(locale.LC_ALL, "en_US")
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0] photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
photo_template = osxphotos.PhotoTemplate(photo) photo_template = osxphotos.PhotoTemplate(photo)

View File

@ -8,6 +8,7 @@ from osxphotos.uti import (
get_preferred_uti_extension, get_preferred_uti_extension,
get_uti_for_extension, get_uti_for_extension,
) )
from osxphotos.utils import is_macos
EXT_DICT = {"heic": "public.heic", "jpg": "public.jpeg", ".jpg": "public.jpeg"} EXT_DICT = {"heic": "public.heic", "jpg": "public.jpeg", ".jpg": "public.jpeg"}
UTI_DICT = {"public.heic": "heic", "public.jpeg": "jpeg"} UTI_DICT = {"public.heic": "heic", "public.jpeg": "jpeg"}
@ -43,12 +44,14 @@ def test_get_uti_for_extension_no_objc():
osxphotos.uti.OS_VER = OLD_VER osxphotos.uti.OS_VER = OLD_VER
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
def test_get_uti_from_mdls(): def test_get_uti_from_mdls():
"""get _get_uti_from_mdls""" """get _get_uti_from_mdls"""
for ext in EXT_DICT: for ext in EXT_DICT:
assert _get_uti_from_mdls(ext) == EXT_DICT[ext] assert _get_uti_from_mdls(ext) == EXT_DICT[ext]
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
def test_get_uti_not_in_dict(): def test_get_uti_not_in_dict():
"""get UTI when objc is not available and it's not in the EXT_UTI_DICT""" """get UTI when objc is not available and it's not in the EXT_UTI_DICT"""
# monkey patch the EXT_UTI_DICT # monkey patch the EXT_UTI_DICT