Skip to content

Commit

Permalink
Merge pull request #178 from DanCardin/dc/method-parse
Browse files Browse the repository at this point in the history
fix: Incorrect handling of methods in Arg.parse.
  • Loading branch information
DanCardin authored Nov 12, 2024
2 parents 721687a + 3868127 commit 4785ce2
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 11 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 0.24

### 0.24.3

- fix: Incorrect handling of methods in Arg.parse.

### 0.24.2

- fix: Literal contained inside non-variadic tuple should not imply "choices".
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.24.2"
version = "0.24.3"
description = "Declarative CLI argument parser."

urls = {repository = "https://github.com/dancardin/cappa"}
Expand Down
10 changes: 9 additions & 1 deletion src/cappa/class_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from cappa.typing import T, find_annotations

if typing.TYPE_CHECKING:
pass
from cappa.command import Command

__all__ = [
"detect",
Expand Down Expand Up @@ -309,3 +309,11 @@ def collect_method_subcommands(cls: type) -> tuple[typing.Callable, ...]:
for _, method in inspect.getmembers(cls, callable)
if hasattr(method, "__cappa__")
)


def has_command(obj) -> bool:
return hasattr(obj, "__cappa__")


def get_command(obj) -> Command | None:
return getattr(obj, "__cappa__", None)
10 changes: 5 additions & 5 deletions src/cappa/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import typing
from collections.abc import Callable

from cappa import class_inspect
from cappa.arg import Arg, Group
from cappa.class_inspect import fields as get_fields
from cappa.class_inspect import get_command, get_command_capable_object
from cappa.docstring import ClassHelpText
from cappa.env import Env
from cappa.help import HelpFormatable, HelpFormatter, format_short_help
Expand Down Expand Up @@ -87,9 +88,8 @@ def get(
if isinstance(obj, cls):
instance = obj
else:
obj = class_inspect.get_command_capable_object(obj)
if getattr(obj, "__cappa__", None):
instance = obj.__cappa__ # type: ignore
obj = get_command_capable_object(obj)
instance = get_command(obj)

if instance:
return dataclasses.replace(instance, help_formatter=help_formatter)
Expand Down Expand Up @@ -121,7 +121,7 @@ def collect(cls, command: Command[T]) -> Command[T]:
if not command.description:
kwargs["description"] = help_text.body

fields = class_inspect.fields(command.cmd_cls)
fields = get_fields(command.cmd_cls)
function_view = CallableView.from_callable(command.cmd_cls, include_extras=True)

if command.arguments:
Expand Down
8 changes: 5 additions & 3 deletions src/cappa/invoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from collections.abc import Callable
from dataclasses import dataclass, field

from cappa.class_inspect import has_command
from cappa.command import Command, HasCommand
from cappa.output import Exit, Output
from cappa.subcommand import Subcommand
Expand Down Expand Up @@ -301,11 +302,12 @@ def fulfill_deps(

# Method `self` arguments can be assumed to be typed as the literal class they reside inside,
# These classes should always be already fulfilled by the root command structure.
elif inspect.ismethod(fn) and index == 0:
elif index == 0 and inspect.ismethod(fn):
cls = get_method_class(fn)

value = fulfilled_deps[cls]
args.append(value)
if has_command(fn.__self__):
value = fulfilled_deps[cls]
args.append(value)

# If there's a default, we can just skip it and let the default fulfill the value.
# Alternatively, `allow_empty` might be True to indicate we shouldn't error.
Expand Down
45 changes: 45 additions & 0 deletions tests/arg/test_parse_method.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from __future__ import annotations

from dataclasses import dataclass

from typing_extensions import Annotated

import cappa
from tests.utils import backends, parse


@dataclass
class Config:
path: str

@classmethod
def from_classmethod(cls, path: str):
return cls(path)

def from_method(self, path: str) -> str:
return "/".join([self.path, path])


config = Config("foo")


@backends
def test_from_classmethod(backend):
@cappa.command(name="command")
@dataclass
class Command:
config: Annotated[Config, cappa.Arg(parse=Config.from_classmethod)]

test = parse(Command, "foo", backend=backend)
assert test == Command(config=Config("foo"))


@backends
def test_method(backend):
@cappa.command(name="command")
@dataclass
class Command:
config: Annotated[str, cappa.Arg(parse=config.from_method)]

test = parse(Command, "bar", backend=backend)
assert test == Command(config="foo/bar")
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.

0 comments on commit 4785ce2

Please sign in to comment.