timetoalign 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- timetoalign-0.0.1.dist-info/METADATA +11 -0
- timetoalign-0.0.1.dist-info/RECORD +13 -0
- timetoalign-0.0.1.dist-info/WHEEL +5 -0
- timetoalign-0.0.1.dist-info/top_level.txt +1 -0
- tta/__init__.py +1 -0
- tta/common.py +58 -0
- tta/conversions.py +3777 -0
- tta/events.py +1527 -0
- tta/parsing.py +2236 -0
- tta/registry.py +546 -0
- tta/representations.py +826 -0
- tta/timelines.py +2192 -0
- tta/utils.py +963 -0
tta/registry.py
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import warnings
|
|
5
|
+
from functools import cache
|
|
6
|
+
from numbers import Number
|
|
7
|
+
from typing import (
|
|
8
|
+
TYPE_CHECKING,
|
|
9
|
+
Any,
|
|
10
|
+
Callable,
|
|
11
|
+
Dict,
|
|
12
|
+
Generic,
|
|
13
|
+
Hashable,
|
|
14
|
+
Iterator,
|
|
15
|
+
Optional,
|
|
16
|
+
Type,
|
|
17
|
+
TypeVar,
|
|
18
|
+
Union,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
import pandas as pd
|
|
23
|
+
|
|
24
|
+
from processing.tta import utils
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from .common import RegisteredObject
|
|
28
|
+
from .conversions import ConversionMap
|
|
29
|
+
from .utils import NumberType, TimeUnit
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ID_str(str):
|
|
33
|
+
def __new__(cls, content: str, uid: Optional[Hashable] = None):
|
|
34
|
+
instance = super().__new__(cls, content)
|
|
35
|
+
instance.uid = uid
|
|
36
|
+
return instance
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# region Base Registry
|
|
40
|
+
|
|
41
|
+
_R = TypeVar("_R") # Type variable for the items stored in the registry
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Registry(Generic[_R]):
|
|
45
|
+
"""
|
|
46
|
+
A generic registry for storing and retrieving items by a hashable key.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self):
|
|
50
|
+
self._objects: Dict[Hashable, _R] = {}
|
|
51
|
+
|
|
52
|
+
def register(self, key: Hashable, obj_instance: _R) -> _R:
|
|
53
|
+
"""
|
|
54
|
+
Registers an item with the given key.
|
|
55
|
+
Warns if overwriting an existing key.
|
|
56
|
+
"""
|
|
57
|
+
if key in self._objects:
|
|
58
|
+
warnings.warn(
|
|
59
|
+
f"Object with key {key!r} already registered in this registry."
|
|
60
|
+
)
|
|
61
|
+
self._objects[key] = obj_instance
|
|
62
|
+
return obj_instance
|
|
63
|
+
|
|
64
|
+
def get(self, key: Hashable, default=None) -> Optional[_R]:
|
|
65
|
+
"""Retrieves an item by its key."""
|
|
66
|
+
return self._objects.get(key, default)
|
|
67
|
+
|
|
68
|
+
def unregister(self, key: Hashable) -> Optional[_R]:
|
|
69
|
+
"""Removes and returns an item by its key, or None if not found."""
|
|
70
|
+
return self._objects.pop(key, None)
|
|
71
|
+
|
|
72
|
+
def __contains__(self, key: Hashable) -> bool:
|
|
73
|
+
return key in self._objects
|
|
74
|
+
|
|
75
|
+
def __len__(self) -> int:
|
|
76
|
+
return len(self._objects)
|
|
77
|
+
|
|
78
|
+
def all_items(self) -> Dict[Hashable, _R]:
|
|
79
|
+
"""Returns a copy of all items in the registry."""
|
|
80
|
+
return dict(self._objects)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# endregion Base Registry
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# region ClassRegistry
|
|
87
|
+
class ClassRegistry(Registry[Type["RegisteredObject"]]):
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
CLASS_REGISTRY = ClassRegistry()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_class(class_name: str | Type["RegisteredObject"]) -> Type["RegisteredObject"]:
|
|
95
|
+
if isinstance(class_name, type):
|
|
96
|
+
if class_name.__name__ not in CLASS_REGISTRY:
|
|
97
|
+
raise ValueError(f"Class {class_name.__name__} is not registered.")
|
|
98
|
+
return class_name
|
|
99
|
+
if not isinstance(class_name, str):
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"Classes are registered with their names as keys. Expected a string, got {type(class_name)}"
|
|
102
|
+
)
|
|
103
|
+
return CLASS_REGISTRY.get(class_name)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def register_class(cls):
|
|
107
|
+
CLASS_REGISTRY.register(cls.__name__, cls)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# endregion ClassRegistry
|
|
111
|
+
|
|
112
|
+
# region ObjectRegistry
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class PrefixRegistry(Registry["RegisteredObject"]):
|
|
116
|
+
"""
|
|
117
|
+
Manages the creation, storage, and retrieval of objects with unique IDs,
|
|
118
|
+
typically objects that self-register using a specific id_prefix. New IDs are generated
|
|
119
|
+
incrementally, the number format can be controlled by passing number_format to the constructor
|
|
120
|
+
which needs to be a valid f-string format (e.g. "04" for padding four-or-less digit numbers
|
|
121
|
+
with leading zeros)
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
type_prefix: str,
|
|
127
|
+
number_format: str = "",
|
|
128
|
+
):
|
|
129
|
+
"""The number_format argument will be used in an f-string to format the number.
|
|
130
|
+
For example, pass number_format="04" to pad four-or-less digit numbers with leading zeros.
|
|
131
|
+
"""
|
|
132
|
+
super().__init__()
|
|
133
|
+
self._type_prefix = type_prefix
|
|
134
|
+
self._counter = 0
|
|
135
|
+
self._number_format = number_format
|
|
136
|
+
|
|
137
|
+
def current_number(self, as_string=True) -> Union[str, int]:
|
|
138
|
+
if as_string:
|
|
139
|
+
return f"{self._counter:{self._number_format}}"
|
|
140
|
+
return self._counter
|
|
141
|
+
|
|
142
|
+
def next_number(self, as_string=True) -> Union[str, int]:
|
|
143
|
+
self._counter += 1
|
|
144
|
+
return self.current_number(as_string=as_string)
|
|
145
|
+
|
|
146
|
+
def _generate_id(
|
|
147
|
+
self,
|
|
148
|
+
description: Optional[str] = None,
|
|
149
|
+
uid: Optional[Hashable] = None,
|
|
150
|
+
) -> ID_str:
|
|
151
|
+
"""Generates a unique ID string based on prefix, counter, and description."""
|
|
152
|
+
# Determine the numeric part of the ID using the counter
|
|
153
|
+
numeric_part_of_id = self.next_number(as_string=True)
|
|
154
|
+
id_string = f"{self._type_prefix}{numeric_part_of_id}"
|
|
155
|
+
|
|
156
|
+
if description:
|
|
157
|
+
sane_description = "".join(
|
|
158
|
+
c if (c.isalnum() or c == "_") else "_" for c in str(description)
|
|
159
|
+
).strip("_")
|
|
160
|
+
if sane_description:
|
|
161
|
+
id_string += "_" + sane_description
|
|
162
|
+
return ID_str(id_string, uid=uid)
|
|
163
|
+
|
|
164
|
+
def create_and_register(
|
|
165
|
+
self, cls, description: str = "", uid: Optional[Hashable] = None, **kwargs
|
|
166
|
+
):
|
|
167
|
+
"""
|
|
168
|
+
Creates an instance of 'cls', assigns a unique ID, registers it,
|
|
169
|
+
and returns the instance.
|
|
170
|
+
'cls' __init__ method must accept 'obj_id' as its first argument after self.
|
|
171
|
+
'description' is used for the descriptive part of the ID.
|
|
172
|
+
Other kwargs are passed to the cls constructor.
|
|
173
|
+
"""
|
|
174
|
+
obj_id = self._generate_id(description=description, uid=uid)
|
|
175
|
+
# The class's __init__ must be prepared to accept obj_id
|
|
176
|
+
instance = cls(id_prefix=self._type_prefix, uid=uid, **kwargs)
|
|
177
|
+
if obj_id in self._objects:
|
|
178
|
+
# This should ideally not happen if _generate_id is robust
|
|
179
|
+
raise ValueError(
|
|
180
|
+
f"Critical error: Duplicate ID {obj_id} generated for {self._type_prefix}."
|
|
181
|
+
)
|
|
182
|
+
self._objects[obj_id] = instance
|
|
183
|
+
return instance
|
|
184
|
+
|
|
185
|
+
def get(self, obj_id: str) -> Optional[Any]:
|
|
186
|
+
return super().get(obj_id)
|
|
187
|
+
|
|
188
|
+
def register_existing(self, obj_instance: Any, _key: Optional[str] = None) -> Any:
|
|
189
|
+
"""Registers an externally created object that already has an ID."""
|
|
190
|
+
if _key is None:
|
|
191
|
+
if not hasattr(obj_instance, "id") or obj_instance.id is None:
|
|
192
|
+
raise ValueError("Object must have an ID to be registered.")
|
|
193
|
+
|
|
194
|
+
obj_id_str = str(obj_instance.id) # Ensure it's a string for startswith
|
|
195
|
+
|
|
196
|
+
else:
|
|
197
|
+
# _key should only be used for external objects
|
|
198
|
+
obj_id_str = _key
|
|
199
|
+
|
|
200
|
+
if not obj_id_str.startswith(self._type_prefix):
|
|
201
|
+
raise ValueError(
|
|
202
|
+
f"Object ID '{obj_id_str}' does not match registry prefix '{self._type_prefix}'"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
super().register(obj_id_str, obj_instance)
|
|
206
|
+
|
|
207
|
+
# Try to update counter if a manually registered ID has a higher sequence
|
|
208
|
+
# ID format is "{id_prefix}{int_counter}[_optional_arbitrary_description]"
|
|
209
|
+
# Escape the prefix in case it contains special regex characters
|
|
210
|
+
escaped_prefix = re.escape(self._type_prefix)
|
|
211
|
+
pattern = rf"^{escaped_prefix}(\d+)(?:_.*)?$"
|
|
212
|
+
|
|
213
|
+
match = re.match(pattern, obj_id_str)
|
|
214
|
+
if match:
|
|
215
|
+
num_str = match.group(1) # Get the captured digits
|
|
216
|
+
seq = int(num_str)
|
|
217
|
+
if seq > self._counter:
|
|
218
|
+
self._counter = seq
|
|
219
|
+
return obj_instance
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
REGISTRY: Dict[str, PrefixRegistry] = {}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def get_registry_by_prefix(prefix: str) -> PrefixRegistry:
|
|
226
|
+
if prefix not in REGISTRY:
|
|
227
|
+
REGISTRY[prefix] = PrefixRegistry(type_prefix=prefix)
|
|
228
|
+
return REGISTRY[prefix]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@cache
|
|
232
|
+
def get_object_by_id(obj_id: str) -> Any:
|
|
233
|
+
"""Retrieves an object by its ID from any known ObjectRegistry."""
|
|
234
|
+
for registry_instance in REGISTRY.values():
|
|
235
|
+
if (obj := registry_instance.get(obj_id)) is not None:
|
|
236
|
+
return obj
|
|
237
|
+
raise KeyError(f"Object with ID '{obj_id}' not found in any registry.")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@cache
|
|
241
|
+
def id_is_registered(obj_id: str) -> Any:
|
|
242
|
+
"""Returns true if obj_id is registered in any ObjectRegistry."""
|
|
243
|
+
for registry_instance in REGISTRY.values():
|
|
244
|
+
if registry_instance.get(obj_id) is not None:
|
|
245
|
+
return True
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def iter_objects_by_ids(*obj_ids: str) -> Iterator["RegisteredObject"]:
|
|
250
|
+
obj_ids = utils.treat_variadic_argument(obj_ids)
|
|
251
|
+
for obj_id in obj_ids:
|
|
252
|
+
yield get_object_by_id(obj_id)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def register_object(
|
|
256
|
+
obj: Any,
|
|
257
|
+
id_prefix: str,
|
|
258
|
+
uid: Optional[Hashable] = None,
|
|
259
|
+
description: Optional[str] = None,
|
|
260
|
+
) -> Any: # Return the object itself
|
|
261
|
+
"""
|
|
262
|
+
Assigns an ID to an object and registers it in the appropriate registry.
|
|
263
|
+
The object's 'id' attribute will be set.
|
|
264
|
+
"""
|
|
265
|
+
reg = get_registry_by_prefix(id_prefix)
|
|
266
|
+
obj._id = reg._generate_id(uid=uid, description=description)
|
|
267
|
+
reg.register_existing(obj) # This uses obj.id as the key
|
|
268
|
+
return obj
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def ensure_registration(
|
|
272
|
+
obj: Any,
|
|
273
|
+
id_prefix: Optional[str] = None,
|
|
274
|
+
uid: Optional[Hashable] = None,
|
|
275
|
+
description: Optional[str] = None,
|
|
276
|
+
) -> str:
|
|
277
|
+
"""This makes sure that the object is part of a registry and can be retrieved via an id.
|
|
278
|
+
For internal objects (:class:`RegisteredObject`) this returns simply the id. External objects
|
|
279
|
+
are added to a registry according to their class name and a new ID is returned.
|
|
280
|
+
"""
|
|
281
|
+
if (existing_id := getattr(obj, "id", None)) is not None:
|
|
282
|
+
if id_is_registered(existing_id):
|
|
283
|
+
return existing_id
|
|
284
|
+
if id_prefix is not None:
|
|
285
|
+
reg = get_registry_by_prefix(id_prefix)
|
|
286
|
+
else:
|
|
287
|
+
reg = get_registry_by_prefix(obj.__class__.__name__)
|
|
288
|
+
if existing_id is None:
|
|
289
|
+
obj_id = reg._generate_id(uid=uid, description=description)
|
|
290
|
+
else:
|
|
291
|
+
obj_id = existing_id
|
|
292
|
+
reg.register_existing(obj, _key=obj_id)
|
|
293
|
+
return obj_id
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# endregion ObjectRegistry
|
|
297
|
+
|
|
298
|
+
# region CoordinatesMapRegistry
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class CoordinatesMapRegistry(Registry["CoordinatesMap"]):
|
|
302
|
+
"""
|
|
303
|
+
Manages CoordinatesMap instances, keyed by (source_type_key, target_type_key).
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
def __init__(self):
|
|
307
|
+
super().__init__()
|
|
308
|
+
self._get_coordinate_type_func_cache: Optional[Callable] = None
|
|
309
|
+
|
|
310
|
+
def _get_coordinate_type_loader(self) -> Callable:
|
|
311
|
+
"""Helper to load get_coordinate_type lazily to avoid import issues."""
|
|
312
|
+
if self._get_coordinate_type_func_cache is None:
|
|
313
|
+
from processing.tta.conversions import get_coordinate_type
|
|
314
|
+
|
|
315
|
+
self._get_coordinate_type_func_cache = get_coordinate_type
|
|
316
|
+
return self._get_coordinate_type_func_cache
|
|
317
|
+
|
|
318
|
+
def make_key_for_map(self, cmap: "ConversionMap") -> tuple:
|
|
319
|
+
return (
|
|
320
|
+
cmap._source_unit,
|
|
321
|
+
cmap._target_unit,
|
|
322
|
+
cmap._target_type,
|
|
323
|
+
cmap._custom_conversion_function,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def register_map(self, cmap: "ConversionMap"):
|
|
327
|
+
"""Registers a CoordinatesMap instance.
|
|
328
|
+
The key is derived from the map's source and target coordinate types.
|
|
329
|
+
"""
|
|
330
|
+
# CoordinatesMap must have source_coordinate_type and target_coordinate_type properties
|
|
331
|
+
key = self.make_key_for_map(cmap)
|
|
332
|
+
if key in self._objects:
|
|
333
|
+
return
|
|
334
|
+
super().register(key, cmap)
|
|
335
|
+
|
|
336
|
+
def get_map(
|
|
337
|
+
self,
|
|
338
|
+
source_unit: TimeUnit,
|
|
339
|
+
target_unit: TimeUnit,
|
|
340
|
+
target_type: Optional[NumberType] = None,
|
|
341
|
+
converter_func: Optional[
|
|
342
|
+
Callable[[Union[Number, np.ndarray]], Union[Number, np.ndarray]]
|
|
343
|
+
] = None,
|
|
344
|
+
) -> Optional["ConversionMap"]:
|
|
345
|
+
"""
|
|
346
|
+
Retrieves a CoordinatesMap instance based on source and target coordinate types.
|
|
347
|
+
"""
|
|
348
|
+
key = (source_unit, target_unit, target_type, converter_func)
|
|
349
|
+
super().get(key)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
CMAP_REGISTRY = CoordinatesMapRegistry()
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def register_cmap(cmap: "ConversionMap"):
|
|
356
|
+
CMAP_REGISTRY.register_map(cmap)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def get_cmap(
|
|
360
|
+
source_unit: TimeUnit,
|
|
361
|
+
target_unit: TimeUnit,
|
|
362
|
+
target_type: Optional[NumberType] = None,
|
|
363
|
+
converter_func: Optional[
|
|
364
|
+
Callable[[Union[Number, np.ndarray]], Union[Number, np.ndarray]]
|
|
365
|
+
] = None,
|
|
366
|
+
) -> "ConversionMap":
|
|
367
|
+
return CMAP_REGISTRY.get_map(
|
|
368
|
+
source_unit=source_unit,
|
|
369
|
+
target_unit=target_unit,
|
|
370
|
+
target_type=target_type,
|
|
371
|
+
converter_func=converter_func,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# endregion CoordinatesMapRegistry
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# region flyweight
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@cache
|
|
382
|
+
def get_property_defaults(cls):
|
|
383
|
+
class_property_defaults = getattr(cls, "_property_defaults", {})
|
|
384
|
+
if class_property_defaults is None:
|
|
385
|
+
class_property_defaults = {}
|
|
386
|
+
elif not isinstance(class_property_defaults, dict):
|
|
387
|
+
class_property_defaults = {field: None for field in class_property_defaults}
|
|
388
|
+
return class_property_defaults
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def get_property_values(
|
|
392
|
+
self,
|
|
393
|
+
include_shared=True,
|
|
394
|
+
include_individual=True,
|
|
395
|
+
exclude_properties: Optional[tuple[str]] = ("data",),
|
|
396
|
+
**column2field,
|
|
397
|
+
) -> dict:
|
|
398
|
+
"""Returns a dictionary of property values for a flyweight object."""
|
|
399
|
+
if exclude_properties is None:
|
|
400
|
+
exclude_properties = []
|
|
401
|
+
if include_shared:
|
|
402
|
+
properties_dict = {
|
|
403
|
+
k: v for k, v in self._shared.items() if k not in exclude_properties
|
|
404
|
+
}
|
|
405
|
+
else:
|
|
406
|
+
properties_dict = {}
|
|
407
|
+
if include_individual:
|
|
408
|
+
for name in dir(self):
|
|
409
|
+
if name.startswith("_") or name in exclude_properties:
|
|
410
|
+
continue
|
|
411
|
+
class_attribute = getattr(type(self), name, None)
|
|
412
|
+
if isinstance(class_attribute, property):
|
|
413
|
+
try:
|
|
414
|
+
value = getattr(self, name)
|
|
415
|
+
properties_dict[name] = value
|
|
416
|
+
except AttributeError:
|
|
417
|
+
properties_dict[name] = None
|
|
418
|
+
except Exception as e:
|
|
419
|
+
properties_dict[name] = f"Error accessing property: {e}"
|
|
420
|
+
for column, field in column2field.items():
|
|
421
|
+
properties_dict[column] = self.get(field)
|
|
422
|
+
return properties_dict
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def flyweight(**decorator_defaults):
|
|
426
|
+
"""Class decorator that lets you define the parameters (and their default values) that are to be
|
|
427
|
+
shared among all instances for which they have the same value configuration. The shared
|
|
428
|
+
values are accessible via the ._shared dict. Shared fields can be defined via decorator
|
|
429
|
+
arguments or a '_property_defaults' class attribute.
|
|
430
|
+
|
|
431
|
+
Conflict Resolution: Decorator arguments take precedence over class attribute defaults.
|
|
432
|
+
|
|
433
|
+
Example:
|
|
434
|
+
|
|
435
|
+
# Defining shared fields via decorator
|
|
436
|
+
@flyweight(data=None)
|
|
437
|
+
class Decorated:
|
|
438
|
+
def __init__(self):
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def data(self):
|
|
443
|
+
return self._shared['data']
|
|
444
|
+
|
|
445
|
+
# Defining shared fields via class attribute (decorator wins)
|
|
446
|
+
|
|
447
|
+
@flyweight(value='decorator_value', another='decorator_another')
|
|
448
|
+
class WithClassAttributes:
|
|
449
|
+
_property_defaults = {'value': 'class_attribute_value', 'default_only': 'only_from_class'}
|
|
450
|
+
|
|
451
|
+
def __init__(self):
|
|
452
|
+
pass
|
|
453
|
+
|
|
454
|
+
@property
|
|
455
|
+
def value(self):
|
|
456
|
+
return self._shared['value']
|
|
457
|
+
|
|
458
|
+
@property
|
|
459
|
+
def another(self):
|
|
460
|
+
return self._shared['another']
|
|
461
|
+
|
|
462
|
+
@property
|
|
463
|
+
def default_only(self):
|
|
464
|
+
return self._shared['default_only']
|
|
465
|
+
|
|
466
|
+
"""
|
|
467
|
+
|
|
468
|
+
def decorator(cls):
|
|
469
|
+
_original_new = cls.__new__
|
|
470
|
+
_original_init = (
|
|
471
|
+
cls.__init__
|
|
472
|
+
if hasattr(cls, "__init__") and cls.__init__ is not object.__init__
|
|
473
|
+
else None
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
_shared_dict_cache = {}
|
|
477
|
+
|
|
478
|
+
# Combining _property_defaults class attributes from all base classes in the MRO
|
|
479
|
+
combined_property_defaults = {}
|
|
480
|
+
for base_class in cls.__mro__:
|
|
481
|
+
class_defaults = get_property_defaults(base_class)
|
|
482
|
+
combined_property_defaults.update(class_defaults)
|
|
483
|
+
|
|
484
|
+
shared_params_defaults = dict(
|
|
485
|
+
combined_property_defaults
|
|
486
|
+
) # start with class attributes
|
|
487
|
+
shared_params_defaults.update(
|
|
488
|
+
decorator_defaults
|
|
489
|
+
) # update (override) with decorator defaults
|
|
490
|
+
|
|
491
|
+
def __new__(cls, *args, **kwargs):
|
|
492
|
+
shared_kwargs_for_key = {
|
|
493
|
+
k: kwargs.get(k, shared_params_defaults.get(k))
|
|
494
|
+
for k in shared_params_defaults
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
hashable_key_parts = []
|
|
498
|
+
for k, v in sorted(shared_kwargs_for_key.items()):
|
|
499
|
+
# Special handling for unhashable types
|
|
500
|
+
if isinstance(v, (list, dict, set, pd.DataFrame, bytearray)):
|
|
501
|
+
hashable_key_parts.append((k, id(v)))
|
|
502
|
+
else:
|
|
503
|
+
hashable_key_parts.append((k, v))
|
|
504
|
+
|
|
505
|
+
shared_key = tuple(hashable_key_parts)
|
|
506
|
+
|
|
507
|
+
if shared_key not in _shared_dict_cache:
|
|
508
|
+
_shared_dict = {}
|
|
509
|
+
for k, v in shared_kwargs_for_key.items():
|
|
510
|
+
_shared_dict[k] = v
|
|
511
|
+
_shared_dict_cache[shared_key] = _shared_dict
|
|
512
|
+
|
|
513
|
+
cached_shared_dict = _shared_dict_cache[shared_key]
|
|
514
|
+
setattr(
|
|
515
|
+
cls, "get_property_values", get_property_values
|
|
516
|
+
) # Add get_property_values method to the class
|
|
517
|
+
new_instance = _original_new(cls)
|
|
518
|
+
new_instance._shared = cached_shared_dict
|
|
519
|
+
return new_instance
|
|
520
|
+
|
|
521
|
+
def __init__(self, *args, **kwargs):
|
|
522
|
+
# Use the combined defaults here to remove shared parameters from kwargs
|
|
523
|
+
for k in shared_params_defaults:
|
|
524
|
+
if k in kwargs:
|
|
525
|
+
del kwargs[k]
|
|
526
|
+
|
|
527
|
+
if _original_init:
|
|
528
|
+
_original_init(self, *args, **kwargs)
|
|
529
|
+
else:
|
|
530
|
+
object.__init__(
|
|
531
|
+
self
|
|
532
|
+
) # Call original object __init__ if no custom one exists
|
|
533
|
+
|
|
534
|
+
cls.shared_params_defaults = shared_params_defaults # For introspection
|
|
535
|
+
cls.__new__ = __new__
|
|
536
|
+
cls.__init__ = __init__
|
|
537
|
+
|
|
538
|
+
return cls
|
|
539
|
+
|
|
540
|
+
return decorator
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
# endregion flyweight
|
|
544
|
+
|
|
545
|
+
if __name__ == "__main__":
|
|
546
|
+
pass
|