Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace dataclasses with attrs and slotted classes #976

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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