From 5eaeb72c3ee296af6abc6ca6ddf8ad05baf02052 Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Sat, 27 Aug 2022 10:50:25 -0700 Subject: [PATCH] Added --print to dump, added {tab} --- osxphotos/cli/dump.py | 54 +++++++++++++++++++++++++++-- osxphotos/phototemplate.py | 4 ++- tests/test_cli_dump.py | 71 ++++++++++++++++++++++++++++++++++++++ tests/test_template.py | 16 ++++++++- 4 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 tests/test_cli_dump.py diff --git a/osxphotos/cli/dump.py b/osxphotos/cli/dump.py index fea3fa1c..568a79ca 100644 --- a/osxphotos/cli/dump.py +++ b/osxphotos/cli/dump.py @@ -3,11 +3,19 @@ import click import osxphotos +from osxphotos.cli.click_rich_echo import ( + rich_click_echo, + set_rich_console, + set_rich_theme, +) +from osxphotos.phototemplate import RenderOptions from osxphotos.queryoptions import QueryOptions +from .color_themes import get_default_theme from .common import DB_ARGUMENT, DB_OPTION, DELETED_OPTIONS, JSON_OPTION, get_photos_db from .list import _list_libraries from .print_photo_info import print_photo_info +from .verbose import get_verbose_console @click.command() @@ -15,12 +23,30 @@ from .print_photo_info import print_photo_info @JSON_OPTION @DELETED_OPTIONS @DB_ARGUMENT +@click.option( + "--print", + "print_template", + metavar="TEMPLATE", + multiple=True, + help="Render TEMPLATE string for each photo queried and print to stdout. " + "TEMPLATE is an osxphotos template string. " + "This may be useful for creating custom reports, etc. " + "If --print TEMPLATE is provided, regular output is suppressed " + "and only the rendered TEMPLATE values are printed. " + "May be repeated to print multiple template strings. ", +) @click.pass_obj @click.pass_context -def dump(ctx, cli_obj, db, json_, deleted, deleted_only, photos_library): +def dump( + ctx, cli_obj, db, json_, deleted, deleted_only, photos_library, print_template +): """Print list of all photos & associated info from the Photos library.""" - db = get_photos_db(*photos_library, db, cli_obj.db) + # below needed for to make CliRunner work for testing + cli_db = cli_obj.db if cli_obj is not None else None + cli_json = cli_obj.json if cli_obj is not None else None + + db = get_photos_db(*photos_library, db, cli_db) if db is None: click.echo(ctx.obj.group.commands["dump"].get_help(ctx), err=True) click.echo("\n\nLocated the following Photos library databases: ", err=True) @@ -33,6 +59,10 @@ def dump(ctx, cli_obj, db, json_, deleted, deleted_only, photos_library): click.echo(ctx.obj.group.commands["dump"].get_help(ctx), err=True) return + # set console for rich_echo to be same as for verbose_ + set_rich_console(get_verbose_console()) + set_rich_theme(get_default_theme()) + photosdb = osxphotos.PhotosDB(dbfile=db) if deleted or deleted_only: photos = photosdb.photos(movies=True, intrash=True) @@ -41,4 +71,22 @@ def dump(ctx, cli_obj, db, json_, deleted, deleted_only, photos_library): if not deleted_only: photos += photosdb.photos(movies=True) - print_photo_info(photos, json_ or cli_obj.json) + if not print_template: + # just dump and be done + print_photo_info(photos, cli_json or json_) + return + + # have print template(s) + options = RenderOptions() + for p in photos: + for template in print_template: + rendered_templates, unmatched = p.render_template( + template, + options, + ) + if unmatched: + rich_click_echo(f"[warning]Unmatched template field: {unmatched}[/]") + for rendered_template in rendered_templates: + if not rendered_template: + continue + rich_click_echo(rendered_template) diff --git a/osxphotos/phototemplate.py b/osxphotos/phototemplate.py index c791a226..5d157d77 100644 --- a/osxphotos/phototemplate.py +++ b/osxphotos/phototemplate.py @@ -181,7 +181,8 @@ TEMPLATE_SUBSTITUTIONS = { "{newline}": r"A newline: '\n'", "{lf}": r"A line feed: '\n', alias for {newline}", "{cr}": r"A carriage return: '\r'", - "{crlf}": r"a carriage return + line feed: '\r\n'", + "{crlf}": r"A carriage return + line feed: '\r\n'", + "{tab}": r":A tab: '\t'", "{osxphotos_version}": f"The osxphotos version, e.g. '{__version__}'", "{osxphotos_cmd_line}": "The full command line used to run osxphotos", } @@ -312,6 +313,7 @@ PUNCTUATION = { "lf": "\n", "cr": "\r", "crlf": "\r\n", + "tab": "\t", } diff --git a/tests/test_cli_dump.py b/tests/test_cli_dump.py new file mode 100644 index 00000000..96ac28e8 --- /dev/null +++ b/tests/test_cli_dump.py @@ -0,0 +1,71 @@ +"""Test osxphotos dump command.""" + +import json +import os +import os.path + +import pytest +from click.testing import CliRunner + +from osxphotos.cli import dump +from osxphotos.photosdb import PhotosDB + +from .test_cli import CLI_PHOTOS_DB + + +@pytest.fixture +def photos(): + """Return photos from CLI_PHOTOS_DB""" + cwd = os.getcwd() + db_path = os.path.join(cwd, CLI_PHOTOS_DB) + return PhotosDB(db_path).photos(intrash=True) + + +def test_dump_basic(photos): + """Test osxphotos dump""" + runner = CliRunner() + cwd = os.getcwd() + db_path = os.path.join(cwd, CLI_PHOTOS_DB) + # pylint: disable=not-context-manager + with runner.isolated_filesystem(): + result = runner.invoke(dump, ["--db", db_path, "--deleted"]) + assert result.exit_code == 0 + assert result.output.startswith("uuid,filename") + for photo in photos: + assert photo.uuid in result.output + + +def test_dump_json(photos): + """Test osxphotos dump --json""" + runner = CliRunner() + cwd = os.getcwd() + db_path = os.path.join(cwd, CLI_PHOTOS_DB) + # pylint: disable=not-context-manager + with runner.isolated_filesystem(): + result = runner.invoke(dump, ["--db", db_path, "--deleted", "--json"]) + assert result.exit_code == 0 + json_data = {record["uuid"]: record for record in json.loads(result.output)} + for photo in photos: + assert photo.uuid in json_data + + +def test_dump_print(photos): + """Test osxphotos dump --print""" + runner = CliRunner() + cwd = os.getcwd() + db_path = os.path.join(cwd, CLI_PHOTOS_DB) + # pylint: disable=not-context-manager + with runner.isolated_filesystem(): + result = runner.invoke( + dump, + [ + "--db", + db_path, + "--deleted", + "--print", + "{uuid}_{photo.original_filename}", + ], + ) + assert result.exit_code == 0 + for photo in photos: + assert f"{photo.uuid}_{photo.original_filename}" in result.output diff --git a/tests/test_template.py b/tests/test_template.py index bc491679..3b2db8fd 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -8,12 +8,13 @@ import osxphotos from osxphotos.exiftool import get_exiftool_path from osxphotos.export_db import ExportDBInMemory from osxphotos.phototemplate import ( + PUNCTUATION, TEMPLATE_SUBSTITUTIONS, TEMPLATE_SUBSTITUTIONS_MULTI_VALUED, PhotoTemplate, RenderOptions, ) - +from osxphotos.photoinfo import PhotoInfoNone from .photoinfo_mock import PhotoInfoMock try: @@ -1345,3 +1346,16 @@ def test_bad_sslice(photosdb): # bad function raises ValueError with pytest.raises((SyntaxError, ValueError)): rendered, _ = photo.render_template("{photo.original_filename|sslice(1:2:3:4)}") + + +def test_punctuation(): + """Test punctuation template fields""" + template_string = "" + expected_string = "" + for field, value in PUNCTUATION.items(): + template_string += "{" + field + "}" + expected_string += f"{value}" + template = PhotoTemplate(PhotoInfoNone()) + options = RenderOptions() + rendered, _ = template.render(template_string, options) + assert rendered == [expected_string]