Feature add query command (#970)
* Added query_command and example * Refactored QUERY_OPTIONS, added query_command, refactored verbose, #930, #931 * Added query options to debug-dump, #966 * Refactored query, #602 * Added precedence test for --load-config * Refactored handling of query options * Refactored export_photo * Removed extraneous print * Updated API_README * Updated examples
This commit is contained in:
@@ -3616,7 +3616,7 @@ def test_export_sidecar_invalid():
|
||||
],
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
assert "Cannot use --sidecar json with --sidecar exiftool" in result.output
|
||||
assert "cannot use --sidecar json with --sidecar exiftool" in result.output
|
||||
|
||||
|
||||
def test_export_live():
|
||||
@@ -4242,7 +4242,9 @@ def test_places():
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(places, [os.path.join(cwd, PLACES_PHOTOS_DB), "--json"])
|
||||
result = runner.invoke(
|
||||
places, ["--db", os.path.join(cwd, PLACES_PHOTOS_DB), "--json"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
assert json_got == json.loads(CLI_PLACES_JSON)
|
||||
@@ -4257,7 +4259,13 @@ def test_place_13():
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
query,
|
||||
[os.path.join(cwd, PLACES_PHOTOS_DB_13), "--json", "--place", "Adelaide"],
|
||||
[
|
||||
"--db",
|
||||
os.path.join(cwd, PLACES_PHOTOS_DB_13),
|
||||
"--json",
|
||||
"--place",
|
||||
"Adelaide",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
@@ -4274,7 +4282,8 @@ def test_no_place_13():
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
query, [os.path.join(cwd, PLACES_PHOTOS_DB_13), "--json", "--no-place"]
|
||||
query,
|
||||
["--db", os.path.join(cwd, PLACES_PHOTOS_DB_13), "--json", "--no-place"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
@@ -4292,7 +4301,13 @@ def test_place_15_1():
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
query,
|
||||
[os.path.join(cwd, PLACES_PHOTOS_DB), "--json", "--place", "Washington"],
|
||||
[
|
||||
"--db",
|
||||
os.path.join(cwd, PLACES_PHOTOS_DB),
|
||||
"--json",
|
||||
"--place",
|
||||
"Washington",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
@@ -4310,7 +4325,13 @@ def test_place_15_2():
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
query,
|
||||
[os.path.join(cwd, PLACES_PHOTOS_DB), "--json", "--place", "United States"],
|
||||
[
|
||||
"--db",
|
||||
os.path.join(cwd, PLACES_PHOTOS_DB),
|
||||
"--json",
|
||||
"--place",
|
||||
"United States",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
@@ -4329,7 +4350,7 @@ def test_no_place_15():
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
query, [os.path.join(cwd, PLACES_PHOTOS_DB), "--json", "--no-place"]
|
||||
query, ["--db", os.path.join(cwd, PLACES_PHOTOS_DB), "--json", "--no-place"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
@@ -4346,7 +4367,14 @@ def test_no_folder_1_15():
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
query, [os.path.join(cwd, PHOTOS_DB_15_7), "--json", "--folder", "Folder1"]
|
||||
query,
|
||||
[
|
||||
"--db",
|
||||
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||
"--json",
|
||||
"--folder",
|
||||
"Folder1",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
@@ -4381,6 +4409,7 @@ def test_no_folder_2_15():
|
||||
result = runner.invoke(
|
||||
query,
|
||||
[
|
||||
"--db",
|
||||
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||
"--json",
|
||||
"--folder",
|
||||
@@ -4408,7 +4437,14 @@ def test_no_folder_1_14():
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
query, [os.path.join(cwd, PHOTOS_DB_14_6), "--json", "--folder", "Folder1"]
|
||||
query,
|
||||
[
|
||||
"--db",
|
||||
os.path.join(cwd, PHOTOS_DB_14_6),
|
||||
"--json",
|
||||
"--folder",
|
||||
"Folder1",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
@@ -5727,19 +5763,6 @@ def test_keywords():
|
||||
assert json_got == KEYWORDS_JSON
|
||||
|
||||
|
||||
# TODO: this fails with result.exit_code == 1 but I think this has to
|
||||
# do with how pytest is invoking the command
|
||||
# def test_albums_str():
|
||||
# """Test osxphotos albums string output """
|
||||
|
||||
# runner = CliRunner()
|
||||
# cwd = os.getcwd()
|
||||
# result = runner.invoke(albums, ["--db", os.path.join(cwd, PHOTOS_DB_15_7), ])
|
||||
# assert result.exit_code == 0
|
||||
|
||||
# assert result.output == ALBUMS_STR
|
||||
|
||||
|
||||
def test_albums_json():
|
||||
"""Test osxphotos albums json output"""
|
||||
|
||||
@@ -6648,6 +6671,57 @@ def test_config_only():
|
||||
assert "config.toml" in files
|
||||
|
||||
|
||||
def test_config_command_line_precedence():
|
||||
"""Test that command line options take precedence over config file"""
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
|
||||
with runner.isolated_filesystem():
|
||||
# create a config file
|
||||
with open("config.toml", "w") as fd:
|
||||
fd.write("[export]\n")
|
||||
fd.write(
|
||||
"uuid = ["
|
||||
+ ", ".join(f'"{u}"' for u in UUID_EXPECTED_FROM_FILE)
|
||||
+ "]\n"
|
||||
)
|
||||
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
"--db",
|
||||
os.path.join(cwd, CLI_PHOTOS_DB),
|
||||
".",
|
||||
"-V",
|
||||
"--load-config",
|
||||
"config.toml",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
for uuid in UUID_EXPECTED_FROM_FILE:
|
||||
assert uuid in result.output
|
||||
|
||||
# now run with a command line option that should override the config file
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
"--db",
|
||||
os.path.join(cwd, CLI_PHOTOS_DB),
|
||||
".",
|
||||
"-V",
|
||||
"--uuid",
|
||||
UUID_NOT_FROM_FILE,
|
||||
"--load-config",
|
||||
"config.toml",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert UUID_NOT_FROM_FILE in result.output
|
||||
for uuid in UUID_EXPECTED_FROM_FILE:
|
||||
assert uuid not in result.output
|
||||
|
||||
|
||||
def test_export_exportdb():
|
||||
"""test --exportdb"""
|
||||
|
||||
@@ -6657,7 +6731,14 @@ def test_export_exportdb():
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[os.path.join(cwd, CLI_PHOTOS_DB), ".", "-V", "--exportdb", "export.db"],
|
||||
[
|
||||
"--db",
|
||||
os.path.join(cwd, CLI_PHOTOS_DB),
|
||||
".",
|
||||
"-V",
|
||||
"--exportdb",
|
||||
"export.db",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert re.search(r"Created export database.*export\.db", result.output)
|
||||
@@ -7921,6 +8002,7 @@ def test_query_function():
|
||||
result = runner.invoke(
|
||||
query,
|
||||
[
|
||||
"--db",
|
||||
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||
"--query-function",
|
||||
f"{tmpdir}/query1.py::query",
|
||||
@@ -7942,6 +8024,7 @@ def test_query_added_after():
|
||||
results = runner.invoke(
|
||||
query,
|
||||
[
|
||||
"--db",
|
||||
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||
"--json",
|
||||
"--added-after",
|
||||
@@ -7962,6 +8045,7 @@ def test_query_added_before():
|
||||
results = runner.invoke(
|
||||
query,
|
||||
[
|
||||
"--db",
|
||||
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||
"--json",
|
||||
"--added-before",
|
||||
|
||||
96
tests/test_cli_all_commands.py
Normal file
96
tests/test_cli_all_commands.py
Normal file
@@ -0,0 +1,96 @@
|
||||
""" Test osxphotos cli commands to verify they run without error.
|
||||
|
||||
These tests simply run the commands to verify no errors are thrown.
|
||||
They do not verify the output of the commands. More complex tests are
|
||||
in test_cli.py and test_cli__xxx.py for specific commands.
|
||||
|
||||
Complex commands such as export are not tested here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, Callable
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
TEST_DB = "tests/Test-13.0.0.photoslibrary"
|
||||
TEST_DB = os.path.join(os.getcwd(), TEST_DB)
|
||||
TEST_RUN_SCRIPT = "examples/cli_example_1.py"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def runner() -> CliRunner:
|
||||
return CliRunner()
|
||||
|
||||
|
||||
from osxphotos.cli import (
|
||||
about,
|
||||
albums,
|
||||
debug_dump,
|
||||
docs_command,
|
||||
dump,
|
||||
grep,
|
||||
help,
|
||||
info,
|
||||
keywords,
|
||||
labels,
|
||||
list_libraries,
|
||||
orphans,
|
||||
persons,
|
||||
places,
|
||||
theme,
|
||||
tutorial,
|
||||
uuid,
|
||||
version,
|
||||
)
|
||||
|
||||
|
||||
def test_about(runner: CliRunner):
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(about)
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"command",
|
||||
[
|
||||
albums,
|
||||
docs_command,
|
||||
dump,
|
||||
help,
|
||||
info,
|
||||
keywords,
|
||||
labels,
|
||||
list_libraries,
|
||||
orphans,
|
||||
persons,
|
||||
places,
|
||||
tutorial,
|
||||
uuid,
|
||||
version,
|
||||
],
|
||||
)
|
||||
def test_cli_comands(runner: CliRunner, command: Callable[..., Any]):
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(albums, ["--db", TEST_DB])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_grep(runner: CliRunner):
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(grep, ["--db", TEST_DB, "test"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_debug_dump(runner: CliRunner):
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(debug_dump, ["--db", TEST_DB, "--dump", "persons"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_theme(runner: CliRunner):
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(theme, ["--list"])
|
||||
assert result.exit_code == 0
|
||||
@@ -8,7 +8,7 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
import osxphotos.sqlitekvstore
|
||||
from osxphotos.sqlitekvstore import SQLiteKVStore
|
||||
|
||||
|
||||
def pickle_and_zip(data: Any) -> bytes:
|
||||
@@ -41,7 +41,7 @@ def unzip_and_unpickle(data: bytes) -> Any:
|
||||
def test_basic_get_set(tmpdir):
|
||||
"""Test basic functionality"""
|
||||
dbpath = tmpdir / "kvtest.db"
|
||||
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(dbpath)
|
||||
kvstore = SQLiteKVStore(dbpath)
|
||||
kvstore.set("foo", "bar")
|
||||
assert kvstore.get("foo") == "bar"
|
||||
assert kvstore.get("FOOBAR") is None
|
||||
@@ -61,7 +61,7 @@ def test_basic_get_set(tmpdir):
|
||||
def test_basic_get_set_wal(tmpdir):
|
||||
"""Test basic functionality with WAL mode"""
|
||||
dbpath = tmpdir / "kvtest.db"
|
||||
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(dbpath, wal=True)
|
||||
kvstore = SQLiteKVStore(dbpath, wal=True)
|
||||
kvstore.set("foo", "bar")
|
||||
assert kvstore.get("foo") == "bar"
|
||||
assert kvstore.get("FOOBAR") is None
|
||||
@@ -84,14 +84,14 @@ def test_set_many(tmpdir):
|
||||
"""Test set_many()"""
|
||||
dbpath = tmpdir / "kvtest.db"
|
||||
|
||||
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(dbpath)
|
||||
kvstore = SQLiteKVStore(dbpath)
|
||||
kvstore.set_many([("foo", "bar"), ("baz", "qux")])
|
||||
assert kvstore.get("foo") == "bar"
|
||||
assert kvstore.get("baz") == "qux"
|
||||
kvstore.close()
|
||||
|
||||
# make sure values got committed
|
||||
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(dbpath)
|
||||
kvstore = SQLiteKVStore(dbpath)
|
||||
assert kvstore.get("foo") == "bar"
|
||||
assert kvstore.get("baz") == "qux"
|
||||
kvstore.close()
|
||||
@@ -101,14 +101,14 @@ def test_set_many_dict(tmpdir):
|
||||
"""Test set_many() with dict of values"""
|
||||
dbpath = tmpdir / "kvtest.db"
|
||||
|
||||
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(dbpath)
|
||||
kvstore = SQLiteKVStore(dbpath)
|
||||
kvstore.set_many({"foo": "bar", "baz": "qux"})
|
||||
assert kvstore.get("foo") == "bar"
|
||||
assert kvstore.get("baz") == "qux"
|
||||
kvstore.close()
|
||||
|
||||
# make sure values got committed
|
||||
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(dbpath)
|
||||
kvstore = SQLiteKVStore(dbpath)
|
||||
assert kvstore.get("foo") == "bar"
|
||||
assert kvstore.get("baz") == "qux"
|
||||
kvstore.close()
|
||||
@@ -118,7 +118,7 @@ def test_basic_context_handler(tmpdir):
|
||||
"""Test basic functionality with context handler"""
|
||||
|
||||
dbpath = tmpdir / "kvtest.db"
|
||||
with osxphotos.sqlitekvstore.SQLiteKVStore(dbpath) as kvstore:
|
||||
with SQLiteKVStore(dbpath) as kvstore:
|
||||
kvstore.set("foo", "bar")
|
||||
assert kvstore.get("foo") == "bar"
|
||||
assert kvstore.get("FOOBAR") is None
|
||||
@@ -134,7 +134,7 @@ def test_basic_context_handler(tmpdir):
|
||||
def test_about(tmpdir):
|
||||
"""Test about property"""
|
||||
dbpath = tmpdir / "kvtest.db"
|
||||
with osxphotos.sqlitekvstore.SQLiteKVStore(dbpath) as kvstore:
|
||||
with SQLiteKVStore(dbpath) as kvstore:
|
||||
kvstore.about = "My description"
|
||||
assert kvstore.about == "My description"
|
||||
kvstore.about = "My new description"
|
||||
@@ -144,17 +144,17 @@ def test_about(tmpdir):
|
||||
def test_existing_db(tmpdir):
|
||||
"""Test that opening an existing database works as expected"""
|
||||
dbpath = tmpdir / "kvtest.db"
|
||||
with osxphotos.sqlitekvstore.SQLiteKVStore(dbpath) as kvstore:
|
||||
with SQLiteKVStore(dbpath) as kvstore:
|
||||
kvstore.set("foo", "bar")
|
||||
|
||||
with osxphotos.sqlitekvstore.SQLiteKVStore(dbpath) as kvstore:
|
||||
with SQLiteKVStore(dbpath) as kvstore:
|
||||
assert kvstore.get("foo") == "bar"
|
||||
|
||||
|
||||
def test_dict_interface(tmpdir):
|
||||
""" "Test dict interface"""
|
||||
dbpath = tmpdir / "kvtest.db"
|
||||
with osxphotos.sqlitekvstore.SQLiteKVStore(dbpath) as kvstore:
|
||||
with SQLiteKVStore(dbpath) as kvstore:
|
||||
kvstore["foo"] = "bar"
|
||||
assert kvstore["foo"] == "bar"
|
||||
assert len(kvstore) == 1
|
||||
@@ -186,9 +186,7 @@ def test_dict_interface(tmpdir):
|
||||
def test_serialize_deserialize(tmpdir):
|
||||
"""Test serialize/deserialize"""
|
||||
dbpath = tmpdir / "kvtest.db"
|
||||
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(
|
||||
dbpath, serialize=json.dumps, deserialize=json.loads
|
||||
)
|
||||
kvstore = SQLiteKVStore(dbpath, serialize=json.dumps, deserialize=json.loads)
|
||||
kvstore.set("foo", {"bar": "baz"})
|
||||
assert kvstore.get("foo") == {"bar": "baz"}
|
||||
assert kvstore.get("FOOBAR") is None
|
||||
@@ -197,7 +195,7 @@ def test_serialize_deserialize(tmpdir):
|
||||
def test_serialize_deserialize_binary_data(tmpdir):
|
||||
"""Test serialize/deserialize with binary data"""
|
||||
dbpath = tmpdir / "kvtest.db"
|
||||
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(
|
||||
kvstore = SQLiteKVStore(
|
||||
dbpath, serialize=pickle_and_zip, deserialize=unzip_and_unpickle
|
||||
)
|
||||
kvstore.set("foo", {"bar": "baz"})
|
||||
@@ -209,16 +207,16 @@ def test_serialize_deserialize_bad_callable(tmpdir):
|
||||
"""Test serialize/deserialize with bad values"""
|
||||
dbpath = tmpdir / "kvtest.db"
|
||||
with pytest.raises(TypeError):
|
||||
osxphotos.sqlitekvstore.SQLiteKVStore(dbpath, serialize=1, deserialize=None)
|
||||
SQLiteKVStore(dbpath, serialize=1, deserialize=None)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
osxphotos.sqlitekvstore.SQLiteKVStore(dbpath, serialize=None, deserialize=1)
|
||||
SQLiteKVStore(dbpath, serialize=None, deserialize=1)
|
||||
|
||||
|
||||
def test_iter(tmpdir):
|
||||
"""Test generator behavior"""
|
||||
dbpath = tmpdir / "kvtest.db"
|
||||
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(dbpath)
|
||||
kvstore = SQLiteKVStore(dbpath)
|
||||
kvstore.set("foo", "bar")
|
||||
kvstore.set("baz", "qux")
|
||||
kvstore.set("quux", "corge")
|
||||
@@ -230,7 +228,7 @@ def test_iter(tmpdir):
|
||||
def test_keys_values_items(tmpdir):
|
||||
"""Test keys, values, items"""
|
||||
dbpath = tmpdir / "kvtest.db"
|
||||
kvstore = osxphotos.sqlitekvstore.SQLiteKVStore(dbpath)
|
||||
kvstore = SQLiteKVStore(dbpath)
|
||||
kvstore.set("foo", "bar")
|
||||
kvstore.set("baz", "qux")
|
||||
kvstore.set("quux", "corge")
|
||||
@@ -243,3 +241,26 @@ def test_keys_values_items(tmpdir):
|
||||
("grault", "garply"),
|
||||
("quux", "corge"),
|
||||
]
|
||||
|
||||
|
||||
def test_path(tmpdir):
|
||||
"""Test path property"""
|
||||
dbpath = tmpdir / "kvtest.db"
|
||||
kvstore = SQLiteKVStore(dbpath)
|
||||
assert kvstore.path == dbpath
|
||||
|
||||
|
||||
def test_wipe(tmpdir):
|
||||
"""Test wipe"""
|
||||
dbpath = tmpdir / "kvtest.db"
|
||||
kvstore = SQLiteKVStore(dbpath)
|
||||
kvstore.set("foo", "bar")
|
||||
kvstore.set("baz", "qux")
|
||||
kvstore.set("quux", "corge")
|
||||
kvstore.set("grault", "garply")
|
||||
assert len(kvstore) == 4
|
||||
kvstore.wipe()
|
||||
assert len(kvstore) == 0
|
||||
assert "foo"
|
||||
kvstore.set("foo", "bar")
|
||||
assert kvstore.get("foo") == "bar"
|
||||
|
||||
Reference in New Issue
Block a user