Repository: wemake-services/wemake-python-styleguide

Issue:
Make abstract classes abstract
There are several classes that are not marked as `abstract`. But, they should be!

Like:
- https://github.com/wemake-services/wemake-python-styleguide/blob/master/wemake_python_styleguide/visitors/base.py
- https://github.com/wemake-services/wemake-python-styleguide/blob/master/wemake_python_styleguide/violations/base.py

We need to import `abc` and use `metaclass=ABCMeta` for all of them.


Discussion:
I can take it.

We just need to establish how mark class attribute as an abstract:

+ leave it as it is, so:

   ```python
   error_template: ClassVar[str]
   ```

+ use `abc.abstractmethod` and `property` decorators:

   ```python
   @property
   @abc.abstractmethod
   def error_template(self) -> str:
       ""Message that will be shown to user after formatting"""
   ```

   however then exceptions can be misleading: `TypeError: Can't instantiate abstract class 
   TestViolation with abstract methods error_template`

+ prepare some custom metaclass or `__init_subclass__` implementation to check if certain class attributes are present

What's your thoughts?
I think that just making classes abstract is good enough. Thanks a lot for your help!

Resolving patch:
diff --git a/wemake_python_styleguide/violations/base.py b/wemake_python_styleguide/violations/base.py
--- a/wemake_python_styleguide/violations/base.py
+++ b/wemake_python_styleguide/violations/base.py
@@ -39,6 +39,7 @@
 
 """
 
+import abc
 import ast
 import tokenize
 from typing import ClassVar, Optional, Set, Tuple, Union
@@ -53,7 +54,7 @@
 ]
 
 
-class BaseViolation(object):
+class BaseViolation(object, metaclass=abc.ABCMeta):
     """
     Abstract base class for all style violations.
 
@@ -120,7 +121,7 @@ def _location(self) -> Tuple[int, int]:
         return 0, 0
 
 
-class _BaseASTViolation(BaseViolation):
+class _BaseASTViolation(BaseViolation, metaclass=abc.ABCMeta):
     """Used as a based type for all ``ast`` violations."""
 
     _node: Optional[ast.AST]
@@ -132,13 +133,13 @@ def _location(self) -> Tuple[int, int]:
         return line_number, column_offset
 
 
-class ASTViolation(_BaseASTViolation):
+class ASTViolation(_BaseASTViolation, metaclass=abc.ABCMeta):
     """Violation for ``ast`` based style visitors."""
 
     _node: ast.AST
 
 
-class MaybeASTViolation(_BaseASTViolation):
+class MaybeASTViolation(_BaseASTViolation, metaclass=abc.ABCMeta):
     """
     Violation for ``ast`` and modules visitors.
 
@@ -151,7 +152,7 @@ def __init__(self, node=None, text: Optional[str] = None) -> None:
         super().__init__(node, text=text)
 
 
-class TokenizeViolation(BaseViolation):
+class TokenizeViolation(BaseViolation, metaclass=abc.ABCMeta):
     """Violation for ``tokenize`` based visitors."""
 
     _node: tokenize.TokenInfo
@@ -161,7 +162,7 @@ def _location(self) -> Tuple[int, int]:
         return self._node.start
 
 
-class SimpleViolation(BaseViolation):
+class SimpleViolation(BaseViolation, metaclass=abc.ABCMeta):
     """Violation for cases where there's no associated nodes."""
 
     _node: None
diff --git a/wemake_python_styleguide/visitors/base.py b/wemake_python_styleguide/visitors/base.py
--- a/wemake_python_styleguide/visitors/base.py
+++ b/wemake_python_styleguide/visitors/base.py
@@ -62,6 +62,7 @@
 
 """
 
+import abc
 import ast
 import tokenize
 from typing import List, Sequence, Type
@@ -74,7 +75,7 @@
 from wemake_python_styleguide.violations.base import BaseViolation
 
 
-class BaseVisitor(object):
+class BaseVisitor(object, metaclass=abc.ABCMeta):
     """
     Abstract base class for different types of visitors.
 
@@ -118,15 +119,14 @@ def add_violation(self, violation: BaseViolation) -> None:
         """Adds violation to the visitor."""
         self.violations.append(violation)
 
+    @abc.abstractmethod
     def run(self) -> None:
         """
-        Abstract method to run a visitor.
+        Runs a visitor.
 
         Each visitor should know what exactly it needs
         to do when it was told to ``run``.
-        This method should be defined in all subclasses.
         """
-        raise NotImplementedError('Should be defined in a subclass')
 
     def _post_visit(self) -> None:
         """
@@ -137,7 +137,7 @@ def _post_visit(self) -> None:
         """
 
 
-class BaseNodeVisitor(ast.NodeVisitor, BaseVisitor):
+class BaseNodeVisitor(ast.NodeVisitor, BaseVisitor, metaclass=abc.ABCMeta):
     """
     Allows to store violations while traversing node tree.
 
@@ -179,7 +179,7 @@ def run(self) -> None:
         self._post_visit()
 
 
-class BaseFilenameVisitor(BaseVisitor):
+class BaseFilenameVisitor(BaseVisitor, metaclass=abc.ABCMeta):
     """
     Abstract base class that allows to visit and check module file names.
 
@@ -192,13 +192,9 @@ class BaseFilenameVisitor(BaseVisitor):
 
     stem: str
 
+    @abc.abstractmethod
     def visit_filename(self) -> None:
-        """
-        Abstract method to check module file names.
-
-        This method should be overridden in a subclass.
-        """
-        raise NotImplementedError('Should be defined in a subclass')
+        """Checks module file names."""
 
     @final
     def run(self) -> None:
@@ -215,7 +211,7 @@ def run(self) -> None:
             self._post_visit()
 
 
-class BaseTokenVisitor(BaseVisitor):
+class BaseTokenVisitor(BaseVisitor, metaclass=abc.ABCMeta):
     """
     Allows to check ``tokenize`` sequences.
 


Test patch:
diff --git a/tests/test_visitors/test_base.py b/tests/test_visitors/test_base.py
--- a/tests/test_visitors/test_base.py
+++ b/tests/test_visitors/test_base.py
@@ -2,30 +2,21 @@
 
 from unittest.mock import MagicMock
 
-import pytest
-
 from wemake_python_styleguide import constants
-from wemake_python_styleguide.visitors.base import (
-    BaseFilenameVisitor,
-    BaseVisitor,
-)
-
-
-def test_visitor_raises_not_implemented(default_options):
-    """Ensures that `BaseChecker` raises `NotImplementedError`."""
-    with pytest.raises(NotImplementedError):
-        BaseVisitor(default_options).run()
+from wemake_python_styleguide.visitors.base import BaseFilenameVisitor
 
 
-def test_base_filename_raises_not_implemented(default_options):
-    """Ensures that `BaseFilenameVisitor` raises `NotImplementedError`."""
-    with pytest.raises(NotImplementedError):
-        BaseFilenameVisitor(default_options, filename='some.py').run()
+class _TestingFilenameVisitor(BaseFilenameVisitor):
+    def visit_filename(self):
+        """Overridden to satisfy abstract base class."""
 
 
 def test_base_filename_run_do_not_call_visit(default_options):
     """Ensures that `run()` does not call `visit()` method for stdin."""
-    instance = BaseFilenameVisitor(default_options, filename=constants.STDIN)
+    instance = _TestingFilenameVisitor(
+        default_options,
+        filename=constants.STDIN,
+    )
     instance.visit_filename = MagicMock()
     instance.run()