Repository: wemake-services/wemake-python-styleguide

Issue:
Allow str + Enum for WPS600 Forbid subclassing lowercase builtins.
# Rule request

"WPS600 Forbid subclassing lowercase builtins" should allow the following

```python
class PizzaToppings(str, Enum):
    MARGHERITA = "MARGHERITA"
    MARINARA = "MARINARA"
```

## Thesis

Enums are a bit akwkard in Python. The `class PizzaToppings(str, Enum):...` construct allows to create what is basically a string equivalent of IntEnum, so that one can do

```python
if current_topping == PizzaToppings.MARINARA:
    ...
```
 
## Reasoning

It seems beneficial, farily widely used, and has no negative side effects I can think of. No reason to ban it. 

Thanks.

<!--

  Love wemake-python-styleguide? ❤️
  Do you want your features to be implemented faster?

  Please consider supporting our collective:
👉  https://opencollective.com/wemake-python-styleguide/donate

-->


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e7cea4e9..cd599da5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -80,6 +80,7 @@ Semantic versioning in our case means:
 - Allows any compares in `assert` statements for `WPS520`, #3112
 - Allows walrus operator (`:=`) in comprehesions #3121
 - Allows `pass` in `case` bodies, #2642
+- Allows subclassing builtins in `WPS600`, when creating an `Enum`, #2506
 - Allows using variables after blocks for `WPS441` in `assert` statements, #2543
 - Does not count `self`, `cls`, and `mcs` as arguments
   for `WPS211` complexity check anymore, #2394
diff --git a/wemake_python_styleguide/logic/naming/enums.py b/wemake_python_styleguide/logic/naming/enums.py
new file mode 100644
index 00000000..5ae0bab5
--- /dev/null
+++ b/wemake_python_styleguide/logic/naming/enums.py
@@ -0,0 +1,17 @@
+import ast
+from typing import Final
+
+_ENUM_NAMES: Final = (
+    'enum.Enum',
+    'enum.EnumType',
+    'enum.EnumMeta',
+    'Enum',
+    'EnumType',
+    'EnumMeta',
+)
+
+
+def has_enum_base(defn: ast.ClassDef) -> bool:
+    """Tells whether some class has `Enum` or similar class as its base."""
+    string_bases = {ast.unparse(base) for base in defn.bases}
+    return any(enum_base in string_bases for enum_base in _ENUM_NAMES)
diff --git a/wemake_python_styleguide/visitors/ast/classes.py b/wemake_python_styleguide/visitors/ast/classes.py
index be996952..b9292ff1 100644
--- a/wemake_python_styleguide/visitors/ast/classes.py
+++ b/wemake_python_styleguide/visitors/ast/classes.py
@@ -9,7 +9,7 @@ from wemake_python_styleguide.compat.aliases import AssignNodes, FunctionNodes
 from wemake_python_styleguide.compat.functions import get_assign_targets
 from wemake_python_styleguide.logic import nodes, source, walk
 from wemake_python_styleguide.logic.arguments import function_args, super_args
-from wemake_python_styleguide.logic.naming import access, name_nodes
+from wemake_python_styleguide.logic.naming import access, enums, name_nodes
 from wemake_python_styleguide.logic.tree import (
     attributes,
     classes,
@@ -77,7 +77,9 @@ class WrongClassDefVisitor(base.BaseNodeVisitor):
 
         if id_attr == 'BaseException':
             self.add_violation(bp.BaseExceptionSubclassViolation(node))
-        elif classes.is_forbidden_super_class(id_attr):
+        elif classes.is_forbidden_super_class(
+            id_attr,
+        ) and not enums.has_enum_base(node):
             self.add_violation(
                 oop.BuiltinSubclassViolation(node, text=id_attr),
             )


Test patch:
diff --git a/tests/test_visitors/test_ast/test_classes/test_base_classes/test_base_builtin_classes.py b/tests/test_visitors/test_ast/test_classes/test_base_classes/test_base_builtin_classes.py
index 307572b6..675d85cd 100644
--- a/tests/test_visitors/test_ast/test_classes/test_base_classes/test_base_builtin_classes.py
+++ b/tests/test_visitors/test_ast/test_classes/test_base_classes/test_base_builtin_classes.py
@@ -3,10 +3,12 @@ import pytest
 from wemake_python_styleguide.violations.oop import BuiltinSubclassViolation
 from wemake_python_styleguide.visitors.ast.classes import WrongClassDefVisitor
 
-class_with_base = """
-class TestClass({0}):
-    '''Docs.'''
-"""
+class_with_base = 'class TestClass({0}): ...'
+
+enum_with_primitive1 = 'class First({0}, enum.Enum): ...'
+enum_with_primitive2 = 'class First({0}, Enum): ...'
+enum_with_primitive3 = 'class First({0}, EnumMeta): ...'
+enum_with_primitive4 = 'class First({0}, enum.EnumType): ...'
 
 
 @pytest.mark.parametrize(
@@ -57,3 +59,39 @@ def test_regular_subclass(
     visitor.run()
 
     assert_errors(visitor, [])
+
+
+@pytest.mark.parametrize(
+    'template',
+    [
+        enum_with_primitive1,
+        enum_with_primitive2,
+        enum_with_primitive3,
+        enum_with_primitive4,
+    ],
+)
+@pytest.mark.parametrize(
+    'code',
+    [
+        'int',
+        'str',
+        'bool',
+        'list',
+        'dict',
+        'float',
+    ],
+)
+def test_builtin_subclass_with_enum(
+    assert_errors,
+    parse_ast_tree,
+    template,
+    code,
+    default_options,
+):
+    """Testing extending `enum` is always fine with primitive."""
+    tree = parse_ast_tree(template.format(code))
+
+    visitor = WrongClassDefVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])