-
Notifications
You must be signed in to change notification settings - Fork 19
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
Is there a method to list all subclasses of a type, taking into account both parametrized, half-parametrized and not-parametrized generics ? #31
Comments
Given that subclasses can be created any time, how could such a method query the set of possible subclasses? Would it query the subclasses currently defined in the runtime? Or in a specific module? How would it access this pool of subclasses? I think this would be difficult to implement, if feasible at all. What would be the use case? |
Each class in python already has a magic method I would use this in the parsyfiles declarative parser: if the user uses a type hint that is a generic class, half- or not-parametrized, I should be able to list all the types that are compliant with that expectation so that I try to parse and convert the file(s) into these types. |
Can you post some imaginary code examples how the method would behave, e.g. what result it should give based on which call. Ideally in a form that can easily be turned into a test for the method. I am confident that we can add this method, maybe as a joined effort. |
from typing import TypeVar, Generic
T = TypeVar('T')
U = TypeVar('U')
class FullUnparam(Generic[T, U]):
pass
class FullUnparam2(FullUnparam):
pass
class HalfParam(FullUnparam[T, int]):
pass
class EntirelyParam(FullUnparam[str, int]):
pass
class EntirelyParam2(HalfParam[str]):
pass
# This works with FullUnparam.__subclasses__() today although intermediate classes appear too
assert get_subclasses(FullUnparam) == [FullUnparam2, HalfParam, EntirelyParam, EntirelyParam2]
# This does not work with FullUnparam.__subclasses__() today. Maybe a bug of stdlib ?
assert get_subclasses(FullUnparam[str, int]) == [EntirelyParam, EntirelyParam2]
# This does not work with HalfParam.__subclasses__() today.
assert get_subclasses(HalfParam) == [EntirelyParam2]
# variant 1: only Generic subclasses
assert get_subclasses(FullUnparam, only_generics=True) == [FullUnparam2, HalfParam]
assert get_subclasses(HalfParam, only_generics=True) == []
# variant 2: only Generic subclasses with same number of free parameters
assert get_subclasses(FullUnparam, only_generics=True, parametrized=False) == [FullUnparam2] I hope this is clear enough, let me know if this requires some more explanation. By the way I am not very good at naming parameters so feel free to change these suggestions :) |
Here is a first draft:
It does not yet consider additional flags like |
Thanks for the fast answer! Yes you're right, my example had a flaw, I fixed it directly in the initial post: I added another class The code you provided is almost functional, except that
|
Note that strictly speaking, So we should maybe think of a better name for this method ( |
I opened a mirror issue in |
My attempt, recursing on subclasses because it sems that def get_all_subclasses(typ, recursive: bool = True, memo = None) -> List[Type[Any]]:
"""
Returns all subclasses, and supports generic types. It is recursive by default
:param typ:
:return:
"""
memo = memo or set()
# if we have collected the subclasses for this already, return
if typ in memo:
return []
# else remember that we have collected them, and collect them
memo.add(typ)
if is_generic_type(typ):
sub_list = get_origin(typ).__subclasses__()
else:
sub_list = typ.__subclasses__()
# recurse
if recursive:
for typpp in sub_list:
for t in get_all_subclasses(typpp, recursive=True, memo=memo):
# unfortunately we have to check 't not in sub_list' because with generics strange things happen
# maybe is_subtype is not the way to go, find a replacement meaning 'is_compliant' ?
if t not in sub_list and is_subtype(t, typ):
sub_list.append(t)
return sub_list It also does not find Nailing it down, this comes from that fact that |
By default, pytypes treats an unassigned typevar
I didn't apply this in my draft of |
I noticed that with your corrected example, |
Thanks for the help with is_subtype! def get_all_subclasses(typ, recursive: bool = True, memo = None) -> List[Type[Any]]:
memo = memo or set()
# if we have collected the subclasses for this already, return
if typ in memo:
return []
# else remember that we have collected them, and collect them
memo.add(typ)
if is_generic_type(typ):
# We now use get_origin() to also find all the concrete subclasses in case the desired type is a generic
sub_list = get_origin(typ).__subclasses__()
else:
sub_list = typ.__subclasses__()
# recurse
result = [t for t in sub_list if t is not typ and is_subtype(t, typ, bound_typevars={})]
if recursive:
for typpp in sub_list:
for t in get_all_subclasses(typpp, recursive=True, memo=memo):
# unfortunately we have to check 't not in sub_list' because with generics strange things happen
# also is_subtype returns false when the parent is a generic
if t not in sub_list and is_subtype(t, typ, bound_typevars={}):
result.append(t)
return result I let you decide whether to include it, with variants or not, in pytypes. In the meantime I will use this copy. Concerning your last comment, that's what I call the 'intermediate classes'. The only reason why |
This can be not just big, but infinite due to nesting. Also, remember that Filtering them out would mean to only include origins in the result? Are The type annotations need to be in Python 2.7 style to keep the code cross-platform. Apart from that |
Note that pytypes cannot include typing_inspect as dependency, because typing_inspect lacks support for various versions of |
Already figured it out. Yes I would add that function. Could you provide updated tests? |
Thanks !
By the way, do you know why |
Sorry, I cannot reproduce
E.g. |
To get this finalized, I just wanted to add the method in its last version. In turned out that this approach seems to fail with typing-3.5.3.0 and earlier. Ideas? |
It seems like behavior of typing <3.5.3.0: Not yet even tried it on Python 3.7. So, this looks too difficult to maintain. |
Argh, that explains a lot of things, thanks for investigating and finding this out! Here is a synthetic view of the relations between the classes (I hope that the display is correct on all browsers/OSes). User-defined classes are wrapped with stars.
So, performing the recursion as we defined above seems to always retrieve the user-defined classes (FU2, HP, EP, EP2) whatever the version of python, correct ? What varies is the presence or not of the other ones (the ones with brackets, that exist only because they are used behind the scenes). If that's correct, adding a post-processing filter to try to identify and remove them (is that only possible?) would be the ideal solution. Otherwise we can simply document the function to explain that there is a guarantee on user-defined subclasses but not on others. |
It's somehow part of pytypes' goal to smoothen the rough edges between typing versions, to normalize uneven behavior. In that fashion, a normalizing solution would be the best I guess. |
Maybe |
Ideally such a method would provide parameters to specify if the caller wishes to
The text was updated successfully, but these errors were encountered: