Repository: wemake-services/wemake-python-styleguide

Issue:
cache all things dependent on configuration options
Hi,

in #1129 domain's names options that allow developers to modify variable names' blacklist were introduced. Construction of this blacklist can be performed only once, because it depends only on some constants and configuration options, that are also static (loaded from CLI flags or ini config), so there is no need to compute them for every file being checked.
We already have discussed using `lru_cache` decorator on `variable_names_blacklist_from` function, but because of my misunderstanding of flake8 and `Checker` lifecycle I haven't used it.
I have `grep`ped `logic` module to check if there is any other usage of configuration options that can be cached, but I haven't found anything else.
Can I add `lru_cache` decorator this time? Well, better late than never :smile:  

Regards,
Łukasz  


Discussion:
Thanks a lot for the issue! Yes, indeed: we can boost things here.
I will prepare PR on the weekend
@skarzi thanks a lot for your **massive** help!

Resolving patch:
diff --git a/wemake_python_styleguide/logic/naming/blacklists.py b/wemake_python_styleguide/logic/naming/blacklists.py
--- a/wemake_python_styleguide/logic/naming/blacklists.py
+++ b/wemake_python_styleguide/logic/naming/blacklists.py
@@ -1,11 +1,13 @@
 # -*- coding: utf-8 -*-
 
+from functools import lru_cache
 from typing import FrozenSet
 
 from wemake_python_styleguide.constants import VARIABLE_NAMES_BLACKLIST
 from wemake_python_styleguide.types import ConfigurationOptions
 
 
+@lru_cache()
 def variable_names_blacklist_from(
     options: ConfigurationOptions,
 ) -> FrozenSet[str]:
diff --git a/wemake_python_styleguide/options/validation.py b/wemake_python_styleguide/options/validation.py
--- a/wemake_python_styleguide/options/validation.py
+++ b/wemake_python_styleguide/options/validation.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 
-from typing import List, Optional
+from typing import Optional, Tuple
 
 import attr
 from typing_extensions import final
@@ -26,8 +26,8 @@ def factory(instance, attribute, field_value):
 
 
 def validate_domain_names_options(
-    allowed_domain_names: List[str],
-    forbidden_domain_names: List[str],
+    allowed_domain_names: Tuple[str, ...],
+    forbidden_domain_names: Tuple[str, ...],
 ) -> None:
     """Validator to check that allowed and forbidden names doesn't intersect.
 
@@ -49,7 +49,7 @@ def validate_domain_names_options(
 
 
 @final
-@attr.dataclass(slots=True)
+@attr.dataclass(slots=True, frozen=True)
 class _ValidatedOptions(object):
     """
     Here we write all the required structured validation for the options.
@@ -64,9 +64,9 @@ class _ValidatedOptions(object):
     max_noqa_comments: int = attr.ib(
         validator=[_min_max(min=1, max=defaults.MAX_NOQA_COMMENTS)],
     )
-    nested_classes_whitelist: List[str]
-    allowed_domain_names: List[str]
-    forbidden_domain_names: List[str]
+    nested_classes_whitelist: Tuple[str, ...] = attr.ib(converter=tuple)
+    allowed_domain_names: Tuple[str, ...] = attr.ib(converter=tuple)
+    forbidden_domain_names: Tuple[str, ...] = attr.ib(converter=tuple)
 
     # Complexity:
     max_arguments: int = attr.ib(validator=[_min_max(min=1)])
diff --git a/wemake_python_styleguide/types.py b/wemake_python_styleguide/types.py
--- a/wemake_python_styleguide/types.py
+++ b/wemake_python_styleguide/types.py
@@ -38,7 +38,7 @@
 """
 
 import ast
-from typing import List, Tuple, Type, Union
+from typing import Tuple, Type, Union
 
 from typing_extensions import Protocol, final
 
@@ -83,7 +83,7 @@
 ]
 
 
-@final
+@final  # noqa: WPS214
 class ConfigurationOptions(Protocol):
     """
     Provides structure for the options we use in our checker and visitors.
@@ -98,35 +98,93 @@ class or structure.
     """
 
     # General:
-    min_name_length: int
-    i_control_code: bool
-    max_name_length: int
-    max_noqa_comments: int
-    nested_classes_whitelist: List[str]
-    allowed_domain_names: List[str]
-    forbidden_domain_names: List[str]
+    @property
+    def min_name_length(self) -> int: ...
+
+    @property
+    def i_control_code(self) -> bool: ...
+
+    @property
+    def max_name_length(self) -> int: ...
+
+    @property
+    def max_noqa_comments(self) -> int: ...
+
+    @property
+    def nested_classes_whitelist(self) -> Tuple[str, ...]: ...
+
+    @property
+    def allowed_domain_names(self) -> Tuple[str, ...]: ...
+
+    @property
+    def forbidden_domain_names(self) -> Tuple[str, ...]: ...
 
     # Complexity:
-    max_arguments: int
-    max_local_variables: int
-    max_returns: int
-    max_expressions: int
-    max_module_members: int
-    max_methods: int
-    max_line_complexity: int
-    max_jones_score: int
-    max_imports: int
-    max_imported_names: int
-    max_base_classes: int
-    max_decorators: int
-    max_string_usages: int
-    max_awaits: int
-    max_try_body_length: int
-    max_module_expressions: int
-    max_function_expressions: int
-    max_asserts: int
-    max_access_level: int
-    max_attributes: int
-    max_cognitive_score: int
-    max_cognitive_average: int
-    max_call_level: int
+    @property
+    def max_arguments(self) -> int: ...
+
+    @property
+    def max_local_variables(self) -> int: ...
+
+    @property
+    def max_returns(self) -> int: ...
+
+    @property
+    def max_expressions(self) -> int: ...
+
+    @property
+    def max_module_members(self) -> int: ...
+
+    @property
+    def max_methods(self) -> int: ...
+
+    @property
+    def max_line_complexity(self) -> int: ...
+
+    @property
+    def max_jones_score(self) -> int: ...
+
+    @property
+    def max_imports(self) -> int: ...
+
+    @property
+    def max_imported_names(self) -> int: ...
+
+    @property
+    def max_base_classes(self) -> int: ...
+
+    @property
+    def max_decorators(self) -> int: ...
+
+    @property
+    def max_string_usages(self) -> int: ...
+
+    @property
+    def max_awaits(self) -> int: ...
+
+    @property
+    def max_try_body_length(self) -> int: ...
+
+    @property
+    def max_module_expressions(self) -> int: ...
+
+    @property
+    def max_function_expressions(self) -> int: ...
+
+    @property
+    def max_asserts(self) -> int: ...
+
+    @property
+    def max_access_level(self) -> int: ...
+
+    @property
+    def max_attributes(self) -> int: ...
+
+    @property
+    def max_cognitive_score(self) -> int: ...
+
+    @property
+    def max_cognitive_average(self) -> int: ...
+
+    @property
+    def max_call_level(self) -> int: ...


Test patch:
diff --git a/tests/test_visitors/test_ast/test_naming/test_naming_rules/test_correct.py b/tests/test_visitors/test_ast/test_naming/test_naming_rules/test_correct.py
--- a/tests/test_visitors/test_ast/test_naming/test_naming_rules/test_correct.py
+++ b/tests/test_visitors/test_ast/test_naming/test_naming_rules/test_correct.py
@@ -45,7 +45,7 @@ def test_name_in_allowed_domain_names_option(
     tree = parse_ast_tree(mode(naming_template.format(allowed_name)))
 
     visitor = WrongNameVisitor(
-        options(allowed_domain_names=[allowed_name]),
+        options(allowed_domain_names=(allowed_name,)),
         tree=tree,
     )
     visitor.run()
diff --git a/tests/test_visitors/test_ast/test_naming/test_naming_rules/test_wrong_names.py b/tests/test_visitors/test_ast/test_naming/test_naming_rules/test_wrong_names.py
--- a/tests/test_visitors/test_ast/test_naming/test_naming_rules/test_wrong_names.py
+++ b/tests/test_visitors/test_ast/test_naming/test_naming_rules/test_wrong_names.py
@@ -49,7 +49,7 @@ def test_name_in_forbidden_domain_names_option(
     tree = parse_ast_tree(mode(naming_template.format(forbidden_name)))
 
     visitor = WrongNameVisitor(
-        options(forbidden_domain_names=[forbidden_name]),
+        options(forbidden_domain_names=(forbidden_name,)),
         tree=tree,
     )
     visitor.run()