-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[ty] implement typing.NewType as Type::NewTypeInstance
#21157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Diagnostic diff on typing conformance testsChanges were detected when running ty on typing conformance tests--- old-output.txt 2025-11-06 22:46:32.505249634 +0000
+++ new-output.txt 2025-11-06 22:46:35.797264599 +0000
@@ -1,4 +1,4 @@
-fatal[panic] Panicked at /home/runner/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/05a9af7/src/function/execute.rs:451:17 when checking `/home/runner/work/ruff/ruff/typing/conformance/tests/aliases_typealiastype.py`: `infer_definition_types(Id(18b71)): execute: too many cycle iterations`
+fatal[panic] Panicked at /home/runner/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/05a9af7/src/function/execute.rs:451:17 when checking `/home/runner/work/ruff/ruff/typing/conformance/tests/aliases_typealiastype.py`: `infer_definition_types(Id(18f71)): execute: too many cycle iterations`
_directives_deprecated_library.py:15:31: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int`
_directives_deprecated_library.py:30:26: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `str`
_directives_deprecated_library.py:36:41: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `Self@__add__`
@@ -41,11 +41,19 @@
aliases_implicit.py:119:10: error[invalid-type-form] Variable of type `Literal["int | str"]` is not allowed in a type expression
aliases_implicit.py:128:1: error[type-assertion-failure] Argument does not have asserted type `list[str]`
aliases_implicit.py:133:6: error[call-non-callable] Object of type `UnionType` is not callable
-aliases_newtype.py:15:1: error[type-assertion-failure] Argument does not have asserted type `int`
-aliases_newtype.py:18:1: error[invalid-assignment] Object of type `NewType` is not assignable to `type`
-aliases_newtype.py:23:16: error[invalid-argument-type] Argument to function `isinstance` is incorrect: Expected `type | UnionType | tuple[Unknown, ...]`, found `NewType`
-aliases_newtype.py:26:21: error[invalid-base] Invalid class base with type `NewType`
-aliases_newtype.py:63:43: error[too-many-positional-arguments] Too many positional arguments to bound method `__init__`: expected 3, got 4
+aliases_newtype.py:11:8: error[invalid-argument-type] Argument is incorrect: Expected `int`, found `Literal["user"]`
+aliases_newtype.py:12:1: error[invalid-assignment] Object of type `Literal[42]` is not assignable to `UserId`
+aliases_newtype.py:18:1: error[invalid-assignment] Object of type `<NewType pseudo-class 'UserId'>` is not assignable to `type`
+aliases_newtype.py:23:16: error[invalid-argument-type] Argument to function `isinstance` is incorrect: Expected `type | UnionType | tuple[Unknown, ...]`, found `<NewType pseudo-class 'UserId'>`
+aliases_newtype.py:26:21: error[invalid-base] Cannot subclass an instance of NewType
+aliases_newtype.py:35:1: error[invalid-newtype] The name of a `NewType` (`BadName`) must match the name of the variable it is assigned to (`GoodName`)
+aliases_newtype.py:41:6: error[invalid-type-form] `GoodNewType1` is a `NewType` and cannot be specialized
+aliases_newtype.py:47:38: error[invalid-newtype] invalid base for `typing.NewType`: type `int | str`
+aliases_newtype.py:52:38: error[invalid-newtype] invalid base for `typing.NewType`: type `Hashable`
+aliases_newtype.py:54:38: error[invalid-newtype] invalid base for `typing.NewType`: type `Literal[7]`
+aliases_newtype.py:61:38: error[invalid-newtype] invalid base for `typing.NewType`: type `TD1`
+aliases_newtype.py:63:15: error[invalid-newtype] Wrong number of arguments in `NewType` creation, expected 2, found 3
+aliases_newtype.py:65:38: error[invalid-newtype] invalid base for `typing.NewType`: type `Any`
aliases_type_statement.py:10:19: error[too-many-positional-arguments] Too many positional arguments: expected 1, got 3
aliases_type_statement.py:17:1: error[unresolved-attribute] Object of type `typing.TypeAliasType` has no attribute `bit_count`
aliases_type_statement.py:19:1: error[call-non-callable] Object of type `TypeAliasType` is not callable
@@ -587,7 +595,7 @@
generics_typevartuple_basic.py:12:14: error[invalid-argument-type] `@Todo(starred expression)` is not a valid argument to `Generic`
generics_typevartuple_basic.py:16:26: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `tuple[@Todo(PEP 646), ...]`
generics_typevartuple_basic.py:23:13: error[invalid-argument-type] `@Todo(starred expression)` is not a valid argument to `Generic`
-generics_typevartuple_basic.py:42:34: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `tuple[@Todo(PEP 646), ...]`, found `Literal[1]`
+generics_typevartuple_basic.py:42:34: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `tuple[@Todo(PEP 646), ...]`, found `Height`
generics_typevartuple_basic.py:65:27: error[unknown-argument] Argument `covariant` does not match any known parameter of function `__new__`
generics_typevartuple_basic.py:66:27: error[too-many-positional-arguments] Too many positional arguments to function `__new__`: expected 2, got 4
generics_typevartuple_basic.py:67:27: error[unknown-argument] Argument `bound` does not match any known parameter of function `__new__`
@@ -1002,5 +1010,5 @@
typeddicts_usage.py:28:17: error[missing-typed-dict-key] Missing required key 'name' in TypedDict `Movie` constructor
typeddicts_usage.py:28:18: error[invalid-key] Invalid key for TypedDict `Movie`: Unknown key "title"
typeddicts_usage.py:40:24: error[invalid-type-form] The special form `typing.TypedDict` is not allowed in type expressions. Did you mean to use a concrete TypedDict or `collections.abc.Mapping[str, object]` instead?
-Found 1004 diagnostics
+Found 1012 diagnostics
WARN A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details. |
typing.NewType as Type::NewTypeInstancetyping.NewType as Type::NewTypeInstance
|
| Lint rule | Added | Removed | Changed |
|---|---|---|---|
invalid-argument-type |
495 | 1 | 3 |
unused-ignore-comment |
0 | 39 | 0 |
unsupported-operator |
5 | 0 | 4 |
invalid-assignment |
4 | 1 | 0 |
invalid-return-type |
2 | 1 | 2 |
type-assertion-failure |
2 | 0 | 2 |
invalid-newtype |
2 | 0 | 0 |
no-matching-overload |
2 | 0 | 0 |
call-non-callable |
0 | 0 | 1 |
non-subscriptable |
1 | 0 | 0 |
unresolved-attribute |
0 | 0 | 1 |
| Total | 513 | 42 | 13 |
This comment was marked as resolved.
This comment was marked as resolved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I just looked at the // REVIEWER comments for now, since it's still in draft, but happy to look at the other bits if you'd like me to!)
611cbb3 to
893a2a5
Compare
|
|
Relevant to the error formatting tweak I just pushed in daeb800, do we like the example in that commit message: from typing import NewType
from nonexistent_module import Foo
Bar = NewType("Bar", Foo)That's a result of the check that says a |
Nice, these changes all look like true positives -- places where we should have been emitting diagnostics, but we previously weren't! |
I think it would be good to suppress the diagnostic here if the second argument has a dynamic type. The user will get a diagnostic from the unresolved import in your example; there's not much use in having cascading diagnostics later on in the module due to the same issue. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's my full review. Overall this looks excellent!! This definitely looks like right overall approach IMO.
My main feedback is that it would be great to have some more tests for various parts of the behaviour that you're implementing here. I left some comments inline about things that look correct, but that it would be great to explicitly test. It might also be worth looking through the typing conformance-suite tests for newtype, and/or mypy's tests for newtype, to see if there are any other things that would be worth testing for explicitly.
| Some(KnownClass::NewType) => { | ||
| Binding::single( | ||
| self, | ||
| Signature::new( | ||
| Parameters::new([ | ||
| Parameter::positional_or_keyword(Name::new_static("name")) | ||
| .with_annotated_type(Type::LiteralString), | ||
| Parameter::positional_or_keyword(Name::new_static("tp")), | ||
| ]), | ||
| // The actual return type is a KnownInstanceType::NewType(_), but each | ||
| // callsite gets a unique payload, including a `Definition` that points | ||
| // to that callsite. See `struct NewType` and `infer_newtype_expression`. | ||
| None, | ||
| ), | ||
| ) | ||
| .into() | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
one issue here is that on Python 3.9, NewType is a function rather than a class. I don't know if this really matters this much TBH, since Python 3.9 is now end-of-life, and supporting both the pre-3.9 version of NewType (where it was a function) and the 3.10-version of NewType (where it's a class) is going to lead to complications for us.
At the very least, it might be good to add an mdtest with python-version = "3.9" so that we document that we don't support NewType on Python <=3.9 currently. But I would certainly be inclined to leave <=3.9 support out of the first PR adding support for NewType.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's your recommendation for this diagnostic? It doesn't look like I have an InferContext at this point to directly emit something. Is there a type that I can return that indirectly forces a diagnostic? (edit: ah you're not asking for a diagnostic)
daeb800 to
7d3cec6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking really good -- a couple more small comments!
c6b8501 to
94cec7a
Compare
| .and_then(|cls| cls.known(self.db())); | ||
| if let Some(KnownClass::NewType) = known_class { | ||
| self.infer_newtype_assignment_deferred(arguments); | ||
| return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just pushed a big rebase (addressing most of @AlexWaygood's comments, but now I see there are more :-D ), and this part ended up being tricky. It looks like if I don't short-circuit here, I run into a panic about inferring the same expression (the second arg) twice. Please let me know if this looks like the right way to avoid that, and whether it's maybe worthy of a block comment saying why this is sensitive?
astral-sh/ty#742. This PR rewrites and supersedes my previous
NewTypePR, #20126. The high-level changes:Type::NewTypeInstanceinstead of expandingNominalInstanceInner.infer_legacy_typevarinTypeInferenceBuilder::infer_assignment_definition_impl.There are severalI've moved this out of Draft.// REVIEWERS:questions inline, and I expect I've gone about some things the wrong way (particularly cycle detection?), so I'm leaving this in Draft status for now.