From 9479e7dfa05203be3f108ce75c68e883de39c627 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Sat, 9 May 2026 19:02:45 +1000 Subject: [PATCH] Route direct Django access through DjangoContext Add wrapper methods on DjangoContext that absorb every direct `_meta`, `apps_registry`, and `settings` access scattered across `main.py`, `lib/helpers.py`, and the transformers. --- mypy_django_plugin/django/context.py | 117 +++++++++++++++++-- mypy_django_plugin/lib/helpers.py | 6 +- mypy_django_plugin/main.py | 15 ++- mypy_django_plugin/transformers/auth.py | 2 +- mypy_django_plugin/transformers/fields.py | 14 +-- mypy_django_plugin/transformers/meta.py | 13 +-- mypy_django_plugin/transformers/models.py | 14 +-- mypy_django_plugin/transformers/querysets.py | 84 +++++++------ mypy_django_plugin/transformers/settings.py | 2 +- 9 files changed, 187 insertions(+), 80 deletions(-) diff --git a/mypy_django_plugin/django/context.py b/mypy_django_plugin/django/context.py index 5229d838a..490f16c45 100644 --- a/mypy_django_plugin/django/context.py +++ b/mypy_django_plugin/django/context.py @@ -116,14 +116,117 @@ def __init__(self, django_settings_module: str) -> None: self.django_settings_module = django_settings_module apps, settings = initialize_django(self.django_settings_module) - self.apps_registry = apps - self.settings = settings + self._apps_registry = apps + self._settings = settings + + # ---- Registry wrappers ---- + + def get_model_class_by_label(self, label: str) -> type[Model] | None: + try: + return self._apps_registry.get_model(label) + except LookupError: + return None + + def is_app_installed(self, dotted_or_label: str) -> bool: + return self._apps_registry.is_installed(dotted_or_label) + + # ---- Settings wrappers ---- + + def get_setting(self, name: str, default: Any = None) -> Any: + return getattr(self._settings, name, default) + + def has_setting(self, name: str) -> bool: + return hasattr(self._settings, name) + + @cached_property + def auth_user_model_label(self) -> str: + return self._settings.AUTH_USER_MODEL + + @cached_property + def default_auto_field_path(self) -> str: + return self._settings.DEFAULT_AUTO_FIELD + + # ---- Single-model meta wrappers ---- + + def get_field_on_model(self, model_cls: type[Model], field_name: str) -> Field[Any, Any] | ForeignObjectRel | None: + try: + return model_cls._meta.get_field(field_name) + except FieldDoesNotExist: + return None + + def is_model_abstract(self, model_cls: type[Model]) -> bool: + return model_cls._meta.abstract + + def get_proxy_target(self, model_cls: type[Model]) -> type[Model] | None: + return model_cls._meta.proxy_for_model + + def get_auto_field(self, model_cls: type[Model]) -> Field[Any, Any] | None: + return model_cls._meta.auto_field + + def get_pk_field_or_none(self, model_cls: type[Model]) -> Field[Any, Any] | None: + # Non-raising parallel of `get_primary_key_field`. + return model_cls._meta.pk + + def get_pk_fields(self, model_cls: type[Model]) -> tuple[Field[Any, Any], ...]: + # Composite-PK ready (Django 5.2+); falls back to the single PK on older Django. + pk_fields = getattr(model_cls._meta, "pk_fields", None) + if pk_fields is not None: + return tuple(pk_fields) + pk = self.get_pk_field_or_none(model_cls) + return (pk,) if pk is not None else () + + def get_model_parents(self, model_cls: type[Model]) -> Sequence[type[Model]]: + # Wraps `_meta.all_parents` (Django 5.2+) / `_meta.get_parent_list()` for older Django. + opts = model_cls._meta + parents = getattr(opts, "all_parents", None) + if parents is not None: + return list(parents) + return opts.get_parent_list() + + def iter_reverse_relations(self, model_cls: type[Model]) -> Iterator[ForeignObjectRel]: + # Public-API equivalent of `_meta.related_objects` (private API). + # Matches `include_hidden=True` to preserve behaviour for hidden M2M etc. + for field in model_cls._meta.get_fields(include_hidden=True): + if isinstance(field, ForeignObjectRel): + yield field + + # ---- Manager wrappers ---- + + def iter_managers(self, model_cls: type[Model]) -> Iterator[tuple[str, models.Manager[Any]]]: + yield from model_cls._meta.managers_map.items() + + def get_default_manager_class(self, model_cls: type[Model]) -> type[models.Manager[Any]]: + return model_cls._meta.default_manager.__class__ # type: ignore[return-value] + + # ---- Query helpers ---- + + def resolve_names_to_path(self, model_cls: type[Model], parts: Sequence[str]) -> tuple[_AnyField, Sequence[str]]: + """Wrap `Query(model).names_to_path(parts, _meta)` so callers don't touch `_meta`. + + Returns `(final_field, remainder)`. Raises Django's `FieldError` for the caller + to translate into a user-facing diagnostic. + """ + _, final_field, _, remainder = Query(model_cls).names_to_path(parts, model_cls._meta) + return final_field, remainder + + def iter_select_related_choices(self, model_cls: type[Model]) -> Iterator[str]: + """Names valid for `QuerySet.select_related(...)` on this model. + + Mirrors Django's `SQLCompiler.get_related_selections._get_field_choices`: + forward FK/O2O field names plus reverse-unique relation query names. + """ + for field in model_cls._meta.fields: + if field.is_relation: + yield field.name + for rel in model_cls._meta.related_objects: + if rel.field.unique: + yield rel.field.related_query_name() @cached_property def model_modules(self) -> dict[str, dict[str, type[Model]]]: """All modules that contain Django models.""" modules: dict[str, dict[str, type[Model]]] = defaultdict(dict) - for concrete_model_cls in self.apps_registry.get_models(include_auto_created=True, include_swapped=True): + for concrete_model_cls in self._apps_registry.get_models(include_auto_created=True, include_swapped=True): modules[concrete_model_cls.__module__][concrete_model_cls.__name__] = concrete_model_cls # collect abstract=True models for model_cls in concrete_model_cls.mro()[1:]: @@ -197,7 +300,7 @@ def get_primary_key_field(self, model_cls: type[Model]) -> Field[Any, Any]: raise ValueError("No primary key defined") def get_expected_types(self, api: TypeChecker, model_cls: type[Model], *, method: str) -> dict[str, MypyType]: - contenttypes_in_apps = self.apps_registry.is_installed("django.contrib.contenttypes") + contenttypes_in_apps = self._apps_registry.is_installed("django.contrib.contenttypes") expected_types = {} # add pk if not abstract=True @@ -272,7 +375,7 @@ def get_expected_types(self, api: TypeChecker, model_cls: type[Model], *, method @cached_property def all_registered_model_classes(self) -> set[type[models.Model]]: - model_classes = self.apps_registry.get_models() + model_classes = self._apps_registry.get_models() all_model_bases = set() for model_cls in model_classes: @@ -387,7 +490,7 @@ def get_field_related_model_cls(self, field: RelatedField[Any, Any] | ForeignObj raise UnregisteredModelError else: try: - related_model_cls = self.apps_registry.get_model(related_model_cls) + related_model_cls = self._apps_registry.get_model(related_model_cls) except LookupError as e: raise UnregisteredModelError from e @@ -577,4 +680,4 @@ def resolve_f_expression_type(self, f_expression_type: Instance) -> ProperType: @cached_property def is_contrib_auth_installed(self) -> bool: - return "django.contrib.auth" in self.settings.INSTALLED_APPS + return "django.contrib.auth" in self._settings.INSTALLED_APPS diff --git a/mypy_django_plugin/lib/helpers.py b/mypy_django_plugin/lib/helpers.py index 0450e2fd6..63bdf311d 100644 --- a/mypy_django_plugin/lib/helpers.py +++ b/mypy_django_plugin/lib/helpers.py @@ -606,8 +606,8 @@ def resolve_string_attribute_value(attr_expr: Expression, django_context: Django if isinstance(attr_expr, MemberExpr): member_name = attr_expr.name if isinstance(attr_expr.expr, NameExpr) and attr_expr.expr.fullname == "django.conf.settings": - if hasattr(django_context.settings, member_name): - return getattr(django_context.settings, member_name) # type: ignore[no-any-return] + if django_context.has_setting(member_name): + return django_context.get_setting(member_name) # type: ignore[no-any-return] return None @@ -736,7 +736,7 @@ def get_model_from_expression( and isinstance(expr.expr, NameExpr) and f"{expr.expr.fullname}.{expr.name}" == fullnames.AUTH_USER_MODEL_FULLNAME ): - lazy_reference = django_context.settings.AUTH_USER_MODEL + lazy_reference = django_context.auth_user_model_label if lazy_reference is not None: model_info = resolve_lazy_reference(lazy_reference, api=api, django_context=django_context, ctx=expr) diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index 0a6c53afe..ccaeee1e3 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -104,13 +104,12 @@ def get_additional_deps(self, file: MypyFile) -> list[tuple[int, str, int]]: # for `get_user_model()` if file.fullname == "django.contrib.auth" or file.fullname in {"django.http", "django.http.request"}: - auth_user_model_name = self.django_context.settings.AUTH_USER_MODEL - try: - auth_user_module = self.django_context.apps_registry.get_model(auth_user_model_name).__module__ - except LookupError: + auth_user_model_name = self.django_context.auth_user_model_label + auth_user_model = self.django_context.get_model_class_by_label(auth_user_model_name) + if auth_user_model is None: # get_user_model() model app is not installed return [] - return [self._new_dependency(auth_user_module), self._new_dependency("django_stubs_ext")] + return [self._new_dependency(auth_user_model.__module__), self._new_dependency("django_stubs_ext")] # ensure that all mentions to='someapp.SomeModel' are loaded with corresponding related Fields defined_model_classes = self.django_context.model_modules.get(file.fullname) @@ -122,8 +121,8 @@ def get_additional_deps(self, file: MypyFile) -> list[tuple[int, str, int]]: for field in itertools.chain( # forward relations self.django_context.get_model_related_fields(model_class), - # reverse relations - `related_objects` is private API (according to docstring) - model_class._meta.related_objects, + # reverse relations + self.django_context.iter_reverse_relations(model_class), ): try: related_model_cls = self.django_context.get_field_related_model_cls(field) # type: ignore[arg-type] @@ -349,7 +348,7 @@ def get_dynamic_class_hook(self, fullname: str) -> Callable[[DynamicClassDefCont def report_config_data(self, ctx: ReportConfigContext) -> dict[str, Any]: # Cache would be cleared if any settings do change. extra_data = { - "AUTH_USER_MODEL": self.django_context.settings.AUTH_USER_MODEL, + "AUTH_USER_MODEL": self.django_context.auth_user_model_label, "django_version": importlib.metadata.version("django"), "django_stubs_version": importlib.metadata.version("django-stubs"), } diff --git a/mypy_django_plugin/transformers/auth.py b/mypy_django_plugin/transformers/auth.py index eeafb6618..7c9e89b32 100644 --- a/mypy_django_plugin/transformers/auth.py +++ b/mypy_django_plugin/transformers/auth.py @@ -30,7 +30,7 @@ def get_user_model(ctx: AnalyzeTypeContext, django_context: DjangoContext) -> My if not django_context.is_contrib_auth_installed: return _get_abstract_base_user(ctx.api.api) - auth_user_model = django_context.settings.AUTH_USER_MODEL + auth_user_model = django_context.auth_user_model_label model_info = helpers.resolve_lazy_reference( auth_user_model, api=ctx.api.api, django_context=django_context, ctx=ctx.context ) diff --git a/mypy_django_plugin/transformers/fields.py b/mypy_django_plugin/transformers/fields.py index 2eb8b7ec7..51e49bfd2 100644 --- a/mypy_django_plugin/transformers/fields.py +++ b/mypy_django_plugin/transformers/fields.py @@ -2,7 +2,6 @@ from typing import TYPE_CHECKING, Any, NamedTuple, cast -from django.core.exceptions import FieldDoesNotExist from django.db.models.fields import AutoField, Field from django.db.models.fields.related import RelatedField from mypy.nodes import AssignmentStmt, NameExpr, TypeInfo @@ -42,10 +41,7 @@ def _get_current_field_from_assignment( if model_cls is None: return None - try: - return model_cls._meta.get_field(field_name) - except FieldDoesNotExist: - return None + return django_context.get_field_on_model(model_cls, field_name) def reparametrize_related_field_type(related_field_type: Instance, set_type: MypyType, get_type: MypyType) -> Instance: @@ -72,11 +68,11 @@ def fill_descriptor_types_for_related_field(ctx: FunctionContext, django_context # self reference with abstract=True on the model where ForeignKey is defined current_model_cls = current_field.model - if current_model_cls._meta.abstract and current_model_cls == related_model_cls: + if django_context.is_model_abstract(current_model_cls) and current_model_cls == related_model_cls: # for all derived non-abstract classes, set variable with this name to # __get__/__set__ of ForeignKey of derived model for model_cls in django_context.all_registered_model_classes: - if issubclass(model_cls, current_model_cls) and not model_cls._meta.abstract: + if issubclass(model_cls, current_model_cls) and not django_context.is_model_abstract(model_cls): derived_model_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), model_cls) if derived_model_info is not None: fk_ref_type = Instance(derived_model_info, []) @@ -87,8 +83,8 @@ def fill_descriptor_types_for_related_field(ctx: FunctionContext, django_context related_model = related_model_cls related_model_to_set = related_model_cls - if related_model_to_set._meta.proxy_for_model is not None: - related_model_to_set = related_model_to_set._meta.proxy_for_model + if (proxy_target := django_context.get_proxy_target(related_model_to_set)) is not None: + related_model_to_set = proxy_target typechecker_api = helpers.get_typechecker_api(ctx) diff --git a/mypy_django_plugin/transformers/meta.py b/mypy_django_plugin/transformers/meta.py index f063c66ab..c5efaffe1 100644 --- a/mypy_django_plugin/transformers/meta.py +++ b/mypy_django_plugin/transformers/meta.py @@ -2,7 +2,6 @@ from typing import TYPE_CHECKING -from django.core.exceptions import FieldDoesNotExist from mypy.types import AnyType, Instance, TypeOfAny, get_proper_type from mypy.types import Type as MypyType @@ -31,12 +30,12 @@ def return_proper_field_type_from_get_field(ctx: MethodContext, django_context: if (django_model := DjangoModel.from_model_type(model_type, django_context)) is None: return ctx.default_return_type - try: - field = django_model.cls._meta.get_field(field_name) - if field_info := helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), field.__class__): - return Instance(field_info, []) - except FieldDoesNotExist as e: - ctx.api.fail(str(e), ctx.context) + field = django_context.get_field_on_model(django_model.cls, field_name) + if field is None: + ctx.api.fail(f"{django_model.cls.__name__} has no field named {field_name!r}", ctx.context) return AnyType(TypeOfAny.from_error) + if field_info := helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), field.__class__): + return Instance(field_info, []) + return ctx.default_return_type diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py index ddae84828..626fd7c83 100644 --- a/mypy_django_plugin/transformers/models.py +++ b/mypy_django_plugin/transformers/models.py @@ -278,7 +278,7 @@ def run(self) -> None: class AddDefaultPrimaryKey(ModelClassInitializer): @override def run_with_model_cls(self, model_cls: type[Model]) -> None: - auto_field = model_cls._meta.auto_field + auto_field = self.django_context.get_auto_field(model_cls) if auto_field: self.create_autofield( auto_field=auto_field, @@ -309,8 +309,8 @@ class AddPrimaryKeyAlias(AddDefaultPrimaryKey): @override def run_with_model_cls(self, model_cls: type[Model]) -> None: # We also need to override existing `pk` definition from `stubs`: - auto_field = model_cls._meta.pk - if auto_field is not None: # type: ignore[comparison-overlap] + auto_field = self.django_context.get_pk_field_or_none(model_cls) + if auto_field is not None: self.create_autofield( auto_field=auto_field, dest_name="pk", @@ -336,7 +336,7 @@ def run_with_model_cls(self, model_cls: type[Model]) -> None: self.add_new_var_to_model_class(field.attname, AnyType(TypeOfAny.explicit)) continue - if related_model_cls._meta.abstract: + if self.django_context.is_model_abstract(related_model_cls): continue rel_target_field = self.django_context.get_related_target_field(related_model_cls, field) @@ -383,7 +383,7 @@ def run_with_model_cls(self, model_cls: type[Model]) -> None: manager_info: TypeInfo | None incomplete_manager_defs = set() - for manager_name, manager in model_cls._meta.managers_map.items(): + for manager_name, manager in self.django_context.iter_managers(model_cls): manager_node = self.model_classdef.info.get(manager_name) manager_fullname = helpers.get_class_fullname(manager.__class__) manager_info = self.lookup_manager(manager_fullname, manager) @@ -483,7 +483,7 @@ def run_with_model_cls(self, model_cls: type[Model]) -> None: if "_default_manager" in self.model_classdef.info.names: return None - default_manager_cls = model_cls._meta.default_manager.__class__ + default_manager_cls = self.django_context.get_default_manager_class(model_cls) default_manager_fullname = helpers.get_class_fullname(default_manager_cls) try: @@ -767,7 +767,7 @@ def run(self) -> None: @cached_property def default_pk_instance(self) -> Instance: - default_pk_field = self.lookup_typeinfo(self.django_context.settings.DEFAULT_AUTO_FIELD) + default_pk_field = self.lookup_typeinfo(self.django_context.default_auto_field_path) if default_pk_field is None: raise helpers.IncompleteDefnException() return Instance( diff --git a/mypy_django_plugin/transformers/querysets.py b/mypy_django_plugin/transformers/querysets.py index 38711f0dc..70ee60108 100644 --- a/mypy_django_plugin/transformers/querysets.py +++ b/mypy_django_plugin/transformers/querysets.py @@ -794,7 +794,7 @@ def check_valid_prefetch_related_lookup( ) -> bool: """Check if a lookup string resolve to something that can be prefetched""" current_model_cls = django_model.cls - contenttypes_installed = django_context.apps_registry.is_installed("django.contrib.contenttypes") + contenttypes_installed = django_context.is_app_installed("django.contrib.contenttypes") for through_attr in lookup.split(LOOKUP_SEP): rel_obj_descriptor = getattr(current_model_cls, through_attr, None) if rel_obj_descriptor is None: @@ -973,15 +973,22 @@ def extract_prefetch_related_annotations(ctx: MethodContext, django_context: Dja def _try_get_field( - ctx: MethodContext, model_cls: type[Model], field_name: str, *, resolve_pk: bool = False + ctx: MethodContext, + django_context: DjangoContext, + model_cls: type[Model], + field_name: str, + *, + resolve_pk: bool = False, ) -> _AnyField | None: - opts = model_cls._meta - resolved_name = opts.pk.name if resolve_pk and field_name == "pk" else field_name - try: - return opts.get_field(resolved_name) - except FieldDoesNotExist as e: - ctx.api.fail(str(e), ctx.context) - return None + if resolve_pk and field_name == "pk": + pk = django_context.get_pk_field_or_none(model_cls) + resolved_name = pk.name if pk is not None else field_name + else: + resolved_name = field_name + field = django_context.get_field_on_model(model_cls, resolved_name) + if field is None: + ctx.api.fail(f"{model_cls.__name__} has no field named {resolved_name!r}", ctx.context) + return field def _check_field_concrete(ctx: MethodContext, field: _AnyField, field_name: str, method: str) -> bool: @@ -993,6 +1000,7 @@ def _check_field_concrete(ctx: MethodContext, field: _AnyField, field_name: str, def _check_field_not_pk( ctx: MethodContext, + django_context: DjangoContext, model_cls: type[Model], field: _AnyField, field_name: str, @@ -1000,10 +1008,9 @@ def _check_field_not_pk( *, attr_name: str | None = None, ) -> bool: - opts = model_cls._meta - all_pk_fields = set(getattr(opts, "pk_fields", [opts.pk])) - for parent in getattr(opts, "all_parents", opts.get_parent_list()): - all_pk_fields.update(getattr(parent._meta, "pk_fields", [parent._meta.pk])) + all_pk_fields: set[_AnyField] = set(django_context.get_pk_fields(model_cls)) + for parent in django_context.get_model_parents(model_cls): + all_pk_fields.update(django_context.get_pk_fields(parent)) if field in all_pk_fields: param_str = f' in "{attr_name}="' if attr_name else "" ctx.api.fail(f'"{method}()" does not support primary key fields{param_str}. Got "{field_name}"', ctx.context) @@ -1036,19 +1043,12 @@ def _extract_field_names_from_varargs(ctx: MethodContext) -> list[str]: ] -def _get_select_related_field_choices(model_cls: type[Model]) -> set[str]: +def _get_select_related_field_choices(django_context: DjangoContext, model_cls: type[Model]) -> set[str]: """ Get valid field choices for select_related lookups. Based on Django's SQLCompiler.get_related_selections._get_field_choices method. """ - opts = model_cls._meta - - # Direct relation fields (forward relations) - direct_choices = (f.name for f in opts.fields if f.is_relation) - - # Reverse relation fields (backward relations with unique=True) - reverse_choices = (f.field.related_query_name() for f in opts.related_objects if f.field.unique) - return {*direct_choices, *reverse_choices} + return set(django_context.iter_select_related_choices(model_cls)) def _validate_select_related_lookup( @@ -1068,7 +1068,7 @@ def _validate_select_related_lookup( lookup_parts = lookup.split(LOOKUP_SEP) observed_model = model_cls for i, part in enumerate(lookup_parts): - valid_choices = _get_select_related_field_choices(observed_model) + valid_choices = _get_select_related_field_choices(django_context, observed_model) if part not in valid_choices: ctx.api.fail( @@ -1106,12 +1106,18 @@ def validate_select_related(ctx: MethodContext, django_context: DjangoContext) - def _validate_bulk_update_field( - ctx: MethodContext, model_cls: type[Model], field_name: str, method: str, *, attr_name: str | None = None + ctx: MethodContext, + django_context: DjangoContext, + model_cls: type[Model], + field_name: str, + method: str, + *, + attr_name: str | None = None, ) -> bool: return ( - (field := _try_get_field(ctx, model_cls, field_name)) is not None + (field := _try_get_field(ctx, django_context, model_cls, field_name)) is not None and _check_field_concrete(ctx, field, field_name, method) - and _check_field_not_pk(ctx, model_cls, field, field_name, method, attr_name=attr_name) + and _check_field_not_pk(ctx, django_context, model_cls, field, field_name, method, attr_name=attr_name) ) @@ -1137,17 +1143,17 @@ def validate_bulk_update( return ctx.default_return_type for field_name in field_names: - _validate_bulk_update_field(ctx, django_model.cls, field_name, method) + _validate_bulk_update_field(ctx, django_context, django_model.cls, field_name, method) return ctx.default_return_type def _validate_bulk_create_unique_field( - ctx: MethodContext, model_cls: type[Model], field_name: str, method: str + ctx: MethodContext, django_context: DjangoContext, model_cls: type[Model], field_name: str, method: str ) -> bool: - return (field := _try_get_field(ctx, model_cls, field_name, resolve_pk=True)) is not None and _check_field_concrete( - ctx, field, field_name, method - ) + return ( + field := _try_get_field(ctx, django_context, model_cls, field_name, resolve_pk=True) + ) is not None and _check_field_concrete(ctx, field, field_name, method) def validate_bulk_create( @@ -1164,26 +1170,30 @@ def validate_bulk_create( update_field_names = _extract_field_names_from_collection(ctx, django_context, arg_index=4) if update_field_names is not None: for field_name in update_field_names: - _validate_bulk_update_field(ctx, django_model.cls, field_name, method, attr_name="update_fields") + _validate_bulk_update_field( + ctx, django_context, django_model.cls, field_name, method, attr_name="update_fields" + ) unique_field_names = _extract_field_names_from_collection(ctx, django_context, arg_index=5) if unique_field_names is not None: for field_name in unique_field_names: - _validate_bulk_create_unique_field(ctx, django_model.cls, field_name, method) + _validate_bulk_create_unique_field(ctx, django_context, django_model.cls, field_name, method) return ctx.default_return_type -def _validate_order_by_lookup(ctx: MethodContext, model_cls: type[Model], parts: list[str]) -> None: +def _validate_order_by_lookup( + ctx: MethodContext, django_context: DjangoContext, model_cls: type[Model], parts: list[str] +) -> None: if len(parts) == 1 and parts[0] == "?": return # Abstract models don't have a pk field, skip validation - if model_cls._meta.abstract: + if django_context.is_model_abstract(model_cls): return try: - _, final_field, _, remainder = Query(model_cls).names_to_path(parts, model_cls._meta) + final_field, remainder = django_context.resolve_names_to_path(model_cls, parts) except FieldError as exc: ctx.api.fail(exc.args[0], ctx.context) return @@ -1213,7 +1223,7 @@ def validate_order_by(ctx: MethodContext, django_context: DjangoContext) -> Mypy if selected_fields is not None and parts[0] in selected_fields: # Skip validation for fields selected via values()/values_list() continue - _validate_order_by_lookup(ctx, django_model.cls, parts) + _validate_order_by_lookup(ctx, django_context, django_model.cls, parts) return ctx.default_return_type diff --git a/mypy_django_plugin/transformers/settings.py b/mypy_django_plugin/transformers/settings.py index ff944c021..6fc391239 100644 --- a/mypy_django_plugin/transformers/settings.py +++ b/mypy_django_plugin/transformers/settings.py @@ -42,7 +42,7 @@ def get_type_of_settings_attribute( # If it does, we just return `Any`, not to raise any false-positives. # But, we cannot reconstruct the exact runtime type. # See https://github.com/typeddjango/django-stubs/pull/1163 - if not plugin_config.strict_settings and hasattr(django_context.settings, setting_name): + if not plugin_config.strict_settings and django_context.has_setting(setting_name): return AnyType(TypeOfAny.implementation_artifact) ctx.api.fail(f"'Settings' object has no attribute {setting_name!r}", ctx.context)