Skip to content

Commit e7e7b7b

Browse files
authored
[ty] Improve debuggability of protocol types (#19662)
1 parent 57e2e86 commit e7e7b7b

File tree

5 files changed

+158
-1
lines changed

5 files changed

+158
-1
lines changed

crates/ty_python_semantic/resources/mdtest/protocols.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,31 @@ class Foo(Protocol):
382382
reveal_type(get_protocol_members(Foo)) # revealed: frozenset[Literal["method_member", "x", "y", "z"]]
383383
```
384384

385+
To see the kinds and types of the protocol members, you can use the debugging aid
386+
`ty_extensions.reveal_protocol_interface`, meanwhile:
387+
388+
```py
389+
from ty_extensions import reveal_protocol_interface
390+
from typing import SupportsIndex, SupportsAbs
391+
392+
# 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` }}`"
393+
reveal_protocol_interface(Foo)
394+
# error: [revealed-type] "Revealed protocol interface: `{"__index__": MethodMember(`(self) -> int`)}`"
395+
reveal_protocol_interface(SupportsIndex)
396+
# error: [revealed-type] "Revealed protocol interface: `{"__abs__": MethodMember(`(self) -> _T_co`)}`"
397+
reveal_protocol_interface(SupportsAbs)
398+
399+
# error: [invalid-argument-type] "Invalid argument to `reveal_protocol_interface`: Only protocol classes can be passed to `reveal_protocol_interface`"
400+
reveal_protocol_interface(int)
401+
# error: [invalid-argument-type] "Argument to function `reveal_protocol_interface` is incorrect: Expected `type`, found `Literal["foo"]`"
402+
reveal_protocol_interface("foo")
403+
404+
# 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
405+
#
406+
# error: [invalid-argument-type] "Invalid argument to `reveal_protocol_interface`: Only protocol classes can be passed to `reveal_protocol_interface`"
407+
reveal_protocol_interface(SupportsAbs[int])
408+
```
409+
385410
Certain special attributes and methods are not considered protocol members at runtime, and should
386411
not be considered protocol members by type checkers either:
387412

crates/ty_python_semantic/src/types/diagnostic.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2251,6 +2251,41 @@ pub(crate) fn report_bad_argument_to_get_protocol_members(
22512251
diagnostic.info("See https://typing.python.org/en/latest/spec/protocol.html#");
22522252
}
22532253

2254+
pub(crate) fn report_bad_argument_to_protocol_interface(
2255+
context: &InferContext,
2256+
call: &ast::ExprCall,
2257+
param_type: Type,
2258+
) {
2259+
let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, call) else {
2260+
return;
2261+
};
2262+
let db = context.db();
2263+
let mut diagnostic = builder.into_diagnostic("Invalid argument to `reveal_protocol_interface`");
2264+
diagnostic
2265+
.set_primary_message("Only protocol classes can be passed to `reveal_protocol_interface`");
2266+
2267+
if let Some(class) = param_type.to_class_type(context.db()) {
2268+
let mut class_def_diagnostic = SubDiagnostic::new(
2269+
SubDiagnosticSeverity::Info,
2270+
format_args!(
2271+
"`{}` is declared here, but it is not a protocol class:",
2272+
class.name(db)
2273+
),
2274+
);
2275+
class_def_diagnostic.annotate(Annotation::primary(
2276+
class.class_literal(db).0.header_span(db),
2277+
));
2278+
diagnostic.sub(class_def_diagnostic);
2279+
}
2280+
2281+
diagnostic.info(
2282+
"A class is only a protocol class if it directly inherits \
2283+
from `typing.Protocol` or `typing_extensions.Protocol`",
2284+
);
2285+
// See TODO in `report_bad_argument_to_get_protocol_members` above
2286+
diagnostic.info("See https://typing.python.org/en/latest/spec/protocol.html");
2287+
}
2288+
22542289
pub(crate) fn report_invalid_arguments_to_callable(
22552290
context: &InferContext,
22562291
subscript: &ast::ExprSubscript,

crates/ty_python_semantic/src/types/function.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ use crate::types::call::{Binding, CallArguments};
6868
use crate::types::context::InferContext;
6969
use crate::types::diagnostic::{
7070
REDUNDANT_CAST, STATIC_ASSERT_ERROR, TYPE_ASSERTION_FAILURE,
71-
report_bad_argument_to_get_protocol_members,
71+
report_bad_argument_to_get_protocol_members, report_bad_argument_to_protocol_interface,
7272
report_runtime_check_against_non_runtime_checkable_protocol,
7373
};
7474
use crate::types::generics::{GenericContext, walk_generic_context};
@@ -1093,6 +1093,8 @@ pub enum KnownFunction {
10931093
TopMaterialization,
10941094
/// `ty_extensions.bottom_materialization`
10951095
BottomMaterialization,
1096+
/// `ty_extensions.reveal_protocol_interface`
1097+
RevealProtocolInterface,
10961098
}
10971099

10981100
impl KnownFunction {
@@ -1158,6 +1160,7 @@ impl KnownFunction {
11581160
| Self::EnumMembers
11591161
| Self::StaticAssert
11601162
| Self::HasMember
1163+
| Self::RevealProtocolInterface
11611164
| Self::AllMembers => module.is_ty_extensions(),
11621165
Self::ImportModule => module.is_importlib(),
11631166
}
@@ -1350,6 +1353,33 @@ impl KnownFunction {
13501353
report_bad_argument_to_get_protocol_members(context, call_expression, *class);
13511354
}
13521355

1356+
KnownFunction::RevealProtocolInterface => {
1357+
let [Some(param_type)] = parameter_types else {
1358+
return;
1359+
};
1360+
let Some(protocol_class) = param_type
1361+
.into_class_literal()
1362+
.and_then(|class| class.into_protocol_class(db))
1363+
else {
1364+
report_bad_argument_to_protocol_interface(
1365+
context,
1366+
call_expression,
1367+
*param_type,
1368+
);
1369+
return;
1370+
};
1371+
if let Some(builder) =
1372+
context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)
1373+
{
1374+
let mut diag = builder.into_diagnostic("Revealed protocol interface");
1375+
let span = context.span(&call_expression.arguments.args[0]);
1376+
diag.annotate(Annotation::primary(span).message(format_args!(
1377+
"`{}`",
1378+
protocol_class.interface(db).display(db)
1379+
)));
1380+
}
1381+
}
1382+
13531383
KnownFunction::IsInstance | KnownFunction::IsSubclass => {
13541384
let [Some(first_arg), Some(Type::ClassLiteral(class))] = parameter_types else {
13551385
return;
@@ -1463,6 +1493,7 @@ pub(crate) mod tests {
14631493
| KnownFunction::TopMaterialization
14641494
| KnownFunction::BottomMaterialization
14651495
| KnownFunction::HasMember
1496+
| KnownFunction::RevealProtocolInterface
14661497
| KnownFunction::AllMembers => KnownModule::TyExtensions,
14671498

14681499
KnownFunction::ImportModule => KnownModule::ImportLib,

crates/ty_python_semantic/src/types/protocol_class.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::fmt::Write;
12
use std::{collections::BTreeMap, ops::Deref};
23

34
use itertools::Itertools;
@@ -215,6 +216,31 @@ impl<'db> ProtocolInterface<'db> {
215216
data.find_legacy_typevars(db, typevars);
216217
}
217218
}
219+
220+
pub(super) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display {
221+
struct ProtocolInterfaceDisplay<'db> {
222+
db: &'db dyn Db,
223+
interface: ProtocolInterface<'db>,
224+
}
225+
226+
impl std::fmt::Display for ProtocolInterfaceDisplay<'_> {
227+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228+
f.write_char('{')?;
229+
for (i, (name, data)) in self.interface.inner(self.db).iter().enumerate() {
230+
write!(f, "\"{name}\": {data}", data = data.display(self.db))?;
231+
if i < self.interface.inner(self.db).len() - 1 {
232+
f.write_str(", ")?;
233+
}
234+
}
235+
f.write_char('}')
236+
}
237+
}
238+
239+
ProtocolInterfaceDisplay {
240+
db,
241+
interface: self,
242+
}
243+
}
218244
}
219245

220246
#[derive(Debug, PartialEq, Eq, Clone, Hash, salsa::Update)]
@@ -256,6 +282,41 @@ impl<'db> ProtocolMemberData<'db> {
256282
qualifiers: self.qualifiers,
257283
}
258284
}
285+
286+
fn display(&self, db: &'db dyn Db) -> impl std::fmt::Display {
287+
struct ProtocolMemberDataDisplay<'db> {
288+
db: &'db dyn Db,
289+
data: ProtocolMemberKind<'db>,
290+
}
291+
292+
impl std::fmt::Display for ProtocolMemberDataDisplay<'_> {
293+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294+
match self.data {
295+
ProtocolMemberKind::Method(callable) => {
296+
write!(f, "MethodMember(`{}`)", callable.display(self.db))
297+
}
298+
ProtocolMemberKind::Property(property) => {
299+
let mut d = f.debug_struct("PropertyMember");
300+
if let Some(getter) = property.getter(self.db) {
301+
d.field("getter", &format_args!("`{}`", &getter.display(self.db)));
302+
}
303+
if let Some(setter) = property.setter(self.db) {
304+
d.field("setter", &format_args!("`{}`", &setter.display(self.db)));
305+
}
306+
d.finish()
307+
}
308+
ProtocolMemberKind::Other(ty) => {
309+
write!(f, "AttributeMember(`{}`)", ty.display(self.db))
310+
}
311+
}
312+
}
313+
}
314+
315+
ProtocolMemberDataDisplay {
316+
db,
317+
data: self.kind,
318+
}
319+
}
259320
}
260321

261322
#[derive(Debug, Copy, Clone, PartialEq, Eq, salsa::Update, Hash)]

crates/ty_vendored/ty_extensions/ty_extensions.pyi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,8 @@ def all_members(obj: Any) -> tuple[str, ...]: ...
6464

6565
# Returns `True` if the given object has a member with the given name.
6666
def has_member(obj: Any, name: str) -> bool: ...
67+
68+
# Passing a protocol type to this function will cause ty to emit an info-level
69+
# diagnostic describing the protocol's interface. Passing a non-protocol type
70+
# will cause ty to emit an error diagnostic.
71+
def reveal_protocol_interface(protocol: type) -> None: ...

0 commit comments

Comments
 (0)