Created
June 16, 2022 19:00
-
-
Save asottile-sentry/c48af22c57b39f4a7cb4f84c08fd4bf4 to your computer and use it in GitHub Desktop.
mypy plugin weirdness
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[mypy] | |
plugins = mypy_plugin.py | |
[mypy-django.*] | |
ignore_missing_imports = True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from __future__ import annotations | |
from typing import Callable | |
from mypy.plugin import AttributeContext | |
from mypy.plugin import Plugin | |
from mypy.types import CallableType | |
from mypy.types import Instance | |
from mypy.types import Type | |
from mypy.types import TypeType | |
from mypy.types import UnionType | |
def _get_objects_type(ctx: AttributeContext) -> Type: | |
def _type_to_res(target_cls: Instance) -> Type: | |
# TODO: this should check if `target_cls` is a `Model` | |
real_type = target_cls.type.get('objects') | |
# could not find the `objects` definition. | |
# probably not our custom model -- probably a django model | |
if real_type is None or not isinstance(real_type.type, Instance): | |
return ctx.default_attr_type | |
else: | |
return Instance(real_type.type.type, (target_cls,)) | |
# early-phase parses will treat these as `() -> TheType` | |
if isinstance(ctx.type, CallableType) and isinstance(ctx.type.ret_type, Instance): | |
return _type_to_res(ctx.type.ret_type) | |
# later-phase parses will treat these as `type[TheType]` | |
elif isinstance(ctx.type, TypeType) and isinstance(ctx.type.item, Instance): | |
return _type_to_res(ctx.type.item) | |
# handle unions | |
elif isinstance(ctx.type, UnionType): | |
object_types = [] | |
for tp in ctx.type.items: | |
if isinstance(tp, TypeType) and isinstance(tp.item, Instance): | |
object_types.append(_type_to_res(tp.item)) | |
else: | |
ctx.api.fail('objects plugin bug 001', ctx.context) | |
return ctx.default_attr_type | |
return UnionType(object_types) | |
else: | |
ctx.api.fail('objects plugin bug 002', ctx.context) | |
return ctx.default_attr_type | |
class CustomPlugin(Plugin): | |
def get_class_attribute_hook( | |
self, | |
fullname: str, | |
) -> Callable[[AttributeContext], Type] | None: | |
if fullname.endswith('.objects'): | |
return _get_objects_type | |
else: | |
return None | |
def plugin(version: str) -> type[Plugin]: | |
return CustomPlugin |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$ mypy t.py | |
t.py:34: note: Revealed type is "t.BaseManager[t.Model1]" | |
t.py:35: note: Revealed type is "t.Model1" | |
t.py:36: note: Revealed type is "t.CustomManager[t.CustomModel]" | |
t.py:37: note: Revealed type is "t.CustomModel" | |
t.py:38: note: Revealed type is "t.CustomModel" | |
Success: no issues found in 1 source file |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$ mypy t.py | |
t.py:34: note: Revealed type is "t.BaseManager[t.Model1]" | |
t.py:35: note: Revealed type is "t.Model1" | |
t.py:36: error: Access to generic instance variables via class is ambiguous | |
t.py:36: note: Revealed type is "t.BaseManager[t.CustomModel]" | |
t.py:37: error: Access to generic instance variables via class is ambiguous | |
t.py:37: note: Revealed type is "t.CustomModel" | |
t.py:38: error: Access to generic instance variables via class is ambiguous | |
t.py:38: note: Revealed type is "Any" | |
Found 3 errors in 1 file (checked 1 source file) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from __future__ import annotations | |
from typing import Any | |
from typing import Generic | |
from typing import TypeVar | |
from django.db.models.manager import BaseManager as DjangoBaseManager | |
M = TypeVar('M') | |
class BaseManager(DjangoBaseManager, Generic[M]): | |
# uncomment this line for some reason to break everything | |
# def __init__(self) -> None: ... | |
def get(self, id: int) -> M: ... | |
class BaseModel: | |
objects: BaseManager[Any] = BaseManager() | |
class Model1(BaseModel): | |
... | |
class CustomManager(BaseManager['CustomModel']): | |
def m(self) -> CustomModel: ... | |
class CustomModel(BaseModel): | |
objects = CustomManager() | |
reveal_type(Model1.objects) # should be `BaseModel[Model1]` | |
reveal_type(Model1.objects.get(1)) # should be `Model1` | |
reveal_type(CustomModel.objects) # should be `CustomManager` | |
reveal_type(CustomModel.objects.get(1)) # should be `CustomModel` | |
reveal_type(CustomModel.objects.m()) # should be `CustomModel` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment