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

get_args #35

Open
kshpytsya opened this issue May 1, 2019 · 11 comments
Open

get_args #35

kshpytsya opened this issue May 1, 2019 · 11 comments

Comments

@kshpytsya
Copy link

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)
@Peilonrayz
Copy link

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]

@kshpytsya
Copy link
Author

@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

@Peilonrayz
Copy link

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)

@kshpytsya
Copy link
Author

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"?

@ilevkivskyi
Copy link
Owner

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.

@Stewori
Copy link

Stewori commented May 3, 2019

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

@kshpytsya
Copy link
Author

kshpytsya commented May 3, 2019 via email

@Stewori
Copy link

Stewori commented May 3, 2019

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.)

@kshpytsya
Copy link
Author

kshpytsya commented May 9, 2019

Regretfully, solution from #35 (comment) 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()

@Peilonrayz
Copy link

Peilonrayz commented May 9, 2019

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.

@Stewori
Copy link

Stewori commented May 10, 2019

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants