Equality
coola.equality ¶
Utilities to compare nested objects for exact or tolerant equality.
The package exports the comparison functions, the result, the configuration, the engine, the testers and the registry. The testers of the optional libraries (NumPy, PyTorch, JAX, xarray, pydantic) are imported the first time they are used.
coola.equality.AttributeName ¶
Bases: str
Name of an attribute in the location of a difference.
It is a str that marks a path component as an attribute (a
dataclass field, for example) rather than a key or an index, so a
path is formatted as ['a'][2].x and not as ['a'][2]['x'].
Example
>>> from coola.equality._helpers.utils import AttributeName
>>> AttributeName("x") == "x"
True
coola.equality.BaseEqualityTester ¶
Bases: InlineDisplayMixin, ABC, Generic[T]
Define the base class to implement an equality operator.
The engine owns the invariants of a comparison (identity, strict
type, depth, cycles, location of the first difference). A tester
never recurses: objects_are_equal receives the two objects,
which have the same type, and the engine, and returns
- a verdict:
True, orctx.fail(reason, *path)(which returnsFalse) for a leaf; - or the child pairs
(actual, expected, path part)that must all be equal, produced lazily and in the order the first difference is wanted, for a container.
Two testers are equal if they have the same type and the same
attributes. A child class with parameters must override
_display_fields.
Example
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import BaseEqualityTester, get_default_registry
>>> class PointTester(BaseEqualityTester[tuple]):
... def objects_are_equal(self, actual, expected, ctx):
... return ((actual[0], expected[0], 0), (actual[1], expected[1], 1))
...
>>> registry = get_default_registry().copy()
>>> registry.register(tuple, PointTester(), exist_ok=True)
>>> Engine(EqualityConfig(registry=registry)).run((1, 2), (1, 2))
True
>>> Engine(EqualityConfig(registry=registry)).run((1, 2), (1, 3))
False
>>> PointTester() == PointTester()
True
coola.equality.BaseEqualityTester.objects_are_equal
abstractmethod
¶
objects_are_equal(
actual: T, expected: T, ctx: Engine
) -> bool | Iterator[tuple[object, object, object]]
Test two objects that have the same type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actual
|
T
|
The actual object. |
required |
expected
|
T
|
The expected object, with the same type. |
required |
ctx
|
Engine
|
The |
required |
Returns:
| Type | Description |
|---|---|
bool | Iterator[tuple[object, object, object]]
|
A verdict, or the child pairs that must all be equal. |
Example
>>> import numpy as np
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import NumpyArrayEqualityTester, get_default_registry
>>> engine = Engine(EqualityConfig())
>>> tester = NumpyArrayEqualityTester()
>>> tester.objects_are_equal(np.ones(2), np.ones(2), engine)
True
>>> tester.objects_are_equal(np.ones(2), np.zeros(2), engine)
False
coola.equality.ComparisonResult
dataclass
¶
Describe the outcome of comparing two objects.
bool(result) is True if the objects are equal, so the result
can be used as a drop-in replacement for the bool returned by
objects_are_equal.
Attributes:
| Name | Type | Description |
|---|---|---|
equal |
bool
|
|
path |
tuple[object, ...]
|
The location of the first difference, e.g. |
reason |
str | None
|
A description of the first difference found, or
|
Example
>>> from coola.equality import compare
>>> result = compare({"a": [1, 2, 3]}, {"a": [1, 2, 4]})
>>> bool(result)
False
>>> result.path
('a', 2)
>>> result.format_path()
"['a'][2]"
coola.equality.ComparisonResult.format_message ¶
format_message(root: str = 'actual') -> str
Format a human-readable description of the result.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
str
|
The name used for the root of the path. |
'actual'
|
Returns:
| Type | Description |
|---|---|
str
|
The message. |
coola.equality.ComparisonResult.format_path ¶
format_path(root: str = '') -> str
Format the path as an indexing expression.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
str
|
The name prepended to the path. |
''
|
Returns:
| Type | Description |
|---|---|
str
|
The formatted path, e.g. |
coola.equality.DataclassEqualityTester ¶
Bases: BaseEqualityTester[Any]
Implement an equality tester for dataclass instances.
The two instances have the same type, and the tester compares their
fields, in the order of declaration. The fields with
compare=False are ignored. The fields are compared with the
engine, so they support the tolerance and equal_nan, and the
location of a difference is the name of the field.
Example
>>> from dataclasses import dataclass
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> @dataclass
... class Point:
... x: float
...
>>> engine = Engine(EqualityConfig())
>>> engine.run(Point(1.0), Point(1.0))
True
>>> engine.run(Point(1.0), Point(2.0))
False
coola.equality.DefaultEqualityTester ¶
Bases: BaseEqualityTester[object]
Implement a default equality tester.
This tester serves as the fallback for types without specialized
equality testers. It uses Python's built-in == operator to test
equality between two objects of the same type. The engine checks
the identity and the type before the tester is called.
This tester is registered for the object type in the default
registry, making it the catch-all for unregistered types via
Python's MRO.
Raises:
| Type | Description |
|---|---|
TypeError
|
if |
Example
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import DefaultEqualityTester, get_default_registry
>>> engine = Engine(EqualityConfig())
>>> tester = DefaultEqualityTester()
>>> tester.objects_are_equal("meow", "meow", engine)
True
>>> tester.objects_are_equal("meow", "meov", engine)
False
coola.equality.DefaultEqualityTester.objects_are_equal ¶
objects_are_equal(
actual: object, expected: object, ctx: Engine
) -> bool
Test two objects with ==.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actual
|
object
|
The actual object. |
required |
expected
|
object
|
The expected object, with the same type. |
required |
ctx
|
Engine
|
The |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
coola.equality.DifferenceCloseEqualityTester ¶
Bases: BaseEqualityTester[Any]
Implement an equality tester for decimal.Decimal,
fractions.Fraction and datetime.timedelta.
These types have no allclose, so the tester compares their
difference with the tolerance (see difference_close). A
Decimal NaN is equal to a NaN only if equal_nan is
True, and atol of a timedelta is a number of seconds.
This tester is registered for Decimal, Fraction and
timedelta in the default registry.
Example
>>> from coola.equality import Tolerance
>>> from decimal import Decimal
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import DifferenceCloseEqualityTester, get_default_registry
>>> registry = get_default_registry()
>>> tester = DifferenceCloseEqualityTester()
>>> tester.objects_are_equal(
... Decimal("1.0"), Decimal("1.00001"), Engine(EqualityConfig())
... )
False
>>> tester.objects_are_equal(
... Decimal("1.0"), Decimal("1.00001"), Engine(EqualityConfig(tolerance=Tolerance(atol=1e-3)))
... )
True
coola.equality.Element
dataclass
¶
Location of an element of a set in the path of a difference.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
object
|
The element. |
required |
Example
>>> from coola.equality import Element
>>> Element(2)
Element(value=2)
coola.equality.Engine ¶
Run the comparison of two objects.
One engine runs one comparison. For each pair of objects, the engine
checks the identity, the strict type, finds the tester (the lookup
is cached for the call) and calls it. A tester that uses the engine
returns either a bool (the verdict) or an iterator of
(actual, expected, path part) child pairs that must all be
equal. The iterators are kept on an explicit stack, depth first, so
the first difference in iteration order is the one found and
nothing after it is visited. The stack use is constant: there is
no RecursionError whatever the nesting.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
EqualityConfig
|
The settings of the comparison (the registry that finds
the tester of a type, the tolerance, |
required |
explaining
|
bool
|
If |
False
|
Attributes:
| Name | Type | Description |
|---|---|---|
atol |
float
|
The absolute tolerance. |
rtol |
float
|
The relative tolerance. |
equal_nan |
bool
|
If |
explaining |
If |
|
failure |
tuple[tuple[object, ...], str | None] | None
|
|
Example
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> Engine(EqualityConfig()).run([1, {"a": 2}], [1, {"a": 2}])
True
>>> Engine(EqualityConfig()).run([1, {"a": 2}], [1, {"a": 3}])
False
coola.equality.Engine.fail ¶
fail(
reason: str | Callable[[], str], *path: object
) -> bool
Record the reason of the difference and return False.
A tester writes return ctx.fail(...).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reason
|
str | Callable[[], str]
|
The reason, or a callable that builds it. A
callable is called only if |
required |
*path
|
object
|
The path parts to add after the ones the engine knows, for example the index in an array. |
()
|
Returns:
| Type | Description |
|---|---|
bool
|
|
Example
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> engine = Engine(EqualityConfig())
>>> engine.fail("values differ", 3)
False
coola.equality.Engine.find ¶
find(data_type: type) -> BaseEqualityTester[Any]
Find the tester of a type.
The registry lookup is paid once per type and per comparison.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data_type
|
type
|
The type of the objects. |
required |
Returns:
| Type | Description |
|---|---|
BaseEqualityTester[Any]
|
The tester of the type. |
coola.equality.Engine.run ¶
Run the comparison.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actual
|
object
|
The actual object. |
required |
expected
|
object
|
The expected object. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Raises:
| Type | Description |
|---|---|
MaxDepthExceededError
|
If the nesting of the objects
exceeds |
coola.equality.Engine.warn_once ¶
warn_once(key: object, message: str) -> bool
Emit a RuntimeWarning once per comparison.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
object
|
The key that identifies the warning. |
required |
message
|
str
|
The message of the warning. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
coola.equality.EqualEqualityTester ¶
Bases: BaseEqualityTester[object]
Implement an equality tester for objects with equal method.
This tester is designed for objects that implement an
equal(other) method for equality comparison. The two objects
have the same type (the engine checks it) and the tester calls the
equal method of the first one. An object without a callable
equal is different.
Example
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import EqualEqualityTester, get_default_registry
>>> class MyFloat:
... def __init__(self, value: float) -> None:
... self._value = float(value)
... def equal(self, other: object) -> bool:
... if type(other) is not type(self):
... return False
... return self._value == other._value
...
>>> engine = Engine(EqualityConfig())
>>> tester = EqualEqualityTester()
>>> tester.objects_are_equal(MyFloat(42), MyFloat(42), engine)
True
>>> tester.objects_are_equal(MyFloat(42), MyFloat(1), engine)
False
coola.equality.EqualNanEqualityTester ¶
Bases: BaseEqualityTester[object]
Implement an equality tester for objects with equal method that supports NaN equality.
This tester is designed for objects that implement an
equal(other, equal_nan) method. The tester calls the equal
method of the first object with equal_nan of the comparison. An
object without a callable equal is different.
Example
>>> import math
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import EqualNanEqualityTester, get_default_registry
>>> class MyFloat:
... def __init__(self, value: float) -> None:
... self._value = float(value)
... def equal(self, other: object, equal_nan: bool = False) -> bool:
... if type(other) is not type(self):
... return False
... if equal_nan and math.isnan(self._value) and math.isnan(other._value):
... return True
... return self._value == other._value
...
>>> registry = get_default_registry()
>>> tester = EqualNanEqualityTester()
>>> nan1, nan2 = MyFloat(float("nan")), MyFloat(float("nan"))
>>> tester.objects_are_equal(nan1, nan2, Engine(EqualityConfig()))
False
>>> tester.objects_are_equal(nan1, nan2, Engine(EqualityConfig(equal_nan=True)))
True
coola.equality.EqualityConfig
dataclass
¶
Define the config to control the comparison rules.
The config is immutable: it can be shared between comparisons and
threads. The state of one comparison lives in the Engine.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
registry
|
EqualityTesterRegistry
|
The registry with the equality tester to use. |
create_default_registry()
|
tolerance
|
Tolerance
|
The tolerance of the numbers. Defaults to no tolerance (exact comparison). |
Tolerance()
|
equal_nan
|
bool
|
If |
False
|
max_depth
|
int | None
|
The nesting limit: the maximum nesting level of the
compared objects. A deeper structure raises
|
None
|
Raises:
| Type | Description |
|---|---|
TypeError
|
if |
ValueError
|
if |
Example
>>> from coola.equality import Tolerance
>>> from coola.equality.config import EqualityConfig
>>> config = EqualityConfig(tolerance=Tolerance(atol=1e-6))
>>> config.atol
1e-06
>>> config.rtol
0.0
coola.equality.EqualityConfig.__post_init__ ¶
__post_init__() -> None
Validate the configuration parameters after initialization.
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
coola.equality.EqualityTesterRegistry ¶
Bases: TypeRegistry[BaseEqualityTester[Any]]
Registry that manages and dispatches equality testers based on data type.
This registry maintains a mapping from Python types to equality tester instances and uses the Method Resolution Order (MRO) for type lookup. When checking equality, it automatically selects the most specific registered equality tester for the data's type, falling back to parent types or a default tester if needed.
The registry includes an internal cache for type lookups to optimize performance in applications that repeatedly check equality of similar data structures.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
initial_state
|
dict[type, T] | None
|
Optional initial mapping of types to equality testers. If provided, the state is copied to prevent external mutations. |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
_state |
dict[K, V]
|
Internal mapping of registered types to equality testers |
Example
Basic usage:
>>> from coola.equality.tester import (
... EqualityTesterRegistry,
... SequenceEqualityTester,
... DefaultEqualityTester,
... )
>>> from coola.equality.config import EqualityConfig
>>> registry = EqualityTesterRegistry(
... {object: DefaultEqualityTester(), list: SequenceEqualityTester()}
... )
>>> registry
EqualityTesterRegistry(
(<class 'object'>): DefaultEqualityTester()
(<class 'list'>): SequenceEqualityTester()
)
>>> config = EqualityConfig()
>>> registry.objects_are_equal([1, 2, 3], [1, 2, 3], config=config)
True
coola.equality.EqualityTesterRegistry.find_equality_tester ¶
find_equality_tester(
data_type: type,
) -> BaseEqualityTester[Any]
Type-specific alias for find: find the appropriate
equality tester for a given type.
See TypeRegistry.resolve for the full behavior
description (MRO resolution, caching, and the KeyError on no
match).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data_type
|
type
|
The Python type to find an equality tester for |
required |
Returns:
| Type | Description |
|---|---|
BaseEqualityTester[Any]
|
The most specific registered equality tester for this type, a parent |
BaseEqualityTester[Any]
|
type's tester via MRO, or the default tester |
Example
>>> from collections.abc import Sequence
>>> from coola.equality.tester import (
... EqualityTesterRegistry,
... SequenceEqualityTester,
... DefaultEqualityTester,
... )
>>> registry = EqualityTesterRegistry({object: DefaultEqualityTester()})
>>> registry.register(Sequence, SequenceEqualityTester())
>>> # Sequence is not in list's MRO, so the registry falls back to DefaultEqualityTester
>>> tester = registry.find_equality_tester(list)
>>> tester
DefaultEqualityTester()
coola.equality.EqualityTesterRegistry.has_equality_tester ¶
has_equality_tester(data_type: type) -> bool
Type-specific alias for has: check if an equality
tester is explicitly registered for the given type.
See Registry.has for the full behavior
description.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data_type
|
type
|
The type to check |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if an equality tester is explicitly registered for this type, |
bool
|
False otherwise |
Example
>>> from coola.equality.tester import EqualityTesterRegistry, SequenceEqualityTester
>>> registry = EqualityTesterRegistry()
>>> registry.register(list, SequenceEqualityTester())
>>> registry.has_equality_tester(list)
True
coola.equality.EqualityTesterRegistry.objects_are_equal ¶
objects_are_equal(
actual: object, expected: object, config: EqualityConfig
) -> bool
Check if two objects are equal by recursively comparing their structure.
This is the main entry point for equality checking. It automatically: 1. Determines the actual object's type 2. Finds the appropriate equality tester 3. Delegates to that tester's objects_are_equal method 4. The tester recursively processes nested structures
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actual
|
object
|
The actual object. |
required |
expected
|
object
|
The expected object. |
required |
config
|
EqualityConfig
|
The equality configuration. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the objects are equal according to the registered testers, |
bool
|
False otherwise |
Example
Checking if two lists of integers are equal:
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.tester import get_default_registry
>>> registry = get_default_registry()
>>> config = EqualityConfig()
>>> registry.objects_are_equal([1, 2, 3], [1, 2, 3], config=config)
True
Checking if two lists of tensors are equal:
>>> import torch
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.tester import get_default_registry
>>> registry = get_default_registry()
>>> config = EqualityConfig()
>>> registry.objects_are_equal(
... [torch.ones(2, 3), torch.zeros(2)],
... [torch.ones(2, 3), torch.zeros(2)],
... config=config,
... )
True
coola.equality.JaxArrayEqualityTester ¶
Bases: BaseEqualityTester[ndarray]
Implement an equality tester for jax.numpy.ndarray.
Two arrays are equal if they have the same data type, the same
shape and equal elements. The numbers are compared with the
tolerance and equal_nan of the comparison, and a boolean array
is exact. The location of a difference is the index of the first
element that differs. Without 64-bit mode, JAX has no float64:
the tolerance is computed in the widest dtype JAX has.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> import jax.numpy as jnp
>>> from coola.equality import compare
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import JaxArrayEqualityTester, get_default_registry
>>> engine = Engine(EqualityConfig())
>>> tester = JaxArrayEqualityTester()
>>> tester.objects_are_equal(jnp.ones((2, 3)), jnp.ones((2, 3)), engine)
True
>>> tester.objects_are_equal(jnp.ones((2, 3)), jnp.zeros((2, 3)), engine)
False
>>> compare(jnp.ones((2, 3)), jnp.zeros((2, 3))).path
(0, 0)
coola.equality.MappingEqualityTester ¶
Bases: BaseEqualityTester['Mapping[Any, Any]']
Implement a mapping equality tester.
This tester handles dictionary-like objects (dict, Mapping ABC). The
two mappings must have the same keys, and the values of the same
key are compared in the order of actual. The keys are compared
with their type, like the elements of sets: {1: "a"} and
{True: "a"} are not the same mapping. With equal_nan=True,
two NaN keys are the same key.
Example
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> registry = get_default_registry()
>>> engine = Engine(EqualityConfig())
>>> engine.run({"a": 1, "b": 2}, {"a": 1, "b": 2})
True
>>> engine.run({"a": 1, "b": 2}, {"a": 1, "b": 4})
False
coola.equality.MaxDepthExceededError ¶
Bases: Exception
Raised when the nesting of the compared objects exceeds the
max_depth of the equality configuration.
coola.equality.NumpyArrayEqualityTester ¶
Bases: BaseEqualityTester[ndarray]
Implement an equality tester for numpy.ndarray.
Two arrays are equal if they have the same data type, the same
shape and equal elements. The numbers are compared with the
tolerance and equal_nan of the comparison (a boolean, a string
and an instant are exact; atol of a duration is a number of
seconds). The elements of an object array are compared by the
engine, one by one. The location of a difference is the index of
the first element that differs (the position in the flattened
array for an object array).
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> import numpy as np
>>> from coola.equality import compare
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import NumpyArrayEqualityTester, get_default_registry
>>> engine = Engine(EqualityConfig())
>>> tester = NumpyArrayEqualityTester()
>>> tester.objects_are_equal(np.ones((2, 3)), np.ones((2, 3)), engine)
True
>>> tester.objects_are_equal(np.ones((2, 3)), np.zeros((2, 3)), engine)
False
>>> compare(np.ones((2, 3)), np.zeros((2, 3))).path
(0, 0)
coola.equality.NumpyMaskedArrayEqualityTester ¶
Bases: BaseEqualityTester[MaskedArray]
Implement an equality tester for numpy.ma.MaskedArray.
Two masked arrays are equal if they have the same data type, the
same shape, the same data (the masked values are compared too), the
same mask and the same fill_value. The location of a difference
is .data, .mask or .fill_value, and then the index in
the array.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> import numpy as np
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> engine = Engine(EqualityConfig())
>>> engine.run(
... np.ma.array([1, 2, 3], mask=[0, 0, 1]), np.ma.array([1, 2, 3], mask=[0, 0, 1])
... )
True
>>> engine.run(
... np.ma.array([1, 2, 3], mask=[0, 0, 1]), np.ma.array([1, 2, 3], mask=[0, 1, 1])
... )
False
coola.equality.NumpyTimeScalarEqualityTester ¶
Bases: BaseEqualityTester[object]
Implement an equality tester for numpy.timedelta64 and
numpy.datetime64 scalars.
NaT is equal to NaT only if equal_nan is True. A
numpy.timedelta64 is a duration: the tolerances apply (atol
is a number of seconds). A numpy.datetime64 is an instant and
is compared exactly.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> from coola.equality import Tolerance
>>> import numpy as np
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import NumpyTimeScalarEqualityTester, get_default_registry
>>> tester = NumpyTimeScalarEqualityTester()
>>> engine = Engine(EqualityConfig(tolerance=Tolerance(atol=1.0)))
>>> tester.objects_are_equal(np.timedelta64(1, "s"), np.timedelta64(2000, "ms"), engine)
True
>>> tester.objects_are_equal(
... np.datetime64("2020-01-01"), np.datetime64("2020-01-02"), engine
... )
False
coola.equality.ObjectAttributesEqualityTester ¶
Bases: BaseEqualityTester[Any]
Implement an equality tester for objects without value semantics.
A plain class, an exception or a functools.partial is equal to
another object only if it is the same object, unless the class
defines __eq__. This tester compares the attributes instead:
the entries of vars, the slots, the args of an exception
and the content of a functools.partial (see
get_object_attributes). The attributes are compared through the
registry, so the tolerance and equal_nan apply to them.
It is not registered by default, because it changes the result for the classes that rely on the identity: register it for the classes that must be compared by their attributes. The two objects have the same type, the same attribute names and equal values. The location of a difference is the name of the attribute.
Example
>>> from coola.equality import objects_are_equal
>>> from coola.equality.tester import (
... DefaultEqualityTester,
... EqualityTesterRegistry,
... ObjectAttributesEqualityTester,
... )
>>> class Point:
... def __init__(self, x):
... self.x = x
...
>>> registry = EqualityTesterRegistry({object: DefaultEqualityTester()})
>>> objects_are_equal(Point(1), Point(1), registry=registry)
False
>>> registry.register(Point, ObjectAttributesEqualityTester())
>>> objects_are_equal(Point(1), Point(1), registry=registry)
True
>>> objects_are_equal(Point(1), Point(2), registry=registry)
False
coola.equality.OrderedMappingEqualityTester ¶
Bases: MappingEqualityTester
Implement an ordered mapping equality tester.
This tester behaves like MappingEqualityTester, and additionally
requires the keys to be in the same order. It is registered for
collections.OrderedDict, for which Python's == is also
order-sensitive.
Example
>>> from collections import OrderedDict
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> engine = Engine(EqualityConfig())
>>> engine.run(OrderedDict(a=1, b=2), OrderedDict(a=1, b=2))
True
>>> engine.run(OrderedDict(a=1, b=2), OrderedDict(b=2, a=1))
False
coola.equality.PydanticModelEqualityTester ¶
Bases: BaseEqualityTester[BaseModel]
Implement an equality tester for pydantic.BaseModel.
The two models must have the same type, the same field values and
the same extra values. The values are compared with the registry, so
they support the tolerance and equal_nan. The private attributes
are not compared, as the fields are what the model validates and
serializes. The other entries of the instance __dict__ (for
example the value of a functools.cached_property that was read
on one model only) are ignored too. The location of a difference is
the name of the field, or model_extra.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> from pydantic import BaseModel
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import PydanticModelEqualityTester, get_default_registry
>>> class Point(BaseModel):
... x: float
... y: float
...
>>> engine = Engine(EqualityConfig())
>>> engine.run(Point(x=1.0, y=2.0), Point(x=1.0, y=2.0))
True
>>> engine.run(Point(x=1.0, y=2.0), Point(x=1.0, y=3.0))
False
coola.equality.ScalarEqualityTester ¶
Bases: BaseEqualityTester[float]
Implement a scalar equality tester.
This tester handles numeric scalar types (int, float, complex) with
support for NaN equality and tolerance-based comparisons. Two NaNs
are equal only if equal_nan is True, a bool and an
enum member are labels and are compared exactly, and the other
numbers are close if
|a - b| <= atol + rtol * max(|a|, |b|).
This tester is registered for int, float and complex in
the default registry.
Example
>>> from coola.equality import Tolerance
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import ScalarEqualityTester, get_default_registry
>>> registry = get_default_registry()
>>> tester = ScalarEqualityTester()
>>> tester.objects_are_equal(42.0, 42.0, Engine(EqualityConfig()))
True
>>> tester.objects_are_equal(42.0, 1.0, Engine(EqualityConfig()))
False
>>> tester.objects_are_equal(42.0, 42.0001, Engine(EqualityConfig(tolerance=Tolerance(atol=1e-3))))
True
coola.equality.ScipySparseEqualityTester ¶
Bases: BaseEqualityTester[Any]
Implement an equality tester for scipy.sparse matrices and
arrays.
Two sparse objects are equal if they have the same class (a matrix
is not equal to an array, and the format is part of the class:
csr_matrix, csc_matrix...; the engine checks it), the same
shape, the same data type, the same non-zero positions and
equal values. The values are compared with the tolerance and
equal_nan of the comparison. The duplicate entries are summed
and the explicitly stored zeros are ignored, so two objects that
represent the same matrix are equal. The tolerance applies to the
stored values: a stored value is never compared with an absent one.
The location of a difference is the coordinates of the first value
that differs.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> from scipy import sparse
>>> from coola.equality import compare
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import ScipySparseEqualityTester
>>> engine = Engine(EqualityConfig())
>>> tester = ScipySparseEqualityTester()
>>> tester.objects_are_equal(sparse.eye(3, format="csr"), sparse.eye(3, format="csr"), engine)
True
>>> tester.objects_are_equal(
... sparse.eye(3, format="csr"), 2 * sparse.eye(3, format="csr"), engine
... )
False
>>> compare(sparse.eye(3, format="csr"), 2 * sparse.eye(3, format="csr")).path
(0, 0)
coola.equality.SequenceEqualityTester ¶
Bases: BaseEqualityTester['Sequence[Any]']
Implement a sequence equality tester.
This tester handles sequence types (list, tuple, deque, Sequence
ABC). The two sequences must have the same length, and the items
are compared in order. A sequence of exact scalars (int,
str, bytes, bool and None) is compared at once.
Example
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> engine = Engine(EqualityConfig())
>>> engine.run([1, 2, 3], [1, 2, 3])
True
>>> engine.run([1, 2, 3], [1, 2, 4])
False
>>> engine.run([1, 2, 3], [1, 2])
False
coola.equality.SetEqualityTester ¶
Bases: BaseEqualityTester['AbstractSet[Any]']
Implement a set equality tester.
This tester handles set and frozenset. Unlike ==, the
elements are matched by hash and type, so equal_nan applies to
them (the tolerance does not). The location of a difference is the
element of actual that has no match in expected.
Example
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> engine = Engine(EqualityConfig())
>>> engine.run({1, 2, 3}, {1, 2, 3})
True
>>> engine.run({1, 2, 3}, {1, 2, 4})
False
coola.equality.TableEqualityTester ¶
Bases: BaseEqualityTester[Any]
Implement an equality tester for the tables of pandas, polars and pyarrow.
The structure is compared first, then the columns one by one, each
by its kind (see Column). The comparison stops at the first
column that differs. A table is a leaf for the engine: the
difference is reported as a whole. The reason says what differs in
the structure (the shapes, the data types, the index, the names of
the columns, ...) or, for the values, the name of the first column
that differs, the first row that differs and the two values. The
row is found only when the difference is explained.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
adapter
|
Callable[[Any], Table]
|
The function that converts an object to a |
required |
Example
>>> import pandas as pd
>>> from coola.equality import compare
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import TableEqualityTester, get_default_registry
>>> from coola.equality.tester.pandas import pandas_frame
>>> engine = Engine(EqualityConfig())
>>> tester = TableEqualityTester(pandas_frame)
>>> tester.objects_are_equal(
... pd.DataFrame({"a": [1, 2]}), pd.DataFrame({"a": [1, 2]}), engine
... )
True
>>> tester.objects_are_equal(
... pd.DataFrame({"a": [1, 2]}), pd.DataFrame({"a": [1, 3]}), engine
... )
False
>>> print(compare(pd.DataFrame({"a": [1, 2]}), pd.DataFrame({"a": [1, 3]})).reason)
tables have different values: column 'a' differs at row 1: 2 vs 3
coola.equality.Tolerance
dataclass
¶
Define the tolerance of a comparison.
Two numbers actual and expected are close if
|actual - expected| <= atol + rtol * max(|actual|, |expected|)
(the numpy.isclose rule made symmetric). The default is no tolerance (exact comparison).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rtol
|
float
|
The relative tolerance parameter. Must be non-negative and finite. Defaults to 0.0. |
0.0
|
atol
|
float
|
The absolute tolerance parameter. Must be non-negative and finite. Defaults to 0.0. |
0.0
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Example
>>> from coola.equality import Tolerance
>>> Tolerance()
Tolerance(rtol=0.0, atol=0.0)
>>> Tolerance.numpy_default()
Tolerance(rtol=1e-05, atol=1e-08)
coola.equality.Tolerance.numpy_default
classmethod
¶
numpy_default() -> Tolerance
Create the tolerance used by objects_are_allclose.
The values (rtol=1e-5, atol=1e-8) are the defaults of
numpy.allclose.
Returns:
| Type | Description |
|---|---|
Tolerance
|
The tolerance |
Example
>>> from coola.equality import Tolerance
>>> Tolerance.numpy_default()
Tolerance(rtol=1e-05, atol=1e-08)
coola.equality.TolerantEqualEqualityTester ¶
Bases: BaseEqualityTester[object]
Implement an equality tester for objects that support both
allclose and equal methods.
This tester is designed for objects that implement both an
allclose(other, rtol, atol, equal_nan) method for
tolerance-based comparison and an equal(other, equal_nan)
method for exact comparison. The tester calls allclose when a
tolerance is not zero, otherwise equal. An object that does not
have both methods is different.
Example
>>> from coola.equality import Tolerance
>>> import math
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import TolerantEqualEqualityTester, get_default_registry
>>> class MyFloat:
... def __init__(self, value: float) -> None:
... self._value = float(value)
... def allclose(
... self,
... other: object,
... rtol: float = 1e-5,
... atol: float = 1e-8,
... equal_nan: bool = False,
... ) -> bool:
... if type(other) is not type(self):
... return False
... if equal_nan and math.isnan(self._value) and math.isnan(other._value):
... return True
... return math.isclose(self._value, other._value, rel_tol=rtol, abs_tol=atol)
... def equal(self, other: object, equal_nan: bool = False) -> bool:
... if type(other) is not type(self):
... return False
... if equal_nan and math.isnan(self._value) and math.isnan(other._value):
... return True
... return self._value == other._value
...
>>> registry = get_default_registry()
>>> tester = TolerantEqualEqualityTester()
>>> tester.objects_are_equal(MyFloat(42), MyFloat(42), Engine(EqualityConfig()))
True
>>> tester.objects_are_equal(MyFloat(1.0), MyFloat(1.4), Engine(EqualityConfig()))
False
>>> tester.objects_are_equal(
... MyFloat(1.0), MyFloat(1.4), Engine(EqualityConfig(tolerance=Tolerance(atol=0.5)))
... )
True
coola.equality.TorchPackedSequenceEqualityTester ¶
Bases: BaseEqualityTester[PackedSequence]
Implement an equality tester for
torch.nn.utils.rnn.PackedSequence.
This tester compares the data tensor and the metadata of two packed
sequences: batch_sizes, sorted_indices and
unsorted_indices. The location of a difference is the name of
the attribute.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> import torch
>>> from torch.nn.utils.rnn import pack_padded_sequence
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> engine = Engine(EqualityConfig())
>>> def make(value):
... return pack_padded_sequence(
... torch.tensor([[1.0, value], [3.0, 4.0]]),
... lengths=torch.tensor([2, 1]),
... batch_first=True,
... enforce_sorted=False,
... )
...
>>> engine.run(make(2.0), make(2.0))
True
>>> engine.run(make(2.0), make(5.0))
False
coola.equality.TorchTensorEqualityTester ¶
Bases: BaseEqualityTester[Tensor]
Implement an equality tester for torch.Tensor.
Two tensors are equal if they have the same data type, shape,
device, layout and equal elements. The numbers are compared with
the tolerance and equal_nan of the comparison, and a boolean
tensor is exact. The tensors on the meta device have no data:
only the metadata is compared. The location of a difference is the
index of the first element that differs.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> import torch
>>> from coola.equality import compare
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import TorchTensorEqualityTester, get_default_registry
>>> engine = Engine(EqualityConfig())
>>> tester = TorchTensorEqualityTester()
>>> tester.objects_are_equal(torch.ones(2, 3), torch.ones(2, 3), engine)
True
>>> tester.objects_are_equal(torch.ones(2, 3), torch.zeros(2, 3), engine)
False
>>> compare(torch.ones(2, 3), torch.zeros(2, 3)).path
(0, 0)
coola.equality.XarrayDataArrayEqualityTester ¶
Bases: BaseEqualityTester[DataArray]
Implement an equality tester for xarray.DataArray.
This tester compares the variable (data, dimensions and attributes), the name and the coordinate variables of two DataArrays. The location of a difference is the name of the attribute.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> import numpy as np
>>> import xarray as xr
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> engine = Engine(EqualityConfig())
>>> engine.run(xr.DataArray(np.arange(6), dims=["z"]), xr.DataArray(np.arange(6), dims=["z"]))
True
>>> engine.run(xr.DataArray(np.ones(6), dims=["z"]), xr.DataArray(np.zeros(6), dims=["z"]))
False
coola.equality.XarrayDatasetEqualityTester ¶
Bases: BaseEqualityTester[Dataset]
Implement an equality tester for xarray.Dataset.
This tester compares the data variables, the coordinates and the attributes of two Datasets. The location of a difference is the name of the attribute.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> import numpy as np
>>> import xarray as xr
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> engine = Engine(EqualityConfig())
>>> engine.run(
... xr.Dataset({"x": ("z", np.arange(6))}), xr.Dataset({"x": ("z", np.arange(6))})
... )
True
>>> engine.run(
... xr.Dataset({"x": ("z", np.ones(6))}), xr.Dataset({"x": ("z", np.zeros(6))})
... )
False
coola.equality.XarrayVariableEqualityTester ¶
Bases: BaseEqualityTester[Variable]
Implement an equality tester for xarray.Variable.
This tester compares the data, the dimension names and the attributes of two Variables. The location of a difference is the name of the attribute.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> import numpy as np
>>> import xarray as xr
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> engine = Engine(EqualityConfig())
>>> engine.run(
... xr.Variable(dims=["z"], data=np.arange(6)),
... xr.Variable(dims=["z"], data=np.arange(6)),
... )
True
>>> engine.run(
... xr.Variable(dims=["z"], data=np.zeros(6)), xr.Variable(dims=["z"], data=np.ones(6))
... )
False
coola.equality.assert_objects_allclose ¶
assert_objects_allclose(
actual: object,
expected: object,
*,
rtol: float = 1e-05,
atol: float = 1e-08,
equal_nan: bool = False,
max_depth: int | None = None,
registry: EqualityTesterRegistry | None = None,
root: str = "actual",
) -> None
Assert that two objects are equal within a tolerance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actual
|
object
|
The actual object. |
required |
expected
|
object
|
The expected object. |
required |
rtol
|
float
|
The relative tolerance parameter. Must be non-negative. |
1e-05
|
atol
|
float
|
The absolute tolerance parameter. Must be non-negative. |
1e-08
|
equal_nan
|
bool
|
If |
False
|
max_depth
|
int | None
|
The nesting limit: the maximum nesting level of the
compared objects. A deeper structure raises
|
None
|
registry
|
EqualityTesterRegistry | None
|
Registry used to resolve type-specific equality
testers. If |
None
|
root
|
str
|
The name used for the root of the path in the message. |
'actual'
|
Raises:
| Type | Description |
|---|---|
AssertionError
|
If the objects are not equal within the tolerances. The message contains the path to the first difference. |
ValueError
|
If |
MaxDepthExceededError
|
If the nesting of the objects exceeds
|
Example
>>> from coola.equality import assert_objects_allclose
>>> assert_objects_allclose({"a": [1.0, 2.0]}, {"a": [1.0, 2.0 + 1e-9]})
>>> assert_objects_allclose([1.0], [1.5], atol=0.1, root="data")
Traceback (most recent call last):
...
AssertionError: objects are not equal at data[0]
numbers are different:
actual : 1.0
expected : 1.5
coola.equality.assert_objects_equal ¶
assert_objects_equal(
actual: object,
expected: object,
*,
equal_nan: bool = False,
max_depth: int | None = None,
registry: EqualityTesterRegistry | None = None,
root: str = "actual",
) -> None
Assert that two objects are equal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actual
|
object
|
The actual object. |
required |
expected
|
object
|
The expected object. |
required |
equal_nan
|
bool
|
If |
False
|
max_depth
|
int | None
|
The nesting limit: the maximum nesting level of the
compared objects. A deeper structure raises
|
None
|
registry
|
EqualityTesterRegistry | None
|
Registry used to resolve type-specific equality
testers. If |
None
|
root
|
str
|
The name used for the root of the path in the message. |
'actual'
|
Raises:
| Type | Description |
|---|---|
AssertionError
|
If the objects are not equal. The message contains the path to the first difference. |
ValueError
|
If |
MaxDepthExceededError
|
If the nesting of the objects exceeds
|
Example
>>> from coola.equality import assert_objects_equal
>>> assert_objects_equal({"a": [1, 2]}, {"a": [1, 2]})
>>> assert_objects_equal({"a": [1, 2]}, {"a": [1, 3]}, root="data")
Traceback (most recent call last):
...
AssertionError: objects are not equal at data['a'][1]
numbers are different:
actual : 2
expected : 3
coola.equality.compare ¶
compare(
actual: object,
expected: object,
*,
rtol: float = 0.0,
atol: float = 0.0,
equal_nan: bool = False,
max_depth: int | None = None,
registry: EqualityTesterRegistry | None = None,
) -> ComparisonResult
Compare two objects and explain the first difference.
Exact equality is used by default. Set rtol or atol to
compare within a tolerance: rtol=1e-5 and atol=1e-8 are the
defaults of objects_are_allclose. Comparison stops at the first
difference, and only this difference is explained.
Note
The difference is the first failure recorded by the engine
(ctx.fail of a tester).
Nothing is logged, and no global state is modified, so this
function can be called concurrently from several threads.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actual
|
object
|
The actual object. |
required |
expected
|
object
|
The expected object. |
required |
rtol
|
float
|
The relative tolerance parameter. Must be non-negative. |
0.0
|
atol
|
float
|
The absolute tolerance parameter. Must be non-negative. |
0.0
|
equal_nan
|
bool
|
If |
False
|
max_depth
|
int | None
|
The nesting limit: the maximum nesting level of the
compared objects. A deeper structure raises
|
None
|
registry
|
EqualityTesterRegistry | None
|
Registry used to resolve type-specific equality
testers. If |
None
|
Returns:
| Type | Description |
|---|---|
ComparisonResult
|
The comparison result. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
MaxDepthExceededError
|
If the nesting of the objects exceeds
|
Example
>>> from coola.equality import compare
>>> compare([1, 2], [1, 2]).equal
True
>>> result = compare({"a": 1}, {"a": 2})
>>> result.path
('a',)
>>> result.equal
False
>>> compare(1.0, 1.0 + 1e-9).equal
False
>>> compare(1.0, 1.0 + 1e-9, rtol=1e-5, atol=1e-8).equal
True
coola.equality.get_default_registry ¶
get_default_registry() -> EqualityTesterRegistry
Get or create the default global registry with common Python types.
Returns a singleton registry instance that is pre-configured with equality testers for Python's built-in types including sequences (list, tuple), mappings (dict), and scalar types (int, float).
This function uses a singleton pattern to ensure the same registry instance is returned on subsequent calls, which is efficient and maintains consistency across an application.
Returns:
| Type | Description |
|---|---|
EqualityTesterRegistry
|
A EqualityTesterRegistry instance with equality testers registered for: - Scalar types (int, float) - Sequences (list, tuple, deque, array.array, Sequence ABC) - Mappings (dict, OrderedDict, MappingProxyType, Mapping ABC) - Third-party library types (numpy, torch, pandas, polars, jax, xarray, pyarrow) |
Notes
The singleton pattern means modifications to the returned registry affect all future calls to this function. If you need an isolated registry, create a new EqualityTesterRegistry instance directly.
Example
>>> from coola.equality.tester import get_default_registry
>>> from coola.equality.config import EqualityConfig
>>> registry = get_default_registry()
>>> # Registry is ready to use with common Python types
>>> config = EqualityConfig()
>>> registry.objects_are_equal([1, 2, 3], [1, 2, 3], config)
True
>>> registry.objects_are_equal([1, 2, 3], [1, 1], config)
False
coola.equality.objects_are_allclose ¶
objects_are_allclose(
actual: object,
expected: object,
*,
rtol: float = 1e-05,
atol: float = 1e-08,
equal_nan: bool = False,
max_depth: int | None = None,
registry: EqualityTesterRegistry | None = None,
) -> bool
Indicate if two objects are equal within a tolerance.
Note
The types are compared first, so 1 and 1.0 are different
(as are [1] and [1.0]), even with a tolerance. A boolean
is a label, not a quantity: True and False are never
close, even with atol=1 (also for numpy.bool_ and for
boolean arrays).
An object is always equal to itself, even if it contains NaN
and equal_nan=False: [n] equals [n] for
n = float("nan"), but not [float("nan")]. Two distinct
objects that contain NaN are equal only with
equal_nan=True.
This also holds for np.nan and math.nan, which are
single objects: [math.nan] equals [math.nan], while
[float("nan")] does not equal another [float("nan")].
Use equal_nan=True if the data can contain a NaN.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actual
|
object
|
The actual object. |
required |
expected
|
object
|
The expected object. |
required |
rtol
|
float
|
The relative tolerance parameter. Must be non-negative. |
1e-05
|
atol
|
float
|
The absolute tolerance parameter. Must be non-negative. |
1e-08
|
equal_nan
|
bool
|
If |
False
|
max_depth
|
int | None
|
The nesting limit: the maximum nesting level of the
compared objects. A deeper structure raises
|
None
|
registry
|
EqualityTesterRegistry | None
|
Registry used to resolve type-specific equality testers.
If |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
|
bool
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
MaxDepthExceededError
|
If the nesting of the objects exceeds
|
Example
>>> from coola.equality import objects_are_allclose
>>> objects_are_allclose([1.0, 2.0], [1.0 + 1e-8, 2.0])
True
>>> objects_are_allclose([1.0, 2.0], [1.0, 2.1], atol=1e-3, rtol=0.0)
False
>>> objects_are_allclose([float("nan")], [float("nan")], equal_nan=False)
False
>>> objects_are_allclose([float("nan")], [float("nan")], equal_nan=True)
True
coola.equality.objects_are_equal ¶
objects_are_equal(
actual: object,
expected: object,
*,
equal_nan: bool = False,
max_depth: int | None = None,
registry: EqualityTesterRegistry | None = None,
) -> bool
Indicate if two objects are equal or not.
Note
The types are compared first, so 1 and 1.0 are different
(as are [1] and [1.0]). There is no tolerance: use
objects_are_allclose to compare numbers within a tolerance.
An object is always equal to itself, even if it contains NaN
and equal_nan=False: [n] equals [n] for
n = float("nan"), but not [float("nan")]. Two distinct
objects that contain NaN are equal only with
equal_nan=True.
This also holds for np.nan and math.nan, which are
single objects: [math.nan] equals [math.nan], while
[float("nan")] does not equal another [float("nan")].
Use equal_nan=True if the data can contain a NaN.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actual
|
object
|
The actual object. |
required |
expected
|
object
|
The expected object. |
required |
equal_nan
|
bool
|
If |
False
|
max_depth
|
int | None
|
The nesting limit: the maximum nesting level of the
compared objects. A deeper structure raises
|
None
|
registry
|
EqualityTesterRegistry | None
|
Registry used to resolve type-specific equality testers.
If |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
MaxDepthExceededError
|
If the nesting of the objects exceeds
|
Example
>>> from coola.equality import objects_are_equal
>>> objects_are_equal([1, {"a": 2}], [1, {"a": 2}])
True
>>> objects_are_equal([1, {"a": 2}], [1, {"a": 3}])
False
coola.equality.register_equality_testers ¶
register_equality_testers(
mapping: Mapping[type, BaseEqualityTester[Any]],
*,
exist_ok: bool = False,
) -> None
Register custom equality testers to the default global registry.
This allows users to add support for custom types without modifying global state directly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mapping
|
Mapping[type, BaseEqualityTester[Any]]
|
Dictionary mapping types to equality tester instances |
required |
exist_ok
|
bool
|
If False, raises error if any type already registered |
False
|
Example
>>> from coola.equality.tester import register_equality_testers, BaseEqualityTester
>>> class MyType:
... def __init__(self, value):
... self.value = value
...
>>> class MyEqualityTester(BaseEqualityTester[MyType]):
... def objects_are_equal(self, actual: MyType, expected: MyType, ctx) -> bool:
... return actual.value == expected.value or ctx.fail("values differ")
...
>>> register_equality_testers({MyType: MyEqualityTester()})
coola.equality.config ¶
Define the equality configuration.
coola.equality.config.EqualityConfig
dataclass
¶
Define the config to control the comparison rules.
The config is immutable: it can be shared between comparisons and
threads. The state of one comparison lives in the Engine.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
registry
|
EqualityTesterRegistry
|
The registry with the equality tester to use. |
create_default_registry()
|
tolerance
|
Tolerance
|
The tolerance of the numbers. Defaults to no tolerance (exact comparison). |
Tolerance()
|
equal_nan
|
bool
|
If |
False
|
max_depth
|
int | None
|
The nesting limit: the maximum nesting level of the
compared objects. A deeper structure raises
|
None
|
Raises:
| Type | Description |
|---|---|
TypeError
|
if |
ValueError
|
if |
Example
>>> from coola.equality import Tolerance
>>> from coola.equality.config import EqualityConfig
>>> config = EqualityConfig(tolerance=Tolerance(atol=1e-6))
>>> config.atol
1e-06
>>> config.rtol
0.0
coola.equality.config.EqualityConfig.__post_init__ ¶
__post_init__() -> None
Validate the configuration parameters after initialization.
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
coola.equality.config.create_default_registry ¶
create_default_registry() -> EqualityTesterRegistry
Create the default equality tester registry.
This helper is used as a dataclasses.field default factory to avoid
importing the registry interface at module import time.
Returns:
| Type | Description |
|---|---|
EqualityTesterRegistry
|
The default equality tester registry. |
coola.equality.engine ¶
Implement the engine that runs a comparison with an explicit stack.
The engine owns the invariants of a comparison: identity, strict type, depth, cycles and the location of the first difference. A tester that uses the engine never recurses: it returns a verdict, or the child pairs that must all be equal.
coola.equality.engine.Element
dataclass
¶
Location of an element of a set in the path of a difference.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
object
|
The element. |
required |
Example
>>> from coola.equality import Element
>>> Element(2)
Element(value=2)
coola.equality.engine.Engine ¶
Run the comparison of two objects.
One engine runs one comparison. For each pair of objects, the engine
checks the identity, the strict type, finds the tester (the lookup
is cached for the call) and calls it. A tester that uses the engine
returns either a bool (the verdict) or an iterator of
(actual, expected, path part) child pairs that must all be
equal. The iterators are kept on an explicit stack, depth first, so
the first difference in iteration order is the one found and
nothing after it is visited. The stack use is constant: there is
no RecursionError whatever the nesting.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
EqualityConfig
|
The settings of the comparison (the registry that finds
the tester of a type, the tolerance, |
required |
explaining
|
bool
|
If |
False
|
Attributes:
| Name | Type | Description |
|---|---|---|
atol |
float
|
The absolute tolerance. |
rtol |
float
|
The relative tolerance. |
equal_nan |
bool
|
If |
explaining |
If |
|
failure |
tuple[tuple[object, ...], str | None] | None
|
|
Example
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> Engine(EqualityConfig()).run([1, {"a": 2}], [1, {"a": 2}])
True
>>> Engine(EqualityConfig()).run([1, {"a": 2}], [1, {"a": 3}])
False
coola.equality.engine.Engine.fail ¶
fail(
reason: str | Callable[[], str], *path: object
) -> bool
Record the reason of the difference and return False.
A tester writes return ctx.fail(...).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reason
|
str | Callable[[], str]
|
The reason, or a callable that builds it. A
callable is called only if |
required |
*path
|
object
|
The path parts to add after the ones the engine knows, for example the index in an array. |
()
|
Returns:
| Type | Description |
|---|---|
bool
|
|
Example
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> engine = Engine(EqualityConfig())
>>> engine.fail("values differ", 3)
False
coola.equality.engine.Engine.find ¶
find(data_type: type) -> BaseEqualityTester[Any]
Find the tester of a type.
The registry lookup is paid once per type and per comparison.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data_type
|
type
|
The type of the objects. |
required |
Returns:
| Type | Description |
|---|---|
BaseEqualityTester[Any]
|
The tester of the type. |
coola.equality.engine.Engine.run ¶
Run the comparison.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actual
|
object
|
The actual object. |
required |
expected
|
object
|
The expected object. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Raises:
| Type | Description |
|---|---|
MaxDepthExceededError
|
If the nesting of the objects
exceeds |
coola.equality.engine.Engine.warn_once ¶
warn_once(key: object, message: str) -> bool
Emit a RuntimeWarning once per comparison.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
object
|
The key that identifies the warning. |
required |
message
|
str
|
The message of the warning. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
coola.equality.tester ¶
Type-aware equality testers used by coola.equality.
This package exposes a registry-based dispatch system with built-in testers for common Python containers and optional scientific types. Extend the default behavior by registering custom testers in the default registry.
Example
>>> from coola.equality.tester import get_default_registry
>>> from coola.equality.config import EqualityConfig
>>> registry = get_default_registry()
>>> config = EqualityConfig()
>>> # Compare lists with nested structures
>>> registry.objects_are_equal([1, {"a": 2}], [1, {"a": 2}], config)
True
coola.equality.tester.BaseEqualityTester ¶
Bases: InlineDisplayMixin, ABC, Generic[T]
Define the base class to implement an equality operator.
The engine owns the invariants of a comparison (identity, strict
type, depth, cycles, location of the first difference). A tester
never recurses: objects_are_equal receives the two objects,
which have the same type, and the engine, and returns
- a verdict:
True, orctx.fail(reason, *path)(which returnsFalse) for a leaf; - or the child pairs
(actual, expected, path part)that must all be equal, produced lazily and in the order the first difference is wanted, for a container.
Two testers are equal if they have the same type and the same
attributes. A child class with parameters must override
_display_fields.
Example
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import BaseEqualityTester, get_default_registry
>>> class PointTester(BaseEqualityTester[tuple]):
... def objects_are_equal(self, actual, expected, ctx):
... return ((actual[0], expected[0], 0), (actual[1], expected[1], 1))
...
>>> registry = get_default_registry().copy()
>>> registry.register(tuple, PointTester(), exist_ok=True)
>>> Engine(EqualityConfig(registry=registry)).run((1, 2), (1, 2))
True
>>> Engine(EqualityConfig(registry=registry)).run((1, 2), (1, 3))
False
>>> PointTester() == PointTester()
True
coola.equality.tester.BaseEqualityTester.objects_are_equal
abstractmethod
¶
objects_are_equal(
actual: T, expected: T, ctx: Engine
) -> bool | Iterator[tuple[object, object, object]]
Test two objects that have the same type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actual
|
T
|
The actual object. |
required |
expected
|
T
|
The expected object, with the same type. |
required |
ctx
|
Engine
|
The |
required |
Returns:
| Type | Description |
|---|---|
bool | Iterator[tuple[object, object, object]]
|
A verdict, or the child pairs that must all be equal. |
Example
>>> import numpy as np
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import NumpyArrayEqualityTester, get_default_registry
>>> engine = Engine(EqualityConfig())
>>> tester = NumpyArrayEqualityTester()
>>> tester.objects_are_equal(np.ones(2), np.ones(2), engine)
True
>>> tester.objects_are_equal(np.ones(2), np.zeros(2), engine)
False
coola.equality.tester.DataclassEqualityTester ¶
Bases: BaseEqualityTester[Any]
Implement an equality tester for dataclass instances.
The two instances have the same type, and the tester compares their
fields, in the order of declaration. The fields with
compare=False are ignored. The fields are compared with the
engine, so they support the tolerance and equal_nan, and the
location of a difference is the name of the field.
Example
>>> from dataclasses import dataclass
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> @dataclass
... class Point:
... x: float
...
>>> engine = Engine(EqualityConfig())
>>> engine.run(Point(1.0), Point(1.0))
True
>>> engine.run(Point(1.0), Point(2.0))
False
coola.equality.tester.DefaultEqualityTester ¶
Bases: BaseEqualityTester[object]
Implement a default equality tester.
This tester serves as the fallback for types without specialized
equality testers. It uses Python's built-in == operator to test
equality between two objects of the same type. The engine checks
the identity and the type before the tester is called.
This tester is registered for the object type in the default
registry, making it the catch-all for unregistered types via
Python's MRO.
Raises:
| Type | Description |
|---|---|
TypeError
|
if |
Example
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import DefaultEqualityTester, get_default_registry
>>> engine = Engine(EqualityConfig())
>>> tester = DefaultEqualityTester()
>>> tester.objects_are_equal("meow", "meow", engine)
True
>>> tester.objects_are_equal("meow", "meov", engine)
False
coola.equality.tester.DefaultEqualityTester.objects_are_equal ¶
objects_are_equal(
actual: object, expected: object, ctx: Engine
) -> bool
Test two objects with ==.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actual
|
object
|
The actual object. |
required |
expected
|
object
|
The expected object, with the same type. |
required |
ctx
|
Engine
|
The |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
coola.equality.tester.DifferenceCloseEqualityTester ¶
Bases: BaseEqualityTester[Any]
Implement an equality tester for decimal.Decimal,
fractions.Fraction and datetime.timedelta.
These types have no allclose, so the tester compares their
difference with the tolerance (see difference_close). A
Decimal NaN is equal to a NaN only if equal_nan is
True, and atol of a timedelta is a number of seconds.
This tester is registered for Decimal, Fraction and
timedelta in the default registry.
Example
>>> from coola.equality import Tolerance
>>> from decimal import Decimal
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import DifferenceCloseEqualityTester, get_default_registry
>>> registry = get_default_registry()
>>> tester = DifferenceCloseEqualityTester()
>>> tester.objects_are_equal(
... Decimal("1.0"), Decimal("1.00001"), Engine(EqualityConfig())
... )
False
>>> tester.objects_are_equal(
... Decimal("1.0"), Decimal("1.00001"), Engine(EqualityConfig(tolerance=Tolerance(atol=1e-3)))
... )
True
coola.equality.tester.EqualEqualityTester ¶
Bases: BaseEqualityTester[object]
Implement an equality tester for objects with equal method.
This tester is designed for objects that implement an
equal(other) method for equality comparison. The two objects
have the same type (the engine checks it) and the tester calls the
equal method of the first one. An object without a callable
equal is different.
Example
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import EqualEqualityTester, get_default_registry
>>> class MyFloat:
... def __init__(self, value: float) -> None:
... self._value = float(value)
... def equal(self, other: object) -> bool:
... if type(other) is not type(self):
... return False
... return self._value == other._value
...
>>> engine = Engine(EqualityConfig())
>>> tester = EqualEqualityTester()
>>> tester.objects_are_equal(MyFloat(42), MyFloat(42), engine)
True
>>> tester.objects_are_equal(MyFloat(42), MyFloat(1), engine)
False
coola.equality.tester.EqualNanEqualityTester ¶
Bases: BaseEqualityTester[object]
Implement an equality tester for objects with equal method that supports NaN equality.
This tester is designed for objects that implement an
equal(other, equal_nan) method. The tester calls the equal
method of the first object with equal_nan of the comparison. An
object without a callable equal is different.
Example
>>> import math
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import EqualNanEqualityTester, get_default_registry
>>> class MyFloat:
... def __init__(self, value: float) -> None:
... self._value = float(value)
... def equal(self, other: object, equal_nan: bool = False) -> bool:
... if type(other) is not type(self):
... return False
... if equal_nan and math.isnan(self._value) and math.isnan(other._value):
... return True
... return self._value == other._value
...
>>> registry = get_default_registry()
>>> tester = EqualNanEqualityTester()
>>> nan1, nan2 = MyFloat(float("nan")), MyFloat(float("nan"))
>>> tester.objects_are_equal(nan1, nan2, Engine(EqualityConfig()))
False
>>> tester.objects_are_equal(nan1, nan2, Engine(EqualityConfig(equal_nan=True)))
True
coola.equality.tester.EqualityTesterRegistry ¶
Bases: TypeRegistry[BaseEqualityTester[Any]]
Registry that manages and dispatches equality testers based on data type.
This registry maintains a mapping from Python types to equality tester instances and uses the Method Resolution Order (MRO) for type lookup. When checking equality, it automatically selects the most specific registered equality tester for the data's type, falling back to parent types or a default tester if needed.
The registry includes an internal cache for type lookups to optimize performance in applications that repeatedly check equality of similar data structures.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
initial_state
|
dict[type, T] | None
|
Optional initial mapping of types to equality testers. If provided, the state is copied to prevent external mutations. |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
_state |
dict[K, V]
|
Internal mapping of registered types to equality testers |
Example
Basic usage:
>>> from coola.equality.tester import (
... EqualityTesterRegistry,
... SequenceEqualityTester,
... DefaultEqualityTester,
... )
>>> from coola.equality.config import EqualityConfig
>>> registry = EqualityTesterRegistry(
... {object: DefaultEqualityTester(), list: SequenceEqualityTester()}
... )
>>> registry
EqualityTesterRegistry(
(<class 'object'>): DefaultEqualityTester()
(<class 'list'>): SequenceEqualityTester()
)
>>> config = EqualityConfig()
>>> registry.objects_are_equal([1, 2, 3], [1, 2, 3], config=config)
True
coola.equality.tester.EqualityTesterRegistry.find_equality_tester ¶
find_equality_tester(
data_type: type,
) -> BaseEqualityTester[Any]
Type-specific alias for find: find the appropriate
equality tester for a given type.
See TypeRegistry.resolve for the full behavior
description (MRO resolution, caching, and the KeyError on no
match).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data_type
|
type
|
The Python type to find an equality tester for |
required |
Returns:
| Type | Description |
|---|---|
BaseEqualityTester[Any]
|
The most specific registered equality tester for this type, a parent |
BaseEqualityTester[Any]
|
type's tester via MRO, or the default tester |
Example
>>> from collections.abc import Sequence
>>> from coola.equality.tester import (
... EqualityTesterRegistry,
... SequenceEqualityTester,
... DefaultEqualityTester,
... )
>>> registry = EqualityTesterRegistry({object: DefaultEqualityTester()})
>>> registry.register(Sequence, SequenceEqualityTester())
>>> # Sequence is not in list's MRO, so the registry falls back to DefaultEqualityTester
>>> tester = registry.find_equality_tester(list)
>>> tester
DefaultEqualityTester()
coola.equality.tester.EqualityTesterRegistry.has_equality_tester ¶
has_equality_tester(data_type: type) -> bool
Type-specific alias for has: check if an equality
tester is explicitly registered for the given type.
See Registry.has for the full behavior
description.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data_type
|
type
|
The type to check |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if an equality tester is explicitly registered for this type, |
bool
|
False otherwise |
Example
>>> from coola.equality.tester import EqualityTesterRegistry, SequenceEqualityTester
>>> registry = EqualityTesterRegistry()
>>> registry.register(list, SequenceEqualityTester())
>>> registry.has_equality_tester(list)
True
coola.equality.tester.EqualityTesterRegistry.objects_are_equal ¶
objects_are_equal(
actual: object, expected: object, config: EqualityConfig
) -> bool
Check if two objects are equal by recursively comparing their structure.
This is the main entry point for equality checking. It automatically: 1. Determines the actual object's type 2. Finds the appropriate equality tester 3. Delegates to that tester's objects_are_equal method 4. The tester recursively processes nested structures
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actual
|
object
|
The actual object. |
required |
expected
|
object
|
The expected object. |
required |
config
|
EqualityConfig
|
The equality configuration. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the objects are equal according to the registered testers, |
bool
|
False otherwise |
Example
Checking if two lists of integers are equal:
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.tester import get_default_registry
>>> registry = get_default_registry()
>>> config = EqualityConfig()
>>> registry.objects_are_equal([1, 2, 3], [1, 2, 3], config=config)
True
Checking if two lists of tensors are equal:
>>> import torch
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.tester import get_default_registry
>>> registry = get_default_registry()
>>> config = EqualityConfig()
>>> registry.objects_are_equal(
... [torch.ones(2, 3), torch.zeros(2)],
... [torch.ones(2, 3), torch.zeros(2)],
... config=config,
... )
True
coola.equality.tester.JaxArrayEqualityTester ¶
Bases: BaseEqualityTester[ndarray]
Implement an equality tester for jax.numpy.ndarray.
Two arrays are equal if they have the same data type, the same
shape and equal elements. The numbers are compared with the
tolerance and equal_nan of the comparison, and a boolean array
is exact. The location of a difference is the index of the first
element that differs. Without 64-bit mode, JAX has no float64:
the tolerance is computed in the widest dtype JAX has.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> import jax.numpy as jnp
>>> from coola.equality import compare
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import JaxArrayEqualityTester, get_default_registry
>>> engine = Engine(EqualityConfig())
>>> tester = JaxArrayEqualityTester()
>>> tester.objects_are_equal(jnp.ones((2, 3)), jnp.ones((2, 3)), engine)
True
>>> tester.objects_are_equal(jnp.ones((2, 3)), jnp.zeros((2, 3)), engine)
False
>>> compare(jnp.ones((2, 3)), jnp.zeros((2, 3))).path
(0, 0)
coola.equality.tester.MappingEqualityTester ¶
Bases: BaseEqualityTester['Mapping[Any, Any]']
Implement a mapping equality tester.
This tester handles dictionary-like objects (dict, Mapping ABC). The
two mappings must have the same keys, and the values of the same
key are compared in the order of actual. The keys are compared
with their type, like the elements of sets: {1: "a"} and
{True: "a"} are not the same mapping. With equal_nan=True,
two NaN keys are the same key.
Example
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> registry = get_default_registry()
>>> engine = Engine(EqualityConfig())
>>> engine.run({"a": 1, "b": 2}, {"a": 1, "b": 2})
True
>>> engine.run({"a": 1, "b": 2}, {"a": 1, "b": 4})
False
coola.equality.tester.NumpyArrayEqualityTester ¶
Bases: BaseEqualityTester[ndarray]
Implement an equality tester for numpy.ndarray.
Two arrays are equal if they have the same data type, the same
shape and equal elements. The numbers are compared with the
tolerance and equal_nan of the comparison (a boolean, a string
and an instant are exact; atol of a duration is a number of
seconds). The elements of an object array are compared by the
engine, one by one. The location of a difference is the index of
the first element that differs (the position in the flattened
array for an object array).
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> import numpy as np
>>> from coola.equality import compare
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import NumpyArrayEqualityTester, get_default_registry
>>> engine = Engine(EqualityConfig())
>>> tester = NumpyArrayEqualityTester()
>>> tester.objects_are_equal(np.ones((2, 3)), np.ones((2, 3)), engine)
True
>>> tester.objects_are_equal(np.ones((2, 3)), np.zeros((2, 3)), engine)
False
>>> compare(np.ones((2, 3)), np.zeros((2, 3))).path
(0, 0)
coola.equality.tester.NumpyMaskedArrayEqualityTester ¶
Bases: BaseEqualityTester[MaskedArray]
Implement an equality tester for numpy.ma.MaskedArray.
Two masked arrays are equal if they have the same data type, the
same shape, the same data (the masked values are compared too), the
same mask and the same fill_value. The location of a difference
is .data, .mask or .fill_value, and then the index in
the array.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> import numpy as np
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> engine = Engine(EqualityConfig())
>>> engine.run(
... np.ma.array([1, 2, 3], mask=[0, 0, 1]), np.ma.array([1, 2, 3], mask=[0, 0, 1])
... )
True
>>> engine.run(
... np.ma.array([1, 2, 3], mask=[0, 0, 1]), np.ma.array([1, 2, 3], mask=[0, 1, 1])
... )
False
coola.equality.tester.NumpyTimeScalarEqualityTester ¶
Bases: BaseEqualityTester[object]
Implement an equality tester for numpy.timedelta64 and
numpy.datetime64 scalars.
NaT is equal to NaT only if equal_nan is True. A
numpy.timedelta64 is a duration: the tolerances apply (atol
is a number of seconds). A numpy.datetime64 is an instant and
is compared exactly.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> from coola.equality import Tolerance
>>> import numpy as np
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import NumpyTimeScalarEqualityTester, get_default_registry
>>> tester = NumpyTimeScalarEqualityTester()
>>> engine = Engine(EqualityConfig(tolerance=Tolerance(atol=1.0)))
>>> tester.objects_are_equal(np.timedelta64(1, "s"), np.timedelta64(2000, "ms"), engine)
True
>>> tester.objects_are_equal(
... np.datetime64("2020-01-01"), np.datetime64("2020-01-02"), engine
... )
False
coola.equality.tester.ObjectAttributesEqualityTester ¶
Bases: BaseEqualityTester[Any]
Implement an equality tester for objects without value semantics.
A plain class, an exception or a functools.partial is equal to
another object only if it is the same object, unless the class
defines __eq__. This tester compares the attributes instead:
the entries of vars, the slots, the args of an exception
and the content of a functools.partial (see
get_object_attributes). The attributes are compared through the
registry, so the tolerance and equal_nan apply to them.
It is not registered by default, because it changes the result for the classes that rely on the identity: register it for the classes that must be compared by their attributes. The two objects have the same type, the same attribute names and equal values. The location of a difference is the name of the attribute.
Example
>>> from coola.equality import objects_are_equal
>>> from coola.equality.tester import (
... DefaultEqualityTester,
... EqualityTesterRegistry,
... ObjectAttributesEqualityTester,
... )
>>> class Point:
... def __init__(self, x):
... self.x = x
...
>>> registry = EqualityTesterRegistry({object: DefaultEqualityTester()})
>>> objects_are_equal(Point(1), Point(1), registry=registry)
False
>>> registry.register(Point, ObjectAttributesEqualityTester())
>>> objects_are_equal(Point(1), Point(1), registry=registry)
True
>>> objects_are_equal(Point(1), Point(2), registry=registry)
False
coola.equality.tester.OrderedMappingEqualityTester ¶
Bases: MappingEqualityTester
Implement an ordered mapping equality tester.
This tester behaves like MappingEqualityTester, and additionally
requires the keys to be in the same order. It is registered for
collections.OrderedDict, for which Python's == is also
order-sensitive.
Example
>>> from collections import OrderedDict
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> engine = Engine(EqualityConfig())
>>> engine.run(OrderedDict(a=1, b=2), OrderedDict(a=1, b=2))
True
>>> engine.run(OrderedDict(a=1, b=2), OrderedDict(b=2, a=1))
False
coola.equality.tester.PydanticModelEqualityTester ¶
Bases: BaseEqualityTester[BaseModel]
Implement an equality tester for pydantic.BaseModel.
The two models must have the same type, the same field values and
the same extra values. The values are compared with the registry, so
they support the tolerance and equal_nan. The private attributes
are not compared, as the fields are what the model validates and
serializes. The other entries of the instance __dict__ (for
example the value of a functools.cached_property that was read
on one model only) are ignored too. The location of a difference is
the name of the field, or model_extra.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> from pydantic import BaseModel
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import PydanticModelEqualityTester, get_default_registry
>>> class Point(BaseModel):
... x: float
... y: float
...
>>> engine = Engine(EqualityConfig())
>>> engine.run(Point(x=1.0, y=2.0), Point(x=1.0, y=2.0))
True
>>> engine.run(Point(x=1.0, y=2.0), Point(x=1.0, y=3.0))
False
coola.equality.tester.ScalarEqualityTester ¶
Bases: BaseEqualityTester[float]
Implement a scalar equality tester.
This tester handles numeric scalar types (int, float, complex) with
support for NaN equality and tolerance-based comparisons. Two NaNs
are equal only if equal_nan is True, a bool and an
enum member are labels and are compared exactly, and the other
numbers are close if
|a - b| <= atol + rtol * max(|a|, |b|).
This tester is registered for int, float and complex in
the default registry.
Example
>>> from coola.equality import Tolerance
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import ScalarEqualityTester, get_default_registry
>>> registry = get_default_registry()
>>> tester = ScalarEqualityTester()
>>> tester.objects_are_equal(42.0, 42.0, Engine(EqualityConfig()))
True
>>> tester.objects_are_equal(42.0, 1.0, Engine(EqualityConfig()))
False
>>> tester.objects_are_equal(42.0, 42.0001, Engine(EqualityConfig(tolerance=Tolerance(atol=1e-3))))
True
coola.equality.tester.ScipySparseEqualityTester ¶
Bases: BaseEqualityTester[Any]
Implement an equality tester for scipy.sparse matrices and
arrays.
Two sparse objects are equal if they have the same class (a matrix
is not equal to an array, and the format is part of the class:
csr_matrix, csc_matrix...; the engine checks it), the same
shape, the same data type, the same non-zero positions and
equal values. The values are compared with the tolerance and
equal_nan of the comparison. The duplicate entries are summed
and the explicitly stored zeros are ignored, so two objects that
represent the same matrix are equal. The tolerance applies to the
stored values: a stored value is never compared with an absent one.
The location of a difference is the coordinates of the first value
that differs.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> from scipy import sparse
>>> from coola.equality import compare
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import ScipySparseEqualityTester
>>> engine = Engine(EqualityConfig())
>>> tester = ScipySparseEqualityTester()
>>> tester.objects_are_equal(sparse.eye(3, format="csr"), sparse.eye(3, format="csr"), engine)
True
>>> tester.objects_are_equal(
... sparse.eye(3, format="csr"), 2 * sparse.eye(3, format="csr"), engine
... )
False
>>> compare(sparse.eye(3, format="csr"), 2 * sparse.eye(3, format="csr")).path
(0, 0)
coola.equality.tester.SequenceEqualityTester ¶
Bases: BaseEqualityTester['Sequence[Any]']
Implement a sequence equality tester.
This tester handles sequence types (list, tuple, deque, Sequence
ABC). The two sequences must have the same length, and the items
are compared in order. A sequence of exact scalars (int,
str, bytes, bool and None) is compared at once.
Example
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> engine = Engine(EqualityConfig())
>>> engine.run([1, 2, 3], [1, 2, 3])
True
>>> engine.run([1, 2, 3], [1, 2, 4])
False
>>> engine.run([1, 2, 3], [1, 2])
False
coola.equality.tester.SetEqualityTester ¶
Bases: BaseEqualityTester['AbstractSet[Any]']
Implement a set equality tester.
This tester handles set and frozenset. Unlike ==, the
elements are matched by hash and type, so equal_nan applies to
them (the tolerance does not). The location of a difference is the
element of actual that has no match in expected.
Example
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> engine = Engine(EqualityConfig())
>>> engine.run({1, 2, 3}, {1, 2, 3})
True
>>> engine.run({1, 2, 3}, {1, 2, 4})
False
coola.equality.tester.TableEqualityTester ¶
Bases: BaseEqualityTester[Any]
Implement an equality tester for the tables of pandas, polars and pyarrow.
The structure is compared first, then the columns one by one, each
by its kind (see Column). The comparison stops at the first
column that differs. A table is a leaf for the engine: the
difference is reported as a whole. The reason says what differs in
the structure (the shapes, the data types, the index, the names of
the columns, ...) or, for the values, the name of the first column
that differs, the first row that differs and the two values. The
row is found only when the difference is explained.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
adapter
|
Callable[[Any], Table]
|
The function that converts an object to a |
required |
Example
>>> import pandas as pd
>>> from coola.equality import compare
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import TableEqualityTester, get_default_registry
>>> from coola.equality.tester.pandas import pandas_frame
>>> engine = Engine(EqualityConfig())
>>> tester = TableEqualityTester(pandas_frame)
>>> tester.objects_are_equal(
... pd.DataFrame({"a": [1, 2]}), pd.DataFrame({"a": [1, 2]}), engine
... )
True
>>> tester.objects_are_equal(
... pd.DataFrame({"a": [1, 2]}), pd.DataFrame({"a": [1, 3]}), engine
... )
False
>>> print(compare(pd.DataFrame({"a": [1, 2]}), pd.DataFrame({"a": [1, 3]})).reason)
tables have different values: column 'a' differs at row 1: 2 vs 3
coola.equality.tester.TolerantEqualEqualityTester ¶
Bases: BaseEqualityTester[object]
Implement an equality tester for objects that support both
allclose and equal methods.
This tester is designed for objects that implement both an
allclose(other, rtol, atol, equal_nan) method for
tolerance-based comparison and an equal(other, equal_nan)
method for exact comparison. The tester calls allclose when a
tolerance is not zero, otherwise equal. An object that does not
have both methods is different.
Example
>>> from coola.equality import Tolerance
>>> import math
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import TolerantEqualEqualityTester, get_default_registry
>>> class MyFloat:
... def __init__(self, value: float) -> None:
... self._value = float(value)
... def allclose(
... self,
... other: object,
... rtol: float = 1e-5,
... atol: float = 1e-8,
... equal_nan: bool = False,
... ) -> bool:
... if type(other) is not type(self):
... return False
... if equal_nan and math.isnan(self._value) and math.isnan(other._value):
... return True
... return math.isclose(self._value, other._value, rel_tol=rtol, abs_tol=atol)
... def equal(self, other: object, equal_nan: bool = False) -> bool:
... if type(other) is not type(self):
... return False
... if equal_nan and math.isnan(self._value) and math.isnan(other._value):
... return True
... return self._value == other._value
...
>>> registry = get_default_registry()
>>> tester = TolerantEqualEqualityTester()
>>> tester.objects_are_equal(MyFloat(42), MyFloat(42), Engine(EqualityConfig()))
True
>>> tester.objects_are_equal(MyFloat(1.0), MyFloat(1.4), Engine(EqualityConfig()))
False
>>> tester.objects_are_equal(
... MyFloat(1.0), MyFloat(1.4), Engine(EqualityConfig(tolerance=Tolerance(atol=0.5)))
... )
True
coola.equality.tester.TorchPackedSequenceEqualityTester ¶
Bases: BaseEqualityTester[PackedSequence]
Implement an equality tester for
torch.nn.utils.rnn.PackedSequence.
This tester compares the data tensor and the metadata of two packed
sequences: batch_sizes, sorted_indices and
unsorted_indices. The location of a difference is the name of
the attribute.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> import torch
>>> from torch.nn.utils.rnn import pack_padded_sequence
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> engine = Engine(EqualityConfig())
>>> def make(value):
... return pack_padded_sequence(
... torch.tensor([[1.0, value], [3.0, 4.0]]),
... lengths=torch.tensor([2, 1]),
... batch_first=True,
... enforce_sorted=False,
... )
...
>>> engine.run(make(2.0), make(2.0))
True
>>> engine.run(make(2.0), make(5.0))
False
coola.equality.tester.TorchTensorEqualityTester ¶
Bases: BaseEqualityTester[Tensor]
Implement an equality tester for torch.Tensor.
Two tensors are equal if they have the same data type, shape,
device, layout and equal elements. The numbers are compared with
the tolerance and equal_nan of the comparison, and a boolean
tensor is exact. The tensors on the meta device have no data:
only the metadata is compared. The location of a difference is the
index of the first element that differs.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> import torch
>>> from coola.equality import compare
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import TorchTensorEqualityTester, get_default_registry
>>> engine = Engine(EqualityConfig())
>>> tester = TorchTensorEqualityTester()
>>> tester.objects_are_equal(torch.ones(2, 3), torch.ones(2, 3), engine)
True
>>> tester.objects_are_equal(torch.ones(2, 3), torch.zeros(2, 3), engine)
False
>>> compare(torch.ones(2, 3), torch.zeros(2, 3)).path
(0, 0)
coola.equality.tester.XarrayDataArrayEqualityTester ¶
Bases: BaseEqualityTester[DataArray]
Implement an equality tester for xarray.DataArray.
This tester compares the variable (data, dimensions and attributes), the name and the coordinate variables of two DataArrays. The location of a difference is the name of the attribute.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> import numpy as np
>>> import xarray as xr
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> engine = Engine(EqualityConfig())
>>> engine.run(xr.DataArray(np.arange(6), dims=["z"]), xr.DataArray(np.arange(6), dims=["z"]))
True
>>> engine.run(xr.DataArray(np.ones(6), dims=["z"]), xr.DataArray(np.zeros(6), dims=["z"]))
False
coola.equality.tester.XarrayDatasetEqualityTester ¶
Bases: BaseEqualityTester[Dataset]
Implement an equality tester for xarray.Dataset.
This tester compares the data variables, the coordinates and the attributes of two Datasets. The location of a difference is the name of the attribute.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> import numpy as np
>>> import xarray as xr
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> engine = Engine(EqualityConfig())
>>> engine.run(
... xr.Dataset({"x": ("z", np.arange(6))}), xr.Dataset({"x": ("z", np.arange(6))})
... )
True
>>> engine.run(
... xr.Dataset({"x": ("z", np.ones(6))}), xr.Dataset({"x": ("z", np.zeros(6))})
... )
False
coola.equality.tester.XarrayVariableEqualityTester ¶
Bases: BaseEqualityTester[Variable]
Implement an equality tester for xarray.Variable.
This tester compares the data, the dimension names and the attributes of two Variables. The location of a difference is the name of the attribute.
Raises:
| Type | Description |
|---|---|
MissingPackageError
|
If |
Example
>>> import numpy as np
>>> import xarray as xr
>>> from coola.equality.config import EqualityConfig
>>> from coola.equality.engine import Engine
>>> from coola.equality.tester import get_default_registry
>>> engine = Engine(EqualityConfig())
>>> engine.run(
... xr.Variable(dims=["z"], data=np.arange(6)),
... xr.Variable(dims=["z"], data=np.arange(6)),
... )
True
>>> engine.run(
... xr.Variable(dims=["z"], data=np.zeros(6)), xr.Variable(dims=["z"], data=np.ones(6))
... )
False
coola.equality.tester.get_default_registry ¶
get_default_registry() -> EqualityTesterRegistry
Get or create the default global registry with common Python types.
Returns a singleton registry instance that is pre-configured with equality testers for Python's built-in types including sequences (list, tuple), mappings (dict), and scalar types (int, float).
This function uses a singleton pattern to ensure the same registry instance is returned on subsequent calls, which is efficient and maintains consistency across an application.
Returns:
| Type | Description |
|---|---|
EqualityTesterRegistry
|
A EqualityTesterRegistry instance with equality testers registered for: - Scalar types (int, float) - Sequences (list, tuple, deque, array.array, Sequence ABC) - Mappings (dict, OrderedDict, MappingProxyType, Mapping ABC) - Third-party library types (numpy, torch, pandas, polars, jax, xarray, pyarrow) |
Notes
The singleton pattern means modifications to the returned registry affect all future calls to this function. If you need an isolated registry, create a new EqualityTesterRegistry instance directly.
Example
>>> from coola.equality.tester import get_default_registry
>>> from coola.equality.config import EqualityConfig
>>> registry = get_default_registry()
>>> # Registry is ready to use with common Python types
>>> config = EqualityConfig()
>>> registry.objects_are_equal([1, 2, 3], [1, 2, 3], config)
True
>>> registry.objects_are_equal([1, 2, 3], [1, 1], config)
False
coola.equality.tester.register_equality_testers ¶
register_equality_testers(
mapping: Mapping[type, BaseEqualityTester[Any]],
*,
exist_ok: bool = False,
) -> None
Register custom equality testers to the default global registry.
This allows users to add support for custom types without modifying global state directly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mapping
|
Mapping[type, BaseEqualityTester[Any]]
|
Dictionary mapping types to equality tester instances |
required |
exist_ok
|
bool
|
If False, raises error if any type already registered |
False
|
Example
>>> from coola.equality.tester import register_equality_testers, BaseEqualityTester
>>> class MyType:
... def __init__(self, value):
... self.value = value
...
>>> class MyEqualityTester(BaseEqualityTester[MyType]):
... def objects_are_equal(self, actual: MyType, expected: MyType, ctx) -> bool:
... return actual.value == expected.value or ctx.fail("values differ")
...
>>> register_equality_testers({MyType: MyEqualityTester()})