Skip to content

fix Django: assigning a query expression (F(), Value()) to a model field is rejected #5322 - #5335

Closed
asukaminato0721 wants to merge 2 commits into
meta:mainfrom
asukaminato0721:5322
Closed

asukaminato0721 wants to merge 2 commits into
meta:mainfrom
asukaminato0721:5322

Conversation

@asukaminato0721

Copy link
Copy Markdown
Collaborator

Summary

Fixes #5322

Django fields now accept F(), Value(), and combined expressions while preserving their read types.

  • I am a human, and I fully understand the code I am submitting.

Test Plan

add test

@meta-codesync

meta-codesync Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

This pull request has been imported. If you are a Meta employee, you can view this in D127050295. (Because this pull request was imported automatically, there will not be any future comments.)

@github-actions

This comment has been minimized.

@stroxler stroxler left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I think this causes a couple of behavior regressions based on a round of review:

Ordinary assignments lose nullable-field narrowing

[class_field.rs:5550–5563](https://github.com/meta/pyrefly/blob/d58a18a517f7168cbb0b551c48ba837b9c7735ed/pyrefly/lib/alt/class/class_field.rs#L5550-L5563)

The new setter returns narrow: false for every Django-field assignment—not just expressions. For an IntegerField(null=True):

book.pages = 1
return book.pages + 1

This now reports an unsupported operation involving None. Nullable strings and foreign keys regress similarly. It also breaks the common “if missing, assign a default, then use it” pattern, consistent with the added nullable-access errors in the primer report.

Fix: preserve assignment narrowing when the RHS fits the read type; suppress it for expressions or coercible values outside that type.

Choice-restricted fields accept values contradicting their read type

[django.rs:325–332](https://github.com/meta/pyrefly/blob/d58a18a517f7168cbb0b551c48ba837b9c7735ed/pyrefly/lib/alt/class/django.rs#L325-L332)

Only the getter is narrowed by narrow_charfield_choices. The setter remains str | int | Combinable, so this becomes accepted:

# suit has choices "A" and "B"
card.suit = "INVALID"
assert_type(card.suit, Literal["A", "B"])

Previously the assignment was rejected. This makes Pyrefly’s existing finite-choice guarantee inconsistent; it is not a claim that Django validates choices during assignment.

Fix: retain choice restrictions for ordinary writes while permitting expressions, or deliberately revise the finite-choice read guarantee.

Expression assignments still fail inside model methods

[class_field.rs:5550–5558](https://github.com/meta/pyrefly/blob/d58a18a517f7168cbb0b551c48ba837b9c7735ed/pyrefly/lib/alt/class/class_field.rs#L5550-L5558)

The new branch requires instance_class, which attribute resolution does not supply for implicit self or bounded type variables:

class Counter(models.Model):
    count = models.IntegerField()

    def bump(self) -> None:
        self.count = F("count") + 1  # Still rejected

An explicitly annotated self: Counter works; def update[T: Counter](counter: T) still fails. This is an incomplete fix rather than a new regression.

Fix: carry the write type through attribute lookup instead of recovering it through a second Django-specific member lookup. That also avoids duplicate lookup and instantiation.

@github-actions

This comment has been minimized.

@stroxler stroxler left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review automatically exported from Phabricator review in Meta.

@stroxler stroxler left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's looking pretty good! I was going to land with fix-forwards, but I think it already needs a rebase; the codebase is churning pretty fast now as we try to clear the backlog.

Could you rebase, and also address these issues that I was handling in my fix-forward:

  • Respect explicit annotations. An inherited Django field’s permissive setter must not make self.count: int = "123" valid. Annotated assignments should retain normal type and qualifier checks.
  • Preserve override compatibility. Django coercion types shouldn’t create new override errors for EmailField/TextField replacing an abstract CharField, or for overriding annotations/properties. Keep read-type invariance and explicit parent-property setter requirements.
  • Handle intersection receivers. Fold distinct write types instead of falling back to one receiver member. Writes must satisfy every member, and read-only/Final restrictions must survive.
  • Keep ordinary attributes cheap. Don’t allocate separate Django read/write collections for unions where nothing transforms.
  • Improve diagnostics. When checking a field against a property setter, report the effective write type, not the read type.

@github-actions

This comment has been minimized.

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown

Diff from mypy_primer, showing the effect of this PR on open source code:

============================================================
SUMMARY
============================================================
Total: +0 new errors, -1 fixed errors
By preset: +0/-1 (default), +0/-1 (strict)

Projects with changes (1):
  zulip: +0 -1
============================================================

FULL DIFF DETAILS
------------------------------------------------------------

zulip (https://github.com/zulip/zulip)
- ERROR zerver/lib/test_classes.py:2859:28-44: `Literal['6cde5f7a-1f7e-4978-9716-49f69ebfc9fe']` is not assignable to attribute `uuid` with type `UUID` [bad-assignment]

@meta-codesync

meta-codesync Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

This pull request has been merged in 9b870b9.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Django: assigning a query expression (F(), Value()) to a model field is rejected

4 participants