diff --git a/docs/changelog.md b/docs/changelog.md index 6959982..29c0fe8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,7 +2,17 @@ ## 1.* -### 1.5.0 - 24-09-02 - `str` support for HDF5 +### 1.5.* + +#### 1.5.1 - 24-09-03 - Fix revalidation with proxy classes + +Bugfix: +- [#14](https://github.com/p2p-ld/numpydantic/pull/14): Allow revalidation of proxied arrays + +Tests: +- Add test module for tests against all interfaces, test for above bug + +#### 1.5.0 - 24-09-02 - `str` support for HDF5 Strings in hdf5 are tricky! HDF5 doesn't have native support for unicode, but it can be persuaded to store data in ASCII or virtualized utf-8 under somewhat obscure conditions. @@ -36,7 +46,9 @@ instance[0,0] = 'hey' assert instance[0,0] == 'hey' ``` -### 1.4.1 - 24-09-02 - `len()` support and dunder method testing +### 1.4.* + +#### 1.4.1 - 24-09-02 - `len()` support and dunder method testing It's pretty natural to want to do `len(array)` as a shorthand for `array.shape[0]`, but since some of the numpydantic classes are passthrough proxy objects, @@ -52,7 +64,7 @@ There is a certain combinatoric explosion when we start testing across all inter for all input types, for all dtype and all shape cases, but for now numpydantic is fast enough that this doesn't matter <3. -### 1.4.0 - 24-09-02 - HDF5 Compound Dtype Support +#### 1.4.0 - 24-09-02 - HDF5 Compound Dtype Support HDF5 can have compound dtypes like: @@ -107,7 +119,9 @@ my_model = MyModel( np.dtype('int64') ``` -### 1.3.3 - 24-08-13 - Callable type annotations +### 1.3.* + +#### 1.3.3 - 24-08-13 - Callable type annotations Problem, when you use a numpydantic `"wrap"` validator, it gives the annotation as a `handler` function. @@ -162,11 +176,11 @@ ShapeError: Invalid shape! expected shape ['3'], got shape (4,) using it. This shaves ~600ms off import time. -### 1.3.2 - 24-08-12 - Allow subclasses of dtypes +#### 1.3.2 - 24-08-12 - Allow subclasses of dtypes (also when using objects for dtypes, subclasses of that object are allowed to validate) -### 1.3.1 - 24-08-12 - Allow arbitrary dtypes, pydantic models as dtypes +#### 1.3.1 - 24-08-12 - Allow arbitrary dtypes, pydantic models as dtypes Previously we would only allow dtypes if we knew for sure that there was some python base type to generate a schema with. @@ -183,7 +197,7 @@ Only one substantial change, and that is a `get_object_dtype` method which interfaces can override if there is some fancy way they have of getting types/items from an object array. -### 1.3.0 - 24-08-05 - Better string dtype handling +#### 1.3.0 - 24-08-05 - Better string dtype handling API Changes: - Split apart the validation methods into smaller chunks to better support @@ -194,7 +208,9 @@ Bugfix: - [#4](https://github.com/p2p-ld/numpydantic/issues/4) - Support dtype checking for strings in zarr and numpy arrays -### 1.2.3 - 24-07-31 - Vendor `nptyping` +### 1.2.* + +#### 1.2.3 - 24-07-31 - Vendor `nptyping` `nptyping` vendored into `numpydantic.vendor.nptyping` - `nptyping` is no longer maintained, and pins `numpy<2`. @@ -221,22 +237,24 @@ Tidying: - Remove `monkeypatch` module! we don't need it anymore! everything has either been upstreamed or vendored. -### 1.2.2 - 24-07-31 +#### 1.2.2 - 24-07-31 Add `datetime` map to numpy's :class:`numpy.datetime64` type -### 1.2.1 - 24-06-27 +#### 1.2.1 - 24-06-27 Fix a minor bug where {class}`~numpydantic.exceptions.DtypeError` would not cause pydantic to throw a {class}`pydantic.ValidationError` because custom validator functions need to raise either `AssertionError` or `ValueError` - made `DtypeError` also inherit from `ValueError` because that is also technically true. -### 1.2.0 - 24-06-13 - Shape ranges +#### 1.2.0 - 24-06-13 - Shape ranges - Add ability to specify shapes as ranges - see [shape ranges](shape-ranges) -### 1.1.0 - 24-05-24 - Instance Checking +### 1.1.* + +#### 1.1.0 - 24-05-24 - Instance Checking https://github.com/p2p-ld/numpydantic/pull/1 diff --git a/pyproject.toml b/pyproject.toml index ceef83f..9b9b15e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "numpydantic" -version = "1.5.0" +version = "1.5.1" description = "Type and shape validation and serialization for arbitrary array types in pydantic models" authors = [ {name = "sneakers-the-rat", email = "sneakers-the-rat@protonmail.com"}, diff --git a/src/numpydantic/interface/hdf5.py b/src/numpydantic/interface/hdf5.py index 6bb7dda..0bb8720 100644 --- a/src/numpydantic/interface/hdf5.py +++ b/src/numpydantic/interface/hdf5.py @@ -194,10 +194,7 @@ class H5Interface(Interface): passthrough numpy-like interface to the dataset. """ - input_types = ( - H5ArrayPath, - H5Arraylike, - ) + input_types = (H5ArrayPath, H5Arraylike, H5Proxy) return_type = H5Proxy @classmethod @@ -211,7 +208,7 @@ class H5Interface(Interface): Check that the given array is a :class:`.H5ArrayPath` or something that resembles one. """ - if isinstance(array, H5ArrayPath): + if isinstance(array, (H5ArrayPath, H5Proxy)): return True if isinstance(array, (tuple, list)) and len(array) in (2, 3): @@ -242,6 +239,9 @@ class H5Interface(Interface): """Create an :class:`.H5Proxy` to use throughout validation""" if isinstance(array, H5ArrayPath): array = H5Proxy.from_h5array(h5array=array) + elif isinstance(array, H5Proxy): + # nothing to do, already proxied + pass elif isinstance(array, (tuple, list)) and len(array) == 2: # pragma: no cover array = H5Proxy(file=array[0], path=array[1]) elif isinstance(array, (tuple, list)) and len(array) == 3: diff --git a/src/numpydantic/interface/video.py b/src/numpydantic/interface/video.py index 0ac7c66..f64457b 100644 --- a/src/numpydantic/interface/video.py +++ b/src/numpydantic/interface/video.py @@ -190,7 +190,7 @@ class VideoInterface(Interface): OpenCV interface to treat videos as arrays. """ - input_types = (str, Path, VideoCapture) + input_types = (str, Path, VideoCapture, VideoProxy) return_type = VideoProxy @classmethod @@ -204,7 +204,9 @@ class VideoInterface(Interface): Check if array is a string or Path with a supported video extension, or an opened VideoCapture object """ - if VideoCapture is not None and isinstance(array, VideoCapture): + if (VideoCapture is not None and isinstance(array, VideoCapture)) or isinstance( + array, VideoProxy + ): return True if isinstance(array, str): @@ -220,6 +222,8 @@ class VideoInterface(Interface): """Get a :class:`.VideoProxy` object for this video""" if isinstance(array, VideoCapture): proxy = VideoProxy(video=array) + elif isinstance(array, VideoProxy): + proxy = array else: proxy = VideoProxy(path=array) return proxy diff --git a/tests/test_interface/test_interface.py b/tests/test_interface/test_interface_base.py similarity index 93% rename from tests/test_interface/test_interface.py rename to tests/test_interface/test_interface_base.py index e8fbe85..baacc60 100644 --- a/tests/test_interface/test_interface.py +++ b/tests/test_interface/test_interface_base.py @@ -1,5 +1,11 @@ -import pytest +""" +Tests for the interface base model, +for tests that should apply to all interfaces, use ``test_interfaces.py`` +""" +import gc + +import pytest import numpy as np from numpydantic.interface import Interface @@ -8,6 +14,7 @@ from numpydantic.interface import Interface @pytest.fixture(scope="module") def interfaces(): """Define test interfaces in this module, and delete afterwards""" + interfaces_enabled = True class Interface1(Interface): input_types = (list,) @@ -24,7 +31,7 @@ def interfaces(): @classmethod def enabled(cls) -> bool: - return True + return interfaces_enabled Interface2 = type("Interface2", Interface1.__bases__, dict(Interface1.__dict__)) Interface2.checked = False @@ -44,7 +51,7 @@ def interfaces(): @classmethod def enabled(cls) -> bool: - return True + return interfaces_enabled class Interfaces: interface1 = Interface1 @@ -55,9 +62,13 @@ def interfaces(): yield Interfaces # Interface.__subclasses__().remove(Interface1) # Interface.__subclasses__().remove(Interface2) + del Interfaces del Interface1 del Interface2 del Interface3 + del Interface4 + interfaces_enabled = False + gc.collect() def test_interface_match_error(interfaces): diff --git a/tests/test_interface/test_interfaces.py b/tests/test_interface/test_interfaces.py new file mode 100644 index 0000000..36308c9 --- /dev/null +++ b/tests/test_interface/test_interfaces.py @@ -0,0 +1,12 @@ +""" +Tests that should be applied to all interfaces +""" + + +def test_interface_revalidate(all_interfaces): + """ + An interface should revalidate with the output of its initial validation + + See: https://github.com/p2p-ld/numpydantic/pull/14 + """ + _ = type(all_interfaces)(array=all_interfaces.array)