Skip to content

fix Preliminary support for class decorators #2752#2773

Open
asukaminato0721 wants to merge 5 commits into
facebook:mainfrom
asukaminato0721:2752
Open

fix Preliminary support for class decorators #2752#2773
asukaminato0721 wants to merge 5 commits into
facebook:mainfrom
asukaminato0721:2752

Conversation

@asukaminato0721

Copy link
Copy Markdown
Collaborator

Summary

Fixes #2752

applying class decorators to the runtime class-value bindingm while preserving the original class object when a decorator still returns a usable type form.

Test Plan

add test

@meta-cla meta-cla Bot added the cla signed label Mar 11, 2026
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@asukaminato0721
asukaminato0721 marked this pull request as ready for review March 11, 2026 01:45
Copilot AI review requested due to automatic review settings March 11, 2026 01:45
@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR enhances Pyrefly’s solver to type-check class decorator application so the decorated class name can be rebound to the decorator’s return type (e.g., @remote turning a class name into an ActorHandle[...]), and adds a regression test covering this behavior.

Changes:

  • Apply non-dataclass-transform class decorators during Binding::ClassDef type solving to infer the post-decoration value type.
  • Extend untype_opt/expr_untype handling for Type::KwCall and name lookups to better support the new decorator behavior.
  • Add a new decorator test case ensuring a class decorator can rebind the class value.

Reviewed changes

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

File Description
pyrefly/lib/test/decorators.rs Adds a regression testcase for a class decorator that rebinds the class symbol to an ActorHandle[T].
pyrefly/lib/alt/solve.rs Implements class-decorator application during solving and adjusts untyping/name handling to support the new behavior.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment thread pyrefly/lib/alt/solve.rs Outdated
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@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/m labels Mar 28, 2026
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@yangdanny97

Copy link
Copy Markdown
Contributor

Overall: This is a clear regression. The PR's class decorator support mishandles decorators whose return types are Unknown/unresolvable, causing 179 false positive errors across the spack project. The decorator @lang.lazy_lexicographic_ordering returns the class itself at runtime, but pyrefly now types the class as ((realf: Unknown) -> Unknown) | Unknown, cascading into missing-attribute, not-a-type, invalid-inheritance, unexpected-keyword, and other errors. None of these are flagged by mypy or pyright.

@migeed-z why does this get classified as "improvement" lol

@github-actions

This comment has been minimized.

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

Primer Diff Classification

❌ 2 regression(s) | ✅ 2 improvement(s) | 4 project(s) total | +7 errors

2 regression(s) across jax, django-modern-rest. error kinds: register_dataclass decorator bad-specialization, register_static decorator bad-specialization, implicit-any-type-argument. 2 improvement(s) across spark, django-stubs.

Project Verdict Changes Error Kinds Root Cause
jax ❌ Regression +4 register_dataclass decorator bad-specialization pyrefly/lib/alt/solve.rs
spark ✅ Improvement +1 bad-argument-type pyrefly/lib/alt/solve.rs
django-stubs ✅ Improvement +1 bad-specialization pyrefly/lib/alt/solve.rs
django-modern-rest ❌ Regression +1 implicit-any-type-argument The PR changes how class decorators are processed in `sol...
Detailed analysis

❌ Regression (2)

jax (+4)

register_dataclass decorator bad-specialization: 3 errors (lines 820, 950, 968) where @partial(jax.tree_util.register_dataclass, ...) is applied to a dataclass. The PR's new decorator handling infers the return type as GenericResidual@Typ instead of preserving the original class type, causing a bad-specialization error claiming it doesn't satisfy type[Any] bound. These decorators return the class unchanged; mypy/pyright don't flag this.
register_static decorator bad-specialization: 1 error (line 963) where @jax.tree_util.register_static is applied to a local dataclass P. The PR's new decorator handling produces a type that pyrefly claims doesn't satisfy the Hashable bound of type variable H. Note that @dataclass with default arguments (eq=True, frozen=False) actually sets __hash__ = None, making instances unhashable. However, jax.tree_util.register_static adds __hash__ and __eq__ methods to the class at runtime, making it hashable. Regardless, this is a pyrefly-only error caused by incorrect decorator return type inference, not flagged by mypy/pyright.

Overall: These are false positives introduced by the PR's new class decorator handling. The JAX decorators jax.tree_util.register_dataclass, jax.tree_util.register_static, and register_pytree_node_serialization all return the class unchanged at runtime. The PR's new logic in solve.rs attempts to apply class decorators and check their return types, but it's incorrectly inferring the return types of these JAX utility decorators (as GenericResidual@Typ instead of the original class type). This causes downstream bad-specialization errors when the decorated classes are used in contexts requiring type[Any] or Hashable bounds. Neither mypy nor pyright flags any of these — they are all pyrefly-only errors on valid, well-tested JAX code.

Attribution: The PR changes class decorator handling in pyrefly/lib/alt/solve.rs in the Binding::ClassDef match arm. Previously, decorators on classes were ignored (TODO: analyze the class decorators). Now, the PR applies decorators to the class type and checks whether the result is callable or a usable type form. The logic at lines 5270-5305 calls the decorator with the class as argument and checks the return type. For decorators like jax.tree_util.register_dataclass (which is partial(register_dataclass, data_fields=..., meta_fields=...)), the return type is likely being inferred as something like GenericResidual@Typ rather than the original class type. The PR's heuristic to preserve the class type when the decorator returns a callable or type form is not correctly handling these JAX decorators, causing the decorated class to be treated as a non-class type that then fails TypeVar bound checks when passed to other generic functions. Similarly, register_static returns something pyrefly interprets as not matching Hashable bounds. The root cause is the new decorator application logic producing incorrect intermediate types that then fail downstream specialization checks.

django-modern-rest (+1)

This is a false positive introduced by the PR's new class decorator handling. The @final decorator on a generic class definition is perfectly valid — @final simply marks the class as not subclassable and returns the class unchanged. The type parameter _DataT_co is a covariant TypeVar that will be bound when the class is instantiated, not at the definition site. Neither mypy nor pyright flag this. The error appears because the new decorator processing code in solve.rs calls call_infer on the @final decorator with the class as an argument, which inadvertently triggers type parameter resolution for the generic class at the definition site, producing a spurious implicit-any-type-argument error.
Attribution: The PR changes how class decorators are processed in solve.rs under Binding::ClassDef. Previously, decorators were ignored (TODO: analyze the class decorators). Now, pyrefly processes each decorator by calling call_infer on it. For @final, the decorator returns the class itself (it's typed as def final(f: _F) -> _F). The new logic checks if the decorator result is callable or can be untyped — if so, it preserves the original class def. However, the issue is that when pyrefly processes @final on SSEvent(Generic[_DataT_co]), the call_infer step apparently triggers type parameter resolution for the generic class, and since @final doesn't explicitly provide the type argument _DataT_co, pyrefly emits implicit-any-type-argument. This is a side effect of the new decorator processing — the decorator call creates a context where the generic class's type parameter needs to be resolved, but there's no concrete type to bind it to at the definition site. This is incorrect because @final doesn't change the class's type and shouldn't trigger type argument resolution.

✅ Improvement (2)

spark (+1)

This is a real type error. unittest.skip is typed in typeshed as def skip(reason: str) -> ..., so passing a class directly as @unittest.skip (without parentheses) is a type error per the stubs. The correct usage would be @unittest.skip('reason') or @unittest.skip(). The fact that it works at runtime is due to an undocumented CPython implementation detail. Pyright also flags this. Pyrefly is now correctly analyzing class decorator applications where it previously skipped them entirely.
Attribution: The change in pyrefly/lib/alt/solve.rs in the Binding::ClassDef match arm now applies class decorators by calling them with the class type as an argument. Previously, decorators were ignored (TODO: analyze the class decorators). Now pyrefly actually type-checks the decorator application, which means @unittest.skip is checked as unittest.skip(SkipClassTests), revealing the type mismatch between type[SkipClassTests] and str.

django-stubs (+1)

This is a real type error that all major type checkers agree on — mypy flags it as type-var, pyright flags it as reportArgumentType, and ty flags it as invalid-argument-type. The existing inline suppression comments on lines 154-155 confirm this. UnrelatedClass does not extend ModelAdmin, so passing it to @admin.register(ActionModel) violates the type variable's upper bound. Pyrefly is now correctly catching this error thanks to the new class decorator analysis. The only issue is that the django-stubs test file already has suppression comments for other checkers but not for pyrefly's error kind (bad-specialization). This is a correct error being newly detected — the stubs project expects this error (evidenced by the # type: ignore[type-var] comment). This is an improvement in pyrefly's analysis capability. Note: this is a stubs project, but the error is on a TEST file that intentionally tests invalid usage (the comment on line 147-148 explicitly says 'This is actually wrong'). The test is designed to verify that type checkers catch this error.
Attribution: The PR changes Binding::ClassDef handling in pyrefly/lib/alt/solve.rs to actually process class decorators. Previously, class decorators were ignored (TODO: analyze the class decorators). Now, the code calls the decorator as a function with the class as an argument. For @admin.register(ActionModel), this means pyrefly now actually checks whether UnrelatedClass satisfies the type parameter constraint of admin.register. The admin.register decorator's return type involves a type variable _ModelAdmin bounded by ModelAdmin[Any], and UnrelatedClass doesn't satisfy this bound. Previously this check was skipped entirely.

Suggested fixes

Summary: The new class decorator handling in solve.rs produces false positive errors when decorators return the class unchanged but pyrefly's heuristic fails to detect this, and when call_infer triggers spurious type argument resolution for generic classes.

1. In the Binding::ClassDef match arm in solve.rs (around line 5280-5305), the heuristic for preserving the original class type after decorator application is too narrow. When decorated_ty is a Type::ClassDef (i.e., the decorator returned a class type), the code should also preserve ty = self.heap.mk_class_def(cls.dupe()). Additionally, when the decorated type is not recognized as callable and untype_opt returns None, but the decorator's declared return type involves a TypeVar bound to the input (like _F -> _F in @final), the class should be preserved. The fix: after computing decorated_ty, add an additional condition to the preservation check: if decorated_ty is a Type::ClassDef or if the decorated_ty matches the input class type (same class), preserve the original class def. As a broader safety net, when decorated_ty is not callable and untype_opt returns None, check if decorated_ty is still a class-like type (ClassDef, ClassType) before replacing ty. If it's an unrecognized/residual type, fall back to preserving the original class.

Files: pyrefly/lib/alt/solve.rs
Confidence: high
Affected projects: jax, django-modern-rest
Fixes: bad-specialization
The current heuristic only preserves the class when the decorator result is callable or can be untyped. For JAX decorators like register_dataclass (a functools.partial object) and register_static, the return type is inferred as a residual/unknown type that fails both checks, causing the class to be replaced with a bad type. Adding a check for ClassDef/ClassType in the decorated result, or falling back to the original class when the result is an unrecognized type, would fix all 4 JAX errors and the 1 django-modern-rest error. Expected outcome: eliminates 5 pyrefly-only errors across 2 projects.

2. In the Binding::ClassDef decorator loop in solve.rs (around line 5290), the call to call_infer can emit errors (like bad-specialization or implicit-any-type-argument) as side effects even when the decorator result will ultimately be discarded in favor of preserving the original class. Consider collecting errors into a temporary/deferred error list during decorator processing, and only committing those errors if the decorated type is actually used (i.e., ty = decorated_ty path is taken). When the class is preserved (ty = self.heap.mk_class_def(cls.dupe())), discard the temporary errors.

Files: pyrefly/lib/alt/solve.rs
Confidence: medium
Affected projects: django-modern-rest
Fixes: bad-specialization
For django-modern-rest, the @final decorator returns the class itself, so the heuristic correctly preserves the class. But the call_infer call itself emits implicit-any-type-argument as a side effect during type parameter resolution of the generic class SSEvent(Generic[_DataT_co]). By deferring error emission until we know whether the decorator result is used, we avoid spurious errors from decorator calls whose results are discarded. This would fix the 1 django-modern-rest error.


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (4 LLM)

@github-actions

Copy link
Copy Markdown

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

jax (https://github.com/google/jax)
+ ERROR jax/experimental/array_serialization/serialization_test.py:820:1-821:38: `GenericResidual@Typ` is not assignable to upper bound `type[Any]` of type variable `Typ` [bad-specialization]
+ ERROR jax/experimental/array_serialization/serialization_test.py:950:5-951:38: `GenericResidual@Typ` is not assignable to upper bound `type[Any]` of type variable `Typ` [bad-specialization]
+ ERROR jax/experimental/array_serialization/serialization_test.py:963:5-35: `UserPytreeAPITest.test_custom_node_registration.P` is not assignable to upper bound `Hashable` of type variable `H` [bad-specialization]
+ ERROR jax/experimental/array_serialization/serialization_test.py:968:5-969:43: `GenericResidual@Typ` is not assignable to upper bound `type[Any]` of type variable `Typ` [bad-specialization]

spark (https://github.com/apache/spark)
+ ERROR python/pyspark/testing/tests/test_skip_class.py:20:1-15: Argument `type[SkipClassTests]` is not assignable to parameter `reason` with type `str` in function `unittest.case.skip` [bad-argument-type]

django-modern-rest (https://github.com/wemake-services/django-modern-rest)
+ ERROR dmr/streaming/sse/metadata.py:37:1-7: Cannot determine the type parameter `_DataT_co` for generic class `SSEvent` [implicit-any-type-argument]

@github-actions

Copy link
Copy Markdown

Primer Diff Classification

❌ 2 regression(s) | ✅ 1 improvement(s) | 3 project(s) total | +6 errors

2 regression(s) across jax, django-modern-rest. error kinds: bad-specialization on register_dataclass decorator, bad-specialization on register_static decorator, implicit-any-type-argument on @final generic class. 1 improvement(s) across spark.

Project Verdict Changes Error Kinds Root Cause
jax ❌ Regression +4 bad-specialization on register_dataclass decorator pyrefly/lib/alt/solve.rs
spark ✅ Improvement +1 bad-argument-type pyrefly/lib/alt/solve.rs
django-modern-rest ❌ Regression +1 implicit-any-type-argument on @final generic class pyrefly/lib/alt/solve.rs
Detailed analysis

❌ Regression (2)

jax (+4)

bad-specialization on register_dataclass decorator: Lines 820, 950, 968: pyrefly incorrectly reports GenericResidual@Typ not assignable to type[Any] when applying jax.tree_util.register_dataclass as a class decorator. The classes being decorated ARE valid type[Any] objects. This is a type inference failure in the new decorator processing code — the internal type GenericResidual@Typ is a pyrefly artifact, not a real type mismatch. 0/3 co-reported by mypy/pyright.
bad-specialization on register_static decorator: Line 963: pyrefly incorrectly reports local class P not assignable to Hashable bound when applying @jax.tree_util.register_static. The register_static decorator takes a class (type object), and all type objects in Python are hashable since type inherits __hash__ from object. This is a false positive from the new decorator analysis. 0/1 co-reported by mypy/pyright.

Overall: These are false positives introduced by the new class decorator handling. The errors claim:

  1. Lines 820, 950, 968: GenericResidual@Typ is not assignable to upper bound type[Any] — but the classes being passed ARE type[Any] (they are class objects like CustomDataclass, CustomDNode, D).
  2. Line 963: P is not assignable to upper bound Hashable — but register_static receives the class object P (i.e., type[P]), and all class objects (type objects) are hashable in Python since type inherits __hash__ from object. The question of whether dataclass instances are hashable is irrelevant here.

The key issue is that pyrefly's new decorator processing is incorrectly resolving type variable bindings when calling these JAX utility decorators. The GenericResidual@Typ notation suggests an internal type representation leak. Neither mypy nor pyright reports these errors, confirming they are false positives from the new decorator analysis logic.

Attribution: The PR changes in pyrefly/lib/alt/solve.rs at the Binding::ClassDef match arm now process class decorators by calling call_infer on each decorator. Previously, class decorators were completely ignored (the TODO comment said 'we don't actually support any type-level analysis of class decorators'). The new code calls as_call_target_or_error and call_infer for each decorator, which triggers type checking of the decorator call including type argument validation. This causes bad-specialization errors when pyrefly incorrectly resolves the type arguments for jax.tree_util.register_dataclass (which is generic with GenericResidual@Typ bounded by type[Any]) and jax.tree_util.register_static (which has a Hashable bound). The decorator functions accept classes and return them, but pyrefly's new inference is producing incorrect type variable assignments that violate the bounds.

django-modern-rest (+1)

implicit-any-type-argument on @Final generic class: False positive. The @final decorator preserves the type identity of the class. The type parameter _DataT_co is properly declared via Generic[_DataT_co]. The error is triggered by the PR's new decorator processing logic which apparently fails to properly handle type parameter preservation when applying @final to a generic class. This is pyrefly-only (not flagged by mypy or pyright).

Overall: This is a false positive introduced by the PR's new class decorator handling. The @final decorator is defined as def final(f: _F) -> _F in typeshed — it returns exactly the same type it receives. The class SSEvent properly declares Generic[_DataT_co] with _DataT_co = TypeVar('_DataT_co', covariant=True). There is no ambiguity about the type parameter at the class definition site. The error Cannot determine the type parameter '_DataT_co' for generic class 'SSEvent' is nonsensical at the class definition — the type parameter is being declared, not instantiated. Neither mypy nor pyright flag this. The PR's new decorator processing code in solve.rs is incorrectly triggering this error when it processes the @final decorator on a generic class.

Attribution: The PR changes how class decorators are processed in pyrefly/lib/alt/solve.rs in the Binding::ClassDef match arm. Previously, decorators were ignored (TODO: analyze the class decorators). Now, pyrefly iterates through decorators and calls call_infer on each. For @final, the decorator returns the class itself (it's typed as def final(f: _F) -> _F). The new code checks decorator_result_is_callable and untype_opt to decide whether to preserve the original class type or use the decorated type. The issue is that when processing @final on a generic class SSEvent[_DataT_co], the new decorator application logic appears to trigger implicit-any-type-argument because it's calling the decorator with the class as an argument and the type parameter inference for the generic class gets confused during this process. The @final decorator should be transparent (it returns the same type), but the new call inference machinery apparently loses track of the type parameter _DataT_co during the decorator application step.

✅ Improvement (1)

spark (+1)

This is a genuine type error. unittest.skip expects a str argument (the skip reason), but @unittest.skip without parentheses passes the class itself as the reason parameter. The correct usage would be @unittest.skip('reason') with parentheses. While CPython's implementation happens to handle this gracefully at runtime, the type stubs correctly declare skip(reason: str), and pyright also flags this. Pyrefly's new class decorator support correctly catches this type mismatch. See https://typing.readthedocs.io/en/latest/spec/callables.html#decorators for decorator call semantics.
Attribution: The change in pyrefly/lib/alt/solve.rs in the Binding::ClassDef match arm now actually processes class decorators by calling call_infer on them, passing the class type as an argument. Previously, pyrefly skipped decorator analysis entirely (the old code had a TODO comment saying 'we don't actually support any type-level analysis of class decorators'). Now that pyrefly processes @unittest.skip as a call with type[SkipClassTests] as the argument, it correctly detects the type mismatch with the reason: str parameter.

Suggested fixes

Summary: The new class decorator processing in solve.rs introduces false positive errors when decorators are applied to generic classes, because the class type passed to call_infer carries unresolved type parameters that fail bound checks.

1. In the Binding::ClassDef match arm in solve.rs (around line 5290), when constructing the arg passed to call_infer for decorator processing, the class type ty (which is a Type::ClassDef with generic parameters) is passed directly. For decorators like register_dataclass(Typ: type[Any]) or register_static(cls: type[Hashable]), pyrefly tries to bind the class's unresolved type parameters against the decorator's parameter bounds, producing false bad-specialization errors. The fix should ensure that when the class being decorated has unresolved type parameters (i.e., it's a generic class definition), the type passed to the decorator call should be treated as type[Any] or the type parameters should be erased/filled with their bounds before checking against the decorator's parameter constraints. Specifically, before calling call_infer, add a step that checks if ty is a generic class def with free type variables, and if so, substitutes them appropriately (e.g., with their upper bounds or Any) so the decorator call doesn't produce spurious bound violations. Pseudo-code: let arg_ty = if ty.has_free_type_params() { self.erase_type_params_to_bounds(ty) } else { ty }; let arg = CallArg::ty(&arg_ty, range);

Files: pyrefly/lib/alt/solve.rs
Confidence: high
Affected projects: jax
Fixes: bad-specialization
The jax errors show 'GenericResidual@Typ is not assignable to type[Any]' — the internal type representation 'GenericResidual@Typ' is leaking because the class's own type parameters are being matched against the decorator's parameter bounds. When a generic class like class D(SomeBase): is decorated with @register_dataclass, pyrefly passes the class type including its unresolved generic residuals to the decorator's type checker, which then fails to match them against type[Any]. Similarly, P fails against Hashable because the class's type parameter is being confused with the class object itself. All 4 jax errors are pyrefly-only (0/4 mypy, 0/4 pyright), confirming these are false positives.

2. In the Binding::ClassDef match arm in solve.rs (around line 5310-5320), the decorator result handling logic checks decorator_result_is_callable and untype_opt to decide whether to preserve the original class type. For @final applied to a generic class like SSEvent[_DataT_co], the decorator is typed as def final(f: _F) -> _F, so it returns the class type back. The issue is that during call_infer, when _F is bound to the generic class SSEvent, pyrefly apparently triggers implicit-any-type-argument because it cannot determine the type parameter _DataT_co for the generic class at the definition site. The fix: add a special case for known identity decorators like @final (and @override, @abstractmethod, etc.) that simply skip the call_infer step entirely, similar to how dataclass_transform decorators are already skipped with continue. Pseudo-code: add a check like if decorator.ty.is_identity_decorator() { continue; } or check if the decorator's function kind matches FunctionKind::Final before the as_call_target_or_error call.

Files: pyrefly/lib/alt/solve.rs
Confidence: high
Affected projects: django-modern-rest
Fixes: implicit-any-type-argument
The django-modern-rest error 'Cannot determine the type parameter _DataT_co for generic class SSEvent' is triggered when @final is processed as a decorator call on a generic class. At the class definition site, type parameters are being declared, not instantiated, so asking 'what is _DataT_co?' is nonsensical. The @final decorator is an identity function (_F -> _F) and should not trigger type parameter resolution. This is 1/1 pyrefly-only (0/1 mypy, 0/1 pyright). Skipping identity decorators like @final would eliminate this false positive while preserving the correct decorator analysis for non-identity decorators.


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (3 LLM)

@github-actions github-actions Bot added size/m and removed size/m labels Jun 16, 2026
@github-actions

Copy link
Copy Markdown

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

spark (https://github.com/apache/spark)
+ ERROR python/pyspark/testing/tests/test_skip_class.py:20:1-15: Argument `type[SkipClassTests]` is not assignable to parameter `reason` with type `str` in function `unittest.case.skip` [bad-argument-type]

jax (https://github.com/google/jax)
+ ERROR jax/experimental/array_serialization/serialization_test.py:820:1-821:38: `GenericResidual@Typ` is not assignable to upper bound `type[Any]` of type variable `Typ` [bad-specialization]
+ ERROR jax/experimental/array_serialization/serialization_test.py:950:5-951:38: `GenericResidual@Typ` is not assignable to upper bound `type[Any]` of type variable `Typ` [bad-specialization]
+ ERROR jax/experimental/array_serialization/serialization_test.py:963:5-35: `UserPytreeAPITest.test_custom_node_registration.P` is not assignable to upper bound `Hashable` of type variable `H` [bad-specialization]
+ ERROR jax/experimental/array_serialization/serialization_test.py:968:5-969:43: `GenericResidual@Typ` is not assignable to upper bound `type[Any]` of type variable `Typ` [bad-specialization]

django-modern-rest (https://github.com/wemake-services/django-modern-rest)
+ ERROR dmr/streaming/sse/metadata.py:37:1-7: Cannot determine the type parameter `_DataT_co` for generic class `SSEvent` [implicit-any-type-argument]

@github-actions

Copy link
Copy Markdown

Primer Diff Classification

❌ 2 regression(s) | ✅ 1 improvement(s) | 3 project(s) total | +6 errors

2 regression(s) across jax, django-modern-rest. error kinds: bad-specialization, implicit-any-type-argument on @final generic class. caused by is_toplevel_callable(). 1 improvement(s) across spark.

Project Verdict Changes Error Kinds Root Cause
spark ✅ Improvement +1 bad-argument-type pyrefly/lib/alt/solve.rs
jax ❌ Regression +4 bad-specialization pyrefly/lib/alt/solve.rs
django-modern-rest ❌ Regression +1 implicit-any-type-argument on @final generic class is_toplevel_callable()
Detailed analysis

❌ Regression (2)

jax (+4)

The PR introduces class decorator support but pyrefly's inference produces GenericResidual@Typ for three of the four errors — an internal type representation that indicates a type resolution failure when processing the return types of JAX decorator functions like jax.tree_util.register_dataclass and jax.tree_util.register_static. These three errors (lines 820, 950, 968) are false positives: the decorators are valid JAX API calls that work correctly at runtime, and neither mypy nor pyright flags them. The GenericResidual@Typ pattern in error messages is a hallmark of inference failures where pyrefly cannot properly resolve the generic type variable Typ in the decorator's return type.

The fourth error (line 963) is different in nature. It reports that UserPytreeAPITest.test_custom_node_registration.P is not assignable to upper bound Hashable of type variable H. Here, class P is a @dataclass (which defines __eq__ and therefore sets __hash__ to None by default, making instances unhashable). However, jax.tree_util.register_static is being used as a class decorator, and its type variable H has an upper bound of Hashable. While the dataclass P without frozen=True or unsafe_hash=True is technically not hashable at the instance level, this code works at runtime because register_static operates on the class object itself (which is hashable as a type), not on instances. This is still a false positive from pyrefly — it's checking the wrong thing (the class as a type argument vs. instance hashability), but the underlying issue is distinct from the GenericResidual@Typ inference failures.

Attribution: The new class decorator processing code in pyrefly/lib/alt/solve.rs (lines 5270-5320 in the Binding::ClassDef match arm) now calls call_infer on decorators. When processing jax.tree_util.register_dataclass (via partial) and jax.tree_util.register_static, the type inference fails to properly resolve the type variables, producing GenericResidual@Typ types that then fail bound checks. Previously these decorators were skipped entirely (the old TODO comment).

django-modern-rest (+1)

implicit-any-type-argument on @Final generic class: The @final decorator should not interfere with generic type parameter resolution. SSEvent(Generic[_DataT_co]) clearly defines _DataT_co as its type parameter. The PR's new decorator processing logic appears to lose this information when applying @final, producing a spurious implicit-any-type-argument error. This is pyrefly-only and is a false positive regression.

Overall: The analysis is factually correct. The @final decorator from typing is a standard decorator that marks a class as not subclassable — it should not affect type parameter resolution. The class SSEvent(_SSEventSlots, Generic[_DataT_co]) clearly defines _DataT_co as its type parameter through inheriting from Generic[_DataT_co]. The error implicit-any-type-argument at line 37 (pointing to @final) indicates that pyrefly's decorator handling is interfering with the generic class's type parameter resolution. This is indeed a false positive — the @final decorator should be transparent with respect to generic type parameters. The class definition is valid Python, and the type parameter _DataT_co is properly defined and used throughout the class. The analysis correctly identifies this as a regression in pyrefly's new decorator handling logic that loses type parameter information when processing @final on generic classes.

Attribution: The PR changes how class decorators are processed in pyrefly/lib/alt/solve.rs. The new code in the Binding::ClassDef branch now iterates over decorators and attempts to call them, then checks if the result is a callable or can be 'untyped' back to a class. For @final, the decorator's return type is type[T] (the same class type), which is neither a 'toplevel callable' in the traditional sense nor necessarily handled by untype_opt. The logic at the end of the decorator loop — if decorated_ty.[is_toplevel_callable()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/solve.rs) || self.untype_opt(decorated_ty.clone(), range, errors).[is_some()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/solve.rs) { ty = self.heap.mk_class_def(cls.dupe()); } — should preserve the class definition for @final since type[SSEvent] should be untypeable. However, the interaction with the generic type parameter _DataT_co appears to be causing the implicit-any-type-argument error, likely because the decorator application process loses track of the type parameter binding. The @final decorator is not in the dataclass_transform_metadata skip list, so it goes through the full decorator application path, and something in that path fails to properly propagate the generic type parameter.

✅ Improvement (1)

spark (+1)

This is a genuine type error. unittest.skip is typed in typeshed as def skip(reason: str) -> Callable[[_FT], _FT]. When used as @unittest.skip (without parentheses), the class SkipClassTests is passed as the reason argument, which has type type[SkipClassTests], not str. Pyright also flags this. While it works at runtime due to CPython's implementation being lenient, the static type signature clearly doesn't match. The PR's new class decorator support correctly identifies this mismatch by actually analyzing decorator applications on classes.
Attribution: The change in pyrefly/lib/alt/solve.rs in the Binding::ClassDef match arm now applies class decorators by calling them with the class type as an argument. Previously, decorators were not analyzed at all (the TODO comment said 'we don't actually support any type-level analysis of class decorators'). Now pyrefly passes type[SkipClassTests] as an argument to unittest.skip, which expects str as its reason parameter, correctly detecting the type mismatch.

Suggested fixes

Summary: The new class decorator processing logic in solve.rs causes false positive errors when decorators like @Final don't change the class type, and when decorator return types contain unresolved generic type variables.

1. In the Binding::ClassDef match arm in solve.rs (around line 5270), add a check to skip decorators that are known to be identity-like (such as typing.final) before calling call_infer. The @final decorator has return type type[T] which when called with the class should resolve back to the class, but the decorator application process is triggering implicit-any-type-argument because the call inference path loses track of the class's generic type parameters. Add a guard like: if the decorator type resolves to typing.final (or more generally, if the decorator's callable signature is (type[T]) -> type[T] with no other effects), skip the call_infer and preserve ty = self.heap.mk_class_def(cls.dupe()). Alternatively, after the decorator loop, if ty is still a class def for the same class cls, ensure the original class def (with its generic parameters intact) is returned rather than a reconstructed one.

Files: pyrefly/lib/alt/solve.rs
Confidence: high
Affected projects: django-modern-rest
Fixes: implicit-any-type-argument
The @Final decorator should be transparent to type parameter resolution. The current code calls call_infer on @Final, which produces type[SSEvent]. Then untype_opt succeeds on this, so ty = self.heap.mk_class_def(cls.dupe()) is executed — which should be correct. However, the call_infer call itself may be emitting the implicit-any-type-argument error as a side effect during type variable resolution, even though the final ty is correct. The fix should either skip @Final entirely (like dataclass_transform decorators are skipped) or suppress errors during the call_infer for identity decorators. This would eliminate the 1 pyrefly-only error in django-modern-rest.

2. In the Binding::ClassDef match arm in solve.rs (around line 5270), when call_infer fails to properly resolve generic type variables in decorator return types (producing GenericResidual@Typ types), the code should fall back to preserving the original class definition. Specifically, after the call_infer call (around line 5310), add a check: if the decorated_ty contains unresolved generic residuals (e.g., GenericResidual@Typ), treat the decorator as identity-like and set ty = self.heap.mk_class_def(cls.dupe()) instead of propagating the broken type. Additionally, the error emitted during call_infer (the bound check failure for Hashable) should be suppressed when the decorator application itself fails to resolve properly.

Files: pyrefly/lib/alt/solve.rs
Confidence: medium
Affected projects: jax
Fixes: bad-argument-type
The JAX decorators register_dataclass (via partial) and register_static have generic return types that pyrefly cannot fully resolve, producing GenericResidual@Typ. The current code path then falls through to ty = decorated_ty (the else branch at line 5318), propagating these broken types. The 3 GenericResidual@Typ errors and the 1 Hashable bound check error are all side effects of calling call_infer on decorators whose types pyrefly cannot fully resolve. When inference fails this way, the safe fallback is to preserve the original class definition. This would eliminate 4 pyrefly-only errors in jax.

3. In the Binding::ClassDef decorator loop in solve.rs, pass errors to a separate error collector (or use a 'tentative' error mode) during call_infer and as_call_target_or_error, and only commit those errors if the decorator application succeeds cleanly (i.e., the result is not a GenericResidual and doesn't indicate inference failure). This prevents side-effect errors from being emitted during decorator processing when the inference cannot fully resolve the decorator's type.

Files: pyrefly/lib/alt/solve.rs
Confidence: medium
Affected projects: jax, django-modern-rest
Fixes: implicit-any-type-argument, bad-argument-type
Both the jax and django-modern-rest regressions share a common root cause: errors are emitted as side effects during call_infer even when the decorator processing ultimately falls back to preserving the class definition. For @Final on django-modern-rest, the implicit-any-type-argument error is emitted during call_infer even though the final type is correct. For jax, bound-check and type-resolution errors are emitted during call_infer for decorators that can't be fully resolved. Using a tentative error collector would prevent these side-effect errors. This would eliminate all 5 pyrefly-only regression errors across both projects.


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (3 LLM)

@github-actions github-actions Bot added size/m and removed size/m labels Jul 17, 2026
@github-actions

Copy link
Copy Markdown

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

jax (https://github.com/google/jax)
+ ERROR jax/experimental/array_serialization/serialization_test.py:946:5-949:55: Argument `type[UserPytreeAPITest.test_register_as_decorator.CustomDNode]` is not assignable to parameter `nodetype` with type `type[T]` [bad-argument-type]
+ ERROR jax/experimental/array_serialization/serialization_test.py:963:5-35: `UserPytreeAPITest.test_custom_node_registration.P` is not assignable to upper bound `Hashable` of type variable `H` [bad-specialization]

spark (https://github.com/apache/spark)
+ ERROR python/pyspark/testing/tests/test_skip_class.py:20:1-15: Argument `type[SkipClassTests]` is not assignable to parameter `reason` with type `str` in function `unittest.case.skip` [bad-argument-type]

django-modern-rest (https://github.com/wemake-services/django-modern-rest)
+ ERROR dmr/streaming/sse/metadata.py:37:1-7: Cannot determine the type parameter `_DataT_co` for generic class `SSEvent[_DataT_co]` [implicit-any-type-argument]

General class-decorator application overlapped with main's handling of untyped decorators and explicit type[Any] erasure. Preserve those diagnostics and escape-hatch semantics while applying typed decorators that transform runtime class values.
@github-actions

Copy link
Copy Markdown

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

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.

Preliminary support for class decorators

5 participants