-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[red-knot] Eagerly normalize type[]
types
#15272
Conversation
|
7d6698c
to
6662dc4
Compare
305174f
to
f0f82f0
Compare
6662dc4
to
5765c8f
Compare
5765c8f
to
21960b4
Compare
What does this PR say about the problem discovered in #15271? Currently pub(super) fn report_invalid_assignment(..., declared_ty: Type, ...) {
match declared_ty {
Type::ClassLiteral(ClassLiteralType { class }) => {
context.report_lint(&INVALID_ASSIGNMENT, node, format_args!(
"Implicit shadowing of class `{}`; annotate to make it explicit if this is intentional",
class.name(context.db())));
}
// ...
}
} ...which seems to rely on the fact that a class definition always results in a from types import EllipsisType
class SomeClass: ...
# ^^^^^^^^^
# `declared_ty`: `ClassLiteral(SomeClass)`
# vvvvvvvvv `inferred_ty`: `InstanceOf(Type)` (irrelevant)
some_variable: type[EllipsisType] = type(...)
# ^^^^^^^^^^^^^^^^^^
# `declared_ty`: previously `SubclassOf(EllipsisType)`, now `ClassLiteral(EllipsisType)` No diagnostics should be emitted here, as |
cdb3a78
to
bee1e59
Compare
21960b4
to
999704a
Compare
Yeah, I'll investigate that a bit more tomorrow. My suspicion is that it's a pre-existing issue exposed by this PR, rather than a regression caused by this PR, per se. But anyway, it's late here, so a full investigation shall have to wait :-) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't finished reviewing, but have to head out, so submitting what I have so far.
reveal_type(x) # revealed: Literal[Foo] | ||
reveal_type(y) # revealed: Literal[EllipsisType] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is probably not acceptable? That is, Literal[X]
for classes X
is already a notation we are inventing; I don't think we can round-trip explicit user annotations of type[X]
as Literal[X]
in our type display, even when they are equivalent.
We could always display Literal[X]
as type[X]
in general. The downside here is that then we are conflating two types that are meaningfully different in how we handle them internally, with identical text representations.
I'm not sure what the alternative is, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few points.
Firstly, we're far from unique in inventing our own notation for type display in some cases and using it in the output of reveal_type
. E.g. take a look at the mypy output here. There have been a couple of complaints about some of the stranger symbols in mypy's reveal_type
output being confusing, but I don't think it's really been a major issue at all. I can only remember one issue about it, and it's not highly upvoted.
Secondly, we're also far from unique in eagerly simplifying types that we see in user annotations and using the simplified types in our type display. We already do this elsewhere when we simplify unions (removing duplicate elements and elements that are subtypes of each other); so does pyright, to some extent.
Notwithstanding those two points, I have been thinking for a while that our current display for class-literal and function-literal types is a little confusing for users. It looks too close to something that would be valid in a type annotation in user code, and users will probably think that we're claiming that Literal[SomeClass]
is valid in a user type annotation. It'll be confusing for them when we then reject them using Literal[SomeClass]
in their own code.
Prior to this PR, I was wondering about Literal[<class 'SomeClass'>]
and Literal[<function 'some_function'>]
as an alternative display for class-literal and function-literal types. But your comments here make me wonder if we could also consider type[SomeClass(final=True)]
or type[SomeClass(@final=True)]
for class-literal types
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I agree that both displaying our own notation for some types, and simplifying user-provided types, are things we can do. It just feels a bit like crossing another line to combine those two things in such a way that we eagerly replace a spellable user-provided type with a non-spellable type display. But maybe this isn't actually a problem in practice. I'm OK with deferring that question from this PR, and at some point following up more holistically on type display. There are a lot of interesting questions here, including how we balance conciseness/readability vs clarity (to first time readers without needing to refer to docs) in the notations we choose. I won't get too deep into the details here, I'll just say that a) I kind of like the idea of representing ClassLiteral
types in some way using type[]
notation, since they are similar, and b) I'm not sure we should use the word "final" in that display, because I think it's too confusing in cases where we have a ClassLiteral
type but for a non-final class.
794078b
to
fc01124
Compare
…es of subtypes of `type[superclass]`
fc01124
to
72f8cec
Compare
Yes, it looks that way to me (though I haven't investigated it thoroughly). It looks like we probably need to actually check the reaching definitions, rather than just the declared type, in deciding whether to emit the special class-shadowing diagnostic. (Or we could just abandon that special case and just decide that the regular invalid-assignment diagnostic is adequate.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! I think it makes sense to go with this direction.
.subclass_of() | ||
.into_class() | ||
.is_some_and(|subclass_class| subclass_class.is_instance_of(db, target_class)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth pulling this into an SubclassOfType::is_instance_of
method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm reluctant to add a SubclassOfType::is_instance_of
method exactly, because I think there's an important-yet-subtle distinction that needs to be made here: the subtype-of relationship is a relationship between types (is this instance type a subtype of the other instance type? is this subclass-of type a subtype of that instance type?) but the subclass-of and instance-of relationships are relationships between runtime objects rather than types (is this class object an instance of that class object? is this instance an instance of that class object? is this class object a subclass of that class object?). So we can ask the question "are all inhabitants of <some SubclassOfType
> instances of <class X
>?", but I don't think we should allow ourselves to ask the question "is <some SubclassOfType
> an instance of <class X
>?"; it blurs the distinction between the concepts in an unfortunate way.
I suppose I already blurred the line recently by adding the KnownInstanceType::is_instance_of()
method:
ruff/crates/red_knot_python_semantic/src/types.rs
Lines 2769 to 2771 in 0dc00e6
pub fn is_instance_of(self, db: &'db dyn Db, class: Class<'db>) -> bool { | |
self.class().is_subclass_of(db, class) | |
} |
it felt okay because this type in particular only has exactly one inhabitant at runtime, so in this particular case the distinction between "the type" and "all inhabitants of the type" is really a distinction without a difference. Maybe it was still a mistake, though; maybe we should get rid of that method.
I'd be open to adding APIs such as SubclassOfType::inhabitants_are_instances_of()
and InstanceType::inhabitants_are_instances_of()
, but the longer names make it slightly more unwieldy. For now I'll leave it, but I'm happy to tackle it in a follow-up if you think it would be worth it!
Co-authored-by: Carl Meyer <[email protected]>
9be33e3
to
6baefda
Compare
* main: Use uv consistently throughout the documentation (#15302) [red-knot] Eagerly normalize `type[]` types (#15272) [`pyupgrade`] Split `UP007` to two individual rules for `Union` and `Optional` (`UP007`, `UP045`) (#15313) [red-knot] Improve symbol-lookup tracing (#14907) [red-knot] improve type shrinking coverage in red-knot property tests (#15297) [`flake8-return`] Recognize functions returning `Never` as non-returning (`RET503`) (#15298) [`flake8-bugbear`] Implement `class-as-data-structure` (`B903`) (#9601) Avoid treating newline-separated sections as sub-sections (#15311) Remove call when removing final argument from `format` (#15309) Don't enforce `object-without-hash-method` in stubs (#15310) Don't special-case class instances in binary expression inference (#15161) Upgrade zizmor to the latest version in CI (#15300)
Summary
This PR refactors
SubclassOfType
so that certaintype[]
types are eagerly normalized in our model when they are constructed:type[object]
is eagerly normalized toType::Instance(<builtins.type>)
rather thanType::SubclassOf(<builtins.object>)
type[<final class>]
is eagerly normalized toType::ClassLiteral(<final class>)
rather thanType::SubclassOf(<final class>)
This already allows us to remove several special cases that we carve out for
type[object]
, inType::is_equivalent_to()
andType::is_subtype_of()
. The logic applied in these special cases just now falls out naturally from our handling of instance types. The approach will also make it easier to apply knowledge of@final
classes toType::is_equivalent_to()
,Type::is_subtype_of()
andType::is_disjoint_from()
. Without this refactor, we'd need to carefully remember to check whether the wrapped class was@final
in every branch forType::SubclassOf
; with this change, however, it's no longer necessary to do so. Several TODO comments are therefore also removed as part of this PR.Details of implementation
The
SubclassOfType
struct is moved to a newtypes
submodule, so that it is impossible to construct instances of the struct from other modules without using the "constructor" function that eagerly normalizes types. The "constructor" function is calledSubclassOfType::from
, and is analogous to our existing methodsUnionType::from_elements
(which can return any variant ofType
, not justType::Union
) andTupleType::from_elements
(which can returnType::Never
as well asType::Tuple()
). I wasn't really sure what the best name was for the "constructor" function, though -- I initially usedSubclassOfType::new()
, but Clippy (reasonably) complained thatnew()
methods are meant to returnSelf
. Suggestions are welcome!Test Plan
Type::is_singleton()
, ensuring thattype[<final class>]
is understood to be a singleton typeQUICKCHECK_TESTS=200000 cargo test -p red_knot_python_semantic -- --ignored types::property_tests::stable
reveals no new property test failures