Skip to content

Commit

Permalink
More typing (#76)
Browse files Browse the repository at this point in the history
* some more typing improvements

* make comment more readable, do not leak defaults in `@overload`

* move `@overload`, try and word the comment better

* move things around
  • Loading branch information
elpekenin authored Feb 13, 2025
1 parent 0a9136a commit 44d7958
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 16 deletions.
11 changes: 7 additions & 4 deletions docs/api_milc_interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ Release the MILC lock.
#### argument

```python
def argument(*args: Any, **kwargs: Any) -> Callable[..., Any]
def argument(*args: Any,
**kwargs: Any) -> Callable[[Callable[P, R]], Callable[P, R]]
```

Decorator to add an argument to a MILC command or subcommand.
Expand Down Expand Up @@ -147,8 +148,10 @@ Execute the entrypoint function.
#### entrypoint

```python
def entrypoint(description: str,
deprecated: Optional[str] = None) -> Callable[..., Any]
def entrypoint(
description: str,
deprecated: Optional[str] = None
) -> Callable[[Callable[P, R]], Callable[P, R]]
```

Decorator that marks the entrypoint used when a subcommand is not supplied.
Expand All @@ -168,7 +171,7 @@ Decorator that marks the entrypoint used when a subcommand is not supplied.
```python
def subcommand(description: str,
hidden: bool = False,
**kwargs: Any) -> Callable[..., Any]
**kwargs: Any) -> Callable[[Callable[P, R]], Callable[P, R]]
```

Decorator to register a subcommand.
Expand Down
6 changes: 3 additions & 3 deletions docs/api_questions.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ def question(prompt: str,
*args: Any,
default: Optional[str] = None,
confirm: bool = False,
answer_type: Callable[[str], str] = str,
validate: Optional[Callable[..., bool]] = None,
**kwargs: Any) -> Union[str, Any]
answer_type: Optional[Callable[[str], T]] = None,
validate: Optional[Callable[Concatenate[str, P], bool]] = None,
**kwargs: Any) -> Union[str, T, None]
```

Allow the user to type in a free-form string to answer.
Expand Down
12 changes: 8 additions & 4 deletions milc/milc_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@
from logging import Logger
from pathlib import Path
from types import TracebackType
from typing import Any, Callable, Dict, Optional, Sequence, Type, Union
from typing import Any, Callable, Dict, Optional, Sequence, Type, TypeVar, Union

from halo import Halo # type: ignore
from typing_extensions import ParamSpec

from .attrdict import AttrDict
from .configuration import Configuration
from .milc import MILC

P = ParamSpec("P")
R = TypeVar("R")


class MILCInterface:
def __init__(self) -> None:
Expand Down Expand Up @@ -145,7 +149,7 @@ def release_lock(self) -> None:
"""
return self.milc.release_lock()

def argument(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
def argument(self, *args: Any, **kwargs: Any) -> Callable[[Callable[P, R]], Callable[P, R]]:
"""Decorator to add an argument to a MILC command or subcommand.
"""
return self.milc.argument(*args, **kwargs)
Expand All @@ -160,7 +164,7 @@ def __call__(self) -> Any:
"""
return self.milc()

def entrypoint(self, description: str, deprecated: Optional[str] = None) -> Callable[..., Any]:
def entrypoint(self, description: str, deprecated: Optional[str] = None) -> Callable[[Callable[P, R]], Callable[P, R]]:
"""Decorator that marks the entrypoint used when a subcommand is not supplied.
Args:
description
Expand All @@ -171,7 +175,7 @@ def entrypoint(self, description: str, deprecated: Optional[str] = None) -> Call
"""
return self.milc.entrypoint(description, deprecated)

def subcommand(self, description: str, hidden: bool = False, **kwargs: Any) -> Callable[..., Any]:
def subcommand(self, description: str, hidden: bool = False, **kwargs: Any) -> Callable[[Callable[P, R]], Callable[P, R]]:
"""Decorator to register a subcommand.
Args:
Expand Down
72 changes: 67 additions & 5 deletions milc/questions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
"""Sometimes you need to ask the user a question. MILC provides basic functions for collecting and validating user input. You can find these in the `milc.questions` module.
"""
from getpass import getpass
from typing import Any, Callable, Optional, Sequence, Union
from typing import Any, Callable, Optional, Sequence, TypeVar, Union, overload

from typing_extensions import Concatenate, ParamSpec

from milc import cli
from .ansi import format_ansi

T = TypeVar("T")
P = ParamSpec("P")


def yesno(prompt: str, *args: Any, default: Optional[bool] = None, **kwargs: Any) -> bool:
"""Displays `prompt` to the user and gets a yes or no response.
Expand Down Expand Up @@ -122,7 +127,7 @@ def password(
return None


def _cast_answer(answer_type: Callable[[str], str], answer: str) -> Any:
def _cast_answer(answer_type: Callable[[str], T], answer: str) -> Optional[T]:
"""Attempt to convert answer to answer_type.
"""
try:
Expand All @@ -132,15 +137,52 @@ def _cast_answer(answer_type: Callable[[str], str], answer: str) -> Any:
return None


@overload
def question(
prompt: str,
*args: Any,
default: Optional[str] = ...,
confirm: bool = ...,
answer_type: None = ...,
validate: Optional[Callable[Concatenate[str, P], bool]] = ...,
**kwargs: Any,
) -> Optional[str]:
...


@overload
def question(
prompt: str,
*args: Any,
default: Optional[str] = ...,
confirm: bool = ...,
answer_type: Callable[[str], T] = ...,
validate: Optional[Callable[Concatenate[str, P], bool]] = ...,
**kwargs: Any,
) -> Optional[T]:
...


# NOTE: can't have a default value on an argument whose type annotation is a TypeVar
# this means that `answer_type: Callable[[str], T] = str` gives a typing error
# see https://github.com/python/mypy/issues/3737
#
# due to this, we leave the default as `None`, while the actual implementation
# lives on a private function that receives all of its arguments from the public API.
# by doing this, the default value is "resolved" on callsite instead, making mypy happy.
#
# for better expresiveness, @overload variants are defined, to let the user know:
# a) no `answer_type` provided: return str | None
# b) `answer_type` converts str into T: return T | None
def question(
prompt: str,
*args: Any,
default: Optional[str] = None,
confirm: bool = False,
answer_type: Callable[[str], str] = str,
validate: Optional[Callable[..., bool]] = None,
answer_type: Optional[Callable[[str], T]] = None,
validate: Optional[Callable[Concatenate[str, P], bool]] = None,
**kwargs: Any,
) -> Union[str, Any]:
) -> Union[str, T, None]:
"""Allow the user to type in a free-form string to answer.
| Argument | Description |
Expand All @@ -151,6 +193,26 @@ def question(
| answer_type | Specify a type function for the answer. Will re-prompt the user if the function raises any errors. Common choices here include int, float, and decimal.Decimal. |
| validate | This is an optional function that can be used to validate the answer. It should return True or False and have the following signature:<br><br>`def function_name(answer, *args, **kwargs):` |
"""
return _question(
prompt,
*args,
default=default,
confirm=confirm,
answer_type=answer_type or str,
validate=validate,
**kwargs,
)


def _question(
prompt: str,
*args: Any,
default: Optional[str],
confirm: bool,
answer_type: Callable[[str], T],
validate: Optional[Callable[Concatenate[str, P], bool]],
**kwargs: Any,
) -> Union[str, T, None]:
if not cli.interactive:
return default

Expand Down

0 comments on commit 44d7958

Please sign in to comment.