Skip to content

Commit

Permalink
improve some typing; particularly circumventing python/mypy#3004
Browse files Browse the repository at this point in the history
  • Loading branch information
BrianPugh committed Jan 12, 2024
1 parent 1f4ef38 commit d78b3b4
Show file tree
Hide file tree
Showing 7 changed files with 26 additions and 19 deletions.
14 changes: 9 additions & 5 deletions cyclopts/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
ValidationError,
format_cyclopts_error,
)
from cyclopts.group import Group, to_group_converter
from cyclopts.group import Group, GroupConverter
from cyclopts.group_extractors import groups_from_app, inverse_groups_from_app
from cyclopts.help import (
HelpPanel,
Expand Down Expand Up @@ -167,21 +167,25 @@ class App:
kw_only=True,
)

group: Tuple[Union[Group, str], ...] = field(default=None, converter=to_tuple_converter, kw_only=True)
# This can ONLY ever be Tuple[Union[Group, str], ...] due to converter.
# The other types is to make mypy happy for Cyclopts users.
group: Union[Group, str, Tuple[Union[Group, str], ...]] = field(
default=None, converter=to_tuple_converter, kw_only=True
)

group_arguments: Group = field(
default=None,
converter=to_group_converter(Group.create_default_arguments()),
converter=GroupConverter(Group.create_default_arguments()),
kw_only=True,
)
group_parameters: Group = field(
default=None,
converter=to_group_converter(Group.create_default_parameters()),
converter=GroupConverter(Group.create_default_parameters()),
kw_only=True,
)
group_commands: Group = field(
default=None,
converter=to_group_converter(Group.create_default_commands()),
converter=GroupConverter(Group.create_default_commands()),
kw_only=True,
)

Expand Down
11 changes: 6 additions & 5 deletions cyclopts/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,16 @@ def create_default_commands(cls):
return cls("Commands")


def to_group_converter(default_group: Group):
def converter(input_value: Union[None, str, Group]) -> Group:
@define
class GroupConverter:
default_group: Group

def __call__(self, input_value: Union[None, str, Group]) -> Group:
if input_value is None:
return default_group
return self.default_group
elif isinstance(input_value, str):
return Group(input_value)
elif isinstance(input_value, Group):
return input_value
else:
raise TypeError

return converter
2 changes: 2 additions & 0 deletions cyclopts/group_extractors.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def groups_from_app(app: "App") -> List[Tuple[Group, List["App"]]]:
# 1. Extract out all Group objects as they may have additional configuration.
# 2. Assign/Create Groups out of the strings, as necessary.
for subapp in subapps:
assert isinstance(subapp.group, tuple)
for group in subapp.group:
if isinstance(group, Group):
for mapping in group_mapping:
Expand All @@ -51,6 +52,7 @@ def groups_from_app(app: "App") -> List[Tuple[Group, List["App"]]]:

for subapp in subapps:
if subapp.group:
assert isinstance(subapp.group, tuple)
for group in subapp.group:
_create_or_append(group_mapping, group, subapp)
else:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ omit = [
[tool.pyright]
venvPath = "."
venv = ".venv"
ignore = ["docs/", "tests/"]
ignore = ["docs/", ]

[tool.ruff]
target-version = 'py38'
Expand Down
12 changes: 6 additions & 6 deletions tests/test_bind_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,13 @@ def test_bind_no_hint_no_default(app, cmd_str, annotated, assert_parse_args):
if annotated:

@app.command
def foo(a: Annotated[Any, Parameter(help="help for a")]):
def foo(a: Annotated[Any, Parameter(help="help for a")]): # pyright: ignore[reportGeneralTypeIssues]
pass

else:

@app.command
def foo(a):
def foo(a): # pyright: ignore[reportGeneralTypeIssues]
pass

assert_parse_args(foo, cmd_str, "1")
Expand All @@ -189,13 +189,13 @@ def test_bind_no_hint_none_default(app, cmd_str, annotated, assert_parse_args):
if annotated:

@app.command
def foo(a: Annotated[Any, Parameter(help="help for a")] = None):
def foo(a: Annotated[Any, Parameter(help="help for a")] = None): # pyright: ignore[reportGeneralTypeIssues]
pass

else:

@app.command
def foo(a=None):
def foo(a=None): # pyright: ignore[reportGeneralTypeIssues]
pass

assert_parse_args(foo, cmd_str, "1")
Expand All @@ -215,13 +215,13 @@ def test_bind_no_hint_typed_default(app, cmd_str, annotated, assert_parse_args):
if annotated:

@app.command
def foo(a: Annotated[Any, Parameter(help="help for a")] = 5):
def foo(a: Annotated[Any, Parameter(help="help for a")] = 5): # pyright: ignore[reportGeneralTypeIssues]
pass

else:

@app.command
def foo(a=5):
def foo(a=5): # pyright: ignore[reportGeneralTypeIssues]
pass

assert_parse_args(foo, cmd_str, 1)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_coercion.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def test_coerce_tuple_len_mismatch_overflow():
def test_coerce_tuple_ellipsis_too_many_inner_types():
with pytest.raises(ValueError): # This is a ValueError because it happens prior to runtime.
# Only 1 inner type annotation allowed
coerce(Tuple[int, int, ...], "1", "2")
coerce(Tuple[int, int, ...], "1", "2") # pyright: ignore


def test_coerce_tuple_ellipsis_non_divisible():
Expand Down
2 changes: 1 addition & 1 deletion tests/validators/test_validator_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,4 @@ def test_validator_number_gte():
def test_validator_number_typeerror():
validator = Number(gte=5)
with pytest.raises(TypeError):
validator(str, "foo")
validator(str, "foo") # pyright: ignore[reportGeneralTypeIssues]

0 comments on commit d78b3b4

Please sign in to comment.