Refactor verbose 931 (#960)

* Began refactoring verbose code for #931

* Fixed tests for timewarp due to verbose refactor

* Updated test data

* More refactoring for #931

* Refactored queryoptions.py

* Refactored queryoptions.py

* Refactored queryoptions.py

* Refactored echo_error

* Refactored debug

* Refactored debug

* Refactored use of verbose in export

* Refactored use of verbose in export

* Refactred --verbose in add-locations and debug-dump

* Refactored --verbose for

* Refactored --verbose for osxphotos exportdb

* Refactored --verbose for osxphotos import

* Refactored --verbose for osxphotos orphans

* Refactored --verbose for osxphotos snap-diff

* Refactored --verbose for osxphotos sync

* Refactored --verbose for osxphotos timewarp

* Added default verbose() function to verbose
This commit is contained in:
Rhet Turnbull 2023-01-28 17:44:20 -08:00 committed by GitHub
parent 770d85759d
commit 0c293d0bf5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 919 additions and 735 deletions

View File

@ -1,3 +1,7 @@
"""__init__.py for osxphotos"""
from __future__ import annotations
import logging
from ._constants import AlbumSortOrder
@ -20,7 +24,13 @@ from .placeinfo import PlaceInfo
from .queryoptions import QueryOptions
from .scoreinfo import ScoreInfo
from .searchinfo import SearchInfo
from .utils import _get_logger
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(filename)s - %(lineno)d - %(message)s",
)
logger: logging.Logger = logging.getLogger("osxphotos")
if not is_debug():
logging.disable(logging.DEBUG)
@ -37,6 +47,7 @@ __all__ = [
"ExportResults",
"FileUtil",
"FileUtilNoOp",
"FolderInfo",
"ImportInfo",
"LikeInfo",
"MomentInfo",
@ -53,8 +64,7 @@ __all__ = [
"ScoreInfo",
"SearchInfo",
"__version__",
"_get_logger",
"is_debug",
"logger",
"set_debug",
"FolderInfo",
]

View File

@ -47,7 +47,7 @@ from .about import about
from .add_locations import add_locations
from .albums import albums
from .cli import cli_main
from .common import get_photos_db, load_uuid_from_file
from .common import get_photos_db
from .debug_dump import debug_dump
from .dump import dump
from .exiftool_cli import exiftool
@ -91,7 +91,6 @@ __all__ = [
"labels",
"list_libraries",
"list_libraries",
"load_uuid_from_file",
"orphans",
"persons",
"photo_inspect",

View File

@ -8,17 +8,12 @@ import click
import photoscript
import osxphotos
from osxphotos.queryoptions import IncompatibleQueryOptions, query_options_from_kwargs
from osxphotos.utils import pluralize
from .click_rich_echo import (
rich_click_echo,
rich_echo_error,
set_rich_console,
set_rich_theme,
set_rich_timestamp,
)
from .color_themes import get_theme
from .common import QUERY_OPTIONS, THEME_OPTION, query_options_from_kwargs
from .click_rich_echo import rich_click_echo as echo
from .click_rich_echo import rich_echo_error as echo_error
from .common import QUERY_OPTIONS, THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
from .param_types import TimeOffset
from .rich_progress import rich_progress
from .verbose import get_verbose_console, verbose_print
@ -94,15 +89,15 @@ def get_location(
help="Don't actually add location, just print what would be done. "
"Most useful with --verbose.",
)
@click.option("--verbose", "-V", "verbose_", is_flag=True, help="Print verbose output.")
@click.option(
"--timestamp", "-T", is_flag=True, help="Add time stamp to verbose output."
)
@VERBOSE_OPTION
@TIMESTAMP_OPTION
@QUERY_OPTIONS
@THEME_OPTION
@click.pass_obj
@click.pass_context
def add_locations(ctx, cli_ob, window, dry_run, verbose_, timestamp, theme, **kwargs):
def add_locations(
ctx, cli_ob, window, dry_run, verbose_flag, timestamp, theme, **kwargs
):
"""Add missing location data to photos in Photos.app using nearest neighbor.
This command will search for photos that are missing location data and look
@ -136,20 +131,19 @@ def add_locations(ctx, cli_ob, window, dry_run, verbose_, timestamp, theme, **kw
use `osxphotos add-locations` to add location information.
See `osxphotos help timewarp` for more information.
"""
color_theme = get_theme(theme)
verbose = verbose_print(
verbose_, timestamp, rich=True, theme=color_theme, highlight=False
)
# set console for rich_echo to be same as for verbose_
set_rich_console(get_verbose_console())
set_rich_theme(color_theme)
set_rich_timestamp(timestamp)
verbose = verbose_print(verbose_flag, timestamp, theme=theme)
verbose("Searching for photos with missing location data...")
# load photos database
photosdb = osxphotos.PhotosDB(verbose=verbose)
query_options = query_options_from_kwargs(**kwargs)
try:
query_options = query_options_from_kwargs(**kwargs)
except IncompatibleQueryOptions as e:
echo_error("Incompatible query options")
echo_error(ctx.obj.group.commands["repl"].get_help(ctx))
ctx.exit(1)
photos = photosdb.query(query_options)
# sort photos by date
@ -159,7 +153,7 @@ def add_locations(ctx, cli_ob, window, dry_run, verbose_, timestamp, theme, **kw
missing_location = 0
found_location = 0
verbose(f"Processing {len(photos)} photos, window = ±{window}...")
with rich_progress(console=get_verbose_console(), mock=verbose_) as progress:
with rich_progress(console=get_verbose_console(), mock=verbose_flag) as progress:
task = progress.add_task(
f"Processing [num]{num_photos}[/] {pluralize(len(photos), 'photo', 'photos')}, window = ±{window}",
total=num_photos,
@ -183,7 +177,7 @@ def add_locations(ctx, cli_ob, window, dry_run, verbose_, timestamp, theme, **kw
f"No location found for [filename]{photo.original_filename}[/] ([uuid]{photo.uuid}[/])"
)
progress.advance(task)
rich_click_echo(
echo(
f"Done. Processed: [num]{num_photos}[/] photos, "
f"missing location: [num]{missing_location}[/], "
f"found location: [num]{found_location}[/] "

View File

@ -1,7 +1,6 @@
"""Globals and constants use by the CLI commands"""
import dataclasses
import os
import pathlib
from datetime import datetime
@ -11,7 +10,6 @@ from packaging import version
from xdg import xdg_config_home, xdg_data_home
import osxphotos
from osxphotos import QueryOptions
from osxphotos._constants import APP_NAME
from osxphotos._version import __version__
from osxphotos.utils import get_latest_version
@ -41,20 +39,14 @@ __all__ = [
"JSON_OPTION",
"QUERY_OPTIONS",
"THEME_OPTION",
"VERBOSE_OPTION",
"TIMESTAMP_OPTION",
"get_photos_db",
"load_uuid_from_file",
"noop",
"query_options_from_kwargs",
"time_stamp",
]
class IncompatibleQueryOptions(Exception):
"""Incompatible query options"""
pass
def noop(*args, **kwargs):
"""no-op function"""
pass
@ -289,7 +281,11 @@ def QUERY_OPTIONS(f):
help="Case insensitive search for title, description, place, keyword, person, or album.",
),
o("--edited", is_flag=True, help="Search for photos that have been edited."),
o("--not-edited", is_flag=True, help="Search for photos that have not been edited."),
o(
"--not-edited",
is_flag=True,
help="Search for photos that have not been edited.",
),
o(
"--external-edit",
is_flag=True,
@ -610,37 +606,22 @@ THEME_OPTION = click.option(
"--theme",
metavar="THEME",
type=click.Choice(["dark", "light", "mono", "plain"], case_sensitive=False),
help="Specify the color theme to use for --verbose output. "
help="Specify the color theme to use for output. "
"Valid themes are 'dark', 'light', 'mono', and 'plain'. "
"Defaults to 'dark' or 'light' depending on system dark mode setting.",
)
VERBOSE_OPTION = click.option(
"--verbose",
"-V",
"verbose_flag",
count=True,
help="Print verbose output; may be specified multiple times for more verbose output.",
)
def load_uuid_from_file(filename):
"""Load UUIDs from file. Does not validate UUIDs.
Format is 1 UUID per line, any line beginning with # is ignored.
Whitespace is stripped.
Arguments:
filename: file name of the file containing UUIDs
Returns:
list of UUIDs or empty list of no UUIDs in file
Raises:
FileNotFoundError if file does not exist
"""
if not pathlib.Path(filename).is_file():
raise FileNotFoundError(f"Could not find file {filename}")
uuid = []
with open(filename, "r") as uuid_file:
for line in uuid_file:
line = line.strip()
if len(line) and line[0] != "#":
uuid.append(line)
return uuid
TIMESTAMP_OPTION = click.option(
"--timestamp", is_flag=True, help="Add time stamp to verbose output"
)
def get_config_dir() -> pathlib.Path:
@ -670,103 +651,3 @@ def check_version():
"to suppress this message and prevent osxphotos from checking for latest version.",
err=True,
)
def query_options_from_kwargs(**kwargs) -> QueryOptions:
"""Validate query options and create a QueryOptions instance"""
# sanity check input args
nonexclusive = [
"added_after",
"added_before",
"added_in_last",
"album",
"duplicate",
"edited",
"exif",
"external_edit",
"folder",
"from_date",
"from_time",
"has_raw",
"keyword",
"label",
"max_size",
"min_size",
"name",
"person",
"query_eval",
"query_function",
"regex",
"selected",
"to_date",
"to_time",
"uti",
"uuid_from_file",
"uuid",
"year",
]
exclusive = [
("burst", "not_burst"),
("cloudasset", "not_cloudasset"),
("favorite", "not_favorite"),
("has_comment", "no_comment"),
("has_likes", "no_likes"),
("hdr", "not_hdr"),
("hidden", "not_hidden"),
("in_album", "not_in_album"),
("incloud", "not_incloud"),
("live", "not_live"),
("location", "no_location"),
("keyword", "no_keyword"),
("missing", "not_missing"),
("only_photos", "only_movies"),
("panorama", "not_panorama"),
("portrait", "not_portrait"),
("screenshot", "not_screenshot"),
("selfie", "not_selfie"),
("shared", "not_shared"),
("slow_mo", "not_slow_mo"),
("time_lapse", "not_time_lapse"),
("is_reference", "not_reference"),
]
# print help if no non-exclusive term or a double exclusive term is given
# TODO: add option to validate requiring at least one query arg
if any(all([kwargs[b], kwargs[n]]) for b, n in exclusive) or any(
[
all([any(kwargs["title"]), kwargs["no_title"]]),
all([any(kwargs["description"]), kwargs["no_description"]]),
all([any(kwargs["place"]), kwargs["no_place"]]),
all([any(kwargs["keyword"]), kwargs["no_keyword"]]),
]
):
raise IncompatibleQueryOptions
# can also be used with --deleted/--not-deleted which are not part of
# standard query options
try:
if kwargs["deleted"] and kwargs["not_deleted"]:
raise IncompatibleQueryOptions
except KeyError:
pass
# actually have something to query
include_photos = True
include_movies = True # default searches for everything
if kwargs["only_movies"]:
include_photos = False
if kwargs["only_photos"]:
include_movies = False
# load UUIDs if necessary and append to any uuids passed with --uuid
uuid = None
if kwargs["uuid_from_file"]:
uuid_list = list(kwargs["uuid"]) # Click option is a tuple
uuid_list.extend(load_uuid_from_file(kwargs["uuid_from_file"]))
uuid = tuple(uuid_list)
query_fields = [field.name for field in dataclasses.fields(QueryOptions)]
query_dict = {field: kwargs.get(field) for field in query_fields}
query_dict["photos"] = include_photos
query_dict["movies"] = include_movies
query_dict["uuid"] = uuid
return QueryOptions(**query_dict)

View File

@ -9,7 +9,15 @@ from rich import print
import osxphotos
from osxphotos._constants import _PHOTOS_4_VERSION, _UNKNOWN_PLACE
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, OSXPHOTOS_HIDDEN, get_photos_db
from .common import (
DB_ARGUMENT,
DB_OPTION,
JSON_OPTION,
OSXPHOTOS_HIDDEN,
TIMESTAMP_OPTION,
VERBOSE_OPTION,
get_photos_db,
)
from .list import _list_libraries
from .verbose import verbose_print
@ -31,13 +39,14 @@ from .verbose import verbose_print
"May be repeated to include multiple UUIDs.",
multiple=True,
)
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
@VERBOSE_OPTION
@TIMESTAMP_OPTION
@click.pass_obj
@click.pass_context
def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid, verbose):
def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid, verbose_flag, timestamp):
"""Print out debug info"""
verbose_ = verbose_print(verbose, rich=True)
verbose = verbose_print(verbose_flag, timestamp)
db = get_photos_db(*photos_library, db, cli_obj.db)
if db is None:
click.echo(ctx.obj.group.commands["debug-dump"].get_help(ctx), err=True)
@ -47,7 +56,7 @@ def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid, verbose):
start_t = time.perf_counter()
print(f"Opening database: {db}")
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_)
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose)
stop_t = time.perf_counter()
print(f"Done; took {(stop_t-start_t):.2f} seconds")

View File

@ -17,15 +17,14 @@ from osxphotos.fileutil import FileUtil, FileUtilNoOp
from osxphotos.photoexporter import ExportOptions, ExportResults, PhotoExporter
from osxphotos.utils import pluralize
from .click_rich_echo import (
rich_click_echo,
rich_echo_error,
set_rich_console,
set_rich_theme,
set_rich_timestamp,
from .click_rich_echo import rich_click_echo, rich_echo_error
from .common import (
DB_OPTION,
THEME_OPTION,
TIMESTAMP_OPTION,
VERBOSE_OPTION,
get_photos_db,
)
from .color_themes import get_theme
from .common import DB_OPTION, THEME_OPTION, get_photos_db
from .export import export, render_and_validate_report
from .param_types import ExportDBType, TemplateString
from .report_writer import ReportWriterNoOp, export_report_writer_factory
@ -166,8 +165,8 @@ from .verbose import get_verbose_console, verbose_print
help="If used with --report, add data to existing report file instead of overwriting it. "
"See also --report.",
)
@click.option("--verbose", "-V", is_flag=True, help="Print verbose output.")
@click.option("--timestamp", is_flag=True, help="Add time stamp to verbose output")
@VERBOSE_OPTION
@TIMESTAMP_OPTION
@click.option(
"--dry-run",
is_flag=True,
@ -203,7 +202,7 @@ def exiftool(
save_config,
theme,
timestamp,
verbose,
verbose_flag,
):
"""Run exiftool on previously exported files to update metadata.
@ -235,6 +234,7 @@ def exiftool(
# need to ensure --exiftool is true in the config options
locals_["exiftool"] = True
locals_["verbose"] = verbose_flag
config = ConfigOptions(
"export",
locals_,
@ -249,14 +249,7 @@ def exiftool(
"save_config",
],
)
color_theme = get_theme(theme)
verbose_ = verbose_print(
verbose, timestamp, rich=True, theme=color_theme, highlight=False
)
# set console for rich_echo to be same as for verbose_
set_rich_console(get_verbose_console())
set_rich_theme(color_theme)
set_rich_timestamp(timestamp)
verbose = verbose_print(verbose_flag, timestamp, theme=theme)
# load config options from either file or export database
# values already set in config will take precedence over any values
@ -269,26 +262,16 @@ def exiftool(
f"[error]Error parsing {load_config} config file: {e.message}", err=True
)
sys.exit(1)
verbose_(f"Loaded options from file [filepath]{load_config}")
verbose(f"Loaded options from file [filepath]{load_config}")
elif db_config:
config = export_db_get_config(exportdb, config)
verbose_("Loaded options from export database")
verbose("Loaded options from export database")
# from here on out, use config.param_name instead of using the params passed into the function
# as the values may have been updated from config file or database
if load_config or db_config:
# config file might have changed verbose
color_theme = get_theme(config.theme)
verbose_ = verbose_print(
config.verbose,
config.timestamp,
rich=True,
theme=color_theme,
highlight=False,
)
# set console for rich_echo to be same as for verbose_
set_rich_console(get_verbose_console())
set_rich_timestamp(config.timestamp)
verbose = verbose_print(config.verbose, config.timestamp, theme=theme)
# validate options
if append and not report:
@ -298,14 +281,14 @@ def exiftool(
config.db = get_photos_db(config.db)
if save_config:
verbose_(f"Saving options to config file '[filepath]{save_config}'")
verbose(f"Saving options to config file '[filepath]{save_config}'")
config.write_to_file(save_config)
process_files(exportdb, export_dir, verbose=verbose_, options=config)
process_files(exportdb, export_dir, verbose=verbose, options=config)
def process_files(
exportdb: str, export_dir: str, verbose: Callable, options: ConfigOptions
exportdb: str, export_dir: str, verbose: Callable[..., None], options: ConfigOptions
):
"""Process files in the export database.
@ -362,6 +345,12 @@ def process_files(
hardlink_ok = True
verbose(f"Processing file [filepath]{file}[/] ([num]{count}/{total}[/num])")
photo = photosdb.get_photo(uuid)
if not photo:
verbose(
f"Could not find photo for [filepath]{file}[/] ([uuid]{uuid}[/])"
)
report_writer.write(ExportResults(missing=[file]))
continue
export_options = ExportOptions(
description_template=options.description_template,
dry_run=options.dry_run,

View File

@ -2,6 +2,7 @@
import os
import pathlib
import platform
import shlex
import subprocess
import sys
@ -54,19 +55,16 @@ from osxphotos.photokit import (
)
from osxphotos.photosalbum import PhotosAlbum
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
from osxphotos.queryoptions import QueryOptions
from osxphotos.queryoptions import QueryOptions, load_uuid_from_file
from osxphotos.uti import get_preferred_uti_extension
from osxphotos.utils import format_sec_to_hhmmss, normalize_fs_path, pluralize
from .click_rich_echo import (
rich_click_echo,
rich_echo,
rich_echo_error,
set_rich_console,
set_rich_theme,
set_rich_timestamp,
from osxphotos.utils import (
get_macos_version,
format_sec_to_hhmmss,
normalize_fs_path,
pluralize,
)
from .color_themes import get_theme
from .click_rich_echo import rich_click_echo, rich_echo, rich_echo_error
from .common import (
CLI_COLOR_ERROR,
CLI_COLOR_WARNING,
@ -78,8 +76,9 @@ from .common import (
OSXPHOTOS_HIDDEN,
QUERY_OPTIONS,
THEME_OPTION,
TIMESTAMP_OPTION,
VERBOSE_OPTION,
get_photos_db,
load_uuid_from_file,
noop,
)
from .help import ExportCommand, get_help_msg
@ -87,13 +86,13 @@ from .list import _list_libraries
from .param_types import ExportDBType, FunctionCall, TemplateString
from .report_writer import ReportWriterNoOp, export_report_writer_factory
from .rich_progress import rich_progress
from .verbose import get_verbose_console, time_stamp, verbose_print
from .verbose import get_verbose_console, verbose_print
@click.command(cls=ExportCommand)
@DB_OPTION
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
@click.option("--timestamp", is_flag=True, help="Add time stamp to verbose output")
@VERBOSE_OPTION
@TIMESTAMP_OPTION
@click.option(
"--no-progress", is_flag=True, help="Do not display progress bar during export."
)
@ -875,7 +874,7 @@ def export(
uti,
uuid,
uuid_from_file,
verbose,
verbose_flag,
xattr_template,
year,
# debug, # debug, watch, breakpoint handled in cli/__init__.py
@ -908,6 +907,10 @@ def export(
locals_ = locals()
set_crash_data("locals", locals_)
# config expects --verbose to be named "verbose" not "verbose_flag"
locals_["verbose"] = verbose_flag
del locals_["verbose_flag"]
# NOTE: because of the way ConfigOptions works, Click options must not
# set defaults which are not None or False. If defaults need to be set
# do so below after load_config and save_config are handled.
@ -917,14 +920,7 @@ def export(
ignore=["ctx", "cli_obj", "dest", "load_config", "save_config", "config_only"],
)
color_theme = get_theme(theme)
verbose_ = verbose_print(
verbose, timestamp, rich=True, theme=color_theme, highlight=False
)
# set console for rich_echo to be same as for verbose_
set_rich_console(get_verbose_console())
set_rich_theme(color_theme)
set_rich_timestamp(timestamp)
verbose = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)
if load_config:
try:
@ -1095,23 +1091,20 @@ def export(
uti = cfg.uti
uuid = cfg.uuid
uuid_from_file = cfg.uuid_from_file
verbose = cfg.verbose
# this is the one option that is named differently in the config file than the variable passed by --verbose (verbose_flag)
verbose_flag = cfg.verbose
xattr_template = cfg.xattr_template
year = cfg.year
# config file might have changed verbose
color_theme = get_theme(theme)
verbose_ = verbose_print(
verbose, timestamp, rich=True, theme=color_theme, highlight=False
)
# set console for rich_echo to be same as for verbose_
set_rich_console(get_verbose_console())
set_rich_timestamp(timestamp)
verbose_(f"Loaded options from file [filepath]{load_config}")
verbose = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)
verbose(f"Loaded options from file [filepath]{load_config}")
set_crash_data("cfg", cfg.asdict())
verbose_(f"osxphotos version {__version__}")
verbose(f"osxphotos version: {__version__}")
verbose(f"Python version: {sys.version}")
verbose(f"Platform: {platform.platform()}, {'.'.join(get_macos_version())}")
verbose(f"Verbose level: {verbose_flag}")
# validate options
exclusive_options = [
@ -1195,7 +1188,7 @@ def export(
sys.exit(1)
if save_config:
verbose_(f"Saving options to config file '[filepath]{save_config}'")
verbose(f"Saving options to config file '[filepath]{save_config}'")
cfg.write_to_file(save_config)
if config_only:
rich_echo(f"Saved config file to '[filepath]{save_config}'")
@ -1260,7 +1253,7 @@ def export(
ctx.exit(1)
if any([exiftool, exiftool_merge_keywords, exiftool_merge_persons]):
verbose_(f"exiftool path: [filepath]{exiftool_path}")
verbose(f"exiftool path: [filepath]{exiftool_path}")
# default searches for everything
photos = True
@ -1331,14 +1324,14 @@ def export(
)
fileutil = FileUtilShUtil if alt_copy else FileUtil
if verbose_:
if verbose:
if export_db.was_created:
verbose_(f"Created export database [filepath]{export_db_path}")
verbose(f"Created export database [filepath]{export_db_path}")
else:
verbose_(f"Using export database [filepath]{export_db_path}")
verbose(f"Using export database [filepath]{export_db_path}")
upgraded = export_db.was_upgraded
if upgraded:
verbose_(
verbose(
f"Upgraded export database [filepath]{export_db_path}[/] from version [num]{upgraded[0]}[/] to [num]{upgraded[1]}[/]"
)
@ -1346,7 +1339,7 @@ def export(
export_db.set_config(cfg.write_to_str())
photosdb = osxphotos.PhotosDB(
dbfile=db, verbose=verbose_, exiftool=exiftool_path, rich=True
dbfile=db, verbose=verbose, exiftool=exiftool_path, rich=True
)
# enable beta features if requested
@ -1401,7 +1394,7 @@ def export(
no_title=no_title,
not_burst=not_burst,
not_cloudasset=not_cloudasset,
not_edited = not_edited,
not_edited=not_edited,
not_favorite=not_favorite,
not_hdr=not_hdr,
not_hidden=not_hidden,
@ -1478,17 +1471,17 @@ def export(
# set up for --add-export-to-album if needed
album_export = (
PhotosAlbum(add_exported_to_album, verbose=verbose_)
PhotosAlbum(add_exported_to_album, verbose=verbose)
if add_exported_to_album
else None
)
album_skipped = (
PhotosAlbum(add_skipped_to_album, verbose=verbose_)
PhotosAlbum(add_skipped_to_album, verbose=verbose)
if add_skipped_to_album
else None
)
album_missing = (
PhotosAlbum(add_missing_to_album, verbose=verbose_)
PhotosAlbum(add_missing_to_album, verbose=verbose)
if add_missing_to_album
else None
)
@ -1552,17 +1545,17 @@ def export(
update_errors=update_errors,
use_photokit=use_photokit,
use_photos_export=use_photos_export,
verbose_=verbose_,
verbose=verbose,
tmpdir=tmpdir,
)
if post_function:
for function in post_function:
# post function is tuple of (function, filename.py::function_name)
verbose_(f"Calling post-function [bold]{function[1]}")
verbose(f"Calling post-function [bold]{function[1]}")
if not dry_run:
try:
function[0](p, export_results, verbose_)
function[0](p, export_results, verbose)
except Exception as e:
rich_echo_error(
f"[error]Error running post-function [italic]{function[1]}[/italic]: {e}"
@ -1576,7 +1569,7 @@ def export(
dry_run=dry_run,
exiftool_path=exiftool_path,
export_db=export_db,
verbose_=verbose_,
verbose=verbose,
)
if album_export and export_results.exported:
@ -1646,7 +1639,7 @@ def export(
finder_tag_template=finder_tag_template,
strip=strip,
export_dir=dest,
verbose_=verbose_,
verbose=verbose,
)
export_results.xattr_written.extend(tags_written)
export_results.xattr_skipped.extend(tags_skipped)
@ -1660,7 +1653,7 @@ def export(
xattr_template,
strip=strip,
export_dir=dest,
verbose_=verbose_,
verbose=verbose,
)
export_results.xattr_written.extend(xattr_written)
export_results.xattr_skipped.extend(xattr_skipped)
@ -1758,7 +1751,7 @@ def export(
all_files += files_to_keep
rich_echo(f"Cleaning up [filepath]{dest}")
cleaned_files, cleaned_dirs = cleanup_files(
dest, all_files, dirs_to_keep, fileutil, verbose_=verbose_
dest, all_files, dirs_to_keep, fileutil, verbose=verbose
)
file_str = "files" if len(cleaned_files) != 1 else "file"
dir_str = "directories" if len(cleaned_dirs) != 1 else "directory"
@ -1775,12 +1768,12 @@ def export(
export_db.set_export_results(results)
if report:
verbose_(f"Wrote export report to [filepath]{report}")
verbose(f"Wrote export report to [filepath]{report}")
report_writer.close()
# close export_db and write changes if needed
if ramdb and not dry_run:
verbose_(f"Writing export database changes back to [filepath]{export_db.path}")
verbose(f"Writing export database changes back to [filepath]{export_db.path}")
export_db.write_to_disk()
export_db.close()
@ -1788,7 +1781,7 @@ def export(
def export_photo(
photo=None,
dest=None,
verbose_=None,
verbose=None,
export_by_date=None,
sidecar=None,
sidecar_drop_ext=False,
@ -1885,7 +1878,7 @@ def export_photo(
update: bool, only export updated photos
update_errors: bool, attempt to re-export photos that previously produced errors even if they otherwise would not be exported
use_photos_export: bool; if True forces the use of AppleScript to export even if photo not missing
verbose_: callable for verbose output
verbose: callable for verbose output
tmpdir: optional str; temporary directory to use for export
Returns:
list of path(s) of exported photo or None if photo was missing
@ -1908,7 +1901,7 @@ def export_photo(
# requested edited version but it's missing, download original
export_original = True
export_edited = False
verbose_(
verbose(
f"Edited file for [filename]{photo.original_filename}[/] is missing, exporting original"
)
@ -2004,7 +1997,7 @@ def export_photo(
)
original_filename = str(original_filename)
verbose_(
verbose(
f"Exporting [filename]{photo.original_filename}[/] ([filename]{photo.filename}[/]) ([count]{photo_num}/{num_photos}[/])"
)
@ -2052,7 +2045,7 @@ def export_photo(
update_errors=update_errors,
use_photos_export=use_photos_export,
use_photokit=use_photokit,
verbose_=verbose_,
verbose=verbose,
tmpdir=tmpdir,
)
@ -2120,7 +2113,7 @@ def export_photo(
f"{edited_filename.stem}{rendered_edited_suffix}{edited_ext}"
)
verbose_(
verbose(
f"Exporting edited version of [filename]{photo.original_filename}[/filename] ([filename]{photo.filename}[/filename])"
)
@ -2168,7 +2161,7 @@ def export_photo(
update_errors=update_errors,
use_photos_export=use_photos_export,
use_photokit=use_photokit,
verbose_=verbose_,
verbose=verbose,
tmpdir=tmpdir,
)
@ -2255,7 +2248,7 @@ def export_photo_to_directory(
update_errors,
use_photos_export,
use_photokit,
verbose_,
verbose,
tmpdir,
):
"""Export photo to directory dest_path"""
@ -2267,7 +2260,7 @@ def export_photo_to_directory(
if photo.intrash and not photo_path and not preview_if_missing:
# skip deleted files if they're missing
# as AppleScript/PhotoKit cannot export deleted photos
verbose_(
verbose(
f"Skipping missing deleted photo {photo.original_filename} ({photo.uuid})"
)
results.missing.append(str(pathlib.Path(dest_path) / filename))
@ -2276,7 +2269,7 @@ def export_photo_to_directory(
render_options = RenderOptions(export_dir=export_dir, dest_path=dest_path)
if not export_original and not edited:
verbose_(f"Skipping original version of [filename]{photo.original_filename}")
verbose(f"Skipping original version of [filename]{photo.original_filename}")
return results
tries = 0
@ -2322,14 +2315,14 @@ def export_photo_to_directory(
use_persons_as_keywords=person_keyword,
use_photokit=use_photokit,
use_photos_export=use_photos_export,
verbose=verbose_,
verbose=verbose,
)
exporter = PhotoExporter(photo)
export_results = exporter.export(
dest=dest_path, filename=filename, options=export_options
)
for warning_ in export_results.exiftool_warning:
verbose_(
verbose(
f"[warning]exiftool warning for file {warning_[0]}: {warning_[1]}"
)
for error_ in export_results.exiftool_error:
@ -2364,19 +2357,19 @@ def export_photo_to_directory(
f"Retrying export for photo ([uuid]{photo.uuid}[/uuid]: [filename]{photo.original_filename}[/filename])"
)
if verbose_:
if verbose:
if update or force_update:
for new in results.new:
verbose_(f"Exported new file [filepath]{new}")
verbose(f"Exported new file [filepath]{new}")
for updated in results.updated:
verbose_(f"Exported updated file [filepath]{updated}")
verbose(f"Exported updated file [filepath]{updated}")
for skipped in results.skipped:
verbose_(f"Skipped up to date file [filepath]{skipped}")
verbose(f"Skipped up to date file [filepath]{skipped}")
else:
for exported in results.exported:
verbose_(f"Exported [filepath]{exported}")
verbose(f"Exported [filepath]{exported}")
for touched in results.touched:
verbose_(f"Touched date on file [filepath]{touched}")
verbose(f"Touched date on file [filepath]{touched}")
return results
@ -2572,7 +2565,7 @@ def collect_files_to_keep(
return files_to_keep, dirs_to_keep
def cleanup_files(dest_path, files_to_keep, dirs_to_keep, fileutil, verbose_):
def cleanup_files(dest_path, files_to_keep, dirs_to_keep, fileutil, verbose):
"""cleanup dest_path by deleting and files and empty directories
not in files_to_keep
@ -2581,7 +2574,7 @@ def cleanup_files(dest_path, files_to_keep, dirs_to_keep, fileutil, verbose_):
files_to_keep: list of full file paths to keep (not delete)
dirs_to_keep: list of full dir paths to keep (not delete if they are empty)
fileutil: FileUtil object
verbose_: verbose callable for printing verbose output
verbose: verbose callable for printing verbose output
Returns:
tuple of (list of files deleted, list of directories deleted)
@ -2593,7 +2586,7 @@ def cleanup_files(dest_path, files_to_keep, dirs_to_keep, fileutil, verbose_):
deleted_files = []
for p in pathlib.Path(dest_path).rglob("*"):
if p.is_file() and normalize_fs_path(str(p).lower()) not in keepers:
verbose_(f"Deleting [filepath]{p}")
verbose(f"Deleting [filepath]{p}")
fileutil.unlink(p)
deleted_files.append(str(p))
@ -2605,7 +2598,7 @@ def cleanup_files(dest_path, files_to_keep, dirs_to_keep, fileutil, verbose_):
continue
if not list(pathlib.Path(dirpath).glob("*")):
# directory and directory is empty
verbose_(f"Deleting empty directory {dirpath}")
verbose(f"Deleting empty directory {dirpath}")
fileutil.rmdir(dirpath)
deleted_dirs.append(str(dirpath))
@ -2623,7 +2616,7 @@ def write_finder_tags(
finder_tag_template=None,
strip=False,
export_dir=None,
verbose_=noop,
verbose=noop,
):
"""Write Finder tags (extended attributes) to files; only writes attributes if attributes on file differ from what would be written
@ -2637,7 +2630,7 @@ def write_finder_tags(
exiftool_merge_keywords: if True, include any keywords in the exif data of the source image as keywords
finder_tag_template: list of templates to evaluate for determining Finder tags
export_dir: value to use for {export_dir} template
verbose_: function to call to print verbose messages
verbose: function to call to print verbose messages
Returns:
(list of file paths that were updated with new Finder tags, list of file paths skipped because Finder tags didn't need updating)
@ -2697,11 +2690,11 @@ def write_finder_tags(
for f in files:
md = OSXMetaData(f)
if sorted(md.tags) != sorted(tags):
verbose_(f"Writing Finder tags to {f}")
verbose(f"Writing Finder tags to {f}")
md.tags = tags
written.append(f)
else:
verbose_(f"Skipping Finder tags for {f}: nothing to do")
verbose(f"Skipping Finder tags for {f}: nothing to do")
skipped.append(f)
return (written, skipped)
@ -2713,14 +2706,17 @@ def write_extended_attributes(
xattr_template,
strip=False,
export_dir=None,
verbose_=noop,
verbose=noop,
):
"""Writes extended attributes to exported files
Args:
photo: a PhotoInfo object
files: list of file paths to write extended attributes to
xattr_template: list of tuples: (attribute name, attribute template)
strip: xattr_template: list of tuples: (attribute name, attribute template)
export_dir: value to use for {export_dir} template
verbose: function to call to print verbose messages
Returns:
tuple(list of file paths that were updated with new attributes, list of file paths skipped because attributes didn't need updating)
@ -2770,10 +2766,10 @@ def write_extended_attributes(
if (not file_value and not value) or file_value == value:
# if both not set or both equal, nothing to do
# get returns None if not set and value will be [] if not set so can't directly compare
verbose_(f"Skipping extended attribute {attr} for {f}: nothing to do")
verbose(f"Skipping extended attribute {attr} for {f}: nothing to do")
skipped.add(f)
else:
verbose_(f"Writing extended attribute {attr} to {f}")
verbose(f"Writing extended attribute {attr} to {f}")
md.set(attr, value)
written.add(f)
@ -2788,7 +2784,7 @@ def run_post_command(
dry_run,
exiftool_path,
export_db,
verbose_,
verbose,
):
# todo: pass in RenderOptions from export? (e.g. so it contains strip, etc?)
# todo: need a shell_quote template type:
@ -2805,7 +2801,7 @@ def run_post_command(
command, _ = template.render(command_template, options=render_options)
command = command[0] if command else None
if command:
verbose_(f'Running command: "{command}"')
verbose(f'Running command: "{command}"')
if not dry_run:
args = shlex.split(command)
cwd = pathlib.Path(f).parent

View File

@ -34,6 +34,7 @@ from .click_rich_echo import (
set_rich_theme,
)
from .color_themes import get_theme
from .common import TIMESTAMP_OPTION, VERBOSE_OPTION
from .export import render_and_validate_report
from .param_types import TemplateString
from .report_writer import export_report_writer_factory
@ -150,7 +151,8 @@ from .verbose import get_verbose_console, verbose_print
help="If used with --report, add data to existing report file instead of overwriting it. "
"See also --report.",
)
@click.option("--verbose", "-V", is_flag=True, help="Print verbose output.")
@VERBOSE_OPTION
@TIMESTAMP_OPTION
@click.option(
"--dry-run",
is_flag=True,
@ -171,6 +173,7 @@ def exportdb(
report,
save_config,
sql,
timestamp,
touch_file,
update_signatures,
uuid_files,
@ -178,17 +181,11 @@ def exportdb(
delete_uuid,
delete_file,
vacuum,
verbose,
verbose_flag,
version,
):
"""Utilities for working with the osxphotos export database"""
color_theme = get_theme()
verbose_ = verbose_print(
verbose, timestamp=False, rich=True, theme=color_theme, highlight=False
)
# set console for rich_echo to be same as for verbose_
set_rich_console(get_verbose_console(theme=color_theme))
set_rich_theme(color_theme)
verbose = verbose_print(verbose_flag, timestamp=timestamp)
# validate options and args
if append and not report:
@ -264,7 +261,7 @@ def exportdb(
if update_signatures:
try:
updated, skipped = export_db_update_signatures(
export_db, export_dir, verbose_, dry_run
export_db, export_dir, verbose, dry_run
)
except Exception as e:
rich_echo(f"[error]Error: {e}[/error]")
@ -299,7 +296,7 @@ def exportdb(
if check_signatures:
try:
matched, notmatched, skipped = export_db_check_signatures(
export_db, export_dir, verbose_=verbose_
export_db, export_dir, verbose_=verbose
)
except Exception as e:
rich_echo(f"[error]Error: {e}[/error]")
@ -314,7 +311,7 @@ def exportdb(
if touch_file:
try:
touched, not_touched, skipped = export_db_touch_files(
export_db, export_dir, verbose_=verbose_, dry_run=dry_run
export_db, export_dir, verbose_=verbose, dry_run=dry_run
)
except Exception as e:
rich_echo(f"[error]Error: {e}[/error]")

View File

@ -27,7 +27,7 @@ from strpdatetime import strpdatetime
from osxphotos._constants import _OSXPHOTOS_NONE_SENTINEL
from osxphotos._version import __version__
from osxphotos.cli.common import get_data_dir
from osxphotos.cli.common import TIMESTAMP_OPTION, VERBOSE_OPTION, get_data_dir
from osxphotos.cli.help import HELP_WIDTH
from osxphotos.cli.param_types import FunctionCall, StrpDateTimePattern, TemplateString
from osxphotos.datetime_utils import (
@ -44,14 +44,7 @@ from osxphotos.phototemplate import PhotoTemplate, RenderOptions
from osxphotos.sqlitekvstore import SQLiteKVStore
from osxphotos.utils import pluralize
from .click_rich_echo import (
rich_click_echo,
rich_echo_error,
set_rich_console,
set_rich_theme,
set_rich_timestamp,
)
from .color_themes import get_theme
from .click_rich_echo import rich_click_echo, rich_echo_error
from .common import THEME_OPTION
from .rich_progress import rich_progress
from .verbose import get_verbose_console, verbose_print
@ -1362,10 +1355,8 @@ class ImportCommand(click.Command):
help="If used with --report, add data to existing report file instead of overwriting it. "
"See also --report.",
)
@click.option("--verbose", "-V", "verbose_", is_flag=True, help="Print verbose output.")
@click.option(
"--timestamp", "-T", is_flag=True, help="Add time stamp to verbose output"
)
@VERBOSE_OPTION
@TIMESTAMP_OPTION
@click.option(
"--no-progress", is_flag=True, help="Do not display progress bar during import."
)
@ -1419,19 +1410,12 @@ def import_cli(
theme,
timestamp,
title,
verbose_,
verbose_flag,
walk,
):
"""Import photos and videos into Photos."""
color_theme = get_theme(theme)
verbose = verbose_print(
verbose_, timestamp, rich=True, theme=color_theme, highlight=False
)
# set console for rich_echo to be same as for verbose_
set_rich_console(get_verbose_console())
set_rich_theme(color_theme)
set_rich_timestamp(timestamp)
verbose = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)
if not files:
echo("Nothing to import", err=True)

View File

@ -19,12 +19,16 @@ from osxphotos.fileutil import FileUtil
from osxphotos.utils import increment_filename, pluralize
from .click_rich_echo import rich_click_echo as echo
from .click_rich_echo import set_rich_console, set_rich_theme, set_rich_timestamp
from .color_themes import get_theme
from .common import DB_OPTION, THEME_OPTION, get_photos_db
from .common import (
DB_OPTION,
THEME_OPTION,
TIMESTAMP_OPTION,
VERBOSE_OPTION,
get_photos_db,
)
from .help import get_help_msg
from .list import _list_libraries
from .verbose import get_verbose_console, verbose_print
from .verbose import verbose_print
@click.command(name="orphans")
@ -36,22 +40,15 @@ from .verbose import get_verbose_console, verbose_print
help="Export orphans to directory EXPORT_PATH. If --export not specified, orphans are listed but not exported.",
)
@DB_OPTION
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
@click.option("--timestamp", is_flag=True, help="Add time stamp to verbose output")
@VERBOSE_OPTION
@TIMESTAMP_OPTION
@THEME_OPTION
@click.pass_obj
@click.pass_context
def orphans(ctx, cli_obj, export, db, verbose, timestamp, theme):
def orphans(ctx, cli_obj, export, db, verbose_flag, timestamp, theme):
"""Find orphaned photos in a Photos library"""
color_theme = get_theme(theme)
verbose_ = verbose_print(
verbose, timestamp, rich=True, theme=color_theme, highlight=False
)
# set console for rich_echo to be same as for verbose_
set_rich_console(get_verbose_console())
set_rich_theme(color_theme)
set_rich_timestamp(timestamp)
verbose_ = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)
# below needed for to make CliRunner work for testing
cli_db = cli_obj.db if cli_obj is not None else None

View File

@ -11,7 +11,7 @@ from osxphotos.cli.click_rich_echo import (
from osxphotos.debug import set_debug
from osxphotos.photosalbum import PhotosAlbum
from osxphotos.phototemplate import RenderOptions
from osxphotos.queryoptions import QueryOptions
from osxphotos.queryoptions import QueryOptions, load_uuid_from_file
from .color_themes import get_default_theme
from .common import (
@ -25,7 +25,6 @@ from .common import (
OSXPHOTOS_HIDDEN,
QUERY_OPTIONS,
get_photos_db,
load_uuid_from_file,
)
from .list import _list_libraries
from .print_photo_info import print_photo_fields, print_photo_info

View File

@ -13,19 +13,22 @@ from rich import pretty, print
import osxphotos
from osxphotos._constants import _PHOTOS_4_VERSION
from osxphotos.cli.click_rich_echo import rich_echo_error as echo_error
from osxphotos.photoinfo import PhotoInfo
from osxphotos.photosdb import PhotosDB
from osxphotos.pyrepl import embed_repl
from osxphotos.queryoptions import QueryOptions
from osxphotos.queryoptions import (
IncompatibleQueryOptions,
QueryOptions,
query_options_from_kwargs,
)
from .common import (
DB_ARGUMENT,
DB_OPTION,
DELETED_OPTIONS,
IncompatibleQueryOptions,
QUERY_OPTIONS,
get_photos_db,
query_options_from_kwargs,
)
@ -70,6 +73,11 @@ def repl(ctx, cli_obj, db, emacs, beta, **kwargs):
logger.disabled = True
pretty.install()
try:
query_options = query_options_from_kwargs(**kwargs)
except IncompatibleQueryOptions as e:
echo_error(f"Incompatible query options: {e}")
ctx.exit(1)
print(f"python version: {sys.version}")
print(f"osxphotos version: {osxphotos._version.__version__}")
db = db or get_photos_db()
@ -80,12 +88,6 @@ def repl(ctx, cli_obj, db, emacs, beta, **kwargs):
print("Beta mode enabled")
print("Getting photos")
tic = time.perf_counter()
try:
query_options = query_options_from_kwargs(**kwargs)
except IncompatibleQueryOptions:
click.echo("Incompatible query options", err=True)
click.echo(ctx.obj.group.commands["repl"].get_help(ctx), err=True)
sys.exit(1)
photos = _query_photos(photosdb, query_options)
all_photos = _get_all_photos(photosdb)
toc = time.perf_counter()

View File

@ -12,7 +12,13 @@ from rich.syntax import Syntax
import osxphotos
from .common import DB_OPTION, OSXPHOTOS_SNAPSHOT_DIR, get_photos_db
from .common import (
DB_OPTION,
OSXPHOTOS_SNAPSHOT_DIR,
TIMESTAMP_OPTION,
VERBOSE_OPTION,
get_photos_db,
)
from .verbose import verbose_print
@ -80,8 +86,9 @@ def snap(ctx, cli_obj, db):
"Default is 'monokai'.",
)
@click.argument("db2", nargs=-1, type=click.Path(exists=True))
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
def diff(ctx, cli_obj, db, raw_output, style, db2, verbose):
@VERBOSE_OPTION
@TIMESTAMP_OPTION
def diff(ctx, cli_obj, db, raw_output, style, db2, verbose_flag, timestamp):
"""Compare two Photos databases and print out differences
To use the diff command, you'll need to install sqldiff via homebrew:
@ -110,7 +117,7 @@ def diff(ctx, cli_obj, db, raw_output, style, db2, verbose):
Works only on Photos library versions since Catalina (10.15) or newer.
"""
verbose_ = verbose_print(verbose, rich=True)
verbose = verbose_print(verbose_flag, timestamp=timestamp)
sqldiff = shutil.which("sqldiff")
if not sqldiff:
@ -118,7 +125,7 @@ def diff(ctx, cli_obj, db, raw_output, style, db2, verbose):
"sqldiff not found; install via homebrew (https://brew.sh/): `brew install sqldiff`"
)
ctx.exit(2)
verbose_(f"sqldiff found at '{sqldiff}'")
verbose(f"sqldiff found at '{sqldiff}'")
db = get_photos_db(db, cli_obj.db)
db_path = pathlib.Path(db)
@ -133,7 +140,7 @@ def diff(ctx, cli_obj, db, raw_output, style, db2, verbose):
else:
# get most recent snapshot
db_folder = os.environ.get("OSXPHOTOS_SNAPSHOT", OSXPHOTOS_SNAPSHOT_DIR)
verbose_(f"Using snapshot folder: '{db_folder}'")
verbose(f"Using snapshot folder: '{db_folder}'")
folders = sorted([f for f in pathlib.Path(db_folder).glob("*") if f.is_dir()])
folder_2 = folders[-1]
db_2 = folder_2 / "Photos.sqlite"
@ -143,7 +150,7 @@ def diff(ctx, cli_obj, db, raw_output, style, db2, verbose):
if not db_2.exists():
print(f"database file {db_2} missing")
verbose_(f"Comparing databases {db_1} and {db_2}")
verbose(f"Comparing databases {db_1} and {db_2}")
diff_proc = subprocess.Popen([sqldiff, db_2, db_1], stdout=subprocess.PIPE)
console = Console()

View File

@ -16,19 +16,23 @@ from osxphotos.photoinfo import PhotoInfoNone
from osxphotos.photosalbum import PhotosAlbum
from osxphotos.photosdb.photosdb_utils import get_db_version
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
from osxphotos.queryoptions import QueryOptions
from osxphotos.queryoptions import (
IncompatibleQueryOptions,
QueryOptions,
query_options_from_kwargs,
)
from osxphotos.sqlitekvstore import SQLiteKVStore
from osxphotos.utils import pluralize
from .click_rich_echo import (
rich_click_echo,
rich_echo_error,
set_rich_console,
set_rich_theme,
set_rich_timestamp,
from .click_rich_echo import rich_click_echo as echo
from .click_rich_echo import rich_echo_error as echo_error
from .common import (
DB_OPTION,
QUERY_OPTIONS,
THEME_OPTION,
TIMESTAMP_OPTION,
VERBOSE_OPTION,
)
from .color_themes import get_theme
from .common import DB_OPTION, QUERY_OPTIONS, THEME_OPTION, query_options_from_kwargs
from .param_types import TemplateString
from .report_writer import sync_report_writer_factory
from .rich_progress import rich_progress
@ -107,7 +111,7 @@ def render_and_validate_report(report: str) -> str:
report = report_file[0]
if os.path.isdir(report):
rich_click_echo(
echo(
f"[error]Report '{report}' is a directory, must be file name",
err=True,
)
@ -183,7 +187,7 @@ def export_metadata(
verbose(f"Analyzing [num]{num_photos}[/] {photo_word} to export")
verbose(f"Exporting [num]{len(photos)}[/] {photo_word} to {output_path}")
export_metadata_to_db(photos, metadata_db, progress=True)
rich_click_echo(
echo(
f"Done: exported metadata for [num]{len(photos)}[/] {photo_word} to [filepath]{output_path}[/]"
)
metadata_db.close()
@ -289,7 +293,7 @@ def import_metadata(
elif import_type == "export":
import_db = open_metadata_db(import_path)
else:
rich_echo_error(
echo_error(
f"Unable to determine type of import file: [filepath]{import_path}[/]"
)
raise click.Abort()
@ -309,7 +313,7 @@ def import_metadata(
elif unmatched:
# unable to find metadata for photo in import_db
for photo in key_photos:
rich_click_echo(
echo(
f"Unable to find metadata for [filename]{photo.original_filename}[/] ([uuid]{photo.uuid}[/]) in [filepath]{import_path}[/]"
)
@ -317,7 +321,7 @@ def import_metadata(
# find any keys in import_db that don't match keys in photos
for key in import_db.keys():
if key not in key_to_photo:
rich_click_echo(f"Unable to find [uuid]{key}[/] in selected photos.")
echo(f"Unable to find [uuid]{key}[/] in selected photos.")
return results
@ -368,7 +372,7 @@ def _update_albums_for_photo(
before = sorted(photo.albums)
albums_to_add = set(value) - set(before)
if not albums_to_add:
verbose(f"\tNothing to do for albums")
verbose(f"\tNothing to do for albums", level=2)
results.add_result(
photo.uuid,
photo.original_filename,
@ -426,7 +430,7 @@ def _set_metadata_for_photo(
if not dry_run:
set_photo_property(photo_, field, value)
else:
verbose(f"\tNothing to do for {field}")
verbose(f"\tNothing to do for {field}", level=2)
results.add_result(
photo.uuid,
@ -464,7 +468,7 @@ def _merge_metadata_for_photo(
before = sorted(before)
if value == before:
verbose(f"\tNothing to do for {field}")
verbose(f"\tNothing to do for {field}", level=2)
results.add_result(
photo.uuid,
photo.original_filename,
@ -486,7 +490,7 @@ def _merge_metadata_for_photo(
elif before is None:
new_value = value
else:
rich_echo_error(
echo_error(
f"Unable to merge {field} for [filename]{photo.original_filename}[filename]"
)
raise click.Abort()
@ -498,7 +502,7 @@ def _merge_metadata_for_photo(
else:
# Merge'd value might still be the same as original value
# (e.g. if value is str and has previously been merged)
verbose(f"\tNothing to do for {field}")
verbose(f"\tNothing to do for {field}", level=2)
results.add_result(
photo.uuid,
@ -534,7 +538,7 @@ def print_import_summary(results: SyncResults):
f"updated {property}: [num]{summary.get(property,0)}[/]"
for property in SYNC_PROPERTIES
)
rich_click_echo(
echo(
f"Processed [num]{summary['total']}[/] photos, updated: [num]{summary['updated']}[/], {property_summary}"
)
@ -634,10 +638,8 @@ def print_import_summary(results: SyncResults):
is_flag=True,
help="Dry run; " "when used with --import, don't actually update metadata.",
)
@click.option("--verbose", "-V", "verbose_", is_flag=True, help="Print verbose output.")
@click.option(
"--timestamp", "-T", is_flag=True, help="Add time stamp to verbose output."
)
@VERBOSE_OPTION
@TIMESTAMP_OPTION
@QUERY_OPTIONS
@DB_OPTION
@THEME_OPTION
@ -657,7 +659,7 @@ def sync(
theme,
timestamp,
unmatched,
verbose_,
verbose_flag,
**kwargs, # query options
):
"""Sync metadata and albums between Photos libraries.
@ -711,23 +713,17 @@ def sync(
osxphotos sync --export /path/to/export/folder/computer2.db --merge all --import /path/to/export/folder/computer1.db
"""
color_theme = get_theme(theme)
verbose = verbose_print(
verbose_, timestamp, rich=True, theme=color_theme, highlight=False
)
# set console for rich_echo to be same as for verbose_
set_rich_console(get_verbose_console())
set_rich_theme(color_theme)
set_rich_timestamp(timestamp)
verbose = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)
if (set_ or merge) and not import_path:
rich_echo_error("--set and --merge can only be used with --import")
echo_error("--set and --merge can only be used with --import")
ctx.exit(1)
# filter out photos in shared albums as these cannot be updated
# Not elegant but works for now without completely refactoring QUERY_OPTIONS
if kwargs.get("shared"):
rich_echo_error(
echo_error(
"[warning]--shared cannot be used with --import/--export "
"as photos in shared iCloud albums cannot be updated; "
"--shared will be ignored[/]"
@ -748,14 +744,19 @@ def sync(
set_ = set(set_)
merge = set(merge)
if set_ & merge:
rich_echo_error(
echo_error(
"--set and --merge cannot be used with the same fields: "
f"set: {set_}, merge: {merge}"
)
ctx.exit(1)
if import_path:
query_options = query_options_from_kwargs(**kwargs)
try:
query_options = query_options_from_kwargs(**kwargs)
except IncompatibleQueryOptions:
echo_error("Incompatible query options")
echo_error(ctx.obj.group.commands["repl"].get_help(ctx))
ctx.exit(1)
photosdb = PhotosDB(dbfile=db, verbose=verbose)
photos = photosdb.query(query_options)
results = import_metadata(

View File

@ -25,16 +25,10 @@ from osxphotos.photosalbum import PhotosAlbumPhotoScript
from osxphotos.phototz import PhotoTimeZone, PhotoTimeZoneUpdater
from osxphotos.utils import noop, pluralize
from .click_rich_echo import (
rich_click_echo,
rich_echo,
rich_echo_error,
set_rich_console,
set_rich_theme,
set_rich_timestamp,
)
from .click_rich_echo import rich_click_echo as echo
from .click_rich_echo import rich_echo_error as echo_error
from .color_themes import get_theme
from .common import THEME_OPTION
from .common import THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
from .darkmode import is_dark_mode
from .help import HELP_WIDTH, rich_text
from .param_types import (
@ -310,7 +304,8 @@ command which can be used to change the time zone of photos after import.
help="When used with --compare-exif, adds any photos with date/time/timezone differences "
"between Photos/EXIF to album ALBUM. If ALBUM does not exist, it will be created.",
)
@click.option("--verbose", "-V", "verbose_", is_flag=True, help="Show verbose output.")
@VERBOSE_OPTION
@TIMESTAMP_OPTION
@click.option(
"--library",
"-L",
@ -326,19 +321,6 @@ command which can be used to change the time zone of photos after import.
type=click.Path(exists=True),
help="Optional path to exiftool executable (will look in $PATH if not specified) for those options which require exiftool.",
)
@click.option(
"--output-file",
"-o",
type=click.File(mode="w", lazy=False),
help="Output file. If not specified, output is written to stdout.",
)
@click.option(
"--terminal-width",
"-w",
type=int,
help="Terminal width in characters.",
hidden=True,
)
@click.option("--timestamp", is_flag=True, help="Add time stamp to verbose output")
@THEME_OPTION
@click.option(
@ -366,13 +348,11 @@ def timewarp(
use_file_time,
add_to_album,
exiftool_path,
verbose_,
verbose_flag,
library,
theme,
parse_date,
plain,
output_file,
terminal_width,
timestamp,
force,
):
@ -419,33 +399,7 @@ def timewarp(
if add_to_album and not compare_exif:
raise click.UsageError("--add-to-album must be used with --compare-exif.")
# configure colored rich output
# TODO: this is all a little hacky, find a better way to do this
color_theme = get_theme(theme)
verbose = verbose_print(
verbose_,
timestamp,
rich=True,
theme=color_theme,
highlight=False,
file=output_file,
)
# set console for rich_echo to be same as for verbose_
terminal_width = terminal_width or (1000 if output_file else None)
if output_file:
set_rich_console(Console(file=output_file, width=terminal_width))
elif terminal_width:
set_rich_console(
Console(
file=sys.stdout,
theme=color_theme,
force_terminal=True,
width=terminal_width,
)
)
else:
set_rich_console(get_verbose_console(theme=color_theme))
set_rich_theme(color_theme)
verbose = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)
if any([compare_exif, push_exif, pull_exif]):
exiftool_path = exiftool_path or get_exiftool_path()
@ -454,19 +408,19 @@ def timewarp(
try:
photos = PhotosLibrary().selection
if not photos:
rich_echo_error("[warning]No photos selected[/]")
echo_error("[warning]No photos selected[/]")
sys.exit(0)
except Exception as e:
# AppleScript error -1728 occurs if user attempts to get selected photos in a Smart Album
if "(-1728)" in str(e):
rich_echo_error(
echo_error(
"[error]Could not get selected photos. Ensure photos is open and photos are selected. "
"If you have selected photos and you see this message, it may be because the selected photos are in a Photos Smart Album. "
f"{APP_NAME} cannot access photos in a Smart Album. Select the photos in a regular album or in 'All Photos' view. "
"Another option is to create a new album using 'File | New Album With Selection' then select the photos in the new album.[/]",
)
else:
rich_echo_error(
echo_error(
f"[error]Could not get selected photos. Ensure Photos is open and photos to process are selected. {e}[/]",
)
sys.exit(1)
@ -535,7 +489,7 @@ def timewarp(
if inspect:
tzinfo = PhotoTimeZone(library_path=library)
if photos:
rich_echo(
echo(
"[filename]filename[/filename], [uuid]uuid[/uuid], "
"[time]photo time (local)[/time], "
"[time]photo time[/time], "
@ -545,7 +499,7 @@ def timewarp(
tz_seconds, tz_str, tz_name = tzinfo.get_timezone(photo)
photo_date_local = datetime_naive_to_local(photo.date)
photo_date_tz = datetime_to_new_tz(photo_date_local, tz_seconds)
rich_echo(
echo(
f"[filename]{photo.filename}[/filename], [uuid]{photo.uuid}[/uuid], "
f"[time]{photo_date_local.strftime(DATETIME_FORMAT)}[/time], "
f"[time]{photo_date_tz.strftime(DATETIME_FORMAT)}[/time], "
@ -563,7 +517,7 @@ def timewarp(
exiftool_path=exiftool_path,
)
if not album:
rich_echo(
echo(
"filename, uuid, photo time (Photos), photo time (EXIF), timezone offset (Photos), timezone offset (EXIF)"
)
for photo in photos:
@ -592,13 +546,13 @@ def timewarp(
else:
verbose(f"Photo {filename} ({uuid}) has same date/time/timezone")
else:
rich_echo(
echo(
f"{filename}, {uuid}, "
f"{diff_results.photos_date} {diff_results.photos_time}, {diff_results.exif_date} {diff_results.exif_time}, "
f"{diff_results.photos_tz}, {diff_results.exif_tz}"
)
if album:
rich_echo(
echo(
f"Compared {len(photos)} photos, found {different_photos} "
f"that {pluralize(different_photos, 'is', 'are')} different and "
f"added {pluralize(different_photos, 'it', 'them')} to album '{album.name}'."
@ -649,12 +603,10 @@ def timewarp(
# before exiftool is run
exif_warn, exif_error = exif_updater.update_exif_from_photos(p)
if exif_warn:
rich_echo_error(
f"[warning]Warning running exiftool: {exif_warn}[/]"
)
echo_error(f"[warning]Warning running exiftool: {exif_warn}[/]")
if exif_error:
rich_echo_error(f"[error]Error running exiftool: {exif_error}[/]")
echo_error(f"[error]Error running exiftool: {exif_error}[/]")
progress.advance(task)
rich_echo("Done.")
echo("Done.")

View File

@ -1,14 +1,22 @@
"""helper functions for printing verbose output"""
from __future__ import annotations
import os
import typing as t
from datetime import datetime
from typing import IO, Any, Callable, Optional
import click
from rich.console import Console
from rich.theme import Theme
from .click_rich_echo import rich_click_echo
from .click_rich_echo import (
rich_click_echo,
set_rich_console,
set_rich_theme,
set_rich_timestamp,
)
from .color_themes import get_theme
from .common import CLI_COLOR_ERROR, CLI_COLOR_WARNING, time_stamp
# set to 1 if running tests
@ -17,14 +25,79 @@ OSXPHOTOS_IS_TESTING = bool(os.getenv("OSXPHOTOS_IS_TESTING", default=False))
# include error/warning emoji's in verbose output
ERROR_EMOJI = True
__all__ = ["get_verbose_console", "verbose_print"]
# global to store verbose level
__verbose_level = 1
# global verbose function
__verbose_function: Callable[..., None] | None = None
__all__ = [
"get_verbose_console",
"get_verbose_level",
"set_verbose_level",
"verbose_print",
"verbose",
]
def _reset_verbose_globals():
"""Reset globals for testing"""
global __verbose_level
global __verbose_function
global _console
__verbose_level = 1
__verbose_function = None
_console = _Console()
def noop(*args, **kwargs):
"""no-op function"""
pass
def verbose(*args, level: int = 1, **kwargs):
"""Print verbose output
Args:
*args: arguments to pass to verbose function for printing
level: verbose level; if level > get_verbose_level(), output is suppressed
Notes:
Normally you should use verbose_print() to get the verbose function instead of calling this directly
"""
global __verbose_function
if __verbose_function is None:
return
__verbose_function(*args, level=level, **kwargs)
def set_verbose_level(level: int):
"""Set verbose level"""
global __verbose_level
global __verbose_function
__verbose_level = level
if level > 0 and __verbose_function is None:
# if verbose level set but verbose function not set, set it to default
# verbose_print sets the global __verbose_function
__verbose_function = _verbose_print_function(level)
elif level == 0 and __verbose_function is not None:
# if verbose level set to 0 but verbose function is set, set it to no-op
__verbose_function = noop
def get_verbose_level() -> int:
"""Get verbose level"""
global __verbose_level
return __verbose_level
class _Console:
"""Store console object for verbose output"""
def __init__(self):
self._console: t.Optional[Console] = None
self._console: Optional[Console] = None
@property
def console(self):
@ -38,12 +111,7 @@ class _Console:
_console = _Console()
def noop(*args, **kwargs):
"""no-op function"""
pass
def get_verbose_console(theme: t.Optional[Theme] = None) -> Console:
def get_verbose_console(theme: Optional[Theme] = None) -> Console:
"""Get console object or create one if not already created
Args:
@ -59,18 +127,68 @@ def get_verbose_console(theme: t.Optional[Theme] = None) -> Console:
def verbose_print(
verbose: int = 1,
timestamp: bool = False,
rich: bool = True,
theme: str | None = None,
highlight: bool = False,
file: Optional[IO] = None,
**kwargs: Any,
) -> Callable[..., None]:
"""Configure verbose printing and create verbose function to print output
Args:
verbose: if > 0, returns verbose print function otherwise returns no-op function; the value of verbose is the verbose level
timestamp: if True, includes timestamp in verbose output
rich: use rich.print instead of click.echo
highlight: if True, use automatic rich.print highlighting
theme: optional name of theme to use for formatting (will be loaded by get_theme())
file: optional file handle to write to instead of stdout
kwargs: any extra arguments to pass to click.echo or rich.print depending on whether rich==True
Returns:
function to print output
Note: sets the console for rich_echo to be the same as the console used for verbose output
"""
set_verbose_level(verbose)
color_theme = get_theme(theme)
verbose_function = _verbose_print_function(
verbose=verbose,
timestamp=timestamp,
rich=rich,
theme=color_theme,
highlight=highlight,
file=file,
**kwargs,
)
# set console for rich_echo to be same as for verbose
set_rich_console(get_verbose_console())
set_rich_theme(color_theme)
set_rich_timestamp(timestamp)
# set global verbose function to match
global __verbose_function
__verbose_function = verbose_function
return verbose_function
def _verbose_print_function(
verbose: bool = True,
timestamp: bool = False,
rich: bool = False,
highlight: bool = False,
theme: t.Optional[Theme] = None,
file: t.Optional[t.IO] = None,
**kwargs: t.Any,
) -> t.Callable:
theme: Optional[Theme] = None,
file: Optional[IO] = None,
**kwargs: Any,
) -> Callable[..., None]:
"""Create verbose function to print output
Args:
verbose: if True, returns verbose print function otherwise returns no-op function
verbose: if > 0, returns verbose print function otherwise returns no-op function; the value of verbose is the verbose level
timestamp: if True, includes timestamp in verbose output
rich: use rich.print instead of click.echo
highlight: if True, use automatic rich.print highlighting
@ -91,8 +209,10 @@ def verbose_print(
_console.console = Console(theme=theme, width=10_000)
# closure to capture timestamp
def verbose_(*args):
def verbose_(*args, level: int = 1):
"""print output if verbose flag set"""
if get_verbose_level() < level:
return
styled_args = []
timestamp_str = f"{str(datetime.now())} -- " if timestamp else ""
for arg in args:
@ -103,10 +223,12 @@ def verbose_print(
elif "warning" in arg.lower():
arg = click.style(arg, fg=CLI_COLOR_WARNING)
styled_args.append(arg)
click.echo(*styled_args, **kwargs)
click.echo(*styled_args, **kwargs, file=file or None)
def rich_verbose_(*args):
def rich_verbose_(*args, level: int = 1):
"""rich.print output if verbose flag set"""
if get_verbose_level() < level:
return
global ERROR_EMOJI
timestamp_str = time_stamp() if timestamp else ""
new_args = []
@ -124,8 +246,10 @@ def verbose_print(
new_args.append(arg)
_console.console.print(*new_args, highlight=highlight, **kwargs)
def rich_verbose_testing_(*args):
def rich_verbose_testing_(*args, level: int = 1):
"""print output if verbose flag set using rich.print"""
if get_verbose_level() < level:
return
global ERROR_EMOJI
timestamp_str = time_stamp() if timestamp else ""
new_args = []

View File

@ -1,5 +1,7 @@
"""Utilities for debugging"""
from __future__ import annotations
import logging
import sys
import time
@ -9,21 +11,33 @@ from typing import Dict, List
import wrapt
from rich import print
__all__ = [
"debug_breakpoint",
"debug_watch",
"get_debug_flags",
"get_debug_options",
"is_debug",
"set_debug",
"wrap_function",
]
# global variable to control debug output
# set via --debug
DEBUG = False
__osxphotos_debug = False
def set_debug(debug: bool):
"""set debug flag"""
global DEBUG
DEBUG = debug
global __osxphotos_debug
__osxphotos_debug = debug
logging.disable(logging.NOTSET if debug else logging.DEBUG)
def is_debug():
"""return debug flag"""
return DEBUG
global __osxphotos_debug
return __osxphotos_debug
def debug_watch(wrapped, instance, args, kwargs):

View File

@ -8,7 +8,6 @@ import contextlib
import dataclasses
import datetime
import json
import logging
import os
import os.path
import pathlib
@ -16,6 +15,7 @@ import plistlib
from datetime import timedelta, timezone
from functools import cached_property
from typing import Any, Dict, Optional
import logging
import yaml
from osxmetadata import OSXMetaData
@ -68,6 +68,7 @@ from .utils import _get_resource_loc, hexdigest, list_directory
__all__ = ["PhotoInfo", "PhotoInfoNone"]
logger = logging.getLogger("osxphotos")
class PhotoInfo:
"""
@ -245,7 +246,7 @@ class PhotoInfo:
photopath = self._path_edited_5()
if photopath is not None and not os.path.isfile(photopath):
logging.debug(
logger.debug(
f"edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
)
photopath = None
@ -285,7 +286,7 @@ class PhotoInfo:
filename = f"{self._uuid}_2_0_a.mov"
else:
# don't know what it is!
logging.debug(f"WARNING: unknown type {self._info['type']}")
logger.debug(f"WARNING: unknown type {self._info['type']}")
return None
return os.path.join(library, "resources", "renders", directory, filename)
@ -332,7 +333,7 @@ class PhotoInfo:
try:
photopath = self._get_predicted_path_edited_4()
except ValueError as e:
logging.debug(f"ERROR: {e}")
logger.debug(f"ERROR: {e}")
photopath = None
if photopath is not None and not os.path.isfile(photopath):
@ -346,12 +347,12 @@ class PhotoInfo:
# check again to see if we found a valid file
if photopath is not None and not os.path.isfile(photopath):
logging.debug(
logger.debug(
f"MISSING PATH: edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
)
photopath = None
else:
logging.debug(f"{self.uuid} hasAdjustments but edit_resource_id is None")
logger.debug(f"{self.uuid} hasAdjustments but edit_resource_id is None")
photopath = None
return photopath
@ -400,7 +401,7 @@ class PhotoInfo:
None,
)
if photopath is None:
logging.debug(
logger.debug(
f"MISSING PATH: edited live photo file for UUID {self._uuid} does not appear to exist"
)
return photopath
@ -496,7 +497,7 @@ class PhotoInfo:
self._db._masters_path, self._info["raw_info"]["imagePath"]
)
if not os.path.isfile(photopath):
logging.debug(
logger.debug(
f"MISSING PATH: RAW photo for UUID {self._uuid} should be at {photopath} but does not appear to exist"
)
photopath = None
@ -912,7 +913,7 @@ class PhotoInfo:
if self.live_photo and not self.ismissing:
live_model_id = self._info["live_model_id"]
if live_model_id is None:
logging.debug(f"missing live_model_id: {self._uuid}")
logger.debug(f"missing live_model_id: {self._uuid}")
photopath = None
else:
folder_id, file_id, nn_id = _get_resource_loc(live_model_id)
@ -1196,7 +1197,7 @@ class PhotoInfo:
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
logging.debug(f"score not implemented for this database version")
logger.debug(f"score not implemented for this database version")
return None
try:
@ -1342,7 +1343,7 @@ class PhotoInfo:
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
logging.debug(f"exif_info not implemented for this database version")
logger.debug(f"exif_info not implemented for this database version")
return None
try:
@ -1369,7 +1370,7 @@ class PhotoInfo:
lens_model=exif["ZLENSMODEL"],
)
except KeyError:
logging.debug(f"Could not find exif record for uuid {self.uuid}")
logger.debug(f"Could not find exif record for uuid {self.uuid}")
exif_info = ExifInfo(
iso=None,
flash_fired=None,
@ -1441,7 +1442,7 @@ class PhotoInfo:
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
logging.debug(f"cloud_metadata not implemented for this database version")
logger.debug(f"cloud_metadata not implemented for this database version")
return {}
_, cursor = self._db.get_db_connection()

View File

@ -34,7 +34,7 @@ from wurlitzer import pipes
from .fileutil import FileUtil
from .uti import get_preferred_uti_extension
from .utils import _get_os_version, increment_filename
from .utils import get_macos_version, increment_filename
__all__ = [
"NSURL_to_path",
@ -124,7 +124,7 @@ def request_photokit_authorization():
will do the actual request.
"""
(_, major, _) = _get_os_version()
(_, major, _) = get_macos_version()
def handler(status):
pass

View File

@ -34,6 +34,8 @@ from ..utils import normalize_unicode
These methods only work on Photos 5 databases. Will print warning on earlier library versions.
"""
logger = logging.getLogger("osxphotos")
def _process_searchinfo(self):
"""load machine learning/search term label info from a Photos library
@ -55,12 +57,12 @@ def _process_searchinfo(self):
self._db_searchinfo_labels_normalized = _db_searchinfo_labels_normalized = {}
if self._skip_searchinfo:
logging.debug("Skipping search info processing")
logger.debug("Skipping search info processing")
return
if self._db_version <= _PHOTOS_4_VERSION:
raise NotImplementedError(
f"search info not implemented for this database version"
"search info not implemented for this database version"
)
search_db_path = pathlib.Path(self._dbfile).parent / "search" / "psi.sqlite"

View File

@ -53,7 +53,6 @@ from .._constants import (
from .._version import __version__
from ..albuminfo import AlbumInfo, FolderInfo, ImportInfo, ProjectInfo
from ..datetime_utils import datetime_has_tz, datetime_naive_to_local
from ..debug import is_debug
from ..fileutil import FileUtil
from ..personinfo import PersonInfo
from ..photoinfo import PhotoInfo
@ -63,13 +62,15 @@ from ..rich_utils import add_rich_markup_tag
from ..sqlite_utils import sqlite_db_is_locked, sqlite_open_ro
from ..utils import (
_check_file_exists,
_get_os_version,
get_macos_version,
get_last_library_path,
noop,
normalize_unicode,
)
from .photosdb_utils import get_db_model_version, get_db_version
logger = logging.getLogger("osxphotos")
__all__ = ["PhotosDB"]
# TODO: Add test for imageTimeZoneOffsetSeconds = None
@ -117,7 +118,7 @@ class PhotosDB:
# Check OS version
system = platform.system()
(ver, major, _) = _get_os_version()
(ver, major, _) = get_macos_version()
if system != "Darwin" or ((ver, major) not in _TESTED_OS_VERSIONS):
logging.warning(
f"WARNING: This module has only been tested with macOS versions "
@ -283,8 +284,7 @@ class PhotosDB:
# key is Z_PK of ZMOMENT table and values are the moment info
self._db_moment_pk = {}
if is_debug():
logging.debug(f"dbfile = {dbfile}")
logger.debug(f"dbfile = {dbfile}")
if dbfile is None:
dbfile = get_last_library_path()
@ -300,8 +300,7 @@ class PhotosDB:
if not _check_file_exists(dbfile):
raise FileNotFoundError(f"dbfile {dbfile} does not exist", dbfile)
if is_debug():
logging.debug(f"dbfile = {dbfile}")
logger.debug(f"dbfile = {dbfile}")
# init database names
# _tmp_db is the file that will processed by _process_database4/5
@ -352,10 +351,9 @@ class PhotosDB:
# set the photos version to actual value based on Photos.sqlite
self._photos_ver = get_db_model_version(self._tmp_db)
if is_debug():
logging.debug(
f"_dbfile = {self._dbfile}, _dbfile_actual = {self._dbfile_actual}"
)
logger.debug(
f"_dbfile = {self._dbfile}, _dbfile_actual = {self._dbfile_actual}"
)
library_path = os.path.dirname(os.path.abspath(dbfile))
(library_path, _) = os.path.split(library_path) # drop /database from path
@ -367,8 +365,7 @@ class PhotosDB:
masters_path = os.path.join(library_path, "originals")
self._masters_path = masters_path
if is_debug():
logging.debug(f"library = {library_path}, masters = {masters_path}")
logger.debug(f"library = {library_path}, masters = {masters_path}")
if int(self._db_version) <= int(_PHOTOS_4_VERSION):
self._process_database4()
@ -628,38 +625,10 @@ class PhotosDB:
print(f"Error copying{fname} to {dest_path}", file=sys.stderr)
raise Exception
if is_debug():
logging.debug(dest_path)
logger.debug(dest_path)
return dest_path
# NOTE: This method seems to cause problems with applescript
# Bummer...would'be been nice to avoid copying the DB
# def _link_db_file(self, fname):
# """ links the sqlite database file to a temp file """
# """ returns the name of the temp file """
# """ If sqlite shared memory and write-ahead log files exist, those are copied too """
# # required because python's sqlite3 implementation can't read a locked file
# # _, suffix = os.path.splitext(fname)
# dest_name = dest_path = ""
# try:
# dest_name = pathlib.Path(fname).name
# dest_path = os.path.join(self._tempdir_name, dest_name)
# FileUtil.hardlink(fname, dest_path)
# # link write-ahead log and shared memory files (-wal and -shm) files if they exist
# if os.path.exists(f"{fname}-wal"):
# FileUtil.hardlink(f"{fname}-wal", f"{dest_path}-wal")
# if os.path.exists(f"{fname}-shm"):
# FileUtil.hardlink(f"{fname}-shm", f"{dest_path}-shm")
# except:
# print("Error linking " + fname + " to " + dest_path, file=sys.stderr)
# raise Exception
# if is_debug():
# logging.debug(dest_path)
# return dest_path
def _process_database4(self):
"""process the Photos database to extract info
works on Photos version <= 4.0"""
@ -741,7 +710,7 @@ class PhotosDB:
self._dbpersons_pk[pk]["photo_uuid"] = person[2]
self._dbpersons_pk[pk]["keyface_uuid"] = person[3]
except KeyError:
logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
logger.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
# get information on detected faces
verbose("Processing detected faces in photos.")
@ -1118,8 +1087,7 @@ class PhotosDB:
self._dbphotos[uuid]["type"] = _MOVIE_TYPE
else:
# unknown
if is_debug():
logging.debug(f"WARNING: {uuid} found unknown type {row[21]}")
logger.debug(f"WARNING: {uuid} found unknown type {row[21]}")
self._dbphotos[uuid]["type"] = None
self._dbphotos[uuid]["UTI"] = row[22]
@ -1352,21 +1320,19 @@ class PhotosDB:
if resource_type == 4:
# photo
if "edit_resource_id_photo" in self._dbphotos[uuid]:
if is_debug():
logging.debug(
f"WARNING: found more than one edit_resource_id_photo for "
f"UUID {row[0]},adjustmentUUID {row[1]}, modelID {row[2]}"
)
logger.debug(
f"WARNING: found more than one edit_resource_id_photo for "
f"UUID {row[0]},adjustmentUUID {row[1]}, modelID {row[2]}"
)
self._dbphotos[uuid]["edit_resource_id_photo"] = row[2]
self._dbphotos[uuid]["UTI_edited_photo"] = row[4]
elif resource_type == 8:
# video
if "edit_resource_id_video" in self._dbphotos[uuid]:
if is_debug():
logging.debug(
f"WARNING: found more than one edit_resource_id_video for "
f"UUID {row[0]},adjustmentUUID {row[1]}, modelID {row[2]}"
)
logger.debug(
f"WARNING: found more than one edit_resource_id_video for "
f"UUID {row[0]},adjustmentUUID {row[1]}, modelID {row[2]}"
)
self._dbphotos[uuid]["edit_resource_id_video"] = row[2]
self._dbphotos[uuid]["UTI_edited_video"] = row[4]
@ -1655,8 +1621,7 @@ class PhotosDB:
but it works so don't touch it.
"""
if is_debug():
logging.debug(f"_process_database5")
logger.debug(f"_process_database5")
verbose = self._verbose
verbose(f"Processing database.")
(conn, c) = sqlite_open_ro(self._tmp_db)
@ -1679,8 +1644,7 @@ class PhotosDB:
hdr_type_column = _DB_TABLE_NAMES[photos_ver]["HDR_TYPE"]
# Look for all combinations of persons and pictures
if is_debug():
logging.debug(f"Getting information about persons")
logger.debug(f"Getting information about persons")
# get info to associate persons with photos
# then get detected faces in each photo and link to persons
@ -1757,7 +1721,7 @@ class PhotosDB:
self._dbpersons_pk[pk]["photo_uuid"] = person[2]
self._dbpersons_pk[pk]["keyface_uuid"] = person[3]
except KeyError:
logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
logger.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
# get information on detected faces
verbose("Processing detected faces in photos.")
@ -2095,8 +2059,7 @@ class PhotosDB:
elif row[17] == 1:
info["type"] = _MOVIE_TYPE
else:
if is_debug():
logging.debug(f"WARNING: {uuid} found unknown type {row[17]}")
logger.debug(f"WARNING: {uuid} found unknown type {row[17]}")
info["type"] = None
info["UTI"] = row[18]
@ -2283,7 +2246,7 @@ class PhotosDB:
self._dbphotos[uuid]["fok_import_session"] = row[2]
self._dbphotos[uuid]["import_uuid"] = row[3]
except KeyError:
logging.debug(f"No info record for uuid {uuid} for import session")
logger.debug(f"No info record for uuid {uuid} for import session")
# Get extended description
verbose("Processing additional photo details.")
@ -2300,10 +2263,9 @@ class PhotosDB:
if uuid in self._dbphotos:
self._dbphotos[uuid]["extendedDescription"] = normalize_unicode(row[1])
else:
if is_debug():
logging.debug(
f"WARNING: found description {row[1]} but no photo for {uuid}"
)
logger.debug(
f"WARNING: found description {row[1]} but no photo for {uuid}"
)
# get information about adjusted/edited photos
c.execute(
@ -2319,10 +2281,9 @@ class PhotosDB:
if uuid in self._dbphotos:
self._dbphotos[uuid]["adjustmentFormatID"] = row[2]
else:
if is_debug():
logging.debug(
f"WARNING: found adjustmentformatidentifier {row[2]} but no photo for uuid {row[0]}"
)
logger.debug(
f"WARNING: found adjustmentformatidentifier {row[2]} but no photo for uuid {row[0]}"
)
# Find missing photos
# TODO: this code is very kludgy and I had to make lots of assumptions
@ -2696,7 +2657,7 @@ class PhotosDB:
try:
folders = self._dbalbum_folders[album_uuid]
except KeyError:
logging.debug(f"Caught _dbalbum_folders KeyError for album: {album_uuid}")
logger.debug(f"Caught _dbalbum_folders KeyError for album: {album_uuid}")
return []
def _recurse_folder_hierarchy(folders, hierarchy=[]):
@ -2733,7 +2694,7 @@ class PhotosDB:
try:
folders = self._dbalbum_folders[album_uuid]
except KeyError:
logging.debug(f"Caught _dbalbum_folders KeyError for album: {album_uuid}")
logger.debug(f"Caught _dbalbum_folders KeyError for album: {album_uuid}")
return []
def _recurse_folder_hierarchy(folders, hierarchy=[]):

View File

@ -1,12 +1,20 @@
""" QueryOptions class for PhotosDB.query """
import dataclasses
import datetime
import pathlib
from dataclasses import asdict, dataclass
from typing import Iterable, List, Optional, Tuple
import bitmath
__all__ = ["QueryOptions"]
__all__ = ["QueryOptions", "query_options_from_kwargs", "IncompatibleQueryOptions"]
class IncompatibleQueryOptions(Exception):
"""Incompatible query options"""
pass
@dataclass
@ -182,3 +190,128 @@ class QueryOptions:
def asdict(self):
return asdict(self)
def query_options_from_kwargs(**kwargs) -> QueryOptions:
"""Validate query options and create a QueryOptions instance"""
# sanity check input args
nonexclusive = [
"added_after",
"added_before",
"added_in_last",
"album",
"duplicate",
"exif",
"external_edit",
"folder",
"from_date",
"from_time",
"has_raw",
"keyword",
"label",
"max_size",
"min_size",
"name",
"person",
"query_eval",
"query_function",
"regex",
"selected",
"to_date",
"to_time",
"uti",
"uuid",
"uuid_from_file",
"year",
]
exclusive = [
("burst", "not_burst"),
("cloudasset", "not_cloudasset"),
("edited", "not_edited"),
("favorite", "not_favorite"),
("has_comment", "no_comment"),
("has_likes", "no_likes"),
("hdr", "not_hdr"),
("hidden", "not_hidden"),
("in_album", "not_in_album"),
("incloud", "not_incloud"),
("is_reference", "not_reference"),
("keyword", "no_keyword"),
("live", "not_live"),
("location", "no_location"),
("missing", "not_missing"),
("only_photos", "only_movies"),
("panorama", "not_panorama"),
("portrait", "not_portrait"),
("screenshot", "not_screenshot"),
("selfie", "not_selfie"),
("shared", "not_shared"),
("slow_mo", "not_slow_mo"),
("time_lapse", "not_time_lapse"),
("deleted", "not_deleted"),
]
# TODO: add option to validate requiring at least one query arg
for arg, not_arg in exclusive:
if kwargs.get(arg) and kwargs.get(not_arg):
arg = arg.replace("_", "-")
not_arg = not_arg.replace("_", "-")
raise IncompatibleQueryOptions(
f"--{arg} and --{not_arg} are mutually exclusive"
)
# some options like title can be specified multiple times
# check if any of them are specified along with their no_ counterpart
exclusive_multi_options = ["title", "description", "place", "keyword"]
for option in exclusive_multi_options:
if kwargs.get(option) and kwargs.get("no_{option}"):
raise IncompatibleQueryOptions(
f"--{option} and --no-{option} are mutually exclusive"
)
include_photos = True
include_movies = True # default searches for everything
if kwargs.get("only_movies"):
include_photos = False
if kwargs.get("only_photos"):
include_movies = False
# load UUIDs if necessary and append to any uuids passed with --uuid
uuid = None
if uuid_from_file := kwargs.get("uuid_from_file"):
uuid_list = list(kwargs.get("uuid", [])) # Click option is a tuple
uuid_list.extend(load_uuid_from_file(uuid_from_file))
uuid = tuple(uuid_list)
query_fields = [field.name for field in dataclasses.fields(QueryOptions)]
query_dict = {field: kwargs.get(field) for field in query_fields}
query_dict["photos"] = include_photos
query_dict["movies"] = include_movies
query_dict["uuid"] = uuid
return QueryOptions(**query_dict)
def load_uuid_from_file(filename):
"""Load UUIDs from file. Does not validate UUIDs.
Format is 1 UUID per line, any line beginning with # is ignored.
Whitespace is stripped.
Arguments:
filename: file name of the file containing UUIDs
Returns:
list of UUIDs or empty list of no UUIDs in file
Raises:
FileNotFoundError if file does not exist
"""
if not pathlib.Path(filename).is_file():
raise FileNotFoundError(f"Could not find file {filename}")
uuid = []
with open(filename, "r") as uuid_file:
for line in uuid_file:
line = line.strip()
if len(line) and line[0] != "#":
uuid.append(line)
return uuid

View File

@ -5,6 +5,7 @@ import pathlib
import sqlite3
from typing import List, Tuple
logger = logging.getLogger("osxphotos")
def sqlite_open_ro(dbname: str) -> Tuple[sqlite3.Connection, sqlite3.Cursor]:
"""opens sqlite file dbname in read-only mode
@ -32,7 +33,7 @@ def sqlite_db_is_locked(dbname):
conn.close()
locked = False
except Exception as e:
logging.debug(f"sqlite_db_is_locked: {e}")
logger.debug(f"sqlite_db_is_locked: {e}")
locked = True
return locked

View File

@ -11,11 +11,11 @@ from Foundation import NSDictionary
# needed to capture system-level stderr
from wurlitzer import pipes
from .utils import _get_os_version
from .utils import get_macos_version
__all__ = ["detect_text", "make_request_handler"]
ver, major, minor = _get_os_version()
ver, major, minor = get_macos_version()
if ver == "10" and int(major) < 15:
vision = False
else:

View File

@ -26,7 +26,7 @@ import tempfile
import CoreServices
import objc
from .utils import _get_os_version
from .utils import get_macos_version
__all__ = ["get_preferred_uti_extension", "get_uti_for_extension"]
@ -522,7 +522,7 @@ def _load_uti_dict():
_load_uti_dict()
# OS version for determining which methods can be used
OS_VER, OS_MAJOR, _ = (int(x) for x in _get_os_version())
OS_VER, OS_MAJOR, _ = (int(x) for x in get_macos_version())
def _get_uti_from_mdls(extension):

View File

@ -25,14 +25,16 @@ import shortuuid
from ._constants import UNICODE_FORMAT
logger = logging.getLogger("osxphotos")
__all__ = [
"dd_to_dms_str",
"expand_and_validate_filepath",
"get_last_library_path",
"get_system_library_path",
"hexdigest",
"increment_filename_with_count",
"increment_filename",
"increment_filename_with_count",
"lineno",
"list_directory",
"list_photo_libraries",
@ -46,23 +48,9 @@ __all__ = [
]
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(levelname)s - %(filename)s - %(lineno)d - %(message)s",
)
VERSION_INFO_URL = "https://pypi.org/pypi/osxphotos/json"
def _get_logger():
"""Used only for testing
Returns:
logging.Logger object -- logging.Logger object for osxphotos
"""
return logging.Logger(__name__)
def noop(*args, **kwargs):
"""do nothing (no operation)"""
pass
@ -76,7 +64,7 @@ def lineno(filename):
return f"{filename}: {line}"
def _get_os_version():
def get_macos_version():
# returns tuple of str containing OS version
# e.g. 10.13.6 = ("10", "13", "6")
version = platform.mac_ver()[0].split(".")
@ -176,9 +164,9 @@ def get_system_library_path():
"""return the path to the system Photos library as string"""
""" only works on MacOS 10.15 """
""" on earlier versions, returns None """
_, major, _ = _get_os_version()
_, major, _ = get_macos_version()
if int(major) < 15:
logging.debug(
logger.debug(
f"get_system_library_path not implemented for MacOS < 10.15: you have {major}"
)
return None
@ -191,7 +179,7 @@ def get_system_library_path():
with open(plist_file, "rb") as fp:
pl = plistload(fp)
else:
logging.debug(f"could not find plist file: {str(plist_file)}")
logger.debug(f"could not find plist file: {str(plist_file)}")
return None
return pl.get("SystemLibraryPath")
@ -208,7 +196,7 @@ def get_last_library_path():
with open(plist_file, "rb") as fp:
pl = plistload(fp)
else:
logging.debug(f"could not find plist file: {str(plist_file)}")
logger.debug(f"could not find plist file: {str(plist_file)}")
return None
# get the IPXDefaultLibraryURLBookmark from com.apple.Photos.plist
@ -244,7 +232,7 @@ def get_last_library_path():
return photospath
else:
logging.debug("Could not get path to Photos database")
logger.debug("Could not get path to Photos database")
return None

View File

@ -7,7 +7,7 @@
<key>hostuuid</key>
<string>585B80BF-8D1F-55EF-A9E8-6CF4E5523959</string>
<key>pid</key>
<integer>1961</integer>
<integer>508</integer>
<key>processname</key>
<string>photolibraryd</string>
<key>uid</key>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View File

@ -2,12 +2,18 @@
import datetime
import pathlib
import time
from tests.parse_timewarp_output import CompareValues, InspectValues
TEST_LIBRARY_TIMEWARP = "tests/TestTimeWarp-10.15.7.photoslibrary"
def is_dst() -> bool:
"""Return True if daylight savings time is in effect"""
return bool(time.localtime().tm_isdst)
def get_file_timestamp(file: str) -> str:
"""Get timestamp of file"""
return datetime.datetime.fromtimestamp(pathlib.Path(file).stat().st_mtime).strftime(
@ -24,6 +30,7 @@ CATALINA_PHOTOS_5 = {
"marigold flowers": "IMG_6517.jpeg",
"multi-colored zinnia flowers": "IMG_6506.jpeg",
"sunset": "IMG_6551.mov",
"palm tree": "20230120_010203-0400.jpg",
},
"inspect": {
# IMG_6501.jpeg
@ -362,4 +369,28 @@ CATALINA_PHOTOS_5 = {
"GMT-0700",
),
},
"parse_date": {
# 20230120_010203-0400.jpg
"uuid": "5285C4E2-BB1A-49DF-AEF5-246AA337ACAB",
"expected": InspectValues(
"20230120_010203-0400.jpg",
"5285C4E2-BB1A-49DF-AEF5-246AA337ACAB",
"2023-01-20 01:02:03-0800" if not is_dst() else "2023-01-20 00:02:03-0700",
"2023-01-20 01:02:03-0800" if not is_dst() else "2023-01-20 00:02:03-0700",
"-0800",
"GMT-0800",
),
},
"parse_date_tz": {
# 20230120_010203-0400.jpg
"uuid": "5285C4E2-BB1A-49DF-AEF5-246AA337ACAB",
"expected": InspectValues(
"20230120_010203-0400.jpg",
"5285C4E2-BB1A-49DF-AEF5-246AA337ACAB",
"2023-01-19 21:02:03-0800" if not is_dst() else "2023-01-19 20:02:03-0700",
"2023-01-20 01:02:03-0400",
"-0400",
"GMT-0400",
),
},
}

View File

@ -66,19 +66,19 @@ elif OS_VER[0] == "13":
TEST_LIBRARY_SYNC = TEST_LIBRARY
from tests.config_timewarp_ventura import TEST_LIBRARY_TIMEWARP
TEST_LIBRARY_ADD_LOCATIONS = None
TEST_LIBRARY_ADD_LOCATIONS = "tests/Test-13.0.0.photoslibrary"
else:
TEST_LIBRARY = None
TEST_LIBRARY_TIMEWARP = None
TEST_LIBRARY_SYNC = None
TEST_LIBRARY_ADD_LOCATIONS = "tests/Test-13.0.0.photoslibrary"
TEST_LIBRARY_ADD_LOCATIONS = None
@pytest.fixture(scope="session", autouse=True)
def setup_photos_timewarp():
if not TEST_TIMEWARP:
return
copy_photos_library(TEST_LIBRARY_TIMEWARP, delay=10)
copy_photos_library(TEST_LIBRARY_TIMEWARP, delay=5)
@pytest.fixture(scope="session", autouse=True)

View File

@ -24,10 +24,8 @@ CompareValues = namedtuple(
def parse_inspect_output(output: str) -> List[InspectValues]:
"""Parse output of --inspect and return list of InspectValues named tuple"""
with open(output, "r") as f:
lines = f.readlines()
lines = [line for line in lines if line.strip()]
lines = [line for line in output.split("\n") if line.strip()]
# remove header
lines.pop(0)
values = []
@ -40,9 +38,7 @@ def parse_inspect_output(output: str) -> List[InspectValues]:
def parse_compare_exif(output: str) -> List[CompareValues]:
"""Parse output of --compare-exif and return list of CompareValues named tuple"""
with open(output, "r") as f:
lines = f.readlines()
lines = [line for line in lines if line.strip()]
lines = [line for line in output.split("\n") if line.strip()]
# remove header
lines.pop(0)
values = []

File diff suppressed because one or more lines are too long

View File

@ -14,9 +14,9 @@ import pytest
import osxphotos
from osxphotos._constants import _UNKNOWN_PERSON
from osxphotos.photoexporter import PhotoExporter
from osxphotos.utils import _get_os_version
from osxphotos.utils import get_macos_version
OS_VERSION = _get_os_version()
OS_VERSION = get_macos_version()
SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "15"
PHOTOS_DB_LOCAL = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")

View File

@ -73,18 +73,18 @@ def test_select_pears(photoslib, suspend_capture):
@pytest.mark.timewarp
def test_inspect(photoslib, suspend_capture, output_file):
def test_inspect(photoslib, suspend_capture):
"""Test --inspect. NOTE: this test requires user interaction"""
from osxphotos.cli.timewarp import timewarp
runner = CliRunner()
result = runner.invoke(
timewarp,
["--inspect", "--plain", "--force", "-o", output_file],
["--inspect", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
assert result.exit_code == 0
values = parse_inspect_output(output_file)
values = parse_inspect_output(result.output)
assert TEST_DATA["inspect"]["expected"] == values
@ -111,7 +111,7 @@ def test_date(photoslib, suspend_capture):
@pytest.mark.timewarp
@pytest.mark.parametrize("input_value,expected", TEST_DATA["date_delta"]["parameters"])
def test_date_delta(photoslib, suspend_capture, input_value, expected, output_file):
def test_date_delta(photoslib, suspend_capture, input_value, expected):
"""Test --date-delta"""
from osxphotos.cli.timewarp import timewarp
@ -129,16 +129,16 @@ def test_date_delta(photoslib, suspend_capture, input_value, expected, output_fi
assert result.exit_code == 0
result = runner.invoke(
timewarp,
["--inspect", "--plain", "--force", "-o", output_file],
["--inspect", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_inspect_output(output_file)
output_values = parse_inspect_output(result.output)
assert output_values[0].date_tz == expected
@pytest.mark.timewarp
@pytest.mark.parametrize("input_value,expected", TEST_DATA["time"]["parameters"])
def test_time(photoslib, suspend_capture, input_value, expected, output_file):
def test_time(photoslib, suspend_capture, input_value, expected):
"""Test --time"""
from osxphotos.cli.timewarp import timewarp
@ -158,16 +158,16 @@ def test_time(photoslib, suspend_capture, input_value, expected, output_file):
# don't use photo.date as it will return local time instead of the time in the timezone
result = runner.invoke(
timewarp,
["--inspect", "--plain", "--force", "-o", output_file],
["--inspect", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_inspect_output(output_file)
output_values = parse_inspect_output(result.output)
assert output_values[0].date_tz == expected
@pytest.mark.timewarp
@pytest.mark.parametrize("input_value,expected", TEST_DATA["time_delta"]["parameters"])
def test_time_delta(photoslib, suspend_capture, input_value, expected, output_file):
def test_time_delta(photoslib, suspend_capture, input_value, expected):
"""Test --time-delta"""
from osxphotos.cli.timewarp import timewarp
@ -185,10 +185,10 @@ def test_time_delta(photoslib, suspend_capture, input_value, expected, output_fi
assert result.exit_code == 0
result = runner.invoke(
timewarp,
["--inspect", "--plain", "--force", "-o", output_file],
["--inspect", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_inspect_output(output_file)
output_values = parse_inspect_output(result.output)
assert output_values[0].date_tz == expected
@ -216,34 +216,28 @@ def test_time_zone(
assert result.exit_code == 0
result = runner.invoke(
timewarp,
["--inspect", "--plain", "--force", "-o", output_file],
["--inspect", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_inspect_output(output_file)
output_values = parse_inspect_output(result.output)
assert output_values[0].date_tz == expected_date
assert output_values[0].tz_offset == expected_tz
@pytest.mark.timewarp
@pytest.mark.parametrize("expected", TEST_DATA["compare_exif"]["expected"])
def test_compare_exif(photoslib, suspend_capture, expected, output_file):
def test_compare_exif(photoslib, suspend_capture, expected):
"""Test --compare-exif"""
from osxphotos.cli.timewarp import timewarp
runner = CliRunner()
result = runner.invoke(
timewarp,
[
"--compare-exif",
"--plain",
"--force",
"-o",
output_file,
],
["--compare-exif", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
assert result.exit_code == 0
output_values = parse_compare_exif(output_file)
output_values = parse_compare_exif(result.output)
assert output_values[0] == expected
@ -281,24 +275,24 @@ def test_select_sunflowers(photoslib, suspend_capture):
@pytest.mark.timewarp
@pytest.mark.parametrize("expected", TEST_DATA["compare_exif_3"]["expected"])
def test_compare_exif_3(photoslib, suspend_capture, expected, output_file):
def test_compare_exif_3(photoslib, suspend_capture, expected):
"""Test --compare-exif"""
from osxphotos.cli.timewarp import timewarp
runner = CliRunner()
result = runner.invoke(
timewarp,
["--compare-exif", "--plain", "--force", "-o", output_file],
["--compare-exif", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
assert result.exit_code == 0
output_values = parse_compare_exif(output_file)
output_values = parse_compare_exif(result.output)
assert output_values[0] == expected
@pytest.mark.timewarp
@pytest.mark.parametrize("input_value,expected", TEST_DATA["match"]["parameters"])
def test_match(photoslib, suspend_capture, input_value, expected, output_file):
def test_match(photoslib, suspend_capture, input_value, expected):
"""Test --timezone --match"""
from osxphotos.cli.timewarp import timewarp
@ -317,10 +311,10 @@ def test_match(photoslib, suspend_capture, input_value, expected, output_file):
assert result.exit_code == 0
result = runner.invoke(
timewarp,
["--inspect", "--plain", "--force", "-o", output_file],
["--inspect", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_inspect_output(output_file)
output_values = parse_inspect_output(result.output)
assert output_values[0].date_tz == expected
@ -380,10 +374,10 @@ def test_push_exif_1(
assert result.exit_code == 0
result = runner.invoke(
timewarp,
["--inspect", "--plain", "--force", "-o", output_file],
["--inspect", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_inspect_output(output_file)
output_values = parse_inspect_output(result.output)
assert output_values[0].date_tz == expected_date
photo = photoslib.selection[0]
@ -402,7 +396,7 @@ def test_select_pears_2(photoslib, suspend_capture):
@pytest.mark.timewarp
def test_push_exif_2(photoslib, suspend_capture, output_file):
def test_push_exif_2(photoslib, suspend_capture):
"""Test --push-exif"""
pre_test = TEST_DATA["push_exif"]["pre"]
post_test = TEST_DATA["push_exif"]["post"]
@ -413,10 +407,10 @@ def test_push_exif_2(photoslib, suspend_capture, output_file):
result = runner.invoke(
timewarp,
["--compare-exif", "--plain", "--force", "-o", output_file],
["--compare-exif", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_compare_exif(output_file)
output_values = parse_compare_exif(result.output)
assert output_values[0] == pre_test
result = runner.invoke(
@ -433,15 +427,15 @@ def test_push_exif_2(photoslib, suspend_capture, output_file):
result = runner.invoke(
timewarp,
["--compare-exif", "--plain", "--force", "-o", output_file],
["--compare-exif", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_compare_exif(output_file)
output_values = parse_compare_exif(result.output)
assert output_values[0] == post_test
@pytest.mark.timewarp
def test_pull_exif_1(photoslib, suspend_capture, output_file):
def test_pull_exif_1(photoslib, suspend_capture):
"""Test --pull-exif"""
pre_test = TEST_DATA["pull_exif_1"]["pre"]
post_test = TEST_DATA["pull_exif_1"]["post"]
@ -460,10 +454,10 @@ def test_pull_exif_1(photoslib, suspend_capture, output_file):
result = runner.invoke(
timewarp,
["--compare-exif", "--plain", "--force", "-o", output_file],
["--compare-exif", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_compare_exif(output_file)
output_values = parse_compare_exif(result.output)
assert output_values[0] == pre_test
result = runner.invoke(
@ -480,10 +474,10 @@ def test_pull_exif_1(photoslib, suspend_capture, output_file):
result = runner.invoke(
timewarp,
["--compare-exif", "--plain", "--force", "-o", output_file],
["--compare-exif", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_compare_exif(output_file)
output_values = parse_compare_exif(result.output)
assert output_values[0] == post_test
@ -494,7 +488,7 @@ def test_select_apple_tree(photoslib, suspend_capture):
@pytest.mark.timewarp
def test_pull_exif_no_time(photoslib, suspend_capture, output_file):
def test_pull_exif_no_time(photoslib, suspend_capture):
"""Test --pull-exif when photo has invalid date/time in EXIF"""
pre_test = TEST_DATA["pull_exif_no_time"]["pre"]
post_test = TEST_DATA["pull_exif_no_time"]["post"]
@ -505,10 +499,10 @@ def test_pull_exif_no_time(photoslib, suspend_capture, output_file):
result = runner.invoke(
timewarp,
["--compare-exif", "--plain", "--force", "-o", output_file],
["--compare-exif", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_compare_exif(output_file)
output_values = parse_compare_exif(result.output)
assert output_values[0] == pre_test
result = runner.invoke(
@ -525,10 +519,10 @@ def test_pull_exif_no_time(photoslib, suspend_capture, output_file):
result = runner.invoke(
timewarp,
["--compare-exif", "--plain", "--force", "-o", output_file],
["--compare-exif", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_compare_exif(output_file)
output_values = parse_compare_exif(result.output)
assert output_values[0] == post_test
@ -539,7 +533,7 @@ def test_select_marigolds(photoslib, suspend_capture):
@pytest.mark.timewarp
def test_pull_exif_no_offset(photoslib, suspend_capture, output_file):
def test_pull_exif_no_offset(photoslib, suspend_capture):
"""Test --pull-exif when photo has no offset in EXIF"""
pre_test = TEST_DATA["pull_exif_no_offset"]["pre"]
post_test = TEST_DATA["pull_exif_no_offset"]["post"]
@ -550,10 +544,10 @@ def test_pull_exif_no_offset(photoslib, suspend_capture, output_file):
result = runner.invoke(
timewarp,
["--compare-exif", "--plain", "--force", "-o", output_file],
["--compare-exif", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_compare_exif(output_file)
output_values = parse_compare_exif(result.output)
assert output_values[0] == pre_test
result = runner.invoke(
@ -570,10 +564,10 @@ def test_pull_exif_no_offset(photoslib, suspend_capture, output_file):
result = runner.invoke(
timewarp,
["--compare-exif", "--plain", "--force", "-o", output_file],
["--compare-exif", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_compare_exif(output_file)
output_values = parse_compare_exif(result.output)
assert output_values[0] == post_test
@ -586,7 +580,7 @@ def test_select_zinnias(photoslib, suspend_capture):
@pytest.mark.timewarp
def test_pull_exif_no_data(photoslib, suspend_capture, output_file):
def test_pull_exif_no_data(photoslib, suspend_capture):
"""Test --pull-exif when photo has no data in EXIF"""
pre_test = TEST_DATA["pull_exif_no_data"]["pre"]
post_test = TEST_DATA["pull_exif_no_data"]["post"]
@ -597,10 +591,10 @@ def test_pull_exif_no_data(photoslib, suspend_capture, output_file):
result = runner.invoke(
timewarp,
["--compare-exif", "--plain", "--force", "-o", output_file],
["--compare-exif", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_compare_exif(output_file)
output_values = parse_compare_exif(result.output)
assert output_values[0] == pre_test
result = runner.invoke(
@ -618,15 +612,15 @@ def test_pull_exif_no_data(photoslib, suspend_capture, output_file):
result = runner.invoke(
timewarp,
["--compare-exif", "--plain", "--force", "-o", output_file],
["--compare-exif", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_compare_exif(output_file)
output_values = parse_compare_exif(result.output)
assert output_values[0] == post_test
@pytest.mark.timewarp
def test_pull_exif_no_data_use_file_time(photoslib, suspend_capture, output_file):
def test_pull_exif_no_data_use_file_time(photoslib, suspend_capture):
"""Test --pull-exif when photo has no data in EXIF with --use-file-time"""
pre_test = TEST_DATA["pull_exif_no_data_use_file_time"]["pre"]
post_test = TEST_DATA["pull_exif_no_data_use_file_time"]["post"]
@ -637,10 +631,10 @@ def test_pull_exif_no_data_use_file_time(photoslib, suspend_capture, output_file
result = runner.invoke(
timewarp,
["--compare-exif", "--plain", "--force", "-o", output_file],
["--compare-exif", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_compare_exif(output_file)
output_values = parse_compare_exif(result.output)
assert output_values[0] == pre_test
result = runner.invoke(
@ -659,10 +653,10 @@ def test_pull_exif_no_data_use_file_time(photoslib, suspend_capture, output_file
result = runner.invoke(
timewarp,
["--compare-exif", "--plain", "--force", "-o", output_file],
["--compare-exif", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_compare_exif(output_file)
output_values = parse_compare_exif(result.output)
assert output_values[0] == post_test
@ -674,7 +668,7 @@ def test_select_sunset_video(photoslib, suspend_capture):
@pytest.mark.timewarp
@pytest.mark.parametrize("expected", TEST_DATA["compare_video_1"]["expected"])
def test_video_compare_exif(photoslib, suspend_capture, expected, output_file):
def test_video_compare_exif(photoslib, suspend_capture, expected):
"""Test --compare-exif with video"""
from osxphotos.cli.timewarp import timewarp
@ -685,13 +679,11 @@ def test_video_compare_exif(photoslib, suspend_capture, expected, output_file):
"--compare-exif",
"--plain",
"--force",
"-o",
output_file,
],
terminal_width=TERMINAL_WIDTH,
)
assert result.exit_code == 0
output_values = parse_compare_exif(output_file)
output_values = parse_compare_exif(result.output)
assert output_values[0] == expected
@ -708,22 +700,17 @@ def test_video_date_delta(
runner = CliRunner()
result = runner.invoke(
timewarp,
[
"--date-delta",
input_value,
"--plain",
"--force",
],
["--date-delta", input_value, "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
assert result.exit_code == 0
result = runner.invoke(
timewarp,
["--inspect", "--plain", "--force", "-o", output_file],
["--inspect", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_inspect_output(output_file)
output_values = parse_inspect_output(result.output)
assert output_values[0].date_tz == expected
@ -751,16 +738,16 @@ def test_video_time_delta(
assert result.exit_code == 0
result = runner.invoke(
timewarp,
["--inspect", "--plain", "--force", "-o", output_file],
["--inspect", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_inspect_output(output_file)
output_values = parse_inspect_output(result.output)
assert output_values[0].date_tz == expected
@pytest.mark.timewarp
@pytest.mark.parametrize("input_value,expected", TEST_DATA["video_date"]["parameters"])
def test_video_date(photoslib, suspend_capture, input_value, expected, output_file):
def test_video_date(photoslib, suspend_capture, input_value, expected):
"""Test --date with video"""
from osxphotos.cli.timewarp import timewarp
@ -780,16 +767,16 @@ def test_video_date(photoslib, suspend_capture, input_value, expected, output_fi
# don't use photo.date as it will return local time instead of the time in the timezone
result = runner.invoke(
timewarp,
["--inspect", "--plain", "--force", "-o", output_file],
["--inspect", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_inspect_output(output_file)
output_values = parse_inspect_output(result.output)
assert output_values[0].date_tz == expected
@pytest.mark.timewarp
@pytest.mark.parametrize("input_value,expected", TEST_DATA["video_time"]["parameters"])
def test_video_time(photoslib, suspend_capture, input_value, expected, output_file):
def test_video_time(photoslib, suspend_capture, input_value, expected):
"""Test --time with video"""
from osxphotos.cli.timewarp import timewarp
@ -809,10 +796,10 @@ def test_video_time(photoslib, suspend_capture, input_value, expected, output_fi
# don't use photo.date as it will return local time instead of the time in the timezone
result = runner.invoke(
timewarp,
["--inspect", "--plain", "--force", "-o", output_file],
["--inspect", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_inspect_output(output_file)
output_values = parse_inspect_output(result.output)
assert output_values[0].date_tz == expected
@ -840,17 +827,17 @@ def test_video_time_zone(
assert result.exit_code == 0
result = runner.invoke(
timewarp,
["--inspect", "--plain", "--force", "-o", output_file],
["--inspect", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_inspect_output(output_file)
output_values = parse_inspect_output(result.output)
assert output_values[0].date_tz == expected_date
assert output_values[0].tz_offset == expected_tz
@pytest.mark.timewarp
@pytest.mark.parametrize("input_value,expected", TEST_DATA["video_match"]["parameters"])
def test_video_match(photoslib, suspend_capture, input_value, expected, output_file):
def test_video_match(photoslib, suspend_capture, input_value, expected):
"""Test --timezone --match with video"""
from osxphotos.cli.timewarp import timewarp
@ -869,15 +856,15 @@ def test_video_match(photoslib, suspend_capture, input_value, expected, output_f
assert result.exit_code == 0
result = runner.invoke(
timewarp,
["--inspect", "--plain", "--force", "-o", output_file],
["--inspect", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_inspect_output(output_file)
output_values = parse_inspect_output(result.output)
assert output_values[0].date_tz == expected
@pytest.mark.timewarp
def test_video_push_exif(photoslib, suspend_capture, output_file):
def test_video_push_exif(photoslib, suspend_capture):
"""Test --push-exif with video"""
pre_test = TEST_DATA["video_push_exif"]["pre"]
post_test = TEST_DATA["video_push_exif"]["post"]
@ -888,10 +875,10 @@ def test_video_push_exif(photoslib, suspend_capture, output_file):
result = runner.invoke(
timewarp,
["--compare-exif", "--plain", "--force", "-o", output_file],
["--compare-exif", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_compare_exif(output_file)
output_values = parse_compare_exif(result.output)
assert output_values[0] == pre_test
result = runner.invoke(
@ -908,15 +895,15 @@ def test_video_push_exif(photoslib, suspend_capture, output_file):
result = runner.invoke(
timewarp,
["--compare-exif", "--plain", "--force", "-o", output_file],
["--compare-exif", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_compare_exif(output_file)
output_values = parse_compare_exif(result.output)
assert output_values[0] == post_test
@pytest.mark.timewarp
def test_video_pull_exif(photoslib, suspend_capture, output_file):
def test_video_pull_exif(photoslib, suspend_capture):
"""Test --pull-exif with video"""
pre_test = TEST_DATA["video_pull_exif"]["pre"]
post_test = TEST_DATA["video_pull_exif"]["post"]
@ -946,10 +933,10 @@ def test_video_pull_exif(photoslib, suspend_capture, output_file):
result = runner.invoke(
timewarp,
["--compare-exif", "--plain", "--force", "-o", output_file],
["--compare-exif", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_compare_exif(output_file)
output_values = parse_compare_exif(result.output)
assert output_values[0] == pre_test
result = runner.invoke(
@ -966,10 +953,10 @@ def test_video_pull_exif(photoslib, suspend_capture, output_file):
result = runner.invoke(
timewarp,
["--compare-exif", "--plain", "--force", "-o", output_file],
["--compare-exif", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_compare_exif(output_file)
output_values = parse_compare_exif(result.output)
assert output_values[0] == post_test
@ -980,7 +967,7 @@ def test_select_pears_3(photoslib, suspend_capture):
@pytest.mark.timewarp
def test_function(photoslib, suspend_capture, output_file):
def test_function(photoslib, suspend_capture):
"""Test timewarp function"""
from osxphotos.cli.timewarp import timewarp
@ -999,10 +986,10 @@ def test_function(photoslib, suspend_capture, output_file):
assert result.exit_code == 0
result = runner.invoke(
timewarp,
["--inspect", "--plain", "--force", "-o", output_file],
["--inspect", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_inspect_output(output_file)
output_values = parse_inspect_output(result.output)
assert output_values[0] == expected
@ -1013,8 +1000,7 @@ def test_select_palm_tree_1(photoslib, suspend_capture):
@pytest.mark.timewarp
@pytest.mark.skipif(get_os_version()[0] != "13", reason="test requires macOS 13")
def test_parse_date(photoslib, suspend_capture, output_file):
def test_parse_date(photoslib, suspend_capture):
"""Test --parse-date"""
from osxphotos.cli.timewarp import timewarp
@ -1033,18 +1019,17 @@ def test_parse_date(photoslib, suspend_capture, output_file):
assert result.exit_code == 0
result = runner.invoke(
timewarp,
["--inspect", "--plain", "--force", "-o", output_file],
["--inspect", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_inspect_output(output_file)
output_values = parse_inspect_output(result.output)
assert output_values[0].date_local == expected.date_local
assert output_values[0].date_tz == expected.date_tz
assert output_values[0].tz_offset == expected.tz_offset
@pytest.mark.timewarp
@pytest.mark.skipif(get_os_version()[0] != "13", reason="test requires macOS 13")
def test_parse_date_tz(photoslib, suspend_capture, output_file):
def test_parse_date_tz(photoslib, suspend_capture):
"""Test --parse-date with a timezone"""
from osxphotos.cli.timewarp import timewarp
@ -1063,10 +1048,10 @@ def test_parse_date_tz(photoslib, suspend_capture, output_file):
assert result.exit_code == 0
result = runner.invoke(
timewarp,
["--inspect", "--plain", "--force", "-o", output_file],
["--inspect", "--plain", "--force"],
terminal_width=TERMINAL_WIDTH,
)
output_values = parse_inspect_output(output_file)
output_values = parse_inspect_output(result.output)
assert output_values[0].date_local == expected.date_local
assert output_values[0].date_tz == expected.date_tz
assert output_values[0].tz_offset == expected.tz_offset

View File

@ -2,6 +2,8 @@
import pytest
from osxphotos.queryoptions import load_uuid_from_file
UUID_FILE = "tests/uuid_from_file.txt"
MISSING_UUID_FILE = "tests/uuid_not_found.txt"
@ -13,8 +15,7 @@ UUID_EXPECTED_FROM_FILE = [
def test_load_uuid_from_file():
"""Test load_uuid_from_file function """
from osxphotos.cli import load_uuid_from_file
"""Test load_uuid_from_file function"""
uuid_got = load_uuid_from_file(UUID_FILE)
assert uuid_got == UUID_EXPECTED_FROM_FILE
@ -22,7 +23,6 @@ def test_load_uuid_from_file():
def test_load_uuid_from_file_filenotfound():
"""Test load_uuid_from_file function raises error if file not found"""
from osxphotos.cli import load_uuid_from_file
with pytest.raises(FileNotFoundError) as err:
uuid_got = load_uuid_from_file(MISSING_UUID_FILE)

116
tests/test_cli_verbose.py Normal file
View File

@ -0,0 +1,116 @@
"""Test verbose functions"""
import re
from io import StringIO
from osxphotos.cli.verbose import (
get_verbose_level,
set_verbose_level,
verbose,
verbose_print,
_reset_verbose_globals,
)
def test_set_get_verbose_level(capsys):
"""Test verbose_print"""
set_verbose_level(2)
assert get_verbose_level() == 2
def test_verbose_print_no_rich(capsys):
"""Test verbose_print"""
set_verbose_level(1)
verbose = verbose_print(1, False, False)
verbose("test")
captured = capsys.readouterr()
assert captured.out.strip() == "test"
verbose("test2", level=1)
captured = capsys.readouterr()
assert captured.out.strip() == "test2"
verbose("test3", level=2)
captured = capsys.readouterr()
assert captured.out.strip() == ""
def test_verbose_print_rich(capsys):
"""Test verbose with rich"""
set_verbose_level(1)
verbose = verbose_print(1, False, True)
verbose("test")
captured = capsys.readouterr()
assert captured.out.strip() == "test"
verbose("test2", level=1)
captured = capsys.readouterr()
assert captured.out.strip() == "test2"
verbose("test3", level=2)
captured = capsys.readouterr()
assert captured.out.strip() == ""
def test_verbose_print_timestamp(capsys):
"""Test verbose with timestamp"""
set_verbose_level(1)
verbose = verbose_print(1, True, False)
verbose("test")
captured = capsys.readouterr()
# regex to match timestamp in this format: 2023-01-25 06:40:18.216297
assert re.match(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}", captured.out.strip())
assert captured.out.strip().endswith("test")
verbose("test2", level=1)
captured = capsys.readouterr()
assert re.match(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}", captured.out.strip())
assert captured.out.strip().endswith("test2")
verbose("test3", level=2)
captured = capsys.readouterr()
assert captured.out.strip() == ""
def test_verbose_print_file():
"""Test verbose with file"""
set_verbose_level(1)
stream = StringIO()
verbose = verbose_print(1, False, False, file=stream)
verbose("test")
assert stream.getvalue().strip() == "test"
def test_verbose_print_noop(capsys):
"""Test verbose with noop"""
set_verbose_level(1)
verbose = verbose_print(0, False, False)
verbose("test")
captured = capsys.readouterr()
assert captured.out.strip() == ""
def test_verbose(capsys):
""" "Test verbose()"""
# reset verbose module globals for testing
_reset_verbose_globals()
set_verbose_level(0)
verbose("test")
captured = capsys.readouterr()
assert captured.out.strip() == ""
set_verbose_level(1)
verbose("test")
captured = capsys.readouterr()
assert captured.out.strip() == "test"
verbose("test2", level=2)
captured = capsys.readouterr()
assert captured.out.strip() == ""
set_verbose_level(2)
verbose("test2", level=2)
captured = capsys.readouterr()
assert captured.out.strip() == "test2"

View File

@ -7,14 +7,29 @@ from osxphotos.debug import is_debug, set_debug
def test_debug_enable():
"""test set_debug()"""
set_debug(True)
logger = osxphotos._get_logger()
assert logger.isEnabledFor(logging.DEBUG)
assert osxphotos.logger.isEnabledFor(logging.DEBUG)
assert is_debug()
def test_debug_disable():
"""test set_debug()"""
set_debug(False)
logger = osxphotos._get_logger()
assert not logger.isEnabledFor(logging.DEBUG)
assert not osxphotos.logger.isEnabledFor(logging.DEBUG)
assert not is_debug()
def test_debug_print_true(caplog):
"""test debug()"""
set_debug(True)
logger = osxphotos.logger
logger.debug("test debug")
assert "test debug" in caplog.text
def test_debug_print_false(caplog):
set_debug(False)
logger = osxphotos.logger
logger.debug("test debug")
assert caplog.text == ""

View File

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

View File

@ -14,9 +14,9 @@ import pytest
import osxphotos
from osxphotos._constants import _UNKNOWN_PERSON
from osxphotos.photoexporter import PhotoExporter
from osxphotos.utils import _get_os_version
from osxphotos.utils import get_macos_version
OS_VERSION = _get_os_version()
OS_VERSION = get_macos_version()
# 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
PHOTOS_DB_LOCAL = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")