Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[red-knot] Deeper understanding of LiteralString #14649

Merged
merged 6 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
# `LiteralString`

`LiteralString` signifies a strings that is either defined directly within the source code or is
InSyncWithFoo marked this conversation as resolved.
Show resolved Hide resolved
made up of such components.

Parts of the testcases defined here were adapted from [the specification's examples][1].

## Usages

### Valid places
InSyncWithFoo marked this conversation as resolved.
Show resolved Hide resolved

It can be used anywhere a type is accepted:

```py
from typing import (
Annotated,
ClassVar,
Final,
Literal,
LiteralString,
NotRequired,
Protocol,
ReadOnly,
Required,
TypedDict,
TypeAlias,
# TODO: Blocking on `sys.version_info` support.
# See `issubclass.md`, section "Handling of `None`"
TypeAliasType, # error: [possibly-unbound-import]
TypeVar,
)

Old: TypeAlias = LiteralString
type New = LiteralString
Backported = TypeAliasType("Backported", LiteralString)

T1 = TypeVar("T1", bound=LiteralString)
T2 = TypeVar("T2", bound=Old)
T3 = TypeVar("T3", bound=New)
T4 = TypeVar("T4", bound=Backported)

variable_annotation_1: LiteralString
variable_annotation_2: Old
variable_annotation_3: New
variable_annotation_4: Backported

type_argument_1: list[LiteralString]
type_argument_2: dict[Old, New]
type_argument_3: set[Backported]

type TA1[LS: LiteralString] = Old
type TA2[LS: Old] = New
type TA3[LS: New] = Backported
type TA4[LS: Backported] = LiteralString

def my_function(literal_string: LiteralString, *args: Old, **kwargs: New) -> Backported: ...

class Foo:
my_attribute: LiteralString
class_var: ClassVar[Old] = "foo"
final: Final[New] = "bar"
annotated_class_var: Annotated[
ClassVar[Backported], Literal[LiteralString] # Second arguments and later must be ignored.
] = "foobar"

# TODO: Support new-style generic classes
# error: [invalid-base]
class PEP695[L: LiteralString](Protocol):
def f[S: Old](self: L | S | New) -> Backported: ...
# ^^^^^^^^^^^ This is valid, as the class is a protocol.

class GenericParameter(PEP695[LiteralString]): ...

# TODO: Support TypedDict
class TD(TypedDict): # error: [invalid-base]
normal: LiteralString
readonly: ReadOnly[Old]
required: Required[New]
not_required: NotRequired[Backported]
```

### Within `Literal`

`LiteralString` cannot be used within `Literal`:

```py
from typing import Literal, LiteralString

bad_union: Literal["hello", LiteralString] # error: [invalid-literal-parameter]
bad_nesting: Literal[LiteralString] # error: [invalid-literal-parameter]
```

### Parametrized

`LiteralString` cannot be parametrized.

```py
# TODO: See above.
# error: [possibly-unbound-import]
from typing import LiteralString, TypeAlias, TypeAliasType
InSyncWithFoo marked this conversation as resolved.
Show resolved Hide resolved

Old: TypeAlias = LiteralString
type New = LiteralString
Backported = TypeAliasType("Backported", LiteralString)
InSyncWithFoo marked this conversation as resolved.
Show resolved Hide resolved

a: LiteralString[str] # error: [invalid-type-parameter]
b: LiteralString["foo"] # error: [invalid-type-parameter]
InSyncWithFoo marked this conversation as resolved.
Show resolved Hide resolved

c: Old[str] # error: [invalid-type-parameter]
d: Old["foo"] # error: [invalid-type-parameter]

# TODO: Emit invalid-type-parameter for the following
e: New[str]
f: New["int"]

g: Backported[str]
h: Backported["int"]

# fine: TypeAliasType instances are subscriptable.
# These are not required to have a meaning outside annotation contexts.
New[str]
New["int"]
Backported[str]
Backported["int"]
```

### As a base class

Subclassing `LiteralString` leads to a runtime error.

```py
from typing import LiteralString

class C(LiteralString): ... # error: [invalid-base]
```

## Inference

### Common operations

```py
foo: LiteralString = "foo"
reveal_type(foo) # revealed: Literal["foo"]

bar: LiteralString = "bar"
reveal_type(foo + bar) # revealed: Literal["foobar"]

baz: LiteralString = "baz"
baz += foo
reveal_type(baz) # revealed: Literal["bazfoo"]

qux = (foo, bar)
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]

# TODO: Infer "LiteralString"
reveal_type(foo.join(qux)) # revealed: @Todo(call type)

template: LiteralString = "{}, {}"
reveal_type(template) # revealed: Literal["{}, {}"]
# TODO: Infer "foo, bar"
InSyncWithFoo marked this conversation as resolved.
Show resolved Hide resolved
reveal_type(template.format(foo, bar)) # revealed: @Todo(call type)
```

### Compatibility
InSyncWithFoo marked this conversation as resolved.
Show resolved Hide resolved

`Literal["", ...]` is compatible with `LiteralString`, `LiteralString` is compatible with `str`, but
not vice versa.
InSyncWithFoo marked this conversation as resolved.
Show resolved Hide resolved

```py
foo_1: Literal["foo"] = "foo"
bar_1: LiteralString = foo_1 # fine

if bool():
foo_2 = "foo"
else:
foo_2 = "bar"
InSyncWithFoo marked this conversation as resolved.
Show resolved Hide resolved
reveal_type(foo_2) # revealed: Literal["foo", "bar"]
bar_2: LiteralString = foo_2 # fine

foo_3: LiteralString = "foo" * 1_000_000_000
bar_3: str = foo_2 # fine

baz_1: str = str()
qux_1: LiteralString = baz_1 # error: [invalid-assignment]

baz_2: LiteralString = "baz" * 1_000_000_000
qux_2: Literal["qux"] = baz_2 # error: [invalid-assignment]

if bool():
baz_3 = "foo"
else:
baz_3 = 1
InSyncWithFoo marked this conversation as resolved.
Show resolved Hide resolved
reveal_type(baz_3) # revealed: Literal["foo"] | Literal[1]
qux_3: LiteralString = baz_3 # error: [invalid-assignment]
```

### Narrowing

```py
lorem: LiteralString = "lorem" * 1_000_000_000
reveal_type(lorem) # revealed: LiteralString

if lorem == "ipsum":
reveal_type(lorem) # revealed: Literal["ipsum"]
```

[1]: https://typing.readthedocs.io/en/latest/spec/literal.html#literalstring
15 changes: 14 additions & 1 deletion crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,11 @@ impl<'db> Type<'db> {
Type::StringLiteral(_) | Type::LiteralString,
Type::Instance(InstanceType { class }),
) if class.is_known(db, KnownClass::Str) => true,
(
Type::Instance(InstanceType { class }),
Type::StringLiteral(_) | Type::LiteralString,
) if class.is_known(db, KnownClass::Str) => false,
(Type::LiteralString, Type::StringLiteral(_)) => false,
InSyncWithFoo marked this conversation as resolved.
Show resolved Hide resolved
(Type::BytesLiteral(_), Type::Instance(InstanceType { class }))
if class.is_known(db, KnownClass::Bytes) =>
{
Expand Down Expand Up @@ -1403,7 +1408,7 @@ impl<'db> Type<'db> {
// `Any` is callable, and its return type is also `Any`.
Type::Any => CallOutcome::callable(Type::Any),

Type::Todo(_) => CallOutcome::callable(todo_type!()),
Type::Todo(_) => CallOutcome::callable(todo_type!("call type")),
InSyncWithFoo marked this conversation as resolved.
Show resolved Hide resolved

Type::Unknown => CallOutcome::callable(Type::Unknown),

Expand Down Expand Up @@ -1556,6 +1561,7 @@ impl<'db> Type<'db> {
Type::KnownInstance(KnownInstanceType::Never | KnownInstanceType::NoReturn) => {
Type::Never
}
Type::KnownInstance(KnownInstanceType::LiteralString) => Type::LiteralString,
_ => todo_type!(),
}
}
Expand Down Expand Up @@ -1888,6 +1894,8 @@ impl<'db> KnownClass {
pub enum KnownInstanceType<'db> {
/// The symbol `typing.Literal` (which can also be found as `typing_extensions.Literal`)
Literal,
/// The symbol `typing.LiteralString` (which can also be found as `typing_extensions.LiteralString`)
LiteralString,
/// The symbol `typing.Optional` (which can also be found as `typing_extensions.Optional`)
Optional,
/// The symbol `typing.Union` (which can also be found as `typing_extensions.Union`)
Expand All @@ -1907,6 +1915,7 @@ impl<'db> KnownInstanceType<'db> {
pub const fn as_str(self) -> &'static str {
match self {
Self::Literal => "Literal",
Self::LiteralString => "LiteralString",
Self::Optional => "Optional",
Self::Union => "Union",
Self::TypeVar(_) => "TypeVar",
Expand All @@ -1920,6 +1929,7 @@ impl<'db> KnownInstanceType<'db> {
pub const fn bool(self) -> Truthiness {
match self {
Self::Literal
| Self::LiteralString
| Self::Optional
| Self::TypeVar(_)
| Self::Union
Expand All @@ -1933,6 +1943,7 @@ impl<'db> KnownInstanceType<'db> {
pub fn repr(self, db: &'db dyn Db) -> &'db str {
match self {
Self::Literal => "typing.Literal",
Self::LiteralString => "typing.LiteralString",
Self::Optional => "typing.Optional",
Self::Union => "typing.Union",
Self::NoReturn => "typing.NoReturn",
Expand All @@ -1946,6 +1957,7 @@ impl<'db> KnownInstanceType<'db> {
pub const fn class(self) -> KnownClass {
match self {
Self::Literal => KnownClass::SpecialForm,
Self::LiteralString => KnownClass::SpecialForm,
Self::Optional => KnownClass::SpecialForm,
Self::Union => KnownClass::SpecialForm,
Self::NoReturn => KnownClass::SpecialForm,
Expand All @@ -1970,6 +1982,7 @@ impl<'db> KnownInstanceType<'db> {
}
match (module.name().as_str(), instance_name) {
("typing" | "typing_extensions", "Literal") => Some(Self::Literal),
("typing" | "typing_extensions", "LiteralString") => Some(Self::LiteralString),
("typing" | "typing_extensions", "Optional") => Some(Self::Optional),
("typing" | "typing_extensions", "Union") => Some(Self::Union),
("typing" | "typing_extensions", "NoReturn") => Some(Self::NoReturn),
Expand Down
11 changes: 11 additions & 0 deletions crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4642,6 +4642,17 @@ impl<'db> TypeInferenceBuilder<'db> {
);
Type::Unknown
}
KnownInstanceType::LiteralString => {
self.diagnostics.add(
subscript.into(),
"invalid-type-parameter",
format_args!(
"Type `{}` expected no type parameter. Did you mean to use `Literal[...]` instead?",
InSyncWithFoo marked this conversation as resolved.
Show resolved Hide resolved
known_instance.repr(self.db)
),
);
Type::Unknown
}
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/red_knot_python_semantic/src/types/mro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ impl<'db> ClassBase<'db> {
KnownInstanceType::TypeVar(_)
| KnownInstanceType::TypeAliasType(_)
| KnownInstanceType::Literal
| KnownInstanceType::LiteralString
| KnownInstanceType::Union
| KnownInstanceType::NoReturn
| KnownInstanceType::Never
Expand Down
13 changes: 13 additions & 0 deletions crates/red_knot_python_semantic/src/types/narrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,15 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
.chain(comparators)
.tuple_windows::<(&ruff_python_ast::Expr, &ruff_python_ast::Expr)>();
let mut constraints = NarrowingConstraints::default();

let mut last_rhs_ty: Option<Type> = None;

for (op, (left, right)) in std::iter::zip(&**ops, comparator_tuples) {
let lhs_ty = last_rhs_ty.unwrap_or_else(|| {
inference.expression_ty(left.scoped_expression_id(self.db, scope))
});
let rhs_ty = inference.expression_ty(right.scoped_expression_id(self.db, scope));
last_rhs_ty = Some(rhs_ty);
InSyncWithFoo marked this conversation as resolved.
Show resolved Hide resolved

match left {
ast::Expr::Name(ast::ExprName {
Expand Down Expand Up @@ -327,6 +334,12 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
constraints.insert(symbol, ty);
}
}
ast::CmpOp::Eq if lhs_ty.is_literal_string() => {
let ty = IntersectionBuilder::new(self.db)
.add_positive(rhs_ty)
.build();
constraints.insert(symbol, ty);
InSyncWithFoo marked this conversation as resolved.
Show resolved Hide resolved
}
_ => {
// TODO other comparison types
}
Expand Down