summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Byrne <31762852+mbyrnepr2@users.noreply.github.com>2021-11-09 12:51:34 +0100
committerGitHub <noreply@github.com>2021-11-09 12:51:34 +0100
commit57515ae304c9e4bae6a94f08ea7ef2d6fadc61ce (patch)
tree926eb6ccea2547632aa5e2624da6657cfcb75854
parent5d39f58b60f635d990324b8759053ff1cc279451 (diff)
downloadpylint-git-57515ae304c9e4bae6a94f08ea7ef2d6fadc61ce.tar.gz
Add checker `using-final-decorator-in-unsupported-version` (#5165)
* Add checker `using-final-in-unsupported-version` This is one of the tasks in issue: #5134 Also: - Ensure the existing checkers for `typing.final` are used irrespective of Python version * Emit `using-final-in-unsupported-version` warning when Python version < 3.8 and none of the other `typing.final`-related warnings * Add `uninferable_final_decorators` Return any `typing.final` decorators for a given `Decorators` node. Used to determine if this decorator is used with a version of Python in which it is unsupported. Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
-rw-r--r--ChangeLog3
-rw-r--r--doc/whatsnew/2.12.rst3
-rw-r--r--pylint/checkers/classes.py17
-rw-r--r--pylint/checkers/unsupported_version.py36
-rw-r--r--pylint/checkers/utils.py45
-rw-r--r--tests/functional/o/overridden_final_method_py38.py3
-rw-r--r--tests/functional/o/overridden_final_method_py38.txt2
-rw-r--r--tests/functional/s/subclassed_final_class_py38.py3
-rw-r--r--tests/functional/s/subclassed_final_class_py38.txt2
-rw-r--r--tests/functional/u/unsupported/unsupported_version_for_final.py36
-rw-r--r--tests/functional/u/unsupported/unsupported_version_for_final.rc2
-rw-r--r--tests/functional/u/unsupported/unsupported_version_for_final.txt9
12 files changed, 148 insertions, 13 deletions
diff --git a/ChangeLog b/ChangeLog
index 565c6d9d0..34a5692bc 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -132,6 +132,9 @@ Release date: TBA
* Fix ``missing-function-docstring`` not being able to check ``__init__`` and other
magic methods even if the ``no-docstring-rgx`` setting was set to do so
+* Added ``using-final-decorator-in-unsupported-version`` checker. Issued when ``py-version``
+ is set to a version that does not support ``typing.final`` (< 3.8)
+
* Added configuration option ``exclude-too-few-public-methods`` to allow excluding
classes from the ``min-public-methods`` checker.
diff --git a/doc/whatsnew/2.12.rst b/doc/whatsnew/2.12.rst
index 3be3f3c7b..760971a3a 100644
--- a/doc/whatsnew/2.12.rst
+++ b/doc/whatsnew/2.12.rst
@@ -43,6 +43,9 @@ New checkers
Closes #4774
+* Added ``using-final-decorator-in-unsupported-version`` checker. Issued when ``py-version``
+ is set to a version that does not support typing.final (< 3.8)
+
* Added configuration option ``exclude-too-few-public-methods`` to allow excluding
classes from the ``min-public-methods`` checker.
diff --git a/pylint/checkers/classes.py b/pylint/checkers/classes.py
index 82fcc8a61..9f7ea9248 100644
--- a/pylint/checkers/classes.py
+++ b/pylint/checkers/classes.py
@@ -79,8 +79,8 @@ from pylint.checkers.utils import (
overrides_a_method,
safe_infer,
unimplemented_abstract_methods,
+ uninferable_final_decorators,
)
-from pylint.constants import PY38_PLUS
from pylint.interfaces import IAstroidChecker
from pylint.utils import get_global_option
@@ -795,6 +795,8 @@ a metaclass class method.",
def open(self) -> None:
self._mixin_class_rgx = get_global_option(self, "mixin-class-rgx")
+ py_version = get_global_option(self, "py-version")
+ self._py38_plus = py_version >= (3, 8)
@astroid.decorators.cachedproperty
def _dummy_rgx(self):
@@ -868,14 +870,16 @@ a metaclass class method.",
def _check_typing_final(self, node: nodes.ClassDef) -> None:
"""Detect that a class does not subclass a class decorated with `typing.final`"""
- if not PY38_PLUS:
+ if not self._py38_plus:
return
for base in node.bases:
ancestor = safe_infer(base)
if not ancestor:
continue
- if isinstance(ancestor, nodes.ClassDef) and decorated_with(
- ancestor, ["typing.final"]
+
+ if isinstance(ancestor, nodes.ClassDef) and (
+ decorated_with(ancestor, ["typing.final"])
+ or (uninferable_final_decorators(ancestor.decorators))
):
self.add_message(
"subclassed-final-class",
@@ -1336,7 +1340,10 @@ a metaclass class method.",
args=(function_node.name, "non-async", "async"),
node=function_node,
)
- if decorated_with(parent_function_node, ["typing.final"]) and PY38_PLUS:
+ if (
+ decorated_with(parent_function_node, ["typing.final"])
+ or uninferable_final_decorators(parent_function_node.decorators)
+ ) and self._py38_plus:
self.add_message(
"overridden-final-method",
args=(function_node.name, parent_function_node.parent.name),
diff --git a/pylint/checkers/unsupported_version.py b/pylint/checkers/unsupported_version.py
index 917d64cba..005bb8f88 100644
--- a/pylint/checkers/unsupported_version.py
+++ b/pylint/checkers/unsupported_version.py
@@ -7,10 +7,15 @@
indicated by the py-version setting.
"""
+
from astroid import nodes
from pylint.checkers import BaseChecker
-from pylint.checkers.utils import check_messages
+from pylint.checkers.utils import (
+ check_messages,
+ safe_infer,
+ uninferable_final_decorators,
+)
from pylint.interfaces import IAstroidChecker
from pylint.lint import PyLinter
from pylint.utils import get_global_option
@@ -30,12 +35,19 @@ class UnsupportedVersionChecker(BaseChecker):
"Used when the py-version set by the user is lower than 3.6 and pylint encounters "
"a f-string.",
),
+ "W1602": (
+ "typing.final is not supported by all versions included in the py-version setting",
+ "using-final-decorator-in-unsupported-version",
+ "Used when the py-version set by the user is lower than 3.8 and pylint encounters "
+ "a ``typing.final`` decorator.",
+ ),
}
def open(self) -> None:
"""Initialize visit variables and statistics."""
py_version = get_global_option(self, "py-version")
self._py36_plus = py_version >= (3, 6)
+ self._py38_plus = py_version >= (3, 8)
@check_messages("using-f-string-in-unsupported-version")
def visit_joinedstr(self, node: nodes.JoinedStr) -> None:
@@ -43,6 +55,28 @@ class UnsupportedVersionChecker(BaseChecker):
if not self._py36_plus:
self.add_message("using-f-string-in-unsupported-version", node=node)
+ @check_messages("using-final-decorator-in-unsupported-version")
+ def visit_decorators(self, node: nodes.Decorators) -> None:
+ """Check decorators"""
+ self._check_typing_final(node)
+
+ def _check_typing_final(self, node: nodes.Decorators) -> None:
+ """Add a message when the `typing.final` decorator is used and the
+ py-version is lower than 3.8"""
+ if self._py38_plus:
+ return
+
+ decorators = []
+ for decorator in node.get_children():
+ inferred = safe_infer(decorator)
+ if inferred and inferred.qname() == "typing.final":
+ decorators.append(decorator)
+
+ for decorator in decorators or uninferable_final_decorators(node):
+ self.add_message(
+ "using-final-decorator-in-unsupported-version", node=decorator
+ )
+
def register(linter: PyLinter) -> None:
"""Required method to auto register this checker"""
diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py
index f5b4f42a5..cff791364 100644
--- a/pylint/checkers/utils.py
+++ b/pylint/checkers/utils.py
@@ -822,7 +822,9 @@ def _is_property_decorator(decorator: nodes.Name) -> bool:
def decorated_with(
- func: Union[nodes.FunctionDef, astroid.BoundMethod, astroid.UnboundMethod],
+ func: Union[
+ nodes.ClassDef, nodes.FunctionDef, astroid.BoundMethod, astroid.UnboundMethod
+ ],
qnames: Iterable[str],
) -> bool:
"""Determine if the `func` node has a decorator with the qualified name `qname`."""
@@ -843,6 +845,47 @@ def decorated_with(
return False
+def uninferable_final_decorators(
+ node: nodes.Decorators,
+) -> List[Optional[Union[nodes.Attribute, nodes.Name]]]:
+ """Return a list of uninferable `typing.final` decorators in `node`.
+
+ This function is used to determine if the `typing.final` decorator is used
+ with an unsupported Python version; the decorator cannot be inferred when
+ using a Python version lower than 3.8.
+ """
+ decorators = []
+ for decorator in getattr(node, "nodes", []):
+ if isinstance(decorator, nodes.Attribute):
+ try:
+ import_node = decorator.expr.lookup(decorator.expr.name)[1][0]
+ except AttributeError:
+ continue
+ elif isinstance(decorator, nodes.Name):
+ import_node = decorator.lookup(decorator.name)[1][0]
+ else:
+ continue
+
+ if not isinstance(import_node, (astroid.Import, astroid.ImportFrom)):
+ continue
+
+ import_names = dict(import_node.names)
+
+ # from typing import final
+ is_from_import = ("final" in import_names) and import_node.modname == "typing"
+ # import typing
+ is_import = ("typing" in import_names) and getattr(
+ decorator, "attrname", None
+ ) == "final"
+
+ if (is_from_import or is_import) and safe_infer(decorator) in [
+ astroid.Uninferable,
+ None,
+ ]:
+ decorators.append(decorator)
+ return decorators
+
+
@lru_cache(maxsize=1024)
def unimplemented_abstract_methods(
node: nodes.ClassDef, is_abstract_cb: nodes.FunctionDef = None
diff --git a/tests/functional/o/overridden_final_method_py38.py b/tests/functional/o/overridden_final_method_py38.py
index d951c26da..d7b27b6e9 100644
--- a/tests/functional/o/overridden_final_method_py38.py
+++ b/tests/functional/o/overridden_final_method_py38.py
@@ -1,8 +1,7 @@
"""Since Python version 3.8, a method decorated with typing.final cannot be
overridden"""
-# pylint: disable=no-init, import-error, invalid-name, using-constant-test, useless-object-inheritance
-# pylint: disable=missing-docstring, too-few-public-methods
+# pylint: disable=no-init, useless-object-inheritance, missing-docstring, too-few-public-methods
from typing import final
diff --git a/tests/functional/o/overridden_final_method_py38.txt b/tests/functional/o/overridden_final_method_py38.txt
index 2c8bca442..66bbf322d 100644
--- a/tests/functional/o/overridden_final_method_py38.txt
+++ b/tests/functional/o/overridden_final_method_py38.txt
@@ -1 +1 @@
-overridden-final-method:16:4:Subclass.my_method:Method 'my_method' overrides a method decorated with typing.final which is defined in class 'Base':HIGH
+overridden-final-method:15:4:Subclass.my_method:Method 'my_method' overrides a method decorated with typing.final which is defined in class 'Base':HIGH
diff --git a/tests/functional/s/subclassed_final_class_py38.py b/tests/functional/s/subclassed_final_class_py38.py
index 816ef537e..7f0671e75 100644
--- a/tests/functional/s/subclassed_final_class_py38.py
+++ b/tests/functional/s/subclassed_final_class_py38.py
@@ -1,8 +1,7 @@
"""Since Python version 3.8, a class decorated with typing.final cannot be
subclassed """
-# pylint: disable=no-init, import-error, invalid-name, using-constant-test, useless-object-inheritance
-# pylint: disable=missing-docstring, too-few-public-methods
+# pylint: disable=no-init, useless-object-inheritance, missing-docstring, too-few-public-methods
from typing import final
diff --git a/tests/functional/s/subclassed_final_class_py38.txt b/tests/functional/s/subclassed_final_class_py38.txt
index 46fb5200e..037e5eb4f 100644
--- a/tests/functional/s/subclassed_final_class_py38.txt
+++ b/tests/functional/s/subclassed_final_class_py38.txt
@@ -1 +1 @@
-subclassed-final-class:15:0:Subclass:"Class 'Subclass' is a subclass of a class decorated with typing.final: 'Base'":HIGH
+subclassed-final-class:14:0:Subclass:"Class 'Subclass' is a subclass of a class decorated with typing.final: 'Base'":HIGH
diff --git a/tests/functional/u/unsupported/unsupported_version_for_final.py b/tests/functional/u/unsupported/unsupported_version_for_final.py
new file mode 100644
index 000000000..efc433dd4
--- /dev/null
+++ b/tests/functional/u/unsupported/unsupported_version_for_final.py
@@ -0,0 +1,36 @@
+"""Tests for the use of typing.final whenever the py-version is set < 3.8"""
+# pylint: disable=missing-class-docstring, no-member, too-few-public-methods, missing-function-docstring, no-name-in-module, reimported
+
+import typing
+import typing as mytyping
+from typing import final
+from typing import final as myfinal
+
+
+@final # [using-final-decorator-in-unsupported-version]
+class MyClass1:
+ @final # [using-final-decorator-in-unsupported-version]
+ @final # [using-final-decorator-in-unsupported-version]
+ def my_method(self):
+ pass
+
+
+@myfinal # [using-final-decorator-in-unsupported-version]
+class MyClass2:
+ @myfinal # [using-final-decorator-in-unsupported-version]
+ def my_method(self):
+ pass
+
+
+@typing.final # [using-final-decorator-in-unsupported-version]
+class MyClass3:
+ @typing.final # [using-final-decorator-in-unsupported-version]
+ def my_method(self):
+ pass
+
+
+@mytyping.final # [using-final-decorator-in-unsupported-version]
+class MyClass4:
+ @mytyping.final # [using-final-decorator-in-unsupported-version]
+ def my_method(self):
+ pass
diff --git a/tests/functional/u/unsupported/unsupported_version_for_final.rc b/tests/functional/u/unsupported/unsupported_version_for_final.rc
new file mode 100644
index 000000000..d10f4f3c2
--- /dev/null
+++ b/tests/functional/u/unsupported/unsupported_version_for_final.rc
@@ -0,0 +1,2 @@
+[master]
+py-version=3.7
diff --git a/tests/functional/u/unsupported/unsupported_version_for_final.txt b/tests/functional/u/unsupported/unsupported_version_for_final.txt
new file mode 100644
index 000000000..f5cc2aa05
--- /dev/null
+++ b/tests/functional/u/unsupported/unsupported_version_for_final.txt
@@ -0,0 +1,9 @@
+using-final-decorator-in-unsupported-version:10:1:MyClass1:typing.final is not supported by all versions included in the py-version setting:HIGH
+using-final-decorator-in-unsupported-version:12:5:MyClass1.my_method:typing.final is not supported by all versions included in the py-version setting:HIGH
+using-final-decorator-in-unsupported-version:13:5:MyClass1.my_method:typing.final is not supported by all versions included in the py-version setting:HIGH
+using-final-decorator-in-unsupported-version:18:1:MyClass2:typing.final is not supported by all versions included in the py-version setting:HIGH
+using-final-decorator-in-unsupported-version:20:5:MyClass2.my_method:typing.final is not supported by all versions included in the py-version setting:HIGH
+using-final-decorator-in-unsupported-version:25:1:MyClass3:typing.final is not supported by all versions included in the py-version setting:HIGH
+using-final-decorator-in-unsupported-version:27:5:MyClass3.my_method:typing.final is not supported by all versions included in the py-version setting:HIGH
+using-final-decorator-in-unsupported-version:32:1:MyClass4:typing.final is not supported by all versions included in the py-version setting:HIGH
+using-final-decorator-in-unsupported-version:34:5:MyClass4.my_method:typing.final is not supported by all versions included in the py-version setting:HIGH