-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[ty] Extend tuple __len__ and __bool__ special casing to also cover tuple subclasses
#19289
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
Conversation
|
| def __bool__(self) -> Literal[True]: | ||
| return True |
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.
Should this not also be a Liskov violation?
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.
Okay i guess its not a Liskov violation, but should we not complain at this?
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.
Ah sorry I retract this, my bad.
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.
Okay changed my mind slightly
def foo(x: tuple[int, ...]):
if x:
# Should we not also be able to say len(x) != 0
# *This should pass
assert len(x) != 0
foo(Baz(()))In rust if you have a len function clippy tells you to have an is_empty function and auto generates to self.len() == 0.
This maybe isn't relevant, but it seems like we shouldn't allow override of just __bool__. I'm not sure where else to go with this to be honest.
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 that's a great point! It seems like in general, as part of our Liskov implementation, we should probably enforce that __bool__ and __len__ are never overridden to be inconsistent with each other on any Sequence subtype. I think otherwise this could indeed cause us to make incorrect assumptions, not just regarding tuples
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.
Cool thanks, so in this case we could emit a diagnostic saying that there should be a ’len’ method that returns ’int & ~Literal[0]’ (or something like that).
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.
Hmm, not sure about that specifically -- I guess I'm not sure we should emit an error on this specific tuple subclass. It seems a bit pedantic -- just because the __len__ method is annotated as returning int doesn't mean that the class is violating the contract of Sequences. It just means that we haven't been given enough information to verify; I think it's probably better to steer clear of false positives in that situation and assume the user knows what they're doing. It's not the goal of ty to catch every possible error that a user could make.
But I do think it's worth emitting errors on these classes, because here we do have enough information to say that __len__ and __bool__ are inconsistent with each other:
from collections import Sequence
from typing import Literal
class Foo(Sequence[int]):
def __len__(self) -> Literal[0]:
return 0
def __bool__(self) -> Literal[True]:
return True
class Bar(Sequence[int]):
def __len__(self) -> Literal[3]:
return 3
def __bool__(self) -> Literal[False]:
return FalseEven here, though, it's questionable whether this is a typing error or more something that's in the domain of a type-aware linter to check. (That doesn't mean that it wouldn't be a useful rule -- I think it would! But it might not be in the core purview of ty right now -- it might be more something for the future, when we start to use ty to power type-aware lint rules in Ruff.)
So I guess I feel like you're raising a great point in general, but that for these specific cases we probably shouldn't emit errors, and even if we should it maybe shouldn't be ty itself that emits these errors but maybe a type-aware linter built on top of ty :-)
sharkdp
left a comment
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!
crates/ty_python_semantic/resources/mdtest/expression/boolean.md
Outdated
Show resolved
Hide resolved
crates/ty_python_semantic/resources/mdtest/expression/boolean.md
Outdated
Show resolved
Hide resolved
Co-authored-by: David Peter <sharkdp@users.noreply.github.com>
Co-authored-by: David Peter <sharkdp@users.noreply.github.com>
0cae67b to
a92ea02
Compare
* main: (25 commits) [ty] Sync vendored typeshed stubs (#19461) [ty] Extend tuple `__len__` and `__bool__` special casing to also cover tuple subclasses (#19289) [ty] bump docstring-adder pin (#19458) [ty] Disallow assignment to `Final` class attributes (#19457) Update dependency ruff to v0.12.4 (#19442) Update pre-commit hook astral-sh/ruff-pre-commit to v0.12.4 (#19443) Update rui314/setup-mold digest to 702b190 (#19441) Update taiki-e/install-action action to v2.56.19 (#19448) Update Rust crate strum_macros to v0.27.2 (#19447) Update Rust crate strum to v0.27.2 (#19446) Update Rust crate rand to v0.9.2 (#19444) Update Rust crate serde_json to v1.0.141 (#19445) Fix `unreachable` panic in parser (#19183) [`ruff`] Support byte strings (`RUF055`) (#18926) [ty] Avoid second lookup for `infer_maybe_standalone_expression` (#19439) [ty] Implemented "go to definition" support for import statements (#19428) [ty] Avoid secondary tree traversal to get call expression for keyword arguments (#19429) [ty] Add goto definition to playground (#19425) [ty] Add support for `@warnings.deprecated` (#19376) [ty] make `del x` force local resolution of `x` in the current scope (#19389) ...
Summary
This PR (a collaboration with @ntBre) extends our existing
__len__and__bool__special casing so that it also works with tuple subclasses. I.e., for a class likeFoohere, we will now understand that instances ofFooare always truthy, and that callinglen()on an instance ofFoowill always returnLiteral[1]:The approach taken is to synthesize
__bool__and__len__class attributes on the generic alias objecttuple[int].tuple[int].__bool__is inferred as a function-likeCallabletype with signature(self: tuple[int], /) -> Literal[True]; a similar approach is taken fortuple.__len__. This means that the correct signature for these methods is naturally inherited by subclasses; it means that protocol subtyping will work correctly for these subclasses; and it means that once we have implemented the Liskov Substitution Principle, unsound overrides of these methods on tuple subclasses will be naturally detected by ty.Test Plan
Mdtests
Co-authored-by: Brent Westbrook 36778786+ntBre@users.noreply.github.com