Skip to content

Commit

Permalink
Add checkers for typing.final for Python version 3.8 or later (#5133)
Browse files Browse the repository at this point in the history
* Add checkers for typing.final for Python version 3.8 or later

- overridden-final-method
- subclassed-final-class

Closes #3197
  • Loading branch information
mbyrnepr2 committed Oct 10, 2021
1 parent 220e27d commit 087fe68
Show file tree
Hide file tree
Showing 9 changed files with 86 additions and 0 deletions.
4 changes: 4 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -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``

Expand Down
8 changes: 8 additions & 0 deletions doc/whatsnew/2.12.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
================
Expand Down
35 changes: 35 additions & 0 deletions pylint/checkers/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
safe_infer,
unimplemented_abstract_methods,
)
from pylint.constants import PY38_PLUS
from pylint.interfaces import IAstroidChecker
from pylint.utils import get_global_option

Expand Down Expand Up @@ -649,6 +650,16 @@ def _has_same_layout_slots(slots, assigned_value):
"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",
Expand Down Expand Up @@ -860,6 +871,7 @@ def visit_classdef(self, node: nodes.ClassDef) -> None:
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):
Expand Down Expand Up @@ -898,6 +910,23 @@ def _check_proper_bases(self, node):
"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:
Expand Down Expand Up @@ -1347,6 +1376,12 @@ def _check_invalid_overridden_method(self, function_node, parent_function_node):
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:
Expand Down
17 changes: 17 additions & 0 deletions tests/functional/o/overridden_final_method_py38.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions tests/functional/o/overridden_final_method_py38.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[testoptions]
min_pyver=3.8
1 change: 1 addition & 0 deletions tests/functional/o/overridden_final_method_py38.txt
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions tests/functional/s/subclassed_final_class_py38.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions tests/functional/s/subclassed_final_class_py38.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[testoptions]
min_pyver=3.8
1 change: 1 addition & 0 deletions tests/functional/s/subclassed_final_class_py38.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
subclassed-final-class:15:0:Subclass:"Class 'Subclass' is a subclass of a class decorated with typing.final: 'Base'":HIGH

0 comments on commit 087fe68

Please sign in to comment.