mirror of
https://github.com/astral-sh/ruff
synced 2026-01-22 14:00:51 -05:00
[ty] Recognize string-literal types as subtypes of Sequence[Literal[chars]] (#22415)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
This commit is contained in:
@@ -113,6 +113,29 @@ static_assert(not is_assignable_to(str, Literal["foo"]))
|
||||
static_assert(not is_assignable_to(str, LiteralString))
|
||||
```
|
||||
|
||||
### String literals and Sequence
|
||||
|
||||
String literals are assignable to `Sequence[Literal[chars...]]` because strings are sequences of
|
||||
their characters.
|
||||
|
||||
```py
|
||||
from typing import Literal, Sequence, Iterable, Collection, Reversible
|
||||
from ty_extensions import is_assignable_to, static_assert
|
||||
|
||||
static_assert(is_assignable_to(Literal["abba"], Sequence[Literal["a", "b"]]))
|
||||
static_assert(is_assignable_to(Literal["abb"], Iterable[Literal["a", "b"]]))
|
||||
static_assert(is_assignable_to(Literal["abb"], Collection[Literal["a", "b"]]))
|
||||
static_assert(is_assignable_to(Literal["abb"], Reversible[Literal["a", "b"]]))
|
||||
static_assert(is_assignable_to(Literal["aaa"], Sequence[Literal["a"]]))
|
||||
static_assert(is_assignable_to(Literal[""], Sequence[Literal["a", "b"]])) # empty string
|
||||
static_assert(is_assignable_to(Literal["ab"], Sequence[Literal["a", "b", "c"]])) # subset of allowed chars
|
||||
|
||||
# String literals are NOT assignable when they contain chars outside the allowed set
|
||||
static_assert(not is_assignable_to(Literal["abc"], Sequence[Literal["a", "b"]])) # 'c' not allowed
|
||||
static_assert(not is_assignable_to(Literal["x"], Sequence[Literal["a", "b"]])) # 'x' not allowed
|
||||
static_assert(not is_assignable_to(Literal["aa"], Sequence[Literal[""]]))
|
||||
```
|
||||
|
||||
### Byte literals
|
||||
|
||||
```py
|
||||
|
||||
@@ -2254,6 +2254,29 @@ static_assert(not is_subtype_of(Callable[[str], str], CallableTypeOf[identity]))
|
||||
static_assert(not is_subtype_of(Callable[[str], int], CallableTypeOf[identity]))
|
||||
```
|
||||
|
||||
## String literals and Sequence
|
||||
|
||||
String literals are subtypes of `Sequence[Literal[chars...]]` because strings are sequences of their
|
||||
characters.
|
||||
|
||||
```py
|
||||
from typing import Literal, Sequence, Iterable, Collection, Reversible
|
||||
from ty_extensions import is_subtype_of, static_assert
|
||||
|
||||
static_assert(is_subtype_of(Literal["abba"], Sequence[Literal["a", "b"]]))
|
||||
static_assert(is_subtype_of(Literal["abb"], Iterable[Literal["a", "b"]]))
|
||||
static_assert(is_subtype_of(Literal["abb"], Collection[Literal["a", "b"]]))
|
||||
static_assert(is_subtype_of(Literal["abb"], Reversible[Literal["a", "b"]]))
|
||||
static_assert(is_subtype_of(Literal["aaa"], Sequence[Literal["a"]]))
|
||||
static_assert(is_subtype_of(Literal[""], Sequence[Literal["a", "b"]])) # empty string
|
||||
static_assert(is_subtype_of(Literal["ab"], Sequence[Literal["a", "b", "c"]])) # subset of allowed chars
|
||||
|
||||
# String literals are NOT subtypes when they contain chars outside the allowed set
|
||||
static_assert(not is_subtype_of(Literal["abc"], Sequence[Literal["a", "b"]])) # 'c' not allowed
|
||||
static_assert(not is_subtype_of(Literal["x"], Sequence[Literal["a", "b"]])) # 'x' not allowed
|
||||
static_assert(not is_subtype_of(Literal["aa"], Sequence[Literal[""]]))
|
||||
```
|
||||
|
||||
[gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form
|
||||
[gradual tuple]: https://typing.python.org/en/latest/spec/tuples.html#tuple-type-form
|
||||
[special case for float and complex]: https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use compact_str::CompactString;
|
||||
use compact_str::{CompactString, ToCompactString};
|
||||
use infer::nearest_enclosing_class;
|
||||
use itertools::{Either, Itertools};
|
||||
use ruff_diagnostics::{Edit, Fix};
|
||||
@@ -1452,6 +1452,10 @@ impl<'db> Type<'db> {
|
||||
Self::StringLiteral(StringLiteralType::new(db, string))
|
||||
}
|
||||
|
||||
pub(crate) fn single_char_string_literal(db: &'db dyn Db, c: char) -> Self {
|
||||
Type::StringLiteral(StringLiteralType::new(db, c.to_compact_string()))
|
||||
}
|
||||
|
||||
pub(crate) fn bytes_literal(db: &'db dyn Db, bytes: &[u8]) -> Self {
|
||||
Self::BytesLiteral(BytesLiteralType::new(db, bytes))
|
||||
}
|
||||
|
||||
@@ -6187,6 +6187,7 @@ pub enum KnownClass {
|
||||
SupportsIndex,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Sequence,
|
||||
Mapping,
|
||||
// typing_extensions
|
||||
ExtensionsTypeVar, // must be distinct from typing.TypeVar, backports new features
|
||||
@@ -6303,6 +6304,7 @@ impl KnownClass {
|
||||
| Self::ABCMeta
|
||||
| Self::Iterable
|
||||
| Self::Iterator
|
||||
| Self::Sequence
|
||||
| Self::Mapping
|
||||
// Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9
|
||||
// and raises a `TypeError` in Python >=3.14
|
||||
@@ -6391,6 +6393,7 @@ impl KnownClass {
|
||||
| KnownClass::SupportsIndex
|
||||
| KnownClass::Iterable
|
||||
| KnownClass::Iterator
|
||||
| KnownClass::Sequence
|
||||
| KnownClass::Mapping
|
||||
| KnownClass::ChainMap
|
||||
| KnownClass::Counter
|
||||
@@ -6478,6 +6481,7 @@ impl KnownClass {
|
||||
| KnownClass::SupportsIndex
|
||||
| KnownClass::Iterable
|
||||
| KnownClass::Iterator
|
||||
| KnownClass::Sequence
|
||||
| KnownClass::Mapping
|
||||
| KnownClass::ChainMap
|
||||
| KnownClass::Counter
|
||||
@@ -6565,6 +6569,7 @@ impl KnownClass {
|
||||
| KnownClass::SupportsIndex
|
||||
| KnownClass::Iterable
|
||||
| KnownClass::Iterator
|
||||
| KnownClass::Sequence
|
||||
| KnownClass::Mapping
|
||||
| KnownClass::ChainMap
|
||||
| KnownClass::Counter
|
||||
@@ -6684,7 +6689,8 @@ impl KnownClass {
|
||||
| Self::ProtocolMeta
|
||||
| Self::Template
|
||||
| Self::Path
|
||||
| Self::Mapping => false,
|
||||
| Self::Mapping
|
||||
| Self::Sequence => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6758,6 +6764,7 @@ impl KnownClass {
|
||||
| KnownClass::SupportsIndex
|
||||
| KnownClass::Iterable
|
||||
| KnownClass::Iterator
|
||||
| KnownClass::Sequence
|
||||
| KnownClass::Mapping
|
||||
| KnownClass::ChainMap
|
||||
| KnownClass::Counter
|
||||
@@ -6850,6 +6857,7 @@ impl KnownClass {
|
||||
Self::Super => "super",
|
||||
Self::Iterable => "Iterable",
|
||||
Self::Iterator => "Iterator",
|
||||
Self::Sequence => "Sequence",
|
||||
Self::Mapping => "Mapping",
|
||||
// For example, `typing.List` is defined as `List = _Alias()` in typeshed
|
||||
Self::StdlibAlias => "_Alias",
|
||||
@@ -7192,6 +7200,7 @@ impl KnownClass {
|
||||
| Self::StdlibAlias
|
||||
| Self::Iterable
|
||||
| Self::Iterator
|
||||
| Self::Sequence
|
||||
| Self::Mapping
|
||||
| Self::ProtocolMeta
|
||||
| Self::SupportsIndex => KnownModule::Typing,
|
||||
@@ -7331,6 +7340,7 @@ impl KnownClass {
|
||||
| Self::InitVar
|
||||
| Self::Iterable
|
||||
| Self::Iterator
|
||||
| Self::Sequence
|
||||
| Self::Mapping
|
||||
| Self::NamedTupleFallback
|
||||
| Self::NamedTupleLike
|
||||
@@ -7423,6 +7433,7 @@ impl KnownClass {
|
||||
| Self::InitVar
|
||||
| Self::Iterable
|
||||
| Self::Iterator
|
||||
| Self::Sequence
|
||||
| Self::Mapping
|
||||
| Self::NamedTupleFallback
|
||||
| Self::NamedTupleLike
|
||||
@@ -7487,6 +7498,7 @@ impl KnownClass {
|
||||
"TypeVar" => &[Self::TypeVar, Self::ExtensionsTypeVar],
|
||||
"Iterable" => &[Self::Iterable],
|
||||
"Iterator" => &[Self::Iterator],
|
||||
"Sequence" => &[Self::Sequence],
|
||||
"Mapping" => &[Self::Mapping],
|
||||
"ParamSpec" => &[Self::ParamSpec, Self::ExtensionsParamSpec],
|
||||
"ParamSpecArgs" => &[Self::ParamSpecArgs],
|
||||
@@ -7630,6 +7642,7 @@ impl KnownClass {
|
||||
| Self::TypeVarTuple
|
||||
| Self::Iterable
|
||||
| Self::Iterator
|
||||
| Self::Sequence
|
||||
| Self::Mapping
|
||||
| Self::ProtocolMeta
|
||||
| Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions),
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
use itertools::Itertools;
|
||||
use ruff_python_ast::name::Name;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use crate::place::{DefinedPlace, Place};
|
||||
use crate::types::builder::RecursivelyDefined;
|
||||
use crate::types::constraints::{IteratorConstraintsExtension, OptionConstraintsExtension};
|
||||
use crate::types::enums::is_single_member_enum;
|
||||
use crate::types::{
|
||||
CallableType, ClassType, CycleDetector, DynamicType, KnownClass, KnownInstanceType,
|
||||
CallableType, ClassBase, ClassType, CycleDetector, DynamicType, KnownClass, KnownInstanceType,
|
||||
MemberLookupPolicy, PairVisitor, ProtocolInstanceType, SubclassOfInner,
|
||||
TypeVarBoundOrConstraints,
|
||||
TypeVarBoundOrConstraints, UnionType,
|
||||
};
|
||||
use crate::{
|
||||
Db,
|
||||
@@ -1044,6 +1047,62 @@ impl<'db> Type<'db> {
|
||||
// All `StringLiteral` types are a subtype of `LiteralString`.
|
||||
(Type::StringLiteral(_), Type::LiteralString) => ConstraintSet::from(true),
|
||||
|
||||
// A string literal `Literal["abc"]` is assignable to `str` *and* to
|
||||
// `Sequence[Literal["a", "b", "c"]]` because strings are sequences of their characters.
|
||||
//
|
||||
// Note that this strictly holds true for all type relations!
|
||||
// However, as an optimisation (to avoid interning many single-character string-literal types),
|
||||
// we only recognise this as being true for assignability.
|
||||
(Type::StringLiteral(value), Type::NominalInstance(instance)) => {
|
||||
let other_class = instance.class(db);
|
||||
|
||||
if other_class.is_known(db, KnownClass::Str) {
|
||||
return ConstraintSet::from(true);
|
||||
}
|
||||
|
||||
if let Some(sequence_class) = KnownClass::Sequence.try_to_class_literal(db)
|
||||
&& !sequence_class
|
||||
.iter_mro(db, None)
|
||||
.filter_map(ClassBase::into_class)
|
||||
.map(|class| class.class_literal(db))
|
||||
.contains(&other_class.class_literal(db))
|
||||
{
|
||||
return ConstraintSet::from(false);
|
||||
}
|
||||
|
||||
let chars: FxHashSet<char> = value.value(db).chars().collect();
|
||||
|
||||
let spec = match chars.len() {
|
||||
0 => Type::Never,
|
||||
1 => Type::single_char_string_literal(db, *chars.iter().next().unwrap()),
|
||||
_ => {
|
||||
// Optimisation: since we know this union will only include string-literal types,
|
||||
// avoid eagerly creating string-literal types when unnecessary, and avoid going
|
||||
// via the union-builder.
|
||||
let union_elements: Box<[Type<'db>]> = chars
|
||||
.iter()
|
||||
.map(|c| Type::single_char_string_literal(db, *c))
|
||||
.collect();
|
||||
Type::Union(UnionType::new(db, union_elements, RecursivelyDefined::No))
|
||||
}
|
||||
};
|
||||
|
||||
KnownClass::Sequence
|
||||
.to_specialized_class_type(db, &[spec])
|
||||
.when_some_and(|sequence| {
|
||||
sequence.has_relation_to_impl(
|
||||
db,
|
||||
other_class,
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
(Type::StringLiteral(_), _) => ConstraintSet::from(false),
|
||||
|
||||
// An instance is a subtype of an enum literal, if it is an instance of the enum class
|
||||
// and the enum has only one member.
|
||||
(Type::NominalInstance(_), Type::EnumLiteral(target_enum_literal)) => {
|
||||
@@ -1057,12 +1116,11 @@ impl<'db> Type<'db> {
|
||||
))
|
||||
}
|
||||
|
||||
// Except for the special `LiteralString` case above,
|
||||
// Except for the special `LiteralString` and `StringLiteral` cases above,
|
||||
// most `Literal` types delegate to their instance fallbacks
|
||||
// unless `self` is exactly equivalent to `target` (handled above)
|
||||
(
|
||||
Type::StringLiteral(_)
|
||||
| Type::LiteralString
|
||||
Type::LiteralString
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
|
||||
Reference in New Issue
Block a user