Skip to content

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

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

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

github-actions Bot commented Nov 1, 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]

This branch has not been deployed

No deployments
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

3 participants