Skip to content

How to Add Support for a New Type

📖 This tutorial walks through teaching coola about one custom class by registering an equality tester, a summarizer, and a hasher. Each one is registered in the default registry of its package, so the top-level functions (objects_are_equal, summary, hash_object) pick it up automatically.

Prerequisites: You'll need to know a bit of Python.

The custom class

>>> class Point:
...     def __init__(self, x: float, y: float) -> None:
...         self.x = x
...         self.y = y

1. Equality

Subclass BaseEqualityTester, implement equal (used to compare testers) and objects_are_equal, then register it.

>>> from coola.equality import objects_are_equal
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.tester import BaseEqualityTester, register_equality_testers
>>> class PointEqualityTester(BaseEqualityTester[Point]):
...     def equal(self, other: object) -> bool:
...         return type(other) is type(self)
...
...     def objects_are_equal(self, actual: Point, expected: Point, config: EqualityConfig) -> bool:
...         return config.registry.objects_are_equal(
...             (actual.x, actual.y), (expected.x, expected.y), config
...         )
>>> register_equality_testers({Point: PointEqualityTester()})
>>> objects_are_equal(Point(1, 2), Point(1, 2))
True
>>> objects_are_equal(Point(1, 2), Point(1, 3))
False

2. Summary

Subclass BaseSummarizer; the registry argument lets you summarize nested values.

>>> from coola.summary import BaseSummarizer, SummarizerRegistry, register_summarizers, summarize
>>> class PointSummarizer(BaseSummarizer[Point]):
...     def equal(self, other: object) -> bool:
...         return type(other) is type(self)
...
...     def summarize(
...         self, data: Point, registry: SummarizerRegistry, depth: int = 0, max_depth: int = 1
...     ) -> str:
...         return f"Point(x={data.x}, y={data.y})"
>>> register_summarizers({Point: PointSummarizer()})
>>> print(summarize(Point(1, 2)))
Point(x=1, y=2)

3. Hashing

Subclass BaseHasher. A hasher must be consistent with equality: objects that compare equal must produce the same hash.

>>> from coola.hashing import BaseHasher, HasherRegistry, hash_object, register_hashers
>>> class PointHasher(BaseHasher[Point]):
...     def equal(self, other: object) -> bool:
...         return type(other) is type(self)
...
...     def hash(
...         self,
...         data: Point,
...         registry: HasherRegistry,
...         length: int = 64,
...         ignore_unhashable: bool = False,
...     ) -> str:
...         return registry.hash((data.x, data.y), length=length)
>>> register_hashers({Point: PointHasher()})
>>> hash_object(Point(1, 2)) == hash_object(Point(1, 2))
True

Notes

  • Registries resolve by type using the MRO, so a tester registered for a base class also handles its subclasses unless a more specific one is registered.
  • Registering an already-registered type raises an error unless exist_ok=True.
  • Custom types nested in lists or dicts work out of the box once registered.

Registering types from a third-party package

A package can add support for its own types without users calling register_* by hand. Expose a function that takes the registry and registers types on it, then declare it as an entry point in one of these groups:

Group Registry
coola.equality.testers equality testers
coola.summary.summarizers summarizers
coola.hashing.hashers hashers
coola.recursive.transformers recursive transformers
coola.iterator.dfs DFS iterators
coola.iterator.bfs BFS child finders
coola.random.managers random managers
# pyproject.toml of the plugin package
[project.entry-points."coola.summary.summarizers"]
mypkg = "mypkg.coola_plugin:register"
# mypkg/coola_plugin.py
def register(registry):
    registry.register_many({MyType: MySummarizer()})

Plugins run once, when the default registry is first built, after the built-in entries. A plugin that raises is skipped with a RuntimeWarning, and the failure is logged with its traceback. Set the COOLA_STRICT_PLUGINS=1 environment variable (e.g. in CI) to re-raise the exception instead, so a broken plugin is not missed.