connor-makowski / type_enforced

A pure python type enforcer for annotations. Enforce types in python functions and methods.
MIT License
45 stars 2 forks source link

Literal in combination with | raises type error #44

Open gu62jen opened 1 week ago

gu62jen commented 1 week ago

From what I have seen in the docs, Literal and | should both be supported as type hints. However, combined they always give me a type error.

from typing import Literal

import type_enforced

@type_enforced.Enforcer
def foo(a: Literal["bar"] | int) -> None:
    pass

foo("bar")  # will raise a type error
foo(42)  # will raise a type error
JSchoeck commented 3 days ago

Same here with

def foo(a: date | Literal["latest"]) -> None:
    ...
connor-makowski commented 3 days ago

Thanks for creating this issue. This does seem a bit counter intuitive from the type_enforced side.

Note that Literal is handled after type checking completes as a constraint. See Literal in the docs here.

Given your definition, this means that first type_enforced checks to see that the passed item is an int and raises an error if it is not. Then if it is an int, the constraint a in ["bar"] is checked. This is a unique aspect of Literal / Constraints.

Now this leaves a conundrum in this case because even if you specified:

@type_enforced.Enforcer
def foo(a: Literal["bar"] | int | str) -> None:
    pass

This would still fail on the Literal check if an int was passed.

Ideally, the Literal check would happen as an option during the checking stage, but this adds some complexity and time as well as other implicaitons.

For now, you could consider using a custom constraint to handle this use case although it does arguably look a bit ugly. @JSchoeck this should probably allow for your date validation with some modification as well.

import type_enforced
from typing import Literal
from type_enforced.utils import GenericConstraint

bar_or_int = GenericConstraint({
    'int_or_str': lambda x: isinstance(x, (int, str)),
    'bar_if_str': lambda x: x == 'bar' if isinstance(x, str) else True,
})

@type_enforced.Enforcer
def foo(a: bar_or_int) -> None:
    pass

foo("baz")  # Passes
foo(1)  # Passes
foo("baz")  # TypeError: (foo): Constraint validation error for variable `a`. Constraint `bar_if_str` not met with the provided value `baz`

I want to mull this over a bit before deciding on a path forward on handling Literals. Any ideas / suggestions are very welcome.