Added --watch, --breakpoint (#652)
This commit is contained in:
parent
be1f3a98d9
commit
ed315fffd2
@ -1,11 +1,46 @@
|
||||
"""cli package for osxphotos"""
|
||||
import sys
|
||||
|
||||
from rich import print
|
||||
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 .albums import albums
|
||||
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 .dump import dump
|
||||
from .export import export
|
||||
@ -50,6 +85,7 @@ __all__ = [
|
||||
"query",
|
||||
"repl",
|
||||
"run",
|
||||
"set_debug",
|
||||
"snap",
|
||||
"tutorial",
|
||||
"uuid",
|
||||
|
||||
@ -13,9 +13,6 @@ from osxphotos._version import __version__
|
||||
from .click_rich_echo import rich_echo
|
||||
from .param_types import *
|
||||
|
||||
# global variable to control debug output
|
||||
# set via --debug
|
||||
DEBUG = False
|
||||
|
||||
# used to show/hide hidden commands
|
||||
OSXPHOTOS_HIDDEN = not bool(os.getenv("OSXPHOTOS_SHOW_HIDDEN", default=False))
|
||||
@ -30,17 +27,6 @@ CLI_COLOR_ERROR = "red"
|
||||
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):
|
||||
"""no-op function"""
|
||||
pass
|
||||
@ -513,6 +499,37 @@ def QUERY_OPTIONS(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):
|
||||
"""Load UUIDs from file. Does not validate UUIDs.
|
||||
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.datetime_formatter import DateTimeFormatter
|
||||
from osxphotos.debug import is_debug, set_debug
|
||||
from osxphotos.exiftool import get_exiftool_path
|
||||
from osxphotos.export_db import ExportDB, ExportDBInMemory
|
||||
from osxphotos.fileutil import FileUtil, FileUtilNoOp
|
||||
@ -55,22 +56,20 @@ from osxphotos.phototemplate import PhotoTemplate, RenderOptions
|
||||
from osxphotos.queryoptions import QueryOptions
|
||||
from osxphotos.uti import get_preferred_uti_extension
|
||||
from osxphotos.utils import format_sec_to_hhmmss, normalize_fs_path
|
||||
|
||||
from .common import (
|
||||
CLI_COLOR_ERROR,
|
||||
CLI_COLOR_WARNING,
|
||||
DB_ARGUMENT,
|
||||
DB_OPTION,
|
||||
DEBUG_OPTIONS,
|
||||
DELETED_OPTIONS,
|
||||
get_photos_db,
|
||||
JSON_OPTION,
|
||||
load_uuid_from_file,
|
||||
noop,
|
||||
OSXPHOTOS_CRASH_LOG,
|
||||
OSXPHOTOS_HIDDEN,
|
||||
QUERY_OPTIONS,
|
||||
get_photos_db,
|
||||
is_debug,
|
||||
load_uuid_from_file,
|
||||
noop,
|
||||
set_debug,
|
||||
verbose_print,
|
||||
)
|
||||
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}. "
|
||||
"Default = 'cumulative'.",
|
||||
)
|
||||
@click.option(
|
||||
"--debug",
|
||||
required=False,
|
||||
is_flag=True,
|
||||
default=False,
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
help="Enable debug output.",
|
||||
)
|
||||
@DEBUG_OPTIONS
|
||||
@DB_ARGUMENT
|
||||
@click.argument("dest", nargs=1, type=click.Path(exists=True))
|
||||
@click.pass_obj
|
||||
@ -790,6 +782,8 @@ def export(
|
||||
profile,
|
||||
profile_sort,
|
||||
debug,
|
||||
watch,
|
||||
breakpoint,
|
||||
):
|
||||
"""Export photos from the Photos database.
|
||||
Export path DEST is required.
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import click
|
||||
|
||||
import osxphotos
|
||||
from osxphotos.debug import set_debug
|
||||
from osxphotos.photosalbum import PhotosAlbum
|
||||
from osxphotos.queryoptions import QueryOptions
|
||||
|
||||
@ -17,7 +18,6 @@ from .common import (
|
||||
QUERY_OPTIONS,
|
||||
get_photos_db,
|
||||
load_uuid_from_file,
|
||||
set_debug,
|
||||
)
|
||||
from .list import _list_libraries
|
||||
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
|
||||
textx>=2.3.0,<2.4.0
|
||||
toml>=0.10.2,<0.11.0
|
||||
wrapt>=1.13.3,<1.14.0
|
||||
wurlitzer>=2.1.0,<2.2.0
|
||||
Loading…
x
Reference in New Issue
Block a user