Skip to content

Commit

Permalink
fix: CI back to 3.9.
Browse files Browse the repository at this point in the history
  • Loading branch information
DanCardin committed Sep 20, 2023
1 parent 055ee5a commit 5b74ded
Show file tree
Hide file tree
Showing 39 changed files with 393 additions and 281 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ API).
from __future__ import annotations

import cappa
from typing import Annotated
from typing_extensions import Annotated
from dataclasses import dataclass


Expand Down
2 changes: 1 addition & 1 deletion docs/docs/command.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ Then, in order to cause the function to be invoked, you would call

```python
from dataclasses import dataclass
from typing import Annotated
from typing_extensions import Annotated

import cappa

Expand Down
2 changes: 1 addition & 1 deletion docs/source/invoke.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ heirarchy.
```

```python
from typing import Annotated
from typing_extensions import Annotated
from cappa import Dep

# No more dependencies, ends the dependency chain.
Expand Down
20 changes: 11 additions & 9 deletions src/cappa/annotation.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from __future__ import annotations

import enum
import types
import typing

from typing_inspect import is_literal_type, is_union_type
from typing_inspect import is_literal_type

from cappa.typing import render_type
from cappa.typing import is_none_type, is_subclass, is_union_type, render_type


def detect_choices(origin: type, type_args: tuple[type, ...]) -> list[str] | None:
if isinstance(origin, type) and issubclass(origin, enum.Enum):
if is_subclass(origin, enum.Enum):
assert issubclass(origin, enum.Enum)
return [v.value for v in origin]

if is_union_type(origin):
Expand All @@ -32,7 +34,7 @@ def literal_mapper(value):

return literal_mapper

if is_union_type(type_) or issubclass(type_, types.UnionType):
if is_union_type(type_):
mappers: list[typing.Callable] = [
generate_map_result(t, typing.get_args(t))
for t in sorted(type_args, key=type_priority_key)
Expand All @@ -51,10 +53,10 @@ def union_mapper(value):

return union_mapper

if issubclass(type_, (str, bool, int, float)):
if is_subclass(type_, (str, bool, int, float)):
return type_

if issubclass(type_, types.NoneType):
if is_none_type(type_):

def map_none(value):
if value is None:
Expand All @@ -64,7 +66,7 @@ def map_none(value):

return map_none

if issubclass(type_, list):
if is_subclass(type_, list):
assert type_args

inner_type = type_args[0]
Expand All @@ -75,7 +77,7 @@ def list_mapper(value: list):

return list_mapper

if issubclass(type_, tuple):
if is_subclass(type_, tuple):
assert type_args

if len(type_args) == 2 and type_args[1] == ...:
Expand Down
10 changes: 5 additions & 5 deletions src/cappa/arg.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from cappa.annotation import detect_choices, generate_map_result
from cappa.class_inspect import Field, extract_dataclass_metadata
from cappa.subcommand import Subcommand
from cappa.typing import MISSING, T, find_type_annotation, missing
from cappa.typing import MISSING, T, find_type_annotation, is_subclass, missing


@enum.unique
Expand Down Expand Up @@ -49,14 +49,14 @@ class Arg(Generic[T]):
class's value has a default. By setting this, you can force a particular value.
"""

name: str | MISSING = missing
short: bool | str = False
long: bool | str = False
count: bool = False
default: T | None | MISSING = ...
help: str | None = None
parse: Callable[[str], T] | None = None

name: str | MISSING = missing
action: ArgAction = ArgAction.set
num_args: int | None = None
map_result: Callable | None = None
Expand Down Expand Up @@ -108,7 +108,7 @@ def collect(

# Coerce raw `bool` into flags by default
if not is_union_type(origin):
if issubclass(origin, bool):
if is_subclass(origin, bool):
if not long:
long = True

Expand All @@ -117,13 +117,13 @@ def collect(
is_positional = not arg.short and not long

if arg.parse is None and not is_union_type(origin):
if issubclass(origin, list):
if is_subclass(origin, list):
if is_positional and arg.num_args is None:
kwargs["num_args"] = -1
else:
kwargs["action"] = ArgAction.append

if issubclass(origin, tuple):
if is_subclass(origin, tuple):
if len(type_args) == 2 and type_args[1] == ...:
if is_positional and arg.num_args is None:
kwargs["num_args"] = -1
Expand Down
85 changes: 79 additions & 6 deletions src/cappa/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from typing_extensions import assert_never

from cappa.arg import Arg
from cappa.arg import Arg, ArgAction
from cappa.command import Command
from cappa.command_def import CommandDefinition, Subcommands
from cappa.typing import assert_not_missing, assert_type
Expand All @@ -16,6 +16,23 @@
except ImportError: # pragma: no cover
pass

if sys.version_info < (3, 9): # pragma: no cover
# Backport https://github.com/python/cpython/pull/3680
original_get_action_name = argparse._get_action_name

def _get_action_name(
argument: argparse.Action | None,
) -> str | None: # pragma: no cover
name = original_get_action_name(argument)

assert argument
if name is None and argument.choices:
return "{" + ",".join(argument.choices) + "}"

return name

argparse._get_action_name = _get_action_name


T = typing.TypeVar("T")

Expand Down Expand Up @@ -55,11 +72,14 @@ def render(
argv: list[str],
exit_with=None,
color: bool = True,
version: str | Arg | None = None,
help: bool | Arg = True,
) -> tuple[Command[T], dict[str, typing.Any]]:
if exit_with is None:
exit_with = sys_exit

parser = create_parser(command_def, exit_with, color=color)
add_help_group(parser, version=version, help=help)

ns = Nestedspace()

Expand All @@ -77,22 +97,64 @@ def render(


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

parser = ArgumentParser(
prog=command_def.command.name,
description=join_help(command_def.title, command_def.description),
exit_on_error=False,
exit_with=exit_with,
allow_abbrev=False,
add_help=False,
formatter_class=choose_help_formatter(color=color),
**kwargs,
)
parser.set_defaults(__command__=command_def.command)

add_arguments(parser, command_def)

return parser


def add_help_group(
parser: argparse.ArgumentParser,
version: str | Arg | None = None,
help: bool | Arg = True,
):
if not version and not help:
return

help_group = parser.add_argument_group("help")
if version:
if isinstance(version, str):
arg: Arg = Arg(
version, short="-v", long="--version", help="Show the version and exit."
)
else:
arg = version

add_argument(help_group, arg, version=arg.name, action=argparse._VersionAction)

if help:
if isinstance(help, bool):
arg = Arg(
name="help",
short="-h",
long="--help",
help="Show this message and exit.",
)
else:
arg = help
arg.name = "help"

add_argument(help_group, arg, action=argparse._HelpAction)


def choose_help_formatter(color: bool = True):
help_formatter: type[
argparse.HelpFormatter
Expand Down Expand Up @@ -121,7 +183,12 @@ def add_arguments(
assert_never(arg)


def add_argument(parser: argparse.ArgumentParser, arg: Arg, dest_prefix=""):
def add_argument(
parser: argparse.ArgumentParser | argparse._ArgumentGroup,
arg: Arg,
dest_prefix="",
**extra_kwargs,
):
name: str = assert_not_missing(arg.name)

names: list[str] = []
Expand All @@ -146,18 +213,24 @@ def add_argument(parser: argparse.ArgumentParser, arg: Arg, dest_prefix=""):
if is_positional:
kwargs["metavar"] = name

if arg.required and names:
if not is_positional and arg.required:
kwargs["required"] = arg.required

if arg.default is not ...:
kwargs["default"] = arg.default

if arg.action is not arg.action.store_true:
if (
isinstance(arg.action, ArgAction)
and arg.action is not arg.action.store_true
and num_args is not None
):
kwargs["nargs"] = num_args

if arg.choices:
kwargs["choices"] = arg.choices

kwargs.update(extra_kwargs)

parser.add_argument(*names, **kwargs)


Expand Down
38 changes: 32 additions & 6 deletions src/cappa/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from __future__ import annotations

import sys
import typing

from cappa.arg import Arg
from cappa.command import Command
from cappa.command_def import CommandDefinition
from cappa.invoke import invoke_callable

T = typing.TypeVar("T")

Expand All @@ -14,6 +18,8 @@ def parse(
render: typing.Callable | None = None,
exit_with=None,
color: bool = True,
version: str | Arg | None = None,
help: bool | Arg = True,
) -> T:
"""Parse the command, returning an instance of `obj`.
Expand All @@ -29,18 +35,26 @@ def parse(
exit_with: Used when parsing fails, to raise/indicate failure. By default, exits
with SystemExit to kill the process.
color: Whether to output in color, if the `color` extra is installed.
version: If a string is supplied, adds a -v/--version flag which returns the
given version string. If an `Arg` is supplied, uses the `name`/`short`/`long`/`help`
fields to add a corresponding version argument.
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.
"""
if argv is None: # pragma: no cover
argv = sys.argv

instance = Command.get(obj)
return CommandDefinition.parse(
instance,
command = Command.get(obj)
_, instance = CommandDefinition.parse_command(
command,
argv=argv,
render=render,
exit_with=exit_with,
color=color,
version=version,
help=help,
)
return instance # type: ignore


def invoke(
Expand All @@ -50,6 +64,8 @@ def invoke(
render: typing.Callable | None = None,
exit_with=None,
color: bool = True,
version: str | Arg | None = None,
help: bool | Arg = True,
):
"""Parse the command, and invoke the selected command or subcommand.
Expand All @@ -65,15 +81,25 @@ def invoke(
exit_with: Used when parsing fails, to raise/indicate failure. By default, exits
with SystemExit to kill the process.
color: Whether to output in color, if the `color` extra is installed.
version: If a string is supplied, adds a -v/--version flag which returns the
given version string. If an `Arg` is supplied, uses the `name`/`short`/`long`
fields to add a corresponding version argument.
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.
"""
if argv is None: # pragma: no cover
argv = sys.argv

instance: Command = Command.get(obj)
return CommandDefinition.invoke(
instance,
command: Command = Command.get(obj)

parsed_command, instance = CommandDefinition.parse_command(
command,
argv=argv,
render=render,
exit_with=exit_with,
color=color,
version=version,
help=help,
)

return invoke_callable(parsed_command, instance)
Loading

0 comments on commit 5b74ded

Please sign in to comment.