Added --watch, --breakpoint (#652)
This commit is contained in:
@@ -1,11 +1,46 @@
|
|||||||
"""cli package for osxphotos"""
|
"""cli package for osxphotos"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from rich import print
|
||||||
from rich.traceback import install as install_traceback
|
from rich.traceback import install as install_traceback
|
||||||
|
|
||||||
|
from osxphotos.debug import (
|
||||||
|
debug_breakpoint,
|
||||||
|
debug_watch,
|
||||||
|
get_debug_args,
|
||||||
|
set_debug,
|
||||||
|
wrap_function,
|
||||||
|
)
|
||||||
|
|
||||||
|
# apply any debug functions
|
||||||
|
# need to do this before importing anything else so that the debug functions
|
||||||
|
# wrap the right function references
|
||||||
|
# if a module does something like "from exiftool import ExifTool" and the user tries
|
||||||
|
# to wrap 'osxphotos.exiftool.ExifTool.asdict', the original ExifTool.asdict will be
|
||||||
|
# wrapped but the caller will have a reference to the function before it was wrapped
|
||||||
|
# reference: https://github.com/GrahamDumpleton/wrapt/blob/develop/blog/13-ordering-issues-when-monkey-patching-in-python.md
|
||||||
|
args = get_debug_args(["--watch", "--breakpoint"], sys.argv)
|
||||||
|
for func_name in args.get("--watch", []):
|
||||||
|
try:
|
||||||
|
wrap_function(func_name, debug_watch)
|
||||||
|
print(f"Watching {func_name}")
|
||||||
|
except AttributeError:
|
||||||
|
print(f"{func_name} does not exist")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
for func_name in args.get("--breakpoint", []):
|
||||||
|
try:
|
||||||
|
wrap_function(func_name, debug_breakpoint)
|
||||||
|
print(f"Breakpoint added for {func_name}")
|
||||||
|
except AttributeError:
|
||||||
|
print(f"{func_name} does not exist")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
from .about import about
|
from .about import about
|
||||||
from .albums import albums
|
from .albums import albums
|
||||||
from .cli import cli_main
|
from .cli import cli_main
|
||||||
from .common import get_photos_db, load_uuid_from_file, set_debug
|
from .common import get_photos_db, load_uuid_from_file
|
||||||
from .debug_dump import debug_dump
|
from .debug_dump import debug_dump
|
||||||
from .dump import dump
|
from .dump import dump
|
||||||
from .export import export
|
from .export import export
|
||||||
@@ -50,6 +85,7 @@ __all__ = [
|
|||||||
"query",
|
"query",
|
||||||
"repl",
|
"repl",
|
||||||
"run",
|
"run",
|
||||||
|
"set_debug",
|
||||||
"snap",
|
"snap",
|
||||||
"tutorial",
|
"tutorial",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ from osxphotos._version import __version__
|
|||||||
from .click_rich_echo import rich_echo
|
from .click_rich_echo import rich_echo
|
||||||
from .param_types import *
|
from .param_types import *
|
||||||
|
|
||||||
# global variable to control debug output
|
|
||||||
# set via --debug
|
|
||||||
DEBUG = False
|
|
||||||
|
|
||||||
# used to show/hide hidden commands
|
# used to show/hide hidden commands
|
||||||
OSXPHOTOS_HIDDEN = not bool(os.getenv("OSXPHOTOS_SHOW_HIDDEN", default=False))
|
OSXPHOTOS_HIDDEN = not bool(os.getenv("OSXPHOTOS_SHOW_HIDDEN", default=False))
|
||||||
@@ -30,17 +27,6 @@ CLI_COLOR_ERROR = "red"
|
|||||||
CLI_COLOR_WARNING = "yellow"
|
CLI_COLOR_WARNING = "yellow"
|
||||||
|
|
||||||
|
|
||||||
def set_debug(debug: bool):
|
|
||||||
"""set debug flag"""
|
|
||||||
global DEBUG
|
|
||||||
DEBUG = debug
|
|
||||||
|
|
||||||
|
|
||||||
def is_debug():
|
|
||||||
"""return debug flag"""
|
|
||||||
return DEBUG
|
|
||||||
|
|
||||||
|
|
||||||
def noop(*args, **kwargs):
|
def noop(*args, **kwargs):
|
||||||
"""no-op function"""
|
"""no-op function"""
|
||||||
pass
|
pass
|
||||||
@@ -513,6 +499,37 @@ def QUERY_OPTIONS(f):
|
|||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def DEBUG_OPTIONS(f):
|
||||||
|
o = click.option
|
||||||
|
options = [
|
||||||
|
o(
|
||||||
|
"--debug",
|
||||||
|
is_flag=True,
|
||||||
|
help="Enable debug output.",
|
||||||
|
hidden=OSXPHOTOS_HIDDEN,
|
||||||
|
),
|
||||||
|
o(
|
||||||
|
"--watch",
|
||||||
|
metavar="FUNCTION_PATH",
|
||||||
|
multiple=True,
|
||||||
|
help="Watch function calls. For example, to watch all calls to FileUtil.copy: "
|
||||||
|
"'--watch osxphotos.fileutil.FileUtil.copy'. More than one --watch option can be specified.",
|
||||||
|
hidden=OSXPHOTOS_HIDDEN,
|
||||||
|
),
|
||||||
|
o(
|
||||||
|
"--breakpoint",
|
||||||
|
metavar="FUNCTION_PATH",
|
||||||
|
multiple=True,
|
||||||
|
help="Add breakpoint to function calls. For example, to add breakpoint to FileUtil.copy: "
|
||||||
|
"'--breakpoint osxphotos.fileutil.FileUtil.copy'. More than one --breakpoint option can be specified.",
|
||||||
|
hidden=OSXPHOTOS_HIDDEN,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
for o in options[::-1]:
|
||||||
|
f = o(f)
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
def load_uuid_from_file(filename):
|
def load_uuid_from_file(filename):
|
||||||
"""Load UUIDs from file. Does not validate UUIDs.
|
"""Load UUIDs from file. Does not validate UUIDs.
|
||||||
Format is 1 UUID per line, any line beginning with # is ignored.
|
Format is 1 UUID per line, any line beginning with # is ignored.
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ from osxphotos.configoptions import (
|
|||||||
)
|
)
|
||||||
from osxphotos.crash_reporter import crash_reporter
|
from osxphotos.crash_reporter import crash_reporter
|
||||||
from osxphotos.datetime_formatter import DateTimeFormatter
|
from osxphotos.datetime_formatter import DateTimeFormatter
|
||||||
|
from osxphotos.debug import is_debug, set_debug
|
||||||
from osxphotos.exiftool import get_exiftool_path
|
from osxphotos.exiftool import get_exiftool_path
|
||||||
from osxphotos.export_db import ExportDB, ExportDBInMemory
|
from osxphotos.export_db import ExportDB, ExportDBInMemory
|
||||||
from osxphotos.fileutil import FileUtil, FileUtilNoOp
|
from osxphotos.fileutil import FileUtil, FileUtilNoOp
|
||||||
@@ -55,22 +56,20 @@ from osxphotos.phototemplate import PhotoTemplate, RenderOptions
|
|||||||
from osxphotos.queryoptions import QueryOptions
|
from osxphotos.queryoptions import QueryOptions
|
||||||
from osxphotos.uti import get_preferred_uti_extension
|
from osxphotos.uti import get_preferred_uti_extension
|
||||||
from osxphotos.utils import format_sec_to_hhmmss, normalize_fs_path
|
from osxphotos.utils import format_sec_to_hhmmss, normalize_fs_path
|
||||||
|
|
||||||
from .common import (
|
from .common import (
|
||||||
CLI_COLOR_ERROR,
|
CLI_COLOR_ERROR,
|
||||||
CLI_COLOR_WARNING,
|
CLI_COLOR_WARNING,
|
||||||
DB_ARGUMENT,
|
DB_ARGUMENT,
|
||||||
DB_OPTION,
|
DB_OPTION,
|
||||||
|
DEBUG_OPTIONS,
|
||||||
DELETED_OPTIONS,
|
DELETED_OPTIONS,
|
||||||
|
get_photos_db,
|
||||||
JSON_OPTION,
|
JSON_OPTION,
|
||||||
|
load_uuid_from_file,
|
||||||
|
noop,
|
||||||
OSXPHOTOS_CRASH_LOG,
|
OSXPHOTOS_CRASH_LOG,
|
||||||
OSXPHOTOS_HIDDEN,
|
OSXPHOTOS_HIDDEN,
|
||||||
QUERY_OPTIONS,
|
QUERY_OPTIONS,
|
||||||
get_photos_db,
|
|
||||||
is_debug,
|
|
||||||
load_uuid_from_file,
|
|
||||||
noop,
|
|
||||||
set_debug,
|
|
||||||
verbose_print,
|
verbose_print,
|
||||||
)
|
)
|
||||||
from .help import ExportCommand, get_help_msg
|
from .help import ExportCommand, get_help_msg
|
||||||
@@ -629,14 +628,7 @@ from .param_types import ExportDBType, FunctionCall
|
|||||||
f"Can be specified multiple times. Valid options are: {PROFILE_SORT_KEYS}. "
|
f"Can be specified multiple times. Valid options are: {PROFILE_SORT_KEYS}. "
|
||||||
"Default = 'cumulative'.",
|
"Default = 'cumulative'.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@DEBUG_OPTIONS
|
||||||
"--debug",
|
|
||||||
required=False,
|
|
||||||
is_flag=True,
|
|
||||||
default=False,
|
|
||||||
hidden=OSXPHOTOS_HIDDEN,
|
|
||||||
help="Enable debug output.",
|
|
||||||
)
|
|
||||||
@DB_ARGUMENT
|
@DB_ARGUMENT
|
||||||
@click.argument("dest", nargs=1, type=click.Path(exists=True))
|
@click.argument("dest", nargs=1, type=click.Path(exists=True))
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
@@ -790,6 +782,8 @@ def export(
|
|||||||
profile,
|
profile,
|
||||||
profile_sort,
|
profile_sort,
|
||||||
debug,
|
debug,
|
||||||
|
watch,
|
||||||
|
breakpoint,
|
||||||
):
|
):
|
||||||
"""Export photos from the Photos database.
|
"""Export photos from the Photos database.
|
||||||
Export path DEST is required.
|
Export path DEST is required.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import click
|
import click
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
from osxphotos.debug import set_debug
|
||||||
from osxphotos.photosalbum import PhotosAlbum
|
from osxphotos.photosalbum import PhotosAlbum
|
||||||
from osxphotos.queryoptions import QueryOptions
|
from osxphotos.queryoptions import QueryOptions
|
||||||
|
|
||||||
@@ -17,7 +18,6 @@ from .common import (
|
|||||||
QUERY_OPTIONS,
|
QUERY_OPTIONS,
|
||||||
get_photos_db,
|
get_photos_db,
|
||||||
load_uuid_from_file,
|
load_uuid_from_file,
|
||||||
set_debug,
|
|
||||||
)
|
)
|
||||||
from .list import _list_libraries
|
from .list import _list_libraries
|
||||||
from .print_photo_info import print_photo_info
|
from .print_photo_info import print_photo_info
|
||||||
|
|||||||
85
osxphotos/debug.py
Normal file
85
osxphotos/debug.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
"""Utilities for debugging"""
|
||||||
|
|
||||||
|
import pdb
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
import wrapt
|
||||||
|
from rich import print
|
||||||
|
|
||||||
|
# global variable to control debug output
|
||||||
|
# set via --debug
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
|
||||||
|
def set_debug(debug: bool):
|
||||||
|
"""set debug flag"""
|
||||||
|
global DEBUG
|
||||||
|
DEBUG = debug
|
||||||
|
|
||||||
|
|
||||||
|
def is_debug():
|
||||||
|
"""return debug flag"""
|
||||||
|
return DEBUG
|
||||||
|
|
||||||
|
|
||||||
|
def debug_watch(wrapped, instance, args, kwargs):
|
||||||
|
"""For use with wrapt.wrap_function_wrapper to watch calls to a function"""
|
||||||
|
caller = sys._getframe().f_back.f_code.co_name
|
||||||
|
name = wrapped.__name__
|
||||||
|
timestamp = datetime.now().isoformat()
|
||||||
|
print(
|
||||||
|
f"{timestamp} {name} called from {caller} with args: {args} and kwargs: {kwargs}"
|
||||||
|
)
|
||||||
|
rv = wrapped(*args, **kwargs)
|
||||||
|
print(f"{timestamp} {name} returned: {rv}")
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
def debug_breakpoint(wrapped, instance, args, kwargs):
|
||||||
|
"""For use with wrapt.wrap_function_wrapper to set breakpoint on a function"""
|
||||||
|
pdb.set_trace()
|
||||||
|
return wrapped(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_function(function_path, wrapper):
|
||||||
|
"""Wrap a function with wrapper function"""
|
||||||
|
module, name = function_path.split(".", 1)
|
||||||
|
try:
|
||||||
|
return wrapt.wrap_function_wrapper(module, name, wrapper)
|
||||||
|
except AttributeError as e:
|
||||||
|
raise AttributeError(f"{module}.{name} does not exist") from e
|
||||||
|
|
||||||
|
|
||||||
|
def get_debug_args(arg_names: List, argv: List) -> Dict:
|
||||||
|
"""Get the arguments for the debug options;
|
||||||
|
Some of the debug options like --watch and --breakpoint need to be processed before any other packages are loaded
|
||||||
|
so they can't be handled in the normal click argument processing, thus this function is called
|
||||||
|
from osxphotos/cli/__init__.py
|
||||||
|
|
||||||
|
Assumes multi-valued options are OK and that all options take form of --option VALUE or --option=VALUE
|
||||||
|
"""
|
||||||
|
# argv[0] is the program name
|
||||||
|
# argv[1] is the command
|
||||||
|
# argv[2:] are the arguments
|
||||||
|
args = {}
|
||||||
|
for arg_name in arg_names:
|
||||||
|
for idx, arg in enumerate(argv[1:]):
|
||||||
|
if arg.startswith(f"{arg_name}="):
|
||||||
|
arg_value = arg.split("=")[1]
|
||||||
|
try:
|
||||||
|
args[arg].append(arg_value)
|
||||||
|
except KeyError:
|
||||||
|
args[arg] = [arg_value]
|
||||||
|
elif arg == arg_name:
|
||||||
|
try:
|
||||||
|
args[arg].append(argv[idx + 2])
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
args[arg] = [argv[idx + 2]]
|
||||||
|
except IndexError as e:
|
||||||
|
raise ValueError(f"Missing value for {arg}") from e
|
||||||
|
except IndexError as e:
|
||||||
|
raise ValueError(f"Missing value for {arg}") from e
|
||||||
|
return args
|
||||||
@@ -22,4 +22,5 @@ PyYAML>=5.4.1,<6.0.0
|
|||||||
rich>=11.2.0,<12.0.0
|
rich>=11.2.0,<12.0.0
|
||||||
textx>=2.3.0,<2.4.0
|
textx>=2.3.0,<2.4.0
|
||||||
toml>=0.10.2,<0.11.0
|
toml>=0.10.2,<0.11.0
|
||||||
|
wrapt>=1.13.3,<1.14.0
|
||||||
wurlitzer>=2.1.0,<2.2.0
|
wurlitzer>=2.1.0,<2.2.0
|
||||||
1
setup.py
1
setup.py
@@ -97,6 +97,7 @@ setup(
|
|||||||
"rich>=11.2.0,<12.0.0",
|
"rich>=11.2.0,<12.0.0",
|
||||||
"textx>=2.3.0,<3.0.0",
|
"textx>=2.3.0,<3.0.0",
|
||||||
"toml>=0.10.2,<0.11.0",
|
"toml>=0.10.2,<0.11.0",
|
||||||
|
"wrapt>=1.13.3,<1.14.0",
|
||||||
"wurlitzer>=2.1.0,<3.0.0",
|
"wurlitzer>=2.1.0,<3.0.0",
|
||||||
],
|
],
|
||||||
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli_main"]},
|
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli_main"]},
|
||||||
|
|||||||
Reference in New Issue
Block a user