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

The way to return WithSubclasses in the form of union #30

Closed Ashark closed 9 months ago

Ashark commented 9 months ago

I have the following code:

import type_enforced
from type_enforced.utils import WithSubclasses
from typing import List, Type

class Dev:
    pass

class PyDev(Dev):
    pass

class JsDev(Dev):
    pass

@type_enforced.Enforcer
def func(way) -> List[str | Type[Dev]]:  # -> list[str | Dev]
    ret = []
    match way:
        case 1:
            pass
        case 2:
            ret.append(Dev())
        case 3:
            ret.append(PyDev())
        case 4:
            ret.append("hello")
    return ret

res1 = func(1)
res2 = func(2)
res3 = func(3)
res4 = func(4)

For my function func I previously used just such annotation: -> list. I now wanted to be a bit more specific, so I wanted to specify that func may return a list that may contain str or a Dev or any subclass of Dev. I then tried to use such annotation: -> list[str | Dev]. And this code works normally by itself, but type_enforcer does not accept the subclasses of Dev.

What is the usage of WithSubclasses for such case? I have found that I can use this:

from typing import Union
def func(way) -> list[str | Union[*WithSubclasses(Dev)]]:

It would be good to support a more nice syntax.

conmakMIT commented 9 months ago

You should be able to take advantage of the standard comma separated syntax with the unpacking * operator. Other similar variants of this passing into Union would also work.

def func(way) -> list[str, *WithSubclasses(Dev)]:
Ashark commented 9 months ago

This way the list is defined not as containing any element of any type (from union) in any order.

In other words,

def func(way) -> list[str, int]:

and

def func(way) -> list[str | int]:

are not the same thing.

The list[str, int] means only the following list may be valid: ["apple", 300, "orange", 400] (i.e., order matters). But with list[str | int] these both may be valid: ["apple", 300, "orange", 400], ["apple", "orange", 400, 500, "banana"]. In the last case I do not have to even keep the "pairs" of elements.

connor-makowski commented 9 months ago

So in this case, type_enforced deviates from the typing package expectations. Essentially unions and list separated types are equivalent (even inside of lists).

Essentially: list[str,int] == Union(list[str], list[int]) == list[Union(str, int)] == list[str | int]

This is defined in supported types and through the docs.

connor-makowski commented 9 months ago

If you prefer to keep the syntax:

def func(way) -> list[Union(str, *WithSubclasses(Dev))]:
connor-makowski commented 9 months ago

I will go ahead and close this issue.