Merge pull request #14 from p2p-ld/bugfix-revalidation

[bugfix] Revalidate with already-validated/proxied array
This commit is contained in:
Jonny Saunders 2024-09-03 13:20:33 -07:00 committed by GitHub
commit c46015d306
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 68 additions and 23 deletions

View file

@ -2,7 +2,17 @@
## 1.* ## 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, 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. 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' 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]`, 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, 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, for all input types, for all dtype and all shape cases,
but for now numpydantic is fast enough that this doesn't matter <3. 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: HDF5 can have compound dtypes like:
@ -107,7 +119,9 @@ my_model = MyModel(
np.dtype('int64') 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. 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. 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) (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 Previously we would only allow dtypes if we knew for sure that there was some
python base type to generate a schema with. 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 interfaces can override if there is some fancy way they have of getting
types/items from an object array. 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: API Changes:
- Split apart the validation methods into smaller chunks to better support - 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 - [#4](https://github.com/p2p-ld/numpydantic/issues/4) - Support dtype checking
for strings in zarr and numpy arrays 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` vendored into `numpydantic.vendor.nptyping` -
`nptyping` is no longer maintained, and pins `numpy<2`. `nptyping` is no longer maintained, and pins `numpy<2`.
@ -221,22 +237,24 @@ Tidying:
- Remove `monkeypatch` module! we don't need it anymore! - Remove `monkeypatch` module! we don't need it anymore!
everything has either been upstreamed or vendored. 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 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 Fix a minor bug where {class}`~numpydantic.exceptions.DtypeError` would not cause
pydantic to throw a {class}`pydantic.ValidationError` because custom validator functions pydantic to throw a {class}`pydantic.ValidationError` because custom validator functions
need to raise either `AssertionError` or `ValueError` - made `DtypeError` also need to raise either `AssertionError` or `ValueError` - made `DtypeError` also
inherit from `ValueError` because that is also technically true. 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) - 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 https://github.com/p2p-ld/numpydantic/pull/1

View file

@ -1,6 +1,6 @@
[project] [project]
name = "numpydantic" name = "numpydantic"
version = "1.5.0" version = "1.5.1"
description = "Type and shape validation and serialization for arbitrary array types in pydantic models" description = "Type and shape validation and serialization for arbitrary array types in pydantic models"
authors = [ authors = [
{name = "sneakers-the-rat", email = "sneakers-the-rat@protonmail.com"}, {name = "sneakers-the-rat", email = "sneakers-the-rat@protonmail.com"},

View file

@ -194,10 +194,7 @@ class H5Interface(Interface):
passthrough numpy-like interface to the dataset. passthrough numpy-like interface to the dataset.
""" """
input_types = ( input_types = (H5ArrayPath, H5Arraylike, H5Proxy)
H5ArrayPath,
H5Arraylike,
)
return_type = H5Proxy return_type = H5Proxy
@classmethod @classmethod
@ -211,7 +208,7 @@ class H5Interface(Interface):
Check that the given array is a :class:`.H5ArrayPath` or something that Check that the given array is a :class:`.H5ArrayPath` or something that
resembles one. resembles one.
""" """
if isinstance(array, H5ArrayPath): if isinstance(array, (H5ArrayPath, H5Proxy)):
return True return True
if isinstance(array, (tuple, list)) and len(array) in (2, 3): 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""" """Create an :class:`.H5Proxy` to use throughout validation"""
if isinstance(array, H5ArrayPath): if isinstance(array, H5ArrayPath):
array = H5Proxy.from_h5array(h5array=array) 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 elif isinstance(array, (tuple, list)) and len(array) == 2: # pragma: no cover
array = H5Proxy(file=array[0], path=array[1]) array = H5Proxy(file=array[0], path=array[1])
elif isinstance(array, (tuple, list)) and len(array) == 3: elif isinstance(array, (tuple, list)) and len(array) == 3:

View file

@ -190,7 +190,7 @@ class VideoInterface(Interface):
OpenCV interface to treat videos as arrays. OpenCV interface to treat videos as arrays.
""" """
input_types = (str, Path, VideoCapture) input_types = (str, Path, VideoCapture, VideoProxy)
return_type = VideoProxy return_type = VideoProxy
@classmethod @classmethod
@ -204,7 +204,9 @@ class VideoInterface(Interface):
Check if array is a string or Path with a supported video extension, Check if array is a string or Path with a supported video extension,
or an opened VideoCapture object 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 return True
if isinstance(array, str): if isinstance(array, str):
@ -220,6 +222,8 @@ class VideoInterface(Interface):
"""Get a :class:`.VideoProxy` object for this video""" """Get a :class:`.VideoProxy` object for this video"""
if isinstance(array, VideoCapture): if isinstance(array, VideoCapture):
proxy = VideoProxy(video=array) proxy = VideoProxy(video=array)
elif isinstance(array, VideoProxy):
proxy = array
else: else:
proxy = VideoProxy(path=array) proxy = VideoProxy(path=array)
return proxy return proxy

View file

@ -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 import numpy as np
from numpydantic.interface import Interface from numpydantic.interface import Interface
@ -8,6 +14,7 @@ from numpydantic.interface import Interface
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def interfaces(): def interfaces():
"""Define test interfaces in this module, and delete afterwards""" """Define test interfaces in this module, and delete afterwards"""
interfaces_enabled = True
class Interface1(Interface): class Interface1(Interface):
input_types = (list,) input_types = (list,)
@ -24,7 +31,7 @@ def interfaces():
@classmethod @classmethod
def enabled(cls) -> bool: def enabled(cls) -> bool:
return True return interfaces_enabled
Interface2 = type("Interface2", Interface1.__bases__, dict(Interface1.__dict__)) Interface2 = type("Interface2", Interface1.__bases__, dict(Interface1.__dict__))
Interface2.checked = False Interface2.checked = False
@ -44,7 +51,7 @@ def interfaces():
@classmethod @classmethod
def enabled(cls) -> bool: def enabled(cls) -> bool:
return True return interfaces_enabled
class Interfaces: class Interfaces:
interface1 = Interface1 interface1 = Interface1
@ -55,9 +62,13 @@ def interfaces():
yield Interfaces yield Interfaces
# Interface.__subclasses__().remove(Interface1) # Interface.__subclasses__().remove(Interface1)
# Interface.__subclasses__().remove(Interface2) # Interface.__subclasses__().remove(Interface2)
del Interfaces
del Interface1 del Interface1
del Interface2 del Interface2
del Interface3 del Interface3
del Interface4
interfaces_enabled = False
gc.collect()
def test_interface_match_error(interfaces): def test_interface_match_error(interfaces):

View file

@ -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)