Skip to content

fix Doesn't allow list["A|B"] #3193#3195

Open
asukaminato0721 wants to merge 3 commits intofacebook:mainfrom
asukaminato0721:3193
Open

fix Doesn't allow list["A|B"] #3193#3195
asukaminato0721 wants to merge 3 commits intofacebook:mainfrom
asukaminato0721:3193

Conversation

@asukaminato0721
Copy link
Copy Markdown
Contributor

Summary

Fixes #3193

binding type-argument subscripts as type expressions even in value context, so list["Tomato | Cucumber"]([t]) no longer errors and forward-ref strings get parsed.

Test Plan

add test

@meta-cla meta-cla Bot added the cla signed label Apr 21, 2026
@asukaminato0721 asukaminato0721 marked this pull request as ready for review April 21, 2026 14:03
Copilot AI review requested due to automatic review settings April 21, 2026 14:03
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes pyrefly’s handling of generic-alias subscripts used in value context (e.g. list["A | B"]([x])) so that string type arguments are treated as forward references and parsed/bound as type expressions, matching other type checkers and resolving #3193.

Changes:

  • Adds a binder special-case to treat certain Expr::Subscript nodes as containing type expressions even in value context.
  • Adds a regression test covering list["Tomato | Cucumber"]([t]) with from __future__ import annotations.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
pyrefly/lib/binding/expr.rs Ensures type-argument slices of certain special subscripts are bound as types in value context (enabling forward-ref parsing).
pyrefly/lib/test/typeform.rs Adds regression test for forward-ref string type arguments in generic-alias value-context usage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pyrefly/lib/binding/expr.rs
Comment thread pyrefly/lib/binding/expr.rs Outdated
@github-actions github-actions Bot added size/s and removed size/s labels Apr 21, 2026
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions github-actions Bot added size/m and removed size/s labels Apr 21, 2026
@github-actions
Copy link
Copy Markdown

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

tornado (https://github.com/tornadoweb/tornado)
- ERROR tornado/routing.py:296:9-15: Expected a type form, got instance of `Literal['Rule']` [not-a-type]
- ERROR tornado/routing.py:298:26-35: Expected a type form, got instance of `Literal['Matcher']` [not-a-type]
- ERROR tornado/routing.py:299:26-35: Expected a type form, got instance of `Literal['Matcher']` [not-a-type]
- ERROR tornado/routing.py:300:26-35: Expected a type form, got instance of `Literal['Matcher']` [not-a-type]
- ERROR tornado/routing.py:349:55-64: Argument `dict[str, Any] | str | Unknown` is not assignable to parameter `target_kwargs` with type `dict[str, Any] | None` in function `Rule.__init__` [bad-argument-type]
+ ERROR tornado/routing.py:349:55-64: Argument `dict[str, Any] | str | Any` is not assignable to parameter `target_kwargs` with type `dict[str, Any] | None` in function `Rule.__init__` [bad-argument-type]
- ERROR tornado/routing.py:349:55-64: Argument `dict[str, Any] | str | Unknown` is not assignable to parameter `name` with type `str | None` in function `Rule.__init__` [bad-argument-type]
+ ERROR tornado/routing.py:349:55-64: Argument `dict[str, Any] | str | Any` is not assignable to parameter `name` with type `str | None` in function `Rule.__init__` [bad-argument-type]
- ERROR tornado/routing.py:351:33-38: Argument `dict[str, Any] | str | Unknown` is not assignable to parameter `matcher` with type `Matcher` in function `Rule.__init__` [bad-argument-type]
+ ERROR tornado/routing.py:351:33-38: Argument `Matcher | dict[str, Any] | str | Any` is not assignable to parameter `matcher` with type `Matcher` in function `Rule.__init__` [bad-argument-type]
- ERROR tornado/routing.py:351:33-38: Argument `dict[str, Any] | str | Unknown` is not assignable to parameter `target_kwargs` with type `dict[str, Any] | None` in function `Rule.__init__` [bad-argument-type]
+ ERROR tornado/routing.py:351:33-38: Argument `Matcher | dict[str, Any] | str | Any` is not assignable to parameter `target_kwargs` with type `dict[str, Any] | None` in function `Rule.__init__` [bad-argument-type]
- ERROR tornado/routing.py:351:33-38: Argument `dict[str, Any] | str | Unknown` is not assignable to parameter `name` with type `str | None` in function `Rule.__init__` [bad-argument-type]
+ ERROR tornado/routing.py:351:33-38: Argument `Matcher | dict[str, Any] | str | Any` is not assignable to parameter `name` with type `str | None` in function `Rule.__init__` [bad-argument-type]
- ERROR tornado/routing.py:353:49-53: Argument `Rule | tuple[str | Unknown, Any] | tuple[str | Unknown, Any, dict[str, Any]] | tuple[str | Unknown, Any, dict[str, Any], str] | Unknown` is not assignable to parameter `rule` with type `Rule` in function `RuleRouter.process_rule` [bad-argument-type]

pyjwt (https://github.com/jpadilla/pyjwt)
- ERROR jwt/algorithms.py:405:28-41: `RSAPrivateKey` may be uninitialized [unbound-name]
- ERROR jwt/algorithms.py:405:43-55: `RSAPublicKey` may be uninitialized [unbound-name]
- ERROR jwt/algorithms.py:593:28-51: `EllipticCurvePrivateKey` may be uninitialized [unbound-name]
- ERROR jwt/algorithms.py:593:53-75: `EllipticCurvePublicKey` may be uninitialized [unbound-name]
- ERROR jwt/algorithms.py:837:21-38: `Ed25519PrivateKey` may be uninitialized [unbound-name]
- ERROR jwt/algorithms.py:838:21-37: `Ed25519PublicKey` may be uninitialized [unbound-name]
- ERROR jwt/algorithms.py:839:21-36: `Ed448PrivateKey` may be uninitialized [unbound-name]
- ERROR jwt/algorithms.py:840:21-35: `Ed448PublicKey` may be uninitialized [unbound-name]

altair (https://github.com/vega/altair)
- ERROR altair/datasets/_cache.py:153:61-71: Expected a type form, got instance of `Literal['_Dataset']` [not-a-type]
- ERROR altair/datasets/_cache.py:153:73-83: Expected a type form, got instance of `Literal['Metadata']` [not-a-type]
- ERROR altair/datasets/_cache.py:225:62-72: Expected a type form, got instance of `Literal['_Dataset']` [not-a-type]
- ERROR altair/datasets/_cache.py:225:74-85: Expected a type form, got instance of `Literal['_FlSchema']` [not-a-type]

discord.py (https://github.com/Rapptz/discord.py)
- ERROR discord/app_commands/translator.py:119:61-85: Expected a type form, got instance of `Literal['Command[Any, ..., Any]']` [not-a-type]
- ERROR discord/app_commands/translator.py:119:87-100: Expected a type form, got instance of `Literal['ContextMenu']` [not-a-type]

django-test-migrations (https://github.com/wemake-services/django-test-migrations)
- ERROR django_test_migrations/db/backends/registry.py:16:10-37: Expected a type form, got instance of `Literal['BaseDatabaseConfiguration']` [not-a-type]

prefect (https://github.com/PrefectHQ/prefect)
- ERROR src/prefect/deployments/schedules.py:14:53-69: Expected a type form, got instance of `Literal['SCHEDULE_TYPES']` [not-a-type]
- ERROR src/prefect/server/database/orm_models.py:1510:38-54: Expected a type form, got instance of `Literal['sa.Column[Any]']` [not-a-type]

@github-actions
Copy link
Copy Markdown

Primer Diff Classification

✅ 6 improvement(s) | 6 project(s) total | +2, -24 errors

6 improvement(s) across tornado, pyjwt, altair, discord.py, django-test-migrations, prefect.

Project Verdict Changes Error Kinds Root Cause
tornado ✅ Improvement +2, -7 Removed not-a-type false positives on forward references as_special_export()
pyjwt ✅ Improvement -8 unbound-name pyrefly/lib/binding/expr.rs
altair ✅ Improvement -4 not-a-type pyrefly/lib/binding/expr.rs
discord.py ✅ Improvement -2 not-a-type pyrefly/lib/binding/expr.rs
django-test-migrations ✅ Improvement -1 not-a-type ensure_type_impl()
prefect ✅ Improvement -2 not-a-type ensure_type_impl()
Detailed analysis

✅ Improvement (6)

tornado (+2, -7)

Removed not-a-type false positives on forward references: The 4 not-a-type errors on "Rule" and "Matcher" in the _RuleList type alias were false positives. These are valid forward references inside Union[...] and tuple[...] subscripts. The PR fix correctly resolves them as type expressions.
Removed cascade bad-argument-type errors with Unknown: The 3 removed bad-argument-type errors containing Unknown were downstream of the unresolved forward references. Now that types are properly resolved, these cascade errors disappear.
New bad-argument-type errors with improved type info: The 2 new errors show dict[str, Any] | str | Any instead of Unknown. These are co-reported by pyright (2/2) and represent genuine type-checking concerns about splatting heterogeneous union tuples. The type information is now more precise, and the errors are arguably correct given the complex unpacking pattern.

Overall: Net change: 7 errors removed (all false positives or their cascades), 2 errors added (co-reported by pyright, representing genuine type-checking concerns about splatting heterogeneous union tuples into Rule.__init__). The removed not-a-type errors on forward references like "Rule" and "Matcher" were clearly wrong — these are standard forward references per the typing spec. The new errors are more precise versions of previously-existing errors (replacing Unknown with Any). Overall this is an improvement: pyrefly now correctly handles forward references in type subscripts in value context.

Per-category reasoning:

  • Removed not-a-type false positives on forward references: The 4 not-a-type errors on "Rule" and "Matcher" in the _RuleList type alias were false positives. These are valid forward references inside Union[...] and tuple[...] subscripts. The PR fix correctly resolves them as type expressions.
  • Removed cascade bad-argument-type errors with Unknown: The 3 removed bad-argument-type errors containing Unknown were downstream of the unresolved forward references. Now that types are properly resolved, these cascade errors disappear.
  • New bad-argument-type errors with improved type info: The 2 new errors show dict[str, Any] | str | Any instead of Unknown. These are co-reported by pyright (2/2) and represent genuine type-checking concerns about splatting heterogeneous union tuples. The type information is now more precise, and the errors are arguably correct given the complex unpacking pattern.

Attribution: The change in pyrefly/lib/binding/expr.rs adds a new Expr::Subscript arm that binds type-argument slices as type expressions when the subscripted value is a known generic type (via as_special_export()). This allows forward-reference strings like "Rule" and "Matcher" inside Union[...] and tuple[...] to be properly resolved, eliminating the 4 not-a-type false positives and their 3 downstream bad-argument-type cascade errors.

pyjwt (-8)

The 8 removed unbound-name errors were all false positives. The names are imported at the top of a try block (lines 36-78), and the code referencing them (lines 403-406, 591-594, 833-843) is inside if has_crypto: which is only True when the try block succeeded. Pyrefly was incorrectly failing to resolve these names when they appeared as type arguments inside Union[...] subscripts used in value context (e.g., get_args(Union[RSAPrivateKey, RSAPublicKey])). The PR fix in pyrefly/lib/binding/expr.rs now correctly binds type-argument subscripts as type expressions even in value context, so the names are properly resolved. Removing these false positives is an improvement.
Attribution: The change in pyrefly/lib/binding/expr.rs in the Expr::Subscript arm adds logic to detect when a subscript's value is a known type-like export (e.g., Union, list, set, etc.) and bind the slice as a type expression rather than a plain value expression. This means Union[RSAPrivateKey, RSAPublicKey] in value context now correctly resolves the forward references / names inside the Union subscript, eliminating the false unbound-name errors.

altair (-4)

The PR fixes a false positive where pyrefly treated string type arguments inside generic subscripts (e.g., dict["_Dataset", "Metadata"]) as literal string values instead of forward-reference type expressions. The file uses from __future__ import annotations, and defines _Dataset and _FlSchema as TypeAlias under TYPE_CHECKING. The expressions dict["_Dataset", "Metadata"] and dict["_Dataset", "_FlSchema"] appear as default parameter values on lines 153 and 225 for type[MutableMapping[...]] parameters. Importantly, from __future__ import annotations only affects annotations (making them lazily evaluated strings) — it does NOT affect runtime expressions like default values. So dict["_Dataset", "Metadata"] as a default value is technically a runtime expression. However, when strings appear as subscript arguments to dict (or other generic types) in a position where type arguments are expected, a type checker should interpret them as forward references to types, not as Literal['_Dataset'] etc. The type checker should recognize that dict["_Dataset", "Metadata"] parameterizes dict with the type aliases _Dataset and Metadata (defined under TYPE_CHECKING), producing dict[_Dataset, Metadata]. Flagging these as Literal['_Dataset'] etc. being non-type-forms is a false positive. Removing all 4 errors is correct.
Attribution: The change in pyrefly/lib/binding/expr.rs in the Expr::Subscript arm adds logic to detect when a subscript's base value is a known generic type (like BuiltinsDict, BuiltinsList, etc.) and bind the slice as a type expression rather than a value expression. This means dict["_Dataset", "Metadata"] now correctly parses the string arguments as forward-reference type annotations instead of treating them as Literal string instances. The SpecialExport::BuiltinsDict match specifically handles the dict[...] case that was producing these false positives.

discord.py (-2)

The removed errors were false positives. Line 119 uses Union['Command[Any, ..., Any]', 'ContextMenu'] as a type argument to TranslationContext[...]. The strings are forward references (the file uses from __future__ import annotations and the types are imported under TYPE_CHECKING). Pyrefly was previously failing to recognize these strings as type forms in this context, incorrectly treating them as Literal string instances. The PR fix in pyrefly/lib/binding/expr.rs correctly binds type arguments of known generic types (including Union) as type expressions, allowing forward-reference strings to be properly parsed. This is a clear improvement — pyrefly now correctly handles a standard Python typing pattern.
Attribution: The change in pyrefly/lib/binding/expr.rs in the Expr::Subscript arm adds logic to detect when a subscript's base is a known special export (like Union, list, dict, etc.) and binds the slice as a type expression (ensure_type_impl) instead of a value expression (ensure_expr). This means Union['Command[Any, ..., Any]', 'ContextMenu'] now has its string arguments parsed as forward references rather than literal string values, eliminating the false not-a-type errors.

django-test-migrations (-1)

This is a clear improvement. The removed error was a false positive — pyrefly was incorrectly reporting type['BaseDatabaseConfiguration'] as not-a-type because it treated the forward reference string as a Literal value rather than parsing it as a type form. The code follows a standard Python typing pattern: importing BaseDatabaseConfiguration under TYPE_CHECKING and using a string forward reference in the type annotation. The PR fix in pyrefly/lib/binding/expr.rs correctly ensures forward-reference strings inside generic type subscripts are parsed as type expressions.
Attribution: The change in pyrefly/lib/binding/expr.rs that adds handling for Expr::Subscript in value contexts is directly responsible. Specifically, the new code checks if the subscript's value is a special export (including SpecialExport::BuiltinsType and SpecialExport::TypingType) and, if so, binds the slice as a type expression via ensure_type_impl() rather than as a plain value expression. This means type['BaseDatabaseConfiguration'] now correctly parses 'BaseDatabaseConfiguration' as a forward reference type form instead of treating it as a string literal value.

prefect (-2)

Both errors were false positives where pyrefly failed to recognize forward-reference strings inside Union subscripts as type expressions. The PR correctly fixes this by binding type argument slices of known generic types as type expressions even in value context, per https://typing.readthedocs.io/en/latest/spec/annotations.html#string-annotations.
Attribution: The change in pyrefly/lib/binding/expr.rs adds a new Expr::Subscript arm that identifies known generic types (including SpecialExport::Union) and binds their slice arguments as type expressions via ensure_type_impl(). This causes forward-reference strings like "SCHEDULE_TYPES" and "sa.Column[Any]" inside Union[...] to be properly parsed as type forms rather than treated as literal string values.


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (6 LLM)

@kinto0
Copy link
Copy Markdown
Contributor

kinto0 commented Apr 24, 2026

thanks for the fix! seems targeted enough and good at first glance. assigning to @stroxler but feel free to reassign if you have too much on your plate

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.

Doesn't allow list["A|B"]

4 participants