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

Update to newer django-stubs and remove hack #7

Merged
merged 1 commit into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions extended_mypy_django_plugin/plugin/_helpers.py

This file was deleted.

72 changes: 1 addition & 71 deletions extended_mypy_django_plugin/plugin/_plugin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import collections
import sys
from typing import Generic

Expand All @@ -9,7 +8,6 @@
from mypy.plugin import (
AnalyzeTypeContext,
AttributeContext,
ClassDefContext,
DynamicClassDefContext,
FunctionContext,
FunctionSigContext,
Expand All @@ -28,16 +26,7 @@
)
from typing_extensions import assert_never

from . import (
_config,
_dependencies,
_helpers,
_hook,
_known_annotations,
_reports,
_store,
actions,
)
from . import _config, _dependencies, _hook, _known_annotations, _reports, _store, actions


class Hook(
Expand Down Expand Up @@ -151,65 +140,6 @@ def get_additional_deps(self, file: MypyFile) -> list[tuple[int, str, int]]:
)
return results

@_hook.hook
class get_base_class_hook(Hook[ClassDefContext, None]):
"""
We need to make up for a bug in django-stubs
"""

def choose(self) -> bool:
if self.super_hook is None:
return False

if _helpers.get_is_abstract_model() is None:
return False

sym = self.plugin.lookup_fully_qualified(self.fullname)
return bool(
sym is not None
and isinstance(sym.node, TypeInfo)
and _helpers.is_model_type(sym.node)
)

def run(self, ctx: ClassDefContext) -> None:
if self.super_hook is None:
return None

# Copy the code in django-stubs that crashes
# And fill in the missing information before continuing
processed_models = set()
model_bases = collections.deque([ctx.cls])
while model_bases:
model = model_bases.popleft()

try:
# Whether this causes an AssertionError or an AttributeError depends
# on whether mypy is compiled or not
# Note that this only appears to trigger on followup changes with a cache
# in very specific situations
for base in model.info.bases:
break
except AssertionError as exc:
if str(exc) == "ClassDef is lacking info":
sym = self.plugin.lookup_fully_qualified(model.fullname)
if sym and isinstance(sym.node, TypeInfo):
model.info = sym.node
except AttributeError as exc:
if str(exc) == "attribute 'bases' of 'TypeInfo' undefined":
sym = self.plugin.lookup_fully_qualified(model.fullname)
if sym and isinstance(sym.node, TypeInfo):
model.info = sym.node

for base in model.info.bases:
if (
_helpers.is_abstract_model(base.type)
and base.type.fullname not in processed_models
):
model_bases.append(base.type.defn)
processed_models.add(base.type.fullname)

return self.super_hook(ctx)

@_hook.hook
class get_dynamic_class_hook(Hook[DynamicClassDefContext, None]):
"""
Expand Down
12 changes: 8 additions & 4 deletions extended_mypy_django_plugin/plugin/_store.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import importlib.metadata
from collections.abc import Iterator, Mapping, Sequence
from typing import Protocol

Expand All @@ -8,9 +9,12 @@

from ._reports import ModelModules

QUERYSET_CLASS_FULLNAME = "django.db.models.query._QuerySet"
MODEL_CLASS_FULLNAME = "django.db.models.base.Model"

QUERYSET_CLASS_FULLNAME = "django.db.models.query.QuerySet"
if importlib.metadata.version("mypy") == "1.4.0":
QUERYSET_CLASS_FULLNAME = "django.db.models.query._QuerySet"


class UnionMustBeOfTypes(Exception):
pass
Expand Down Expand Up @@ -99,7 +103,7 @@ def realise_querysets(
for fullname, model in querysets:
queryset = lookup_info(fullname)
if not queryset:
raise RestartDmypy()
raise RestartDmypy(f"Could not find queryset for {fullname}")

if not queryset.is_generic():
yield Instance(queryset, [])
Expand Down Expand Up @@ -184,7 +188,7 @@ def _get_dynamic_manager(
"""
model_cls = self._get_model_class_by_fullname(model.fullname)
if model_cls is None:
raise RestartDmypy()
raise RestartDmypy(f"Could not find model class for {model.fullname}")

manager = model_cls._default_manager
if manager is None:
Expand All @@ -203,7 +207,7 @@ def _get_dynamic_manager(

base_manager_info = lookup_info(base_manager_fullname)
if not base_manager_info:
raise RestartDmypy()
raise RestartDmypy(f"Could not find base manager for {base_manager_fullname}")

metadata = base_manager_info.metadata

Expand Down
8 changes: 4 additions & 4 deletions extended_mypy_django_plugin/plugin/actions/_type_analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ def find_concrete_querysets(self, unbound_type: UnboundType) -> MypyType:

try:
querysets = tuple(self.store.realise_querysets(UnionType(concrete), self.lookup_info))
except _store.RestartDmypy:
self.api.fail("You probably need to restart dmypy", unbound_type)
except _store.RestartDmypy as err:
self.api.fail(f"You probably need to restart dmypy: {err}", unbound_type)
return AnyType(TypeOfAny.from_error)
except _store.UnionMustBeOfTypes:
self.api.fail("Union must be of instances of models", unbound_type)
Expand All @@ -93,8 +93,8 @@ def find_default_queryset(self, unbound_type: UnboundType) -> MypyType:

try:
querysets = tuple(self.store.realise_querysets(type_arg, self.lookup_info))
except _store.RestartDmypy:
self.api.fail("You probably need to restart dmypy", unbound_type)
except _store.RestartDmypy as err:
self.api.fail(f"You probably need to restart dmypy: {err}", unbound_type)
return AnyType(TypeOfAny.from_error)
except _store.UnionMustBeOfTypes:
self.api.fail("Union must be of instances of models", unbound_type)
Expand Down
8 changes: 4 additions & 4 deletions extended_mypy_django_plugin/plugin/actions/_type_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,8 @@ def get_concrete_queryset_return_type(
assert isinstance(concrete, Instance)
try:
result.extend(self.store.realise_querysets(concrete, self.lookup_info))
except _store.RestartDmypy:
self.api.fail("You probably need to restart dmypy", context)
except _store.RestartDmypy as err:
self.api.fail(f"You probably need to restart dmypy: {err}", context)
return AnyType(TypeOfAny.from_error)
except _store.UnionMustBeOfTypes:
self.api.fail("Union must be of instances of models", context)
Expand All @@ -326,8 +326,8 @@ def get_default_queryset_return_type(
) -> MypyType:
try:
querysets = tuple(self.store.realise_querysets(instances, self.lookup_info))
except _store.RestartDmypy:
self.api.fail("You probably need to restart dmypy", context)
except _store.RestartDmypy as err:
self.api.fail(f"You probably need to restart dmypy: {err}", context)
return AnyType(TypeOfAny.from_error)
except _store.UnionMustBeOfTypes:
self.api.fail("Union must be of instances of models", context)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ authors = [
[project.optional-dependencies]
stubs-latest = [
"mypy==1.10.0",
"django-stubs==5.0.0",
"django-stubs==5.0.2",
]
stubs-older = [
"mypy==1.4.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ def __init__(

self.target_file = target_file

def _normalise_message(self, message: str) -> str:
if importlib.metadata.version("mypy") == "1.4.0":
return message.replace("type[", "Type[").replace(
"django.db.models.query.QuerySet", "django.db.models.query._QuerySet"
)
else:
return message

def clear(self) -> Self:
self._build.result.clear()
return self
Expand All @@ -63,6 +71,11 @@ def on(self, path: str) -> Self:
return self.__class__(build=self._build, target_file=path)

def from_out(self, out: str, regex: bool = False) -> Self:
if importlib.metadata.version("mypy") == "1.4.0":
out = out.replace("type[", "Type[").replace(
"django.db.models.query.QuerySet", "django.db.models.query._QuerySet"
)

self._build.result.extend(
extract_output_matchers_from_out(
out, {}, regex=regex, for_daemon=self._build.for_daemon
Expand All @@ -71,17 +84,16 @@ def from_out(self, out: str, regex: bool = False) -> Self:
return self

def add_revealed_type(self, lnum: int, revealed_type: str) -> Self:
if importlib.metadata.version("mypy") == "1.4.0":
revealed_type = revealed_type.replace("type[", "Type[")
revealed_type = self._normalise_message(revealed_type)

assert self.target_file is not None
self._build.add(
self.target_file, lnum, None, "note", f'Revealed type is "{revealed_type}"'
)
return self

def change_revealed_type(self, lnum: int, message: str) -> Self:
if importlib.metadata.version("mypy") == "1.4.0":
message = message.replace("type[", "Type[")
message = self._normalise_message(message)

assert self.target_file is not None

Expand Down Expand Up @@ -122,11 +134,15 @@ def remove_errors(self, lnum: int) -> Self:
return self

def add_error(self, lnum: int, error_type: str, message: str) -> Self:
message = self._normalise_message(message)

assert self.target_file is not None
self._build.add(self.target_file, lnum, None, "error", f"{message} [{error_type}]")
return self

def remove_from_revealed_type(self, lnum: int, remove: str) -> Self:
remove = self._normalise_message(remove)

assert self.target_file is not None

found: list[FileOutputMatcher] = []
Expand Down
6 changes: 3 additions & 3 deletions tests/test_concrete_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def check_instance_with_type_guard(cls: Parent) -> TypeGuard[Concrete[Parent]]:
)
.add_revealed_type(
10,
"Union[django.db.models.query._QuerySet[myapp.models.Child1, myapp.models.Child1], myapp.models.Child2QuerySet, django.db.models.query._QuerySet[myapp.models.Child3, myapp.models.Child3], django.db.models.query._QuerySet[myapp2.models.ChildOther, myapp2.models.ChildOther]]",
"Union[django.db.models.query.QuerySet[myapp.models.Child1, myapp.models.Child1], myapp.models.Child2QuerySet, django.db.models.query.QuerySet[myapp.models.Child3, myapp.models.Child3], django.db.models.query.QuerySet[myapp2.models.ChildOther, myapp2.models.ChildOther]]",
)
.add_revealed_type(
20,
Expand Down Expand Up @@ -83,7 +83,7 @@ def _(expected: OutputBuilder) -> None:
)
.add_revealed_type(
9,
"Union[django.db.models.query._QuerySet[myapp.models.Child1, myapp.models.Child1], myapp.models.Child2QuerySet, django.db.models.query._QuerySet[myapp.models.Child3, myapp.models.Child3], django.db.models.query._QuerySet[myapp2.models.ChildOther, myapp2.models.ChildOther]]",
"Union[django.db.models.query.QuerySet[myapp.models.Child1, myapp.models.Child1], myapp.models.Child2QuerySet, django.db.models.query.QuerySet[myapp.models.Child3, myapp.models.Child3], django.db.models.query.QuerySet[myapp2.models.ChildOther, myapp2.models.ChildOther]]",
)
)

Expand All @@ -101,7 +101,7 @@ def _(expected: OutputBuilder) -> None:
)
.remove_from_revealed_type(
9,
", django.db.models.query._QuerySet[myapp2.models.ChildOther, myapp2.models.ChildOther]",
", django.db.models.query.QuerySet[myapp2.models.ChildOther, myapp2.models.ChildOther]",
)
)

Expand Down
2 changes: 1 addition & 1 deletion tests/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,6 @@ def method_with_instance_typeguard(
line
for line in out.split("\n")
if "Only concrete class can be given" not in line
).replace('Revealed type is "type', 'Revealed type is "Type')
)

expected.from_out(out)
28 changes: 19 additions & 9 deletions tests/test_works.yml → tests/test_works.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
- case: mypy_path_from_env
start: .
disable_cache: true
out: |
from extended_mypy_django_plugin_test_driver import OutputBuilder, Scenario


def test_works(scenario: Scenario) -> None:
out = """
main:33: note: Revealed type is "Union[django.db.models.manager.Manager[myapp.models.Child1], myapp.models.ManagerFromChild2QuerySet[myapp.models.Child2], django.db.models.manager.Manager[myapp.models.Child3], django.db.models.manager.Manager[myapp2.models.ChildOther]]"
main:38: note: Revealed type is "myapp.models.Child1"
main:41: note: Revealed type is "Union[django.db.models.query._QuerySet[myapp.models.Child1, myapp.models.Child1], myapp.models.Child2QuerySet, django.db.models.query._QuerySet[myapp.models.Child3, myapp.models.Child3], django.db.models.query._QuerySet[myapp2.models.ChildOther, myapp2.models.ChildOther]]"
main:44: note: Revealed type is "django.db.models.query._QuerySet[myapp.models.Child1, myapp.models.Child1]"
main:41: note: Revealed type is "Union[django.db.models.query.QuerySet[myapp.models.Child1, myapp.models.Child1], myapp.models.Child2QuerySet, django.db.models.query.QuerySet[myapp.models.Child3, myapp.models.Child3], django.db.models.query.QuerySet[myapp2.models.ChildOther, myapp2.models.ChildOther]]"
main:44: note: Revealed type is "django.db.models.query.QuerySet[myapp.models.Child1, myapp.models.Child1]"
main:47: note: Revealed type is "myapp.models.Child2QuerySet"
main:48: note: Revealed type is "myapp.models.Child2QuerySet"
main:49: note: Revealed type is "myapp.models.ManagerFromChild2QuerySet[myapp.models.Child2]"
main:50: note: Revealed type is "myapp.models.Child2QuerySet[myapp.models.Child2]"
main:53: note: Revealed type is "django.db.models.query._QuerySet[myapp.models.Child1, myapp.models.Child1]"
main:53: note: Revealed type is "django.db.models.query.QuerySet[myapp.models.Child1, myapp.models.Child1]"
main:56: note: Revealed type is "myapp.models.Child2QuerySet"
main:59: note: Revealed type is "Union[myapp.models.Child2QuerySet, django.db.models.query._QuerySet[myapp.models.Child1, myapp.models.Child1]]"
main: |
main:59: note: Revealed type is "Union[myapp.models.Child2QuerySet, django.db.models.query.QuerySet[myapp.models.Child1, myapp.models.Child1]]"
"""

main = """
from extended_mypy_django_plugin import Concrete, ConcreteQuerySet, DefaultQuerySet

from myapp.models import Parent, Child1, Child2
Expand Down Expand Up @@ -73,3 +76,10 @@ def ones(model: type[Concrete[Parent]]) -> list[str]:

tvqsmult = make_multiple_queryset(Child1)
reveal_type(tvqsmult)
"""

@scenario.run_and_check_mypy_after
def _(expected: OutputBuilder) -> None:
scenario.make_file("main.py", main)

expected.from_out(out)
Loading