1
1
import asyncio
2
- import dataclasses
3
2
import enum
4
3
import fnmatch
5
4
import re
6
5
import urllib .parse
7
6
from typing import Collection , FrozenSet , Iterable , Iterator , List , Mapping , \
8
7
MutableMapping , NewType , Optional , Pattern , Set , Union
9
8
9
+ import attrs
10
+
10
11
# A namespace specification with globs, negations, and some minimal syntax; see `match_namespace()`.
11
12
# Regexps are also supported if pre-compiled from the code, not from the CLI options as raw strings.
12
13
NamespacePattern = Union [str , Pattern ]
@@ -100,7 +101,7 @@ def match_namespace(name: NamespaceName, pattern: NamespacePattern) -> bool:
100
101
K8S_VERSION_PATTERN = re .compile (r'^v\d+(?:(?:alpha|beta)\d+)?$' )
101
102
102
103
103
- @dataclasses . dataclass (frozen = True , eq = False , repr = False )
104
+ @attrs . define (frozen = True )
104
105
class Resource :
105
106
"""
106
107
A reference to a very specific custom or built-in resource kind.
@@ -250,7 +251,7 @@ class Marker(enum.Enum):
250
251
EVERYTHING = Marker .EVERYTHING
251
252
252
253
253
- @dataclasses . dataclass (frozen = True )
254
+ @attrs . define (frozen = True , init = False )
254
255
class Selector :
255
256
"""
256
257
A resource specification that can match several resource kinds.
@@ -265,61 +266,59 @@ class Selector:
265
266
resource kinds. Even if those specifications look very concrete and allow
266
267
no variations, they still remain specifications.
267
268
"""
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
-
274
269
group : Optional [str ] = None
275
270
version : Optional [str ] = None
276
-
277
271
kind : Optional [str ] = None
278
272
plural : Optional [str ] = None
279
273
singular : Optional [str ] = None
280
274
shortcut : Optional [str ] = None
281
275
category : Optional [str ] = None
282
276
any_name : Optional [Union [str , Marker ]] = None
283
277
284
- def __post_init__ (
278
+ def __init__ (
285
279
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 ,
290
292
) -> None :
293
+ super ().__init__ ()
291
294
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
296
297
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." )
300
299
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
311
306
elif arg1 is not None and isinstance (arg1 , Marker ):
312
- object . __setattr__ ( self , ' any_name' , arg1 )
307
+ any_name = arg1
313
308
elif arg1 is not None and '.' in arg1 and K8S_VERSION_PATTERN .match (arg1 .split ('.' )[1 ]):
314
309
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 ('.' )
318
313
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 )
321
315
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
+ )
323
322
324
323
# Verify that explicit & interpreted arguments have produced an unambiguous specification.
325
324
names = [self .kind , self .plural , self .singular , self .shortcut , self .category , self .any_name ]
@@ -336,8 +335,7 @@ def __post_init__(
336
335
raise TypeError ("Names must not be empty strings; either None or specific strings." )
337
336
338
337
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 ])
341
339
clsname = self .__class__ .__name__
342
340
return f'{ clsname } ({ kwtext } )'
343
341
@@ -473,7 +471,7 @@ async def wait_for(
473
471
return self [selector ]
474
472
475
473
476
- @dataclasses . dataclass (frozen = True )
474
+ @attrs . define (frozen = True )
477
475
class Insights :
478
476
"""
479
477
Actual resources & namespaces served by the operator.
@@ -483,15 +481,15 @@ class Insights:
483
481
# - **Indexed** resources block the operator startup until all objects are initially indexed.
484
482
# - **Watched** resources spawn the watch-streams; the set excludes all webhook-only resources.
485
483
# - **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 )
491
489
492
490
# 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 )
494
492
495
493
# 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