Source code for holoviews.element.comparison

Helper classes for comparing the equality of two HoloViews objects.

These classes are designed to integrate with unittest.TestCase (see
the tests directory) while making equality testing easily accessible
to the user.

For instance, to test if two Matrix objects are equal you can use:

Comparison.assertEqual(matrix1, matrix2)

This will raise an AssertionError if the two matrix objects are not
equal, including information regarding what exactly failed to match.

Note that this functionality could not be provided using comparison
methods on all objects as comparison operators only return Booleans and
thus would not supply any information regarding *why* two elements are
considered different.
import contextlib
from functools import partial
from unittest import TestCase
from unittest.util import safe_repr

import numpy as np
import pandas as pd
from numpy.testing import assert_array_almost_equal, assert_array_equal

from ..core import (
from ..core.options import Cycle, Options
from ..core.util import cast_array_to_int64, datetime_types, dt_to_int, is_float
from . import *  # noqa (All Elements need to support comparison)

[docs]class ComparisonInterface: """ This class is designed to allow equality testing to work seamlessly with unittest.TestCase as a mix-in by implementing a compatible interface (namely the assertEqual method). The assertEqual class method is to be overridden by an instance method of the same name when used as a mix-in with TestCase. The contents of the equality_type_funcs dictionary is suitable for use with TestCase.addTypeEqualityFunc. """ equality_type_funcs = {} failureException = AssertionError
[docs] @classmethod def simple_equality(cls, first, second, msg=None): """ Classmethod equivalent to unittest.TestCase method (longMessage = False.) """ check = first==second if not isinstance(check, bool) and hasattr(check, "all"): check = check.all() if not check: standardMsg = f'{safe_repr(first)} != {safe_repr(second)}' raise cls.failureException(msg or standardMsg)
[docs] @classmethod def assertEqual(cls, first, second, msg=None): """ Classmethod equivalent to unittest.TestCase method """ asserter = None if type(first) is type(second) or (is_float(first) and is_float(second)): asserter = cls.equality_type_funcs.get(type(first)) if asserter is not None: if isinstance(asserter, str): asserter = getattr(cls, asserter) if asserter is None: asserter = cls.simple_equality if msg is None: asserter(first, second) else: asserter(first, second, msg=msg)
[docs]class Comparison(ComparisonInterface): """ Class used for comparing two HoloViews objects, including complex composite objects. Comparisons are available as classmethods, the most general being the assertEqual method that is intended to work with any input. For instance, to test if two Image objects are equal you can use: Comparison.assertEqual(matrix1, matrix2) """ # someone might prefer to use a different function, e.g. assert_all_close assert_array_almost_equal_fn = partial(assert_array_almost_equal, decimal=6) @classmethod def register(cls): # Float comparisons cls.equality_type_funcs[float] = cls.compare_floats cls.equality_type_funcs[np.float32] = cls.compare_floats cls.equality_type_funcs[np.float64] = cls.compare_floats # List and tuple comparisons cls.equality_type_funcs[list] = cls.compare_lists cls.equality_type_funcs[tuple] = cls.compare_tuples # Dictionary comparisons cls.equality_type_funcs[dict] = cls.compare_dictionaries # Numpy array comparison cls.equality_type_funcs[np.ndarray] = cls.compare_arrays cls.equality_type_funcs[] = cls.compare_arrays # Pandas dataframe comparison cls.equality_type_funcs[pd.DataFrame] = cls.compare_dataframe # Dimension objects cls.equality_type_funcs[Dimension] = cls.compare_dimensions cls.equality_type_funcs[Dimensioned] = cls.compare_dimensioned # Used in unit tests cls.equality_type_funcs[Element] = cls.compare_elements # Used in unit tests # Composition (+ and *) cls.equality_type_funcs[Overlay] = cls.compare_overlays cls.equality_type_funcs[Layout] = cls.compare_layouttrees cls.equality_type_funcs[Empty] = cls.compare_empties # Annotations cls.equality_type_funcs[VLine] = cls.compare_vline cls.equality_type_funcs[HLine] = cls.compare_hline cls.equality_type_funcs[VSpan] = cls.compare_vspan cls.equality_type_funcs[HSpan] = cls.compare_hspan cls.equality_type_funcs[Spline] = cls.compare_spline cls.equality_type_funcs[Arrow] = cls.compare_arrow cls.equality_type_funcs[Text] = cls.compare_text cls.equality_type_funcs[Div] = cls.compare_div # Path comparisons cls.equality_type_funcs[Path] = cls.compare_paths cls.equality_type_funcs[Contours] = cls.compare_contours cls.equality_type_funcs[Polygons] = cls.compare_polygons cls.equality_type_funcs[Box] = cls.compare_box cls.equality_type_funcs[Ellipse] = cls.compare_ellipse cls.equality_type_funcs[Bounds] = cls.compare_bounds # Rasters cls.equality_type_funcs[Image] = cls.compare_image cls.equality_type_funcs[RGB] = cls.compare_rgb cls.equality_type_funcs[HSV] = cls.compare_hsv cls.equality_type_funcs[Raster] = cls.compare_raster cls.equality_type_funcs[QuadMesh] = cls.compare_quadmesh cls.equality_type_funcs[Surface] = cls.compare_surface cls.equality_type_funcs[HeatMap] = cls.compare_dataset # Geometries cls.equality_type_funcs[Segments] = cls.compare_segments cls.equality_type_funcs[Rectangles] = cls.compare_boxes # Charts cls.equality_type_funcs[Dataset] = cls.compare_dataset cls.equality_type_funcs[Curve] = cls.compare_curve cls.equality_type_funcs[ErrorBars] = cls.compare_errorbars cls.equality_type_funcs[Spread] = cls.compare_spread cls.equality_type_funcs[Area] = cls.compare_area cls.equality_type_funcs[Scatter] = cls.compare_scatter cls.equality_type_funcs[Scatter3D] = cls.compare_scatter3d cls.equality_type_funcs[TriSurface] = cls.compare_trisurface cls.equality_type_funcs[Histogram] = cls.compare_histogram cls.equality_type_funcs[Bars] = cls.compare_bars cls.equality_type_funcs[Spikes] = cls.compare_spikes cls.equality_type_funcs[BoxWhisker] = cls.compare_boxwhisker cls.equality_type_funcs[VectorField] = cls.compare_vectorfield # Graphs cls.equality_type_funcs[Graph] = cls.compare_graph cls.equality_type_funcs[Nodes] = cls.compare_nodes cls.equality_type_funcs[EdgePaths] = cls.compare_edgepaths cls.equality_type_funcs[TriMesh] = cls.compare_trimesh # Tables cls.equality_type_funcs[ItemTable] = cls.compare_itemtables cls.equality_type_funcs[Table] = cls.compare_tables cls.equality_type_funcs[Points] = cls.compare_points # Statistical cls.equality_type_funcs[Bivariate] = cls.compare_bivariate cls.equality_type_funcs[Distribution] = cls.compare_distribution cls.equality_type_funcs[HexTiles] = cls.compare_hextiles # NdMappings cls.equality_type_funcs[NdLayout] = cls.compare_gridlayout cls.equality_type_funcs[AdjointLayout] = cls.compare_adjointlayouts cls.equality_type_funcs[NdOverlay] = cls.compare_ndoverlays cls.equality_type_funcs[GridSpace] = cls.compare_grids cls.equality_type_funcs[GridMatrix] = cls.compare_grids cls.equality_type_funcs[HoloMap] = cls.compare_holomap cls.equality_type_funcs[DynamicMap] = cls.compare_dynamicmap # Option objects cls.equality_type_funcs[Options] = cls.compare_options cls.equality_type_funcs[Cycle] = cls.compare_cycles return cls.equality_type_funcs @classmethod def compare_dictionaries(cls, d1, d2, msg='Dictionaries'): keys= set(d1.keys()) keys2 = set(d2.keys()) symmetric_diff = keys ^ keys2 if symmetric_diff: msg = f"Dictionaries have different sets of keys: {symmetric_diff!r}\n\n" msg += f"Dictionary 1: {d1}\n" msg += f"Dictionary 2: {d2}" raise cls.failureException(msg) for k in keys: cls.assertEqual(d1[k], d2[k]) @classmethod def compare_lists(cls, l1, l2, msg=None): try: cls.assertEqual(len(l1), len(l2)) for v1, v2 in zip(l1, l2): cls.assertEqual(v1, v2) except AssertionError as e: raise AssertionError(msg or f'{l1!r} != {l2!r}') from e @classmethod def compare_tuples(cls, t1, t2, msg=None): try: cls.assertEqual(len(t1), len(t2)) for i1, i2 in zip(t1, t2): cls.assertEqual(i1, i2) except AssertionError as e: raise AssertionError(msg or f'{t1!r} != {t2!r}') from e #=====================# # Literal comparisons # #=====================# @classmethod def compare_floats(cls, arr1, arr2, msg='Floats'): cls.compare_arrays(arr1, arr2, msg) @classmethod def compare_arrays(cls, arr1, arr2, msg='Arrays'): try: if arr1.dtype.kind == 'M': arr1 = cast_array_to_int64(arr1.astype('datetime64[ns]')) if arr2.dtype.kind == 'M': arr2 = cast_array_to_int64(arr2.astype('datetime64[ns]')) assert_array_equal(arr1, arr2) except Exception: try: cls.assert_array_almost_equal_fn(arr1, arr2) except AssertionError as e: raise cls.failureException(msg + str(e)[11:]) from e @classmethod def bounds_check(cls, el1, el2, msg=None): lbrt1 = el1.bounds.lbrt() lbrt2 = el2.bounds.lbrt() try: for v1, v2 in zip(lbrt1, lbrt2): if isinstance(v1, datetime_types): v1 = dt_to_int(v1) if isinstance(v2, datetime_types): v2 = dt_to_int(v2) cls.assert_array_almost_equal_fn(v1, v2) except AssertionError as e: raise cls.failureException(f"BoundingBoxes are mismatched: {el1.bounds.lbrt()} != {el2.bounds.lbrt()}.") from e #=======================================# # Dimension and Dimensioned comparisons # #=======================================# @classmethod def compare_dimensions(cls, dim1, dim2, msg=None): # 'Weak' equality semantics if != raise cls.failureException(f"Dimension names mismatched: {} != {}") if dim1.label != dim2.label: raise cls.failureException(f"Dimension labels mismatched: {dim1.label} != {dim2.label}") # 'Deep' equality of dimension metadata (all parameters) dim1_params = dim1.param.values() dim2_params = dim2.param.values() if set(dim1_params.keys()) != set(dim2_params.keys()): raise cls.failureException(f"Dimension parameter sets mismatched: {set(dim1_params.keys())} != {set(dim2_params.keys())}") for k in dim1_params.keys(): if (dim1.param.objects('existing')[k].__class__.__name__ == 'Callable' and dim2.param.objects('existing')[k].__class__.__name__ == 'Callable'): continue try: # This is needed as two lists are not compared by contents using == cls.assertEqual(dim1_params[k], dim2_params[k], msg=None) except AssertionError as e: msg = f'Dimension parameter {k!r} mismatched: ' raise cls.failureException(f"{msg}{e!s}") from e @classmethod def compare_labelled_data(cls, obj1, obj2, msg=None): cls.assertEqual(,, "Group labels mismatched.") cls.assertEqual(obj1.label, obj2.label, "Labels mismatched.") @classmethod def compare_dimension_lists(cls, dlist1, dlist2, msg='Dimension lists'): if len(dlist1) != len(dlist2): raise cls.failureException(f'{msg} mismatched') for d1, d2 in zip(dlist1, dlist2): cls.assertEqual(d1, d2) @classmethod def compare_dimensioned(cls, obj1, obj2, msg=None): cls.compare_labelled_data(obj1, obj2) cls.compare_dimension_lists(obj1.vdims, obj2.vdims, 'Value dimension list') cls.compare_dimension_lists(obj1.kdims, obj2.kdims, 'Key dimension list') @classmethod def compare_elements(cls, obj1, obj2, msg=None): cls.compare_labelled_data(obj1, obj2) cls.assertEqual(, #===============================# # Compositional trees (+ and *) # #===============================# @classmethod def compare_trees(cls, el1, el2, msg='Trees'): if len(el1.keys()) != len(el2.keys()): raise cls.failureException(f"{msg} have mismatched path counts.") if el1.keys() != el2.keys(): raise cls.failureException(f"{msg} have mismatched paths.") for element1, element2 in zip(el1.values(), el2.values()): cls.assertEqual(element1, element2) @classmethod def compare_layouttrees(cls, el1, el2, msg=None): cls.compare_dimensioned(el1, el2) cls.compare_trees(el1, el2, msg='Layouts') @classmethod def compare_empties(cls, el1, el2, msg=None): if not all(isinstance(el, Empty) for el in [el1, el2]): raise cls.failureException("Compared elements are not both Empty()") @classmethod def compare_overlays(cls, el1, el2, msg=None): cls.compare_dimensioned(el1, el2) cls.compare_trees(el1, el2, msg='Overlays') #================================# # AttrTree and Map based classes # #================================# @classmethod def compare_ndmappings(cls, el1, el2, msg='NdMappings'): cls.compare_dimensioned(el1, el2) if len(el1.keys()) != len(el2.keys()): raise cls.failureException(f"{msg} have different numbers of keys.") if set(el1.keys()) != set(el2.keys()): diff1 = [el for el in el1.keys() if el not in el2.keys()] diff2 = [el for el in el2.keys() if el not in el1.keys()] raise cls.failureException(f"{msg} have different sets of keys. " + f"In first, not second {diff1}. " + f"In second, not first: {diff2}.") for element1, element2 in zip(el1, el2): cls.assertEqual(element1, element2) @classmethod def compare_holomap(cls, el1, el2, msg='HoloMaps'): cls.compare_dimensioned(el1, el2) cls.compare_ndmappings(el1, el2, msg) @classmethod def compare_dynamicmap(cls, el1, el2, msg='DynamicMap'): cls.compare_dimensioned(el1, el2) cls.compare_ndmappings(el1, el2, msg) @classmethod def compare_gridlayout(cls, el1, el2, msg=None): cls.compare_dimensioned(el1, el2) if len(el1) != len(el2): raise cls.failureException("Layouts have different sizes.") if set(el1.keys()) != set(el2.keys()): raise cls.failureException("Layouts have different keys.") for element1, element2 in zip(el1, el2): cls.assertEqual(element1,element2) @classmethod def compare_ndoverlays(cls, el1, el2, msg=None): cls.compare_dimensioned(el1, el2) if len(el1) != len(el2): raise cls.failureException("NdOverlays have different lengths.") for (layer1, layer2) in zip(el1, el2): cls.assertEqual(layer1, layer2) @classmethod def compare_adjointlayouts(cls, el1, el2, msg=None): cls.compare_dimensioned(el1, el2) for element1, element2 in zip(el1, el1): cls.assertEqual(element1, element2) #=============# # Annotations # #=============# @classmethod def compare_annotation(cls, el1, el2, msg='Annotation'): cls.compare_dimensioned(el1, el2) cls.assertEqual(, @classmethod def compare_hline(cls, el1, el2, msg='HLine'): cls.compare_annotation(el1, el2, msg=msg) @classmethod def compare_vline(cls, el1, el2, msg='VLine'): cls.compare_annotation(el1, el2, msg=msg) @classmethod def compare_vspan(cls, el1, el2, msg='VSpan'): cls.compare_annotation(el1, el2, msg=msg) @classmethod def compare_hspan(cls, el1, el2, msg='HSpan'): cls.compare_annotation(el1, el2, msg=msg) @classmethod def compare_spline(cls, el1, el2, msg='Spline'): cls.compare_annotation(el1, el2, msg=msg) @classmethod def compare_arrow(cls, el1, el2, msg='Arrow'): cls.compare_annotation(el1, el2, msg=msg) @classmethod def compare_text(cls, el1, el2, msg='Text'): cls.compare_annotation(el1, el2, msg=msg) @classmethod def compare_div(cls, el1, el2, msg='Div'): cls.compare_annotation(el1, el2, msg=msg) #=======# # Paths # #=======# @classmethod def compare_paths(cls, el1, el2, msg='Path'): cls.compare_dataset(el1, el2, msg) paths1 = el1.split() paths2 = el2.split() if len(paths1) != len(paths2): raise cls.failureException(f"{msg} objects do not have a matching number of paths.") for p1, p2 in zip(paths1, paths2): cls.compare_dataset(p1, p2, f'{msg} data') @classmethod def compare_contours(cls, el1, el2, msg='Contours'): cls.compare_paths(el1, el2, msg=msg) @classmethod def compare_polygons(cls, el1, el2, msg='Polygons'): cls.compare_paths(el1, el2, msg=msg) @classmethod def compare_box(cls, el1, el2, msg='Box'): cls.compare_paths(el1, el2, msg=msg) @classmethod def compare_ellipse(cls, el1, el2, msg='Ellipse'): cls.compare_paths(el1, el2, msg=msg) @classmethod def compare_bounds(cls, el1, el2, msg='Bounds'): cls.compare_paths(el1, el2, msg=msg) #========# # Charts # #========# @classmethod def compare_dataset(cls, el1, el2, msg='Dataset'): cls.compare_dimensioned(el1, el2) tabular = not (el1.interface.gridded and el2.interface.gridded) dimension_data = [(d, el1.dimension_values(d, expanded=tabular), el2.dimension_values(d, expanded=tabular)) for d in el1.kdims] dimension_data += [(d, el1.dimension_values(d, flat=tabular), el2.dimension_values(d, flat=tabular)) for d in el1.vdims] if el1.shape[0] != el2.shape[0]: raise AssertionError("%s not of matching length, %d vs. %d." % (msg, el1.shape[0], el2.shape[0])) for dim, d1, d2 in dimension_data: with contextlib.suppress(Exception): np.testing.assert_equal(d1, d2) continue # if equal, no need to check further if d1.dtype != d2.dtype: failure_msg = ( f"{msg} {dim.pprint_label} columns have different type. " f"First has type {d1}, and second has type {d2}." ) raise cls.failureException(failure_msg) if d1.dtype.kind in 'SUOV': if list(d1) == list(d2): failure_msg = f"{msg} along dimension {dim.pprint_label} not equal." raise cls.failureException(failure_msg) else: cls.compare_arrays(d1, d2, msg) @classmethod def compare_curve(cls, el1, el2, msg='Curve'): cls.compare_dataset(el1, el2, msg) @classmethod def compare_errorbars(cls, el1, el2, msg='ErrorBars'): cls.compare_dataset(el1, el2, msg) @classmethod def compare_spread(cls, el1, el2, msg='Spread'): cls.compare_dataset(el1, el2, msg) @classmethod def compare_area(cls, el1, el2, msg='Area'): cls.compare_dataset(el1, el2, msg) @classmethod def compare_scatter(cls, el1, el2, msg='Scatter'): cls.compare_dataset(el1, el2, msg) @classmethod def compare_scatter3d(cls, el1, el2, msg='Scatter3D'): cls.compare_dataset(el1, el2, msg) @classmethod def compare_trisurface(cls, el1, el2, msg='TriSurface'): cls.compare_dataset(el1, el2, msg) @classmethod def compare_histogram(cls, el1, el2, msg='Histogram'): cls.compare_dataset(el1, el2, msg) @classmethod def compare_points(cls, el1, el2, msg='Points'): cls.compare_dataset(el1, el2, msg) @classmethod def compare_vectorfield(cls, el1, el2, msg='VectorField'): cls.compare_dataset(el1, el2, msg) @classmethod def compare_bars(cls, el1, el2, msg='Bars'): cls.compare_dataset(el1, el2, msg) @classmethod def compare_spikes(cls, el1, el2, msg='Spikes'): cls.compare_dataset(el1, el2, msg) @classmethod def compare_boxwhisker(cls, el1, el2, msg='BoxWhisker'): cls.compare_dataset(el1, el2, msg) #============# # Geometries # #============# @classmethod def compare_segments(cls, el1, el2, msg='Segments'): cls.compare_dataset(el1, el2, msg) @classmethod def compare_boxes(cls, el1, el2, msg='Rectangles'): cls.compare_dataset(el1, el2, msg) #=========# # Graphs # #=========# @classmethod def compare_graph(cls, el1, el2, msg='Graph'): cls.compare_dataset(el1, el2, msg) cls.compare_nodes(el1.nodes, el2.nodes, msg) if el1._edgepaths or el2._edgepaths: cls.compare_edgepaths(el1.edgepaths, el2.edgepaths, msg) @classmethod def compare_trimesh(cls, el1, el2, msg='TriMesh'): cls.compare_graph(el1, el2, msg) @classmethod def compare_nodes(cls, el1, el2, msg='Nodes'): cls.compare_dataset(el1, el2, msg) @classmethod def compare_edgepaths(cls, el1, el2, msg='Nodes'): cls.compare_paths(el1, el2, msg) #=========# # Rasters # #=========# @classmethod def compare_raster(cls, el1, el2, msg='Raster'): cls.compare_dimensioned(el1, el2) cls.compare_arrays(,, msg) @classmethod def compare_quadmesh(cls, el1, el2, msg='QuadMesh'): cls.compare_dataset(el1, el2, msg) @classmethod def compare_heatmap(cls, el1, el2, msg='HeatMap'): cls.compare_dataset(el1, el2, msg) @classmethod def compare_image(cls, el1, el2, msg='Image'): cls.bounds_check(el1,el2) cls.compare_dataset(el1, el2, msg) @classmethod def compare_rgb(cls, el1, el2, msg='RGB'): cls.bounds_check(el1,el2) cls.compare_dataset(el1, el2, msg) @classmethod def compare_hsv(cls, el1, el2, msg='HSV'): cls.bounds_check(el1,el2) cls.compare_dataset(el1, el2, msg) @classmethod def compare_surface(cls, el1, el2, msg='Surface'): cls.bounds_check(el1,el2) cls.compare_dataset(el1, el2, msg) #========# # Tables # #========# @classmethod def compare_itemtables(cls, el1, el2, msg=None): cls.compare_dimensioned(el1, el2) if el1.rows != el2.rows: raise cls.failureException("ItemTables have different numbers of rows.") if el1.cols != el2.cols: raise cls.failureException("ItemTables have different numbers of columns.") if [ for d in el1.vdims] != [ for d in el2.vdims]: raise cls.failureException("ItemTables have different Dimensions.") @classmethod def compare_tables(cls, el1, el2, msg='Table'): cls.compare_dataset(el1, el2, msg) #========# # Pandas # #========# @classmethod def compare_dataframe(cls, df1, df2, msg='DFrame'): from pandas.testing import assert_frame_equal try: assert_frame_equal(df1, df2) except AssertionError as e: raise cls.failureException(f'{msg}: {e}') from e #============# # Statistics # #============# @classmethod def compare_distribution(cls, el1, el2, msg='Distribution'): cls.compare_dataset(el1, el2, msg) @classmethod def compare_bivariate(cls, el1, el2, msg='Bivariate'): cls.compare_dataset(el1, el2, msg) @classmethod def compare_hextiles(cls, el1, el2, msg='HexTiles'): cls.compare_dataset(el1, el2, msg) #=======# # Grids # #=======# @classmethod def _compare_grids(cls, el1, el2, name): if len(el1.keys()) != len(el2.keys()): raise cls.failureException(f"{name}s have different numbers of items.") if set(el1.keys()) != set(el2.keys()): raise cls.failureException(f"{name}s have different keys.") if len(el1) != len(el2): raise cls.failureException(f"{name}s have different depths.") for element1, element2 in zip(el1, el2): cls.assertEqual(element1, element2) @classmethod def compare_grids(cls, el1, el2, msg=None): cls.compare_dimensioned(el1, el2) cls._compare_grids(el1, el2, 'GridSpace') #=========# # Options # #=========# @classmethod def compare_options(cls, options1, options2, msg=None): cls.assertEqual(options1.kwargs, options2.kwargs) @classmethod def compare_cycles(cls, cycle1, cycle2, msg=None): cls.assertEqual(cycle1.values, cycle2.values)
[docs]class ComparisonTestCase(Comparison, TestCase): """ Class to integrate the Comparison class with unittest.TestCase. """ def __init__(self, *args, **kwargs): TestCase.__init__(self, *args, **kwargs) registry = Comparison.register() for k, v in registry.items(): self.addTypeEqualityFunc(k, v)
_assert_element_equal = ComparisonTestCase().assertEqual def assert_element_equal(element1, element2): # Filter non-holoviews elements hv_types = (Element, Layout) if not isinstance(element1, hv_types): raise TypeError(f"First argument is not an allowed type but a {type(element1).__name__!r}.") if not isinstance(element2, hv_types): raise TypeError(f"Second argument is not an allowed type but a {type(element2).__name__!r}.") _assert_element_equal(element1, element2)