team23 / pydantic-partial

Create partial models from pydantic models
MIT License
51 stars 8 forks source link

Partial classes cannot be used in type annotations #2

Open jdinunzio opened 2 years ago

jdinunzio commented 2 years ago

Issue

Using python 3.10.7, pydantic 4.3.2, pydantic-partial 0.3.2 and 0.3.3, mypy 0.971

from pydantic import BaseModel
from pydantic_partial import PartialModelMixin

class Foo(PartialModelMixin, BaseModel):
    id: int

PartialFoo = Foo.as_partial()

reveal_type(Foo())
reveal_type(PartialFoo())

def something(x: PartialFoo):
    return x.dict()

running mypy will return:

tmp/foo.py:10: note: Revealed type is "foo.Foo" tmp/foo.py:11: note: Revealed type is "foo.Foo" tmp/foo.py:13: error: Variable "foo.PartialFoo" is not valid as a type tmp/foo.py:13: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases tmp/foo.py:14: error: PartialFoo? has no attribute "dict"

(revealed type for PartialFoo() is "Any" in 0.3.2 and "foo.Foo" in 0.3.3).

Question / Request

How to use partials as type annotations? If there's a way to do it, could it be documented?

ddanier commented 2 years ago

I initially thought that adding a real class will fix this, but mypy cannot resolve the type then:

from pydantic import BaseModel
from pydantic_partial import PartialModelMixin

class Foo(PartialModelMixin, BaseModel):
    id: int

class PartialFoo(Foo.as_partial()):
    pass

reveal_type(Foo())
reveal_type(PartialFoo())

def something(x: PartialFoo):
    return x.dict()

...this will still give you test.py:7: error: Unsupported dynamic base class "Foo.as_partial" as an error.

The main issue is, that we are constructing a type during runtime. This dynamic type is not supported by mypy at all.
(see https://github.com/python/mypy/issues/2477 for some background)

I tried to mark PartialModelMixin.as_partial() and create_partial_model(...) to return the same type, so for the type checker the class PartialFoo is basically the same as Foo. This is due to the fact that the dynamically changed type cannot be defined in the Python typing system. Sadly this seems to not be enough for mypy.

You can work around this issue by tricking mypy into not seeing the conversion to partial at all. Note however that this still means all partial model instances will just be seen as instanced of Foo.

See the following example code:

from typing import TYPE_CHECKING

from pydantic import BaseModel
from pydantic_partial import PartialModelMixin

class Foo(PartialModelMixin, BaseModel):
    id: int

if TYPE_CHECKING:
    PartialFoo = Foo
else:
    PartialFoo = Foo.as_partial()

reveal_type(Foo())
reveal_type(PartialFoo())

def something(x: PartialFoo):
    return x.dict()

Which will produce the following mypy output:

test.py:14: note: Revealed type is "test.Foo"
test.py:15: note: Revealed type is "test.Foo"
Success: no issues found in 1 source file
ddanier commented 2 years ago

Side note: If anyone knows a better way to solve this or how to define the types in this library, please feel free to send me some suggestions or even a pull request. ;-)

ddanier commented 2 years ago

Currently my feeling is I have to write a mypy plugin... 🤷‍♂️🤔

jdinunzio commented 2 years ago

The ideal solution in my mind would look something like


PartialFoo = Partial[Foo]

if only typing.Generic would allow being sub-classed into a meta-class, but sadly that seems not to be an option.

ddanier commented 2 years ago

@jdinunzio Yeah, that would be the best syntax. Sadly using __class_getitem__ will confuse mypy and at least PyCharm. But I will see what I can do when thinking about a plugin. Have to read into this first though....so this will probably take some time.

If anyone else is interested in solving this a PR would be very much appreciated. 👍
(but please drop me a note here - as I will do when I start producing some real code)

ddanier commented 2 years ago

About Partial[...], see https://github.com/python/mypy/issues/11501
(mypy currently does always expect __class_getitem__ to be used for generic types)

ddanier commented 1 year ago

Note: pydantic itself thought about adding partial support, but then decided to not do this for now. Reason is - like with this ticket - that there is no good way to get the typing definition done, as there is no partial equivalent in the python typing system now. As of this I will do the same and kind of ignore the fact that pydantic-partial will not (and kind of cannot) produce partial models in a way type checkers could recognise. There is just no base typing mechanism to support this.

See https://github.com/pydantic/pydantic/issues/1673#issuecomment-1557267229 for reference.

Note: I will keep this issue open to have this documented. It still is an open issue - but just one we cannot resolve in a good way.