Repository navigation
Description
Consider the following code from my hypothetical ORM that has three different ways of building an object
from typing import TypeVar, Type
T = TypeVar('T')
class ModelMeta(type):
def build(cls: Type[T]) -> T:
return cls()
def build_other(cls):
return cls()
class Model(metaclass=ModelMeta):
@classmethod
def new(cls: Type[T]) -> T:
return cls()
I'm having trouble defining build in a way that type checks. In this example, I get an error on build about how Type[T] is not a supertype of the class ModelMeta.
build_other passes the type checker, but usages don't seem to catch anything (implicit Any perhaps?)
class Dog(Model):
def __init__(self):
print("Built Dog")
class Cat(Model):
def __init__(self):
print("Built Cat")
c: Cat = Dog.new() # type check fails
c2: Cat = Dog.build() # type check fails
c3: Cat = Dog.build_other() # passes
Considering class methods work, it feels like metaclass methods should equally be able to "unwrap" the instance type for a class, but there might be subtleties I'm missing here.
My wish list here would be:
- have the inverse of
Type(something likeInstance) so I could write something likedef build(cls: T) -> Instance[T]. This would be (I think) a bit clearer. - Have an intersection type, with which I could declare something like
def build(cls: (Type[T] & ModelMeta)) -> T. This would help with a lot of other problems as well. This seems like it would be a useful shortcut to writing subclasses - Have a magic hook in
subtypes.is_subtypethat would let me write a custom subtype check in some types
Full context: I'm writing some stubs for Django's ORM, and was hitting issues around the type of Model.objects.create. So in my case I end up needing a "class property" (hence doing things through the metaclass), which is why the simple new() call strategy hasn't been working for me.
I think this is related to #3438, but I'm having a hard time identifying the link.
Activity
elazarg commentedon Jun 28, 2017
Related to #3346 and some other discussions which I can't seem to find right now.
build_otheris not checked at all, since it is not annotated. Also thatcls()is not checked in your code, sinceType[T]is considered instantiable with arbitrary arguments.I think your first error message does not show up on master. However, it does show up if you constrain your variable "properly":
The reason is that it is unsafe:
But I think @ilevkivskyi argued that this slight unsafety is useful, as in your case. If the metaclass was somehow denied of being used outside the file, it could be even possible to check at the point of definition, and not just access.
Intersection types can only help in the implementation of the function, but at some point the arguments should be checked to see that they are of
Type[T]which is not guaranteed by the lookup.Maybe a constraint on TypeVar could have helped:
Or perhaps something like
But I'm not sure exactly what should it mean. Maybe that's equivalent to your first wish. I'm just thinking out loud.
ilevkivskyi commentedon Jun 28, 2017
Concerning this example:
I think this can be made safe if mypy will give the error at the point of use, not at the point of definition of
build. This can be probably done incheckmember.bind_self.Concerning the plugin hook, we support many plugin hooks, but I am not sure about his one. @JukkaL are there plans to support plugins for
subtypes.is_subtype?elazarg commentedon Jun 28, 2017
I'm not against the idea of checking on access, but it comes with some cost:
JukkaL commentedon Jun 28, 2017
Mypy could also reject the class
NonModeldue to metaclass conflict (because ofbuild) instead of the call toNonModel.build().There are currently no concrete plans to support subtyping plugin hooks, but the plugin system is still experimental and we can add all sorts of new hooks if they seem useful. @rtpg Feel free to create issues for any new plugin hooks that you would find useful, preferably with examples of how you'd use them.
elazarg commentedon Jun 28, 2017
Interesting... and it solves points (1) and (2) from my earlier comment.
rtpg commentedon Jun 29, 2017
I hadn't thought too deeply about adding bounds to the
TypeVar, but while stepping through mypy code to figure out what was happening in the resolution, there is already a sort of "self" check in place to ensure that the type ofselfcan properly unify with the type of the class... I imagine this could also handle meta-class checks similarly.Though in my case, knowing that I will have at least a partial solution in #3346 helps me to solve most of my worries about the type here.
rtpg commentedon Jul 20, 2017
@elazarg I just pulled in
masterand tried to run the following code, but am still hitting some issues. Perhaps I'm missing a detail:The error messages I get here are:
The first one is the system "working as intended". But I'm not quite sure what to do about those method type error messages
elazarg commentedon Jul 20, 2017
with #3227 you get the following error:
tmp.py:14: error: The erased type of self 'Type[builtins.object]' is not a supertype of its class 'tmp.ModelMeta'Which is more explanatory I believe. It's the same problem discussed in my first comment and hadn't been resolved yet.
rtpg commentedon Jul 21, 2017
ah sorry, I read your comment as saying that if I don't add bounds to the type var, then this could be setup.
This is a bit frustrating, because I feel like I'm so close to getting type checking on a pretty big part of the Django ORM but am hitting this issue, even though it feels like mypy "gets it".
I'm trying to figure out an alternative way to tackle this issue by having the type variable be on the
Managerobject instead.In this I also get the method failures, I imagine the same erased type issue is happening in this case as well?
mitar commentedon Dec 2, 2017
Another similar issue:
elazarg commentedon Dec 2, 2017
Technically there are two issues in your example: the first is the discussed issue (there is no guarantee that
selfwill be compatible withType[Foo]).The second issue is that there is no reason to assume that
Type[Foo]will have an attributename- and indeed it doesn't;type(Foo).namefails at runtime. I'm not sure why mypy doesn't complain about this error - looks like a bug to me.mitar commentedon Dec 2, 2017
Ehm,
Foo.nameworks, at least this is the idea here.Type[T]->Type[Foo]->Fooclass, soselfisFoowhich hasname.elazarg commentedon Dec 2, 2017
Indeed. Perhaps I'm confusing
type(Foo)andType[Foo](the former being the metaclass itself) for no reason.ilevkivskyi commentedon Nov 2, 2019
Note that this is closely related to #7191 (in the sense that the solution is the same, move consistency check from definition to use site, i.e. to
bind_self).Allow fancy self-types (python#7860)