Skip to content

Feat: --known-enum-bases option to support custom enum-like base cl…#3513

Merged
sobolevn merged 3 commits intowemake-services:masterfrom
imtoopunkforyou:issues/3497
Aug 25, 2025
Merged

Feat: --known-enum-bases option to support custom enum-like base cl…#3513
sobolevn merged 3 commits intowemake-services:masterfrom
imtoopunkforyou:issues/3497

Conversation

@imtoopunkforyou
Copy link
Copy Markdown
Contributor

Checklist

  • I have double checked that there are no unrelated changes in this pull request (old patches, accidental config files, etc)
  • I have created at least one test case for the changes I have made
  • I have updated the documentation for the changes I have made
  • I have added my changes to the CHANGELOG.md

Related issues

@codecov
Copy link
Copy Markdown

codecov Bot commented Aug 23, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (f4c70bc) to head (f13641b).
⚠️ Report is 42 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff            @@
##            master     #3513   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          363       363           
  Lines        12033     12071   +38     
  Branches       820       823    +3     
=========================================
+ Hits         12033     12071   +38     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@imtoopunkforyou
Copy link
Copy Markdown
Contributor Author

@CodiumAI-Agent /improve

@QodoAI-Agent
Copy link
Copy Markdown

QodoAI-Agent commented Aug 23, 2025

PR Code Suggestions ✨

Latest suggestions up to f13641b

CategorySuggestion                                                                                                                                    Impact
Possible issue
Make normalization order-stable

Preserve the intended insertion order for normalized to avoid non-deterministic
behavior caused by converting a set to a tuple. Replace the set-comprehension with
an ordered, duplicate-filtering approach. This ensures consistent matching and
stable test results across runs and Python versions.

wemake_python_styleguide/logic/naming/enums.py [87-92]

 enum_bases = config.known_enum_bases
 if enum_bases:
-    normalized: tuple[str, ...] = tuple({
-        name for base in enum_bases for name in (base, base.split('.')[-1])
-    })
+    seen: set[str] = set()
+    normalized_list: list[str] = []
+    for base in enum_bases:
+        for name in (base, base.split('.')[-1]):
+            if name not in seen:
+                seen.add(name)
+                normalized_list.append(name)
+    normalized: tuple[str, ...] = tuple(normalized_list)
     return _has_one_of_base_classes(defn, normalized)
Suggestion importance[1-10]: 7

__

Why: Replacing a set-comprehension with an order-preserving de-duplication avoids non-determinism from set ordering and stabilizes behavior; the fix correctly reflects the intended change and fits the surrounding code.

Medium
General
Cache normalized enum bases

Avoid repeating normalization work for each class by caching the expanded
known_enum_bases on the options object. This reduces per-class overhead in large
codebases where the visitor processes many classes. Use a simple attribute cache
with a fallback computation.

wemake_python_styleguide/logic/naming/enums.py [74-94]

 def has_enum_like_base_with_config(
     defn: ast.ClassDef,
     config: ValidatedOptions,
 ) -> bool:
     """
     Tells if some class has `Enum` or semantically similar class as its base.
 
     This function also checks user-defined enum-like base classes from
     configuration.
     """
     if has_enum_like_base(defn):
         return True
 
     enum_bases = config.known_enum_bases
     if enum_bases:
-        normalized: tuple[str, ...] = tuple({
-            name for base in enum_bases for name in (base, base.split('.')[-1])
-        })
-        return _has_one_of_base_classes(defn, normalized)
+        # Cache expanded enum bases on the options object to avoid recomputation:
+        expanded = getattr(config, '_expanded_known_enum_bases', None)
+        if expanded is None:
+            seen: set[str] = set()
+            normalized_list: list[str] = []
+            for base in enum_bases:
+                for name in (base, base.split('.')[-1]):
+                    if name not in seen:
+                        seen.add(name)
+                        normalized_list.append(name)
+            expanded = tuple(normalized_list)
+            setattr(config, '_expanded_known_enum_bases', expanded)
+        return _has_one_of_base_classes(defn, expanded)
 
     return False
Suggestion importance[1-10]: 5

__

Why: Caching can reduce repeated normalization overhead and the code is consistent with the PR, but it mutates the options object with ad-hoc attributes and offers minor performance gains for typical cases.

Low

Previous suggestions

Suggestions up to commit f13641b
CategorySuggestion                                                                                                                                    Impact
Possible issue
Preserve order; avoid set dedup

Preserve the configured order and avoid set-based deduplication to prevent
accidental loss of intended precedence and to keep duplicate dotted/short names
harmless. Use an ordered sequence and only append the short-name variant when it
differs.

wemake_python_styleguide/logic/naming/enums.py [87-92]

 enum_bases = config.known_enum_bases
 if enum_bases:
-    normalized: tuple[str, ...] = tuple({
-        name for base in enum_bases for name in (base, base.split('.')[-1])
-    })
+    normalized_list: list[str] = []
+    for base in enum_bases:
+        normalized_list.append(base)
+        short = base.split('.')[-1]
+        if short != base:
+            normalized_list.append(short)
+    normalized: tuple[str, ...] = tuple(normalized_list)
     return _has_one_of_base_classes(defn, normalized)
Suggestion importance[1-10]: 6

__

Why: The existing code uses a set, which needlessly reorders items; preserving order while adding short-name variants is a sensible, low-risk improvement, though order is unlikely to affect correctness here.

Low
Suggestions up to commit f13641b
CategorySuggestion                                                                                                                                    Impact
General
Add missing enum import in test

The test code references enum.Enum without importing enum, causing parsing or
runtime errors in test setup. Inline an import enum in the snippet to ensure the AST
parses consistently across environments.

tests/test_visitors/test_ast/test_naming/test_class_attributes.py [48-56]

 custom_enum_base_class = """
+import enum
+
 class BaseEnum(enum.Enum):
     def __init__(self, short: str, long: str) -> None:
         self.short = short
         self.long = long
 
 class ConcreteEnum(BaseEnum):
     HELLO = 'h', 'hello'
     WORLD = 'w', 'world'
 """
Suggestion importance[1-10]: 8

__

Why: The snippet uses enum.Enum without importing enum; adding the import makes the test snippet self-contained and prevents parse/runtime errors, directly improving test correctness.

Medium
Import enum in multi-base test

This snippet also uses enum.Enum without importing enum, which will break AST
parsing in tests. Include import enum at the top of the snippet to make tests
reliable.

tests/test_visitors/test_ast/test_naming/test_class_attributes.py [60-75]

 multiple_custom_enum_bases = """
+import enum
+
 class FirstEnum(enum.Enum):
     pass
 
 class SecondEnum(enum.Enum):
     pass
 
 class FirstEnumSuccessor(FirstEnum):
     HELLO = 'hello'
     WORLD = 'world'
 
 class SecondEnumSuccessor(SecondEnum):
     PING = 'pong'
 
 class MultiInheritanceEnum(FirstEnum, SecondEnum):
     MULTI = 'multi'
 """
Suggestion importance[1-10]: 8

__

Why: This snippet also references enum.Enum without an import; adding import enum is correct and important to ensure the AST parses and tests run reliably.

Medium
Possible issue
Ensure deterministic, trimmed base matching

Avoid using a set to build normalized because it makes matching order
non-deterministic and may drop duplicates that could be significant. Also strip
whitespace from configured names to prevent subtle mismatches. Build a tuple
deterministically while de-duplicating conservatively.

wemake_python_styleguide/logic/naming/enums.py [87-92]

 enum_bases = config.known_enum_bases
 if enum_bases:
-    normalized: tuple[str, ...] = tuple({
-        name for base in enum_bases for name in (base, base.split('.')[-1])
-    })
+    names: list[str] = []
+    for base in enum_bases:
+        b = base.strip()
+        if not b:
+            continue
+        for name in (b, b.split('.')[-1]):
+            if name not in names:
+                names.append(name)
+    normalized: tuple[str, ...] = tuple(names)
     return _has_one_of_base_classes(defn, normalized)
Suggestion importance[1-10]: 6

__

Why: The change avoids set-induced nondeterminism and trims whitespace, which is a reasonable robustness improvement; it's accurate and aligns with the code context, but impact is moderate since order likely doesn't affect matching.

Low
Suggestions up to commit 2fdedc7
CategorySuggestion                                                                                                                                    Impact
Possible issue
Normalize configured enum base names

Normalize configured base names to match how _has_one_of_base_classes expects fully
qualified and short names. Without normalization, users passing plain names like
MyEnum or qualified ones inconsistently may not match bases like module.MyEnum.
Expand the configured names to include both the raw and module-qualified variants as
supported.

wemake_python_styleguide/logic/naming/enums.py [74-90]

 def has_enum_like_base_with_config(
     defn: ast.ClassDef,
     config: ValidatedOptions,
 ) -> bool:
     """
     Tells if some class has `Enum` or semantically similar class as its base.
 
     This function also checks user-defined enum-like base classes from
     configuration.
     """
     if has_enum_like_base(defn):
         return True
 
-    if config.known_enum_bases:
-        return _has_one_of_base_classes(defn, config.known_enum_bases)
+    bases = tuple(config.known_enum_bases or ())
+    if bases:
+        # Add both short and dotted variants for robustness:
+        normalized: tuple[str, ...] = tuple({
+            name for base in bases for name in {base, base.split('.')[-1]}
+        })
+        return _has_one_of_base_classes(defn, normalized)
 
     return False
Suggestion importance[1-10]: 6

__

Why: The suggestion is plausible and could improve matching by handling both dotted and short base names, but the current code may already work with short names and there's no evidence _has_one_of_base_classes requires this normalization. Impact is moderate, not critical.

Low

options,
):
"""Testing that custom enum base classes work with configuration."""
code = """
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we define examples on a module level

Copy link
Copy Markdown
Contributor Author

@imtoopunkforyou imtoopunkforyou Aug 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in f13641b

Copy link
Copy Markdown
Member

@sobolevn sobolevn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, also don't forget to add new configuration option to the related violations docs.

Comment on lines +89 to +92
normalized: tuple[str, ...] = tuple({
name for base in enum_bases for name in (base, base.split('.')[-1])
})
return _has_one_of_base_classes(defn, normalized)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
normalized: tuple[str, ...] = tuple({
name for base in enum_bases for name in (base, base.split('.')[-1])
})
return _has_one_of_base_classes(defn, normalized)
return _has_one_of_base_classes(
defn,
tuple({
name
for base in enum_bases
for name in (base, base.split('.')[-1])
}),
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

: tuple[str, ...] annotation looks suspicious :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll fix it next week.

@imtoopunkforyou
Copy link
Copy Markdown
Contributor Author

Please, also don't forget to add new configuration option to the related violations docs.

oh, yes, okay. I'll add it in a week

Copy link
Copy Markdown
Member

@sobolevn sobolevn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to make a release, with this feature in it :)
You can address issues in the follow-up PR, since they are minor.

@sobolevn sobolevn merged commit c5c229c into wemake-services:master Aug 25, 2025
9 checks passed
@imtoopunkforyou
Copy link
Copy Markdown
Contributor Author

I want to make a release, with this feature in it :) You can address issues in the follow-up PR, since they are minor.

I'll create a new PR with a link to this one as soon as I make the changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

When inheriting an enum, WPS115 occurs.

3 participants