-
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?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,5 @@ | ||||||||||||||||||||||||||||||||||||||||||
| # NewType | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Currently, ty doesn't support `typing.NewType` in type annotations. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ## Valid forms | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ```py | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -12,13 +10,257 @@ X = GenericAlias(type, ()) | |||||||||||||||||||||||||||||||||||||||||
| A = NewType("A", int) | ||||||||||||||||||||||||||||||||||||||||||
| # TODO: typeshed for `typing.GenericAlias` uses `type` for the first argument. `NewType` should be special-cased | ||||||||||||||||||||||||||||||||||||||||||
| # to be compatible with `type` | ||||||||||||||||||||||||||||||||||||||||||
| # error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `type`, found `NewType`" | ||||||||||||||||||||||||||||||||||||||||||
| # error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `type`, found `<NewType pseudo-class 'A'>`" | ||||||||||||||||||||||||||||||||||||||||||
| B = GenericAlias(A, ()) | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| def _( | ||||||||||||||||||||||||||||||||||||||||||
| a: A, | ||||||||||||||||||||||||||||||||||||||||||
| b: B, | ||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(a) # revealed: @Todo(Support for `typing.NewType` instances in type expressions) | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(a) # revealed: A | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(b) # revealed: @Todo(Support for `typing.GenericAlias` instances in type expressions) | ||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ## Subtyping | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| The basic purpose of `NewType` is that it acts like a subtype of its base, but not the exact same | ||||||||||||||||||||||||||||||||||||||||||
| type (i.e. not an alias). | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ```py | ||||||||||||||||||||||||||||||||||||||||||
| from typing_extensions import NewType | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Foo = NewType("Foo", int) | ||||||||||||||||||||||||||||||||||||||||||
| Bar = NewType("Bar", Foo) | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+30
to
+33
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we could add some more assertions here that directly check the subtype relationship:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Foo(42) | ||||||||||||||||||||||||||||||||||||||||||
| Foo(Foo(42)) # allowed: `Foo` is a subtype of `int`. | ||||||||||||||||||||||||||||||||||||||||||
| Foo(Bar(Foo(42))) # allowed: `Bar` is a subtype of `int`. | ||||||||||||||||||||||||||||||||||||||||||
| Foo(True) # allowed: `bool` is a subtype of `int`. | ||||||||||||||||||||||||||||||||||||||||||
| Foo("forty-two") # error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `Literal["forty-two"]`" | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| def f(_: int): ... | ||||||||||||||||||||||||||||||||||||||||||
| def g(_: Foo): ... | ||||||||||||||||||||||||||||||||||||||||||
| def h(_: Bar): ... | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| f(42) | ||||||||||||||||||||||||||||||||||||||||||
| f(Foo(42)) | ||||||||||||||||||||||||||||||||||||||||||
| f(Bar(Foo(42))) | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| g(42) # error: [invalid-argument-type] "Argument to function `g` is incorrect: Expected `Foo`, found `Literal[42]`" | ||||||||||||||||||||||||||||||||||||||||||
| g(Foo(42)) | ||||||||||||||||||||||||||||||||||||||||||
| g(Bar(Foo(42))) | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| h(42) # error: [invalid-argument-type] "Argument to function `h` is incorrect: Expected `Bar`, found `Literal[42]`" | ||||||||||||||||||||||||||||||||||||||||||
| h(Foo(42)) # error: [invalid-argument-type] "Argument to function `h` is incorrect: Expected `Bar`, found `Foo`" | ||||||||||||||||||||||||||||||||||||||||||
| h(Bar(Foo(42))) | ||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ## Member and method lookup work | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ```py | ||||||||||||||||||||||||||||||||||||||||||
| from typing_extensions import NewType | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| class Foo: | ||||||||||||||||||||||||||||||||||||||||||
| foo_member: str = "hello" | ||||||||||||||||||||||||||||||||||||||||||
| def foo_method(self) -> int: | ||||||||||||||||||||||||||||||||||||||||||
| return 42 | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Bar = NewType("Bar", Foo) | ||||||||||||||||||||||||||||||||||||||||||
| Baz = NewType("Baz", Bar) | ||||||||||||||||||||||||||||||||||||||||||
| baz = Baz(Bar(Foo())) | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(baz.foo_member) # revealed: str | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(baz.foo_method()) # revealed: int | ||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ## `NewType` wrapper functions are `Callable` | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ```py | ||||||||||||||||||||||||||||||||||||||||||
| from collections.abc import Callable | ||||||||||||||||||||||||||||||||||||||||||
| from typing_extensions import NewType | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Foo = NewType("Foo", int) | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+78
to
+81
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. again here, it could be worth adding a direct assertion for the property we're trying to test (the callable supertype of the NewType pseudo-class) as well as the existing assertions you have for the impact this has in user code:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| def f(_: Callable[[int], Foo]): ... | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| f(Foo) | ||||||||||||||||||||||||||||||||||||||||||
| map(Foo, [1, 2, 3]) | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| def g(_: Callable[[str], Foo]): ... | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| g(Foo) # error: [invalid-argument-type] | ||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ## `NewType` instances are `Callable` if the base type is | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ```py | ||||||||||||||||||||||||||||||||||||||||||
| from typing import NewType, Callable, Any | ||||||||||||||||||||||||||||||||||||||||||
| from ty_extensions import CallableTypeOf | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| N = NewType("N", int) | ||||||||||||||||||||||||||||||||||||||||||
| i = N(42) | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| y: Callable[..., Any] = i # error: [invalid-assignment] "Object of type `N` is not assignable to `(...) -> Any`" | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| # error: [invalid-type-form] "Expected the first argument to `ty_extensions.CallableTypeOf` to be a callable object, but got an object of type `N`" | ||||||||||||||||||||||||||||||||||||||||||
| def f(x: CallableTypeOf[i]): | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(x) # revealed: Unknown | ||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
AlexWaygood marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ## The name must be a string literal | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ```py | ||||||||||||||||||||||||||||||||||||||||||
| from typing_extensions import NewType | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| def _(name: str) -> None: | ||||||||||||||||||||||||||||||||||||||||||
| _ = NewType(name, int) # error: [invalid-newtype] "The first argument to `NewType` must be a string literal" | ||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| However, the literal doesn't necessarily need to be inline, as long as we infer it: | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ```py | ||||||||||||||||||||||||||||||||||||||||||
| name = "Foo" | ||||||||||||||||||||||||||||||||||||||||||
| Foo = NewType(name, int) # allowed | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+133
to
+134
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe demonstrate here that we infer the name just fine?
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ## The second argument must be a class type or another newtype | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Other typing constructs like `Union` are not allowed. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ```py | ||||||||||||||||||||||||||||||||||||||||||
| from typing_extensions import NewType | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| # error: [invalid-newtype] "invalid base for `typing.NewType`" | ||||||||||||||||||||||||||||||||||||||||||
| Foo = NewType("Foo", int | str) | ||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| We don't emit the "invalid base" diagnostic for `Unknown`, because that typically results from other | ||||||||||||||||||||||||||||||||||||||||||
| errors that already have a diagnostic, and there's no need to pile on. For example: | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ```py | ||||||||||||||||||||||||||||||||||||||||||
| # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression" | ||||||||||||||||||||||||||||||||||||||||||
| Foo = NewType("Foo", 42) | ||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+151
to
+154
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had to read this twice because the prose describes how we aren't going to emit a diagnostic but then the example shows us emitting a diagnostic 😆
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ## Newtypes can be cyclic in various ways | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Cyclic newtypes are kind of silly, but it's possible for the user to express them, and it's | ||||||||||||||||||||||||||||||||||||||||||
| important that we don't go into infinite recursive loops and crash with a stack overflow. In fact, | ||||||||||||||||||||||||||||||||||||||||||
| this is *why* base type evaluation is deferred; otherwise Salsa itself would crash. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ```py | ||||||||||||||||||||||||||||||||||||||||||
| from typing_extensions import NewType, reveal_type, cast | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| # Define a directly cyclic newtype. | ||||||||||||||||||||||||||||||||||||||||||
| A = NewType("A", "A") | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(A) # revealed: <NewType pseudo-class 'A'> | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| # Typechecking still works. We can't construct an `A` "honestly", but we can `cast` into one. | ||||||||||||||||||||||||||||||||||||||||||
| a: A | ||||||||||||||||||||||||||||||||||||||||||
| a = 42 # error: [invalid-assignment] "Object of type `Literal[42]` is not assignable to `A`" | ||||||||||||||||||||||||||||||||||||||||||
| a = A(42) # error: [invalid-argument-type] "Argument is incorrect: Expected `A`, found `Literal[42]`" | ||||||||||||||||||||||||||||||||||||||||||
| a = cast(A, 42) | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(a) # revealed: A | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| # A newtype cycle might involve more than one step. | ||||||||||||||||||||||||||||||||||||||||||
| B = NewType("B", "C") | ||||||||||||||||||||||||||||||||||||||||||
| C = NewType("C", "B") | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(B) # revealed: <NewType pseudo-class 'B'> | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(C) # revealed: <NewType pseudo-class 'C'> | ||||||||||||||||||||||||||||||||||||||||||
| b: B = cast(B, 42) | ||||||||||||||||||||||||||||||||||||||||||
| c: C = C(b) | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(b) # revealed: B | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(c) # revealed: C | ||||||||||||||||||||||||||||||||||||||||||
| # Cyclic types behave in surprising ways. These assignments are legal, even though B and C aren't | ||||||||||||||||||||||||||||||||||||||||||
| # the same type, because each of them is a subtype of the other. | ||||||||||||||||||||||||||||||||||||||||||
| b = c | ||||||||||||||||||||||||||||||||||||||||||
| c = b | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| # Another newtype could inherit from a cyclic one. | ||||||||||||||||||||||||||||||||||||||||||
| D = NewType("D", C) | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(D) # revealed: <NewType pseudo-class 'D'> | ||||||||||||||||||||||||||||||||||||||||||
| d: D | ||||||||||||||||||||||||||||||||||||||||||
| d = D(42) # error: [invalid-argument-type] "Argument is incorrect: Expected `C`, found `Literal[42]`" | ||||||||||||||||||||||||||||||||||||||||||
| d = D(c) | ||||||||||||||||||||||||||||||||||||||||||
| d = D(b) # Allowed, the same surprise as above. B and C are subtypes of each other. | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(d) # revealed: D | ||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Normal classes can't inherit from newtypes, but generic classes can be parametrized with them, so we | ||||||||||||||||||||||||||||||||||||||||||
| also need to detect "ordinary" type cycles that happen to involve a newtype. | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ```py | ||||||||||||||||||||||||||||||||||||||||||
| E = NewType("E", list["E"]) | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(E) # revealed: <NewType pseudo-class 'E'> | ||||||||||||||||||||||||||||||||||||||||||
| e: E = E([]) | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(e) # revealed: E | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+200
to
+207
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could add some more stress tests here to show that we really do understand the recursive nature of the newtype (which is really cool!)
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ## `NewType` wrapping preserves singleton-ness and single-valued-ness | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ```py | ||||||||||||||||||||||||||||||||||||||||||
| from typing_extensions import NewType | ||||||||||||||||||||||||||||||||||||||||||
| from ty_extensions import is_singleton, is_single_valued, static_assert | ||||||||||||||||||||||||||||||||||||||||||
| from types import EllipsisType | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| A = NewType("A", EllipsisType) | ||||||||||||||||||||||||||||||||||||||||||
| static_assert(is_singleton(A)) | ||||||||||||||||||||||||||||||||||||||||||
| static_assert(is_single_valued(A)) | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+217
to
+219
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and these also reveal the correct result, which is pretty cool!
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| B = NewType("B", int) | ||||||||||||||||||||||||||||||||||||||||||
| static_assert(not is_singleton(B)) | ||||||||||||||||||||||||||||||||||||||||||
| static_assert(not is_single_valued(B)) | ||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ## `NewType` tuples can be iterated/unpacked | ||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ```py | ||||||||||||||||||||||||||||||||||||||||||
| from typing import NewType | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| N = NewType("N", tuple[int, str]) | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| a, b = N((1, "foo")) | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| reveal_type(a) # revealed: int | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(b) # revealed: str | ||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ## `isinstance` of a `NewType` instance and its base class is inferred `True` | ||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ```py | ||||||||||||||||||||||||||||||||||||||||||
| from typing import NewType | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| N = NewType("N", int) | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| def f(x: N): | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(isinstance(x, int)) # revealed: Literal[True] | ||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| However, a `NewType` isn't a real class, so it isn't a valid second argument to `isinstance`: | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ```py | ||||||||||||||||||||||||||||||||||||||||||
| def f(x: N): | ||||||||||||||||||||||||||||||||||||||||||
| # error: [invalid-argument-type] "Argument to function `isinstance` is incorrect" | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(isinstance(x, N)) # revealed: bool | ||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Because of that, we don't generate any narrowing constraints for it: | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ```py | ||||||||||||||||||||||||||||||||||||||||||
| def f(x: N | str): | ||||||||||||||||||||||||||||||||||||||||||
| if isinstance(x, N): # error: [invalid-argument-type] | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(x) # revealed: N | str | ||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||
| reveal_type(x) # revealed: N | str | ||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ## Trying to subclass a `NewType` produces an error matching CPython | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| <!-- snapshot-diagnostics --> | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| ```py | ||||||||||||||||||||||||||||||||||||||||||
| from typing import NewType | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| X = NewType("X", int) | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| class Foo(X): ... # error: [invalid-base] | ||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| --- | ||
| source: crates/ty_test/src/lib.rs | ||
| expression: snapshot | ||
| --- | ||
| --- | ||
| mdtest name: new_types.md - NewType - Trying to subclass a `NewType` produces an error matching CPython | ||
| mdtest path: crates/ty_python_semantic/resources/mdtest/annotations/new_types.md | ||
| --- | ||
|
|
||
| # Python source files | ||
|
|
||
| ## mdtest_snippet.py | ||
|
|
||
| ``` | ||
| 1 | from typing import NewType | ||
| 2 | | ||
| 3 | X = NewType("X", int) | ||
| 4 | | ||
| 5 | class Foo(X): ... # error: [invalid-base] | ||
| ``` | ||
|
|
||
| # Diagnostics | ||
|
|
||
| ``` | ||
| error[invalid-base]: Cannot subclass an instance of NewType | ||
| --> src/mdtest_snippet.py:5:11 | ||
| | | ||
| 3 | X = NewType("X", int) | ||
| 4 | | ||
| 5 | class Foo(X): ... # error: [invalid-base] | ||
| | ^ | ||
| | | ||
| info: Perhaps you were looking for: `Foo = NewType('Foo', X)` | ||
| info: Definition of class `Foo` will raise `TypeError` at runtime | ||
| info: rule `invalid-base` is enabled by default | ||
|
|
||
| ``` |
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 pathological thing I wondered while reviewing this PR was "What happens if you have a newtype of an enum?" I think our behaviour makes more sense than mypy/pyright here, so it might be worth adding a test for it. (I think it's correct not to narrow the type of
xin the firstmatchcase toLiteral[Foo.X], for example, becauseLiteral[Foo.X]is actually disjoint fromN: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 think we're also still missing a test that demonstrates our
NewTypebehaviour on Python 3.9, too (#21157 (comment))It's fine to leave support for 3.9
NewTypeout of this PR, I think (and possibly never add it, since Python 3.9 is end-of-life). But I'd love a test that just demonstrates what our behaviour is when you usetyping.NewTypeon Python 3.9There 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 thing the typing conformance suite points out is that we need to emit an error if you try to create a generic
NewType-- e.g. we should emit an error on this snippet, but that's not currently implemented in this PR:For this as well, I think it's fine to leave it out of this PR, but it would be great to add a test with a TODO comment saying that we should try to emit a diagnostic on this in the future
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.
it would also be nice to add a test somewhere that demonstrates that we infer member access on the pseudo-class itself correctly, e.g.