Skip to content
Draft
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
6 changes: 6 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/bidirectional.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ def f[T](x: T, cond: bool) -> T | list[T]:
return x if cond else [x]

l5: int | list[int] = f(1, True)

a: list[int] = [1, 2, *(3, 4, 5)]
reveal_type(a) # revealed: list[int]

b: list[list[int]] = [[1], [2], *([3], [4])]
reveal_type(b) # revealed: list[list[int]]
```

`typed_dict.py`:
Expand Down
19 changes: 19 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,22 @@ B = bytes

reveal_mro(C) # revealed: (<class 'C'>, <class 'int'>, <class 'G[bytes]'>, typing.Generic, <class 'object'>)
```

## Starred bases

These are currently not supported, but ideally we would support them in some limited situations.

```py
from ty_extensions import reveal_mro

class A: ...
class B: ...
class C: ...

bases = (A, B, C)

class Foo(*bases): ...

# revealed: (<class 'Foo'>, @Todo(Starred expressions in class bases), <class 'object'>)
reveal_mro(Foo)
```
14 changes: 4 additions & 10 deletions crates/ty_python_semantic/resources/mdtest/expression/len.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,11 @@ reveal_type(len(())) # revealed: Literal[0]
reveal_type(len((1,))) # revealed: Literal[1]
reveal_type(len((1, 2))) # revealed: Literal[2]
reveal_type(len(tuple())) # revealed: Literal[0]

# TODO: Handle star unpacks; Should be: Literal[0]
reveal_type(len((*[],))) # revealed: Literal[1]
reveal_type(len((*[],))) # revealed: Literal[0]

# fmt: off

# TODO: Handle star unpacks; Should be: Literal[1]
reveal_type(len( # revealed: Literal[2]
reveal_type(len( # revealed: Literal[1]
(
*[],
1,
Expand All @@ -58,11 +55,8 @@ reveal_type(len( # revealed: Literal[2]

# fmt: on

# TODO: Handle star unpacks; Should be: Literal[2]
reveal_type(len((*[], 1, 2))) # revealed: Literal[3]

# TODO: Handle star unpacks; Should be: Literal[0]
reveal_type(len((*[], *{}))) # revealed: Literal[2]
reveal_type(len((*[], 1, 2))) # revealed: Literal[2]
reveal_type(len((*[], *{}))) # revealed: Literal[0]
```

Tuple subclasses:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -531,4 +531,34 @@ x: list[Literal[1, 2, 3]] = list((1, 2, 3))
reveal_type(x) # revealed: list[Literal[1, 2, 3]]
```

## Tuples with starred elements

```py
from typing import Literal, Sequence

x = (1, *range(3), 3)
reveal_type(x) # revealed: tuple[Literal[1], *tuple[int, ...], Literal[3]]

y = 1, 2

reveal_type(("foo", *y)) # revealed: tuple[Literal["foo"], Literal[1], Literal[2]]

aa: tuple[list[int], ...] = ([42], *{[56], [78]}, [100])
reveal_type(aa) # revealed: tuple[list[int], list[int], list[int], list[int]]

bb: tuple[list[Literal[42, 56]], ...] = ([42], *{[56, 42], [42]}, [42, 42, 56])
reveal_type(bb) # revealed: tuple[list[Literal[42, 56]], list[Literal[42, 56]], list[Literal[42, 56]], list[Literal[42, 56]]]

reveal_type((*[],)) # revealed: tuple[()]
reveal_type((42, *[], 56, *[])) # revealed: tuple[Literal[42], Literal[56]]

tup: Sequence[str] = (*{"foo": 42, "bar": 56},)

# TODO: `tuple[str, str]` would be better, given the type annotation
reveal_type(tup) # revealed: tuple[Unknown | str, Unknown | str]

def f(x: list[int]):
reveal_type((42, 56, *x, 97)) # revealed: tuple[Literal[42], Literal[56], *tuple[int, ...], Literal[97]]
```

[not a singleton type]: https://discuss.python.org/t/should-we-specify-in-the-language-reference-that-the-empty-tuple-is-a-singleton/67957
6 changes: 5 additions & 1 deletion crates/ty_python_semantic/src/types/call/bind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2974,7 +2974,11 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
) {
let parameters = self.signature.parameters();
let parameter = &parameters[parameter_index];
if let Some(mut expected_ty) = parameter.annotated_type() {

// TODO: handle starred annotations, e.g. `*args: *Ts` or `*args: *tuple[int, *tuple[str, ...]]`
if let Some(mut expected_ty) = parameter.annotated_type()
&& !parameter.has_starred_annotation()
{
if let Some(specialization) = self.specialization {
argument_type = argument_type.apply_specialization(self.db, specialization);
expected_ty = expected_ty.apply_specialization(self.db, specialization);
Expand Down
29 changes: 19 additions & 10 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use crate::types::{
ManualPEP695TypeAliasType, MaterializationKind, NormalizedVisitor, PropertyInstanceType,
StringLiteralType, TypeAliasType, TypeContext, TypeMapping, TypeRelation, TypedDictParams,
UnionBuilder, VarianceInferable, binding_type, declaration_type, determine_upper_bound,
todo_type,
};
use crate::{
Db, FxIndexMap, FxIndexSet, FxOrderSet, Program,
Expand Down Expand Up @@ -1664,20 +1665,28 @@ impl<'db> ClassLiteral<'db> {
let class_definition =
semantic_index(db, self.file(db)).expect_single_definition(class_stmt);

if self.is_known(db, KnownClass::VersionInfo) {
let tuple_type = TupleType::new(db, &TupleSpec::version_info_spec(db))
.expect("sys.version_info tuple spec should always be a valid tuple");
if class_stmt.bases().iter().any(ast::Expr::is_starred_expr) {
return Box::new([todo_type!("Starred expressions in class bases")]);
}

Box::new([
definition_expression_type(db, class_definition, &class_stmt.bases()[0]),
Type::from(tuple_type.to_class_type(db)),
])
} else {
class_stmt
match self.known(db) {
Some(KnownClass::VersionInfo) => {
let tuple_type = TupleType::new(db, &TupleSpec::version_info_spec(db))
.expect("sys.version_info tuple spec should always be a valid tuple");

Box::new([
definition_expression_type(db, class_definition, &class_stmt.bases()[0]),
Type::from(tuple_type.to_class_type(db)),
])
}
// Special-case `NotImplementedType`: typeshed says that it inherits from `Any`,
// but this causes more problems than it fixes.
Some(KnownClass::NotImplementedType) => Box::new([]),
_ => class_stmt
.bases()
.iter()
.map(|base_node| definition_expression_type(db, class_definition, base_node))
.collect()
.collect(),
}
}

Expand Down
22 changes: 22 additions & 0 deletions crates/ty_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
//! be considered a bug.)

use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast as ast;
use ruff_text_size::Ranged;
use rustc_hash::{FxHashMap, FxHashSet};
use salsa;
Expand Down Expand Up @@ -384,6 +385,27 @@ impl<'db> TypeContext<'db> {
self.annotation
.is_some_and(|ty| ty.is_typealias_special_form())
}

// TODO: we could just always use `Iterable[<expected_element_type>]`
// as the type context once <https://github.com/astral-sh/ty/issues/1576> is fixed.
pub(crate) fn for_starred_expression(
db: &'db dyn Db,
expected_element_type: Type<'db>,
expr: &ast::ExprStarred,
) -> Self {
match &*expr.value {
ast::Expr::List(_) => Self::new(Some(
KnownClass::List.to_specialized_instance(db, [expected_element_type]),
)),
ast::Expr::Set(_) => Self::new(Some(
KnownClass::Set.to_specialized_instance(db, [expected_element_type]),
)),
ast::Expr::Tuple(_) => {
Self::new(Some(Type::homogeneous_tuple(db, expected_element_type)))
}
_ => Self::default(),
}
}
}

/// Returns the statically-known truthiness of a given expression.
Expand Down
Loading
Loading