edc.dataclass

Contents

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