[ty] Improve debuggability of protocol types (#19662)

This commit is contained in:
Alex Waygood 2025-08-01 15:16:13 +01:00 committed by GitHub
parent 57e2e8664f
commit e7e7b7bf21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 158 additions and 1 deletions

View file

@ -382,6 +382,31 @@ class Foo(Protocol):
reveal_type(get_protocol_members(Foo)) # revealed: frozenset[Literal["method_member", "x", "y", "z"]] reveal_type(get_protocol_members(Foo)) # revealed: frozenset[Literal["method_member", "x", "y", "z"]]
``` ```
To see the kinds and types of the protocol members, you can use the debugging aid
`ty_extensions.reveal_protocol_interface`, meanwhile:
```py
from ty_extensions import reveal_protocol_interface
from typing import SupportsIndex, SupportsAbs
# error: [revealed-type] "Revealed protocol interface: `{"method_member": MethodMember(`(self) -> bytes`), "x": AttributeMember(`int`), "y": PropertyMember { getter: `def y(self) -> str` }, "z": PropertyMember { getter: `def z(self) -> int`, setter: `def z(self, z: int) -> None` }}`"
reveal_protocol_interface(Foo)
# error: [revealed-type] "Revealed protocol interface: `{"__index__": MethodMember(`(self) -> int`)}`"
reveal_protocol_interface(SupportsIndex)
# error: [revealed-type] "Revealed protocol interface: `{"__abs__": MethodMember(`(self) -> _T_co`)}`"
reveal_protocol_interface(SupportsAbs)
# error: [invalid-argument-type] "Invalid argument to `reveal_protocol_interface`: Only protocol classes can be passed to `reveal_protocol_interface`"
reveal_protocol_interface(int)
# error: [invalid-argument-type] "Argument to function `reveal_protocol_interface` is incorrect: Expected `type`, found `Literal["foo"]`"
reveal_protocol_interface("foo")
# TODO: this should be a `revealed-type` diagnostic rather than `invalid-argument-type`, and it should reveal `{"__abs__": MethodMember(`(self) -> int`)}` for the protocol interface
#
# error: [invalid-argument-type] "Invalid argument to `reveal_protocol_interface`: Only protocol classes can be passed to `reveal_protocol_interface`"
reveal_protocol_interface(SupportsAbs[int])
```
Certain special attributes and methods are not considered protocol members at runtime, and should Certain special attributes and methods are not considered protocol members at runtime, and should
not be considered protocol members by type checkers either: not be considered protocol members by type checkers either:

View file

@ -2251,6 +2251,41 @@ pub(crate) fn report_bad_argument_to_get_protocol_members(
diagnostic.info("See https://typing.python.org/en/latest/spec/protocol.html#"); diagnostic.info("See https://typing.python.org/en/latest/spec/protocol.html#");
} }
pub(crate) fn report_bad_argument_to_protocol_interface(
context: &InferContext,
call: &ast::ExprCall,
param_type: Type,
) {
let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, call) else {
return;
};
let db = context.db();
let mut diagnostic = builder.into_diagnostic("Invalid argument to `reveal_protocol_interface`");
diagnostic
.set_primary_message("Only protocol classes can be passed to `reveal_protocol_interface`");
if let Some(class) = param_type.to_class_type(context.db()) {
let mut class_def_diagnostic = SubDiagnostic::new(
SubDiagnosticSeverity::Info,
format_args!(
"`{}` is declared here, but it is not a protocol class:",
class.name(db)
),
);
class_def_diagnostic.annotate(Annotation::primary(
class.class_literal(db).0.header_span(db),
));
diagnostic.sub(class_def_diagnostic);
}
diagnostic.info(
"A class is only a protocol class if it directly inherits \
from `typing.Protocol` or `typing_extensions.Protocol`",
);
// See TODO in `report_bad_argument_to_get_protocol_members` above
diagnostic.info("See https://typing.python.org/en/latest/spec/protocol.html");
}
pub(crate) fn report_invalid_arguments_to_callable( pub(crate) fn report_invalid_arguments_to_callable(
context: &InferContext, context: &InferContext,
subscript: &ast::ExprSubscript, subscript: &ast::ExprSubscript,

View file

@ -68,7 +68,7 @@ use crate::types::call::{Binding, CallArguments};
use crate::types::context::InferContext; use crate::types::context::InferContext;
use crate::types::diagnostic::{ use crate::types::diagnostic::{
REDUNDANT_CAST, STATIC_ASSERT_ERROR, TYPE_ASSERTION_FAILURE, REDUNDANT_CAST, STATIC_ASSERT_ERROR, TYPE_ASSERTION_FAILURE,
report_bad_argument_to_get_protocol_members, report_bad_argument_to_get_protocol_members, report_bad_argument_to_protocol_interface,
report_runtime_check_against_non_runtime_checkable_protocol, report_runtime_check_against_non_runtime_checkable_protocol,
}; };
use crate::types::generics::{GenericContext, walk_generic_context}; use crate::types::generics::{GenericContext, walk_generic_context};
@ -1093,6 +1093,8 @@ pub enum KnownFunction {
TopMaterialization, TopMaterialization,
/// `ty_extensions.bottom_materialization` /// `ty_extensions.bottom_materialization`
BottomMaterialization, BottomMaterialization,
/// `ty_extensions.reveal_protocol_interface`
RevealProtocolInterface,
} }
impl KnownFunction { impl KnownFunction {
@ -1158,6 +1160,7 @@ impl KnownFunction {
| Self::EnumMembers | Self::EnumMembers
| Self::StaticAssert | Self::StaticAssert
| Self::HasMember | Self::HasMember
| Self::RevealProtocolInterface
| Self::AllMembers => module.is_ty_extensions(), | Self::AllMembers => module.is_ty_extensions(),
Self::ImportModule => module.is_importlib(), Self::ImportModule => module.is_importlib(),
} }
@ -1350,6 +1353,33 @@ impl KnownFunction {
report_bad_argument_to_get_protocol_members(context, call_expression, *class); report_bad_argument_to_get_protocol_members(context, call_expression, *class);
} }
KnownFunction::RevealProtocolInterface => {
let [Some(param_type)] = parameter_types else {
return;
};
let Some(protocol_class) = param_type
.into_class_literal()
.and_then(|class| class.into_protocol_class(db))
else {
report_bad_argument_to_protocol_interface(
context,
call_expression,
*param_type,
);
return;
};
if let Some(builder) =
context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)
{
let mut diag = builder.into_diagnostic("Revealed protocol interface");
let span = context.span(&call_expression.arguments.args[0]);
diag.annotate(Annotation::primary(span).message(format_args!(
"`{}`",
protocol_class.interface(db).display(db)
)));
}
}
KnownFunction::IsInstance | KnownFunction::IsSubclass => { KnownFunction::IsInstance | KnownFunction::IsSubclass => {
let [Some(first_arg), Some(Type::ClassLiteral(class))] = parameter_types else { let [Some(first_arg), Some(Type::ClassLiteral(class))] = parameter_types else {
return; return;
@ -1463,6 +1493,7 @@ pub(crate) mod tests {
| KnownFunction::TopMaterialization | KnownFunction::TopMaterialization
| KnownFunction::BottomMaterialization | KnownFunction::BottomMaterialization
| KnownFunction::HasMember | KnownFunction::HasMember
| KnownFunction::RevealProtocolInterface
| KnownFunction::AllMembers => KnownModule::TyExtensions, | KnownFunction::AllMembers => KnownModule::TyExtensions,
KnownFunction::ImportModule => KnownModule::ImportLib, KnownFunction::ImportModule => KnownModule::ImportLib,

View file

@ -1,3 +1,4 @@
use std::fmt::Write;
use std::{collections::BTreeMap, ops::Deref}; use std::{collections::BTreeMap, ops::Deref};
use itertools::Itertools; use itertools::Itertools;
@ -215,6 +216,31 @@ impl<'db> ProtocolInterface<'db> {
data.find_legacy_typevars(db, typevars); data.find_legacy_typevars(db, typevars);
} }
} }
pub(super) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display {
struct ProtocolInterfaceDisplay<'db> {
db: &'db dyn Db,
interface: ProtocolInterface<'db>,
}
impl std::fmt::Display for ProtocolInterfaceDisplay<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_char('{')?;
for (i, (name, data)) in self.interface.inner(self.db).iter().enumerate() {
write!(f, "\"{name}\": {data}", data = data.display(self.db))?;
if i < self.interface.inner(self.db).len() - 1 {
f.write_str(", ")?;
}
}
f.write_char('}')
}
}
ProtocolInterfaceDisplay {
db,
interface: self,
}
}
} }
#[derive(Debug, PartialEq, Eq, Clone, Hash, salsa::Update)] #[derive(Debug, PartialEq, Eq, Clone, Hash, salsa::Update)]
@ -256,6 +282,41 @@ impl<'db> ProtocolMemberData<'db> {
qualifiers: self.qualifiers, qualifiers: self.qualifiers,
} }
} }
fn display(&self, db: &'db dyn Db) -> impl std::fmt::Display {
struct ProtocolMemberDataDisplay<'db> {
db: &'db dyn Db,
data: ProtocolMemberKind<'db>,
}
impl std::fmt::Display for ProtocolMemberDataDisplay<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.data {
ProtocolMemberKind::Method(callable) => {
write!(f, "MethodMember(`{}`)", callable.display(self.db))
}
ProtocolMemberKind::Property(property) => {
let mut d = f.debug_struct("PropertyMember");
if let Some(getter) = property.getter(self.db) {
d.field("getter", &format_args!("`{}`", &getter.display(self.db)));
}
if let Some(setter) = property.setter(self.db) {
d.field("setter", &format_args!("`{}`", &setter.display(self.db)));
}
d.finish()
}
ProtocolMemberKind::Other(ty) => {
write!(f, "AttributeMember(`{}`)", ty.display(self.db))
}
}
}
}
ProtocolMemberDataDisplay {
db,
data: self.kind,
}
}
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq, salsa::Update, Hash)] #[derive(Debug, Copy, Clone, PartialEq, Eq, salsa::Update, Hash)]

View file

@ -64,3 +64,8 @@ def all_members(obj: Any) -> tuple[str, ...]: ...
# Returns `True` if the given object has a member with the given name. # Returns `True` if the given object has a member with the given name.
def has_member(obj: Any, name: str) -> bool: ... def has_member(obj: Any, name: str) -> bool: ...
# Passing a protocol type to this function will cause ty to emit an info-level
# diagnostic describing the protocol's interface. Passing a non-protocol type
# will cause ty to emit an error diagnostic.
def reveal_protocol_interface(protocol: type) -> None: ...