mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34:57 +00:00
[ty] Improve debuggability of protocol types (#19662)
This commit is contained in:
parent
57e2e8664f
commit
e7e7b7bf21
5 changed files with 158 additions and 1 deletions
|
@ -382,6 +382,31 @@ class Foo(Protocol):
|
|||
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
|
||||
not be considered protocol members by type checkers either:
|
||||
|
||||
|
|
|
@ -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#");
|
||||
}
|
||||
|
||||
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(
|
||||
context: &InferContext,
|
||||
subscript: &ast::ExprSubscript,
|
||||
|
|
|
@ -68,7 +68,7 @@ use crate::types::call::{Binding, CallArguments};
|
|||
use crate::types::context::InferContext;
|
||||
use crate::types::diagnostic::{
|
||||
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,
|
||||
};
|
||||
use crate::types::generics::{GenericContext, walk_generic_context};
|
||||
|
@ -1093,6 +1093,8 @@ pub enum KnownFunction {
|
|||
TopMaterialization,
|
||||
/// `ty_extensions.bottom_materialization`
|
||||
BottomMaterialization,
|
||||
/// `ty_extensions.reveal_protocol_interface`
|
||||
RevealProtocolInterface,
|
||||
}
|
||||
|
||||
impl KnownFunction {
|
||||
|
@ -1158,6 +1160,7 @@ impl KnownFunction {
|
|||
| Self::EnumMembers
|
||||
| Self::StaticAssert
|
||||
| Self::HasMember
|
||||
| Self::RevealProtocolInterface
|
||||
| Self::AllMembers => module.is_ty_extensions(),
|
||||
Self::ImportModule => module.is_importlib(),
|
||||
}
|
||||
|
@ -1350,6 +1353,33 @@ impl KnownFunction {
|
|||
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 => {
|
||||
let [Some(first_arg), Some(Type::ClassLiteral(class))] = parameter_types else {
|
||||
return;
|
||||
|
@ -1463,6 +1493,7 @@ pub(crate) mod tests {
|
|||
| KnownFunction::TopMaterialization
|
||||
| KnownFunction::BottomMaterialization
|
||||
| KnownFunction::HasMember
|
||||
| KnownFunction::RevealProtocolInterface
|
||||
| KnownFunction::AllMembers => KnownModule::TyExtensions,
|
||||
|
||||
KnownFunction::ImportModule => KnownModule::ImportLib,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use std::fmt::Write;
|
||||
use std::{collections::BTreeMap, ops::Deref};
|
||||
|
||||
use itertools::Itertools;
|
||||
|
@ -215,6 +216,31 @@ impl<'db> ProtocolInterface<'db> {
|
|||
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)]
|
||||
|
@ -256,6 +282,41 @@ impl<'db> ProtocolMemberData<'db> {
|
|||
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)]
|
||||
|
|
|
@ -64,3 +64,8 @@ def all_members(obj: Any) -> tuple[str, ...]: ...
|
|||
|
||||
# Returns `True` if the given object has a member with the given name.
|
||||
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: ...
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue