refactor: replace enum with intenum and strenum

This commit is contained in:
Evgenii Alekseev 2023-11-05 01:26:28 +02:00
parent b116e6fa07
commit f51b8e2358
13 changed files with 118 additions and 78 deletions

View File

@ -26,7 +26,7 @@ In order to resolve all difficult cases the `autopep8` is used. You can perform
Again, the most checks can be performed by `make check` command, though some additional guidelines must be applied: Again, the most checks can be performed by `make check` command, though some additional guidelines must be applied:
* Every class, every function (including private and protected), every attribute must be documented. The project follows [Google style documentation](https://google.github.io/styleguide/pyguide.html). The only exception is local functions. * Every class, every function (including private and protected), every attribute must be documented. The project follows [Google style documentation](https://google.github.io/styleguide/pyguide.html). The only exception is local functions.
* Correct way to document function, if section is empty, e.g. no notes or there are no args, it should be omitted: * Correct way to document function (if a section is empty, e.g. no notes or there are no args, it should be omitted) is the following:
```python ```python
def foo(argument: str, *, flag: bool = False) -> int: def foo(argument: str, *, flag: bool = False) -> int:
@ -64,7 +64,7 @@ Again, the most checks can be performed by `make check` command, though some add
```python ```python
class Clazz(BaseClazz): class Clazz(BaseClazz):
""" """
brand-new implementation of ``BaseClazz`` brand-new implementation of :class:`BaseClazz`
Attributes: Attributes:
CLAZZ_ATTRIBUTE(int): (class attribute) a brand-new class attribute CLAZZ_ATTRIBUTE(int): (class attribute) a brand-new class attribute
@ -92,7 +92,7 @@ Again, the most checks can be performed by `make check` command, though some add
* Type annotations are the must, even for local functions. For the function argument `self` (for instance methods) and `cls` (for class methods) should not be annotated. * Type annotations are the must, even for local functions. For the function argument `self` (for instance methods) and `cls` (for class methods) should not be annotated.
* For collection types built-in classes must be used if possible (e.g. `dict` instead of `typing.Dict`, `tuple` instead of `typing.Tuple`). In case if built-in type is not available, but `collections.abc` provides interface, it must be used (e.g. `collections.abc.Awaitable` instead of `typing.Awaitable`, `collections.abc.Iterable` instead of `typing.Iterable`). For union classes, the bar operator (`|`) must be used (e.g. `float | int` instead of `typing.Union[float, int]`), which also includes `typinng.Optional` (e.g. `str | None` instead of `Optional[str]`). * For collection types built-in classes must be used if possible (e.g. `dict` instead of `typing.Dict`, `tuple` instead of `typing.Tuple`). In case if built-in type is not available, but `collections.abc` provides interface, it must be used (e.g. `collections.abc.Awaitable` instead of `typing.Awaitable`, `collections.abc.Iterable` instead of `typing.Iterable`). For union classes, the bar operator (`|`) must be used (e.g. `float | int` instead of `typing.Union[float, int]`), which also includes `typinng.Optional` (e.g. `str | None` instead of `Optional[str]`).
* `classmethod` should always return `Self`. In case of mypy warning (e.g. if there is a branch in which function doesn't return the instance of `cls`) consider using `staticmethod` instead. * `classmethod` should (almost) always return `Self`. In case of mypy warning (e.g. if there is a branch in which function doesn't return the instance of `cls`) consider using `staticmethod` instead.
* Recommended order of function definitions in class: * Recommended order of function definitions in class:
```python ```python
@ -121,10 +121,12 @@ Again, the most checks can be performed by `make check` command, though some add
def __hash__(self) -> int: ... # basically any magic (or look-alike) method def __hash__(self) -> int: ... # basically any magic (or look-alike) method
``` ```
Methods inside one group should be ordered alphabetically, the only exception is `__init__` method (`__post__init__` for dataclasses) which should be defined first. For test methods it is recommended to follow the order in which functions are defined. Methods inside one group should be ordered alphabetically, the only exceptions are `__init__` (`__post_init__` for dataclasses) and `__new__` methods which should be defined first. For test methods it is recommended to follow the order in which functions are defined.
Though, we would like to highlight abstract methods (i.e. ones which raise `NotImplementedError`), we still keep in global order at the moment. Though, we would like to highlight abstract methods (i.e. ones which raise `NotImplementedError`), we still keep in global order at the moment.
For the most cases there is custom `pylint` plugin, which performs checks automatically.
* Abstract methods must raise `NotImplementedError` instead of using `abc.abstractmethod`. The reason behind this restriction is the fact that we have class/static abstract methods for those we need to define their attribute first making the code harder to read. * Abstract methods must raise `NotImplementedError` instead of using `abc.abstractmethod`. The reason behind this restriction is the fact that we have class/static abstract methods for those we need to define their attribute first making the code harder to read.
* For any path interactions `pathlib.Path` must be used. * For any path interactions `pathlib.Path` must be used.
* Configuration interactions must go through `ahriman.core.configuration.Configuration` class instance. * Configuration interactions must go through `ahriman.core.configuration.Configuration` class instance.
@ -167,7 +169,7 @@ Again, the most checks can be performed by `make check` command, though some add
* No global variable is allowed outside of `ahriman` module. `ahriman.core.context` is also special case. * No global variable is allowed outside of `ahriman` module. `ahriman.core.context` is also special case.
* Single quotes are not allowed. The reason behind this restriction is the fact that docstrings must be written by using double quotes only, and we would like to make style consistent. * Single quotes are not allowed. The reason behind this restriction is the fact that docstrings must be written by using double quotes only, and we would like to make style consistent.
* If your class writes anything to log, the `ahriman.core.log.LazyLogging` trait must be used. * If your class writes anything to log, the `ahriman.core.log.LazyLogging` trait must be used.
* Web API methods must be documented by using `aiohttp_apispec` library. Schema testing mostly should be implemented in related view class tests. Recommended example for documentation (excluding comments): * Web API methods must be documented by using `aiohttp_apispec` library. The schema testing mostly should be implemented in related view class tests. Recommended example for documentation (excluding comments):
```python ```python
import aiohttp_apispec import aiohttp_apispec
@ -191,6 +193,7 @@ Again, the most checks can be performed by `make check` command, though some add
class Foo(BaseView): class Foo(BaseView):
POST_PERMISSION = ... POST_PERMISSION = ...
ROUTES = ...
@aiohttp_apispec.docs( @aiohttp_apispec.docs(
tags=["Tag"], tags=["Tag"],
@ -216,6 +219,7 @@ Again, the most checks can be performed by `make check` command, though some add
* It is allowed to change web API to add new fields or remove optional ones. However, in case of model changes, new API version must be introduced. * It is allowed to change web API to add new fields or remove optional ones. However, in case of model changes, new API version must be introduced.
* On the other hand, it is allowed to change method signatures, however, it is recommended to add new parameters as optional if possible. Deprecated API can be dropped during major release. * On the other hand, it is allowed to change method signatures, however, it is recommended to add new parameters as optional if possible. Deprecated API can be dropped during major release.
* Enumerations (`Enum` classes) are allowed and recommended. However, it is recommended to use `StrEnum` class if there are from/to string conversions and `IntEnum` otherwise.
### Other checks ### Other checks

View File

@ -19,33 +19,33 @@
# #
from astroid import nodes from astroid import nodes
from collections.abc import Iterable from collections.abc import Iterable
from enum import Enum from enum import StrEnum
from pylint.checkers import BaseRawFileChecker from pylint.checkers import BaseRawFileChecker
from pylint.lint import PyLinter from pylint.lint import PyLinter
from typing import Any from typing import Any
class MethodTypeOrder(int, Enum): class MethodTypeOrder(StrEnum):
""" """
method type enumeration method type enumeration
Attributes: Attributes:
New(MethodTypeOrder): (class attribute) constructor method
Init(MethodTypeOrder): (class attribute) initialization method
Property(MethodTypeOrder): (class attribute) property method
Class(MethodTypeOrder): (class attribute) class method Class(MethodTypeOrder): (class attribute) class method
Static(MethodTypeOrder): (class attribute) static method Init(MethodTypeOrder): (class attribute) initialization method
Normal(MethodTypeOrder): (class attribute) usual method
Magic(MethodTypeOrder): (class attribute) other magical methods Magic(MethodTypeOrder): (class attribute) other magical methods
New(MethodTypeOrder): (class attribute) constructor method
Normal(MethodTypeOrder): (class attribute) usual method
Property(MethodTypeOrder): (class attribute) property method
Static(MethodTypeOrder): (class attribute) static method
""" """
New = 0 Class = "classmethod"
Init = 1 Init = "init"
Property = 2 Magic = "magic"
Class = 3 New = "new"
Static = 4 Normal = "regular"
Normal = 5 Property = "property"
Magic = 6 Static = "staticmethod"
class DefinitionOrder(BaseRawFileChecker): class DefinitionOrder(BaseRawFileChecker):
@ -65,45 +65,31 @@ class DefinitionOrder(BaseRawFileChecker):
name = "method-ordering" name = "method-ordering"
msgs = { msgs = {
"W0001": ( "W6001": (
"Invalid method order %s, expected %s", "Invalid method order %s, expected %s",
"methods-out-of-order", "methods-out-of-order",
"Methods are defined out of recommended order.", "Methods are defined out of recommended order.",
) )
} }
options = () options = (
(
@staticmethod "method-type-order",
def comparator(function: nodes.FunctionDef) -> tuple[int, str]: {
""" "default": [
compare key for function node "new",
"init",
Args: "property",
function(nodes.FunctionDef): function definition "classmethod",
"staticmethod",
Returns: "regular",
tuple[int, str]: comparison key "magic",
""" ],
# init methods "type": "csv",
if function.name in ("__new__",): "metavar": "<comma-separated types>",
return MethodTypeOrder.New, function.name "help": "Method types order to check.",
if function.name in ("__init__", "__post_init__"): },
return MethodTypeOrder.Init, function.name ),
)
# decorated methods
decorators = []
if function.decorators is not None:
decorators = [getattr(decorator, "name", None) for decorator in function.decorators.get_children()]
for decorator in decorators:
if decorator in DefinitionOrder.DECORATED_METHODS_ORDER:
return DefinitionOrder.DECORATED_METHODS_ORDER[decorator], function.name
# magic methods
if function.name.startswith("__") and function.name.endswith("__"):
return MethodTypeOrder.Magic, function.name
# normal method
return MethodTypeOrder.Normal, function.name
@staticmethod @staticmethod
def methods(source: Iterable[Any], start_lineno: int = 0) -> list[nodes.FunctionDef]: def methods(source: Iterable[Any], start_lineno: int = 0) -> list[nodes.FunctionDef]:
@ -124,6 +110,38 @@ class DefinitionOrder(BaseRawFileChecker):
return list(filter(is_defined_function, source)) return list(filter(is_defined_function, source))
@staticmethod
def resolve_type(function: nodes.FunctionDef) -> MethodTypeOrder:
"""
resolve type of the function
Args:
function(nodes.FunctionDef): function definition
Returns:
MethodTypeOrder: resolved function type
"""
# init methods
if function.name in ("__new__",):
return MethodTypeOrder.New
if function.name in ("__init__", "__post_init__"):
return MethodTypeOrder.Init
# decorated methods
decorators = []
if function.decorators is not None:
decorators = [getattr(decorator, "name", None) for decorator in function.decorators.get_children()]
for decorator in decorators:
if decorator in DefinitionOrder.DECORATED_METHODS_ORDER:
return DefinitionOrder.DECORATED_METHODS_ORDER[decorator]
# magic methods
if function.name.startswith("__") and function.name.endswith("__"):
return MethodTypeOrder.Magic
# normal method
return MethodTypeOrder.Normal
def check_class(self, clazz: nodes.ClassDef) -> None: def check_class(self, clazz: nodes.ClassDef) -> None:
""" """
check class functions ordering check class functions ordering
@ -131,7 +149,7 @@ class DefinitionOrder(BaseRawFileChecker):
Args: Args:
clazz(nodes.ClassDef): class definition clazz(nodes.ClassDef): class definition
""" """
methods = DefinitionOrder.methods(clazz.values(), clazz.lineno) methods = self.methods(clazz.values(), clazz.lineno)
self.check_functions(methods) self.check_functions(methods)
def check_functions(self, functions: list[nodes.FunctionDef]) -> None: def check_functions(self, functions: list[nodes.FunctionDef]) -> None:
@ -141,12 +159,30 @@ class DefinitionOrder(BaseRawFileChecker):
Args: Args:
functions(list[nodes.FunctionDef]): list of functions in their defined order functions(list[nodes.FunctionDef]): list of functions in their defined order
""" """
for real, expected in zip(functions, sorted(functions, key=DefinitionOrder.comparator)): for real, expected in zip(functions, sorted(functions, key=self.comparator)):
if real == expected: if real == expected:
continue continue
self.add_message("methods-out-of-order", line=real.lineno, args=(real.name, expected.name)) self.add_message("methods-out-of-order", line=real.lineno, args=(real.name, expected.name))
break break
def comparator(self, function: nodes.FunctionDef) -> tuple[int, str]:
"""
compare key for sorting function
Args:
function(nodes.FunctionDef): function definition
Returns:
tuple[int, str]: comparison key for the specified function definition
"""
function_type = self.resolve_type(function)
try:
function_type_index = self.linter.config.method_type_order.index(function_type)
except ValueError:
function_type_index = 10 # not in the list
return function_type_index, function.name
def process_module(self, node: nodes.Module) -> None: def process_module(self, node: nodes.Module) -> None:
""" """
process module process module
@ -155,7 +191,7 @@ class DefinitionOrder(BaseRawFileChecker):
node(nodes.Module): module node to check node(nodes.Module): module node to check
""" """
# check global methods # check global methods
self.check_functions(DefinitionOrder.methods(node.values())) self.check_functions(self.methods(node.values()))
# check class definitions # check class definitions
for clazz in filter(lambda method: isinstance(method, nodes.ClassDef), node.values()): for clazz in filter(lambda method: isinstance(method, nodes.ClassDef), node.values()):
self.check_class(clazz) self.check_class(clazz)

View File

@ -17,10 +17,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from enum import Enum from enum import StrEnum
class Action(str, Enum): class Action(StrEnum):
""" """
base action enumeration base action enumeration

View File

@ -19,10 +19,10 @@
# #
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import StrEnum
class AuthSettings(str, Enum): class AuthSettings(StrEnum):
""" """
web authorization type web authorization type

View File

@ -18,13 +18,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from dataclasses import dataclass, field, fields from dataclasses import dataclass, field, fields
from enum import Enum from enum import StrEnum
from typing import Any, Self from typing import Any, Self
from ahriman.core.util import filter_json, pretty_datetime, utcnow from ahriman.core.util import filter_json, pretty_datetime, utcnow
class BuildStatusEnum(str, Enum): class BuildStatusEnum(StrEnum):
""" """
build status enumeration build status enumeration

View File

@ -17,10 +17,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from enum import Enum from enum import StrEnum
class LogHandler(str, Enum): class LogHandler(StrEnum):
""" """
log handler as described by default configuration log handler as described by default configuration

View File

@ -19,7 +19,7 @@
# #
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import StrEnum
from pathlib import Path from pathlib import Path
from urllib.parse import urlparse from urllib.parse import urlparse
@ -27,7 +27,7 @@ from ahriman.core.util import package_like
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
class PackageSource(str, Enum): class PackageSource(StrEnum):
""" """
package source for addition enumeration package source for addition enumeration

View File

@ -17,10 +17,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from enum import Enum from enum import IntEnum
class PacmanSynchronization(int, Enum): class PacmanSynchronization(IntEnum):
""" """
pacman database synchronization flag pacman database synchronization flag

View File

@ -19,10 +19,10 @@
# #
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import StrEnum
class ReportSettings(str, Enum): class ReportSettings(StrEnum):
""" """
report targets enumeration report targets enumeration

View File

@ -19,10 +19,10 @@
# #
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import StrEnum
class SignSettings(str, Enum): class SignSettings(StrEnum):
""" """
sign targets enumeration sign targets enumeration

View File

@ -19,10 +19,10 @@
# #
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import StrEnum
class SmtpSSLSettings(str, Enum): class SmtpSSLSettings(StrEnum):
""" """
SMTP SSL mode enumeration SMTP SSL mode enumeration

View File

@ -19,10 +19,10 @@
# #
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import StrEnum
class UploadSettings(str, Enum): class UploadSettings(StrEnum):
""" """
remote synchronization targets enumeration remote synchronization targets enumeration

View File

@ -19,10 +19,10 @@
# #
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import StrEnum
class UserAccess(str, Enum): class UserAccess(StrEnum):
""" """
web user access enumeration web user access enumeration