Skip to content

Commit

Permalink
Merge pull request #3890 from JulienPalard/issue3882
Browse files Browse the repository at this point in the history
Handle class decorators during typing checks.
  • Loading branch information
hippo91 committed Nov 21, 2020
2 parents 9a5e1b3 + 9b7fbc3 commit 5b4e9a0
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CONTRIBUTORS.txt
Expand Up @@ -428,4 +428,6 @@ contributors:

* Joffrey Mander: contributor

* Julien Palard: contributor

* Raphael Gaschignard: contributor
4 changes: 4 additions & 0 deletions ChangeLog
Expand Up @@ -2,6 +2,10 @@
Pylint's ChangeLog
------------------

* Handle class decorators applied to function.

Closes #3882

* Add check for empty comments

* Fix minor documentation issue in contribute.rst
Expand Down
5 changes: 4 additions & 1 deletion doc/development_guide/contribute.rst
Expand Up @@ -110,7 +110,10 @@ your patch gets accepted.
(`What's New` section). For the release document we usually write some more details,
and it is also a good place to offer examples on how the new change is supposed to work.

- Add yourself to the `CONTRIBUTORS` file, if you are not already there.
- Add a short entry in :file:`doc/whatsnew/VERSION.rst`.

- Add yourself to the `CONTRIBUTORS` file, flag youself appropriately
(if in doubt, you're a ``contributor``).

- Write a comprehensive commit message

Expand Down
11 changes: 10 additions & 1 deletion pylint/checkers/typecheck.py
Expand Up @@ -65,7 +65,7 @@
import astroid.arguments
import astroid.context
import astroid.nodes
from astroid import bases, decorators, exceptions, modutils, objects
from astroid import bases, decorators, exceptions, helpers, modutils, objects
from astroid.interpreter import dunder_lookup

from pylint.checkers import BaseChecker, utils
Expand Down Expand Up @@ -1720,9 +1720,18 @@ def visit_subscript(self, node):
return

inferred = safe_infer(node.value)

if inferred is None or inferred is astroid.Uninferable:
return

if getattr(inferred, "decorators", None):
first_decorator = helpers.safe_infer(inferred.decorators.nodes[0])
if isinstance(first_decorator, astroid.ClassDef):
inferred = first_decorator.instantiate_class()
else:
return # It would be better to handle function
# decorators, but let's start slow.

if not supported_protocol(inferred):
self.add_message(msg, args=node.value.as_string(), node=node.value)

Expand Down
137 changes: 137 additions & 0 deletions tests/checkers/unittest_typecheck.py
Expand Up @@ -24,6 +24,7 @@
import pytest

from pylint.checkers import typecheck
from pylint.interfaces import UNDEFINED
from pylint.testutils import CheckerTestCase, Message, set_config

try:
Expand Down Expand Up @@ -356,3 +357,139 @@ def get_num(n):
Message("not-callable", node=call, args="get_num(10)")
):
self.checker.visit_call(call)


class TestTypeCheckerOnDecorators(CheckerTestCase):
"Tests for pylint.checkers.typecheck on decorated functions."
CHECKER_CLASS = typecheck.TypeChecker

def test_issue3882_class_decorators(self):
decorators = """
class Unsubscriptable:
def __init__(self, f):
self.f = f
class Subscriptable:
def __init__(self, f):
self.f = f
def __getitem__(self, item):
return item
"""
for generic in "Optional", "List", "ClassVar", "Final", "Literal":
self.typing_objects_are_subscriptable(generic)

self.getitem_on_modules()
self.decorated_by_a_subscriptable_class(decorators)
self.decorated_by_an_unsubscriptable_class(decorators)

self.decorated_by_subscriptable_then_unsubscriptable_class(decorators)
self.decorated_by_unsubscriptable_then_subscriptable_class(decorators)

def getitem_on_modules(self):
"""Mainly validate the code won't crash if we're not having a function."""
module = astroid.parse(
"""
import collections
test = collections[int]
"""
)
subscript = module.body[-1].value
with self.assertAddsMessages(
Message(
"unsubscriptable-object",
node=subscript.value,
args="collections",
confidence=UNDEFINED,
)
):
self.checker.visit_subscript(subscript)

def typing_objects_are_subscriptable(self, generic):
module = astroid.parse(
"""
import typing
test = typing.{}[int]
""".format(
generic
)
)
subscript = module.body[-1].value
with self.assertNoMessages():
self.checker.visit_subscript(subscript)

def decorated_by_a_subscriptable_class(self, decorators):
module = astroid.parse(
decorators
+ """
@Subscriptable
def decorated():
...
test = decorated[None]
"""
)
subscript = module.body[-1].value
with self.assertNoMessages():
self.checker.visit_subscript(subscript)

def decorated_by_subscriptable_then_unsubscriptable_class(self, decorators):
module = astroid.parse(
decorators
+ """
@Unsubscriptable
@Subscriptable
def decorated():
...
test = decorated[None]
"""
)
subscript = module.body[-1].value
with self.assertAddsMessages(
Message(
"unsubscriptable-object",
node=subscript.value,
args="decorated",
confidence=UNDEFINED,
)
):
self.checker.visit_subscript(subscript)

def decorated_by_unsubscriptable_then_subscriptable_class(self, decorators):
module = astroid.parse(
decorators
+ """
@Subscriptable
@Unsubscriptable
def decorated():
...
test = decorated[None]
"""
)
subscript = module.body[-1].value
with self.assertNoMessages():
self.checker.visit_subscript(subscript)

def decorated_by_an_unsubscriptable_class(self, decorators):
module = astroid.parse(
decorators
+ """
@Unsubscriptable
def decorated():
...
test = decorated[None]
"""
)
subscript = module.body[-1].value
with self.assertAddsMessages(
Message(
"unsubscriptable-object",
node=subscript.value,
args="decorated",
confidence=UNDEFINED,
)
):
self.checker.visit_subscript(subscript)

0 comments on commit 5b4e9a0

Please sign in to comment.