Added --watch, --breakpoint (#652)

This commit is contained in:
Rhet Turnbull 2022-03-04 06:45:57 -08:00 committed by GitHub
parent be1f3a98d9
commit ed315fffd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 164 additions and 30 deletions

View File

@ -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",

View File

@ -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.

View File

@ -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.

View File

@ -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
View 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

View File

@ -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

View File

@ -97,6 +97,7 @@ setup(
"rich>=11.2.0,<12.0.0",
"textx>=2.3.0,<3.0.0",
"toml>=0.10.2,<0.11.0",
"wrapt>=1.13.3,<1.14.0",
"wurlitzer>=2.1.0,<3.0.0",
],
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli_main"]},