[Feature Request] `beartype.vale` + PEP 593 `typing.Annotated[...]`` + PEP 692 `typing.Unpack[...]` = :hurtrealbad:
Let's say you want to write a variadic function, and you want none of its arguments to be none:
def fn(x, *args):
assert x is not None
for arg in args:
assert arg is not None
# do something fun with x and args
If you want type-annotate this function:
def fn[T, *Ts](
x: T,
*args: *Ts # or equivalently, args: Unpack[Ts]
):
assert x is not None
for arg in args:
assert arg is not None
# do something fun with x and args
Let's say you want to use a beartype.vale validator to make sure that your values are non-None. This is possible on x:
@beartype
def fn[T, *Ts](
x: Annotated[T, ~IsEqual[None]],
*args: *Ts # or equivalently, args: Unpack[Ts]
):
...
But it breaks down on args. First, Annotated[*Ts, ...] is invalid Python annotation, with an error of
TypeError: Annotated[...] should not be used with an unpacked TypeVarTuple
(This happens at definition time for 3.13 and at get_annotations() time for 3.14.)
Second, this isn't supported by beartype:
@beartype
def fn[T, *Ts](
x: Annotated[T, ~IsEqual[None]],
*args: Unpack[Annotated[Ts, ~IsEqual[None]]]
):
...
Is it possible for beartype to support this type of validators?
typinox looks so cool! Come look at what @EtaoinWu is doing with jaxtyping and equinox, @patrick-kidger. 🆒
Back to this awesome feature request. Let's see here...
First,
Annotated[*Ts, ...]is invalid Python annotation, with an error ofTypeError: Annotated[...] should not be used with an unpacked TypeVarTuple
...wat!?! Really? That's super-nonsense. Annotated[{hint}, ...] is supposed to be valid for all possible valid hints {hint}. *Ts is a valid type hint in many contexts. Ergo, Annotated[*Ts, ...] should also be a valid type hint in the same contexts. Unsurprisingly, typing devs got that one wrong. And they got that one wrong for the usual reason they get things wrong: "They thought they knew better. But they didn't, did they?"
There are probably many PEP-compliant ways to circumvent that nonsense. Here is one:
from typing import Annotated, TypeVarTuple, Unpack
Ts = TypeVarTuple('Ts')
type Ts_unpacked = Unpack[Ts]
Annotated[Ts_unpacked, object]
Totally works! Hah! Suck it, CPython devs. The complexity of their own typing system has gotten away from them. They can no longer maintain their nonsensical authoritarian control mechanism. Good. Good, I say.
Does the above work? In theory, @beartype should support that. No idea if it actually does, of course. If not, @beartype could almost certainly support syntax like Unpack[Annotated[Ts, ~IsEqual[None]]]. It's... a bit weird. But so what? I'm weird, too. I support weird syntax – especially when it circumvents nonsense in the typing module. Two bear claws way up. 🐾
Of course, I have no idea how or when I'll ever find the scarce volunteer time for this. Maybe I can sic gemini-cli on this? I've been meaning to start throwing Gemini at @beartype. I fear the time has finally come for @beartype to become friends with code-generating LLMs, gentlemen.
Lastly, I love sentences like this:
you want none of its arguments to be none
There are so many nones happening here all at once. It's like when you double a double-negative. 😻
Apparently this abomination already works:
from typing import Annotated, TypeVar, TypeVarTuple, Unpack
import pytest
from beartype import beartype
from beartype.vale import IsEqual
from beartype.roar import BeartypeCallHintViolation
Ts = TypeVarTuple('Ts')
T = TypeVar('T')
type Ts_unpacked = Unpack[Ts]
@beartype
def fn(n: Annotated[T, IsEqual[1]], *args: Annotated[Ts_unpacked, ~IsEqual[None]]) -> None:
...
assert fn(1, 2, 3) is None
assert fn(1) is None
with pytest.raises(BeartypeCallHintViolation):
fn(1, None)
with pytest.raises(BeartypeCallHintViolation):
fn(1, 2, None)
with pytest.raises(BeartypeCallHintViolation):
fn(1, None, 2)
And it can be simplified to:
from typing import Annotated, TypeAliasType
@beartype
def fn[T, *Ts](
n: Annotated[T, ~IsEqual[None]],
*args: Annotated[TypeAliasType("*Ts", *Ts), ~IsEqual[None]],
) -> None: ...
Static type checkers are very unhappy though.
...lol. Annotated[TypeAliasType("*Ts", *Ts), ~IsEqual[None]] is incredibly clever. I never knew TypeAliasType could be instantiated from Python code. Today, I learned many things. You've gone beyond where even @leycec dares to tread. Indeed, you've pioneered a whole new world of typing barbarism. Whenever I need inspiration, I now know where to turn. 😆