Skip to content

Commit

Permalink
Merge pull request #33 from DanCardin/dc/central-command-collection
Browse files Browse the repository at this point in the history
  • Loading branch information
DanCardin authored Oct 22, 2023
2 parents aaad54f + 4e9c05f commit df0467d
Show file tree
Hide file tree
Showing 12 changed files with 159 additions and 233 deletions.
16 changes: 8 additions & 8 deletions docs/source/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ and redefining argparse's objects with slight, necessary modifications.

## Cappa backend

Some "headline" features you get when using the cappa backend:
Ihe main "headline" feature you get when using the cappa backend is: Automatic
[completion](./completion.md) support.

- Automatic [completion](./completion.md) support
- Automatic rich-powered, colored help-text formatting
Is roughly 1/3 the size in LOC, given that much of the featureset and
flexibility of argparse are unused and a source of contention with cappa's
design. It's going to be easier to support arbitrary features with the native
parser than with an external backend.

But generally, it's going to be easier to support arbitrary features with a
custom parser than with an external backend.

This backend is not currently the default due to its relative infancy. It will,
however, **become** the default before 1.0 of the library.
This backend is not currently the default, however it **will** become the
default before 1.0 of the library.
5 changes: 0 additions & 5 deletions docs/source/rich.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
# Rich

```{note}
In order to get rich "support" (essentially colored help text) when using the
argparse backend, you should separately depend upon the "rich-argparse" dependency.
```

## Color

Colored output, including help-text generation, is automatically enabled.
Expand Down
62 changes: 22 additions & 40 deletions src/cappa/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from cappa.arg import Arg, ArgAction
from cappa.command import Command, Subcommand
from cappa.help import create_help_arg, create_version_arg, generate_arg_groups
from cappa.help import format_help, generate_arg_groups
from cappa.invoke import fullfill_deps
from cappa.output import Exit, HelpExit
from cappa.parser import RawOption, Value
Expand Down Expand Up @@ -42,10 +42,6 @@ def __init__(self, metavar=None, **kwargs):
self.metavar = metavar
super().__init__(**kwargs)

def __call__(self, parser, namespace, values, option_string=None):
parser.print_help()
raise HelpExit("", code=0)


class _VersionAction(argparse._VersionAction):
def __init__(self, metavar=None, **kwargs):
Expand All @@ -72,8 +68,9 @@ def __init__(self, metavar=None, **kwargs):


class ArgumentParser(argparse.ArgumentParser):
def __init__(self, *args, **kwargs):
def __init__(self, *args, command: Command, **kwargs):
super().__init__(*args, **kwargs)
self.command = command

self.register("action", "store_true", _StoreTrueAction)
self.register("action", "store_false", _StoreFalseAction)
Expand All @@ -84,6 +81,9 @@ def __init__(self, *args, **kwargs):
def exit(self, status=0, message=None):
raise Exit(message, code=status)

def print_help(self):
raise HelpExit(format_help(self.command, self.prog))


class BooleanOptionalAction(argparse.Action):
"""Simplified backport of same-named class from 3.9 onward.
Expand Down Expand Up @@ -143,19 +143,19 @@ def __setattr__(self, name, value):


def backend(
command: Command[T],
argv: list[str],
color: bool = True,
version: str | Arg | None = None,
help: bool | Arg = True,
completion: bool | Arg = True,
command: Command[T], argv: list[str]
) -> tuple[typing.Any, Command[T], dict[str, typing.Any]]:
version = create_version_arg(version)
command.add_meta_actions(help=create_help_arg(help), version=version)
parser = create_parser(command)

parser = create_parser(command, color=color)
if version:
try:
version = next(
a
for a in command.arguments
if isinstance(a, Arg) and a.action is ArgAction.version
)
parser.version = version.name # type: ignore
except StopIteration:
pass

ns = Nestedspace()

Expand All @@ -170,20 +170,17 @@ def backend(
return parser, command, result


def create_parser(
command: Command,
color: bool = True,
) -> argparse.ArgumentParser:
def create_parser(command: Command) -> argparse.ArgumentParser:
kwargs: dict[str, typing.Any] = {}
if sys.version_info >= (3, 9): # pragma: no cover
kwargs["exit_on_error"] = False

parser = ArgumentParser(
command=command,
prog=command.real_name(),
description=join_help(command.help, command.description),
allow_abbrev=False,
add_help=False,
formatter_class=choose_help_formatter(color=color),
**kwargs,
)
parser.set_defaults(__command__=command)
Expand All @@ -193,22 +190,6 @@ def create_parser(
return parser


def choose_help_formatter(color: bool = True):
help_formatter: type[
argparse.HelpFormatter
] = argparse.ArgumentDefaultsHelpFormatter

if color is True:
try: # pragma: no cover
from rich_argparse import ArgumentDefaultsRichHelpFormatter

help_formatter = ArgumentDefaultsRichHelpFormatter
except ImportError: # pragma: no cover
pass

return help_formatter


def add_arguments(parser: argparse.ArgumentParser, command: Command, dest_prefix=""):
arg_groups = generate_arg_groups(command, include_hidden=True)
for group_name, args in arg_groups:
Expand Down Expand Up @@ -242,7 +223,7 @@ def add_argument(

is_positional = not names

num_args = backend_num_args(arg.num_args, is_positional)
num_args = backend_num_args(arg.num_args)

kwargs: dict[str, typing.Any] = {
"dest": dest_prefix + name,
Expand Down Expand Up @@ -285,7 +266,6 @@ def add_subcommands(
subparsers = parser.add_subparsers(
title=group,
required=assert_type(subcommands.required, bool),
description=subcommands.help,
parser_class=ArgumentParser,
)

Expand All @@ -297,6 +277,8 @@ def add_subcommands(
description=subcommand.description,
formatter_class=parser.formatter_class,
add_help=False,
command=subcommand, # type: ignore
prog=f"{parser.prog} {subcommand.real_name()}",
)
subparser.set_defaults(
__command__=subcommand, **{nested_dest_prefix + "__name__": name}
Expand All @@ -309,7 +291,7 @@ def add_subcommands(
)


def backend_num_args(num_args: int | None, is_positional) -> int | str | None:
def backend_num_args(num_args: int | None) -> int | str | None:
if num_args is None or num_args == 1:
return None

Expand Down
84 changes: 66 additions & 18 deletions src/cappa/base.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
from __future__ import annotations

import dataclasses
import os
import typing

from rich.theme import Theme
from typing_extensions import dataclass_transform

from cappa import argparse
from cappa.class_inspect import detect
from cappa.command import Command
from cappa.help import (
create_completion_arg,
create_help_arg,
create_version_arg,
)
from cappa.invoke import invoke_callable
from cappa.output import Output

Expand Down Expand Up @@ -39,9 +46,7 @@ def parse(
necessary when testing.
backend: A function used to perform the underlying parsing and return a raw
parsed state. This defaults to constructing built-in function using argparse.
color: Whether to output in color. Note by default this will only affect the native
`cappa.backend` backend. If using the argparse backend, `rich-argparse` must be
separately installed.
color: Whether to output in color.
version: If a string is supplied, adds a -v/--version flag which returns the
given string as the version. If an `Arg` is supplied, uses the `name`/`short`/`long`/`help`
fields to add a corresponding version argument. Note the `name` is assumed to **be**
Expand All @@ -53,20 +58,26 @@ def parse(
the argument's behavior.
theme: Optional rich theme to customized output formatting.
"""
command = Command.get(obj)
if backend is None: # pragma: no cover
from cappa import argparse

backend = argparse.backend

command: Command = collect(
obj,
help=help,
version=version,
completion=completion,
color=color,
backend=backend,
)
output = Output.from_theme(theme)
_, _, instance = Command.parse_command(
command,
argv=argv,
backend=backend,
color=color,
version=version,
help=help,
output=output,
completion=completion,
)

return instance


Expand Down Expand Up @@ -95,9 +106,7 @@ def invoke(
necessary when testing.
backend: A function used to perform the underlying parsing and return a raw
parsed state. This defaults to constructing built-in function using argparse.
color: Whether to output in color. Note by default this will only affect the native
`cappa.backend` backend. If using the argparse backend, `rich-argparse` must be
separately installed.
color: Whether to output in color.
version: If a string is supplied, adds a -v/--version flag which returns the
given string as the version. If an `Arg` is supplied, uses the `name`/`short`/`long`/`help`
fields to add a corresponding version argument. Note the `name` is assumed to **be**
Expand All @@ -109,20 +118,26 @@ def invoke(
the argument's behavior.
theme: Optional rich theme to customized output formatting.
"""
command: Command = Command.get(obj)
if backend is None: # pragma: no cover
from cappa import argparse

backend = argparse.backend

command: Command = collect(
obj,
help=help,
version=version,
completion=completion,
color=color,
backend=backend,
)
output = Output.from_theme(theme)
command, parsed_command, instance = Command.parse_command(
command,
argv=argv,
backend=backend,
color=color,
version=version,
help=help,
output=output,
completion=completion,
)

return invoke_callable(command, parsed_command, instance, output=output, deps=deps)


Expand Down Expand Up @@ -168,3 +183,36 @@ def wrapper(_decorated_cls):
if _cls is not None:
return wrapper(_cls)
return wrapper


def collect(
obj: type,
*,
backend: typing.Callable | None = None,
version: str | Arg | None = None,
help: bool | Arg = True,
completion: bool | Arg = True,
color: bool = True,
):
if not color:
# XXX: This should probably be doing something with the Output rather than
# mutating global state.
os.environ["NO_COLOR"] = "1"

if backend is None: # pragma: no cover
backend = argparse.backend

command: Command = Command.get(obj)
command = Command.collect(command)

if backend is argparse.backend:
completion = False

help_arg = create_help_arg(help)
version_arg = create_version_arg(version)
completion_arg = create_completion_arg(completion)

command.add_meta_actions(
help=help_arg, version=version_arg, completion=completion_arg
)
return command
24 changes: 3 additions & 21 deletions src/cappa/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,33 +121,15 @@ def parse_command(
cls,
command: Command[T],
*,
argv: list[str] | None = None,
output: Output,
backend: typing.Callable | None = None,
color: bool = True,
version: str | Arg | None = None,
help: bool | Arg = True,
completion: bool | Arg = True,
backend: typing.Callable,
argv: list[str] | None = None,
) -> tuple[Command, Command[T], T]:
if argv is None: # pragma: no cover
argv = sys.argv[1:]

command = cls.collect(command)

if backend is None: # pragma: no cover
from cappa import argparse

backend = argparse.backend

try:
_, parsed_command, parsed_args = backend(
command,
argv,
color=color,
version=version,
help=help,
completion=completion,
)
_, parsed_command, parsed_args = backend(command, argv)
result = command.map_result(command, parsed_args)
except Exit as e:
output.exit(e)
Expand Down
Loading

0 comments on commit df0467d

Please sign in to comment.