Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
163 changes: 97 additions & 66 deletions crates/ty/docs/rules.md

Large diffs are not rendered by default.

18 changes: 14 additions & 4 deletions crates/ty_ide/src/goto_type_definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,10 +276,20 @@ mod tests {
"#,
);

// TODO: Goto type definition currently doesn't work for type param specs
// because the inference doesn't support them yet.
// This snapshot should show a single target pointing to `T`
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:2:14
|
2 | type Alias[**P = [int, str]] = Callable[P, int]
| ^
|
info: Source
--> main.py:2:41
|
2 | type Alias[**P = [int, str]] = Callable[P, int]
| ^
|
");
}

#[test]
Expand Down
5 changes: 3 additions & 2 deletions crates/ty_ide/src/hover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1633,11 +1633,12 @@ def ab(a: int, *, c: int):
"#,
);

// TODO: This should be `P@Alias (<variance>)`
assert_snapshot!(test.hover(), @r"
@Todo
typing.ParamSpec
---------------------------------------------
```python
@Todo
typing.ParamSpec
```
---------------------------------------------
info[hover]: Hovered content is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,9 @@ Using a `ParamSpec` in a `Callable` annotation:
from typing_extensions import Callable

def _[**P1](c: Callable[P1, int]):
reveal_type(P1.args) # revealed: @Todo(ParamSpec)
reveal_type(P1.kwargs) # revealed: @Todo(ParamSpec)
# TODO: Should reveal `ParamSpecArgs` and `ParamSpecKwargs`
reveal_type(P1.args) # revealed: Unknown
reveal_type(P1.kwargs) # revealed: Unknown

# TODO: Signature should be (**P1) -> int
reveal_type(c) # revealed: (...) -> int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:

def g() -> TypeGuard[int]: ...
def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co:
reveal_type(args) # revealed: tuple[@Todo(Support for `typing.ParamSpec`), ...]
reveal_type(kwargs) # revealed: dict[str, @Todo(Support for `typing.ParamSpec`)]
# TODO: Should reveal a type representing `P.args` and `P.kwargs`
reveal_type(args) # revealed: tuple[Unknown, ...]
reveal_type(kwargs) # revealed: dict[str, Unknown]
return callback(42, *args, **kwargs)

class Foo:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ reveal_type(generic_context(SingleTypevar))
# revealed: tuple[T@MultipleTypevars, S@MultipleTypevars]
reveal_type(generic_context(MultipleTypevars))

# TODO: support `ParamSpec`/`TypeVarTuple` properly (these should not reveal `None`)
reveal_type(generic_context(SingleParamSpec)) # revealed: None
reveal_type(generic_context(TypeVarAndParamSpec)) # revealed: None
# revealed: tuple[P@SingleParamSpec]
reveal_type(generic_context(SingleParamSpec))
# revealed: tuple[P@TypeVarAndParamSpec, T@TypeVarAndParamSpec]
reveal_type(generic_context(TypeVarAndParamSpec))

# TODO: support `TypeVarTuple` properly (these should not reveal `None`)
reveal_type(generic_context(SingleTypeVarTuple)) # revealed: None
reveal_type(generic_context(TypeVarAndTypeVarTuple)) # revealed: None
```
Expand Down
154 changes: 154 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/paramspec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# `ParamSpec`

## Definition

### Valid

```py
from typing import ParamSpec

P = ParamSpec("P")
reveal_type(type(P)) # revealed: <class 'ParamSpec'>
reveal_type(P) # revealed: typing.ParamSpec
reveal_type(P.__name__) # revealed: Literal["P"]
```

The paramspec name can also be provided as a keyword argument:

```py
from typing import ParamSpec

P = ParamSpec(name="P")
reveal_type(P.__name__) # revealed: Literal["P"]
```

### Must be directly assigned to a variable

```py
from typing import ParamSpec

P = ParamSpec("P")
# error: [invalid-paramspec]
P1: ParamSpec = ParamSpec("P1")

# error: [invalid-paramspec]
tuple_with_typevar = ("foo", ParamSpec("W"))
reveal_type(tuple_with_typevar[1]) # revealed: ParamSpec
```

```py
from typing_extensions import ParamSpec

T = ParamSpec("T")
# error: [invalid-paramspec]
P1: ParamSpec = ParamSpec("P1")

# error: [invalid-paramspec]
tuple_with_typevar = ("foo", ParamSpec("P2"))
reveal_type(tuple_with_typevar[1]) # revealed: ParamSpec
```

### `TypeVar` parameter must match variable name

```py
from typing import ParamSpec

P1 = ParamSpec("P1")

# error: [invalid-paramspec]
P2 = ParamSpec("P3")
```

### Accepts only a single `name` argument

> The runtime should accept bounds and covariant and contravariant arguments in the declaration just
> as typing.TypeVar does, but for now we will defer the standardization of the semantics of those
> options to a later PEP.
```py
from typing import ParamSpec

# error: [invalid-paramspec]
P1 = ParamSpec("P1", bound=int)
# error: [invalid-paramspec]
P2 = ParamSpec("P2", int, str)
# error: [invalid-paramspec]
P3 = ParamSpec("P3", covariant=True)
# error: [invalid-paramspec]
P4 = ParamSpec("P4", contravariant=True)
```

### Defaults

```toml
[environment]
python-version = "3.13"
```

The default value for a `ParamSpec` can be either a list of types, `...`, or another `ParamSpec`.

```py
from typing import ParamSpec

P1 = ParamSpec("P1", default=[int, str])
P2 = ParamSpec("P2", default=...)
P3 = ParamSpec("P3", default=P2)
```

Other values are invalid.

```py
# error: [invalid-paramspec]
P4 = ParamSpec("P4", default=int)
```

### PEP 695

```toml
[environment]
python-version = "3.12"
```

#### Valid

```py
def foo1[**P]() -> None:
reveal_type(P) # revealed: typing.ParamSpec

def foo2[**P = ...]() -> None:
reveal_type(P) # revealed: typing.ParamSpec

def foo2[**P = [int, str]]() -> None: ...
```

#### Invalid

ParamSpec, when defined using the new syntax, does not allow defining bounds or constraints.

This results in a lot of syntax errors mainly because the AST doesn't accept them in this position.
The parser could do a better job in recovering from these errors.

<!-- blacken-docs:off -->

```py
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
def foo[**P: int]() -> None:
# error: [invalid-syntax]
# error: [invalid-syntax]
pass
```

<!-- blacken-docs:on -->

#### Invalid default

```py
# error: [invalid-paramspec]
def foo[**P = int]() -> None:
pass
```
Original file line number Diff line number Diff line change
Expand Up @@ -1171,9 +1171,7 @@ class EggsLegacy(Generic[T, P]): ...
static_assert(not is_assignable_to(Spam, Callable[..., Any]))
static_assert(not is_assignable_to(SpamLegacy, Callable[..., Any]))
static_assert(not is_assignable_to(Eggs, Callable[..., Any]))

# TODO: should pass
static_assert(not is_assignable_to(EggsLegacy, Callable[..., Any])) # error: [static-assert-error]
static_assert(not is_assignable_to(EggsLegacy, Callable[..., Any]))
```

### Classes with `__call__` as attribute
Expand Down
40 changes: 28 additions & 12 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6974,7 +6974,7 @@ impl<'db> Type<'db> {
Type::TypeVar(bound_typevar) => {
if matches!(
bound_typevar.typevar(db).kind(db),
TypeVarKind::Legacy | TypeVarKind::TypingSelf
TypeVarKind::Legacy | TypeVarKind::TypingSelf | TypeVarKind::ParamSpec
) && binding_context.is_none_or(|binding_context| {
bound_typevar.binding_context(db) == BindingContext::Definition(binding_context)
}) {
Expand Down Expand Up @@ -7682,6 +7682,9 @@ impl<'db> KnownInstanceType<'db> {
fn class(self, db: &'db dyn Db) -> KnownClass {
match self {
Self::SubscriptedProtocol(_) | Self::SubscriptedGeneric(_) => KnownClass::SpecialForm,
Self::TypeVar(typevar_instance) if typevar_instance.kind(db).is_paramspec() => {
KnownClass::ParamSpec
}
Self::TypeVar(_) => KnownClass::TypeVar,
Self::TypeAliasType(TypeAliasType::PEP695(alias)) if alias.is_specialized(db) => {
KnownClass::GenericAlias
Expand Down Expand Up @@ -7746,7 +7749,13 @@ impl<'db> KnownInstanceType<'db> {
// This is a legacy `TypeVar` _outside_ of any generic class or function, so we render
// it as an instance of `typing.TypeVar`. Inside of a generic class or function, we'll
// have a `Type::TypeVar(_)`, which is rendered as the typevar's name.
KnownInstanceType::TypeVar(_) => f.write_str("typing.TypeVar"),
KnownInstanceType::TypeVar(typevar_instance) => {
if typevar_instance.kind(self.db).is_paramspec() {
f.write_str("typing.ParamSpec")
} else {
f.write_str("typing.TypeVar")
}
}
KnownInstanceType::Deprecated(_) => f.write_str("warnings.deprecated"),
KnownInstanceType::Field(field) => {
f.write_str("dataclasses.Field")?;
Expand Down Expand Up @@ -7801,9 +7810,6 @@ pub enum DynamicType<'db> {
///
/// This variant should be created with the `todo_type!` macro.
Todo(TodoType),
/// A special Todo-variant for PEP-695 `ParamSpec` types. A temporary variant to detect and special-
/// case the handling of these types in `Callable` annotations.
TodoPEP695ParamSpec,
/// A special Todo-variant for type aliases declared using `typing.TypeAlias`.
/// A temporary variant to detect and special-case the handling of these aliases in autocomplete suggestions.
TodoTypeAlias,
Expand Down Expand Up @@ -7831,13 +7837,6 @@ impl std::fmt::Display for DynamicType<'_> {
// `DynamicType::Todo`'s display should be explicit that is not a valid display of
// any other type
DynamicType::Todo(todo) => write!(f, "@Todo{todo}"),
DynamicType::TodoPEP695ParamSpec => {
if cfg!(debug_assertions) {
f.write_str("@Todo(ParamSpec)")
} else {
f.write_str("@Todo")
}
}
DynamicType::TodoUnpack => {
if cfg!(debug_assertions) {
f.write_str("@Todo(typing.Unpack)")
Expand Down Expand Up @@ -8172,12 +8171,20 @@ pub enum TypeVarKind {
Pep695,
/// `typing.Self`
TypingSelf,
/// `P = ParamSpec("P")`
ParamSpec,
/// `def foo[**P]() -> None: ...`
Pep695ParamSpec,
}

impl TypeVarKind {
const fn is_self(self) -> bool {
matches!(self, Self::TypingSelf)
}

const fn is_paramspec(self) -> bool {
matches!(self, Self::ParamSpec | Self::Pep695ParamSpec)
}
}

/// The identity of a type variable.
Expand Down Expand Up @@ -8530,6 +8537,15 @@ impl<'db> TypeVarInstance<'db> {
let expr = &call_expr.arguments.find_keyword("default")?.value;
Some(definition_expression_type(db, definition, expr))
}
// PEP 695 ParamSpec
DefinitionKind::ParamSpec(paramspec) => {
let paramspec_node = paramspec.node(&module);
Some(definition_expression_type(
db,
definition,
paramspec_node.default.as_ref()?,
))
}
_ => None,
}
}
Expand Down
5 changes: 1 addition & 4 deletions crates/ty_python_semantic/src/types/class_base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,7 @@ impl<'db> ClassBase<'db> {
ClassBase::Dynamic(DynamicType::Any) => "Any",
ClassBase::Dynamic(DynamicType::Unknown) => "Unknown",
ClassBase::Dynamic(
DynamicType::Todo(_)
| DynamicType::TodoPEP695ParamSpec
| DynamicType::TodoTypeAlias
| DynamicType::TodoUnpack,
DynamicType::Todo(_) | DynamicType::TodoTypeAlias | DynamicType::TodoUnpack,
) => "@Todo",
ClassBase::Dynamic(DynamicType::Divergent(_)) => "Divergent",
ClassBase::Protocol => "Protocol",
Expand Down
25 changes: 25 additions & 0 deletions crates/ty_python_semantic/src/types/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_EXCEPTION_CAUGHT);
registry.register_lint(&INVALID_GENERIC_CLASS);
registry.register_lint(&INVALID_LEGACY_TYPE_VARIABLE);
registry.register_lint(&INVALID_PARAMSPEC);
registry.register_lint(&INVALID_TYPE_ALIAS_TYPE);
registry.register_lint(&INVALID_METACLASS);
registry.register_lint(&INVALID_OVERLOAD);
Expand Down Expand Up @@ -880,6 +881,30 @@ declare_lint! {
}
}

declare_lint! {
/// ## What it does
/// Checks for the creation of invalid `ParamSpec`s
///
/// ## Why is this bad?
/// There are several requirements that you must follow when creating a `ParamSpec`.
///
/// ## Examples
/// ```python
/// from typing import ParamSpec
///
/// P1 = ParamSpec("P1") # okay
/// P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assigned to
/// ```
///
/// ## References
/// - [Typing spec: ParamSpec](https://typing.python.org/en/latest/spec/generics.html#paramspec)
pub(crate) static INVALID_PARAMSPEC = {
summary: "detects invalid ParamSpec usage",
status: LintStatus::stable("0.0.1-alpha.1"),
default_level: Level::Error,
}
}

declare_lint! {
/// ## What it does
/// Checks for the creation of invalid `TypeAliasType`s
Expand Down
Loading
Loading