-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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
Can understand functools.total_ordering #7831
Conversation
Without realizing it, I made #7848 one day later to do exactly the same thing. Great minds think alike I guess 😝 I think the code in this PR is a better base to build on than the code in #7848, so once the questions above are answered and anything from my PR that isn't here is migrated over (see below), I think the way forward is to land this one and close mine. What I see missing from this version of the change is the checking for the existence of the |
mypy/plugins/functools.py
Outdated
if isinstance(root_method.type, CallableType): | ||
ret_type = root_method.type.ret_type | ||
else: | ||
ret_type = ctx.api.named_type('__builtins__.bool') |
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.
Shouldn't it just be an error if __lt__
(etc.) is not callable?
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.
If __lt__
is not callable, it's okay to either generate an error or to fall back to some default behavior.
mypy/plugins/functools.py
Outdated
for additional_op in _ORDERING_METHODS - {root}: | ||
if additional_op not in comparison_methods: |
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.
for additional_op in _ORDERING_METHODS - set(comparison_methods.keys())
is an alternative. I guess it's debatable whether it's clearer, but it does avoid a level of indentation...
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 like this better than using a set operation.
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.
Thanks for the PR! total_ordering
is a useful feature to support. Generally looks good; left various minor comments, mostly about various special cases.
mypy/plugins/functools.py
Outdated
|
||
comparison_methods = _analyze_class(ctx) | ||
if not comparison_methods: | ||
ctx.api.fail('must define at least one ordering operation: < > <= >=', ctx.reason) |
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.
For consistency, error message should start with a capital letter.
Maybe also mention total_ordering
in the error message?
mypy/plugins/functools.py
Outdated
auto_attribs_default: bool = False) -> None: | ||
"""Add dunder methods to classes decorated with functools.total_ordering.""" | ||
if ctx.api.options.python_version < (3, 2): | ||
ctx.api.fail("functools.total_ordering is not supported in Python 2 or 3.1", ctx.reason) |
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.
Nit: add double quotes around functools.total_ordering
for consistency.
mypy/plugins/functools.py
Outdated
cur_pos_arg = 0 | ||
other_arg = None | ||
for arg in method.arguments: | ||
if arg.kind == ARG_POS: |
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.
ARG_OPT
seems valid here as well (though pretty unusual).
ret_type = ctx.api.named_type('__builtins__.bool') | ||
for additional_op in _ORDERING_METHODS - {root}: | ||
if additional_op not in comparison_methods: | ||
args = [Argument(Var('other', other_type), other_type, None, ARG_POS)] |
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.
If other_type
is None
, the argument type shoud probably be AnyType
instead of None
.
mypy/plugins/functools.py
Outdated
if name in cls.names and name not in comparison_methods: | ||
node = cls.names[name].node | ||
if isinstance(node, FuncItem): | ||
comparison_methods[name] = node |
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.
What if the method is implemented using a variable (e.g. __lt__ = lambda ...
)? More generally, if the dunder method exists as any kind of node in the symbol table (not just FuncItem
), we probably don't want to generate a method.
Random idea: use Optional[FuncItem]
as the dictionary value type. If it's None
, the method is defined, but it's something unexpected. In this case it's okay to fall back to default types for the signature of a generated method.
Ord() <= 1 # E: Unsupported operand types for <= ("Ord" and "int") | ||
Ord() == 1 | ||
Ord() > 1 # E: Unsupported operand types for > ("Ord" and "int") | ||
Ord() >= 1 # E: Unsupported operand types for >= ("Ord" and "int") |
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.
Add reveal_type(Ord() <= Ord())
or similar to verify the type of the operation is as expected. Maybe use a non-bool
return type in some test case.
test-data/unit/check-functools.test
Outdated
[builtins fixtures/ops.pyi] | ||
[builtins fixtures/dict.pyi] | ||
|
||
[case testTotalOrderingEqLe] |
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.
The remaining tests are a bit verbose. Maybe simplify some of them and only test cases that are expected to be materially different from other test cases? Another option is to vary the test cases in some aspects. Here are some ideas (some of these could be separate tests as well):
- Omit the function type annotation in some test case (fall back to
Any
types). - Use a different argument type in some test case.
- Use an unusual return type in some test case, such as
str
. - Test non-callable
__lt__
. - Test method defined as a variable (
__lt__ = lambda: ...
, for example).
mypy/plugins/functools.py
Outdated
root_method = comparison_methods[root] | ||
other_type = _find_other_type(root_method) | ||
if isinstance(root_method.type, CallableType): | ||
ret_type = root_method.type.ret_type |
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.
The generated methods seem to always return bool
values at runtime. Copying of the return type thus seems incorrect. However, having inconsistent return types is problematic as well. Maybe use bool
as the return type if the existing methods use bool
, and otherwise fall back to Any
to avoid trouble.
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've implemented this but it wasn't totally simple. The return type of "bool" is unbound when the plugin sees it so I've had to do some matching that feel a bit hacky. Let me know if you think there's a better way of doing it.
mypy/plugins/functools.py
Outdated
if isinstance(root_method.type, CallableType): | ||
ret_type = root_method.type.ret_type | ||
else: | ||
ret_type = ctx.api.named_type('__builtins__.bool') |
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.
If __lt__
is not callable, it's okay to either generate an error or to fall back to some default behavior.
mypy/plugins/functools.py
Outdated
for additional_op in _ORDERING_METHODS - {root}: | ||
if additional_op not in comparison_methods: |
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 like this better than using a set operation.
All the comments are addressed so this should be ready for another round of review. |
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 have a couple of minor comments, but otherwise this looks good to go. Sorry for the long wait.
mypy/plugins/functools.py
Outdated
auto_attribs_default: bool = False) -> None: | ||
"""Add dunder methods to classes decorated with functools.total_ordering.""" | ||
if ctx.api.options.python_version < (3, 2): | ||
ctx.api.fail('"functools.total_ordering" is not supported in Python 2 or 3.1', ctx.reason) |
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.
Or 3.0 I guess. It doesn't really matter since mypy doesn't support any of these early Python 3 versions, so this could just check for Python 2 too.
mypy/plugins/functools.py
Outdated
if root_method.type.ret_type != ctx.api.named_type('__builtins__.bool'): | ||
proper_ret_type = get_proper_type(root_method.type.ret_type) | ||
if not (isinstance(proper_ret_type, UnboundType) | ||
and proper_ret_type.name.endswith('bool')): |
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.
endswith seems wrong, what if I make a type called "notabool"?
mypy/plugins/functools.py
Outdated
|
||
|
||
def _find_other_type(method: _MethodInfo) -> Type: | ||
"""Find the type of the ``other`` argument in a comparision 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.
"""Find the type of the ``other`` argument in a comparision method.""" | |
"""Find the type of the ``other`` argument in a comparison method.""" |
@AWhetter are you available to follow up on the PR? |
Sorry, this fell of my radar. I've made the requested changes. |
This change adds support for understanding the additional methods that are added by decorating a class with
functools.total_ordering
.Limitations
There's still some potential improvements to make to this before merging:
mypy/plugins/functools.py:_analyze_class
looks specifically forFuncItem
s. I wasn't sure how to get the type of the arguments off of an overload?mypy/plugins/functools.py:_find_other_type
: I don't know if there is an existing function that I can use to find the value of the first argument instead?__eq__
supports (depending on which comparison method was implemented. For example if__ge__
was implemented, the argument to__le__
needs to be compatible with__ge__
and__eq__
(https://github.com/python/cpython/blob/bdac32e9fe25fdb97a7172a93aabd1ffead89462/Lib/functools.py#L146).Union
of the__eq__
return type and the return type of the implemented method. Sometimes it's the return type of the__bool__
of those types. How can I condense theUnion
of the two potential return types if possible (eg how could the code know to usebool
instead ofUnion[bool,bool]
?Closes #4610