diff --git a/doc/conf.py b/doc/conf.py index c9735b1f..8de6c071 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -45,3 +45,5 @@ # a list of builtin themes. # html_theme = "sphinx_rtd_theme" + +autodoc_member_order = "bysource" diff --git a/doc/usage.rst b/doc/usage.rst index ffe26687..d55afef4 100644 --- a/doc/usage.rst +++ b/doc/usage.rst @@ -58,6 +58,7 @@ Bitshuffle .. autoclass:: Bitshuffle :members: + :inherited-members: FilterRefBase, Mapping :undoc-members: Blosc @@ -65,6 +66,7 @@ Blosc .. autoclass:: Blosc :members: + :inherited-members: FilterRefBase, Mapping :undoc-members: Blosc2 @@ -72,6 +74,7 @@ Blosc2 .. autoclass:: Blosc2 :members: + :inherited-members: FilterRefBase, Mapping :undoc-members: BZip2 @@ -79,6 +82,7 @@ BZip2 .. autoclass:: BZip2 :members: + :inherited-members: FilterRefBase, Mapping :undoc-members: FciDecomp @@ -86,6 +90,7 @@ FciDecomp .. autoclass:: FciDecomp :members: + :inherited-members: FilterRefBase, Mapping :undoc-members: LZ4 @@ -93,6 +98,7 @@ LZ4 .. autoclass:: LZ4 :members: + :inherited-members: FilterRefBase, Mapping :undoc-members: Sperr @@ -100,6 +106,7 @@ Sperr .. autoclass:: Sperr :members: + :inherited-members: FilterRefBase, Mapping :undoc-members: SZ @@ -107,6 +114,7 @@ SZ .. autoclass:: SZ :members: + :inherited-members: FilterRefBase, Mapping :undoc-members: SZ3 @@ -114,6 +122,7 @@ SZ3 .. autoclass:: SZ3 :members: + :inherited-members: FilterRefBase, Mapping :undoc-members: Zfp @@ -121,6 +130,7 @@ Zfp .. autoclass:: Zfp :members: + :inherited-members: FilterRefBase, Mapping :undoc-members: Zstd @@ -128,36 +138,9 @@ Zstd .. autoclass:: Zstd :members: + :inherited-members: FilterRefBase, Mapping :undoc-members: - -Get dataset compression -+++++++++++++++++++++++ - -For **built-in** compression filters (i.e., GZIP, LZF, SZIP), -dataset compression configuration can be retrieved with `h5py.Dataset`_'s -`compression `_ and -`compression_opts `_ properties. - -For **third-party** compression filters such as the one supported by `hdf5plugin`, -the dataset compression configuration is stored in HDF5 -`filter pipeline `_. -This filter pipeline configuration can be retrieved with `h5py.Dataset`_ "low level" API. -For a given `h5py.Dataset`_, ``dataset``: - -.. code-block:: python - - create_plist = dataset.id.get_create_plist() - - for index in range(create_plist.get_nfilters()): - filter_id, _, filter_options, _ = create_plist.get_filter(index) - print(filter_id, filter_options) - -For compression filters supported by `hdf5plugin`, -:func:`hdf5plugin.from_filter_options` instantiates the filter configuration from the filter id and options. - -.. autofunction:: from_filter_options - Get information about hdf5plugin ++++++++++++++++++++++++++++++++ @@ -186,6 +169,33 @@ Registering with this function is required to perform additional initialisation .. autofunction:: register +Get dataset compression ++++++++++++++++++++++++ + +For **built-in** compression filters (i.e., GZIP, LZF, SZIP), +dataset compression configuration can be retrieved with `h5py.Dataset`_'s +`compression `_ and +`compression_opts `_ properties. + +For **third-party** compression filters such as the one supported by `hdf5plugin`, +the dataset compression configuration is stored in HDF5 +`filter pipeline `_. +This filter pipeline configuration can be retrieved with `h5py.Dataset`_ "low level" API. +For a given `h5py.Dataset`_, ``dataset``: + +.. code-block:: python + + create_plist = dataset.id.get_create_plist() + + for index in range(create_plist.get_nfilters()): + filter_id, _, filter_options, _ = create_plist.get_filter(index) + print(filter_id, filter_options) + +For compression filters supported by `hdf5plugin`, +:func:`hdf5plugin.from_filter_options` instantiates the filter configuration from the filter id and options. + +.. autofunction:: from_filter_options + Use HDF5 filters in other applications ++++++++++++++++++++++++++++++++++++++ diff --git a/src/hdf5plugin/_filters.py b/src/hdf5plugin/_filters.py index 4e3878da..3f6e3c3a 100644 --- a/src/hdf5plugin/_filters.py +++ b/src/hdf5plugin/_filters.py @@ -26,6 +26,8 @@ import logging import math import struct +from collections.abc import Mapping +from typing import Literal, TypeVar import h5py @@ -73,6 +75,24 @@ class FilterBase(h5py.filters.FilterRefBase): filter_id: int filter_name: str + def _init( + self, + filter_options: tuple[int, ...] = (), + config: Mapping[str, int | float | bool | str] | None = None, + ) -> None: + self.filter_options = filter_options + self.__config = {} if config is None else dict(config) + + def get_config(self) -> dict[str, int | float | bool | str]: + """Returns filter configuration""" + return self.__config.copy() + + def __repr__(self) -> str: + arguments = ", ".join( + f"{name}={value!r}" for name, value in self.get_config().items() + ) + return f"{self.__class__.__name__}({arguments})" + @classmethod def _from_filter_options(cls, filter_options: tuple[int, ...]) -> FilterBase: """Returns compression arguments from HDF5 compression filters "cd_values" options @@ -83,7 +103,12 @@ def _from_filter_options(cls, filter_options: tuple[int, ...]) -> FilterBase: raise NotImplementedError() -def _cname_from_id(compression_id: int, compressions: dict[str, int]) -> str: +_CNameLiteral = TypeVar("_CNameLiteral", bound=str) + + +def _cname_from_id( + compression_id: int, compressions: dict[_CNameLiteral, int] +) -> _CNameLiteral: for cname, cid in compressions.items(): if compression_id == cid: return cname @@ -106,9 +131,8 @@ class Bitshuffle(FilterBase): The number of elements per block. It needs to be divisible by eight. Default: 0 (for about 8 kilobytes per block). - :param cname: - `lz4` (default), `none`, `zstd` - :param clevel: Compression level, used only for `zstd` compression. + :param cname: Compressor name. + :param clevel: Compression level, used **only for "zstd"** compression. Can be negative, and must be below or equal to 22 (maximum compression). Default: 3. """ @@ -116,7 +140,9 @@ class Bitshuffle(FilterBase): filter_name = "bshuf" filter_id = BSHUF_ID - __COMPRESSIONS = { + _CNameType = Literal["none", "lz4", "zstd"] + + __COMPRESSIONS: dict[_CNameType, int] = { "none": 0, "lz4": 2, "zstd": 3, @@ -125,7 +151,7 @@ class Bitshuffle(FilterBase): def __init__( self, nelems: int = 0, - cname: str = None, + cname: Bitshuffle._CNameType | None = None, clevel: int = 3, lz4: bool = None, ): @@ -158,10 +184,27 @@ def __init__( if cname not in self.__COMPRESSIONS: raise ValueError(f"Unsupported compression: {cname}") + filter_options: tuple[int, ...] = (nelems, self.__COMPRESSIONS[cname]) + config = {"cname": cname, "nelems": nelems} if cname == "zstd": - self.filter_options = (nelems, self.__COMPRESSIONS[cname], clevel) - else: - self.filter_options = (nelems, self.__COMPRESSIONS[cname]) + filter_options += (clevel,) + config["clevel"] = clevel + self._init(filter_options, config) + + @property + def nelems(self) -> int: + """Number of elements per block""" + return self.filter_options[0] + + @property + def cname(self) -> Bitshuffle._CNameType: + """Compressor name""" + return _cname_from_id(self.filter_options[1], self.__COMPRESSIONS) + + @property + def clevel(self) -> int | None: + """Compression level, only for `zstd` compressor, None for others""" + return self.filter_options[2] if self.cname == "zstd" else None @classmethod def _from_filter_options(cls, filter_options: tuple[int, ...]) -> Bitshuffle: @@ -203,8 +246,8 @@ class Blosc(FilterBase): f.close() :param cname: - `blosclz`, `lz4` (default), `lz4hc`, `zlib`, `zstd` - Optional: `snappy`, depending on compilation (requires C++11). + Compressor name. + `snappy` availability depends on compilation (requires C++11). :param clevel: Compression level from 0 (no compression) to 9 (maximum compression). Default: 5. @@ -227,7 +270,9 @@ class Blosc(FilterBase): filter_name = "blosc" filter_id = BLOSC_ID - __COMPRESSIONS = { + _CNameType = Literal["blosclz", "lz4", "lz4hc", "snappy", "zlib", "zstd"] + + __COMPRESSIONS: dict[_CNameType, int] = { "blosclz": 0, "lz4": 1, "lz4hc": 2, @@ -236,14 +281,38 @@ class Blosc(FilterBase): "zstd": 5, } - def __init__(self, cname: str = "lz4", clevel: int = 5, shuffle: int = SHUFFLE): + def __init__( + self, + cname: Blosc._CNameType = "lz4", + clevel: int = 5, + shuffle: int = SHUFFLE, + ): compression = self.__COMPRESSIONS[cname] clevel = int(clevel) if not 0 <= clevel <= 9: raise ValueError("clevel must be in the range [0, 9]") if shuffle not in (self.NOSHUFFLE, self.SHUFFLE, self.BITSHUFFLE): raise ValueError(f"shuffle={shuffle} is not supported") - self.filter_options = (0, 0, 0, 0, clevel, shuffle, compression) + + self._init( + filter_options=(0, 0, 0, 0, clevel, shuffle, compression), + config={"cname": cname, "clevel": clevel, "shuffle": shuffle}, + ) + + @property + def cname(self) -> Blosc._CNameType: + """Compressor name""" + return _cname_from_id(self.filter_options[6], self.__COMPRESSIONS) + + @property + def clevel(self) -> int: + """Compression level from 0 (no compression) to 9 (maximum compression)""" + return self.filter_options[4] + + @property + def shuffle(self) -> int: + """Shuffle mode one of: NOSHUFFLE, SHUFFLE, BITSHUFFLE""" + return self.filter_options[5] @classmethod def _from_filter_options(cls, filter_options: tuple[int, ...]) -> Blosc: @@ -252,7 +321,7 @@ def _from_filter_options(cls, filter_options: tuple[int, ...]) -> Blosc: :param filter_options: Expected format: (_, _, _, _, clevel*, shuffle*, compression*) :raises ValueError: Unsupported filter_options """ - default_cname = "blosclz" + default_cname: Blosc._CNameType = "blosclz" if len(filter_options) <= 4: return cls(default_cname) @@ -283,8 +352,7 @@ class Blosc2(FilterBase): compression=hdf5plugin.Blosc2(cname='blosclz', clevel=9, filters=hdf5plugin.Blosc2.SHUFFLE)) f.close() - :param cname: - `blosclz` (default), `lz4`, `lz4hc`, `zlib`, `zstd` + :param cname: Compressor name. :param clevel: Compression level from 0 (no compression) to 9 (maximum compression). Default: 5. @@ -315,7 +383,9 @@ class Blosc2(FilterBase): filter_id = BLOSC2_ID filter_name = "blosc2" - __COMPRESSIONS = { + _CNameType = Literal["blosclz", "lz4", "lz4hc", "zlib", "zstd"] + + __COMPRESSIONS: dict[_CNameType, int] = { "blosclz": 0, "lz4": 1, "lz4hc": 2, @@ -323,7 +393,12 @@ class Blosc2(FilterBase): "zstd": 5, } - def __init__(self, cname: str = "blosclz", clevel: int = 5, filters: int = SHUFFLE): + def __init__( + self, + cname: Blosc2._CNameType = "blosclz", + clevel: int = 5, + filters: int = SHUFFLE, + ): compression = self.__COMPRESSIONS[cname] clevel = int(clevel) if not 0 <= clevel <= 9: @@ -336,7 +411,25 @@ def __init__(self, cname: str = "blosclz", clevel: int = 5, filters: int = SHUFF self.TRUNC_PREC, ): raise ValueError(f"filters={filters} is not supported") - self.filter_options = (0, 0, 0, 0, clevel, filters, compression) + self._init( + filter_options=(0, 0, 0, 0, clevel, filters, compression), + config={"cname": cname, "clevel": clevel, "filters": filters}, + ) + + @property + def cname(self) -> Blosc2._CNameType: + """Compressor name""" + return _cname_from_id(self.filter_options[6], self.__COMPRESSIONS) + + @property + def clevel(self) -> int: + """Compression level from 0 (no compression) to 9 (maximum compression)""" + return self.filter_options[4] + + @property + def filters(self) -> int: + """Pre-compression filter, one of: NOFILTER, SHUFFLE, BITSHUFFLE, DELTA, TRUNC_PREC""" + return self.filter_options[5] @classmethod def _from_filter_options(cls, filter_options: tuple[int, ...]) -> Blosc2: @@ -345,7 +438,7 @@ def _from_filter_options(cls, filter_options: tuple[int, ...]) -> Blosc2: :param filter_options: Expected format: (_, _, _, _, clevel*, filters*, compression*) :raises ValueError: Unsupported filter_options """ - default_cname = "blosclz" + default_cname: Blosc2._CNameType = "blosclz" if len(filter_options) <= 4: return cls(default_cname) @@ -386,7 +479,16 @@ def __init__(self, blocksize: int = 9): blocksize = int(blocksize) if not 1 <= blocksize <= 9: raise ValueError("blocksize must be in the range [1, 9]") - self.filter_options = (blocksize,) + + self._init( + filter_options=(blocksize,), + config={"blocksize": blocksize}, + ) + + @property + def blocksize(self) -> int: + """Size of the blocks as a multiple of 100k in [1, 9]""" + return self.filter_options[0] @classmethod def _from_filter_options(cls, filter_options: tuple[int, ...]) -> BZip2: @@ -418,12 +520,12 @@ class FciDecomp(FilterBase): filter_id = FCIDECOMP_ID def __init__(self) -> None: - super().__init__() if not build_config.cpp11: logger.error( "The FciDecomp filter is not available as hdf5plugin was not built with C++11.\n" "You may need to reinstall hdf5plugin with a recent version of pip, or rebuild it with a newer compiler." ) + self._init(filter_options=(), config={}) @classmethod def _from_filter_options(cls, filter_options: tuple[int, ...]) -> FciDecomp: @@ -457,7 +559,17 @@ def __init__(self, nbytes: int = 0): nbytes = int(nbytes) if not 0 <= nbytes <= 0x7E000000: raise ValueError("clevel must be in the range [0, 2113929216]") - self.filter_options = (nbytes,) + self._init( + filter_options=(nbytes,), + config={"nbytes": nbytes}, + ) + + @property + def nbytes(self) -> int: + """The number of bytes per block. + + If 0, block size is 1GB.""" + return self.filter_options[0] @classmethod def _from_filter_options(cls, filter_options: tuple[int, ...]) -> LZ4: @@ -566,24 +678,31 @@ def __init__( maxprec: int = None, minexp: int = None, ): + filter_options: tuple[int, ...] + if rate is not None: - rateHigh, rateLow = struct.unpack("II", struct.pack("d", float(rate))) - self.filter_options = 1, 0, rateHigh, rateLow, 0, 0 + rate = float(rate) + rateHigh, rateLow = struct.unpack("II", struct.pack("d", rate)) + filter_options = 1, 0, rateHigh, rateLow, 0, 0 + config = {"rate": rate} logger.info("ZFP mode 1 used. H5Z_ZFP_MODE_RATE") elif precision is not None: - self.filter_options = 2, 0, int(precision), 0, 0, 0 + precision = int(precision) + filter_options = 2, 0, precision, 0, 0, 0 + config = {"precision": float(precision)} logger.info("ZFP mode 2 used. H5Z_ZFP_MODE_PRECISION") elif accuracy is not None: - accuracyHigh, accuracyLow = struct.unpack( - "II", struct.pack("d", float(accuracy)) - ) - self.filter_options = 3, 0, accuracyHigh, accuracyLow, 0, 0 + accuracy = float(accuracy) + accuracyHigh, accuracyLow = struct.unpack("II", struct.pack("d", accuracy)) + filter_options = 3, 0, accuracyHigh, accuracyLow, 0, 0 + config = {"accuracy": accuracy} logger.info("ZFP mode 3 used. H5Z_ZFP_MODE_ACCURACY") elif reversible: - self.filter_options = 5, 0, 0, 0, 0, 0 + filter_options = 5, 0, 0, 0, 0, 0 + config = {"reversible": True} logger.info("ZFP mode 5 used. H5Z_ZFP_MODE_REVERSIBLE") elif minbits is not None: @@ -594,14 +713,23 @@ def __init__( minbits = int(minbits) maxbits = int(maxbits) maxprec = int(maxprec) - minexp = struct.unpack("I", struct.pack("i", int(minexp)))[0] - self.filter_options = 4, 0, minbits, maxbits, maxprec, minexp + minexp = int(minexp) + minexp_converted = struct.unpack("I", struct.pack("i", minexp))[0] + filter_options = 4, 0, minbits, maxbits, maxprec, minexp_converted + config = { + "minbits": minbits, + "maxbits": maxbits, + "maxprec": maxprec, + "minexp": minexp, + } logger.info("ZFP mode 4 used. H5Z_ZFP_MODE_EXPERT") else: logger.info("ZFP default used") + filter_options = () + config = {} - logger.info(f"filter options = {self.filter_options}") + self._init(filter_options, config) # From zfp.h _ZFP_MIN_BITS = 1 # minimum number of bits per block @@ -771,21 +899,29 @@ def __init__( if peak_signal_to_noise_ratio is not None: if peak_signal_to_noise_ratio <= 0: raise ValueError("peak_signal_to_noise_ratio must be strictly positive") + mode_name = "peak_signal_to_noise_ratio" mode = 2 quality = peak_signal_to_noise_ratio elif absolute is not None: if absolute <= 0: raise ValueError("absolute must be strictly positive") + mode_name = "absolute" mode = 3 quality = absolute else: if rate is not None and not 0 < rate < 64: raise ValueError("rate must be None or in the range ]0, 64[") + mode_name = "rate" mode = 1 quality = 16 if rate is None else rate - self.filter_options = self.__pack_options( - mode, quality, swap, missing_value_mode + self._init( + filter_options=self.__pack_options(mode, quality, swap, missing_value_mode), + config={ + mode_name: quality, + "swap": swap, + "missing_value_mode": missing_value_mode, + }, ) @classmethod @@ -969,14 +1105,17 @@ def __init__( # Get SZ encoding options if absolute is not None: sz_mode = 0 + config = {"absolute": absolute} elif relative is not None: sz_mode = 1 + config = {"relative": relative} else: sz_mode = 10 if pointwise_relative is None: pointwise_relative = 1e-5 + config = {"pointwise_relative": pointwise_relative} - compression_opts = ( + filter_options = ( sz_mode, *_sz_pack_float64(absolute or 0.0), *_sz_pack_float64(relative or 0.0), @@ -985,9 +1124,9 @@ def __init__( ) logger.info(f"SZ mode {sz_mode} used.") - logger.info(f"filter options {compression_opts}") + logger.info(f"filter options {filter_options}") - self.filter_options = compression_opts + self._init(filter_options, config) @classmethod def _from_filter_options(cls, filter_options: tuple[int, ...]) -> SZ: @@ -1062,16 +1201,20 @@ def __init__( # Get SZ3 encoding options: range [0, 5] if absolute is not None: sz_mode = 0 + config = {"absolute": absolute} elif relative is not None: sz_mode = 1 + config = {"relative": relative} elif norm2 is not None: sz_mode = 2 + config = {"norm2": norm2} elif peak_signal_to_noise_ratio is not None: sz_mode = 3 + config = {"peak_signal_to_noise_ratio": peak_signal_to_noise_ratio} if sz_mode not in [0, 2]: logger.warning("Only absolute and norm2 modes properly tested") - compression_opts = ( + filter_options = ( sz_mode, *_sz_pack_float64(absolute or 0.0), *_sz_pack_float64(relative or 0.0), @@ -1079,12 +1222,12 @@ def __init__( *_sz_pack_float64(peak_signal_to_noise_ratio or 0.0), ) logger.info(f"SZ3 mode {sz_mode} used.") - logger.info(f"filter options {compression_opts}") + logger.info(f"filter options {filter_options}") # 9 values needed - if len(compression_opts) != 9: + if len(filter_options) != 9: raise IndexError("Invalid number of arguments") - self.filter_options = compression_opts + self._init(filter_options, config) @classmethod def _from_filter_options(cls, filter_options: tuple[int, ...]) -> SZ3: @@ -1137,7 +1280,15 @@ class Zstd(FilterBase): def __init__(self, clevel: int = 3): if not 1 <= clevel <= 22: raise ValueError("clevel must be in the range [1, 22]") - self.filter_options = (clevel,) + self._init( + filter_options=(clevel,), + config={"clevel": clevel}, + ) + + @property + def clevel(self) -> int: + """Compression level from 1 (lowest compression) to 22 (maximum compression)""" + return self.filter_options[0] @classmethod def _from_filter_options(cls, filter_options: tuple[int, ...]) -> Zstd: diff --git a/src/hdf5plugin/test.py b/src/hdf5plugin/test.py index 59676444..5592c605 100644 --- a/src/hdf5plugin/test.py +++ b/src/hdf5plugin/test.py @@ -792,6 +792,68 @@ def testZstd(self): self._test(hdf5plugin.Zstd(), data) +class TestFilterGetConfig(unittest.TestCase): + """Test filter's get_config method""" + + def testGetConfigRoundtrip(self): + """Test that filter's get_config method returned value roundtrips""" + for filter_class in _filters.FILTER_CLASSES: + with self.subTest(filter=filter_class.filter_name): + filter_instance = filter_class() + config = filter_instance.get_config() + self.assertIsInstance(config, dict) + self.assertEqual(filter_instance, filter_class(**config)) + + +class TestFilterProperties(unittest.TestCase): + """Test filter's parameter properties""" + + def testBitshuffle(self): + """Test Bitshuffle filter properties""" + lz4_filter = hdf5plugin.Bitshuffle(nelems=512, cname="lz4") + self.assertEqual(lz4_filter.nelems, 512) + self.assertEqual(lz4_filter.cname, "lz4") + self.assertIsNone(lz4_filter.clevel) + + zstd_filter = hdf5plugin.Bitshuffle(nelems=512, cname="zstd", clevel=5) + self.assertEqual(zstd_filter.nelems, 512) + self.assertEqual(zstd_filter.cname, "zstd") + self.assertEqual(zstd_filter.clevel, 5) + + def testBlosc(self): + """Test Blosc filter properties""" + filter_ = hdf5plugin.Blosc( + cname="zlib", clevel=7, shuffle=hdf5plugin.Blosc.BITSHUFFLE + ) + self.assertEqual(filter_.cname, "zlib") + self.assertEqual(filter_.clevel, 7) + self.assertEqual(filter_.shuffle, hdf5plugin.Blosc.BITSHUFFLE) + + def testBlosc2(self): + """Test Blosc2 filter properties""" + filter_ = hdf5plugin.Blosc2( + cname="zstd", clevel=9, filters=hdf5plugin.Blosc2.NOFILTER + ) + self.assertEqual(filter_.cname, "zstd") + self.assertEqual(filter_.clevel, 9) + self.assertEqual(filter_.filters, hdf5plugin.Blosc2.NOFILTER) + + def testBZip2(self): + """Test BZip2 filter properties""" + filter_ = hdf5plugin.BZip2(blocksize=7) + self.assertEqual(filter_.blocksize, 7) + + def testLZ4(self): + """Test LZ4 filter properties""" + filter_ = hdf5plugin.LZ4(nbytes=2048) + self.assertEqual(filter_.nbytes, 2048) + + def testZstd(self): + """Test Zstd filter properties""" + filter_ = hdf5plugin.Zstd(clevel=15) + self.assertEqual(filter_.clevel, 15) + + class TestPackage(unittest.TestCase): """Test general features of the hdf5plugin package""" @@ -1038,6 +1100,8 @@ def suite() -> unittest.TestSuite: TestFromFilterOptionsMethods, TestFromFilterOptions, TestFromFilterOptionsRoundtrip, + TestFilterGetConfig, + TestFilterProperties, TestPackage, TestRegisterFilter, TestGetFilters, diff --git a/test/test.py b/test/test.py index 9eb300cc..ee9695d1 100644 --- a/test/test.py +++ b/test/test.py @@ -35,6 +35,7 @@ import numpy import hdf5plugin +from hdf5plugin import _filters from hdf5plugin.test import suite as hdf5plugin_suite @@ -360,9 +361,25 @@ def testSZ3(self): h5.close() +class TestRepr(unittest.TestCase): + """Test filter's __repr__ method""" + + def testReprRoundtrip(self): + """Test that filter's __repr__ method return value roundtrips""" + for filter_class in _filters.FILTER_CLASSES: + with self.subTest(filter=filter_class.filter_name): + filter_instance = filter_class() + repr_string = repr(filter_instance) + repr_instance = eval( + repr_string, {filter_class.__name__: filter_class} + ) # nosec + self.assertEqual(filter_instance, repr_instance) + + def suite(): testSuite = unittest.TestSuite() testSuite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestHDF5PluginRead)) + testSuite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestRepr)) testSuite.addTest(hdf5plugin_suite()) return testSuite