Skip to content

Commit 404976e

Browse files
committed
implement typing.NewType by adding Type::NewTypeInstance
1 parent 4b026c2 commit 404976e

File tree

21 files changed

+914
-108
lines changed

21 files changed

+914
-108
lines changed

crates/ty/docs/rules.md

Lines changed: 96 additions & 66 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ty_ide/src/completion.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ impl<'db> Completion<'db> {
127127
Type::NominalInstance(_)
128128
| Type::PropertyInstance(_)
129129
| Type::BoundSuper(_)
130-
| Type::TypedDict(_) => CompletionKind::Struct,
130+
| Type::TypedDict(_)
131+
| Type::NewTypeInstance(_) => CompletionKind::Struct,
131132
Type::IntLiteral(_)
132133
| Type::BooleanLiteral(_)
133134
| Type::TypeIs(_)

crates/ty_ide/src/goto.rs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -209,16 +209,11 @@ impl<'db> DefinitionsOrTargets<'db> {
209209
ty_python_semantic::types::TypeDefinition::Module(module) => {
210210
ResolvedDefinition::Module(module.file(db)?)
211211
}
212-
ty_python_semantic::types::TypeDefinition::Class(definition) => {
213-
ResolvedDefinition::Definition(definition)
214-
}
215-
ty_python_semantic::types::TypeDefinition::Function(definition) => {
216-
ResolvedDefinition::Definition(definition)
217-
}
218-
ty_python_semantic::types::TypeDefinition::TypeVar(definition) => {
219-
ResolvedDefinition::Definition(definition)
220-
}
221-
ty_python_semantic::types::TypeDefinition::TypeAlias(definition) => {
212+
ty_python_semantic::types::TypeDefinition::Class(definition)
213+
| ty_python_semantic::types::TypeDefinition::Function(definition)
214+
| ty_python_semantic::types::TypeDefinition::TypeVar(definition)
215+
| ty_python_semantic::types::TypeDefinition::TypeAlias(definition)
216+
| ty_python_semantic::types::TypeDefinition::NewType(definition) => {
222217
ResolvedDefinition::Definition(definition)
223218
}
224219
};
Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# NewType
22

3-
Currently, ty doesn't support `typing.NewType` in type annotations.
4-
53
## Valid forms
64

75
```py
@@ -12,13 +10,133 @@ X = GenericAlias(type, ())
1210
A = NewType("A", int)
1311
# TODO: typeshed for `typing.GenericAlias` uses `type` for the first argument. `NewType` should be special-cased
1412
# to be compatible with `type`
15-
# error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `type`, found `NewType`"
13+
# error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `type`, found `<NewType pseudo-class 'A'>`"
1614
B = GenericAlias(A, ())
1715

1816
def _(
1917
a: A,
2018
b: B,
2119
):
22-
reveal_type(a) # revealed: @Todo(Support for `typing.NewType` instances in type expressions)
20+
reveal_type(a) # revealed: A
2321
reveal_type(b) # revealed: @Todo(Support for `typing.GenericAlias` instances in type expressions)
2422
```
23+
24+
## Subtyping
25+
26+
The basic purpose of `NewType` is that it acts like a subtype of its base, but not the exact same
27+
type (i.e. not an alias).
28+
29+
```py
30+
from typing_extensions import NewType
31+
32+
Foo = NewType("Foo", int)
33+
Bar = NewType("Bar", Foo)
34+
35+
Foo(42)
36+
Foo(Foo(42)) # allowed: `Foo` is a subtype of `int`.
37+
Foo(Bar(Foo(42))) # allowed: `Bar` is a subtype of `int`.
38+
Foo(True) # allowed: `bool` is a subtype of `int`.
39+
Foo("forty-two") # error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `Literal["forty-two"]`"
40+
41+
def f(_: int): ...
42+
def g(_: Foo): ...
43+
def h(_: Bar): ...
44+
45+
f(42)
46+
f(Foo(42))
47+
f(Bar(Foo(42)))
48+
49+
g(42) # error: [invalid-argument-type] "Argument to function `g` is incorrect: Expected `Foo`, found `Literal[42]`"
50+
g(Foo(42))
51+
g(Bar(Foo(42)))
52+
53+
h(42) # error: [invalid-argument-type] "Argument to function `h` is incorrect: Expected `Bar`, found `Literal[42]`"
54+
h(Foo(42)) # error: [invalid-argument-type] "Argument to function `h` is incorrect: Expected `Bar`, found `Foo`"
55+
h(Bar(Foo(42)))
56+
```
57+
58+
## The name must be a string literal
59+
60+
```py
61+
from typing_extensions import NewType
62+
63+
def _(name: str) -> None:
64+
_ = NewType(name, int) # error: [invalid-newtype] "The first argument to `NewType` must be a string literal"
65+
```
66+
67+
However, the literal doesn't necessarily need to be inline, as long as we infer it:
68+
69+
```py
70+
name = "Foo"
71+
Foo = NewType(name, int) # allowed
72+
```
73+
74+
## The second argument must be a class type or another newtype
75+
76+
Other typing constructs like `Union` are not allowed.
77+
78+
```py
79+
from typing_extensions import NewType
80+
81+
# error: [invalid-newtype] "invalid base for `typing.NewType`"
82+
Foo = NewType("Foo", int | str)
83+
# error: [invalid-newtype] "invalid base for `typing.NewType`"
84+
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
85+
Foo = NewType("Foo", 42)
86+
```
87+
88+
## Newtypes can be cyclic in various ways
89+
90+
Cyclic newtypes are kind of silly, but it's possible for the user to express them, and it's
91+
important that we don't go into infinite recursive loops and crash with a stack overflow. In fact,
92+
this is *why* base type evaluation is deferred; otherwise Salsa itself would crash.
93+
94+
```py
95+
from typing_extensions import NewType, reveal_type, cast
96+
97+
# Define a directly cyclic newtype.
98+
A = NewType("A", "A")
99+
reveal_type(A) # revealed: <NewType pseudo-class 'A'>
100+
101+
# Typechecking still works. We can't construct an `A` "honestly", but we can `cast` into one.
102+
a: A
103+
a = 42 # error: [invalid-assignment] "Object of type `Literal[42]` is not assignable to `A`"
104+
a = A(42) # error: [invalid-argument-type] "Argument is incorrect: Expected `A`, found `Literal[42]`"
105+
a = cast(A, 42)
106+
reveal_type(a) # revealed: A
107+
108+
# A newtype cycle might involve more than one step.
109+
B = NewType("B", "C")
110+
C = NewType("C", "B")
111+
reveal_type(B) # revealed: <NewType pseudo-class 'B'>
112+
reveal_type(C) # revealed: <NewType pseudo-class 'C'>
113+
b: B = cast(B, 42)
114+
c: C = C(b)
115+
reveal_type(b) # revealed: B
116+
reveal_type(c) # revealed: C
117+
# Cyclic types behave in surprising ways. These assignments are legal, even though B and C aren't
118+
# the same type, because each of them is a subtype of the other.
119+
b = c
120+
c = b
121+
122+
# Another newtype could inherit from a cyclic one.
123+
D = NewType("D", C)
124+
reveal_type(D) # revealed: <NewType pseudo-class 'D'>
125+
d: D
126+
d = D(42) # error: [invalid-argument-type] "Argument is incorrect: Expected `C`, found `Literal[42]`"
127+
d = D(c)
128+
d = D(b) # Allowed, the same surprise as above. B and C are subtypes of each other.
129+
reveal_type(d) # revealed: D
130+
```
131+
132+
Normal classes can't inherit from newtypes, but generic classes can be parametrized with them, so we
133+
also need to detect "ordinary" type cycles that happen to involve a newtype. (This turns out to be
134+
tricky in the implementation, see comments around `NewType::normalized_impl` and
135+
`NewType::apply_type_mapping_impl`.)
136+
137+
```py
138+
E = NewType("E", list["E"])
139+
reveal_type(E) # revealed: <NewType pseudo-class 'E'>
140+
e: E = E([])
141+
reveal_type(e) # revealed: E
142+
```

0 commit comments

Comments
 (0)