-
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] Pure instance variables declared in class body #15515
Conversation
915cc09
to
77cd8c3
Compare
This comment was marked as resolved.
This comment was marked as resolved.
c52a570
to
ca07032
Compare
# TODO: should be `str` | ||
reveal_type(c_instance.variable_with_class_default) # revealed: @Todo(instance attributes) | ||
reveal_type(c_instance.variable_with_class_default1) # revealed: str | ||
reveal_type(c_instance.variable_with_class_default2) # revealed: Unknown | Literal[1] |
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 correct that we want Unknown | Literal[1]
for the instance attribute access, but `Literal[1] for the class variable access? (above)
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.
No, I think we should probably add the union with Unknown
for class attributes also; the same principles apply. (Though perhaps less strongly in practice, since mutation of class attributes is much less common -- but pyright and mypy both allow it, and we should too since it's allowed at runtime.)
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.
Ok, I added an additional test with a TODO for this case.
reveal_type((2).bit_length) # revealed: @Todo(bound method) | ||
reveal_type((2).denominator) # revealed: @Todo(@property) |
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.
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 don't see a problem with landing this as-is.
# TODO: should be `str` | ||
reveal_type(c_instance.variable_with_class_default) # revealed: @Todo(instance attributes) | ||
reveal_type(c_instance.variable_with_class_default1) # revealed: str | ||
reveal_type(c_instance.variable_with_class_default2) # revealed: Unknown | Literal[1] |
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.
No, I think we should probably add the union with Unknown
for class attributes also; the same principles apply. (Though perhaps less strongly in practice, since mutation of class attributes is much less common -- but pyright and mypy both allow it, and we should too since it's allowed at runtime.)
@@ -2308,6 +2305,7 @@ impl<'db> Type<'db> { | |||
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::BareAnnotated], | |||
fallback_type: Type::unknown(), | |||
}), | |||
Type::KnownInstance(KnownInstanceType::ClassVar) => Ok(Type::unknown()), |
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.
This should be a TODO, no?
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.
Not exactly sure what you meant by that comment, but I added the following TODO comment:
// TODO: A bare `ClassVar` should rather be treated as if the symbol was not
// declared at all.
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.
Sorry, I should have spelled out what (in my mind) the TODO is. I don't think the added comment is quite right, because a bare ClassVar
is not equivalent to "not annotated". It means "this attribute is a pure ClassVar (that is, not settable on instances) but its declared type must be inferred from the RHS and unioned with Unknown." The latter part of this is equivalent to "not annotated" but the former part is not.
ca07032
to
f46cefd
Compare
f46cefd
to
b8c920a
Compare
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.
Very nice!
// TODO: Eventually, we are going to process all decorators correctly. This is | ||
// just a temporary heuristic to provide a broad categorization into properties | ||
// and non-property methods. | ||
if function.has_decorator(db, KnownClass::Property.to_class_literal(db)) { | ||
todo_type!("@property").into() | ||
} else { | ||
todo_type!("bound method").into() | ||
} |
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 really hopeful that we can keep our special-casing of properties to a minimum if we have a solid implementation of the descriptor protocol, since they really are just descriptor instances at the end of the day. But this seems fine for now, since properties are by far the most common descriptors around, and this improves our TODO messages, making it clear why we infer bad types for various @property
attributes for the time being
* main: [red-knot] Inline `SubclassOfType::as_instance_type_of_metaclass()` (#15556) [`flake8-comprehensions`] strip parentheses around generators in `unnecessary-generator-set` (`C401`) (#15553) [`pylint`] Implement `redefined-slots-in-subclass` (`W0244`) (#9640) [`flake8-bugbear`] Do not raise error if keyword argument is present and target-python version is less or equals than 3.9 (`B903`) (#15549) [red-knot] `type[T]` is disjoint from `type[S]` if the metaclass of `T` is disjoint from the metaclass of `S` (#15547) [red-knot] Pure instance variables declared in class body (#15515) Update snapshots of #15507 with new annotated snipetts rendering (#15546) [`pylint`] Do not report methods with only one `EM101`-compatible `raise` (`PLR6301`) (#15507) Fix unstable f-string formatting for expressions containing a trailing comma (#15545) Support `knot.toml` files in project discovery (#15505) Add support for configuring knot in `pyproject.toml` files (#15493) Fix bracket spacing for single-element tuples in f-string expressions (#15537) [`flake8-simplify`] Do not emit diagnostics for expressions inside string type annotations (`SIM222`, `SIM223`) (#15405) [`flake8-pytest-style`] Do not emit diagnostics for empty `for` loops (`PT012`, `PT031`) (#15542)
Summary
This is a small, tentative step towards the bigger goal of understanding instance attributes. I could imagine that this might not be desirable as a (mergeable) first iteration, as it could potentially turn some
@Todo(instance attributes)
types into concrete types that are not correct. For example, we currently turn the instance attribute access to avariable: ClassVar[str]
intostr
without raising any diagnostic (though I'm not sure ifUnknown
would be better here thanstr
). So I'm also fine with keeping this open untilClassVar
support is also implemented.What the PR currently does is the following:
property
as a known class to query for@property
decorators@Todo(instance attributes)
cases into sub-categories.Test Plan
Modified existing MD tests.