typing_inspect icon indicating copy to clipboard operation
typing_inspect copied to clipboard

get_args

Open kshpytsya opened this issue 5 years ago • 11 comments

Is it possible to use typing_inspect to achieve the following?

from typing import Any, List, TypeVar

import typing_inspect


class C:
    x: int

    def __init__(self, x: int) -> None:
        self.x = x


T = TypeVar("T")


class L(List[T]):
    def append_new(self, *args: Any, **kw: Any) -> T:
        item_class = ???(T)
        self.append(item_class(*args, **kw))
        return self[-1]


l = L[C]()
print(l.append_new(42).x)

kshpytsya avatar May 01 '19 19:05 kshpytsya

T doesn't hold any of the cool information you think it does, it's L's instance that does. You may want to have a look at the alternates to get_args and it's arguments, but the following works in Python 3.7.2.

item_class = typing_inspect.get_args(typing_inspect.get_generic_type(self))[0]

Peilonrayz avatar May 01 '19 19:05 Peilonrayz

@Peilonrayz this works, both on 3.7.3 and 3.6.8 (which include "new" and "old" versions of typing). I am aware that T doesn't hold any cool information but is merely a token. However, I imagine it should be possible to use it instead of an explicit index ([0]). So, I would expect to have something like typing_inspect.get_xxx (cannot find a good name for it), that would work in the following scenario:

from typing import List, Tuple, TypeVar

import typing_inspect

U = TypeVar("U")
V = TypeVar("V")
W = TypeVar("W")
Z = 1


class L(List[Tuple[U, V]]):
    def __init__(self) -> None:
        self._type_U = typing_inspect.get_xxx(self, U)  # int
        self._type_V = typing_inspect.get_xxx(self, V)  # str
        self._type_W = typing_inspect.get_xxx(self, W)  # exception: "type W is not among generic parameters"
        self._type_Z = typing_inspect.get_xxx(self, Z)  # exception: "1 is not a type"


l = L[int, str]()
l.f()

Another interesting alternative I see would be a decorator that injects all types as instance variables:

from typing import List, Tuple, TypeVar

import typing_inspect

U = TypeVar("UU")
V = TypeVar("VV")


@typing_inspect.inject_types_or_some_better_name
class L(List[Tuple[U, V]]):
    pass

l = L[int, str]()
assert l._type_UU == int
assert l._type_VV == str

kshpytsya avatar May 02 '19 11:05 kshpytsya

It doesn't look like this is at all possible to do in the constructor, in Python 3.7.2 at least.

This is as typing_inspect.get_generic_type(self) returns <class '__main__.L'> not __main__.L[int, str].

Otherwise it looks rather simple:

from typing import List, Tuple, TypeVar

import typing_inspect

U = TypeVar("U")
V = TypeVar("V")
W = TypeVar("W")
Z = 1


def get_type_argument(instance, type_var):
    generic = typing_inspect.get_generic_type(instance)
    origin = typing_inspect.get_origin(generic)
    params = typing_inspect.get_parameters(origin)
    index = params.index(type_var)
    return typing_inspect.get_args(generic)[index]


class L(List[Tuple[U, V]]):
    pass


l = L[int, str]()
print(get_type_argument(l, U))
print(get_type_argument(l, V))
try:
    print(get_type_argument(l, W))
except ValueError as e:
    print(e)
try:
    print(get_type_argument(l, Z))
except ValueError as e:
    print(e)

Peilonrayz avatar May 02 '19 12:05 Peilonrayz

So, do you think something like that should be included as a part of typing_inspect and not be left as "an exercise for the reader"?

kshpytsya avatar May 02 '19 13:05 kshpytsya

On one hand, there is a plan to add some extra functionality to typing_inspect (see #31), I could imagine some kind of helper like this can be added there. On the other hand get_generic_type(self) returning the plain class needs to be fixed somehow.

ilevkivskyi avatar May 03 '19 02:05 ilevkivskyi

Maybe pytypes.get_arg_for_TypeVar(typevar, generic) is what you are looking for.

Stewori avatar May 03 '19 06:05 Stewori

From my experience, pytypes still lacks proper python 3.7 support.

On Fri, 3 May 2019, 09:40 Stefan Richthofer, [email protected] wrote:

Maybe pytypes.get_arg_for_TypeVar(typevar, generic) https://github.com/Stewori/pytypes#get_arg_for_typevartypevar-generic is what you are looking for.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/ilevkivskyi/typing_inspect/issues/35#issuecomment-488965943, or mute the thread https://github.com/notifications/unsubscribe-auth/AAOCW3S52ZKUQTNG72CAWUTPTPM5JANCNFSM4HJWBBZA .

kshpytsya avatar May 03 '19 06:05 kshpytsya

Feel free to make a PR for that ;) (Edit: I really hope to be able to provide Python 3.7 support for pytypes. Still I found it important to cross-link this feature here already now. In the hope that it can be useful.)

Stewori avatar May 03 '19 06:05 Stewori

Regretfully, solution from https://github.com/ilevkivskyi/typing_inspect/issues/35#issuecomment-488386120 fails in case of inheritance:

from typing import List, TypeVar

import typing_inspect


T = TypeVar("T")


class L(List[T]):
    def f(self) -> None:
        print(typing_inspect.get_args(typing_inspect.get_generic_type(self)))


class L2(L[int]):
    pass


L[int]().f()
L2().f()

prints

(<class 'int'>,)
()

Digging into typing.py it seems to make sense why this happens: _GenericAlias.__call__ is the place where __orig_class__ is set. Which also explains why this does not work in constructor. But does it mean there is no elegant solution? The only practical one I can think of, which isn't anywhere close to elegant to my taste is something like:

from typing import List, TypeVar

import typing_inspect


T = TypeVar("T")


class L(List[T]):
    def f(self) -> None:
        types = getattr(self, "_L_types", None)
        if types is None:
            types = typing_inspect.get_args(typing_inspect.get_generic_type(self))
        print(types)


class L2(L[int]):
    _L_types = (int, )


L[int]().f()
L2().f()

kshpytsya avatar May 09 '19 17:05 kshpytsya

This is as the arguments are on the base class. Using get_generic_bases works fine.

from typing import List, TypeVar

import typing_inspect

T = TypeVar("T")


class L(List[T]):
    def f(self) -> None:
        types = typing_inspect.get_args(typing_inspect.get_generic_type(self))
        if not types:
            types = typing_inspect.get_args(typing_inspect.get_generic_bases(self)[0])
        print(types)


class L2(L[int]):
    pass


L().f()
L[int]().f()
L2().f()

It should be noted that this is hacky solution, and may give unexpected results if the class is class C(Mapping[int, T], Sequence[str]). The behavior can be seen in one of typing_inspect_lib.get_mro_orig tests. This function isn't available until #31 has been completed.

Using this should be fairly simple when it does come out: (Note: untested and subject to change)

for type_info in get_mro_orig(L2()):
    if type_info.typing is L:
        print(get_args(type_info.orig))

It should be noted that this won't return (int, T, str) if used on C, in an equivalent way. It however is rather simple to change it to return this, return a combination of Mapping and Sequence.

Peilonrayz avatar May 09 '19 22:05 Peilonrayz

For the record. In a recent commit I fixed workability of pytypes.get_Generic_parameters for Python 3.7 (however, not released). It can now be used like this:

T = TypeVar("T")
		
class L(List[T]):
    def f(self) -> None:
        print(pytypes.get_Generic_parameters(pytypes.get_Generic_type(self)))

class L2(L[int]):
    pass
	
	
L[int]().f()
L2().f()

I know, the name is misleading as it retrieves args, not parameters. That came because it is originally intended to provide control over the parameters to select: the second, optional argument can be some base, e.g. Container or Mapping then it retrieves only the args corresponding to the parameters specified by this base.

Stewori avatar May 10 '19 02:05 Stewori