Skip to content

Commit

Permalink
Support typing.NoReturn and typing.Never (#14559)
Browse files Browse the repository at this point in the history
Fix #14558 
## Summary

- Add `typing.NoReturn` and `typing.Never` to known instances and infer
them as `Type::Never`
- Add `is_assignable_to` cases for `Type::Never`

I skipped emitting diagnostic for when a function is annotated as
`NoReturn` but it actually returns.

## Test Plan

Added tests from

https://github.com/python/typing/blob/main/conformance/tests/specialtypes_never.py
except from generics and checking if the return value of the function
and the annotations match.

---------

Co-authored-by: Alex Waygood <[email protected]>
Co-authored-by: Carl Meyer <[email protected]>
  • Loading branch information
3 people authored Nov 25, 2024
1 parent f98eebd commit 557d583
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# NoReturn & Never

`NoReturn` is used to annotate the return type for functions that never return. `Never` is the
bottom type, representing the empty set of Python objects. These two annotations can be used
interchangeably.

## Function Return Type Annotation

```py
from typing import NoReturn

def stop() -> NoReturn:
raise RuntimeError("no way")

# revealed: Never
reveal_type(stop())
```

## Assignment

```py
from typing import NoReturn, Never, Any

# error: [invalid-type-parameter] "Type `typing.Never` expected no type parameter"
x: Never[int]
a1: NoReturn
# TODO: Test `Never` is only available in python >= 3.11
a2: Never
b1: Any
b2: int

def f():
# revealed: Never
reveal_type(a1)
# revealed: Never
reveal_type(a2)

# Never is assignable to all types.
v1: int = a1
v2: str = a1
# Other types are not assignable to Never except for Never (and Any).
v3: Never = b1
v4: Never = a2
v5: Any = b2
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Never`"
v6: Never = 1
```

## Typing Extensions

```py
from typing_extensions import NoReturn, Never

x: NoReturn
y: Never

def f():
# revealed: Never
reveal_type(x)
# revealed: Never
reveal_type(y)
```
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ reveal_type(1 if 0 else 2) # revealed: Literal[2]

(issue #14588)

The test inside an if expression should not affect code outside of the block.
The test inside an if expression should not affect code outside of the expression.

```py
def bool_instance() -> bool:
Expand Down
27 changes: 22 additions & 5 deletions crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1553,6 +1553,9 @@ impl<'db> Type<'db> {
// TODO map this to a new `Type::TypeVar` variant
Type::KnownInstance(KnownInstanceType::TypeVar(_)) => *self,
Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)) => alias.value_ty(db),
Type::KnownInstance(KnownInstanceType::Never | KnownInstanceType::NoReturn) => {
Type::Never
}
_ => todo_type!(),
}
}
Expand Down Expand Up @@ -1889,6 +1892,10 @@ pub enum KnownInstanceType<'db> {
Optional,
/// The symbol `typing.Union` (which can also be found as `typing_extensions.Union`)
Union,
/// The symbol `typing.NoReturn` (which can also be found as `typing_extensions.NoReturn`)
NoReturn,
/// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`)
Never,
/// A single instance of `typing.TypeVar`
TypeVar(TypeVarInstance<'db>),
/// A single instance of `typing.TypeAliasType` (PEP 695 type alias)
Expand All @@ -1899,11 +1906,13 @@ pub enum KnownInstanceType<'db> {
impl<'db> KnownInstanceType<'db> {
pub const fn as_str(self) -> &'static str {
match self {
KnownInstanceType::Literal => "Literal",
KnownInstanceType::Optional => "Optional",
KnownInstanceType::Union => "Union",
KnownInstanceType::TypeVar(_) => "TypeVar",
KnownInstanceType::TypeAliasType(_) => "TypeAliasType",
Self::Literal => "Literal",
Self::Optional => "Optional",
Self::Union => "Union",
Self::TypeVar(_) => "TypeVar",
Self::NoReturn => "NoReturn",
Self::Never => "Never",
Self::TypeAliasType(_) => "TypeAliasType",
}
}

Expand All @@ -1914,6 +1923,8 @@ impl<'db> KnownInstanceType<'db> {
| Self::Optional
| Self::TypeVar(_)
| Self::Union
| Self::NoReturn
| Self::Never
| Self::TypeAliasType(_) => Truthiness::AlwaysTrue,
}
}
Expand All @@ -1924,6 +1935,8 @@ impl<'db> KnownInstanceType<'db> {
Self::Literal => "typing.Literal",
Self::Optional => "typing.Optional",
Self::Union => "typing.Union",
Self::NoReturn => "typing.NoReturn",
Self::Never => "typing.Never",
Self::TypeVar(typevar) => typevar.name(db),
Self::TypeAliasType(_) => "typing.TypeAliasType",
}
Expand All @@ -1935,6 +1948,8 @@ impl<'db> KnownInstanceType<'db> {
Self::Literal => KnownClass::SpecialForm,
Self::Optional => KnownClass::SpecialForm,
Self::Union => KnownClass::SpecialForm,
Self::NoReturn => KnownClass::SpecialForm,
Self::Never => KnownClass::SpecialForm,
Self::TypeVar(_) => KnownClass::TypeVar,
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
}
Expand All @@ -1957,6 +1972,8 @@ impl<'db> KnownInstanceType<'db> {
("typing" | "typing_extensions", "Literal") => Some(Self::Literal),
("typing" | "typing_extensions", "Optional") => Some(Self::Optional),
("typing" | "typing_extensions", "Union") => Some(Self::Union),
("typing" | "typing_extensions", "NoReturn") => Some(Self::NoReturn),
("typing" | "typing_extensions", "Never") => Some(Self::Never),
_ => None,
}
}
Expand Down
16 changes: 14 additions & 2 deletions crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4575,7 +4575,7 @@ impl<'db> TypeInferenceBuilder<'db> {

match value_ty {
Type::KnownInstance(known_instance) => {
self.infer_parameterized_known_instance_type_expression(known_instance, slice)
self.infer_parameterized_known_instance_type_expression(subscript, known_instance)
}
_ => {
self.infer_type_expression(slice);
Expand All @@ -4586,9 +4586,10 @@ impl<'db> TypeInferenceBuilder<'db> {

fn infer_parameterized_known_instance_type_expression(
&mut self,
subscript: &ast::ExprSubscript,
known_instance: KnownInstanceType,
parameters: &ast::Expr,
) -> Type<'db> {
let parameters = &*subscript.slice;
match known_instance {
KnownInstanceType::Literal => match self.infer_literal_parameter_type(parameters) {
Ok(ty) => ty,
Expand Down Expand Up @@ -4629,6 +4630,17 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_type_expression(parameters);
todo_type!("generic type alias")
}
KnownInstanceType::NoReturn | KnownInstanceType::Never => {
self.diagnostics.add(
subscript.into(),
"invalid-type-parameter",
format_args!(
"Type `{}` expected no type parameter",
known_instance.repr(self.db)
),
);
Type::Unknown
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/red_knot_python_semantic/src/types/mro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,8 @@ impl<'db> ClassBase<'db> {
| KnownInstanceType::TypeAliasType(_)
| KnownInstanceType::Literal
| KnownInstanceType::Union
| KnownInstanceType::NoReturn
| KnownInstanceType::Never
| KnownInstanceType::Optional => None,
},
}
Expand Down

0 comments on commit 557d583

Please sign in to comment.