Skip to content

cattrs support #4

@HexDecimal

Description

@HexDecimal

I was asked to support cattrs and other generic serialization libraries. With the boilerplate required to do this externally it would be better to add cattrs support directly to this package instead.

This has to do the following:

  • Save and load anonymous Entity objects which are always a (world, uid) pair.
  • Save and load anonymous objects without knowing their type ahead of time. These may be components, tags, or uid's.

Code examples from @slavfox

How to handle object() as uid?

# EntityId: some opaque type with a zero-argument constructor EntityId()
# ecs.entdict: dict[EntityId, collection[Any]]

# note: only typechecks with --enable-recursive-aliases
JsonValue: TypeAlias = dict[str, "JsonValue"] | list["JsonValue"] | int | float | bool | str | None

def serialize_ecs(ecs):
    idmap: dict[EntityId, int] = {id_: i for i, id_ in enumerate(ecs.entdict)}
    return {
        idmap[id_]: [serialize_value(c, idmap=idmap) for c in components] 
        for id_, c in ecs.entdict.items()
    }

def serialize_value(value: Any, idmap: dict[EntityId, int]) -> JsonValue:
    match value:
        case int(_) | float(_) | bool(_) | str(_) | None:
            return value
        case [*xs]:
            return [serialize_value(x, idmap) for x in xs]
        case {**xs}:
            return {str(k): serialize_value(v, idmap) for (k, v) in xs}
        case EntityId():
            return idmap[value]
        case _:
            return {
                field.name: serialize_value(field.value) for field in get_ecs_fields(value)
            }

How to handle component types held by World?

The common pattern for using cattr is tagged enums, which you can think of as, roughly:

serializers = {}
deserializers = {}

# Decorator to register a class with cattr
def cattr(cls_):
    serializers[cls_] = ...
    cls_key = make_cls_key(cls_)
    deserializers[cls_key] = ...
    ...
    return cls_


def serialize(inst):
    return {
        "__type": make_cls_key(type(inst)), 
        **serializers[type(inst)](inst)
    }

def deserialize(serialized):
    key = serialized.pop("__type")
    return deserializers[key](serialized)

The cattrs docs are pretty good, but it will take me a while to internalize them.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions