Skip to content

Commit

Permalink
fix: action inference when default is an Env.
Browse files Browse the repository at this point in the history
  • Loading branch information
DanCardin committed Jun 23, 2024
1 parent 2a62727 commit 570fbc3
Show file tree
Hide file tree
Showing 7 changed files with 52 additions and 20 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## 0.21

### 0.21.2
- fix: action inference when `default` is an `Env`.

### 0.21.1

- feat: Update automatic inference to support `date`, `time`, and `datetime` parsing (for isoformat).
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.21.1"
version = "0.21.2"
description = "Declarative CLI argument parser."

repository = "https://github.com/dancardin/cappa"
Expand Down
3 changes: 3 additions & 0 deletions src/cappa/arg.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,9 @@ def infer_action(

# Coerce raw `bool` into flags by default
if is_subclass(origin, bool):
if isinstance(default, Env):
default = default.default

if default is not missing and bool(default):
return ArgAction.store_false

Expand Down
4 changes: 2 additions & 2 deletions src/cappa/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from cappa.arg import Arg, ArgAction, no_extra_arg_actions
from cappa.command import Command, Subcommand
from cappa.help import generate_arg_groups
from cappa.invoke import fullfill_deps
from cappa.invoke import fulfill_deps
from cappa.output import Exit, HelpExit, Output
from cappa.parser import RawOption, Value
from cappa.typing import assert_type, missing
Expand Down Expand Up @@ -129,7 +129,7 @@ def __call__( # type: ignore
if option_string:
fullfilled_deps[RawOption] = RawOption.from_str(option_string)

deps = fullfill_deps(action, fullfilled_deps)
deps = fulfill_deps(action, fullfilled_deps)
result = action(**deps)
setattr(namespace, self.dest, result)

Expand Down
26 changes: 13 additions & 13 deletions src/cappa/invoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class InvokeResolutionError(RuntimeError):

@dataclass(frozen=True)
class Dep(typing.Generic[C]):
"""Describes the callable required to fullfill a given dependency."""
"""Describes the callable required to fulfill a given dependency."""

callable: Callable

Expand Down Expand Up @@ -151,8 +151,8 @@ def resolve_callable(

global_deps = resolve_global_deps(deps, implicit_deps)

fullfilled_deps = {**implicit_deps, **global_deps}
kwargs = fullfill_deps(fn, fullfilled_deps)
fulfilled_deps = {**implicit_deps, **global_deps}
kwargs = fulfill_deps(fn, fulfilled_deps)
except InvokeResolutionError as e:
raise InvokeResolutionError(
f"Failed to invoke {parsed_command.cmd_cls} due to resolution failure."
Expand All @@ -177,7 +177,7 @@ def resolve_global_deps(
for source_function, dep in deps.items():
# Deps need to be fulfilled, whereas raw values are taken directly.
if isinstance(dep, Dep):
value = Resolved(dep.callable, fullfill_deps(dep.callable, implicit_deps))
value = Resolved(dep.callable, fulfill_deps(dep.callable, implicit_deps))
else:
value = Resolved(source_function, result=dep, is_resolved=True)

Expand Down Expand Up @@ -259,7 +259,7 @@ def resolve_implicit_deps(command: Command, instance: HasCommand) -> dict:
return deps


def fullfill_deps(fn: Callable, fullfilled_deps: dict) -> typing.Any:
def fulfill_deps(fn: Callable, fulfilled_deps: dict) -> typing.Any:
result = {}

signature = getattr(fn, "__signature__", None) or inspect.signature(fn)
Expand All @@ -286,29 +286,29 @@ def fullfill_deps(fn: Callable, fullfilled_deps: dict) -> typing.Any:

if dep is None:
# Non-annotated args are either implicit dependencies (and thus already fulfilled),
# or arguments that we cannot fullfill
if annotation not in fullfilled_deps:
# or arguments that we cannot fulfill
if annotation not in fulfilled_deps:
if param.default is param.empty:
annotation_name = annotation.__name__ if annotation else "<empty>"
raise InvokeResolutionError(
f"`{name}: {annotation_name}` is not a valid dependency for Dep({fn.__name__})."
)

# if there's a default, we can just skip it and let the default fullfill the value.
# if there's a default, we can just skip it and let the default fulfill the value.
continue

value = fullfilled_deps[annotation]
value = fulfilled_deps[annotation]

else:
# Whereas everything else should be a resolvable explicit Dep, which might have either
# already been fullfullfilled, or yet need to be.
if dep in fullfilled_deps:
value = fullfilled_deps[dep]
if dep in fulfilled_deps:
value = fulfilled_deps[dep]
else:
value = Resolved(
dep.callable, fullfill_deps(dep.callable, fullfilled_deps)
dep.callable, fulfill_deps(dep.callable, fulfilled_deps)
)
fullfilled_deps[dep] = value
fulfilled_deps[dep] = value

result[name] = value

Expand Down
8 changes: 4 additions & 4 deletions src/cappa/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from cappa.command import Command, Subcommand
from cappa.completion.types import Completion, FileCompletion
from cappa.help import format_subcommand_names
from cappa.invoke import fullfill_deps
from cappa.invoke import fulfill_deps
from cappa.output import Exit, HelpExit, Output
from cappa.typing import T, assert_type

Expand Down Expand Up @@ -579,17 +579,17 @@ def consume_arg(
else:
action_handler = action

fullfilled_deps: dict = {
fulfilled_deps: dict = {
Command: context.command,
Output: context.output,
ParseContext: context,
Arg: arg,
Value: Value(result),
}
if option:
fullfilled_deps[RawOption] = option
fulfilled_deps[RawOption] = option

kwargs = fullfill_deps(action_handler, fullfilled_deps)
kwargs = fulfill_deps(action_handler, fulfilled_deps)
context.result[arg.field_name] = action_handler(**kwargs)

check_deprecated(context, arg, option)
Expand Down
26 changes: 26 additions & 0 deletions tests/arg/test_bool.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from dataclasses import dataclass
from unittest.mock import patch

import cappa
import pytest
Expand Down Expand Up @@ -142,3 +143,28 @@ class ArgTest:

test = parse(ArgTest, "-t", "asdf", backend=backend)
assert test.true_false is True


@backends
def test_env_default_value_precedence(backend):
"""Assert a bool flag yields correct value given an env default."""

@dataclass
class ArgTest:
env_default: Annotated[
bool, cappa.Arg(long=True, default=cappa.Env("ENV_DEFAULT"))
] = False

test = parse(ArgTest, backend=backend)
assert test.env_default is False

with patch("os.environ", new={"ENV_DEFAULT": "1"}):
test = parse(ArgTest, backend=backend)
assert test.env_default is True

test = parse(ArgTest, "--env-default", backend=backend)
assert test.env_default is True

with patch("os.environ", new={"ENV_DEFAULT": "1"}):
test = parse(ArgTest, "--env-default", backend=backend)
assert test.env_default is True

0 comments on commit 570fbc3

Please sign in to comment.