Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dec028448 | ||
|
|
8be6a98c32 | ||
|
|
ce73c9cab8 |
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -32,4 +32,4 @@ jobs:
|
|||||||
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
run: |
|
run: |
|
||||||
python -m pytest tests/
|
python -m pytest -v tests/
|
||||||
|
|||||||
40
README.md
40
README.md
@@ -162,7 +162,41 @@ Commands:
|
|||||||
uuid Print out unique IDs (UUID) of photos selected in Photos
|
uuid Print out unique IDs (UUID) of photos selected in Photos
|
||||||
```
|
```
|
||||||
|
|
||||||
To get help on a specific command, use `osxphotos help <command_name>`
|
To get help on a specific command, use `osxphotos help command_name`, for example, `osxphotos help export` to get help on the `export` command.
|
||||||
|
|
||||||
|
Some of the commands such as `export` and `query` have a large number of options. To search for options related to a specific topic, you can use `osxphotos help command_name topic_name`. For example, `osxphotos help export raw` finds the options related to RAW files (search is case-insensitive):
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage: osxphotos export [OPTIONS] [PHOTOS_LIBRARY]... DEST
|
||||||
|
|
||||||
|
Export photos from the Photos database. Export path DEST is required.
|
||||||
|
Optionally, query the Photos database using 1 or more search options; if
|
||||||
|
more than one option is provided, they are treated as "AND" (e.g. search for
|
||||||
|
photos matching all options). If no query options are provided, all photos
|
||||||
|
will be exported. By default, all versions of all photos will be exported
|
||||||
|
including edited versions, live photo movies, burst photos, and associated
|
||||||
|
raw images. See --skip-edited, --skip-live, --skip-bursts, and --skip-raw
|
||||||
|
options to modify this behavior.
|
||||||
|
|
||||||
|
Options that match 'raw':
|
||||||
|
|
||||||
|
--has-raw Search for photos with both a jpeg and
|
||||||
|
raw version
|
||||||
|
--skip-raw Do not export associated RAW image of a
|
||||||
|
RAW+JPEG pair. Note: this does not skip RAW
|
||||||
|
photos if the RAW photo does not have an
|
||||||
|
associated JPEG image (e.g. the RAW file was
|
||||||
|
imported to Photos without a JPEG preview).
|
||||||
|
--convert-to-jpeg Convert all non-JPEG images (e.g. RAW, HEIC,
|
||||||
|
PNG, etc) to JPEG upon export. Note: does not
|
||||||
|
convert the RAW component of a RAW+JPEG pair as
|
||||||
|
the associated JPEG image will be exported. You
|
||||||
|
can use --skip-raw to skip
|
||||||
|
exporting the associated RAW image of a
|
||||||
|
RAW+JPEG pair. See also --jpeg-quality and
|
||||||
|
--jpeg-ext. Only works if your Mac has a GPU
|
||||||
|
(thus may not work on virtual machines).
|
||||||
|
```
|
||||||
|
|
||||||
### Command line examples
|
### Command line examples
|
||||||
|
|
||||||
@@ -1741,7 +1775,7 @@ Substitution Description
|
|||||||
{lf} A line feed: '\n', alias for {newline}
|
{lf} A line feed: '\n', alias for {newline}
|
||||||
{cr} A carriage return: '\r'
|
{cr} A carriage return: '\r'
|
||||||
{crlf} a carriage return + line feed: '\r\n'
|
{crlf} a carriage return + line feed: '\r\n'
|
||||||
{osxphotos_version} The osxphotos version, e.g. '0.47.1'
|
{osxphotos_version} The osxphotos version, e.g. '0.47.2'
|
||||||
{osxphotos_cmd_line} The full command line used to run osxphotos
|
{osxphotos_cmd_line} The full command line used to run osxphotos
|
||||||
|
|
||||||
The following substitutions may result in multiple values. Thus if specified for
|
The following substitutions may result in multiple values. Thus if specified for
|
||||||
@@ -3645,7 +3679,7 @@ The following template field substitutions are availabe for use the templating s
|
|||||||
|{lf}|A line feed: '\n', alias for {newline}|
|
|{lf}|A line feed: '\n', alias for {newline}|
|
||||||
|{cr}|A carriage return: '\r'|
|
|{cr}|A carriage return: '\r'|
|
||||||
|{crlf}|a carriage return + line feed: '\r\n'|
|
|{crlf}|a carriage return + line feed: '\r\n'|
|
||||||
|{osxphotos_version}|The osxphotos version, e.g. '0.47.1'|
|
|{osxphotos_version}|The osxphotos version, e.g. '0.47.2'|
|
||||||
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|
||||||
|{album}|Album(s) photo is contained in|
|
|{album}|Album(s) photo is contained in|
|
||||||
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Sphinx build info version 1
|
# Sphinx build info version 1
|
||||||
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
|
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||||
config: bc3dce8a14bcd1b0c8a34e4d16f0011f
|
config: 27df32b0956a3bb04b6e6f0109bb2213
|
||||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Overview: module code — osxphotos 0.47.1 documentation</title>
|
<title>Overview: module code — osxphotos 0.47.2 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="../_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="../_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="../_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="../_static/alabaster.css" />
|
||||||
<script data-url_root="../" id="documentation_options" src="../_static/documentation_options.js"></script>
|
<script data-url_root="../" id="documentation_options" src="../_static/documentation_options.js"></script>
|
||||||
|
|||||||
2
docs/_static/documentation_options.js
vendored
2
docs/_static/documentation_options.js
vendored
@@ -1,6 +1,6 @@
|
|||||||
var DOCUMENTATION_OPTIONS = {
|
var DOCUMENTATION_OPTIONS = {
|
||||||
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
||||||
VERSION: '0.47.1',
|
VERSION: '0.47.2',
|
||||||
LANGUAGE: 'None',
|
LANGUAGE: 'None',
|
||||||
COLLAPSE_INDEX: false,
|
COLLAPSE_INDEX: false,
|
||||||
BUILDER: 'html',
|
BUILDER: 'html',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
||||||
|
|
||||||
<title>osxphotos command line interface (CLI) — osxphotos 0.47.1 documentation</title>
|
<title>osxphotos command line interface (CLI) — osxphotos 0.47.2 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Index — osxphotos 0.47.1 documentation</title>
|
<title>Index — osxphotos 0.47.2 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
||||||
|
|
||||||
<title>Welcome to osxphotos’s documentation! — osxphotos 0.47.1 documentation</title>
|
<title>Welcome to osxphotos’s documentation! — osxphotos 0.47.2 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
||||||
|
|
||||||
<title>osxphotos — osxphotos 0.47.1 documentation</title>
|
<title>osxphotos — osxphotos 0.47.2 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
||||||
|
|
||||||
<title>osxphotos package — osxphotos 0.47.1 documentation</title>
|
<title>osxphotos package — osxphotos 0.47.2 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Search — osxphotos 0.47.1 documentation</title>
|
<title>Search — osxphotos 0.47.2 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||||
|
|
||||||
|
|||||||
42
osxphotos/cli/click_rich_echo.py
Normal file
42
osxphotos/cli/click_rich_echo.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"""click.echo replacement that supports rich text formatting"""
|
||||||
|
|
||||||
|
import typing as t
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
import click
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
|
||||||
|
def rich_echo(
|
||||||
|
message: t.Optional[t.Any] = None,
|
||||||
|
**kwargs: t.Any,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Echo text to the console with rich formatting.
|
||||||
|
|
||||||
|
This is a wrapper around click.echo that supports rich text formatting.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: The string or bytes to output. Other objects are converted to strings.
|
||||||
|
kwargs: any extra arguments are passed to rich.console.Console.print() and click.echo
|
||||||
|
if kwargs contains 'file', 'nl', 'err', 'color', these are passed to click.echo,
|
||||||
|
all other values passed to rich.console.Console.print()
|
||||||
|
"""
|
||||||
|
|
||||||
|
# args for click.echo that may have been passed in kwargs
|
||||||
|
echo_args = {}
|
||||||
|
for arg in ("file", "nl", "err", "color"):
|
||||||
|
val = kwargs.pop(arg, None)
|
||||||
|
if val is not None:
|
||||||
|
echo_args[arg] = val
|
||||||
|
|
||||||
|
# click.echo will include "\n" so don't add it here unless specified
|
||||||
|
end = kwargs.pop("end", "")
|
||||||
|
|
||||||
|
# rich.console.Console defaults to 80 chars if it can't auto-detect, which in this case it won't
|
||||||
|
# so we need to set the width manually to a ridiculously large number
|
||||||
|
width = kwargs.pop("width", 10000)
|
||||||
|
output = StringIO()
|
||||||
|
console = Console(force_terminal=True, file=output, width=width)
|
||||||
|
console.print(message, end=end, **kwargs)
|
||||||
|
click.echo(output.getvalue(), **echo_args)
|
||||||
@@ -3,17 +3,16 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
from typing import Callable
|
import typing as t
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
from osxphotos._version import __version__
|
from osxphotos._version import __version__
|
||||||
|
|
||||||
|
from .click_rich_echo import rich_echo
|
||||||
from .param_types import *
|
from .param_types import *
|
||||||
|
|
||||||
from rich import print as rprint
|
|
||||||
|
|
||||||
# global variable to control debug output
|
# global variable to control debug output
|
||||||
# set via --debug
|
# set via --debug
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
@@ -48,14 +47,15 @@ def noop(*args, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
def verbose_print(
|
def verbose_print(
|
||||||
verbose: bool = True, timestamp: bool = False, rich=False
|
verbose: bool = True, timestamp: bool = False, rich=False, **kwargs: t.Any
|
||||||
) -> Callable:
|
) -> t.Callable:
|
||||||
"""Create verbose function to print output
|
"""Create verbose function to print output
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
verbose: if True, returns verbose print function otherwise returns no-op function
|
verbose: if True, returns verbose print function otherwise returns no-op function
|
||||||
timestamp: if True, includes timestamp in verbose output
|
timestamp: if True, includes timestamp in verbose output
|
||||||
rich: use rich.print instead of click.echo
|
rich: use rich.print instead of click.echo
|
||||||
|
kwargs: any extra arguments to pass to click.echo or rich.print depending on whether rich==True
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
function to print output
|
function to print output
|
||||||
@@ -64,10 +64,10 @@ def verbose_print(
|
|||||||
return noop
|
return noop
|
||||||
|
|
||||||
# closure to capture timestamp
|
# closure to capture timestamp
|
||||||
def verbose_(*args, **kwargs):
|
def verbose_(*args):
|
||||||
"""print output if verbose flag set"""
|
"""print output if verbose flag set"""
|
||||||
styled_args = []
|
styled_args = []
|
||||||
timestamp_str = str(datetime.datetime.now()) + " -- " if timestamp else ""
|
timestamp_str = f"{str(datetime.datetime.now())} -- " if timestamp else ""
|
||||||
for arg in args:
|
for arg in args:
|
||||||
if type(arg) == str:
|
if type(arg) == str:
|
||||||
arg = timestamp_str + arg
|
arg = timestamp_str + arg
|
||||||
@@ -78,9 +78,10 @@ def verbose_print(
|
|||||||
styled_args.append(arg)
|
styled_args.append(arg)
|
||||||
click.echo(*styled_args, **kwargs)
|
click.echo(*styled_args, **kwargs)
|
||||||
|
|
||||||
def rich_verbose_(*args, **kwargs):
|
def rich_verbose_(*args):
|
||||||
"""print output if verbose flag set using rich.print"""
|
"""print output if verbose flag set using rich.print"""
|
||||||
timestamp_str = str(datetime.datetime.now()) + " -- " if timestamp else ""
|
timestamp_str = f"{str(datetime.datetime.now())} -- " if timestamp else ""
|
||||||
|
new_args = []
|
||||||
for arg in args:
|
for arg in args:
|
||||||
if type(arg) == str:
|
if type(arg) == str:
|
||||||
arg = timestamp_str + arg
|
arg = timestamp_str + arg
|
||||||
@@ -88,7 +89,8 @@ def verbose_print(
|
|||||||
arg = f"[{CLI_COLOR_ERROR}]{arg}[/{CLI_COLOR_ERROR}]"
|
arg = f"[{CLI_COLOR_ERROR}]{arg}[/{CLI_COLOR_ERROR}]"
|
||||||
elif "warning" in arg.lower():
|
elif "warning" in arg.lower():
|
||||||
arg = f"[{CLI_COLOR_WARNING}]{arg}[/{CLI_COLOR_WARNING}]"
|
arg = f"[{CLI_COLOR_WARNING}]{arg}[/{CLI_COLOR_WARNING}]"
|
||||||
rprint(arg, **kwargs)
|
new_args.append(arg)
|
||||||
|
rich_echo(*new_args, **kwargs)
|
||||||
|
|
||||||
return rich_verbose_ if rich else verbose_
|
return rich_verbose_ if rich else verbose_
|
||||||
|
|
||||||
|
|||||||
@@ -823,7 +823,7 @@ def export(
|
|||||||
ignore=["ctx", "cli_obj", "dest", "load_config", "save_config", "config_only"],
|
ignore=["ctx", "cli_obj", "dest", "load_config", "save_config", "config_only"],
|
||||||
)
|
)
|
||||||
|
|
||||||
verbose_ = verbose_print(verbose, timestamp)
|
verbose_ = verbose_print(verbose, timestamp, rich=True, highlight=False)
|
||||||
|
|
||||||
if load_config:
|
if load_config:
|
||||||
try:
|
try:
|
||||||
@@ -972,7 +972,7 @@ def export(
|
|||||||
xattr_template = cfg.xattr_template
|
xattr_template = cfg.xattr_template
|
||||||
|
|
||||||
# config file might have changed verbose
|
# config file might have changed verbose
|
||||||
verbose_ = verbose_print(verbose, timestamp)
|
verbose_ = verbose_print(verbose, timestamp, rich=True, highlight=False)
|
||||||
verbose_(f"Loaded options from file {load_config}")
|
verbose_(f"Loaded options from file {load_config}")
|
||||||
|
|
||||||
verbose_(f"osxphotos version {__version__}")
|
verbose_(f"osxphotos version {__version__}")
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
"""Help text helper class for osxphotos CLI """
|
"""Help text helper class for osxphotos CLI """
|
||||||
|
|
||||||
|
import inspect
|
||||||
import io
|
import io
|
||||||
import re
|
import re
|
||||||
|
import typing as t
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import osxmetadata
|
import osxmetadata
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
|
|
||||||
|
from .click_rich_echo import rich_echo
|
||||||
|
|
||||||
from osxphotos._constants import (
|
from osxphotos._constants import (
|
||||||
EXTENDED_ATTRIBUTE_NAMES,
|
EXTENDED_ATTRIBUTE_NAMES,
|
||||||
EXTENDED_ATTRIBUTE_NAMES_QUOTED,
|
EXTENDED_ATTRIBUTE_NAMES_QUOTED,
|
||||||
@@ -32,6 +36,8 @@ __all__ = [
|
|||||||
"get_help_msg",
|
"get_help_msg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
HIGHLIGHT_COLOR = "yellow"
|
||||||
|
|
||||||
|
|
||||||
def get_help_msg(command):
|
def get_help_msg(command):
|
||||||
"""get help message for a Click command"""
|
"""get help message for a Click command"""
|
||||||
@@ -41,18 +47,131 @@ def get_help_msg(command):
|
|||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.argument("topic", default=None, required=False, nargs=1)
|
@click.argument("topic", default=None, required=False, nargs=1)
|
||||||
|
@click.argument("subtopic", default=None, required=False, nargs=1)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def help(ctx, topic, **kw):
|
def help(ctx, topic, subtopic, **kw):
|
||||||
"""Print help; for help on commands: help <command>."""
|
"""Print help; for help on commands: help <command>."""
|
||||||
if topic is None:
|
if topic is None:
|
||||||
click.echo(ctx.parent.get_help())
|
click.echo(ctx.parent.get_help())
|
||||||
return
|
return
|
||||||
elif topic in ctx.obj.group.commands:
|
|
||||||
|
if subtopic:
|
||||||
|
cmd = ctx.obj.group.commands[topic]
|
||||||
|
rich_echo(
|
||||||
|
get_subtopic_help(cmd, ctx, subtopic), width=click.HelpFormatter().width
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if topic in ctx.obj.group.commands:
|
||||||
ctx.info_name = topic
|
ctx.info_name = topic
|
||||||
click.echo_via_pager(ctx.obj.group.commands[topic].get_help(ctx))
|
click.echo_via_pager(ctx.obj.group.commands[topic].get_help(ctx))
|
||||||
|
return
|
||||||
|
|
||||||
|
# didn't find any valid help topics
|
||||||
|
click.echo(f"Invalid command: {topic}", err=True)
|
||||||
|
click.echo(ctx.parent.get_help())
|
||||||
|
|
||||||
|
|
||||||
|
def get_subtopic_help(cmd: click.Command, ctx: click.Context, subtopic: str):
|
||||||
|
"""Get help for a command including only options that match a subtopic"""
|
||||||
|
|
||||||
|
# set ctx.info_name or click prints the wrong usage str (usage for help instead of cmd)
|
||||||
|
ctx.info_name = cmd.name
|
||||||
|
usage_str = cmd.get_help(ctx)
|
||||||
|
usage_str = usage_str.partition("\n")[0]
|
||||||
|
|
||||||
|
info = cmd.to_info_dict(ctx)
|
||||||
|
help_str = info.get("help", "")
|
||||||
|
|
||||||
|
options = get_matching_options(cmd, ctx, subtopic)
|
||||||
|
|
||||||
|
# format help text and options
|
||||||
|
formatter = click.HelpFormatter()
|
||||||
|
formatter.write(usage_str)
|
||||||
|
formatter.write_paragraph()
|
||||||
|
format_help_text(help_str, formatter)
|
||||||
|
formatter.write_paragraph()
|
||||||
|
if options:
|
||||||
|
option_str = format_options_help(options, ctx, highlight=subtopic)
|
||||||
|
formatter.write(
|
||||||
|
f"Options that match '[{HIGHLIGHT_COLOR}]{subtopic}[/{HIGHLIGHT_COLOR}]':\n"
|
||||||
|
)
|
||||||
|
formatter.write_paragraph()
|
||||||
|
formatter.write(option_str)
|
||||||
else:
|
else:
|
||||||
click.echo(f"Invalid command: {topic}", err=True)
|
formatter.write(
|
||||||
click.echo(ctx.parent.get_help())
|
f"No options match '[{HIGHLIGHT_COLOR}]{subtopic}[/{HIGHLIGHT_COLOR}]'"
|
||||||
|
)
|
||||||
|
return formatter.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def get_matching_options(
|
||||||
|
command: click.Command, ctx: click.Context, topic: str
|
||||||
|
) -> t.List:
|
||||||
|
"""Get matching options for a command that contain a topic
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: click.Command
|
||||||
|
ctx: click.Context
|
||||||
|
topic: str, topic to match
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list of matching click.Option objects
|
||||||
|
|
||||||
|
"""
|
||||||
|
options = []
|
||||||
|
topic = topic.lower()
|
||||||
|
for option in command.params:
|
||||||
|
help_record = option.get_help_record(ctx)
|
||||||
|
if help_record and (topic in help_record[0] or topic in help_record[1]):
|
||||||
|
options.append(option)
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
def format_options_help(
|
||||||
|
options: t.List[click.Option], ctx: click.Context, highlight: t.Optional[str] = None
|
||||||
|
) -> str:
|
||||||
|
"""Format options help for display
|
||||||
|
|
||||||
|
Args:
|
||||||
|
options: list of click.Option objects
|
||||||
|
ctx: click.Context
|
||||||
|
highlight: str, if set, add rich highlighting to options that match highlight str
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str with formatted help
|
||||||
|
|
||||||
|
"""
|
||||||
|
formatter = click.HelpFormatter()
|
||||||
|
opt_help = [opt.get_help_record(ctx) for opt in options]
|
||||||
|
if highlight:
|
||||||
|
# convert list of tuples to list of lists
|
||||||
|
opt_help = [list(opt) for opt in opt_help]
|
||||||
|
for record in opt_help:
|
||||||
|
record[0] = re.sub(
|
||||||
|
f"({highlight})",
|
||||||
|
f"[{HIGHLIGHT_COLOR}]\\1" + f"[/{HIGHLIGHT_COLOR}]",
|
||||||
|
record[0],
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
record[1] = re.sub(
|
||||||
|
f"({highlight})",
|
||||||
|
f"[{HIGHLIGHT_COLOR}]\\1" + f"[/{HIGHLIGHT_COLOR}]",
|
||||||
|
record[1],
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
# convert back to list of tuples as that's what write_dl expects
|
||||||
|
opt_help = [tuple(opt) for opt in opt_help]
|
||||||
|
formatter.write_dl(opt_help)
|
||||||
|
return formatter.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def format_help_text(text: str, formatter: click.HelpFormatter):
|
||||||
|
text = inspect.cleandoc(text).partition("\f")[0]
|
||||||
|
formatter.write_paragraph()
|
||||||
|
|
||||||
|
with formatter.indentation():
|
||||||
|
formatter.write_text(text)
|
||||||
|
|
||||||
|
|
||||||
# TODO: The following help text could probably be done as mako template
|
# TODO: The following help text could probably be done as mako template
|
||||||
|
|||||||
Reference in New Issue
Block a user