Skip to content

Conversation

@AlexWaygood
Copy link
Member

@AlexWaygood AlexWaygood commented Apr 22, 2025

Summary

Passing a non-protocol class to get_protocol_members fails at runtime. This PR adds logic to red-knot so that we detect that error and warn the user about it.

Stacked on top of #17550

Test Plan

Existing mdtests updated and extended. Snapshots added!

Screenshot of what the diagnostics look like in the terminal:
image

@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Apr 22, 2025
@AlexWaygood AlexWaygood force-pushed the alex/get-protocol-members-validation branch from 8eac7ad to 9255038 Compare April 22, 2025 13:57
@github-actions
Copy link
Contributor

github-actions bot commented Apr 22, 2025

mypy_primer results

No ecosystem changes detected ✅

@AlexWaygood
Copy link
Member Author

I don't know why the ruff (not red-knot!) ecosystem check is failing, but it doesn't seem related to this PR.

@dhruvmanila
Copy link
Member

I don't know why the ruff (not red-knot!) ecosystem check is failing, but it doesn't seem related to this PR.

It's because you're using stacked PRs, once the PR that's below this one has the ecosystem check completed, you can re-run here so that it can find the base ecosystem report which would be part of that PR.

@AlexWaygood AlexWaygood force-pushed the alex/protocol-member-tests branch from 801147a to abc9ff6 Compare April 22, 2025 16:40
@AlexWaygood AlexWaygood force-pushed the alex/get-protocol-members-validation branch from 9255038 to 0d68ffd Compare April 22, 2025 16:44
@AlexWaygood
Copy link
Member Author

I don't know why the ruff (not red-knot!) ecosystem check is failing, but it doesn't seem related to this PR.

It's because you're using stacked PRs, once the PR that's below this one has the ecosystem check completed, you can re-run here so that it can find the base ecosystem report which would be part of that PR.

Hrm, I've tried rerunning the job several times now (and the PR below this one has definitely completed its CI!).

I think it's possibly because the PR below this one (#17550) had the Linux build skipped entirely in its CI run? I'm not sure why -- PRs that only change .md files don't seem to have any tests run on them right now, which is sorta problematic for red-knot PRs 😆

@dhruvmanila
Copy link
Member

I think it's possibly because the PR below this one (#17550) had the Linux build skipped entirely in its CI run?

Ah, yeah that's probably the case. The Ruff ecosystem checks needs a base report to compare against. This is usually from main but as this is a stacked PR, it'll fetch it from the previous PR (which is correct) but that doesn't exists and so this workflow fails.

@AlexWaygood AlexWaygood force-pushed the alex/get-protocol-members-validation branch 3 times, most recently from ece0c86 to b3a1538 Compare April 22, 2025 19:35
@AlexWaygood AlexWaygood force-pushed the alex/get-protocol-members-validation branch from b3a1538 to e34384c Compare April 22, 2025 19:44
Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

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

Nice!

Comment on lines 1322 to 1360
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 `get_protocol_members`");
diagnostic.set_primary_message("This call will raise `TypeError` at runtime");
diagnostic.info("Only protocol classes can be passed to `get_protocol_members`");

let class_scope = class.body_scope(db);
let class_node = class_scope.node(db).expect_class();
let class_name = &class_node.name;
let class_def_diagnostic_range = TextRange::new(
class_name.start(),
class_node
.arguments
.as_deref()
.map(Ranged::end)
.unwrap_or_else(|| class_name.end()),
);
let mut class_def_diagnostic = SubDiagnostic::new(
Severity::Info,
format_args!("`{class_name}` is declared here, but it is not a protocol class:"),
);
class_def_diagnostic.annotate(Annotation::primary(
Span::from(class_scope.file(db)).with_range(class_def_diagnostic_range),
));
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`",
);
diagnostic.info("See https://typing.python.org/en/latest/spec/protocol.html#");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a lovely diagnostic, and a great example for what we can be doing with the new diagnostic framework in other cases -- but it's also a lot of code for an extremely narrow case (misuse of one specific function)! Not really suggesting we change anything here, just not sure it will be worth investing this much in every narrow edge-case diagnostic...

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I know what you mean.

I think there might be ways to make the diagnostic APIs a little less verbose? The code here isn't that complicated, just a little boilerplate-y. And I think the way you get a reputation for having "excellent error messages" like rustc is by paying attention to lots of little details like this.

Base automatically changed from alex/protocol-member-tests to main April 23, 2025 10:03
@AlexWaygood AlexWaygood force-pushed the alex/get-protocol-members-validation branch from e34384c to 0c975b4 Compare April 23, 2025 10:09
@AlexWaygood AlexWaygood enabled auto-merge (squash) April 23, 2025 10:10
@AlexWaygood AlexWaygood merged commit 0a1f9d0 into main Apr 23, 2025
30 checks passed
@AlexWaygood AlexWaygood deleted the alex/get-protocol-members-validation branch April 23, 2025 10:13
dcreager added a commit that referenced this pull request Apr 23, 2025
* main: (28 commits)
  [red-knot] Make `BoundMethodType` a salsa interned (#17581)
  [red-knot] Emit a diagnostic if a non-protocol is passed to `get_protocol_members` (#17551)
  [red-knot] Add more tests for protocol members (#17550)
  [red-knot] Assignability for subclasses of `Any` and `Unknown` (#17557)
  [red-knot] mypy_primer: add strawberry, print compilation errors to stderr (#17578)
  [red-knot] GenericAlias instances as a base class (#17575)
  Remove redundant `type_to_visitor_function` entries (#17564)
  Fixes how the checker visits `typing.cast`/`typing.NewType` arguments (#17538)
  [red-knot] Class literal `__new__` function callable subtyping (#17533)
  [red-knot] Surround intersections with `()` in potentially ambiguous contexts (#17568)
  [minor] Delete outdated TODO comment (#17565)
  [red-knot] add regression test for fixed cycle panic (#17535)
  [red-knot] fix unions of literals, again (#17534)
  red_knot_python_semantic: remove last vestige of old diagnostics!
  red_knot_python_semantic: migrate `types` to new diagnostics
  red_knot_python_semantic: migrate `types/diagnostic` to new diagnostics
  red_knot_python_semantic: migrate `types/call/bind` to new diagnostics
  red_knot_python_semantic: migrate `types/string_annotation` to new diagnostics
  red_knot_python_semantic: migrate `types/infer` to new diagnostic model
  red_knot_python_semantic: migrate INVALID_ASSIGNMENT for inference
  ...
dcreager added a commit that referenced this pull request Apr 23, 2025
…var-instance

* dcreager/generic-constructor: (29 commits)
  We don't need this
  [red-knot] Make `BoundMethodType` a salsa interned (#17581)
  [red-knot] Emit a diagnostic if a non-protocol is passed to `get_protocol_members` (#17551)
  [red-knot] Add more tests for protocol members (#17550)
  [red-knot] Assignability for subclasses of `Any` and `Unknown` (#17557)
  [red-knot] mypy_primer: add strawberry, print compilation errors to stderr (#17578)
  [red-knot] GenericAlias instances as a base class (#17575)
  Remove redundant `type_to_visitor_function` entries (#17564)
  Fixes how the checker visits `typing.cast`/`typing.NewType` arguments (#17538)
  [red-knot] Class literal `__new__` function callable subtyping (#17533)
  [red-knot] Surround intersections with `()` in potentially ambiguous contexts (#17568)
  [minor] Delete outdated TODO comment (#17565)
  [red-knot] add regression test for fixed cycle panic (#17535)
  [red-knot] fix unions of literals, again (#17534)
  red_knot_python_semantic: remove last vestige of old diagnostics!
  red_knot_python_semantic: migrate `types` to new diagnostics
  red_knot_python_semantic: migrate `types/diagnostic` to new diagnostics
  red_knot_python_semantic: migrate `types/call/bind` to new diagnostics
  red_knot_python_semantic: migrate `types/string_annotation` to new diagnostics
  red_knot_python_semantic: migrate `types/infer` to new diagnostic model
  ...
@carljm carljm mentioned this pull request Apr 24, 2025
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants