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.32k stars 1.14k forks source link

Decorator false positive: ``unsubscriptable-object``, ``no-member``, ``unsupported-membership-test``, ``not-an-iterable``, ``too-many-function-args`` #1694

Open ghost opened 7 years ago

ghost commented 7 years ago

Steps to reproduce

I use simple classproperty decorator:

class classproperty(classmethod):

    def __init__(self, fget):
        if isinstance(fget, (classmethod, staticmethod)):
            self.fget = lambda cls: fget.__get__(None, cls)()
        else:
            self.fget = fget
        self.cached = {}
        super(classproperty, self).__init__(self.fget)

    def __get__(self, instance, cls):
        if cls in self.cached:
            return self.cached[cls]
        value = self.cached[cls] = self.fget(cls)
        return value

Example of usages:

from collections import OrderedDict

from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy.inspection import inspect

Base = declarative_base()

class Model(Base):
    __abstract__ = True

    def __init__(self, **kwargs):
        super(Model, self).__init__()
        self.set_attrs(**kwargs)

    def set_attrs(self, **attrs):
        for attr_name, attr_value in attrs.items():
            if attr_name in self.fields:
                setattr(self, attr_name, attr_value)

    def to_dict(self):
        result = {}
        for attr_name in self.fields:
            attr_value = getattr(self, attr_name)
            if attr_value is not None:
                result[attr_name] = attr_value
        return result

    @declared_attr
    def __tablename__(cls):
        # pylint: disable=no-self-argument
        return plural(decapitalize(cls.__name__))

    @classproperty
    def columns(cls):
        columns = inspect(cls).mapper.column_attrs
        columns = list(sorted(columns, key=lambda column: column.key))
        return columns

    @classproperty
    def fields(cls):
        fields = OrderedDict([(column.key, getattr(cls, column.key))
                              for column in cls.columns])
        return fields

    @classproperty
    def primary_key_columns(cls):
        columns = list(inspect(cls).mapper.primary_key)
        return columns

    @classproperty
    def primary_key_fields(cls):
        fields = OrderedDict([(column.key, getattr(cls, column.key))
                              for column in cls.primary_key_columns])
        return fields

Current behavior

There are a lot of false positive errors:

[E1135(unsupported-membership-test), Model.set_attrs] Value 'self.fields' doesn't support membership test
[E1133(not-an-iterable), Model.to_dict] Non-iterable value self.fields is used in an iterating context
[E1133(not-an-iterable), Model.fields] Non-iterable value cls.columns is used in an iterating context
[E1133(not-an-iterable), Model.primary_key_fields] Non-iterable value cls.primary_key_columns is used in an iterating context

Expected behavior

If I make interface class where declare expected value the errors are not raised:


class iface(object):
    columns = ()
    fields = {}
    primary_key_columns = ()
    primary_key_fields = {}

class Model(Base, iface):
    # ...

Expected behavior: no false-positive errors

pylint --version output

pylint 1.7.4, astroid 1.5.3 Python 3.5.2 (default, Sep 14 2017, 22:51:06) [GCC 5.4.0 20160609]

PCManticore commented 7 years ago

This is a known issue, we don't yet support custom decorators/descriptors.

haxwithaxe commented 6 years ago

I'd like to write this but I'm not finding any unit/functional tests for the existing decorator code. Am I missing something? Edit: clarified question.

chrisjsewell commented 3 years ago

Heya, I assume this is still an issue?

Using the example of a classproperty decorator taken from https://github.com/python/mypy/pull/2266#issuecomment-265600482

"""My example"""
from typing import Any, Callable, Generic, TypeVar

ReturnType = TypeVar('ReturnType')

class classproperty(Generic[ReturnType]):  # pylint: disable=invalid-name
    """A decorator for class properties"""

    def __init__(self, getter: Callable[[Any], ReturnType]) -> None:
        self.getter = getter

    def __get__(self, instance: Any, owner: Any) -> ReturnType:
        return self.getter(owner)

class Example:
    """An example class"""

    @classproperty
    def class_property(cls) -> str:
        return 'class property'

Example.class_property.isdigit()

This works correctly for static analysers like mypy and VS Code's Pylance. But with:

pylint 2.11.1
astroid 2.8.0
Python 3.8.12

You get:

$ pylint example.py
************* Module example
example.py:19:4: E0213: Method should have "self" as first argument (no-self-argument)
example.py:19:4: R0201: Method could be a function (no-self-use)
example.py:22:0: E1101: Method 'class_property' has no 'isdigit' member (no-member)
chrisjsewell commented 3 years ago

FYI, I created a quick and dirty pylint plugin to get rid of these, but any feedback/improvements welcome!

"""pylint plugin for ``aiida-core`` specific issues."""
import astroid

def register(linter):  # pylint: disable=unused-argument
    """Register linters (unused)"""

def remove_classprop_imports(import_from: astroid.ImportFrom):
    """Remove any ``classproperty`` imports (handled in ``replace_classprops``)"""
    import_from.names = [name for name in import_from.names if name[0] != 'classproperty']

def replace_classprops(func: astroid.FunctionDef):
    """Replace ``classproperty`` decorated methods.

    As discussed in https://github.com/PyCQA/pylint/issues/1694, pylint does not understand the ``@classproperty``
    decorator, and so mistakes the method as a function, rather than an attribute of the class.
    If the method is annotated, this leads to pylint issuing ``no-member`` errors.

    This transform replaces ``classproperty`` decorated methods with an annotated attribute::

        from aiida.common.lang import classproperty

        class MyClass:
            @classproperty
            def my_property(cls) -> AnnotatedType:
                return cls.my_value

        MyClass.my_property.attribute  # <-- pylint issues: Method 'my_property' has no 'attribute' member (no-member)

        class MyClass:
            my_property: AnnotatedType

    """
    # ignore methods without annotations or decorators
    if not (func.returns and func.decorators and func.decorators.nodes):
        return
    # ignore methods that are specified as abstract
    if any(isinstance(node, astroid.Name) and 'abstract' in node.name for node in func.decorators.nodes):
        return
    if any(isinstance(node, astroid.Attribute) and 'abstract' in node.attrname for node in func.decorators.nodes):
        return
    # convert methods with @classproperty decorator
    if isinstance(func.decorators.nodes[0], astroid.Name) and func.decorators.nodes[0].name == 'classproperty':
        assign = astroid.AnnAssign(lineno=func.lineno, col_offset=func.col_offset, parent=func.parent)
        assign.simple = 1
        assign.target = astroid.AssignName(func.name, lineno=assign.lineno, col_offset=assign.col_offset, parent=assign)
        assign.annotation = func.returns
        assign.annotation.parent = assign
        func.parent.locals[func.name] = [assign.target]
        return assign

astroid.MANAGER.register_transform(astroid.ImportFrom, remove_classprop_imports)
astroid.MANAGER.register_transform(astroid.FunctionDef, replace_classprops)
jacobtylerwalls commented 2 years ago

Test case from #818 involving the Voluptuous library:

"""Demonstration of no-value-for-parameter using Voluptuous"""
#
# This code shows the proper use of the python Voluptuous
# library.  IsFile() is a callable that can be passed
# as a dictionary value in a schema definition.  When called
# it validates the parameter that is passed in.
#
# It is defined as
#    def IsFile(v):
# but called from within Voluptuous as
#    IsFile()('passed param')
#
# Pylint finds
#
# E: 24,39: No value for argument 'v' in function call (no-value-for-parameter)
#
from voluptuous import Schema, IsFile, MultipleInvalid

def main():
    """Testing Voluptuous IsFile() validator"""

    data = {'filename': 'not a file.txt'}

    schema = Schema({'filename': IsFile()})

    try:
        schema(data)
    except MultipleInvalid as e:
        print("Success:", e.msg)
    else:
        print("Failure: IsFile did not work")

if __name__ == "__main__":
    main()