[pylint] Fix false positives, add missing methods, and support positional-only parameters (PLE0302) (#16263)

## Summary

Resolves 3/4 requests in #16217:

-  Remove not special methods: `__cmp__`, `__div__`, `__nonzero__`, and
`__unicode__`.
-  Add special methods: `__next__`, `__buffer__`, `__class_getitem__`,
`__mro_entries__`, `__release_buffer__`, and `__subclasshook__`.
-  Support positional-only arguments.
-  Add support for module functions `__dir__` and `__getattr__`. As
mentioned in the issue the check is scoped for methods rather than
module functions. I am hesitant to expand the scope of this check
without a discussion.

## Test Plan

- Manually confirmed each example file from the issue functioned as
expected.
- Ran cargo nextest to ensure `unexpected_special_method_signature` test
still passed.

Fixes #16217.
This commit is contained in:
Darius Carrier 2025-02-21 05:38:51 -08:00 committed by GitHub
parent b3c5932fda
commit b9b094869a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 119 additions and 14 deletions

View file

@ -68,8 +68,44 @@ class TestClass:
def __eq__(self, **kwargs): # ignore **kwargs
...
def __eq__(self, /, other=42): # ignore positional-only args
def __eq__(self, /, other=42): # support positional-only args
...
def __eq__(self, *, other=42): # ignore positional-only args
def __eq__(self, *, other=42): # ignore keyword-only args
...
def __cmp__(self): # #16217 assert non-special method is skipped, expects 2 parameters
...
def __div__(self): # #16217 assert non-special method is skipped, expects 2 parameters
...
def __nonzero__(self, x): # #16217 assert non-special method is skipped, expects 1 parameter
...
def __unicode__(self, x): # #16217 assert non-special method is skipped, expects 1 parameter
...
def __next__(self, x): # #16217 assert special method is linted, expects 1 parameter
...
def __buffer__(self): # #16217 assert special method is linted, expects 2 parameters
...
def __class_getitem__(self): # #16217 assert special method is linted, expects 2 parameters
...
def __mro_entries__(self): # #16217 assert special method is linted, expects 2 parameters
...
def __release_buffer__(self): # #16217 assert special method is linted, expects 2 parameters
...
def __subclasshook__(self): # #16217 assert special method is linted, expects 2 parameters
...
def __setattr__(self, /, name): # #16217 assert positional-only special method is linted, expects 3 parameters
...
def __setitem__(self, key, /, value, extra_value): # #16217 assert positional-only special method is linted, expects 3 parameters
...

View file

@ -23,10 +23,8 @@ impl ExpectedParams {
| "__neg__" | "__pos__" | "__abs__" | "__invert__" | "__complex__" | "__int__"
| "__float__" | "__index__" | "__trunc__" | "__floor__" | "__ceil__" | "__enter__"
| "__aenter__" | "__getnewargs_ex__" | "__getnewargs__" | "__getstate__"
| "__reduce__" | "__copy__" | "__unicode__" | "__nonzero__" | "__await__"
| "__aiter__" | "__anext__" | "__fspath__" | "__subclasses__" => {
Some(ExpectedParams::Fixed(0))
}
| "__reduce__" | "__copy__" | "__await__" | "__aiter__" | "__anext__"
| "__fspath__" | "__subclasses__" | "__next__" => Some(ExpectedParams::Fixed(0)),
"__format__" | "__lt__" | "__le__" | "__eq__" | "__ne__" | "__gt__" | "__ge__"
| "__getattr__" | "__getattribute__" | "__delattr__" | "__delete__"
| "__instancecheck__" | "__subclasscheck__" | "__getitem__" | "__missing__"
@ -37,8 +35,9 @@ impl ExpectedParams {
| "__rpow__" | "__rlshift__" | "__rrshift__" | "__rand__" | "__rxor__" | "__ror__"
| "__iadd__" | "__isub__" | "__imul__" | "__itruediv__" | "__ifloordiv__"
| "__imod__" | "__ilshift__" | "__irshift__" | "__iand__" | "__ixor__" | "__ior__"
| "__ipow__" | "__setstate__" | "__reduce_ex__" | "__deepcopy__" | "__cmp__"
| "__matmul__" | "__rmatmul__" | "__imatmul__" | "__div__" => {
| "__ipow__" | "__setstate__" | "__reduce_ex__" | "__deepcopy__" | "__matmul__"
| "__rmatmul__" | "__imatmul__" | "__buffer__" | "__class_getitem__"
| "__mro_entries__" | "__release_buffer__" | "__subclasshook__" => {
Some(ExpectedParams::Fixed(1))
}
"__setattr__" | "__get__" | "__set__" | "__setitem__" | "__set_name__" => {
@ -147,11 +146,8 @@ pub(crate) fn unexpected_special_method_signature(
return;
}
// Ignore methods with positional-only or keyword-only parameters, or variadic parameters.
if !parameters.posonlyargs.is_empty()
|| !parameters.kwonlyargs.is_empty()
|| parameters.kwarg.is_some()
{
// Ignore methods with keyword-only parameters or variadic parameters.
if !parameters.kwonlyargs.is_empty() || parameters.kwarg.is_some() {
return;
}
@ -160,10 +156,11 @@ pub(crate) fn unexpected_special_method_signature(
return;
}
let actual_params = parameters.args.len();
let actual_params = parameters.args.len() + parameters.posonlyargs.len();
let mandatory_params = parameters
.args
.iter()
.chain(parameters.posonlyargs.iter())
.filter(|arg| arg.default.is_none())
.count();

View file

@ -71,3 +71,75 @@ unexpected_special_method_signature.py:65:9: PLE0302 The special method `__round
| ^^^^^^^^^ PLE0302
66 | ...
|
unexpected_special_method_signature.py:89:9: PLE0302 The special method `__next__` expects 1 parameter, 2 were given
|
87 | ...
88 |
89 | def __next__(self, x): # #16217 assert special method is linted, expects 1 parameter
| ^^^^^^^^ PLE0302
90 | ...
|
unexpected_special_method_signature.py:92:9: PLE0302 The special method `__buffer__` expects 2 parameters, 1 was given
|
90 | ...
91 |
92 | def __buffer__(self): # #16217 assert special method is linted, expects 2 parameters
| ^^^^^^^^^^ PLE0302
93 | ...
|
unexpected_special_method_signature.py:95:9: PLE0302 The special method `__class_getitem__` expects 2 parameters, 1 was given
|
93 | ...
94 |
95 | def __class_getitem__(self): # #16217 assert special method is linted, expects 2 parameters
| ^^^^^^^^^^^^^^^^^ PLE0302
96 | ...
|
unexpected_special_method_signature.py:98:9: PLE0302 The special method `__mro_entries__` expects 2 parameters, 1 was given
|
96 | ...
97 |
98 | def __mro_entries__(self): # #16217 assert special method is linted, expects 2 parameters
| ^^^^^^^^^^^^^^^ PLE0302
99 | ...
|
unexpected_special_method_signature.py:101:9: PLE0302 The special method `__release_buffer__` expects 2 parameters, 1 was given
|
99 | ...
100 |
101 | def __release_buffer__(self): # #16217 assert special method is linted, expects 2 parameters
| ^^^^^^^^^^^^^^^^^^ PLE0302
102 | ...
|
unexpected_special_method_signature.py:104:9: PLE0302 The special method `__subclasshook__` expects 2 parameters, 1 was given
|
102 | ...
103 |
104 | def __subclasshook__(self): # #16217 assert special method is linted, expects 2 parameters
| ^^^^^^^^^^^^^^^^ PLE0302
105 | ...
|
unexpected_special_method_signature.py:107:9: PLE0302 The special method `__setattr__` expects 3 parameters, 2 were given
|
105 | ...
106 |
107 | def __setattr__(self, /, name): # #16217 assert positional-only special method is linted, expects 3 parameters
| ^^^^^^^^^^^ PLE0302
108 | ...
|
unexpected_special_method_signature.py:110:9: PLE0302 The special method `__setitem__` expects 3 parameters, 4 were given
|
108 | ...
109 |
110 | def __setitem__(self, key, /, value, extra_value): # #16217 assert positional-only special method is linted, expects 3 parameters
| ^^^^^^^^^^^ PLE0302
111 | ...
|