pylint-dev / pylint

It's not just a linter that annoys you!
https://pylint.readthedocs.io/en/latest/
GNU General Public License v2.0
5.26k stars 1.12k forks source link

Pylint does not appear to understand the descriptor protocol #7820

Open tomkcook opened 1 year ago

tomkcook commented 1 year ago

Bug description

Writing a custom @property-like descriptor causes pylint to issue various false postives. Example code:

"Demonstrate class property problems"

from typing import Callable

class ClassROProperty(property):
    "Decorator class to create a read-only class property"
    def __init__(self, getter:Callable):
        self._getter = getter

    def __get__(self, _, cls):
        return self._getter(cls)

class MyClass:
    "A class which has only class properties"

    @ClassROProperty
    def prop_1(cls):
        "Return an iterable value"
        return [1, 2, 3, 4]

for val in MyClass.prop_1:
    print(val)
print(MyClass.prop_1[1])

Running pylint --disable=too-few-public-methods on this results in these failures:

************* Module test
test.py:17:4: E0213: Method 'prop_1' should have "self" as first argument (no-self-argument)
test.py:21:11: E1133: Non-iterable value MyClass.prop_1 is used in an iterating context (not-an-iterable)
test.py:23:6: E1136: Value 'MyClass.prop_1' is unsubscriptable (unsubscriptable-object)

------------------------------------------------------------------
Your code has been rated at 0.00/10 (previous run: 0.00/10, +0.00)

E0213 can be fairly easily handled by disabling no-self-argument module- or class-wide. But E1133 and E1136 are much more difficult to suppress, because they happen everywhere the properties are used, not where they are defined. So anyone using your code needs to know to suppress these errors in their code.

I raised this on discord and was asked to file a report here.

Command used

venv/bin/pylint --disable=too-few-public-methods test.py

Pylint output

************* Module test
test.py:17:4: E0213: Method 'prop_1' should have "self" as first argument (no-self-argument)
test.py:21:11: E1133: Non-iterable value MyClass.prop_1 is used in an iterating context (not-an-iterable)
test.py:23:6: E1136: Value 'MyClass.prop_1' is unsubscriptable (unsubscriptable-object)

------------------------------------------------------------------
Your code has been rated at 0.00/10 (previous run: 0.00/10, +0.00)

Expected behavior

These appear to be false positives.

Pylint version

pylint 2.15.6
astroid 2.12.13
Python 3.10.6 (main, Nov  2 2022, 18:53:38) [GCC 11.3.0]

OS / Environment

Ubuntu 22.04

rafaelclp commented 5 months ago

Bumping.

Pylint still doesn't understand descriptors at all (except for the few ones that seem to be hardcoded in pylint, like classmethod, property etc).

Edit: here's an example that confuses pylint:

from typing import Any, Callable, Concatenate, Generic, ParamSpec, TypeVar, overload

from typing_extensions import Self

IR = TypeVar("IR")
IP = ParamSpec("IP")
KR = TypeVar("KR")
KP = ParamSpec("KP")
IT = TypeVar("IT")
KT = TypeVar("KT")

class Hybrid(Generic[IT, KT, IP, IR, KP, KR]):
    def __init__(
        self,
        instance_method: Callable[Concatenate[IT, IP], IR],
        klass_method: Callable[Concatenate[type[KT], KP], KR],
    ):
        self.im = instance_method
        self.km = klass_method

    def __call__(self, _: Any) -> Self:
        return self

    @overload
    def __get__(self, obj: IT, objtype: Any) -> Callable[IP, IR]: ...
    @overload
    def __get__(self, obj: None, objtype: type[KT]) -> Callable[KP, KR]: ...
    def __get__(self, obj: Any, objtype: Any = None):
        if obj is None:
            return self.km.__get__(obj, objtype)
        return self.im.__get__(obj, objtype)

class IntAdder:
    def __init__(self, v: int):
        self.v = v

    def _i_add(self, b: int) -> int:
        return self.v + b

    @classmethod
    def _c_add(cls, a: int, b: int) -> int:
        return a + b

    @Hybrid(_i_add, _c_add)
    def add(self):  # pylint: disable=missing-function-docstring
        ...

assert IntAdder(3).add(4) == 7
assert IntAdder.add(3, 4) == 7
image
jacobtylerwalls commented 2 months ago

A from-the-hip naive patch that solves #9832 but causes some failures worth understanding:

diff --git a/astroid/bases.py b/astroid/bases.py
index 4a684cf1..3dfd71f1 100644
--- a/astroid/bases.py
+++ b/astroid/bases.py
@@ -44,9 +44,7 @@ if PY310_PLUS:

 # List of possible property names. We use this list in order
 # to see if a method is a property or not. This should be
-# pretty reliable and fast, the alternative being to check each
-# decorator to see if its a real property-like descriptor, which
-# can be too complicated.
+# pretty reliable and fast.
 # Also, these aren't qualified, because each project can
 # define them, we shouldn't expect to know every possible
 # property-like decorator!
@@ -87,6 +85,8 @@ def _is_property(
         if inferred is None or isinstance(inferred, UninferableBase):
             continue
         if isinstance(inferred, nodes.ClassDef):
+            if inferred.lookup("__get__"):
+                return True
             for base_class in inferred.bases:
                 if not isinstance(base_class, nodes.Name):
                     continue