fix(solver): enforce nominal subtyping for NewType wrappers (#3081)#3083
fix(solver): enforce nominal subtyping for NewType wrappers (#3081)#3083Arths17 wants to merge 4 commits intofacebook:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes a solver edge case where NewType(..., NoneType) (and related normalized forms) could lose nominal behavior and/or be rejected during class metadata parsing, causing plain None to incorrectly satisfy a NewType wrapper.
Changes:
- Teach base-class parsing to treat
Type::Noneas a valid NewType base by mapping it to the stdlibNoneTypeclass. - Refine invalid-base validation to avoid rejecting
NewTypeoverNoneTypevia final/enum inheritance guards. - Add a regression test module for issue #3081 and register it in the test index.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
pyrefly/lib/alt/class/class_metadata.rs |
Adjusts NewType base parsing for Type::None and relaxes final/enum checks for NoneType NewType bases. |
pyrefly/lib/test/issue_3081_repro.rs |
Adds regression coverage ensuring None is not accepted where NewNoneType is required. |
pyrefly/lib/test/mod.rs |
Registers the new regression test module. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let is_none_type_newtype_base = | ||
| is_new_type && class_object.has_toplevel_qname("types", "NoneType"); | ||
| if !is_none_type_newtype_base | ||
| && (metadata.is_final() | ||
| || (metadata.is_enum() | ||
| && !self.get_enum_members(&class_object).is_empty())) |
There was a problem hiding this comment.
is_none_type_newtype_base only checks for types.NoneType. In Python <3.10, Stdlib::none_type resolves to _typeshed.NoneType (also @final), and this branch can still be reached when the NewType base type gets canonicalized to Type::None (e.g. via type[None]). In that case, the final-class guard will still emit Cannot extend final class even though this should be permitted for NewType wrappers. Consider matching against self.stdlib.none_type().class_object() (or allowing both types.NoneType and _typeshed.NoneType) so the exception applies consistently across Python versions.
pyrefly/lib/test/issue_3081_repro.rs
Outdated
| from typing import NewType | ||
| from types import NoneType | ||
|
|
||
| NewNoneType = NewType("NewNoneType", NoneType) | ||
| NewNone = NewNoneType(None) |
There was a problem hiding this comment.
The regression test covers NewType(..., NoneType), but the original issue repro uses NewType(..., type[None]). To ensure the reported scenario stays fixed (and to exercise the type[None] -> Type::None normalization path), consider adding an additional assertion/case in this test module that uses type[None] as the NewType base and verifies that test(None) is still rejected.
rchen152
left a comment
There was a problem hiding this comment.
Thanks for the PR! The general direction looks good. Left a few comments on test coverage and the final check being too narrow. It also looks like the tests failed to run due to a formatting error.
pyrefly/lib/test/issue_3081_repro.rs
Outdated
|
|
||
| use crate::testcase; | ||
|
|
||
| testcase!( |
There was a problem hiding this comment.
Could you please put this test case in the existing new_type.rs test file?
pyrefly/lib/test/issue_3081_repro.rs
Outdated
| from typing import NewType | ||
| from types import NoneType | ||
|
|
||
| NewNoneType = NewType("NewNoneType", NoneType) |
There was a problem hiding this comment.
Please also add a test verifying that NewType("NewNoneType", type[None]) works as expected.
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Rebecca Chen <rchen152@gmail.com>
|
Thanks for the detailed review! I've updated the PR to address all your feedback: |
rchen152
left a comment
There was a problem hiding this comment.
Thanks! A couple more minor comments.
| } | ||
| }) | ||
| } | ||
| Type::Type(box Type::None) if is_new_type => { |
There was a problem hiding this comment.
You can combine this case and the previous one by doing Type::None | Type::Type(box Type::None) if is_new_type.
| // The alias body is `type[T]` for bare aliases and `Annotated[T]` for Annotated aliases; | ||
| // we must handle both. | ||
| match &ty { | ||
| Type::Type(box Type::ClassType(cls)) |
There was a problem hiding this comment.
Is this case necessary? It looks like type[None] is handled by the Type::Type(box Type::None) case you added below.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Thanks for the follow-up nits! I've just pushed the fixes: The tests are still green and mypy_primer is showing that this actually improves compatibility for several open-source projects (like trio and spark) by correctly allowing NewType wrappers on final classes. |
|
Diff from mypy_primer, showing the effect of this PR on open source code: colour (https://github.com/colour-science/colour)
- ERROR colour/hints/__init__.py:130:34-46: Cannot extend final class `RegexFlag` [invalid-inheritance]
core (https://github.com/home-assistant/core)
- ERROR homeassistant/components/matrix/__init__.py:79:50-60: Cannot extend final class `Pattern` [invalid-inheritance]
trio (https://github.com/python-trio/trio)
- ERROR src/trio/_core/_windows_cffi.py:23:28-33: Cannot extend final class `_CDataBase` [invalid-inheritance]
- ERROR src/trio/_core/_windows_cffi.py:24:38-43: Cannot extend final class `_CDataBase` [invalid-inheritance]
spark (https://github.com/apache/spark)
- ERROR python/pyspark/sql/connect/_typing.py:56:86-98: Cannot extend final class `FunctionType` [invalid-inheritance]
- ERROR python/pyspark/sql/pandas/_typing/__init__.pyi:458:86-98: Cannot extend final class `FunctionType` [invalid-inheritance]
|
Primer Diff Classification✅ 4 improvement(s) | 4 project(s) total | -6 errors 4 improvement(s) across colour, core, trio, spark.
Detailed analysis✅ Improvement (4)colour (-1)
core (-1)
trio (-2)
spark (-2)
Was this helpful? React with 👍 or 👎 Classification by primer-classifier (4 LLM) |
|
@rchen152 has imported this pull request. If you are a Meta employee, you can view this in D100284447. |
Title
Fix NewType nominal handling for NoneType base (Fixes #3081)
Summary
This PR fixes a type-checking bug where a NewType defined over NoneType could be treated too much like its underlying type instead of preserving nominal behavior.
Specifically, NewType("NewNoneType", NoneType) should remain a distinct nominal type for static checking. Plain None must not satisfy a parameter annotated as NewNoneType unless explicitly wrapped.
Problem
Pyrefly was incorrectly normalizing parts of the NoneType NewType flow, which could lead to type confusion in analysis:
What changed
Behavior before
Behavior after
Why this is correct
This aligns static behavior with PEP 484 nominal NewType semantics:
Test plan
Both passed.
Risk and compatibility
Suggested reviewer focus
Noneincorrectly satisfiesNewType('N', type[None])#3081.