Extending ParityOS

This guide explains how to extend ParityOS with custom classes while maintaining compatibility with the serialization and validation.

Extending Base Classes

Serialization

ParityOS uses (de)serialization to send input to and fetch results from ParityOS cloud services. It uses a custom serialization library closely inspired by cattrs. However, in constrast to cattrs, ParityOS serialization relies on unique class tags for deserialization rather than type annotations. While this makes serialization much more flexible and customizable, it requires that classes must be registered with a serializer’s class tagger, before they can be (de)serialized.

ParityOS uses two pre-configured serializer, which can be imported from parityos.utils.io_utils:

  • DETERMINISTIC_SERIALIZER: A serializer that produces deterministic serialization output by sorting unordered containers such as sets and dictionaries

  • FAST_SERIALIZER: A serializer that produces equivalent, but not deterministic serialization output. Unordered containers are not sorted. A full serialization - deserialization cycle yields the same object, but the serialized output might differ between different runs.

Both serializers use the same name_class_tagger. For ParityOS to be able to correctly deserialize a custom class, it needs to be explicitly registered with name_class_tagger. For this purpose, the register decorator can be used: it automatically adds the decorated class and all its subclasses to the class tagger.

Most

When creating a new custom class, the register decorator can be used to tag the class.

Warning

Only attrs classes currently support the register decorator.

from typing import ClassVar

from attrs import frozen
from parityos.bits import Qubit
from parityos.utils.io_utils import DETERMINISTIC_SERIALIZER, json_dumps, json_loads, register


# Automatically registers with serializer
@frozen
class CustomBit(Qubit):
    """Custom bit class."""

    STR_PREFIX: ClassVar[str] = "cb_"


my_qubit = CustomBit(1)
# Serialize to a json string
# DETERMINISTIC_SERIALIZER recognises CustomBit as it's registered via the Qubit NameClassTagger()
serialized_bit = json_dumps(my_qubit, DETERMINISTIC_SERIALIZER)
# Deserialize back to original object
deserialized_bit = json_loads(serialized_bit, DETERMINISTIC_SERIALIZER)
assert deserialized_bit == my_qubit


# Manual registration with serializer
@register
@frozen
class NewClass:
    """Custom base class."""
    ...

my_object = NewClass()

# Serialize to a json string
# DETERMINISTIC_SERIALIZER recognises NewClass as it's registered manually with @register
serialized_obj = json_dumps(my_object, DETERMINISTIC_SERIALIZER)
# Deserialize back to original object
deserialized_obj = json_loads(serialized_obj, DETERMINISTIC_SERIALIZER)
# assert round trip serialization yields the same object
assert deserialized_obj == my_object

Typing

ParityOS is fully typed and uses generic types extensively. It follows basedpyright’s recommended typing rules with a few exceptions.[1]

This enables keeping full compatibility with existing interfaces when properly subclassing ParityOS classes. E.g. functions or classes that accept Qubits or are generic over their Qubit type also accept custom subclasses. While this also holds for ParityOS service inputs, any additional custom attributes or methods are disregarded by the cloud services and only those defined by the built-in ParityOS classes are used.