dataioc 0.1.0__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.
- dataioc/__init__.py +40 -0
- dataioc/_data.py +915 -0
- dataioc/_data_ndarray.py +114 -0
- dataioc/py.typed +0 -0
- dataioc-0.1.0.dist-info/METADATA +119 -0
- dataioc-0.1.0.dist-info/RECORD +8 -0
- dataioc-0.1.0.dist-info/WHEEL +4 -0
- dataioc-0.1.0.dist-info/licenses/LICENSE +21 -0
dataioc/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Lazy, indexed data dependency injection without mandatory numerical libraries."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ._data import (
|
|
6
|
+
DataDescriptor,
|
|
7
|
+
DataIoC,
|
|
8
|
+
DescribedData,
|
|
9
|
+
IndexedData,
|
|
10
|
+
IndexedDataIoC,
|
|
11
|
+
IndexedDataMeta,
|
|
12
|
+
IndexedDataTypeDescriptor,
|
|
13
|
+
SupportsBuild,
|
|
14
|
+
UniqueData,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ._data_ndarray import DataNDArray as DataNDArray
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"DataDescriptor",
|
|
24
|
+
"DataIoC",
|
|
25
|
+
"DescribedData",
|
|
26
|
+
"IndexedData",
|
|
27
|
+
"IndexedDataIoC",
|
|
28
|
+
"IndexedDataMeta",
|
|
29
|
+
"IndexedDataTypeDescriptor",
|
|
30
|
+
"SupportsBuild",
|
|
31
|
+
"UniqueData",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def __getattr__(name: str):
|
|
36
|
+
if name == "DataNDArray":
|
|
37
|
+
from ._data_ndarray import DataNDArray
|
|
38
|
+
|
|
39
|
+
return DataNDArray
|
|
40
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
dataioc/_data.py
ADDED
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import copy
|
|
5
|
+
import inspect
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import (
|
|
9
|
+
Any,
|
|
10
|
+
Generic,
|
|
11
|
+
Optional,
|
|
12
|
+
Protocol,
|
|
13
|
+
TypeVar,
|
|
14
|
+
Union,
|
|
15
|
+
cast,
|
|
16
|
+
overload,
|
|
17
|
+
runtime_checkable,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if sys.version_info >= (3, 11):
|
|
21
|
+
from typing import Self
|
|
22
|
+
else:
|
|
23
|
+
from typing_extensions import Self
|
|
24
|
+
|
|
25
|
+
DataT = TypeVar("DataT")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@runtime_checkable
|
|
29
|
+
class SupportsBuild(Protocol):
|
|
30
|
+
"""Protocol for objects that can build a value from a data container."""
|
|
31
|
+
|
|
32
|
+
def __build__(self, container: DataIoC) -> Any:
|
|
33
|
+
"""Build and return a value using ``container``."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DataDescriptor(Generic[DataT]):
|
|
38
|
+
"""Identify a value in ``DataIoC`` and optionally define how to build it.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
id
|
|
43
|
+
Data ID. When omitted, the descriptor is a weak reference to ID 0.
|
|
44
|
+
|
|
45
|
+
Notes
|
|
46
|
+
-----
|
|
47
|
+
A ``DataDescriptor`` is used as a dictionary key, so its subclasses must remain
|
|
48
|
+
hashable and comparable. A subclass satisfies this requirement automatically
|
|
49
|
+
when each of its attributes is itself hashable and comparable.
|
|
50
|
+
|
|
51
|
+
IDs distinguish multiple values of the same type. The default ID is ``0`` when
|
|
52
|
+
no ID is specified. For example, readings from three sensors of the same type
|
|
53
|
+
can be identified by ``Sensor``, ``Sensor[1]``, and ``Sensor[2]`` and mapped to
|
|
54
|
+
different values in ``DataIoC``.
|
|
55
|
+
|
|
56
|
+
Examples
|
|
57
|
+
--------
|
|
58
|
+
An unindexed descriptor such as ``SensorData`` has a weak ID and identifies a
|
|
59
|
+
data type. When used to retrieve data inside ``__build__``, it is automatically
|
|
60
|
+
rebound to the concrete ID currently being built.
|
|
61
|
+
|
|
62
|
+
>>> from dataioc import DataDescriptor, DataIoC, DataNDArray
|
|
63
|
+
>>>
|
|
64
|
+
>>> class SensorData(DataNDArray):
|
|
65
|
+
... def __new__(cls, data, **kwargs):
|
|
66
|
+
... return super().__new__(cls, data, **kwargs)
|
|
67
|
+
>>>
|
|
68
|
+
>>> class Sum(DataDescriptor):
|
|
69
|
+
... def __build__(self, container: DataIoC):
|
|
70
|
+
... return container[SensorData].sum() # SensorData follows Sum's ID
|
|
71
|
+
>>>
|
|
72
|
+
>>> container = DataIoC().with_data(
|
|
73
|
+
... SensorData([1, 1, 1]), SensorData[1]([2, 2, 2])
|
|
74
|
+
... )
|
|
75
|
+
>>> print(container[Sum[0]], container[Sum[1]])
|
|
76
|
+
3 6
|
|
77
|
+
|
|
78
|
+
An explicitly indexed descriptor such as ``SensorData[1]`` has a strong ID and
|
|
79
|
+
identifies one specific data group. It is not rebound inside ``__build__``.
|
|
80
|
+
|
|
81
|
+
>>> class OffsetSensor0(DataDescriptor):
|
|
82
|
+
... def __build__(self, container: DataIoC):
|
|
83
|
+
... base = container[SensorData[0]].sum() # Explicitly read ID 0
|
|
84
|
+
... return base + container[SensorData]
|
|
85
|
+
>>>
|
|
86
|
+
>>> print(container[OffsetSensor0[0]], container[OffsetSensor0[1]])
|
|
87
|
+
[4 4 4] [5 5 5]
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
__slots__ = ["_id"]
|
|
91
|
+
DefaultWeakID = -1
|
|
92
|
+
DefaultID = -DefaultWeakID - 1
|
|
93
|
+
|
|
94
|
+
def __init__(self, id=DefaultWeakID) -> None:
|
|
95
|
+
self.id = id
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def id(self):
|
|
99
|
+
"""The non-negative data ID represented by this descriptor."""
|
|
100
|
+
if self.signed_id < 0:
|
|
101
|
+
return -self.signed_id - 1
|
|
102
|
+
else:
|
|
103
|
+
return self.signed_id
|
|
104
|
+
|
|
105
|
+
@id.setter
|
|
106
|
+
def id(self, val):
|
|
107
|
+
self._id = val
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def signed_id(self):
|
|
111
|
+
"""The internal signed ID, where a negative value marks a weak binding."""
|
|
112
|
+
return self._id
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def is_weak_id(self):
|
|
116
|
+
"""Whether this descriptor may inherit the current build ID."""
|
|
117
|
+
return self.signed_id < 0
|
|
118
|
+
|
|
119
|
+
def __build__(self, container: DataIoC) -> DataT:
|
|
120
|
+
"""Build the value identified by this descriptor.
|
|
121
|
+
|
|
122
|
+
Subclasses override this method to request dependencies from ``container``
|
|
123
|
+
and return the resulting value. The base implementation requires the value
|
|
124
|
+
to be provided explicitly.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
container
|
|
129
|
+
Container used to resolve dependencies.
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
DataT
|
|
134
|
+
The constructed value.
|
|
135
|
+
|
|
136
|
+
Raises
|
|
137
|
+
------
|
|
138
|
+
NotImplementedError
|
|
139
|
+
If the subclass does not provide a builder.
|
|
140
|
+
"""
|
|
141
|
+
raise NotImplementedError(f"{self!r} must be provided.")
|
|
142
|
+
|
|
143
|
+
def index_implicit(self, new_index):
|
|
144
|
+
"""Weakly rebind to ``new_index``, preserving an existing strong ID."""
|
|
145
|
+
return self.index(new_index, weak=True)
|
|
146
|
+
|
|
147
|
+
def index_explicit(self, new_index):
|
|
148
|
+
"""Explicitly rebind to ``new_index``, overriding any existing ID."""
|
|
149
|
+
return self.index(new_index, weak=False)
|
|
150
|
+
|
|
151
|
+
def index(self, new_index, weak=True):
|
|
152
|
+
"""Bind this descriptor to another ID for related values of the same type.
|
|
153
|
+
|
|
154
|
+
For example, readings from three sensors of the same type can be identified
|
|
155
|
+
by:
|
|
156
|
+
|
|
157
|
+
* ``Sensor[0]``
|
|
158
|
+
* ``Sensor[1]``
|
|
159
|
+
* ``Sensor[2]``
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
new_index
|
|
164
|
+
New data ID.
|
|
165
|
+
weak
|
|
166
|
+
Whether the rebinding is weak. A weak rebinding has no effect when this
|
|
167
|
+
descriptor already has a strong ID.
|
|
168
|
+
|
|
169
|
+
Returns
|
|
170
|
+
-------
|
|
171
|
+
DataDescriptor
|
|
172
|
+
This descriptor if a weak rebinding is blocked by a strong ID;
|
|
173
|
+
otherwise, a copy bound to ``new_index``.
|
|
174
|
+
"""
|
|
175
|
+
ret = self
|
|
176
|
+
if not weak or self.is_weak_id:
|
|
177
|
+
# Explicit rebinding, or rebinding a weak ID, may select the new ID.
|
|
178
|
+
ret = copy.copy(ret)
|
|
179
|
+
ret.id = new_index
|
|
180
|
+
|
|
181
|
+
return ret
|
|
182
|
+
|
|
183
|
+
def __getitem__(self, index) -> DataDescriptor[DataT]:
|
|
184
|
+
"""Return a descriptor explicitly rebound to ``index``.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
index
|
|
189
|
+
New data ID.
|
|
190
|
+
|
|
191
|
+
Returns
|
|
192
|
+
-------
|
|
193
|
+
DataDescriptor
|
|
194
|
+
A copy bound to ``index``.
|
|
195
|
+
|
|
196
|
+
Notes
|
|
197
|
+
-----
|
|
198
|
+
Internal code should prefer ``index_implicit`` so weak IDs can inherit the
|
|
199
|
+
current build ID automatically.
|
|
200
|
+
"""
|
|
201
|
+
return self.index_explicit(index)
|
|
202
|
+
|
|
203
|
+
def __class_getitem__(cls, id, *args):
|
|
204
|
+
if isinstance(id, int):
|
|
205
|
+
return cls(*args, id=id)
|
|
206
|
+
else:
|
|
207
|
+
# Generic supplies this method at runtime, outside the typed MRO.
|
|
208
|
+
return cast(Any, super()).__class_getitem__(id, *args)
|
|
209
|
+
|
|
210
|
+
def __hash__(self):
|
|
211
|
+
return hash(tuple(getattr(self, k) for k in sorted(self.keys)))
|
|
212
|
+
|
|
213
|
+
def __eq__(self, other):
|
|
214
|
+
if type(self) is not type(other):
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
for k in self.keys:
|
|
218
|
+
if getattr(self, k) != getattr(other, k):
|
|
219
|
+
return False
|
|
220
|
+
else:
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def keys(self):
|
|
225
|
+
"""Attribute names that participate in equality and hashing."""
|
|
226
|
+
keys = []
|
|
227
|
+
for c in reversed(inspect.getmro(type(self))):
|
|
228
|
+
keys.extend(getattr(c, "__slots__", []))
|
|
229
|
+
keys.extend(getattr(self, "__dict__", []))
|
|
230
|
+
|
|
231
|
+
keys = set(keys)
|
|
232
|
+
keys.remove("_id")
|
|
233
|
+
keys.add("id") # Keep the public ID non-negative.
|
|
234
|
+
|
|
235
|
+
return keys
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def params(self):
|
|
239
|
+
"""Constructor parameters reconstructed from the descriptor state."""
|
|
240
|
+
return {k.strip("_"): getattr(self, k) for k in sorted(self.keys)}
|
|
241
|
+
|
|
242
|
+
def __repr__(self):
|
|
243
|
+
params = self.params
|
|
244
|
+
del params["id"]
|
|
245
|
+
|
|
246
|
+
id_str = f"[{self.id}]" if self.id > 0 else ""
|
|
247
|
+
param_str = ", ".join([f"{k}={repr(v)}" for k, v in params.items()])
|
|
248
|
+
|
|
249
|
+
return f"{type(self).__name__}{id_str}({param_str})"
|
|
250
|
+
|
|
251
|
+
def __copy__(self):
|
|
252
|
+
return type(self)(**self.params)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class IndexedDataTypeDescriptor(DataDescriptor[DataT]):
|
|
256
|
+
"""Bind an indexed data type to a data ID."""
|
|
257
|
+
|
|
258
|
+
__slots__ = ["_dtype"]
|
|
259
|
+
|
|
260
|
+
@classmethod
|
|
261
|
+
def of(cls, dtype, id=DataDescriptor.DefaultWeakID):
|
|
262
|
+
"""Create the appropriate descriptor for ``dtype`` and ``id``."""
|
|
263
|
+
if isinstance(dtype, IndexedDataMeta):
|
|
264
|
+
return type(dtype).__getitem__(dtype, id)
|
|
265
|
+
elif issubclass(dtype, DataDescriptor):
|
|
266
|
+
return dtype(id=id)
|
|
267
|
+
else:
|
|
268
|
+
# Ordinary types cannot be indexed and always use the default ID.
|
|
269
|
+
return cls(dtype, id=DataDescriptor.DefaultID)
|
|
270
|
+
|
|
271
|
+
def __init__(self, dtype: type, *args, **kwargs) -> None:
|
|
272
|
+
super().__init__(*args, **kwargs)
|
|
273
|
+
self._dtype = dtype
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def dtype(self):
|
|
277
|
+
"""The data type bound by this descriptor."""
|
|
278
|
+
return self._dtype
|
|
279
|
+
|
|
280
|
+
def __build__(self, container: DataIoC) -> DataT:
|
|
281
|
+
builder = _extract_builder_with_context(self.dtype, self)
|
|
282
|
+
if builder is None:
|
|
283
|
+
raise TypeError(f"No builder for {self.dtype!r}")
|
|
284
|
+
ret = builder(container)
|
|
285
|
+
return ret
|
|
286
|
+
|
|
287
|
+
def __call__(self, *args, **kwargs):
|
|
288
|
+
"""Construct a value and associate it with this descriptor."""
|
|
289
|
+
return DescribedData(self, self.dtype(*args, **kwargs))
|
|
290
|
+
|
|
291
|
+
def __repr__(self):
|
|
292
|
+
id_str = f"[{self.id}]" if self.id > 0 else ""
|
|
293
|
+
return f"{self.dtype.__name__}{id_str}"
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class DescribedData:
|
|
297
|
+
"""Pair a value with the descriptor under which it should be registered."""
|
|
298
|
+
|
|
299
|
+
def __init__(self, desc: DataDescriptor[DataT], data: DataT):
|
|
300
|
+
self.desc = desc
|
|
301
|
+
self.data = data
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class IndexedDataMeta(type):
|
|
305
|
+
"""Make a data type indexable so ``DataIoC`` can bind related data groups.
|
|
306
|
+
|
|
307
|
+
Notes
|
|
308
|
+
-----
|
|
309
|
+
A derived class can define ``__class_index__`` to customize its indexing
|
|
310
|
+
behavior.
|
|
311
|
+
|
|
312
|
+
A derived class can inherit ``UniqueData`` to indicate that the type is unique
|
|
313
|
+
and does not require indexing.
|
|
314
|
+
|
|
315
|
+
Every type that is not an ``IndexedData`` subclass is treated as unique and is
|
|
316
|
+
not indexed.
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
def __getitem__(self, id=DataDescriptor.DefaultWeakID, *args):
|
|
320
|
+
class_getitem = getattr(self, "__class_index__", None)
|
|
321
|
+
if class_getitem is not None:
|
|
322
|
+
return class_getitem(id, *args)
|
|
323
|
+
else:
|
|
324
|
+
return IndexedDataTypeDescriptor(self, *args, id=id)
|
|
325
|
+
|
|
326
|
+
def __repr__(self):
|
|
327
|
+
return f"{self.__name__}"
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class IndexedData(metaclass=IndexedDataMeta):
|
|
331
|
+
"""Marker base class for data types that can have one value per ID."""
|
|
332
|
+
|
|
333
|
+
pass
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class UniqueData:
|
|
337
|
+
"""Make an indexed data type share one value across all build IDs."""
|
|
338
|
+
|
|
339
|
+
@classmethod
|
|
340
|
+
def __class_index__(cls, id, *args):
|
|
341
|
+
# Bind to the default ID so every implicit access resolves the same value.
|
|
342
|
+
return IndexedDataTypeDescriptor(cls, *args, id=DataDescriptor.DefaultID)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class DataIoC:
|
|
346
|
+
"""Resolve data dependencies on demand and cache constructed values."""
|
|
347
|
+
|
|
348
|
+
def __init__(self, allow_implicit_registering=True, record_all=False):
|
|
349
|
+
"""Initialize a data IoC container.
|
|
350
|
+
|
|
351
|
+
Parameters
|
|
352
|
+
----------
|
|
353
|
+
allow_implicit_registering
|
|
354
|
+
Allow a requested target's own builder to be registered on first
|
|
355
|
+
access. If false, only explicitly registered data and builders can be
|
|
356
|
+
retrieved or built.
|
|
357
|
+
record_all
|
|
358
|
+
Retain all container access records. By default, only dependencies from
|
|
359
|
+
the current access are retained for diagnostic output. If true, the
|
|
360
|
+
complete access tree is retained and may add overhead.
|
|
361
|
+
"""
|
|
362
|
+
self._collection: dict[Union[DataDescriptor, type], Any] = {}
|
|
363
|
+
self._lazy_collection: dict[
|
|
364
|
+
Union[DataDescriptor, type], Callable[[DataIoC], Any]
|
|
365
|
+
] = {}
|
|
366
|
+
self.allow_implicit_register = allow_implicit_registering
|
|
367
|
+
self.record_all = record_all
|
|
368
|
+
|
|
369
|
+
self._logger = _DataIoCAccessLogger(key=self)
|
|
370
|
+
|
|
371
|
+
def with_data(self, *data: Any) -> Self:
|
|
372
|
+
"""Register existing values and return this container.
|
|
373
|
+
|
|
374
|
+
Parameters
|
|
375
|
+
----------
|
|
376
|
+
*data
|
|
377
|
+
Values to register by their types. Values created through an indexed
|
|
378
|
+
type, such as ``Sensor[1](...)``, retain their descriptors.
|
|
379
|
+
|
|
380
|
+
Returns
|
|
381
|
+
-------
|
|
382
|
+
DataIoC
|
|
383
|
+
This container, allowing chained registration calls.
|
|
384
|
+
"""
|
|
385
|
+
for d in data:
|
|
386
|
+
if isinstance(d, DescribedData):
|
|
387
|
+
self[d.desc] = d.data
|
|
388
|
+
else:
|
|
389
|
+
self[type(d)] = d
|
|
390
|
+
|
|
391
|
+
return self
|
|
392
|
+
|
|
393
|
+
def add(
|
|
394
|
+
self,
|
|
395
|
+
data_type: Union[DataT, type[DataT], DataDescriptor[DataT]],
|
|
396
|
+
data: Optional[DataT] = None,
|
|
397
|
+
) -> Self:
|
|
398
|
+
"""Register existing data or a target's own lazy builder.
|
|
399
|
+
|
|
400
|
+
Passing a type or descriptor without ``data`` registers the builder defined
|
|
401
|
+
by that target. Passing an existing value as ``data_type`` registers it by
|
|
402
|
+
type. Passing both a key and a non-``None`` value registers that value under
|
|
403
|
+
the key.
|
|
404
|
+
|
|
405
|
+
Parameters
|
|
406
|
+
----------
|
|
407
|
+
data_type
|
|
408
|
+
Existing value to register, or the type or descriptor used as a key.
|
|
409
|
+
data
|
|
410
|
+
Existing non-``None`` value to register under ``data_type``. Because
|
|
411
|
+
``None`` selects builder registration, assign through ``container[key]``
|
|
412
|
+
to store an explicit ``None`` value.
|
|
413
|
+
|
|
414
|
+
Returns
|
|
415
|
+
-------
|
|
416
|
+
DataIoC
|
|
417
|
+
This container, allowing chained registration calls.
|
|
418
|
+
|
|
419
|
+
Raises
|
|
420
|
+
------
|
|
421
|
+
TypeError
|
|
422
|
+
If no builder can be found or the supplied key is invalid.
|
|
423
|
+
"""
|
|
424
|
+
data_type = _descriptor_instance(data_type)
|
|
425
|
+
if data is None:
|
|
426
|
+
if isinstance(data_type, DataDescriptor) or isinstance(data_type, type):
|
|
427
|
+
builder = _extract_builder_with_context(data_type)
|
|
428
|
+
if builder is None:
|
|
429
|
+
raise TypeError(f"No builder for {data_type!r}")
|
|
430
|
+
self._lazy_collection[data_type] = builder
|
|
431
|
+
else:
|
|
432
|
+
self.with_data(data_type)
|
|
433
|
+
else:
|
|
434
|
+
if not isinstance(data_type, (type, DataDescriptor)):
|
|
435
|
+
raise TypeError("A data key must be a type or DataDescriptor.")
|
|
436
|
+
self[data_type] = data
|
|
437
|
+
|
|
438
|
+
return self
|
|
439
|
+
|
|
440
|
+
def add_provider(
|
|
441
|
+
self,
|
|
442
|
+
data_type: Union[type[DataT], DataDescriptor[DataT]],
|
|
443
|
+
provider: Union[SupportsBuild, Callable],
|
|
444
|
+
) -> Self:
|
|
445
|
+
"""Register an alternative lazy builder for a data key.
|
|
446
|
+
|
|
447
|
+
Registering another provider for the same key replaces the builder used by
|
|
448
|
+
future uncached requests. Existing cached values are retained.
|
|
449
|
+
|
|
450
|
+
Parameters
|
|
451
|
+
----------
|
|
452
|
+
data_type
|
|
453
|
+
Type or descriptor whose value the provider builds.
|
|
454
|
+
provider
|
|
455
|
+
Callable accepting a container, or an object that defines
|
|
456
|
+
``__build__``.
|
|
457
|
+
|
|
458
|
+
Returns
|
|
459
|
+
-------
|
|
460
|
+
DataIoC
|
|
461
|
+
This container, allowing chained registration calls.
|
|
462
|
+
|
|
463
|
+
Raises
|
|
464
|
+
------
|
|
465
|
+
TypeError
|
|
466
|
+
If ``provider`` is neither callable nor an object with ``__build__``.
|
|
467
|
+
"""
|
|
468
|
+
data_type = _descriptor_instance(data_type)
|
|
469
|
+
initiator = None
|
|
470
|
+
if isinstance(data_type, DataDescriptor):
|
|
471
|
+
initiator = data_type
|
|
472
|
+
|
|
473
|
+
builder = _extract_builder(provider)
|
|
474
|
+
if builder is None:
|
|
475
|
+
raise TypeError("A provider must be callable or define __build__.")
|
|
476
|
+
self._lazy_collection[data_type] = Provider(
|
|
477
|
+
initiator=initiator, builder=builder, target=provider
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
return self
|
|
481
|
+
|
|
482
|
+
@property
|
|
483
|
+
def logger(self):
|
|
484
|
+
"""The dependency access logger used for diagnostics."""
|
|
485
|
+
return self._logger
|
|
486
|
+
|
|
487
|
+
@overload
|
|
488
|
+
def __getitem__(self, dtype: DataDescriptor[DataT]) -> DataT: ...
|
|
489
|
+
|
|
490
|
+
@overload
|
|
491
|
+
def __getitem__(self, dtype: type[DataDescriptor[DataT]]) -> DataT: ...
|
|
492
|
+
|
|
493
|
+
@overload
|
|
494
|
+
def __getitem__(self, dtype: type[DataT]) -> DataT: ...
|
|
495
|
+
|
|
496
|
+
def __getitem__(self, dtype: Any) -> Any:
|
|
497
|
+
"""Resolve and return the value identified by ``dtype``.
|
|
498
|
+
|
|
499
|
+
A cached value is returned immediately. Otherwise, the registered or
|
|
500
|
+
implicit builder is called and a successful result, including ``None``, is
|
|
501
|
+
cached under the requested key.
|
|
502
|
+
|
|
503
|
+
Parameters
|
|
504
|
+
----------
|
|
505
|
+
dtype
|
|
506
|
+
Data type or descriptor to resolve.
|
|
507
|
+
|
|
508
|
+
Returns
|
|
509
|
+
-------
|
|
510
|
+
Any
|
|
511
|
+
The registered or constructed value.
|
|
512
|
+
|
|
513
|
+
Raises
|
|
514
|
+
------
|
|
515
|
+
RuntimeError
|
|
516
|
+
If implicit registration is disabled and no builder is registered.
|
|
517
|
+
TypeError
|
|
518
|
+
If no suitable builder exists.
|
|
519
|
+
"""
|
|
520
|
+
dtype = _descriptor_instance(dtype)
|
|
521
|
+
ret: Any = None
|
|
522
|
+
with self._logger.add(dtype):
|
|
523
|
+
if dtype in self._collection:
|
|
524
|
+
ret = self._collection[dtype]
|
|
525
|
+
else:
|
|
526
|
+
builder = self.find_builder(dtype)
|
|
527
|
+
|
|
528
|
+
if builder is None:
|
|
529
|
+
if not self.allow_implicit_register:
|
|
530
|
+
raise RuntimeError(
|
|
531
|
+
f"Builder for {dtype!r} not found in DataIoC."
|
|
532
|
+
)
|
|
533
|
+
else:
|
|
534
|
+
self.add(dtype)
|
|
535
|
+
builder = self.find_builder(dtype)
|
|
536
|
+
|
|
537
|
+
if builder is None:
|
|
538
|
+
raise TypeError(f"No builder for {dtype!r}")
|
|
539
|
+
if builder is not None:
|
|
540
|
+
if isinstance(builder, Provider):
|
|
541
|
+
self._logger.mark_overwrite(builder.target)
|
|
542
|
+
|
|
543
|
+
try:
|
|
544
|
+
ret = builder(self)
|
|
545
|
+
except Exception:
|
|
546
|
+
self._logger.mark_failed()
|
|
547
|
+
if self._logger.at_level0:
|
|
548
|
+
print(self._logger)
|
|
549
|
+
raise
|
|
550
|
+
|
|
551
|
+
self._logger.mark_new()
|
|
552
|
+
self[dtype] = ret
|
|
553
|
+
|
|
554
|
+
if self._logger.at_root and not self.record_all:
|
|
555
|
+
self._logger.clear()
|
|
556
|
+
|
|
557
|
+
return ret
|
|
558
|
+
|
|
559
|
+
def __setitem__(
|
|
560
|
+
self, data_type: Union[type[DataT], DataDescriptor[DataT]], data: DataT
|
|
561
|
+
):
|
|
562
|
+
"""Store ``data`` under a type or descriptor key.
|
|
563
|
+
|
|
564
|
+
Assigning to an indexed ID 0 key also makes the value available through an
|
|
565
|
+
unindexed class lookup. Assigning to a class creates the corresponding ID 0
|
|
566
|
+
mapping as well.
|
|
567
|
+
"""
|
|
568
|
+
data_type = _descriptor_instance(data_type)
|
|
569
|
+
self._collection[data_type] = data
|
|
570
|
+
if isinstance(data_type, IndexedDataTypeDescriptor) and data_type.id == 0:
|
|
571
|
+
self._collection[data_type.dtype] = data
|
|
572
|
+
if isinstance(data_type, type):
|
|
573
|
+
# A class binding also supplies the corresponding ID 0 value.
|
|
574
|
+
self._collection[IndexedDataTypeDescriptor.of(data_type)] = data
|
|
575
|
+
|
|
576
|
+
def find_builder(self, dtype: Union[type[DataT], DataDescriptor[DataT]]):
|
|
577
|
+
"""Find the registered builder for a type or descriptor.
|
|
578
|
+
|
|
579
|
+
A builder registered by class can be reused by
|
|
580
|
+
``IndexedDataTypeDescriptor`` instances at every ID. A builder registered
|
|
581
|
+
for a specific ``DataDescriptor`` applies only to that descriptor.
|
|
582
|
+
|
|
583
|
+
Parameters
|
|
584
|
+
----------
|
|
585
|
+
dtype
|
|
586
|
+
Type or descriptor whose builder should be found.
|
|
587
|
+
|
|
588
|
+
Returns
|
|
589
|
+
-------
|
|
590
|
+
Callable or None
|
|
591
|
+
The matching builder, with indexed context bound when necessary, or
|
|
592
|
+
``None`` if no builder is registered.
|
|
593
|
+
"""
|
|
594
|
+
# Class reads target ID 0; class registrations remain fallbacks for all IDs.
|
|
595
|
+
if isinstance(dtype, IndexedDataMeta):
|
|
596
|
+
dtype = IndexedDataTypeDescriptor.of(dtype, id=DataDescriptor.DefaultID)
|
|
597
|
+
|
|
598
|
+
builder = self._lazy_collection.get(dtype, None)
|
|
599
|
+
if builder is None:
|
|
600
|
+
if isinstance(dtype, IndexedDataTypeDescriptor):
|
|
601
|
+
# For an indexed type, also search for its type-wide builder.
|
|
602
|
+
builder = self._lazy_collection.get(dtype.dtype, None)
|
|
603
|
+
if builder is not None:
|
|
604
|
+
builder = _bind_builder_context(initiator=dtype, builder=builder)
|
|
605
|
+
|
|
606
|
+
return builder
|
|
607
|
+
|
|
608
|
+
def __str__(self):
|
|
609
|
+
return type(self).__name__
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
class IndexedDataIoC(DataIoC):
|
|
613
|
+
"""Container view that rebinds weak dependencies to the active build ID."""
|
|
614
|
+
|
|
615
|
+
def __init__(self, base_container: DataIoC, initiator=None):
|
|
616
|
+
"""Initialize an indexed view over ``base_container``.
|
|
617
|
+
|
|
618
|
+
Parameters
|
|
619
|
+
----------
|
|
620
|
+
base_container
|
|
621
|
+
Container that stores and resolves the underlying values.
|
|
622
|
+
initiator
|
|
623
|
+
Descriptor whose signed ID supplies the current build context.
|
|
624
|
+
"""
|
|
625
|
+
super().__init__()
|
|
626
|
+
self._base_container = base_container
|
|
627
|
+
self._initiator = initiator
|
|
628
|
+
|
|
629
|
+
@property
|
|
630
|
+
def id(self):
|
|
631
|
+
"""The signed ID used when implicitly rebinding dependencies."""
|
|
632
|
+
if self._initiator is None:
|
|
633
|
+
return DataDescriptor.DefaultWeakID
|
|
634
|
+
else:
|
|
635
|
+
return self._initiator.signed_id
|
|
636
|
+
|
|
637
|
+
def __getattr__(self, item):
|
|
638
|
+
return getattr(self._base_container, item)
|
|
639
|
+
|
|
640
|
+
@overload
|
|
641
|
+
def __getitem__(self, dtype: DataDescriptor[DataT]) -> DataT: ...
|
|
642
|
+
|
|
643
|
+
@overload
|
|
644
|
+
def __getitem__(self, dtype: type[DataDescriptor[DataT]]) -> DataT: ...
|
|
645
|
+
|
|
646
|
+
@overload
|
|
647
|
+
def __getitem__(self, dtype: type[DataT]) -> DataT: ...
|
|
648
|
+
|
|
649
|
+
def __getitem__(self, dtype: Any) -> Any:
|
|
650
|
+
"""Resolve ``dtype`` after applying the current indexed context."""
|
|
651
|
+
item = dtype
|
|
652
|
+
if isinstance(item, type):
|
|
653
|
+
item = IndexedDataTypeDescriptor.of(item, id=self.id)
|
|
654
|
+
elif isinstance(item, DataDescriptor):
|
|
655
|
+
item = item.index_implicit(self.id)
|
|
656
|
+
|
|
657
|
+
ret = self._base_container[item]
|
|
658
|
+
|
|
659
|
+
return ret
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
class _DataIoCDependency:
|
|
663
|
+
def __init__(self, parent: Optional[_DataIoCDependency], key, new=False):
|
|
664
|
+
self._children: list[tuple[Any, _DataIoCDependency]] = []
|
|
665
|
+
self._parent = parent
|
|
666
|
+
self._key = key
|
|
667
|
+
self._new = new
|
|
668
|
+
self._overwrite = None
|
|
669
|
+
self._failed = False
|
|
670
|
+
|
|
671
|
+
def __iter__(self):
|
|
672
|
+
return iter(self._children)
|
|
673
|
+
|
|
674
|
+
def __len__(self):
|
|
675
|
+
return len(self._children)
|
|
676
|
+
|
|
677
|
+
def clear(self):
|
|
678
|
+
self._children.clear()
|
|
679
|
+
|
|
680
|
+
def mark_new(self):
|
|
681
|
+
self._new = True
|
|
682
|
+
|
|
683
|
+
def mark_overwrite(self, key):
|
|
684
|
+
self._overwrite = key
|
|
685
|
+
|
|
686
|
+
def mark_failed(self):
|
|
687
|
+
self._failed = True
|
|
688
|
+
|
|
689
|
+
def add(self, key):
|
|
690
|
+
ret = self.get(key, None)
|
|
691
|
+
if ret is None:
|
|
692
|
+
ret = _DataIoCDependency(parent=self, key=key)
|
|
693
|
+
self[key] = ret
|
|
694
|
+
|
|
695
|
+
return ret
|
|
696
|
+
|
|
697
|
+
@property
|
|
698
|
+
def parent(self):
|
|
699
|
+
return self._parent
|
|
700
|
+
|
|
701
|
+
@property
|
|
702
|
+
def last_child(self):
|
|
703
|
+
if len(self) == 0:
|
|
704
|
+
return None
|
|
705
|
+
|
|
706
|
+
return self._children[-1][1]
|
|
707
|
+
|
|
708
|
+
def __contains__(self, item):
|
|
709
|
+
for k, _v in self:
|
|
710
|
+
if item == k:
|
|
711
|
+
return True
|
|
712
|
+
else:
|
|
713
|
+
return False
|
|
714
|
+
|
|
715
|
+
def __setitem__(self, key, value):
|
|
716
|
+
self._children.append((key, value))
|
|
717
|
+
|
|
718
|
+
def __getitem__(self, item):
|
|
719
|
+
for k, v in self:
|
|
720
|
+
if item == k:
|
|
721
|
+
return v
|
|
722
|
+
else:
|
|
723
|
+
raise KeyError(item)
|
|
724
|
+
|
|
725
|
+
def get(self, item, default=None):
|
|
726
|
+
try:
|
|
727
|
+
return self[item]
|
|
728
|
+
except KeyError:
|
|
729
|
+
return default
|
|
730
|
+
|
|
731
|
+
def to_str(
|
|
732
|
+
self, prefix=None, indent="", table_prefix="", align_first_non_blank=True
|
|
733
|
+
):
|
|
734
|
+
if prefix is None:
|
|
735
|
+
prefix = " * "
|
|
736
|
+
|
|
737
|
+
this_prefix = prefix
|
|
738
|
+
properties = []
|
|
739
|
+
if self._failed:
|
|
740
|
+
this_prefix = this_prefix.replace("*", "X")
|
|
741
|
+
if this_prefix == prefix:
|
|
742
|
+
properties.append(" X <--- Failed here ")
|
|
743
|
+
if self._new:
|
|
744
|
+
properties.append("Created")
|
|
745
|
+
|
|
746
|
+
if len(properties) == 0:
|
|
747
|
+
props = ""
|
|
748
|
+
else:
|
|
749
|
+
props = " /" + ", ".join(properties) + "/"
|
|
750
|
+
|
|
751
|
+
key = str(self._key)
|
|
752
|
+
if self._overwrite is not None:
|
|
753
|
+
key = str(self._overwrite) + f" ( <- {key})"
|
|
754
|
+
|
|
755
|
+
ret = table_prefix + this_prefix + key + props + "\n"
|
|
756
|
+
|
|
757
|
+
if align_first_non_blank:
|
|
758
|
+
for c in prefix:
|
|
759
|
+
if c == " ":
|
|
760
|
+
indent += " "
|
|
761
|
+
else:
|
|
762
|
+
break
|
|
763
|
+
|
|
764
|
+
remaining = len(self)
|
|
765
|
+
for _k, v in self:
|
|
766
|
+
if remaining > 1:
|
|
767
|
+
ret += v.to_str(
|
|
768
|
+
prefix=prefix, table_prefix=indent + "├─", indent=indent + "│ "
|
|
769
|
+
)
|
|
770
|
+
else:
|
|
771
|
+
ret += v.to_str(
|
|
772
|
+
prefix=prefix, table_prefix=indent + "└─", indent=indent + " "
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
remaining -= 1
|
|
776
|
+
|
|
777
|
+
return ret
|
|
778
|
+
|
|
779
|
+
def __str__(self):
|
|
780
|
+
return self.to_str()
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
class _DataIoCAccessLogger:
|
|
784
|
+
def __init__(self, key: Any = "root"):
|
|
785
|
+
self.root = _DataIoCDependency(None, key)
|
|
786
|
+
self.current = self.root
|
|
787
|
+
|
|
788
|
+
@property
|
|
789
|
+
def at_level0(self):
|
|
790
|
+
"""Whether the current node is the root of a child access tree."""
|
|
791
|
+
return self.current.parent is self.root
|
|
792
|
+
|
|
793
|
+
@property
|
|
794
|
+
def at_root(self):
|
|
795
|
+
"""Whether the logger is positioned at the root node."""
|
|
796
|
+
return self.current is self.root
|
|
797
|
+
|
|
798
|
+
@contextlib.contextmanager
|
|
799
|
+
def add(self, key):
|
|
800
|
+
child = self.current.add(key)
|
|
801
|
+
|
|
802
|
+
try:
|
|
803
|
+
self.enter(child)
|
|
804
|
+
yield
|
|
805
|
+
finally:
|
|
806
|
+
self.exit()
|
|
807
|
+
|
|
808
|
+
def clear(self):
|
|
809
|
+
self.root.clear()
|
|
810
|
+
|
|
811
|
+
def mark_new(self):
|
|
812
|
+
self.current.mark_new()
|
|
813
|
+
|
|
814
|
+
def mark_overwrite(self, key):
|
|
815
|
+
self.current.mark_overwrite(key)
|
|
816
|
+
|
|
817
|
+
def mark_failed(self):
|
|
818
|
+
self.current.mark_failed()
|
|
819
|
+
|
|
820
|
+
def __enter__(self):
|
|
821
|
+
self.enter()
|
|
822
|
+
|
|
823
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
824
|
+
self.exit()
|
|
825
|
+
|
|
826
|
+
def enter(self, node=None):
|
|
827
|
+
if node is None:
|
|
828
|
+
node = self.current.last_child
|
|
829
|
+
if node is None:
|
|
830
|
+
raise RuntimeError("No dependency to enter.")
|
|
831
|
+
self.current = node
|
|
832
|
+
|
|
833
|
+
def exit(self):
|
|
834
|
+
parent = self.current.parent
|
|
835
|
+
if parent is None:
|
|
836
|
+
raise RuntimeError("Cannot exit the dependency tree root.")
|
|
837
|
+
self.current = parent
|
|
838
|
+
|
|
839
|
+
def to_str(self, prefix=None):
|
|
840
|
+
return self.root.to_str(prefix=prefix)
|
|
841
|
+
|
|
842
|
+
def __str__(self):
|
|
843
|
+
return self.to_str()
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def _descriptor_instance(dtype: Any) -> Any:
|
|
847
|
+
if isinstance(dtype, type) and issubclass(dtype, DataDescriptor):
|
|
848
|
+
return dtype()
|
|
849
|
+
return dtype
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def _extract_builder_with_context(
|
|
853
|
+
dtype: Union[DataDescriptor, SupportsBuild, Callable], initiator=None
|
|
854
|
+
):
|
|
855
|
+
dtype = _descriptor_instance(dtype)
|
|
856
|
+
if initiator is None:
|
|
857
|
+
if isinstance(dtype, DataDescriptor):
|
|
858
|
+
initiator = dtype
|
|
859
|
+
|
|
860
|
+
builder = _extract_builder(dtype)
|
|
861
|
+
|
|
862
|
+
if builder is None:
|
|
863
|
+
return builder
|
|
864
|
+
else:
|
|
865
|
+
return _bind_builder_context(builder, initiator=initiator)
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def _extract_builder(dtype: Union[DataDescriptor, SupportsBuild, Callable]):
|
|
869
|
+
if isinstance(dtype, SupportsBuild):
|
|
870
|
+
builder = dtype.__build__
|
|
871
|
+
elif callable(dtype):
|
|
872
|
+
# Either a type constructor or a direct builder function.
|
|
873
|
+
# TODO: Validate that the callable can be used by DataIoC.
|
|
874
|
+
builder = dtype
|
|
875
|
+
else:
|
|
876
|
+
builder = None
|
|
877
|
+
|
|
878
|
+
return builder
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def _bind_builder_context(builder, initiator):
|
|
882
|
+
if initiator is None:
|
|
883
|
+
return builder
|
|
884
|
+
elif isinstance(builder, BuilderWithContext):
|
|
885
|
+
return builder.with_initiator(initiator)
|
|
886
|
+
else:
|
|
887
|
+
return BuilderWithContext(initiator, builder)
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
class BuilderWithContext:
|
|
891
|
+
def __init__(self, initiator, builder):
|
|
892
|
+
self.initiator = initiator
|
|
893
|
+
self.builder = builder
|
|
894
|
+
|
|
895
|
+
def __call__(self, container: DataIoC):
|
|
896
|
+
return self.builder(IndexedDataIoC(container, initiator=self.initiator))
|
|
897
|
+
|
|
898
|
+
def with_initiator(self, initiator):
|
|
899
|
+
ret = copy.copy(self)
|
|
900
|
+
ret.initiator = initiator
|
|
901
|
+
|
|
902
|
+
return ret
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
class Provider(BuilderWithContext):
|
|
906
|
+
def __init__(self, initiator, builder, target):
|
|
907
|
+
super().__init__(initiator, builder)
|
|
908
|
+
self._target = target
|
|
909
|
+
|
|
910
|
+
@property
|
|
911
|
+
def target(self):
|
|
912
|
+
if isinstance(self._target, DataDescriptor):
|
|
913
|
+
return self._target
|
|
914
|
+
else:
|
|
915
|
+
return getattr(self._target, "__name__", type(self._target).__name__)
|
dataioc/_data_ndarray.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Union
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from numpy.typing import ArrayLike
|
|
7
|
+
|
|
8
|
+
from ._data import DataIoC, IndexedData, IndexedDataIoC
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_homogeneous(inp: np.ndarray, out: np.ndarray):
|
|
12
|
+
"""Whether a ufunc result can safely retain the input array subclass."""
|
|
13
|
+
if inp.shape != out.shape:
|
|
14
|
+
# Array shape changes do not preserve the data type's structure.
|
|
15
|
+
return False
|
|
16
|
+
try:
|
|
17
|
+
promoted_dtype = np.promote_types(inp.dtype, out.dtype)
|
|
18
|
+
except TypeError:
|
|
19
|
+
# Dtypes such as StringDType and bool have no common promoted dtype.
|
|
20
|
+
return False
|
|
21
|
+
if out.dtype != promoted_dtype:
|
|
22
|
+
# Results may retain the subclass only through dtype promotion.
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DataNDArray(np.ndarray, IndexedData):
|
|
29
|
+
"""NumPy array base class that participates in indexed data resolution.
|
|
30
|
+
|
|
31
|
+
A single input array is viewed as the subclass without an unnecessary copy.
|
|
32
|
+
Multiple input arrays must have the same sample count and are stacked as
|
|
33
|
+
columns. Shape-preserving ufunc results retain the subclass when their dtype is
|
|
34
|
+
the promoted input/output dtype. Slices and reshaped arrays are returned as
|
|
35
|
+
plain ``numpy.ndarray`` instances.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __new__(cls, *arrays: ArrayLike, force_column_stack=False, **kwargs):
|
|
39
|
+
"""Construct an indexed array from one or more array-like objects.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
*arrays
|
|
44
|
+
One or more array-like objects. Multiple inputs are stacked as columns.
|
|
45
|
+
force_column_stack
|
|
46
|
+
Stack a single input as one column instead of preserving its shape.
|
|
47
|
+
**kwargs
|
|
48
|
+
Additional arguments accepted by subclass constructors.
|
|
49
|
+
|
|
50
|
+
Raises
|
|
51
|
+
------
|
|
52
|
+
ValueError
|
|
53
|
+
If no arrays are provided or their sample counts differ.
|
|
54
|
+
TypeError
|
|
55
|
+
If an input is scalar.
|
|
56
|
+
"""
|
|
57
|
+
if not arrays:
|
|
58
|
+
raise ValueError("At least one array is required.")
|
|
59
|
+
converted = [np.asarray(array) for array in arrays]
|
|
60
|
+
if any(array.ndim == 0 for array in converted):
|
|
61
|
+
raise TypeError("Arrays must have at least one dimension.")
|
|
62
|
+
if len({len(array) for array in converted}) != 1:
|
|
63
|
+
raise ValueError("Arrays have inconsistent numbers of samples.")
|
|
64
|
+
if force_column_stack or len(arrays) > 1:
|
|
65
|
+
return np.column_stack(converted).view(cls)
|
|
66
|
+
else:
|
|
67
|
+
return converted[0].view(cls)
|
|
68
|
+
|
|
69
|
+
def __array_finalize__(self, obj, **__):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
def __array_ufunc__(self, ufunc, method, *inputs, out=None, **kwargs):
|
|
73
|
+
typ = type(self)
|
|
74
|
+
|
|
75
|
+
inputs = tuple(
|
|
76
|
+
np.asarray(inp) if isinstance(inp, typ) else inp for inp in inputs
|
|
77
|
+
)
|
|
78
|
+
if out is not None:
|
|
79
|
+
kwargs["out"] = tuple(
|
|
80
|
+
np.asarray(o) if isinstance(o, typ) else o for o in out
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
ret = getattr(ufunc, method)(*inputs, **kwargs)
|
|
84
|
+
|
|
85
|
+
if ret is NotImplemented:
|
|
86
|
+
return NotImplemented
|
|
87
|
+
|
|
88
|
+
results = ret if isinstance(ret, tuple) else (ret,)
|
|
89
|
+
outputs = out if out is not None else (None,) * len(results)
|
|
90
|
+
wrapped = []
|
|
91
|
+
for result, output in zip(results, outputs):
|
|
92
|
+
# Explicit outputs must retain their identity, including in-place updates.
|
|
93
|
+
if output is not None:
|
|
94
|
+
wrapped.append(output)
|
|
95
|
+
elif isinstance(result, np.ndarray) and is_homogeneous(self, result):
|
|
96
|
+
wrapped.append(result.view(typ))
|
|
97
|
+
else:
|
|
98
|
+
wrapped.append(result)
|
|
99
|
+
return tuple(wrapped) if isinstance(ret, tuple) else wrapped[0]
|
|
100
|
+
|
|
101
|
+
def reshape(self, *shape, **kwargs) -> Any:
|
|
102
|
+
"""Return a reshaped plain ``numpy.ndarray``."""
|
|
103
|
+
return self.view(np.ndarray).reshape(*shape, **kwargs)
|
|
104
|
+
|
|
105
|
+
def __getitem__(self, key):
|
|
106
|
+
"""Return slices as plain arrays while preserving NumPy scalar behavior."""
|
|
107
|
+
result = super().__getitem__(key)
|
|
108
|
+
return result.view(np.ndarray) if isinstance(result, np.ndarray) else result
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def __build__(cls, container: Union[DataIoC, IndexedDataIoC]):
|
|
112
|
+
"""Require instances of the array subclass to be provided explicitly."""
|
|
113
|
+
id_str = f"[{container.id}]" if isinstance(container, IndexedDataIoC) else ""
|
|
114
|
+
raise NotImplementedError(f"{cls.__name__}{id_str} must be provided.")
|
dataioc/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dataioc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Declarative data dependency graphs for Python, resolved on demand with provider overrides.
|
|
5
|
+
Project-URL: Documentation, https://dyuu7.github.io/dataioc/
|
|
6
|
+
Project-URL: Issues, https://github.com/dyuu7/dataioc/issues
|
|
7
|
+
Project-URL: Repository, https://github.com/dyuu7/dataioc
|
|
8
|
+
Author: yanang007, dyuu7
|
|
9
|
+
Maintainer: dyuu7
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: IoC,data dependencies,dependency graph,dependency injection,lazy evaluation,providers
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: typing-extensions>=4.10.0; python_version < '3.11'
|
|
26
|
+
Provides-Extra: numpy
|
|
27
|
+
Requires-Dist: numpy<3,>=1.26; extra == 'numpy'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# dataioc
|
|
31
|
+
|
|
32
|
+
**Declarative data dependency graphs for Python.**
|
|
33
|
+
|
|
34
|
+
Each `DataDescriptor` names a quantity and defines how it is derived from its direct dependencies. `DataIoC` composes these local rules into a graph, then resolves and caches only the subgraph required by the result you request. Bind a provider to any quantity to replace that part of the graph without changing downstream calculations.
|
|
35
|
+
|
|
36
|
+
[English](https://github.com/dyuu7/dataioc/blob/main/README.md) | [简体中文](https://github.com/dyuu7/dataioc/blob/main/README.zh-CN.md) | [Documentation](https://dyuu7.github.io/dataioc/) | [PyPI](https://pypi.org/project/dataioc/)
|
|
37
|
+
|
|
38
|
+
[](https://github.com/dyuu7/dataioc/actions/workflows/ci.yml)
|
|
39
|
+
|
|
40
|
+
## Example
|
|
41
|
+
|
|
42
|
+
The raw readings `10`, `20`, and `60` represent measurements of `1`, `2`, and `6`.
|
|
43
|
+
|
|
44
|
+
<img src="https://raw.githubusercontent.com/dyuu7/dataioc/main/docs/assets/data-flow.png" width="720" alt="By default, RawReadings are converted into Measurements. When a provider binds recorded data to Measurements, that default derivation is replaced while Statistics and Report remain unchanged." />
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from dataioc import DataDescriptor, DataIoC
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class RawReadings(DataDescriptor):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Measurements(DataDescriptor):
|
|
55
|
+
def __build__(self, data):
|
|
56
|
+
return tuple(value / 10 for value in data[RawReadings])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Statistics(DataDescriptor):
|
|
60
|
+
def __build__(self, data):
|
|
61
|
+
values = data[Measurements]
|
|
62
|
+
return sum(values) / len(values), max(values)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Report(DataDescriptor):
|
|
66
|
+
def __build__(self, data):
|
|
67
|
+
mean, peak = data[Statistics]
|
|
68
|
+
return f"mean={mean:g}, peak={peak:g}"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
data = DataIoC().add(RawReadings, (10, 20, 60))
|
|
72
|
+
assert data[Report] == "mean=3, peak=6"
|
|
73
|
+
assert data[Measurements] is data[Measurements]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`data[Report]` is the only request the caller has to make. The container follows the dependencies and caches each value it builds.
|
|
77
|
+
|
|
78
|
+
To use recorded measurements instead, bind a provider for `Measurements`:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
recorded = DataIoC().add_provider(Measurements, lambda _: (1.0, 2.0, 6.0))
|
|
82
|
+
assert recorded[Report] == "mean=3, peak=6"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`RawReadings` is no longer needed; `Statistics` and `Report` stay as they are.
|
|
86
|
+
|
|
87
|
+
## When it helps
|
|
88
|
+
|
|
89
|
+
If both the inputs and the calculation path are fixed, ordinary function calls are simpler. Use `dataioc` when the relationships stay stable but a value may come from live measurements, recorded data, a simulation, or an estimate.
|
|
90
|
+
|
|
91
|
+
`dataioc` grew out of [deinterf](https://github.com/dyuu7/deinterf). In its [direction-cosine example](https://github.com/dyuu7/deinterf/blob/main/examples/replace_direction_cosine_source_tmi.py), the same compensation terms work whether direction cosines are derived from magnetic-vector measurements or supplied by an INS estimate. [dvmss](https://github.com/dyuu7/dvmss) applies the pattern to simulation: supply the inputs, request `Tmi`, and let the container resolve the intermediate quantities.
|
|
92
|
+
|
|
93
|
+
## Scope
|
|
94
|
+
|
|
95
|
+
`dataioc` resolves data dependencies synchronously in the current process; it is not a workflow scheduler. Use a fresh container for each dataset or provider configuration. See [Core concepts](https://dyuu7.github.io/dataioc/concepts/) for caching, diagnostics, and other runtime limits.
|
|
96
|
+
|
|
97
|
+
## Install
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
python -m pip install dataioc
|
|
101
|
+
python -m pip install "dataioc[numpy]"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Documentation
|
|
105
|
+
|
|
106
|
+
- [Quickstart](https://dyuu7.github.io/dataioc/quickstart/): build a result from local dependency rules.
|
|
107
|
+
- [Core concepts](https://dyuu7.github.io/dataioc/concepts/): understand descriptors, builders, caching, and failure behavior.
|
|
108
|
+
- [Providers](https://dyuu7.github.io/dataioc/providers/): bind a quantity to another source or derivation.
|
|
109
|
+
- [Indexed data](https://dyuu7.github.io/dataioc/indexed-data/): reuse one model across related data groups.
|
|
110
|
+
- [NumPy](https://dyuu7.github.io/dataioc/numpy/): use array subclasses.
|
|
111
|
+
- [API](https://dyuu7.github.io/dataioc/api/): look up interfaces.
|
|
112
|
+
|
|
113
|
+
## Contributors
|
|
114
|
+
|
|
115
|
+
[yanang007](https://github.com/yanang007) wrote the original container. [dyuu7](https://github.com/dyuu7) shaped the design, extracted it into `dataioc`, and maintains the project.
|
|
116
|
+
|
|
117
|
+
[](https://github.com/dyuu7/dataioc/graphs/contributors)
|
|
118
|
+
|
|
119
|
+
Licensed under the [MIT License](https://github.com/dyuu7/dataioc/blob/main/LICENSE).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
dataioc/__init__.py,sha256=yV5P2U6HVkPpDoCJDrllOz9h830j_dHlmVcbsPL8KDs,838
|
|
2
|
+
dataioc/_data.py,sha256=KjXjVDENNw48tl1GaaB3fzCd3ha0_C1chVJLMQn9xms,28048
|
|
3
|
+
dataioc/_data_ndarray.py,sha256=uptogKrVmYlPZ1zaH57J0JFj-7aQRsWXVOMJ9No63ao,4425
|
|
4
|
+
dataioc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
dataioc-0.1.0.dist-info/METADATA,sha256=HUBQy3hOpTayd3fYjSAbi1cM3K_4wG4Jrq6JWoTdB5g,5681
|
|
6
|
+
dataioc-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
dataioc-0.1.0.dist-info/licenses/LICENSE,sha256=fqt0g3WFWGRYHlDbA2oA-QIqwQoTsLnhVvQBGE87Wg8,1062
|
|
8
|
+
dataioc-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 dyuu7
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|