diff --git a/aiida/common/utils.py b/aiida/common/utils.py index d72ad924a4..f730c7e66e 100644 --- a/aiida/common/utils.py +++ b/aiida/common/utils.py @@ -15,6 +15,7 @@ import os import re import sys +from typing import Any, Dict from uuid import UUID from .lang import classproperty @@ -388,7 +389,7 @@ def _prettify_label_latex_simple(cls, label): return re.sub(r'(\d+)', r'$_{\1}$', label) @classproperty - def prettifiers(cls): # pylint: disable=no-self-argument + def prettifiers(cls) -> Dict[str, Any]: # pylint: disable=no-self-argument """ Property that returns a dictionary that for each string associates the function to prettify a label @@ -412,7 +413,7 @@ def get_prettifiers(cls): :return: a list of strings """ - return sorted(cls.prettifiers.keys()) # pylint: disable=no-member + return sorted(cls.prettifiers.keys()) def __init__(self, format): # pylint: disable=redefined-builtin """ diff --git a/aiida/transports/transport.py b/aiida/transports/transport.py index b85ea93a53..6c88aa167e 100644 --- a/aiida/transports/transport.py +++ b/aiida/transports/transport.py @@ -194,10 +194,10 @@ def get_valid_auth_params(cls): if cls._valid_auth_options is None: raise NotImplementedError else: - return cls.auth_options.keys() # pylint: disable=no-member + return cls.auth_options.keys() @classproperty - def auth_options(cls): # pylint: disable=no-self-argument + def auth_options(cls) -> OrderedDict: # pylint: disable=no-self-argument """Return the authentication options to be used for building the CLI. :return: `OrderedDict` of tuples, with first element option name and second dictionary of kwargs diff --git a/pyproject.toml b/pyproject.toml index 016921bb7e..dae3953e5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools>=40.8.0", "wheel", "fastentrypoints~=0.12"] build-backend = "setuptools.build_meta" [tool.pylint.master] -load-plugins = "pylint_django" +load-plugins = ["pylint_django", "utils.pylint_aiida"] # this currently fails with aiida.common.exceptions.ProfileConfigurationError: no profile has been loaded # we woud need a static settings module to use this # django-settings-module = "aiida.backends.djsite.settings" diff --git a/utils/pylint_aiida.py b/utils/pylint_aiida.py new file mode 100644 index 0000000000..f577057b7b --- /dev/null +++ b/utils/pylint_aiida.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +"""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)