Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## 0.31.0
- feat: Apply default parsing automatically, with Arg.parse_inference == False to disable.

## 0.30.4
- fix: Use of a synchronous context manager inside an `invoke_async` invoke context.
- fix: Stripped type aliases losing e.g. Dep metadata.
Expand Down
9 changes: 7 additions & 2 deletions docs/source/arg.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,9 +527,14 @@ Note cappa itself contains a number of component `parse_*` functions inside the
module, which can be used in combination with your own custom `parse` functions.
```

### `default_parse`
### `default_parse`/`Arg.parse_inference

A function [cappa.default_parse](cappa.default_parse) is exposed
A function [cappa.default_parse](cappa.default_parse) is exposed and **can** be directly referenced
in parse function sequences. However by default the `default_parse` function is applied automatically
at the end of the user-provided parse function/sequence.

With that said, if the default behavior is undesirable, one can set `Arg(parse_inference=False)` to
**only** execute the user provided parser function(s).

(parsing-json)=
### Parsing JSON
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "cappa"
version = "0.30.4"
version = "0.31.0"
description = "Declarative CLI argument parser."

urls = { repository = "https://github.com/dancardin/cappa" }
Expand Down
25 changes: 16 additions & 9 deletions src/cappa/arg.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ class Arg(Generic[T]):
return the parsed value. This can be either a single function, or a sequence of functions.
If a sequence is provided, they will be called (in order) with their return value
chained into the next function in the sequence.
parse_inference: Whether to include the default inferred parser for the annotated.

group: Optional group names for the argument. This affects how they're displayed
in the backend's help text. Note this can also be a `Group` instance in order
Expand Down Expand Up @@ -192,6 +193,7 @@ def __hash__(self):
default: T | EmptyType | None = Empty
help: str | None = None
parse: Callable[..., T] | Sequence[Callable[..., Any]] | None = None
parse_inference: bool = True

group: str | tuple[int, str] | Group | EmptyType = Empty

Expand Down Expand Up @@ -627,22 +629,27 @@ def infer_num_args(
def infer_parse(
arg: Arg[Any], type_view: TypeView[Any], state: State[Any] | None = None
) -> Callable[..., Any]:
if arg.parse:
parse: Parser[Any] | Sequence[Parser[Any]] = arg.parse
else:
parse = parse_value(type_view)
parse: Parser[Any] | Sequence[Parser[Any]] = arg.parse # type: ignore
default_parse = parse_value(type_view)

# This is the original arg choices, e.g. explicitly provided. Inferred choices
# need to be handled internally to the parsers.
if arg.choices:
if not isinstance(parse, Sequence):
parse = [parse]
parsers: Sequence[Parser[Any]] = []
if parse:
if isinstance(parse, Sequence):
parsers = [*cast(Sequence[Any], parse)]
else:
parsers = [parse]

if arg.choices:
literal_type = Literal[tuple(arg.choices)] # type: ignore
literal_parse: Parser[Any] = parse_literal(literal_type) # type: ignore
parse = cast(Sequence[Parser[Any]], [*parse, literal_parse])
parsers = cast(Sequence[Parser[Any]], [*parsers, literal_parse])

if arg.parse_inference:
parsers = [*parsers, default_parse]

return evaluate_parse(parse, type_view, state=state)
return evaluate_parse(parsers, type_view, state=state)


def infer_help(arg: Arg[Any], fallback_help: str | None) -> str | None:
Expand Down
25 changes: 16 additions & 9 deletions src/cappa/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
cast,
)

from typing_extensions import Never

from cappa.file_io import FileMode
from cappa.state import S, State
from cappa.type_view import TypeView
Expand Down Expand Up @@ -105,7 +103,7 @@ def parse_value(typ: MaybeTypeView[T]) -> Parser[T]:
return type_view.annotation

if type_view.is_none_type:
return parse_none
return parse_none # type: ignore

if type_view.is_subclass_of(enum.Enum):
return parse_enum(type_view)
Expand All @@ -131,7 +129,17 @@ def parse_value(typ: MaybeTypeView[T]) -> Parser[T]:
if type_view.is_subclass_of((TextIO, BinaryIO)):
return parse_file_io(type_view)

return type_view.annotation
return parse_fallback(type_view.annotation)


def parse_fallback(fallback: type[T]) -> Parser[T]:
def fallback_mapper(v: Any) -> T:
if isinstance(v, fallback):
return v

return fallback(v) # type: ignore

return fallback_mapper


def parse_literal(typ: MaybeTypeView[T]) -> Parser[T]:
Expand Down Expand Up @@ -252,11 +260,10 @@ def union_mapper(value: Any) -> Any:
return union_mapper


def parse_none(value: Any) -> Never:
"""Create a value parser for None.

Default values are not run through Arg.parse, so there's no way to arrive at a `None` value.
"""
def parse_none(value: Any) -> None:
"""Create a value parser for None."""
if value is None:
return
raise ValueError(value)


Expand Down
19 changes: 19 additions & 0 deletions tests/arg/test_parse_inference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from __future__ import annotations

from dataclasses import dataclass

from typing_extensions import Annotated

import cappa
from tests.utils import Backend, backends, parse


@dataclass
class Args:
numbers: Annotated[str, cappa.Arg(parse=int, parse_inference=False)]


@backends
def test_disable_default_parser(backend: Backend):
test = parse(Args, "1", backend=backend)
assert test.numbers == 1
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading