9

I've made a classproperty descriptor and whenever I use a function decorated with it, I get multiple pylint inspection errors.

Here is a sample class with a sample decorated function:

class Bar:
    """
    Bar documentation.
    """
    # pylint: disable=no-method-argument

    @classproperty
    def foo():
        """
        Retrieve foo.
        """
        return "foo"

Thanks to the descriptor, I can call Bar.foo and get the string foo returned.

Unfortunately, whenever I use functions like this with slightly more complex items (e.g. functions which return instances of objects), pylint starts complaining about things such as no-member or unexpected-keyword-arg, simply because it thinks Bar.foo is a method, rather than a wrapped classproperty object.

I would like to disable warnings for any code that uses my function - I definitely can't allow having to write # pylint: disable every single time I use the classproperty-wrapped methods. How can I do it with pylint? Or maybe I should switch to use a different linter instead?

Here is an example of a warning generated because of the reasons above:

class Bar:
    """
    Bar documentation.
    """
    # pylint: disable=no-method-argument

    @classproperty
    def foo():
        """
        Retrieve an object.
        """
        return NotImplementedError("Argument")

print(Bar.foo.args)

pylint complains that E1101: Method 'foo' has no 'args' member (no-member) (even though I know it definitely has), and I would like to completely disable some warnings for any module/class/function that uses Bar.foo.args or similar.

For anyone interested, here is a minimal implementation of a classproperty descriptor:

class classproperty:
    """
    Minimal descriptor.
    """
    # pylint: disable=invalid-name

    def __init__(self, func):
        self._func = func

    def __get__(self, _obj, _type):
        return self._func()
Kacperito
  • 1,277
  • 1
  • 10
  • 27
  • What is args and why would foo have one? – quamrana Jul 18 '20 at 08:59
  • 2
    @quamrana It's a field of `NotImplementedError` class used in the sample. The output of `print(Bar.foo.args)` in this case would be `("Argument",)` – Kacperito Jul 18 '20 at 09:34
  • 2
    It would have it because the descriptor has a `__get__` which calls `foo()` when accessing `Bar.foo` and returns the value (as mentioned in the question's description) – Kacperito Jul 18 '20 at 09:37
  • err... No, `Bar.foo` is a method, and I don't think it has an `args` member. If you are talking about `NotImplemented`, then maybe you meant `print(Bar.foo().args)`? – quamrana Jul 18 '20 at 09:42
  • 3
    I think this might be useful for you - https://docs.python.org/3/howto/descriptor.html - as I've mentioned `foo` is decorated with a descriptor written by me, that allows calling `Bar.foo` instead of `Bar.foo()`, that's the whole point. – Kacperito Jul 18 '20 at 09:43

2 Answers2

4

I have managed to create a dirty hack by type-hinting the items as None:

class Bar:
    """
    Bar documentation.
    """
    # pylint: disable=no-method-argument,function-redefined,too-few-public-methods
    foo: None

    @classproperty
    def foo():
        """
        Retrieve an object.
        """
        return NotImplementedError("Argument")

I would rather avoid having code like this because I can't actually import the items which should be type-hinted due to the circular imports issue (hence None), but it tricks pylint well.

Kacperito
  • 1,277
  • 1
  • 10
  • 27
  • Not sure what the _circular import_ really is in your case, but... would `from __future__ import annotations` help? https://www.python.org/dev/peps/pep-0563/#enabling-the-future-behavior-in-python-3-7 – sinoroc Jul 20 '20 at 15:30
  • @sinoroc No, this is related to `django` and the way it handles database models (you can't import them as easily as standard Python classes). I can't import the right classes where I want to define the `classproperty`-wrapped methods. But thanks for the link - interesting read! – Kacperito Jul 20 '20 at 15:58
3

As far as I know, it's not possible.

I haven't found a way to solve this in pylint's configuration. The closest I could find is the property-classes option, but it only influences the invalid-name checker, so not what we are looking for here:

:property-classes:
  List of decorators that produce properties, such as abc.abstractproperty. Add
  to this list to register other decorators that produce valid properties.
  These decorators are taken in consideration only for invalid-name.

  Default: ``abc.abstractproperty``

Maybe it's a question that is worth asking pylint's developers directly.

Seems to me like it's something that could be solved with a transform plugin (Maybe this for inspiration?). Pylint handles @property decorators perfectly fine, so something like the @classproperty suggested here, should be feasible as well.


Aside

(You might know those already)

For properties on classes:

sinoroc
  • 18,409
  • 2
  • 39
  • 70
  • Thanks for linking things :) Unfortunately, a basic transform plugin won't work in my case (due to `django` being what it is), but it was a good resource to learn about. For now, I have a quick hack (mentioned as an answer to this question), but I will try to get in touch with `pylint` developers for a better solution. I haven't tried the "inspiration" link yet, but I will give it a go. – Kacperito Jul 20 '20 at 14:51
  • No problem. After looking a bit more into it, I am not really sure about the _inspiration_ link, it might be a false lead. The _transform plugin_ still sounds to me like a promising solution, at least technically. But practically it might be complicated (to use, distribute, etc. unless there is just one project you want to lint and you have full control over it). -- But in general, _pylint_ does it for the regular `@property` constructors, so I doubt there is much difference to how `@classproperty` should be handled. – sinoroc Jul 20 '20 at 15:27