Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/named_tuple.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,127 @@ reveal_type(alice5.id) # revealed: int
reveal_type(alice5.name) # revealed: str
```

### Functional syntax with string annotations

String annotations (forward references) are properly evaluated to types:

```py
from typing import NamedTuple

Point = NamedTuple("Point", [("x", "int"), ("y", "int")])
p = Point(1, 2)

reveal_type(p.x) # revealed: int
reveal_type(p.y) # revealed: int
```

Recursive references in functional syntax are supported:

```py
from typing import NamedTuple

Node = NamedTuple("Node", [("value", int), ("next", "Node | None")])
n = Node(1, None)

reveal_type(n.value) # revealed: int
reveal_type(n.next) # revealed: Node | None
```

### Functional syntax with tuple literal

Using a tuple literal for fields instead of a list:

```py
from typing import NamedTuple

Point = NamedTuple("Point", (("x", int), ("y", int)))
p = Point(1, 2)

reveal_type(p.x) # revealed: int
reveal_type(p.y) # revealed: int
```

### Functional syntax with variable fields and string annotations

String annotations in variable fields don't currently resolve (this is a known limitation). The
types are extracted from the inferred tuple type, but string literals don't get evaluated as type
expressions:

```py
from typing import NamedTuple

fields = [("value", "int"), ("label", "str")]
Item = NamedTuple("Item", fields)
item = Item(42, "test")

# TODO: These should resolve to `int` and `str`, but string annotations in variable fields
# aren't currently evaluated.
reveal_type(item.value) # revealed: Any
reveal_type(item.label) # revealed: Any
```

When using non-string types in variable fields, they work correctly when using a tuple literal:

```py
from typing import NamedTuple

tree_fields = (("value", int), ("left", int | None), ("right", int | None))
Tree = NamedTuple("Tree", tree_fields)
t = Tree(1, None, None)

reveal_type(t.value) # revealed: int
reveal_type(t.left) # revealed: int | None
reveal_type(t.right) # revealed: int | None
```

### Functional syntax as base class (dangling call)

When NamedTuple is used directly as a base class without being assigned to a variable first, it's a
"dangling call". The types are still properly inferred:

```py
from typing import NamedTuple

class Point(NamedTuple("Point", [("x", int), ("y", int)])):
def magnitude(self) -> float:
return (self.x**2 + self.y**2) ** 0.5

p = Point(3, 4)
reveal_type(p.x) # revealed: int
reveal_type(p.y) # revealed: int
reveal_type(p.magnitude()) # revealed: int | float
```

String annotations in dangling calls work correctly for forward references to classes defined in the
same scope. This allows recursive types:

```py
from typing import NamedTuple

class Node(NamedTuple("Node", [("value", int), ("next", "Node | None")])):
pass

n = Node(1, None)
reveal_type(n.value) # revealed: int
reveal_type(n.next) # revealed: Node | None
```

Note that the string annotation must reference a name that exists in scope. References to the
internal NamedTuple name (if different from the class name) won't work:

```py
from typing import NamedTuple

# The string "X" refers to the internal name, not "BadNode", so it won't resolve:
class BadNode(NamedTuple("X", [("value", int), ("next", "X | None")])):
pass

n = BadNode(1, None)
reveal_type(n.value) # revealed: int
# X is not in scope, so it resolves to Unknown; None is correctly resolved
reveal_type(n.next) # revealed: Unknown | None
```

### Functional syntax with variable name

When the typename is passed via a variable, we can extract it from the inferred literal string type:
Expand Down Expand Up @@ -196,6 +317,25 @@ reveal_type(url[1]) # revealed: int
url[2]
```

### Functional syntax with Final variable field names

When field names are `Final` variables, they resolve to their literal string values:

```py
from typing import Final, NamedTuple

X: Final = "x"
Y: Final = "y"
N = NamedTuple("N", [(X, int), (Y, int)])

reveal_type(N(x=3, y=4).x) # revealed: int
reveal_type(N(x=3, y=4).y) # revealed: int

# error: [invalid-argument-type]
# error: [invalid-argument-type]
N(x="", y="")
```

### Functional syntax with variadic tuple fields

When fields are passed as a variadic tuple (e.g., `tuple[..., *tuple[T, ...]]`), we cannot determine
Expand Down Expand Up @@ -887,6 +1027,8 @@ reveal_type(LegacyProperty[str].value.fget) # revealed: (self, /) -> str
reveal_type(LegacyProperty("height", 3.4).value) # revealed: int | float
```

### Functional syntax with generics

Generic namedtuples can also be defined using the functional syntax with type variables in the field
types. We don't currently support this, but mypy does:

Expand Down
5 changes: 3 additions & 2 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ pub(crate) use self::cyclic::{PairVisitor, TypeTransformer};
pub(crate) use self::diagnostic::register_lints;
pub use self::diagnostic::{TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_REFERENCE};
pub(crate) use self::infer::{
TypeContext, infer_deferred_types, infer_definition_types, infer_expression_type,
infer_expression_types, infer_scope_types, static_expression_truthiness,
DeferredAnchor, TypeContext, infer_deferred_namedtuple_call_types, infer_deferred_types,
infer_definition_types, infer_expression_type, infer_expression_types, infer_scope_types,
static_expression_truthiness,
};
pub use self::signatures::ParameterKind;
pub(crate) use self::signatures::{CallableSignature, Signature};
Expand Down
Loading
Loading