Skip to content

Commit f21c018

Browse files
committed
Replace dataclasses with attrs and slotted classes
Signed-off-by: Sergey Vasilyev <nolar@nolar.info>
1 parent 825151a commit f21c018

23 files changed

+171
-168
lines changed

kopf/_cogs/configs/configuration.py

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,16 @@
2626
used interchangeably -- but so that it is understandable what is meant.
2727
"""
2828
import concurrent.futures
29-
import dataclasses
3029
import logging
3130
from typing import Iterable, Optional, Union
3231

32+
import attrs
33+
3334
from kopf._cogs.configs import diffbase, progress
3435
from kopf._cogs.structs import reviews
3536

3637

37-
@dataclasses.dataclass
38+
@attrs.define
3839
class ProcessSettings:
3940
"""
4041
Settings for Kopf's OS processes: e.g. when started via CLI as `kopf run`.
@@ -59,7 +60,7 @@ class ProcessSettings:
5960
"""
6061

6162

62-
@dataclasses.dataclass
63+
@attrs.define
6364
class PostingSettings:
6465

6566
enabled: bool = True
@@ -81,7 +82,7 @@ class PostingSettings:
8182
"""
8283

8384

84-
@dataclasses.dataclass
85+
@attrs.define
8586
class PeeringSettings:
8687

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

164165

165-
@dataclasses.dataclass
166+
@attrs.define
166167
class WatchingSettings:
167168

168169
server_timeout: Optional[float] = None
@@ -187,7 +188,7 @@ class WatchingSettings:
187188
"""
188189

189190

190-
@dataclasses.dataclass
191+
@attrs.define
191192
class BatchingSettings:
192193
"""
193194
Settings for how raw events are batched and processed.
@@ -224,7 +225,7 @@ class BatchingSettings:
224225
"""
225226

226227

227-
@dataclasses.dataclass
228+
@attrs.define
228229
class ScanningSettings:
229230
"""
230231
Settings for dynamic runtime observation of the cluster's setup.
@@ -249,7 +250,7 @@ class ScanningSettings:
249250
"""
250251

251252

252-
@dataclasses.dataclass
253+
@attrs.define
253254
class AdmissionSettings:
254255

255256
server: Optional[reviews.WebhookServerProtocol] = None
@@ -290,14 +291,13 @@ class AdmissionSettings:
290291
"""
291292

292293

293-
@dataclasses.dataclass
294+
@attrs.define
294295
class ExecutionSettings:
295296
"""
296297
Settings for synchronous handlers execution (e.g. thread-/process-pools).
297298
"""
298299

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

330330

331-
@dataclasses.dataclass
331+
@attrs.define
332332
class NetworkingSettings:
333333

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

355355

356-
@dataclasses.dataclass
356+
@attrs.define
357357
class PersistenceSettings:
358358

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

365-
progress_storage: progress.ProgressStorage = dataclasses.field(
366-
default_factory=progress.SmartProgressStorage)
365+
progress_storage: progress.ProgressStorage = attrs.Factory(progress.SmartProgressStorage)
367366
"""
368367
How to persist the handlers' state between multiple handling cycles.
369368
"""
370369

371-
diffbase_storage: diffbase.DiffBaseStorage = dataclasses.field(
372-
default_factory=diffbase.AnnotationsDiffBaseStorage)
370+
diffbase_storage: diffbase.DiffBaseStorage = attrs.Factory(diffbase.AnnotationsDiffBaseStorage)
373371
"""
374372
How the resource's essence (non-technical, contentful fields) are stored.
375373
"""
376374

377375

378-
@dataclasses.dataclass
376+
@attrs.define
379377
class BackgroundSettings:
380378
"""
381379
Settings for background routines in general, daemons & timers specifically.
@@ -434,16 +432,16 @@ class BackgroundSettings:
434432
"""
435433

436434

437-
@dataclasses.dataclass
435+
@attrs.define
438436
class OperatorSettings:
439-
process: ProcessSettings = dataclasses.field(default_factory=ProcessSettings)
440-
posting: PostingSettings = dataclasses.field(default_factory=PostingSettings)
441-
peering: PeeringSettings = dataclasses.field(default_factory=PeeringSettings)
442-
watching: WatchingSettings = dataclasses.field(default_factory=WatchingSettings)
443-
batching: BatchingSettings = dataclasses.field(default_factory=BatchingSettings)
444-
scanning: ScanningSettings = dataclasses.field(default_factory=ScanningSettings)
445-
admission: AdmissionSettings =dataclasses.field(default_factory=AdmissionSettings)
446-
execution: ExecutionSettings = dataclasses.field(default_factory=ExecutionSettings)
447-
background: BackgroundSettings = dataclasses.field(default_factory=BackgroundSettings)
448-
networking: NetworkingSettings = dataclasses.field(default_factory=NetworkingSettings)
449-
persistence: PersistenceSettings = dataclasses.field(default_factory=PersistenceSettings)
437+
process: ProcessSettings = attrs.Factory(ProcessSettings)
438+
posting: PostingSettings = attrs.Factory(PostingSettings)
439+
peering: PeeringSettings = attrs.Factory(PeeringSettings)
440+
watching: WatchingSettings = attrs.Factory(WatchingSettings)
441+
batching: BatchingSettings = attrs.Factory(BatchingSettings)
442+
scanning: ScanningSettings = attrs.Factory(ScanningSettings)
443+
admission: AdmissionSettings =attrs.Factory(AdmissionSettings)
444+
execution: ExecutionSettings = attrs.Factory(ExecutionSettings)
445+
background: BackgroundSettings = attrs.Factory(BackgroundSettings)
446+
networking: NetworkingSettings = attrs.Factory(NetworkingSettings)
447+
persistence: PersistenceSettings = attrs.Factory(PersistenceSettings)

kopf/_cogs/structs/credentials.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@
2525
"""
2626
import asyncio
2727
import collections
28-
import dataclasses
2928
import datetime
3029
import random
3130
from typing import AsyncIterable, AsyncIterator, Callable, Dict, List, \
3231
Mapping, NewType, Optional, Tuple, TypeVar, cast
3332

33+
import attrs
34+
3435
from kopf._cogs.aiokits import aiotoggles
3536

3637

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

4445

45-
@dataclasses.dataclass(frozen=True)
46+
@attrs.define(frozen=True)
4647
class ConnectionInfo:
4748
"""
4849
A single endpoint with specific credentials and connection flags to use.
@@ -70,7 +71,7 @@ class ConnectionInfo:
7071
VaultKey = NewType('VaultKey', str)
7172

7273

73-
@dataclasses.dataclass
74+
@attrs.define
7475
class VaultItem:
7576
"""
7677
The actual item stored in the vault. It is never exposed externally.

kopf/_cogs/structs/references.py

Lines changed: 48 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import asyncio
2-
import dataclasses
32
import enum
43
import fnmatch
54
import re
65
import urllib.parse
76
from typing import Collection, FrozenSet, Iterable, Iterator, List, Mapping, \
87
MutableMapping, NewType, Optional, Pattern, Set, Union
98

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

102103

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

252253

253-
@dataclasses.dataclass(frozen=True)
254+
@attrs.define(frozen=True, init=False)
254255
class Selector:
255256
"""
256257
A resource specification that can match several resource kinds.
@@ -265,61 +266,59 @@ class Selector:
265266
resource kinds. Even if those specifications look very concrete and allow
266267
no variations, they still remain specifications.
267268
"""
268-
269-
arg1: dataclasses.InitVar[Union[None, str, Marker]] = None
270-
arg2: dataclasses.InitVar[Union[None, str, Marker]] = None
271-
arg3: dataclasses.InitVar[Union[None, str, Marker]] = None
272-
argN: dataclasses.InitVar[None] = None # a runtime guard against too many positional arguments
273-
274269
group: Optional[str] = None
275270
version: Optional[str] = None
276-
277271
kind: Optional[str] = None
278272
plural: Optional[str] = None
279273
singular: Optional[str] = None
280274
shortcut: Optional[str] = None
281275
category: Optional[str] = None
282276
any_name: Optional[Union[str, Marker]] = None
283277

284-
def __post_init__(
278+
def __init__(
285279
self,
286-
arg1: Union[None, str, Marker],
287-
arg2: Union[None, str, Marker],
288-
arg3: Union[None, str, Marker],
289-
argN: None, # a runtime guard against too many positional arguments
280+
arg1: Union[None, str, Marker] = None,
281+
arg2: Union[None, str, Marker] = None,
282+
arg3: Union[None, str, Marker] = None,
283+
*,
284+
group: Optional[str] = None,
285+
version: Optional[str] = None,
286+
kind: Optional[str] = None,
287+
plural: Optional[str] = None,
288+
singular: Optional[str] = None,
289+
shortcut: Optional[str] = None,
290+
category: Optional[str] = None,
291+
any_name: Optional[Union[str, Marker]] = None,
290292
) -> None:
293+
super().__init__()
291294

292-
# Since the class is frozen & read-only, post-creation field adjustment is done via a hack.
293-
# This is the same hack as used in the frozen dataclasses to initialise their fields.
294-
if argN is not None:
295-
raise TypeError("Too many positional arguments. Max 3 positional args are accepted.")
295+
if arg3 is not None and not isinstance(arg1, Marker) and not isinstance(arg2, Marker):
296+
group, version, any_name = arg1, arg2, arg3
296297
elif arg3 is not None:
297-
object.__setattr__(self, 'group', arg1)
298-
object.__setattr__(self, 'version', arg2)
299-
object.__setattr__(self, 'any_name', arg3)
298+
raise TypeError("Only the last positional argument can be an everything-marker.")
300299
elif arg2 is not None and isinstance(arg1, str) and '/' in arg1:
301-
object.__setattr__(self, 'group', arg1.rsplit('/', 1)[0])
302-
object.__setattr__(self, 'version', arg1.rsplit('/')[-1])
303-
object.__setattr__(self, 'any_name', arg2)
304-
elif arg2 is not None and arg1 == 'v1':
305-
object.__setattr__(self, 'group', '')
306-
object.__setattr__(self, 'version', arg1)
307-
object.__setattr__(self, 'any_name', arg2)
308-
elif arg2 is not None:
309-
object.__setattr__(self, 'group', arg1)
310-
object.__setattr__(self, 'any_name', arg2)
300+
group, version = arg1.rsplit('/', 1)
301+
any_name = arg2
302+
elif arg2 is not None and isinstance(arg1, str) and arg1 == 'v1':
303+
group, version, any_name = '', arg1, arg2
304+
elif arg2 is not None and not isinstance(arg1, Marker):
305+
group, any_name = arg1, arg2
311306
elif arg1 is not None and isinstance(arg1, Marker):
312-
object.__setattr__(self, 'any_name', arg1)
307+
any_name = arg1
313308
elif arg1 is not None and '.' in arg1 and K8S_VERSION_PATTERN.match(arg1.split('.')[1]):
314309
if len(arg1.split('.')) >= 3:
315-
object.__setattr__(self, 'group', arg1.split('.', 2)[2])
316-
object.__setattr__(self, 'version', arg1.split('.')[1])
317-
object.__setattr__(self, 'any_name', arg1.split('.')[0])
310+
any_name, version, group = arg1.split('.', 2)
311+
else:
312+
any_name, version = arg1.split('.')
318313
elif arg1 is not None and '.' in arg1:
319-
object.__setattr__(self, 'group', arg1.split('.', 1)[1])
320-
object.__setattr__(self, 'any_name', arg1.split('.')[0])
314+
any_name, group = arg1.split('.', 1)
321315
elif arg1 is not None:
322-
object.__setattr__(self, 'any_name', arg1)
316+
any_name = arg1
317+
318+
self.__attrs_init__(
319+
group=group, version=version, kind=kind, plural=plural, singular=singular,
320+
shortcut=shortcut, category=category, any_name=any_name
321+
)
323322

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

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

@@ -473,7 +471,7 @@ async def wait_for(
473471
return self[selector]
474472

475473

476-
@dataclasses.dataclass(frozen=True)
474+
@attrs.define(frozen=True)
477475
class Insights:
478476
"""
479477
Actual resources & namespaces served by the operator.
@@ -483,15 +481,15 @@ class Insights:
483481
# - **Indexed** resources block the operator startup until all objects are initially indexed.
484482
# - **Watched** resources spawn the watch-streams; the set excludes all webhook-only resources.
485483
# - **Webhook** resources are served via webhooks; the set excludes all watch-only resources.
486-
webhook_resources: Set[Resource] = dataclasses.field(default_factory=set)
487-
indexed_resources: Set[Resource] = dataclasses.field(default_factory=set)
488-
watched_resources: Set[Resource] = dataclasses.field(default_factory=set)
489-
namespaces: Set[Namespace] = dataclasses.field(default_factory=set)
490-
backbone: Backbone = dataclasses.field(default_factory=Backbone)
484+
webhook_resources: Set[Resource] = attrs.field(factory=set)
485+
indexed_resources: Set[Resource] = attrs.field(factory=set)
486+
watched_resources: Set[Resource] = attrs.field(factory=set)
487+
namespaces: Set[Namespace] = attrs.field(factory=set)
488+
backbone: Backbone = attrs.field(factory=Backbone)
491489

492490
# Signalled when anything changes in the insights.
493-
revised: asyncio.Condition = dataclasses.field(default_factory=asyncio.Condition)
491+
revised: asyncio.Condition = attrs.field(factory=asyncio.Condition)
494492

495493
# The flags that are set after the initial listing is finished. Not cleared afterwards.
496-
ready_namespaces: asyncio.Event = dataclasses.field(default_factory=asyncio.Event)
497-
ready_resources: asyncio.Event = dataclasses.field(default_factory=asyncio.Event)
494+
ready_namespaces: asyncio.Event = attrs.field(factory=asyncio.Event)
495+
ready_resources: asyncio.Event = attrs.field(factory=asyncio.Event)

0 commit comments

Comments
 (0)