Add argcomplete support
This commit is contained in:
+160
-103
@@ -2,12 +2,17 @@
|
||||
Command-line interface for gallery generation.
|
||||
|
||||
Provides a CLI entry point for gallery generation and config management.
|
||||
Shell autocomplete: add the following line to your .bashrc / .zshrc:
|
||||
eval "$(register-python-argcomplete gallery)"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import argcomplete
|
||||
|
||||
from gallery import generate
|
||||
from gallery.config import ConfigManager, GalleryConfig, GallerySource, default_config_path
|
||||
|
||||
@@ -22,7 +27,7 @@ Getting started:
|
||||
gallery config set paths.web_folder /path/to/your/public_html
|
||||
|
||||
2. Add one or more plot sources:
|
||||
gallery config add-source --name my_plots --path /path/to/plots
|
||||
gallery config add-source --path /path/to/plots
|
||||
|
||||
3. Generate your gallery:
|
||||
gallery generate
|
||||
@@ -31,8 +36,8 @@ Config commands:
|
||||
gallery config list Show all settings
|
||||
gallery config get <key> Get a single value (e.g. gallery.png_dpi)
|
||||
gallery config set <key> <value> Update a setting (e.g. paths.web_folder /my/web)
|
||||
gallery config add-source --name X --path P Add a plot source
|
||||
gallery config remove-source <name> Remove a plot source
|
||||
gallery config add-source --path P Add a plot source (--name defaults to dir name)
|
||||
gallery config remove-source <name> Remove a plot source
|
||||
gallery config sources List configured sources
|
||||
gallery config path Show config file location
|
||||
|
||||
@@ -43,6 +48,9 @@ Generate commands:
|
||||
gallery generate --source /path/to/plots Regenerate one source only
|
||||
gallery generate --verbose Print detailed output
|
||||
|
||||
Shell autocomplete (run once after install):
|
||||
gallery install-completion
|
||||
|
||||
Config file: {config_path}
|
||||
"""
|
||||
|
||||
@@ -50,51 +58,140 @@ Config file: {config_path}
|
||||
def _is_configured(config_path: Path) -> bool:
|
||||
"""Return True if web_folder is set to a non-empty value."""
|
||||
try:
|
||||
mgr = ConfigManager(config_path)
|
||||
web_folder = mgr.get("paths.web_folder")
|
||||
web_folder = ConfigManager(config_path).get("paths.web_folder")
|
||||
return bool(web_folder and str(web_folder).strip())
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _source_names(prefix, parsed_args, **kwargs):
|
||||
"""Autocomplete helper: return configured source names."""
|
||||
try:
|
||||
config_path = Path(parsed_args.config) if getattr(parsed_args, "config", None) else default_config_path()
|
||||
return [s["name"] for s in ConfigManager(config_path).list_sources()]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _config_keys(prefix, parsed_args, **kwargs):
|
||||
"""Autocomplete helper: return known dot-notation config keys."""
|
||||
try:
|
||||
config_path = Path(parsed_args.config) if getattr(parsed_args, "config", None) else default_config_path()
|
||||
data = ConfigManager(config_path).list_all()
|
||||
keys = []
|
||||
for section, values in data.items():
|
||||
if isinstance(values, dict):
|
||||
for k in values:
|
||||
keys.append(f"{section}.{k}")
|
||||
else:
|
||||
keys.append(section)
|
||||
return [k for k in keys if k.startswith(prefix)]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config subcommand
|
||||
# Parser construction (separated so TUI can reuse the structure)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _cmd_config(argv: list) -> int:
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="gallery config",
|
||||
description="Read and write gallery configuration",
|
||||
prog="gallery",
|
||||
description="Scientific Plot Gallery Generator",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Path to config file (default: package config)",
|
||||
metavar="FILE",
|
||||
help="Path to config file (default: package bundled config)",
|
||||
).completer = argcomplete.completers.FilesCompleter(["yaml", "yml"])
|
||||
|
||||
sub = parser.add_subparsers(dest="command", metavar="COMMAND")
|
||||
|
||||
# --- generate -----------------------------------------------------------
|
||||
gen = sub.add_parser(
|
||||
"generate",
|
||||
help="Generate the gallery",
|
||||
description="Generate scientific gallery from plot collections",
|
||||
)
|
||||
sub = parser.add_subparsers(dest="action", metavar="ACTION")
|
||||
gen.add_argument("--clean", action="store_true", help="Clean gallery directory before generation")
|
||||
gen.add_argument(
|
||||
"--source",
|
||||
type=str,
|
||||
default=None,
|
||||
metavar="DIR",
|
||||
help="Only recompute a specific source directory (name defaults to dir name)",
|
||||
).completer = argcomplete.completers.DirectoriesCompleter()
|
||||
gen.add_argument("-v", "--verbose", action="store_true", help="Print verbose output")
|
||||
|
||||
sub.add_parser("list", help="Print all config values")
|
||||
sub.add_parser("path", help="Print the resolved config file path")
|
||||
# --- config -------------------------------------------------------------
|
||||
cfg = sub.add_parser(
|
||||
"config",
|
||||
help="Read and write configuration",
|
||||
description="Read and write gallery configuration",
|
||||
)
|
||||
cfg_sub = cfg.add_subparsers(dest="action", metavar="ACTION")
|
||||
|
||||
p_get = sub.add_parser("get", help="Get a config value by key")
|
||||
p_get.add_argument("key", help="Dot-separated key, e.g. gallery.png_dpi")
|
||||
sub.add_parser("install-completion", help="Install shell tab-completion (bash/zsh)")
|
||||
|
||||
p_set = sub.add_parser("set", help="Set a config value")
|
||||
p_set.add_argument("key", help="Dot-separated key, e.g. gallery.png_dpi")
|
||||
cfg_sub.add_parser("list", help="Print all config values")
|
||||
cfg_sub.add_parser("path", help="Print the resolved config file path")
|
||||
cfg_sub.add_parser("sources", help="List all configured sources")
|
||||
|
||||
p_get = cfg_sub.add_parser("get", help="Get a config value")
|
||||
p_get.add_argument("key", help="Dot-separated key, e.g. gallery.png_dpi").completer = _config_keys
|
||||
|
||||
p_set = cfg_sub.add_parser("set", help="Set a config value")
|
||||
p_set.add_argument("key", help="Dot-separated key, e.g. gallery.png_dpi").completer = _config_keys
|
||||
p_set.add_argument("value", help="New value (YAML-parsed: use true/false for bools)")
|
||||
|
||||
p_add = sub.add_parser("add-source", help="Add a plot source")
|
||||
p_add = cfg_sub.add_parser("add-source", help="Add a plot source")
|
||||
p_add.add_argument("--name", default=None, help="Source name (default: bottom-level directory name)")
|
||||
p_add.add_argument("--path", required=True, help="Path to source directory")
|
||||
p_add.add_argument("--path", required=True, metavar="DIR",
|
||||
help="Path to source directory").completer = argcomplete.completers.DirectoriesCompleter()
|
||||
|
||||
p_rm = sub.add_parser("remove-source", help="Remove a plot source by name")
|
||||
p_rm.add_argument("name", help="Source name to remove")
|
||||
p_rm = cfg_sub.add_parser("remove-source", help="Remove a plot source by name")
|
||||
p_rm.add_argument("name", help="Source name to remove").completer = _source_names
|
||||
|
||||
sub.add_parser("sources", help="List all configured sources")
|
||||
return parser
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Command handlers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _run_generate(args: argparse.Namespace) -> int:
|
||||
config_path = Path(args.config) if args.config else default_config_path()
|
||||
try:
|
||||
config = GalleryConfig.from_yaml(config_path)
|
||||
|
||||
source_to_update = None
|
||||
if args.source:
|
||||
source_path = Path(args.source).resolve()
|
||||
matching = next((s for s in config.sources if Path(s.path).resolve() == source_path), None)
|
||||
if matching is None:
|
||||
source_to_update = GallerySource(name=source_path.name, path=source_path)
|
||||
config.sources.append(source_to_update)
|
||||
if args.verbose:
|
||||
print(f"Source {args.source} not in config. Adding temporarily as '{source_path.name}'")
|
||||
else:
|
||||
source_to_update = matching
|
||||
|
||||
success = generate(config=config, clean_first=args.clean, verbose=args.verbose,
|
||||
source_to_update=source_to_update)
|
||||
return 0 if success else 1
|
||||
|
||||
except (FileNotFoundError, ValueError) as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
def _run_config(args: argparse.Namespace) -> int:
|
||||
config_path = Path(args.config) if args.config else default_config_path()
|
||||
mgr = ConfigManager(config_path)
|
||||
|
||||
@@ -105,10 +202,17 @@ def _cmd_config(argv: list) -> int:
|
||||
import yaml
|
||||
print(yaml.dump(mgr.list_all(), default_flow_style=False).rstrip())
|
||||
|
||||
elif args.action == "sources":
|
||||
sources = mgr.list_sources()
|
||||
if not sources:
|
||||
print("No sources configured.")
|
||||
else:
|
||||
for s in sources:
|
||||
print(f" {s['name']}: {s['path']}")
|
||||
|
||||
elif args.action == "get":
|
||||
try:
|
||||
value = mgr.get(args.key)
|
||||
print(value)
|
||||
print(mgr.get(args.key))
|
||||
except KeyError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
@@ -135,94 +239,41 @@ def _cmd_config(argv: list) -> int:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
elif args.action == "sources":
|
||||
sources = mgr.list_sources()
|
||||
if not sources:
|
||||
print("No sources configured.")
|
||||
else:
|
||||
for s in sources:
|
||||
print(f" {s['name']}: {s['path']}")
|
||||
|
||||
else:
|
||||
parser.print_help()
|
||||
# `gallery config` with no action → show config help
|
||||
build_parser().parse_args(["config", "--help"])
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Generate subcommand (existing behaviour)
|
||||
# install-completion handler
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _cmd_generate(argv: list) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="gallery generate",
|
||||
description="Generate scientific gallery from plot collections",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
gallery generate # Generate using package config
|
||||
gallery generate --config myconfig.yaml # Use custom config file
|
||||
gallery generate --clean # Clean and regenerate
|
||||
gallery generate --source /path/to/plots # Generate only specific source
|
||||
""",
|
||||
)
|
||||
_COMPLETION_LINE = 'eval "$(register-python-argcomplete gallery)"'
|
||||
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Path to config.yaml (default: package bundled config)",
|
||||
)
|
||||
parser.add_argument("--clean", action="store_true", help="Clean gallery directory before generation")
|
||||
parser.add_argument(
|
||||
"--source",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Only recompute a specific source directory",
|
||||
)
|
||||
parser.add_argument("-v", "--verbose", action="store_true", help="Print verbose output")
|
||||
_SHELL_RC = {
|
||||
"zsh": ".zshrc",
|
||||
"bash": ".bashrc",
|
||||
"fish": ".config/fish/config.fish",
|
||||
}
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
config_path = Path(args.config) if args.config else default_config_path()
|
||||
def _run_install_completion() -> int:
|
||||
shell = Path(os.environ.get("SHELL", "")).name # e.g. "bash", "zsh"
|
||||
rc_name = _SHELL_RC.get(shell, ".bashrc")
|
||||
rc = Path.home() / rc_name
|
||||
|
||||
try:
|
||||
config = GalleryConfig.from_yaml(config_path)
|
||||
if rc.exists() and _COMPLETION_LINE in rc.read_text():
|
||||
print(f"Shell completion already configured in {rc}")
|
||||
return 0
|
||||
|
||||
source_to_update = None
|
||||
if args.source:
|
||||
source_path = Path(args.source).resolve()
|
||||
matching_source = None
|
||||
for source in config.sources:
|
||||
if Path(source.path).resolve() == source_path:
|
||||
matching_source = source
|
||||
break
|
||||
if matching_source is None:
|
||||
source_name = source_path.name
|
||||
source_to_update = GallerySource(name=source_name, path=source_path)
|
||||
config.sources.append(source_to_update)
|
||||
if args.verbose:
|
||||
print(f"Source {args.source} not in config. Adding temporarily as '{source_name}'")
|
||||
else:
|
||||
source_to_update = matching_source
|
||||
with open(rc, "a") as f:
|
||||
f.write(f"\n# gallery shell completion\n{_COMPLETION_LINE}\n")
|
||||
|
||||
success = generate(
|
||||
config=config,
|
||||
clean_first=args.clean,
|
||||
verbose=args.verbose,
|
||||
source_to_update=source_to_update,
|
||||
)
|
||||
return 0 if success else 1
|
||||
|
||||
except FileNotFoundError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
except ValueError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
print(f"Shell completion installed to {rc}")
|
||||
print(f"Reload with: source {rc}")
|
||||
return 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -230,10 +281,16 @@ Examples:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "config":
|
||||
sys.exit(_cmd_config(sys.argv[2:]))
|
||||
elif len(sys.argv) > 1 and sys.argv[1] == "generate":
|
||||
sys.exit(_cmd_generate(sys.argv[2:]))
|
||||
parser = build_parser()
|
||||
argcomplete.autocomplete(parser) # no-op when not completing; exits during completion
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "generate":
|
||||
sys.exit(_run_generate(args))
|
||||
elif args.command == "config":
|
||||
sys.exit(_run_config(args))
|
||||
elif args.command == "install-completion":
|
||||
sys.exit(_run_install_completion())
|
||||
else:
|
||||
print(_WELCOME.format(config_path=default_config_path()))
|
||||
sys.exit(0)
|
||||
|
||||
@@ -36,6 +36,7 @@ classifiers = [
|
||||
dependencies = [
|
||||
"Jinja2>=3.0.0",
|
||||
"PyYAML>=5.0",
|
||||
"argcomplete>=3.0",
|
||||
"pytest",
|
||||
]
|
||||
|
||||
|
||||
@@ -10,6 +10,15 @@ resolution-markers = [
|
||||
"python_full_version < '3.9'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argcomplete"
|
||||
version = "3.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "astroid"
|
||||
version = "3.2.4"
|
||||
@@ -272,7 +281,7 @@ version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" },
|
||||
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||
wheels = [
|
||||
@@ -281,9 +290,10 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "gallery"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "argcomplete" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
|
||||
@@ -309,6 +319,7 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "argcomplete", specifier = ">=3.0" },
|
||||
{ name = "black", marker = "extra == 'dev'", specifier = ">=22.0" },
|
||||
{ name = "jinja2", specifier = ">=3.0.0" },
|
||||
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=0.900" },
|
||||
|
||||
Reference in New Issue
Block a user