Skip to content

Commit

Permalink
Merge pull request #36 from DanCardin/dc/idempotent-collection
Browse files Browse the repository at this point in the history
  • Loading branch information
DanCardin authored Oct 22, 2023
2 parents df0467d + 241117c commit e3c039b
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 38 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 0.8.9

- Avoid mutating command when adding meta arguments
- Avoid setting global NO_COLOR env var when disabling color

## 0.8.8

- Clean up help text output formatting
- Show rich-style help text when using argparse backend

## 0.8.7

- Allow defining custom callable as an `action`.
Expand Down
2 changes: 1 addition & 1 deletion docs/source/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

```{eval-rst}
.. autoapimodule:: cappa
:members: parse, invoke, command, Command, Subcommand, Dep, Arg, ArgAction, Exit, Env, Completion, Output
:members: parse, invoke, collect, command, Command, Subcommand, Dep, Arg, ArgAction, Exit, Env, Completion, Output
```

```{eval-rst}
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "cappa"
version = "0.8.8"
version = "0.8.9"
description = "Declarative CLI argument parser."

repository = "https://github.com/dancardin/cappa"
Expand Down
3 changes: 2 additions & 1 deletion src/cappa/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from cappa.base import command, invoke, parse
from cappa.base import collect, command, invoke, parse
from cappa.command import Command
from cappa.completion.types import Completion
from cappa.env import Env
Expand Down Expand Up @@ -27,6 +27,7 @@
"Subcommands",
"argparse",
"backend",
"collect",
"command",
"invoke",
"parse",
Expand Down
45 changes: 26 additions & 19 deletions src/cappa/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

import dataclasses
import os
import typing

from rich.theme import Theme
Expand All @@ -25,7 +24,7 @@


def parse(
obj: type[T],
obj: type[T] | Command[T],
*,
argv: list[str] | None = None,
backend: typing.Callable | None = None,
Expand Down Expand Up @@ -63,15 +62,14 @@ def parse(

backend = argparse.backend

command: Command = collect(
command: Command[T] = collect(
obj,
help=help,
version=version,
completion=completion,
color=color,
backend=backend,
)
output = Output.from_theme(theme)
output = Output.from_theme(theme, color=color)
_, _, instance = Command.parse_command(
command,
argv=argv,
Expand All @@ -82,7 +80,7 @@ def parse(


def invoke(
obj: type,
obj: type[T] | Command[T],
*,
deps: typing.Sequence[typing.Callable] | None = None,
argv: list[str] | None = None,
Expand All @@ -92,7 +90,7 @@ def invoke(
help: bool | Arg = True,
completion: bool | Arg = True,
theme: Theme | None = None,
):
) -> T:
"""Parse the command, and invoke the selected command or subcommand.
In the event that a subcommand is selected, only the selected subcommand
Expand Down Expand Up @@ -128,10 +126,9 @@ def invoke(
help=help,
version=version,
completion=completion,
color=color,
backend=backend,
)
output = Output.from_theme(theme)
output = Output.from_theme(theme, color=color)
command, parsed_command, instance = Command.parse_command(
command,
argv=argv,
Expand Down Expand Up @@ -186,23 +183,34 @@ def wrapper(_decorated_cls):


def collect(
obj: type,
obj: type[T] | Command[T],
*,
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"
) -> Command[T]:
"""Retrieve the `Command` object from a cappa-capable source class.
Arguments:
obj: A class which can represent a CLI command chain.
backend: A function used to perform the underlying parsing and return a raw
parsed state. This defaults to constructing built-in function using argparse.
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**
the CLI's version, e.x. `Arg('1.2.3', help="Prints the version")`.
help: If `True` (default to True), adds a -h/--help flag. If an `Arg` is supplied,
uses the `short`/`long`/`help` fields to add a corresponding help argument.
completion: Enables completion when using the cappa `backend` option. If `True`
(default to True), adds a --completion flag. An `Arg` can be supplied to customize
the argument's behavior.
color: Whether to output in color.
"""
if backend is None: # pragma: no cover
backend = argparse.backend

command: Command = Command.get(obj)
command: Command[T] = Command.get(obj)
command = Command.collect(command)

if backend is argparse.backend:
Expand All @@ -212,7 +220,6 @@ def collect(
version_arg = create_version_arg(version)
completion_arg = create_completion_arg(completion)

command.add_meta_actions(
return command.add_meta_actions(
help=help_arg, version=version_arg, completion=completion_arg
)
return command
39 changes: 26 additions & 13 deletions src/cappa/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from collections.abc import Callable

import docstring_parser
from typing_extensions import Self, get_type_hints
from typing_extensions import get_type_hints

from cappa import class_inspect
from cappa.arg import Arg, ArgAction
Expand Down Expand Up @@ -56,12 +56,14 @@ class Command(typing.Generic[T]):
description: str | None = None
invoke: Callable | str | None = None

_collected: bool = False

@classmethod
def get(cls, obj: typing.Type[T]) -> Self:
def get(cls, obj: typing.Type[T] | Command[T]) -> Command[T]:
if isinstance(obj, cls):
return obj

return getattr(obj, "__cappa__", cls(obj))
return getattr(obj, "__cappa__", cls(obj)) # type: ignore

def real_name(self) -> str:
if self.name is not None:
Expand All @@ -73,7 +75,7 @@ def real_name(self) -> str:
return re.sub(r"(?<!^)(?=[A-Z])", "-", cls_name).lower()

@classmethod
def collect(cls, command: Command):
def collect(cls, command: Command[T]) -> Command[T]:
kwargs: CommandArgs = {}
arg_help_map = {}

Expand Down Expand Up @@ -186,18 +188,29 @@ def add_meta_actions(
version: Arg | None = None,
completion: Arg | None = None,
):
if help:
for arg in self.arguments:
if isinstance(arg, Subcommand):
for option in arg.options.values():
option.add_meta_actions(help)
if self._collected:
return self

arguments = [
dataclasses.replace(
arg,
options={
name: option.add_meta_actions(help)
for name, option in arg.options.items()
},
)
if help and isinstance(arg, Subcommand)
else arg
for arg in self.arguments
]

self.arguments.append(help)
if help:
arguments.append(help)
if version:
self.arguments.append(version)
arguments.append(version)
if completion:
self.arguments.append(completion)
return self
arguments.append(completion)
return dataclasses.replace(self, arguments=arguments, _collected=True)


H = typing.TypeVar("H", covariant=True)
Expand Down
8 changes: 5 additions & 3 deletions src/cappa/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ class Output:
)

@classmethod
def from_theme(cls, theme: Theme | None = None):
output_console = Console(file=sys.stdout, theme=theme or cls.theme)
error_console = Console(file=sys.stderr, theme=theme or cls.theme)
def from_theme(cls, theme: Theme | None = None, color: bool = True):
no_color = None if color else True
theme = theme or cls.theme
output_console = Console(file=sys.stdout, theme=theme, no_color=no_color)
error_console = Console(file=sys.stderr, theme=theme, no_color=no_color)
return cls(output_console, error_console)

def exit(self, e: Exit):
Expand Down
12 changes: 12 additions & 0 deletions tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,15 @@ class Example:

out = capsys.readouterr().out
assert "usage: example" in out.lower()


@backends
def test_collect_composes_with_parse(backend):
@dataclass
class Example:
...

command = cappa.collect(Example)
result = cappa.parse(command, argv=[], backend=backend)

assert result == Example()

0 comments on commit e3c039b

Please sign in to comment.