From 659a800b2a35418cc8c1b8451921f2ce436dca9e Mon Sep 17 00:00:00 2001 From: dhitalkamal <158161693+dhitalkamal@users.noreply.github.com> Date: Sun, 1 Nov 2026 09:42:29 +0545 Subject: [PATCH] allow django models to declare their own id primary key pyrefly gives every Django model without its own primary key a made-up `id: int`, including AbstractBaseUser and PermissionsMixin. A user model that declares `id = UUIDField(primary_key=True)` was flagged as an inconsistent override of that made-up field. Django only adds the automatic `id` to the final model when no primary key is declared, so a child's own `id` primary key replaces it rather than overriding it. The override check now skips a parent's `id` when it was made up by pyrefly and the child's `id` is its primary key. An `id` written out in a parent is still checked. Fixes #4596 --- pyrefly/lib/alt/class/class_field.rs | 16 +++++++ .../lib/test/django/auto_generated_fields.rs | 42 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/pyrefly/lib/alt/class/class_field.rs b/pyrefly/lib/alt/class/class_field.rs index 73288304bf..fbc6ed00c3 100644 --- a/pyrefly/lib/alt/class/class_field.rs +++ b/pyrefly/lib/alt/class/class_field.rs @@ -4333,6 +4333,22 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { else { continue; }; + // Django gives a model without its own primary key a synthesized `id: int`, + // including abstract bases like `AbstractBaseUser` (stubs omit `Meta.abstract`, + // so we can't tell them apart). Django only adds that field to the concrete model + // when no pk is declared, so a child declaring `id` as its primary key replaces it + // rather than overriding it. An explicitly declared parent `id` is still checked. + if field_name.as_str() == "id" + && metadata + .django_model_metadata() + .and_then(|dm| dm.custom_primary_key_field.as_ref()) + == Some(field_name) + && self + .get_non_synthesized_class_member(parent_cls, field_name) + .is_none() + { + continue; + } // `__call__` is checked against a Protocol parent only, unless the user // opts in with `@override`. Implementing a callable interface is // ubiquitous - argparse actions, auth handlers, metaclasses - and diff --git a/pyrefly/lib/test/django/auto_generated_fields.rs b/pyrefly/lib/test/django/auto_generated_fields.rs index 749ef9d1a6..a3c9889eb5 100644 --- a/pyrefly/lib/test/django/auto_generated_fields.rs +++ b/pyrefly/lib/test/django/auto_generated_fields.rs @@ -165,3 +165,45 @@ assert_type(obj.id, str) assert_type(obj.pk, str) "#, ); + +// Abstract bases without their own pk get a synthesized `id: int`. A child that +// declares its own `id` primary key should not be flagged as overriding it. +// https://github.com/meta/pyrefly/issues/4596 +django_testcase!( + test_custom_id_pk_overrides_synthesized_id, + r#" +from typing import assert_type +import uuid +from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin +from django.db import models + +class User(AbstractBaseUser, PermissionsMixin): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + +class Base(models.Model): + class Meta: + abstract = True + +class Child(Base): + id = models.CharField(max_length=36, primary_key=True) + +assert_type(User().id, uuid.UUID) +assert_type(User().pk, uuid.UUID) +assert_type(Child().id, str) +assert_type(Child().pk, str) +"#, +); + +// An `id` that is not the primary key is still checked against the synthesized one. +django_testcase!( + test_non_pk_id_still_checked_against_synthesized_id, + r#" +from django.db import models + +class Base(models.Model): + pass + +class Child(Base): + id = models.CharField(max_length=36) # E: `Child.id` overrides parent class `Base` +"#, +);