Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More typing #76

Merged
merged 5 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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
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
95 changes: 77 additions & 18 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,25 +137,23 @@ def _cast_answer(answer_type: Callable[[str], str], answer: str) -> Any:
return None


def question(
# NOTE: can't have a default value on an argument whose type annotation has a TypeVar
# this means that `answer_type: Callable[[str], T] = str` (which would make T==str) throws error
# see https://github.com/python/mypy/issues/3737
#
# as such, we work-around with an inner function (without any defaults) that receives all of its
# arguments from the user-facing API, then we also add two overloads
# * not passing an `answer_type` (aka: the overload with `: None`) -> returns str | None
# * providing `answer_type` (overload with `Callable[...]`) -> returns casted value (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,
default: Optional[str],
confirm: bool,
answer_type: Callable[[str], T],
validate: Optional[Callable[Concatenate[str, P], bool]],
**kwargs: Any,
) -> Union[str, Any]:
"""Allow the user to type in a free-form string to answer.

| Argument | Description |
|----------|-------------|
| prompt | The prompt to present to the user. Can include ANSI and format strings like milc's `cli.echo()`. |
| default | The value to return when the user doesn't enter any value. Use None to prompt until they enter a value. |
| confirm | Present the user with a confirmation dialog before accepting their answer. |
| 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):` |
"""
) -> Union[str, T, None]:
if not cli.interactive:
return default

Expand All @@ -177,6 +180,62 @@ def question(
return default


@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]:
...


def question(
prompt: str,
*args: Any,
default: Optional[str] = None,
confirm: bool = False,
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.

| Argument | Description |
|----------|-------------|
| prompt | The prompt to present to the user. Can include ANSI and format strings like milc's `cli.echo()`. |
| default | The value to return when the user doesn't enter any value. Use None to prompt until they enter a value. |
| confirm | Present the user with a confirmation dialog before accepting their answer. |
| 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 choice(
heading: str,
options: Sequence[str],
Expand Down