Skip to content

Commit

Permalink
Replace dataclasses with attrs and slotted classes
Browse files Browse the repository at this point in the history
Signed-off-by: Sergey Vasilyev <[email protected]>
  • Loading branch information
nolar committed Nov 13, 2022
1 parent 825151a commit f21c018
Show file tree
Hide file tree
Showing 23 changed files with 171 additions and 168 deletions.
58 changes: 28 additions & 30 deletions kopf/_cogs/configs/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,16 @@
used interchangeably -- but so that it is understandable what is meant.
"""
import concurrent.futures
import dataclasses
import logging
from typing import Iterable, Optional, Union

import attrs

from kopf._cogs.configs import diffbase, progress
from kopf._cogs.structs import reviews


@dataclasses.dataclass
@attrs.define
class ProcessSettings:
"""
Settings for Kopf's OS processes: e.g. when started via CLI as `kopf run`.
Expand All @@ -59,7 +60,7 @@ class ProcessSettings:
"""


@dataclasses.dataclass
@attrs.define
class PostingSettings:

enabled: bool = True
Expand All @@ -81,7 +82,7 @@ class PostingSettings:
"""


@dataclasses.dataclass
@attrs.define
class PeeringSettings:

name: str = 'default'
Expand Down Expand Up @@ -162,7 +163,7 @@ def namespaced(self, value: bool) -> None:
self.clusterwide = not value


@dataclasses.dataclass
@attrs.define
class WatchingSettings:

server_timeout: Optional[float] = None
Expand All @@ -187,7 +188,7 @@ class WatchingSettings:
"""


@dataclasses.dataclass
@attrs.define
class BatchingSettings:
"""
Settings for how raw events are batched and processed.
Expand Down Expand Up @@ -224,7 +225,7 @@ class BatchingSettings:
"""


@dataclasses.dataclass
@attrs.define
class ScanningSettings:
"""
Settings for dynamic runtime observation of the cluster's setup.
Expand All @@ -249,7 +250,7 @@ class ScanningSettings:
"""


@dataclasses.dataclass
@attrs.define
class AdmissionSettings:

server: Optional[reviews.WebhookServerProtocol] = None
Expand Down Expand Up @@ -290,14 +291,13 @@ class AdmissionSettings:
"""


@dataclasses.dataclass
@attrs.define
class ExecutionSettings:
"""
Settings for synchronous handlers execution (e.g. thread-/process-pools).
"""

executor: concurrent.futures.Executor = dataclasses.field(
default_factory=concurrent.futures.ThreadPoolExecutor)
executor: concurrent.futures.Executor = attrs.Factory(concurrent.futures.ThreadPoolExecutor)
"""
The executor to be used for synchronous handler invocation.
Expand Down Expand Up @@ -328,7 +328,7 @@ def max_workers(self, value: int) -> None:
raise TypeError("Current executor does not support `max_workers`.")


@dataclasses.dataclass
@attrs.define
class NetworkingSettings:

request_timeout: Optional[float] = 5 * 60 # == aiohttp.client.DEFAULT_TIMEOUT
Expand All @@ -353,7 +353,7 @@ class NetworkingSettings:
"""


@dataclasses.dataclass
@attrs.define
class PersistenceSettings:

finalizer: str = 'kopf.zalando.org/KopfFinalizerMarker'
Expand All @@ -362,20 +362,18 @@ class PersistenceSettings:
from being deleted without framework's/operator's permission.
"""

progress_storage: progress.ProgressStorage = dataclasses.field(
default_factory=progress.SmartProgressStorage)
progress_storage: progress.ProgressStorage = attrs.Factory(progress.SmartProgressStorage)
"""
How to persist the handlers' state between multiple handling cycles.
"""

diffbase_storage: diffbase.DiffBaseStorage = dataclasses.field(
default_factory=diffbase.AnnotationsDiffBaseStorage)
diffbase_storage: diffbase.DiffBaseStorage = attrs.Factory(diffbase.AnnotationsDiffBaseStorage)
"""
How the resource's essence (non-technical, contentful fields) are stored.
"""


@dataclasses.dataclass
@attrs.define
class BackgroundSettings:
"""
Settings for background routines in general, daemons & timers specifically.
Expand Down Expand Up @@ -434,16 +432,16 @@ class BackgroundSettings:
"""


@dataclasses.dataclass
@attrs.define
class OperatorSettings:
process: ProcessSettings = dataclasses.field(default_factory=ProcessSettings)
posting: PostingSettings = dataclasses.field(default_factory=PostingSettings)
peering: PeeringSettings = dataclasses.field(default_factory=PeeringSettings)
watching: WatchingSettings = dataclasses.field(default_factory=WatchingSettings)
batching: BatchingSettings = dataclasses.field(default_factory=BatchingSettings)
scanning: ScanningSettings = dataclasses.field(default_factory=ScanningSettings)
admission: AdmissionSettings =dataclasses.field(default_factory=AdmissionSettings)
execution: ExecutionSettings = dataclasses.field(default_factory=ExecutionSettings)
background: BackgroundSettings = dataclasses.field(default_factory=BackgroundSettings)
networking: NetworkingSettings = dataclasses.field(default_factory=NetworkingSettings)
persistence: PersistenceSettings = dataclasses.field(default_factory=PersistenceSettings)
process: ProcessSettings = attrs.Factory(ProcessSettings)
posting: PostingSettings = attrs.Factory(PostingSettings)
peering: PeeringSettings = attrs.Factory(PeeringSettings)
watching: WatchingSettings = attrs.Factory(WatchingSettings)
batching: BatchingSettings = attrs.Factory(BatchingSettings)
scanning: ScanningSettings = attrs.Factory(ScanningSettings)
admission: AdmissionSettings =attrs.Factory(AdmissionSettings)
execution: ExecutionSettings = attrs.Factory(ExecutionSettings)
background: BackgroundSettings = attrs.Factory(BackgroundSettings)
networking: NetworkingSettings = attrs.Factory(NetworkingSettings)
persistence: PersistenceSettings = attrs.Factory(PersistenceSettings)
7 changes: 4 additions & 3 deletions kopf/_cogs/structs/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@
"""
import asyncio
import collections
import dataclasses
import datetime
import random
from typing import AsyncIterable, AsyncIterator, Callable, Dict, List, \
Mapping, NewType, Optional, Tuple, TypeVar, cast

import attrs

from kopf._cogs.aiokits import aiotoggles


Expand All @@ -42,7 +43,7 @@ class AccessError(Exception):
""" Raised when the operator cannot access the cluster API. """


@dataclasses.dataclass(frozen=True)
@attrs.define(frozen=True)
class ConnectionInfo:
"""
A single endpoint with specific credentials and connection flags to use.
Expand Down Expand Up @@ -70,7 +71,7 @@ class ConnectionInfo:
VaultKey = NewType('VaultKey', str)


@dataclasses.dataclass
@attrs.define
class VaultItem:
"""
The actual item stored in the vault. It is never exposed externally.
Expand Down
98 changes: 48 additions & 50 deletions kopf/_cogs/structs/references.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import asyncio
import dataclasses
import enum
import fnmatch
import re
import urllib.parse
from typing import Collection, FrozenSet, Iterable, Iterator, List, Mapping, \
MutableMapping, NewType, Optional, Pattern, Set, Union

import attrs

# A namespace specification with globs, negations, and some minimal syntax; see `match_namespace()`.
# Regexps are also supported if pre-compiled from the code, not from the CLI options as raw strings.
NamespacePattern = Union[str, Pattern]
Expand Down Expand Up @@ -100,7 +101,7 @@ def match_namespace(name: NamespaceName, pattern: NamespacePattern) -> bool:
K8S_VERSION_PATTERN = re.compile(r'^v\d+(?:(?:alpha|beta)\d+)?$')


@dataclasses.dataclass(frozen=True, eq=False, repr=False)
@attrs.define(frozen=True)
class Resource:
"""
A reference to a very specific custom or built-in resource kind.
Expand Down Expand Up @@ -250,7 +251,7 @@ class Marker(enum.Enum):
EVERYTHING = Marker.EVERYTHING


@dataclasses.dataclass(frozen=True)
@attrs.define(frozen=True, init=False)
class Selector:
"""
A resource specification that can match several resource kinds.
Expand All @@ -265,61 +266,59 @@ class Selector:
resource kinds. Even if those specifications look very concrete and allow
no variations, they still remain specifications.
"""

arg1: dataclasses.InitVar[Union[None, str, Marker]] = None
arg2: dataclasses.InitVar[Union[None, str, Marker]] = None
arg3: dataclasses.InitVar[Union[None, str, Marker]] = None
argN: dataclasses.InitVar[None] = None # a runtime guard against too many positional arguments

group: Optional[str] = None
version: Optional[str] = None

kind: Optional[str] = None
plural: Optional[str] = None
singular: Optional[str] = None
shortcut: Optional[str] = None
category: Optional[str] = None
any_name: Optional[Union[str, Marker]] = None

def __post_init__(
def __init__(
self,
arg1: Union[None, str, Marker],
arg2: Union[None, str, Marker],
arg3: Union[None, str, Marker],
argN: None, # a runtime guard against too many positional arguments
arg1: Union[None, str, Marker] = None,
arg2: Union[None, str, Marker] = None,
arg3: Union[None, str, Marker] = None,
*,
group: Optional[str] = None,
version: Optional[str] = None,
kind: Optional[str] = None,
plural: Optional[str] = None,
singular: Optional[str] = None,
shortcut: Optional[str] = None,
category: Optional[str] = None,
any_name: Optional[Union[str, Marker]] = None,
) -> None:
super().__init__()

# Since the class is frozen & read-only, post-creation field adjustment is done via a hack.
# This is the same hack as used in the frozen dataclasses to initialise their fields.
if argN is not None:
raise TypeError("Too many positional arguments. Max 3 positional args are accepted.")
if arg3 is not None and not isinstance(arg1, Marker) and not isinstance(arg2, Marker):
group, version, any_name = arg1, arg2, arg3
elif arg3 is not None:
object.__setattr__(self, 'group', arg1)
object.__setattr__(self, 'version', arg2)
object.__setattr__(self, 'any_name', arg3)
raise TypeError("Only the last positional argument can be an everything-marker.")
elif arg2 is not None and isinstance(arg1, str) and '/' in arg1:
object.__setattr__(self, 'group', arg1.rsplit('/', 1)[0])
object.__setattr__(self, 'version', arg1.rsplit('/')[-1])
object.__setattr__(self, 'any_name', arg2)
elif arg2 is not None and arg1 == 'v1':
object.__setattr__(self, 'group', '')
object.__setattr__(self, 'version', arg1)
object.__setattr__(self, 'any_name', arg2)
elif arg2 is not None:
object.__setattr__(self, 'group', arg1)
object.__setattr__(self, 'any_name', arg2)
group, version = arg1.rsplit('/', 1)
any_name = arg2
elif arg2 is not None and isinstance(arg1, str) and arg1 == 'v1':
group, version, any_name = '', arg1, arg2
elif arg2 is not None and not isinstance(arg1, Marker):
group, any_name = arg1, arg2
elif arg1 is not None and isinstance(arg1, Marker):
object.__setattr__(self, 'any_name', arg1)
any_name = arg1
elif arg1 is not None and '.' in arg1 and K8S_VERSION_PATTERN.match(arg1.split('.')[1]):
if len(arg1.split('.')) >= 3:
object.__setattr__(self, 'group', arg1.split('.', 2)[2])
object.__setattr__(self, 'version', arg1.split('.')[1])
object.__setattr__(self, 'any_name', arg1.split('.')[0])
any_name, version, group = arg1.split('.', 2)
else:
any_name, version = arg1.split('.')
elif arg1 is not None and '.' in arg1:
object.__setattr__(self, 'group', arg1.split('.', 1)[1])
object.__setattr__(self, 'any_name', arg1.split('.')[0])
any_name, group = arg1.split('.', 1)
elif arg1 is not None:
object.__setattr__(self, 'any_name', arg1)
any_name = arg1

self.__attrs_init__(
group=group, version=version, kind=kind, plural=plural, singular=singular,
shortcut=shortcut, category=category, any_name=any_name
)

# Verify that explicit & interpreted arguments have produced an unambiguous specification.
names = [self.kind, self.plural, self.singular, self.shortcut, self.category, self.any_name]
Expand All @@ -336,8 +335,7 @@ def __post_init__(
raise TypeError("Names must not be empty strings; either None or specific strings.")

def __repr__(self) -> str:
kwargs = {f.name: getattr(self, f.name) for f in dataclasses.fields(self)}
kwtext = ', '.join([f'{key!s}={val!r}' for key, val in kwargs.items() if val is not None])
kwtext = ', '.join([f'{k!s}={v!r}' for k, v in attrs.asdict(self).items() if v is not None])
clsname = self.__class__.__name__
return f'{clsname}({kwtext})'

Expand Down Expand Up @@ -473,7 +471,7 @@ async def wait_for(
return self[selector]


@dataclasses.dataclass(frozen=True)
@attrs.define(frozen=True)
class Insights:
"""
Actual resources & namespaces served by the operator.
Expand All @@ -483,15 +481,15 @@ class Insights:
# - **Indexed** resources block the operator startup until all objects are initially indexed.
# - **Watched** resources spawn the watch-streams; the set excludes all webhook-only resources.
# - **Webhook** resources are served via webhooks; the set excludes all watch-only resources.
webhook_resources: Set[Resource] = dataclasses.field(default_factory=set)
indexed_resources: Set[Resource] = dataclasses.field(default_factory=set)
watched_resources: Set[Resource] = dataclasses.field(default_factory=set)
namespaces: Set[Namespace] = dataclasses.field(default_factory=set)
backbone: Backbone = dataclasses.field(default_factory=Backbone)
webhook_resources: Set[Resource] = attrs.field(factory=set)
indexed_resources: Set[Resource] = attrs.field(factory=set)
watched_resources: Set[Resource] = attrs.field(factory=set)
namespaces: Set[Namespace] = attrs.field(factory=set)
backbone: Backbone = attrs.field(factory=Backbone)

# Signalled when anything changes in the insights.
revised: asyncio.Condition = dataclasses.field(default_factory=asyncio.Condition)
revised: asyncio.Condition = attrs.field(factory=asyncio.Condition)

# The flags that are set after the initial listing is finished. Not cleared afterwards.
ready_namespaces: asyncio.Event = dataclasses.field(default_factory=asyncio.Event)
ready_resources: asyncio.Event = dataclasses.field(default_factory=asyncio.Event)
ready_namespaces: asyncio.Event = attrs.field(factory=asyncio.Event)
ready_resources: asyncio.Event = attrs.field(factory=asyncio.Event)
Loading

0 comments on commit f21c018

Please sign in to comment.