Open ghost opened 7 years ago
This is a known issue, we don't yet support custom decorators/descriptors.
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.
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)
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)
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()
Steps to reproduce
I use simple classproperty decorator:
Example of usages:
Current behavior
There are a lot of false positive errors:
Expected behavior
If I make interface class where declare expected value the errors are not raised:
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]