summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Byrne <31762852+mbyrnepr2@users.noreply.github.com>2021-10-10 09:08:31 +0200
committerGitHub <noreply@github.com>2021-10-10 09:08:31 +0200
commit087fe6856aac60c8781106c96a2b014fedffffb8 (patch)
tree0775e4f7c2d7c3e6d61f5504615c126235fef28e
parent220e27dc5bdd6bdd9dbee56d5c7d33a946c8ad17 (diff)
downloadpylint-git-087fe6856aac60c8781106c96a2b014fedffffb8.tar.gz
Add checkers for typing.final for Python version 3.8 or later (#5133)
* Add checkers for typing.final for Python version 3.8 or later - overridden-final-method - subclassed-final-class Closes #3197
-rw-r--r--ChangeLog4
-rw-r--r--doc/whatsnew/2.12.rst8
-rw-r--r--pylint/checkers/classes.py35
-rw-r--r--tests/functional/o/overridden_final_method_py38.py17
-rw-r--r--tests/functional/o/overridden_final_method_py38.rc2
-rw-r--r--tests/functional/o/overridden_final_method_py38.txt1
-rw-r--r--tests/functional/s/subclassed_final_class_py38.py16
-rw-r--r--tests/functional/s/subclassed_final_class_py38.rc2
-rw-r--r--tests/functional/s/subclassed_final_class_py38.txt1
9 files changed, 86 insertions, 0 deletions
diff --git a/ChangeLog b/ChangeLog
index 464a62121..30de6af72 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -11,6 +11,10 @@ Release date: TBA
..
Put new features here and also in 'doc/whatsnew/2.12.rst'
+* Add checkers ``overridden-final-method`` & ``subclassed-final-class``
+
+ Closes #3197
+
* Added support for ``ModuleNotFoundError`` (``import-error`` and ``no-name-in-module``).
``ModuleNotFoundError`` inherits from ``ImportError`` and was added in Python ``3.6``
diff --git a/doc/whatsnew/2.12.rst b/doc/whatsnew/2.12.rst
index 494393adb..7cdd12003 100644
--- a/doc/whatsnew/2.12.rst
+++ b/doc/whatsnew/2.12.rst
@@ -12,6 +12,14 @@ Summary -- Release highlights
New checkers
============
+* Checkers for ``typing.final``
+
+ * Added ``overridden-final-method``: Emitted when a method which is annotated with ``typing.final`` is overridden
+
+ * Added ``subclassed-final-class``: Emitted when a class which is annotated with ``typing.final`` is subclassed
+
+ Closes #3197
+
Removed checkers
================
diff --git a/pylint/checkers/classes.py b/pylint/checkers/classes.py
index 14e0ef635..489bb5063 100644
--- a/pylint/checkers/classes.py
+++ b/pylint/checkers/classes.py
@@ -79,6 +79,7 @@ from pylint.checkers.utils import (
safe_infer,
unimplemented_abstract_methods,
)
+from pylint.constants import PY38_PLUS
from pylint.interfaces import IAstroidChecker
from pylint.utils import get_global_option
@@ -649,6 +650,16 @@ MSGS = { # pylint: disable=consider-using-namedtuple-or-dataclass
"unused-private-member",
"Emitted when a private member of a class is defined but not used.",
),
+ "W0239": (
+ "Method %r overrides a method decorated with typing.final which is defined in class %r",
+ "overridden-final-method",
+ "Used when a method decorated with typing.final has been overridden.",
+ ),
+ "W0240": (
+ "Class %r is a subclass of a class decorated with typing.final: %r",
+ "subclassed-final-class",
+ "Used when a class decorated with typing.final has been subclassed.",
+ ),
"E0236": (
"Invalid object %r in __slots__, must contain only non empty strings",
"invalid-slots-object",
@@ -860,6 +871,7 @@ a metaclass class method.",
self.add_message("no-init", args=node, node=node)
self._check_slots(node)
self._check_proper_bases(node)
+ self._check_typing_final(node)
self._check_consistent_mro(node)
def _check_consistent_mro(self, node):
@@ -898,6 +910,23 @@ a metaclass class method.",
"useless-object-inheritance", args=node.name, node=node
)
+ 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:
+ 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"]
+ ):
+ self.add_message(
+ "subclassed-final-class",
+ args=(node.name, ancestor.name),
+ node=node,
+ )
+
@check_messages("unused-private-member", "attribute-defined-outside-init")
def leave_classdef(self, node: nodes.ClassDef) -> None:
"""close a class node:
@@ -1347,6 +1376,12 @@ 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:
+ self.add_message(
+ "overridden-final-method",
+ args=(function_node.name, parent_function_node.parent.name),
+ node=function_node,
+ )
def _check_slots(self, node):
if "__slots__" not in node.locals:
diff --git a/tests/functional/o/overridden_final_method_py38.py b/tests/functional/o/overridden_final_method_py38.py
new file mode 100644
index 000000000..d951c26da
--- /dev/null
+++ b/tests/functional/o/overridden_final_method_py38.py
@@ -0,0 +1,17 @@
+"""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
+
+from typing import final
+
+class Base:
+ @final
+ def my_method(self):
+ pass
+
+
+class Subclass(Base):
+ def my_method(self): # [overridden-final-method]
+ pass
diff --git a/tests/functional/o/overridden_final_method_py38.rc b/tests/functional/o/overridden_final_method_py38.rc
new file mode 100644
index 000000000..85fc502b3
--- /dev/null
+++ b/tests/functional/o/overridden_final_method_py38.rc
@@ -0,0 +1,2 @@
+[testoptions]
+min_pyver=3.8
diff --git a/tests/functional/o/overridden_final_method_py38.txt b/tests/functional/o/overridden_final_method_py38.txt
new file mode 100644
index 000000000..2c8bca442
--- /dev/null
+++ b/tests/functional/o/overridden_final_method_py38.txt
@@ -0,0 +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
diff --git a/tests/functional/s/subclassed_final_class_py38.py b/tests/functional/s/subclassed_final_class_py38.py
new file mode 100644
index 000000000..816ef537e
--- /dev/null
+++ b/tests/functional/s/subclassed_final_class_py38.py
@@ -0,0 +1,16 @@
+"""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
+
+from typing import final
+
+
+@final
+class Base:
+ pass
+
+
+class Subclass(Base): # [subclassed-final-class]
+ pass
diff --git a/tests/functional/s/subclassed_final_class_py38.rc b/tests/functional/s/subclassed_final_class_py38.rc
new file mode 100644
index 000000000..85fc502b3
--- /dev/null
+++ b/tests/functional/s/subclassed_final_class_py38.rc
@@ -0,0 +1,2 @@
+[testoptions]
+min_pyver=3.8
diff --git a/tests/functional/s/subclassed_final_class_py38.txt b/tests/functional/s/subclassed_final_class_py38.txt
new file mode 100644
index 000000000..46fb5200e
--- /dev/null
+++ b/tests/functional/s/subclassed_final_class_py38.txt
@@ -0,0 +1 @@
+subclassed-final-class:15:0:Subclass:"Class 'Subclass' is a subclass of a class decorated with typing.final: 'Base'":HIGH