Skip to content

Commit

Permalink
🔧 MAINTAIN: Add pylint_aiida plugin (#5182)
Browse files Browse the repository at this point in the history
As discussed in #5176 and pylint-dev/pylint#1694,
pylint does not understand the `@classproperty` decorator,
and so mistakes the method as a function, rather than an attribute of the class.
This commit adds a pylint transform plugin, to remove false-positives.
(see: https://pylint.pycqa.org/en/latest/how_tos/transform_plugins.html)
  • Loading branch information
chrisjsewell authored Oct 19, 2021
1 parent 8fb1457 commit bd91bdb
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 5 deletions.
5 changes: 3 additions & 2 deletions aiida/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import os
import re
import sys
from typing import Any, Dict
from uuid import UUID

from .lang import classproperty
Expand Down Expand Up @@ -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
Expand All @@ -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
"""
Expand Down
4 changes: 2 additions & 2 deletions aiida/transports/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
57 changes: 57 additions & 0 deletions utils/pylint_aiida.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit bd91bdb

Please sign in to comment.