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
4 changes: 3 additions & 1 deletion crates/ty_python_semantic/resources/mdtest/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -893,8 +893,10 @@ class LotsOfBindings(Protocol):
match object():
case l: # error: [ambiguous-protocol-member]
...
# error: [ambiguous-protocol-member] "Consider adding an annotation, e.g. `m: int | str = ...`"
m = 1 if 1.2 > 3.4 else "a"
Comment on lines +896 to +897
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, unlikely to ever appear in practice. But a nice way to demonstrate how the previous Type::literal_promotion_type was different from Type::promote_literals (this only works with the latter).

Copy link
Member

Choose a reason for hiding this comment

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

unlikely to ever appear in practice

what do you mean, i write protocols like this all the time


# revealed: frozenset[Literal["Nested", "NestedProtocol", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"]]
# revealed: frozenset[Literal["Nested", "NestedProtocol", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m"]]
reveal_type(get_protocol_members(LotsOfBindings))

class Foo(Protocol):
Expand Down
31 changes: 17 additions & 14 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1166,21 +1166,26 @@ impl<'db> Type<'db> {
}
}

/// If this type is a literal, promote it to a type that this literal is an instance of.
/// Promote (possibly nested) literals to types that these literals are instances of.
///
/// Note that this function tries to promote literals to a more user-friendly form than their
/// fallback instance type. For example, `def _() -> int` is promoted to `Callable[[], int]`,
/// as opposed to `FunctionType`.
pub(crate) fn literal_promotion_type(self, db: &'db dyn Db) -> Option<Type<'db>> {
pub(crate) fn promote_literals(self, db: &'db dyn Db) -> Type<'db> {
self.apply_type_mapping(db, &TypeMapping::PromoteLiterals)
}

/// Like [`Type::promote_literals`], but does not recurse into nested types.
fn promote_literals_impl(self, db: &'db dyn Db) -> Type<'db> {
match self {
Type::StringLiteral(_) | Type::LiteralString => Some(KnownClass::Str.to_instance(db)),
Type::BooleanLiteral(_) => Some(KnownClass::Bool.to_instance(db)),
Type::IntLiteral(_) => Some(KnownClass::Int.to_instance(db)),
Type::BytesLiteral(_) => Some(KnownClass::Bytes.to_instance(db)),
Type::ModuleLiteral(_) => Some(KnownClass::ModuleType.to_instance(db)),
Type::EnumLiteral(literal) => Some(literal.enum_class_instance(db)),
Type::FunctionLiteral(literal) => Some(Type::Callable(literal.into_callable_type(db))),
_ => None,
Type::StringLiteral(_) | Type::LiteralString => KnownClass::Str.to_instance(db),
Type::BooleanLiteral(_) => KnownClass::Bool.to_instance(db),
Type::IntLiteral(_) => KnownClass::Int.to_instance(db),
Type::BytesLiteral(_) => KnownClass::Bytes.to_instance(db),
Type::ModuleLiteral(_) => KnownClass::ModuleType.to_instance(db),
Copy link
Member

Choose a reason for hiding this comment

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

I'm not actually sure when promotion of module-literal types is desirable -- we might want to remove it from this method. As you discovered on your "remove | Unknown from module-globals PR (sorry for looking while it was still in draft 🙈), promotion of module-literal types to types.ModuleType can cause serious issues because each module-literal type has a distinct set of attributes available on it. This makes module-literal types somewhat different to all our other literal types

Copy link
Contributor Author

@sharkdp sharkdp Sep 30, 2025

Choose a reason for hiding this comment

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

Was thinking the exact same thing when I changed this code. In fact, I did arrive here in the first place because promotion of module literals was causing the exact problem you are describing. I'll propose that as a separate change.

Edit: I should have read the rest of your comment before writing mine.

Type::EnumLiteral(literal) => literal.enum_class_instance(db),
Type::FunctionLiteral(literal) => Type::Callable(literal.into_callable_type(db)),
_ => self,
}
}

Expand Down Expand Up @@ -6080,8 +6085,7 @@ impl<'db> Type<'db> {
let function = Type::FunctionLiteral(function.apply_type_mapping_impl(db, type_mapping, visitor));

match type_mapping {
TypeMapping::PromoteLiterals => function.literal_promotion_type(db)
.expect("function literal should have a promotion type"),
TypeMapping::PromoteLiterals => function.promote_literals_impl(db),
_ => function
}
}
Expand Down Expand Up @@ -6189,8 +6193,7 @@ impl<'db> Type<'db> {
TypeMapping::ReplaceSelf { .. } |
TypeMapping::MarkTypeVarsInferable(_) |
TypeMapping::Materialize(_) => self,
TypeMapping::PromoteLiterals => self.literal_promotion_type(db)
.expect("literal type should have a promotion type"),
TypeMapping::PromoteLiterals => self.promote_literals_impl(db)
}

Type::Dynamic(_) => match type_mapping {
Expand Down
10 changes: 7 additions & 3 deletions crates/ty_python_semantic/src/types/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2625,6 +2625,12 @@ pub(crate) fn report_undeclared_protocol_member(
SubclassOfInner::Dynamic(_) => return false,
},
Type::NominalInstance(instance) => instance.class(db),
Type::Union(union) => {
return union
.elements(db)
.iter()
.all(|elem| should_give_hint(db, *elem));
}
Comment on lines +2628 to +2633
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Admittedly more of a showcase for the refactor here. I'm also happy to remove this again (and the test above).

_ => return false,
};

Expand Down Expand Up @@ -2656,9 +2662,7 @@ pub(crate) fn report_undeclared_protocol_member(
if definition.kind(db).is_unannotated_assignment() {
let binding_type = binding_type(db, definition);

let suggestion = binding_type
.literal_promotion_type(db)
.unwrap_or(binding_type);
let suggestion = binding_type.promote_literals(db);

if should_give_hint(db, suggestion) {
diagnostic.set_primary_message(format_args!(
Expand Down
5 changes: 2 additions & 3 deletions crates/ty_python_semantic/src/types/infer/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ use crate::types::{
DynamicType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType,
MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter, ParameterForm,
Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet, Truthiness, Type,
TypeAliasType, TypeAndQualifiers, TypeContext, TypeMapping, TypeQualifiers,
TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers,
TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarInstance, TypeVarKind,
UnionBuilder, UnionType, binding_type, todo_type,
};
Expand Down Expand Up @@ -5432,8 +5432,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {

// Convert any element literals to their promoted type form to avoid excessively large
// unions for large nested list literals, which the constraint solver struggles with.
let inferred_elt_ty =
inferred_elt_ty.apply_type_mapping(self.db(), &TypeMapping::PromoteLiterals);
let inferred_elt_ty = inferred_elt_ty.promote_literals(self.db());

builder
.infer(Type::TypeVar(*elt_ty), inferred_elt_ty)
Expand Down
Loading