edc.dataclass#
- etils.edc.dataclass(cls: None = None, *, kw_only: bool = False, replace: bool = True, repr: bool = True, auto_cast: bool = True, contextvars: bool = True, allow_unfrozen: bool = False) Callable[[etils.edc.dataclass_utils._ClsT], etils.edc.dataclass_utils._ClsT] [source]#
- etils.edc.dataclass(cls: etils.edc.dataclass_utils._ClsT, *, kw_only: bool = False, replace: bool = True, repr: bool = True, auto_cast: bool = True, contextvars: bool = True, allow_unfrozen: bool = False) etils.edc.dataclass_utils._ClsT
Augment a dataclass with additional features.
auto_cast: Auto-convert init assignements to the annotated class.
@edc.dataclass class A: path: edc.AutoCast[epath.Path] some_enum: edc.AutoCast[MyEnum] x: edc.AutoCast[str] a = A( path='/some/path', some_enum='A', x=123 ) # Fields annotated with `AutoCast` are automatically casted to their type assert a.path == epath.Path('/some/path') assert a.some_enum is MyEnum.A assert a.x == '123'
allow_unfrozen: allow nested dataclass to be updated. This add two methods:
.unfrozen(): Create a lazy deep-copy of the current dataclass. Updates to nested attributes will be propagated to the top-level dataclass.
.frozen(): Returns the frozen dataclass, after it was mutated.
Example:
old_x = X(y=Y(z=123)) x = old_x.unfrozen() x.y.z = 456 x = x.frozen() assert x == X(y=Y(z=123)) # Only new x is mutated assert old_x == X(y=Y(z=456)) # Old x is not mutated
Note:
Only the last .frozen() call resolve the dataclass by calling .replace recursivelly.
Dataclass returned by .unfrozen() and nested attributes are not the original dataclass but proxy objects which track the mutations. As such, those object are not compatible with isinstance(), jax.tree_map,…
Only the top-level dataclass need to be allow_unfrozen=True
Avoid using unfrozen if 2 attributes of the dataclass point to the same nested dataclass. Updates on one attribute might not be reflected on the other.
``python y = Y(y=123) x = X(x0=y, x1=y) # Same instance assigned twice in `x0 and x1 x = x.unfrozen() x.x0.y = 456 # Changes in x0 not reflected in x1 x = x.frozen()
assert x == X(x0=Y(y=456), x1=Y(y=123)) ```
This is because only attributes which are accessed are tracked, so etils do not know the object exist somewhere else in the attribute tree.
After .frozen() has been called, any of the temporary sub-attribute become invalid:
```python a = a.unfrozen() y = a.y a = a.frozen()
y.x # Raise error (created between the unfrozen/frozen call) a.y.x # Work ```
contextvars: Fields annotated as
edc.ContextVar
are wrapped in a contextvars.ContextVar. Afterward each thread / asyncio coroutine will have its own version of the fields (similarly to threading.local).The contextvars are lazily initialized at first usage.
Example:
@edc.dataclass @dataclasses.dataclass class Context: thread_id: edc.ContextVar[int] = dataclasses.field( default_factory=threading.get_native_id ) stack: edc.ContextVar[list[str]] = dataclasses.field(default_factory=list) # Global context object context = Context(thread_id=0) def worker(): # Inside each thread, the worker use its own context assert context.thread_id != 0 context.stack.append(1) with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: for _ in range(10): executor.submit(worker)
- Parameters:
cls – The dataclass to decorate
kw_only – If True, make the dataclass __init__ keyword-only.
replace – If True, add a .replace( alias of dataclasses.replace.
repr – If True, the class __repr__ will return a pretty-printed str (one attribute per line)
auto_cast – If True, fields annotated as x: edc.AutoCast[Cls] will be converted to x: Cls = edc.field(validator=Cls).
contextvars – It True, fields annotated as x: edc.AutoCast[T] are converted to contextvars. This allow to have a threading.local-like API for contextvars.
allow_unfrozen – If True, add .frozen, .unfrozen methods.
- Returns:
Decorated class