Skip to content

fix assert_type with metaclasses #3228#3243

Open
asukaminato0721 wants to merge 2 commits intofacebook:mainfrom
asukaminato0721:3228
Open

fix assert_type with metaclasses #3228#3243
asukaminato0721 wants to merge 2 commits intofacebook:mainfrom
asukaminato0721:3228

Conversation

@asukaminato0721
Copy link
Copy Markdown
Contributor

@asukaminato0721 asukaminato0721 commented Apr 25, 2026

Summary

Fixes #3228

assert_type no longer accepts a class object as exactly equal to its non-type metaclass.

Test Plan

add test

@github-actions

This comment has been minimized.

@asukaminato0721 asukaminato0721 marked this pull request as ready for review April 25, 2026 19:00
Copilot AI review requested due to automatic review settings April 25, 2026 19:00
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 typing.assert_type behavior for classes with custom metaclasses so that asserting a class object against its metaclass (e.g., assert_type(A, Meta)) correctly fails, aligning with the typing spec and other type checkers.

Changes:

  • Updated call_assert_type to explicitly treat “class object is an instance of a non-type metaclass” as a mismatch for assert_type equivalence.
  • Added a regression test covering assert_type with a custom metaclass.

Reviewed changes

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

File Description
pyrefly/lib/test/simple.rs Adds a regression testcase ensuring assert_type(A, Meta) produces an error for metaclass instances.
pyrefly/lib/alt/special_calls.rs Adjusts assert_type checking logic to reject metaclass-instance matches (except for builtin type) even if prior equivalence logic allowed them.

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

Comment thread pyrefly/lib/alt/special_calls.rs Outdated
@kinto0 kinto0 self-assigned this Apr 29, 2026
@meta-codesync
Copy link
Copy Markdown
Contributor

meta-codesync Bot commented Apr 29, 2026

@kinto0 has imported this pull request. If you are a Meta employee, you can view this in D103049155.

Copy link
Copy Markdown
Contributor

@grievejia grievejia left a comment

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.

@kinto0
Copy link
Copy Markdown
Contributor

kinto0 commented Apr 30, 2026

from @grievejia

I'm not certain this is the correct location to address the issue. assert_type(A, B) is defined in the spec to check for equivalence—meaning mutual subtyping for static types. The diff seems to alter this behavior, which would reduce our spec compliance.
The real problem appears to be in the shared subset/equivalence logic. It's concerning that we currently treat Meta as a subtype of type[A]; this may be the root cause. The assert_type() issue is likely just a symptom.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

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

comtypes (https://github.com/enthought/comtypes)
+ ERROR comtypes/safearray.py:112:19-28: No matching overload found for function `_ctypes.POINTER` called with arguments: (_PyCStructType) [no-matching-overload]
+ ERROR comtypes/safearray.py:380:27-36: No matching overload found for function `_ctypes.POINTER` called with arguments: (_PyCStructType) [no-matching-overload]
- ERROR comtypes/safearray.py:393:13-51: Expected first argument to `super` to be a class object, got `type[_Pointer[_Pointer[Unknown]]]` [invalid-argument]
+ ERROR comtypes/safearray.py:393:34-43: No matching overload found for function `_ctypes.POINTER` called with arguments: (_PyCStructType) [no-matching-overload]

manticore (https://github.com/trailofbits/manticore)
+ ERROR manticore/core/state_pb2.py:333:25-35: Argument `GeneratedProtocolMessageType` is not assignable to parameter `message` with type `Message | type[Message]` in function `google.protobuf.symbol_database.SymbolDatabase.RegisterMessage` [bad-argument-type]
+ ERROR manticore/core/state_pb2.py:344:25-30: Argument `GeneratedProtocolMessageType` is not assignable to parameter `message` with type `Message | type[Message]` in function `google.protobuf.symbol_database.SymbolDatabase.RegisterMessage` [bad-argument-type]
+ ERROR manticore/core/state_pb2.py:355:25-34: Argument `GeneratedProtocolMessageType` is not assignable to parameter `message` with type `Message | type[Message]` in function `google.protobuf.symbol_database.SymbolDatabase.RegisterMessage` [bad-argument-type]
+ ERROR manticore/core/state_pb2.py:366:25-36: Argument `GeneratedProtocolMessageType` is not assignable to parameter `message` with type `Message | type[Message]` in function `google.protobuf.symbol_database.SymbolDatabase.RegisterMessage` [bad-argument-type]

streamlit (https://github.com/streamlit/streamlit)
+ ERROR lib/streamlit/elements/lib/options_selector_utils.py:162:36-49: Expected class object, got `EnumMeta & type[E2]` [invalid-argument]
+ ERROR lib/streamlit/elements/lib/options_selector_utils.py:198:12-49: Cannot index into `EnumMeta` [bad-index]
+ ERROR lib/streamlit/elements/lib/options_selector_utils.py:254:52-64: Argument `EnumMeta` is not assignable to parameter `to_enum_class` with type `type[@_]` in function `_coerce_enum` [bad-argument-type]
+ ERROR lib/streamlit/elements/lib/options_selector_utils.py:303:31-43: Argument `EnumMeta` is not assignable to parameter `to_enum_class` with type `type[@_]` in function `_coerce_enum` [bad-argument-type]

pandera (https://github.com/pandera-dev/pandera)
+ ERROR tests/pandas/test_decorators.py:1801:29-38: Argument `type[test_coroutines.Meta]` is not assignable to parameter `mcs` with type `type[type[test_coroutines.SomeClass]]` in function `Meta.class_meta_coroutine` [bad-argument-type]
+ ERROR tests/pandas/test_decorators.py:1805:27-35: Argument `type[test_coroutines.Meta]` is not assignable to parameter `mcs` with type `type[type[test_coroutines.SomeClass]]` in function `Meta.class_meta_coroutine` [bad-argument-type]

ibis (https://github.com/ibis-project/ibis)
+ ERROR ibis/common/tests/test_grounds.py:250:19-21: Expected 0 positional arguments, got 1 in function `Between.__init__` [bad-argument-count]
+ ERROR ibis/common/tests/test_grounds.py:250:23-28: Unexpected keyword argument `lower` in function `Between.__init__` [unexpected-keyword]
+ ERROR ibis/common/tests/test_grounds.py:264:20-22: Expected 0 positional arguments, got 1 in function `Between.__init__` [bad-argument-count]
+ ERROR ibis/common/tests/test_grounds.py:264:24-29: Unexpected keyword argument `lower` in function `Between.__init__` [unexpected-keyword]

strawberry (https://github.com/strawberry-graphql/strawberry)
+ ERROR strawberry/schema/schema_converter.py:178:32-66: No matching overload found for function `enum.EnumMeta.__call__` called with arguments: (Any) [no-matching-overload]
+ ERROR strawberry/schema/schema_converter.py:183:32-79: No matching overload found for function `enum.EnumMeta.__call__` called with arguments: (Any) [no-matching-overload]

zope.interface (https://github.com/zopefoundation/zope.interface)
+ ERROR src/zope/interface/common/collections.py:131:5-8: Class member `IReversible.abc` overrides parent class `IIterable` in an inconsistent manner [bad-override-mutable-attribute]
+ ERROR src/zope/interface/common/collections.py:144:5-8: Class member `IGenerator.abc` overrides parent class `IIterator` in an inconsistent manner [bad-override-mutable-attribute]
+ ERROR src/zope/interface/common/collections.py:157:5-8: Class member `ICollection.abc` overrides parent class `ISized` in an inconsistent manner [bad-override-mutable-attribute]
+ ERROR src/zope/interface/common/collections.py:157:5-8: Class member `ICollection.abc` overrides parent class `IIterable` in an inconsistent manner [bad-override-mutable-attribute]
+ ERROR src/zope/interface/common/collections.py:157:5-8: Class member `ICollection.abc` overrides parent class `IContainer` in an inconsistent manner [bad-override-mutable-attribute]
+ ERROR src/zope/interface/common/collections.py:199:9-12: Class member `IByteString.abc` overrides parent class `ISequence` in an inconsistent manner [bad-override-mutable-attribute]
+ ERROR src/zope/interface/tests/odd.py:98:13-105:2: `MetaMetaClass` is not assignable to variable `MetaClass` with type `type[MetaClass]` [bad-assignment]
+ ERROR src/zope/interface/tests/test_odd_declarations.py:60:7-46: `MetaClass` is not assignable to variable `Odd` with type `type[Odd]` [bad-assignment]
- ERROR src/zope/interface/tests/test_odd_declarations.py:151:17-20: Invalid base class: `Odd | Unknown` [invalid-inheritance]
- ERROR src/zope/interface/tests/test_odd_declarations.py:155:17-20: Invalid base class: `Odd | Unknown` [invalid-inheritance]
- ERROR src/zope/interface/tests/test_odd_declarations.py:172:17-20: Invalid base class: `Odd | Unknown` [invalid-inheritance]
- ERROR src/zope/interface/tests/test_odd_declarations.py:176:17-20: Invalid base class: `Odd | Unknown` [invalid-inheritance]
- ERROR src/zope/interface/tests/test_odd_declarations.py:200:17-20: Invalid base class: `Odd | Unknown` [invalid-inheritance]
- ERROR src/zope/interface/tests/test_odd_declarations.py:204:17-20: Invalid base class: `Odd | Unknown` [invalid-inheritance]
- ERROR src/zope/interface/tests/test_odd_declarations.py:245:18-21: Invalid base class: `Odd | Unknown` [invalid-inheritance]
+ ERROR src/zope/interface/tests/test_odd_declarations.py:269:13-56: `MetaClass` is not assignable to variable `A` with type `type[Test.test_odd_metaclass_that_doesnt_subclass_type.A]` [bad-assignment]
+ ERROR src/zope/interface/tests/test_odd_declarations.py:274:13-56: `MetaClass` is not assignable to variable `B` with type `type[Test.test_odd_metaclass_that_doesnt_subclass_type.B]` [bad-assignment]
+ ERROR src/zope/interface/tests/test_odd_declarations.py:298:26-29: Object of class `C` has no attribute `c` [missing-attribute]

AutoSplit (https://github.com/Toufool/AutoSplit)
+ ERROR src/capture_method/__init__.py:57:16-23: No matching overload found for function `enum.EnumMeta.__call__` called with arguments: (object) [no-matching-overload]

kornia (https://github.com/kornia/kornia)
+ ERROR kornia/constants.py:36:32-34: Argument `Self@_KORNIA_EnumMeta` is not assignable to parameter `self` with type `type[@_]` in function `enum.EnumMeta.__iter__` [bad-argument-type]

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

Primer Diff Classification

❌ 7 regression(s) | ✅ 1 improvement(s) | ❓ 1 needs review | 9 project(s) total | +30, -8 errors

7 regression(s) across comtypes, manticore, streamlit, pandera, strawberry, AutoSplit, kornia. error kinds: bad-argument-type, no-matching-overload on POINTER() with metaclass argument, Removed invalid-argument on super(). caused by is_subset(). 1 improvement(s) across zope.interface.

Project Verdict Changes Error Kinds Root Cause
comtypes ❌ Regression +3, -1 no-matching-overload on POINTER() with metaclass argument pyrefly/lib/solver/subset.rs
manticore ❌ Regression +4 bad-argument-type is_subset()
streamlit ❌ Regression +4 bad-argument-type, bad-index pyrefly/lib/solver/subset.rs
pandera ❌ Regression +2 bad-argument-type pyrefly/lib/solver/subset.rs
ibis ❓ Needs Review +4 bad-argument-count, unexpected-keyword
strawberry ❌ Regression +2 no-matching-overload pyrefly/lib/solver/subset.rs
zope.interface ✅ Improvement +9, -7 bad-assignment on MetaClass reassignment pyrefly/lib/solver/subset.rs
AutoSplit ❌ Regression +1 no-matching-overload on metaclass __call__ pyrefly/lib/solver/subset.rs
kornia ❌ Regression +1 bad-argument-type pyrefly/lib/solver/subset.rs
Detailed analysis

❌ Regression (7)

comtypes (+3, -1)

no-matching-overload on POINTER() with metaclass argument: All 3 new errors flag POINTER(sa_type) where sa_type is a _PyCStructType instance. At runtime this is a valid ctypes class and POINTER() accepts it. The PR's new subset rules are too strict — they prevent metaclass instances from matching type[X] overload parameters. Neither mypy nor pyright flags these. These are false positives (regression).
Removed invalid-argument on super(): The removed error claimed type[_Pointer[_Pointer[Unknown]]] was invalid as first argument to super(). This was a false positive — POINTER(POINTER(sa_type)) returns a valid class. Removing it is an improvement.

Overall: The PR fixes a real issue (metaclass instances shouldn't be exactly equal to type[A] for assert_type), but the implementation is too broad. It now rejects metaclass instances from matching type[X] in ALL contexts, including function calls like POINTER(). This creates 3 false positive no-matching-overload errors on valid code. The 1 removed error was a genuine false positive. Net: +3 false positives, -1 false positive = regression.

Per-category reasoning:

  • no-matching-overload on POINTER() with metaclass argument: All 3 new errors flag POINTER(sa_type) where sa_type is a _PyCStructType instance. At runtime this is a valid ctypes class and POINTER() accepts it. The PR's new subset rules are too strict — they prevent metaclass instances from matching type[X] overload parameters. Neither mypy nor pyright flags these. These are false positives (regression).
  • Removed invalid-argument on super(): The removed error claimed type[_Pointer[_Pointer[Unknown]]] was invalid as first argument to super(). This was a false positive — POINTER(POINTER(sa_type)) returns a valid class. Removing it is an improvement.

Attribution: The new subset rules added in pyrefly/lib/solver/subset.rs (the two new match arms for Type::ClassType(class), Type::Type(want) and Type::ClassType(class), Type::ClassDef(_)) prevent metaclass instances from being treated as type[X]. This causes POINTER(sa_type) to fail overload resolution because sa_type (typed as _PyCStructType) no longer matches the type[_CT] parameter of POINTER()'s overloads.

manticore (+4)

The PR correctly tightens metaclass-to-type[X] assignability. GeneratedProtocolMessageType is a metaclass, and its instances (the created classes) should be typed as type[Message], but the protobuf stubs type the return of the metaclass call as GeneratedProtocolMessageType rather than type[Message]. Pyright agrees with these errors (4/4). This is a legitimate type-level issue in the protobuf stubs/generated code, and pyrefly is correct to flag it.
Attribution: The change to Subset::[is_subset()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/solver/subset.rs) in pyrefly/lib/solver/subset.rs adds two new match arms that reject Type::ClassType(class) (a metaclass instance) from being assignable to Type::Type(want) or Type::ClassDef(_) when the class is a subclass of type but has empty type arguments. This prevents GeneratedProtocolMessageType from being treated as type[Message].

streamlit (+4)

The PR correctly tightens the relationship between metaclass instances and type[X], but the implementation is too aggressive in this case. Errors 1 and 2 are clear false positives: isinstance(value, enum_meta_instance) is valid Python, and EnumMeta.__getitem__ is a well-defined operation. Errors 3 and 4 have some validity (pyright agrees), but they arise from a common Python pattern where EnumMeta is used interchangeably with type[Enum]. The net effect is 2 clear false positives and 2 debatable errors, making this a regression overall.
Attribution: The new match arms in pyrefly/lib/solver/subset.rs that reject Type::ClassType(class) against Type::Type(want) and Type::ClassDef(_) when the class is a metaclass subclass with empty targs. This causes EnumMeta (a metaclass) to no longer be treated as compatible with type[E2], which cascades into the isinstance and indexing errors.

pandera (+2)

The new errors are false positives. The code is valid Python — calling a @classmethod on a metaclass via a class that uses that metaclass is standard practice. Meta is a metaclass (subclass of type), and class_meta_coroutine is a @classmethod on Meta. When accessed via SomeClass (an instance of Meta), Python passes Meta itself as the mcs parameter (since @classmethod on a metaclass receives the metaclass, not the instance class). The type of Meta is type[Meta], but since Meta is itself type, this should be compatible. The error message reveals a type inference bug: pyrefly expects mcs to have type type[type[SomeClass]] (double-wrapped), which is incorrect. The correct expected type for mcs in a metaclass classmethod called via SomeClass should be type[Meta] (or simply the metaclass of Meta). The double-wrapping of type[type[...]] indicates pyrefly is incorrectly applying the classmethod cls parameter type resolution twice — once for the metaclass level and once for the class level. This is a pyrefly bug in handling classmethods defined on metaclasses.
Attribution: The change to subset.rs in pyrefly/lib/solver/subset.rs added new (Type::ClassType(class), Type::Type(want)) and (Type::ClassType(class), Type::ClassDef(_)) match arms that reject metaclass instances as subtypes of specific type[X]. This interacts poorly with how pyrefly resolves the mcs parameter type in metaclass classmethods, producing the spurious bad-argument-type errors. The inferred expected type type[type[SomeClass]] appears to be a double-wrapping bug — mcs in a metaclass classmethod should expect type[Meta] or just Meta, not type[type[SomeClass]].

strawberry (+2)

These are false positives. The code self.wrapped_cls(value) where wrapped_cls: type[Enum] is perfectly valid Python — it's standard enum instantiation. EnumMeta.__call__ has overloads that accept (cls, value). The PR's new metaclass subset rules are too aggressive and break overload resolution for enum class calls. Neither mypy nor pyright flags this code.
Attribution: The change to subset.rs in pyrefly/lib/solver/subset.rs adds two new match arms that reject Type::ClassType(class) against Type::Type(want) and Type::ClassDef(_) when the class is a metaclass subclass with empty targs. This stricter metaclass handling is cascading into overload resolution for EnumMeta.__call__, causing pyrefly to fail to find a matching overload when calling an enum class with a value argument.

AutoSplit (+1)

no-matching-overload on metaclass call: The new subtyping restriction in subset.rs prevents ContainerEnumMeta (a metaclass) from matching type[X] in overload resolution for EnumMeta.__call__. This breaks the standard pattern of calling cls(value) inside a metaclass method. The code is correct — this is how Python enums work. Neither mypy nor pyright flag this. This is a false positive introduced by the PR's overly broad metaclass subtyping change.

Overall: This is a false positive / regression. Inside a metaclass method like ContainerEnumMeta.__contains__, cls is the metaclass instance (i.e., the class itself). Calling cls(value) is the standard way to construct/look up an enum member by value. The EnumMeta.__call__ method is designed to be called exactly this way. The PR's change to reject metaclass instances as subtypes of type[X] is too aggressive — it breaks legitimate metaclass usage patterns. The fact that neither mypy nor pyright flag this confirms it's overly strict. The PR was intended to fix assert_type with metaclasses (a narrow fix), but it has a broader side effect on overload resolution for metaclass method calls.

Attribution: The change in pyrefly/lib/solver/subset.rs adds two new match arms that reject Type::ClassType(class) being a subtype of Type::Type(want) or Type::ClassDef(_) when the class is a metaclass (subclass of type) but has empty type arguments. This makes metaclass instances no longer assignable to specific type[X] types. In the context of EnumMeta.__call__, cls has type ContainerEnumMeta (which extends EnumMeta which extends type). When pyrefly tries to resolve cls(value), it needs to match ContainerEnumMeta against the overloads of EnumMeta.__call__, which likely expect type[SomeEnum] as self. The new stricter subtyping rule prevents ContainerEnumMeta from matching type[X] for specific X, causing the overload resolution to fail.

kornia (+1)

This is a type inference issue. The @_ in type[@_] indicates pyrefly failed to properly resolve the type parameter in EnumMeta.__iter__'s signature, which is an inference limitation. The code is correct — _KORNIA_EnumMeta inherits from EnumMeta, so when __iter__ calls super().__iter__(), it's passing self (of type Self@_KORNIA_EnumMeta) to EnumMeta.__iter__. Since _KORNIA_EnumMeta is a subclass of EnumMeta, this should be valid. The type[@_] in the error message shows that the type parameter wasn't properly resolved during inference, leading to a type mismatch that wouldn't occur if the parameter were correctly inferred. While pyright also flags this (suggesting some genuine typing complexity with metaclass self parameters), the unresolved @_ type variable is the proximate cause of the error in pyrefly. The code follows a standard pattern of calling super().__iter__() in a metaclass subclass, and the error stems from the type checker's inability to fully resolve the generic type parameter in this metaclass context.
Attribution: The change in pyrefly/lib/solver/subset.rs adds new rules that prevent metaclass instances from being assignable to specific type[X] types. Specifically, the new (Type::ClassType(class), Type::Type(want)) match arm rejects metaclass instances when the type[] parameter is not Any or object. This causes Self@_KORNIA_EnumMeta (a metaclass instance) to fail when checked against type[@_] (the self parameter type of EnumMeta.__iter__). The @_ in the error message indicates pyrefly couldn't resolve the type variable in EnumMeta.__iter__'s self parameter, making this partially an inference issue on top of the new stricter rule.

✅ Improvement (1)

zope.interface (+9, -7)

bad-assignment on MetaClass reassignment: These 4 errors reflect the PR's intended stricter handling of metaclass instances vs type[X]. Pyright agrees on all 4. The code reassigns MetaClass/Odd to metaclass instances, and pyrefly now correctly refuses to treat them as type[OriginalClass]. Improvement.
bad-override-mutable-attribute on ABCInterface.abc: These 4 errors flag subclasses that override the abc class attribute inherited from parent ABCInterface subclasses: IReversible overrides IIterable's abc, IGenerator overrides IIterator's abc, ICollection overrides ISized's abc, and IByteString overrides ISequence's abc. Each parent defines abc as a class variable (e.g., abc = abc.Iterable), and each child reassigns it to a different ABC (e.g., abc = abc.Reversible). While technically a valid mutable attribute override concern, this is the intentional and fundamental design pattern of the ABCInterface framework. Neither mypy nor pyright flags these. Practically these are false positives / unhelpful noise. Regression.
missing-attribute C.c: C is created via odd.MetaClass('A', ...) (non-standard metaclass that doesn't subclass type), and C.c is dynamically assigned at line 297 (C.c = 1). The type checker can't see this dynamic assignment on a non-standard metaclass instance. Pyright also flags this. Borderline — the code works at runtime but is hard to type-check statically. Neutral/minor regression.
Removed invalid-inheritance errors: 7 false positives removed. Odd is a valid base class at runtime, created by odd.MetaClass('Odd', Odd.__bases__, {}). Pyrefly was incorrectly flagging 'Invalid base class: Odd | Unknown'. Improvement.

Overall: Mixed results. The removed invalid-inheritance errors (7) were false positives — clear improvement. The bad-assignment errors (4) are mostly correct per the PR's intent and pyright agreement. The 4 bad-override-mutable-attribute errors flag subclasses of ABCInterface (IReversible overrides IIterable, IGenerator overrides IIterator, ICollection overrides ISized, IByteString overrides ISequence) that each reassign the abc class attribute to a different ABC. While this is technically a valid type concern (mutable attribute override with incompatible type), it's the fundamental design pattern of the ABCInterface framework, and neither mypy nor pyright flags it, making these practically false positives. The missing-attribute on C.c is borderline (dynamic attribute via non-standard metaclass, assigned at line 297 via C.c = 1). Net: 7 false positives removed, ~5 correct new errors, ~4-5 practically false positive new errors. Overall slightly positive but mixed.

Attribution: The changes to pyrefly/lib/solver/subset.rs in the Subset impl added two new match arms that reject Type::ClassType(metaclass) from being assignable to Type::Type(specific_class) or Type::ClassDef. This directly caused the bad-assignment errors in odd.py. The bad-override-mutable-attribute errors appear to be a side effect — the changed metaclass handling likely altered how _new_in_ver's return type is resolved, causing type mismatches in the abc attribute across the ABCInterface hierarchy. The removal of invalid-inheritance errors suggests the new code paths also improved resolution of metaclass-created classes as valid base classes.

❓ Needs Review (1)

ibis (+4)

LLM requested additional files that could not be resolved. Non-trivial change (4 added, 0 removed).

Suggested fixes

Summary: The new metaclass subset rules in subset.rs are too broad — they reject ALL metaclass instances from matching type[X], but should allow matching when the metaclass instance is actually a subclass of X (i.e., when the metaclass created a class that inherits from X).

1. In the first new match arm in is_subset() in pyrefly/lib/solver/subset.rs (the (Type::ClassType(class), Type::Type(want)) arm around line 2191), the current logic unconditionally rejects any metaclass instance (subclass of type with empty targs) from being assignable to type[X] unless X is Any or object. This is too aggressive — it should only reject when we know the metaclass instance cannot be a subclass of X. The fix: instead of returning Err(SubsetError::Other) immediately, fall through to the existing logic that handles ClassType vs Type matching. Specifically, remove the two new match arms entirely, and instead add a narrower check only in the assert_type path (or equivalent exact-type-equality check). Alternatively, if the goal is to prevent Meta from matching type[A] in general assignment, the check should still allow the match when the metaclass instance could plausibly create classes of type X — for example, when the want type's metaclass IS the class type (e.g., EnumMeta instance should match type[Enum] because Enum's metaclass is EnumMeta). The guard condition should be: reject ONLY when want's metaclass is NOT class (or a superclass of class). In pseudo-code: after confirming class is a metaclass subclass with empty targs, check if self.type_order.get_metaclass(want) == class.[class_object()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/solver/subset.rs) || self.type_order.is_subclass(class.[class_object()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/solver/subset.rs), self.type_order.get_metaclass(want)) { /* allow - fall through */ } else { Err(SubsetError::Other) }

Files: pyrefly/lib/solver/subset.rs
Confidence: high
Affected projects: comtypes, streamlit, strawberry, AutoSplit, kornia, pandera, ibis
Fixes: no-matching-overload, bad-argument-type
The core issue is that metaclass instances like EnumMeta, _PyCStructType, ABCMeta, etc. ARE valid type[X] values when X's metaclass is that metaclass. For example, an EnumMeta instance IS a type[Enum] because all enum classes are created by EnumMeta. The current blanket rejection breaks: (1) POINTER() calls in comtypes where _PyCStructType instances should match type[_CData] overloads (3 errors), (2) EnumMeta usage in streamlit/strawberry/AutoSplit/kornia where enum metaclass instances should match type[Enum] overloads (5+ errors), (3) metaclass classmethod calls in pandera where mcs parameter resolution breaks (2 errors). Adding a check that allows the match when the target type's metaclass is compatible with the got metaclass would fix all ~13 pyrefly-only false positives across 6 projects while preserving the intended fix for assert_type (since type[A] is not exactly Meta even if A's metaclass is Meta).


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (9 LLM)

@kinto0 kinto0 assigned grievejia and unassigned kinto0 May 1, 2026
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.

assert_type with metaclasses

4 participants