Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,15 @@ reveal_type(os.stat_result.__mro__)
reveal_type(os.stat_result.__getitem__)
```

But perhaps the most commonly used tuple subclass instance is the singleton `sys.version_info`:

```py
import sys

# revealed: Overload[(self, index: Literal[-5, 0], /) -> Literal[3], (self, index: Literal[-4, 1], /) -> Literal[11], (self, index: Literal[-3, -1, 2, 4], /) -> int, (self, index: Literal[-2, 3], /) -> Literal["alpha", "beta", "candidate", "final"], (self, index: SupportsIndex, /) -> int | Literal["alpha", "beta", "candidate", "final"], (self, index: slice[Any, Any, Any], /) -> tuple[int | Literal["alpha", "beta", "candidate", "final"], ...]]
reveal_type(type(sys.version_info).__getitem__)
```

Because of the synthesized `__getitem__` overloads we synthesize for tuples and tuple subclasses,
tuples are naturally understood as being subtypes of protocols that have precise return types from
`__getitem__` method members:
Expand Down
22 changes: 16 additions & 6 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::types::function::{DataclassTransformerParams, KnownFunction};
use crate::types::generics::{GenericContext, Specialization, walk_specialization};
use crate::types::infer::nearest_enclosing_class;
use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature};
use crate::types::tuple::TupleSpec;
use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::{
BareTypeAliasType, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams,
DeprecatedInstance, KnownInstanceType, StringLiteralType, TypeAliasType, TypeMapping,
Expand Down Expand Up @@ -1348,11 +1348,21 @@ impl<'db> ClassLiteral<'db> {
let class_definition =
semantic_index(db, self.file(db)).expect_single_definition(class_stmt);

class_stmt
.bases()
.iter()
.map(|base_node| definition_expression_type(db, class_definition, base_node))
.collect()
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");
Comment on lines +1352 to +1353
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just something to be aware of. This has a risk of lock congestion if explict_bases is very hot because we keep interning the same TupleType. I don't think explict_bases is that hot that this is an issue here but I thought it's worth noting (and it's also something that we might just need to fix in Salsa)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is likely not an issue here because we only intern when we call explicit_bases on the VersionInfo class specifically. And this method is cached so if we do that frequently, all but the first time in a revision wouldn't reach here anyway.


Box::new([
definition_expression_type(db, class_definition, &class_stmt.bases()[0]),
Type::from(tuple_type.to_class_type(db)),
])
} else {
class_stmt
.bases()
.iter()
.map(|base_node| definition_expression_type(db, class_definition, base_node))
.collect()
}
}

/// Return `Some()` if this class is known to be a [`SolidBase`], or `None` if it is not.
Expand Down
66 changes: 22 additions & 44 deletions crates/ty_python_semantic/src/types/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use crate::types::cyclic::PairVisitor;
use crate::types::enums::is_single_member_enum;
use crate::types::protocol_class::walk_protocol_interface;
use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::{ClassBase, DynamicType, TypeMapping, TypeRelation, TypeTransformer, UnionType};
use crate::{Db, FxOrderSet, Program};
use crate::types::{ClassBase, DynamicType, TypeMapping, TypeRelation, TypeTransformer};
use crate::{Db, FxOrderSet};

pub(super) use synthesized_protocol::SynthesizedProtocolType;

Expand Down Expand Up @@ -115,47 +115,6 @@ impl<'db> NominalInstanceType<'db> {
/// I.e., for the type `tuple[int, str]`, this will return the tuple spec `[int, str]`.
/// For a subclass of `tuple[int, str]`, it will return the same tuple spec.
pub(super) fn tuple_spec(&self, db: &'db dyn Db) -> Option<Cow<'db, TupleSpec<'db>>> {
fn own_tuple_spec_of_class<'db>(
db: &'db dyn Db,
class: ClassType<'db>,
) -> Option<Cow<'db, TupleSpec<'db>>> {
let (class_literal, specialization) = class.class_literal(db);
match class_literal.known(db)? {
KnownClass::Tuple => Some(
specialization
.and_then(|spec| Some(Cow::Borrowed(spec.tuple(db)?)))
.unwrap_or_else(|| Cow::Owned(TupleSpec::homogeneous(Type::unknown()))),
),
KnownClass::VersionInfo => {
let python_version = Program::get(db).python_version(db);
let int_instance_ty = KnownClass::Int.to_instance(db);

// TODO: just grab this type from typeshed (it's a `sys._ReleaseLevel` type alias there)
let release_level_ty = {
let elements: Box<[Type<'db>]> = ["alpha", "beta", "candidate", "final"]
.iter()
.map(|level| Type::string_literal(db, level))
.collect();

// For most unions, it's better to go via `UnionType::from_elements` or use `UnionBuilder`;
// those techniques ensure that union elements are deduplicated and unions are eagerly simplified
// into other types where necessary. Here, however, we know that there are no duplicates
// in this union, so it's probably more efficient to use `UnionType::new()` directly.
Type::Union(UnionType::new(db, elements))
};

Some(Cow::Owned(TupleSpec::from_elements([
Type::IntLiteral(python_version.major.into()),
Type::IntLiteral(python_version.minor.into()),
int_instance_ty,
release_level_ty,
int_instance_ty,
])))
}
_ => None,
}
}

match self.0 {
NominalInstanceInner::ExactTuple(tuple) => Some(Cow::Borrowed(tuple.tuple(db))),
NominalInstanceInner::NonTuple(class) => {
Expand All @@ -169,7 +128,26 @@ impl<'db> NominalInstanceType<'db> {
class
.iter_mro(db)
.filter_map(ClassBase::into_class)
.find_map(|class| own_tuple_spec_of_class(db, class))
.find_map(|class| match class.known(db)? {
// N.B. this is a pure optimisation: iterating through the MRO would give us
// the correct tuple spec for `sys._version_info`, since we special-case the class
// in `ClassLiteral::explicit_bases()` so that it is inferred as inheriting from
// a tuple type with the correct spec for the user's configured Python version and platform.
KnownClass::VersionInfo => {
Some(Cow::Owned(TupleSpec::version_info_spec(db)))
}
KnownClass::Tuple => Some(
class
.into_generic_alias()
.and_then(|alias| {
Some(Cow::Borrowed(alias.specialization(db).tuple(db)?))
})
.unwrap_or_else(|| {
Cow::Owned(TupleSpec::homogeneous(Type::unknown()))
}),
),
_ => None,
})
}
}
}
Expand Down
30 changes: 29 additions & 1 deletion crates/ty_python_semantic/src/types/tuple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use crate::types::{
UnionBuilder, UnionType, cyclic::PairVisitor,
};
use crate::util::subscript::{Nth, OutOfBoundsError, PyIndex, PySlice, StepSizeZeroError};
use crate::{Db, FxOrderSet};
use crate::{Db, FxOrderSet, Program};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum TupleLength {
Expand Down Expand Up @@ -1162,6 +1162,34 @@ impl<'db> Tuple<Type<'db>> {
Tuple::Variable(_) => false,
}
}

/// Return the `TupleSpec` for the singleton `sys.version_info`
pub(crate) fn version_info_spec(db: &'db dyn Db) -> TupleSpec<'db> {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fairly easy to add caching to this method if we revert d76fd10, but I still haven't been able to find any evidence yet that this would provide a significant speedup. I also haven't tried doing that as an isolated change on the codspeed runners, though; I've only experimented by running benchmarks locally.

let python_version = Program::get(db).python_version(db);
let int_instance_ty = KnownClass::Int.to_instance(db);

// TODO: just grab this type from typeshed (it's a `sys._ReleaseLevel` type alias there)
let release_level_ty = {
let elements: Box<[Type<'db>]> = ["alpha", "beta", "candidate", "final"]
.iter()
.map(|level| Type::string_literal(db, level))
.collect();

// For most unions, it's better to go via `UnionType::from_elements` or use `UnionBuilder`;
// those techniques ensure that union elements are deduplicated and unions are eagerly simplified
// into other types where necessary. Here, however, we know that there are no duplicates
// in this union, so it's probably more efficient to use `UnionType::new()` directly.
Type::Union(UnionType::new(db, elements))
};

TupleSpec::from_elements([
Type::IntLiteral(python_version.major.into()),
Type::IntLiteral(python_version.minor.into()),
int_instance_ty,
release_level_ty,
int_instance_ty,
])
}
}

impl<T> From<FixedLengthTuple<T>> for Tuple<T> {
Expand Down
Loading