From 0cbdfb28909fe17d8e99e579a579d580c1883fb8 Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Fri, 13 Dec 2024 15:36:00 -0800 Subject: [PATCH 1/7] add 3.13 to test matrix --- .github/workflows/tests.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f2c8974..881ef98 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,20 +15,26 @@ jobs: matrix: platform: ["ubuntu-latest", "macos-latest", "windows-latest"] numpy-version: ["<2.0.0", ">=2.0.0"] - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] exclude: - numpy-version: "<2.0.0" python-version: "3.10" - numpy-version: "<2.0.0" python-version: "3.11" + - numpy-version: "<2.0.0" + python-version: "3.12" - platform: "macos-latest" python-version: "3.10" - platform: "macos-latest" python-version: "3.11" + - platform: "macos-latest" + python-version: "3.12" - platform: "windows-latest" python-version: "3.10" - platform: "windows-latest" python-version: "3.11" + - platform: "windows-latest" + python-version: "3.12" runs-on: ${{ matrix.platform }} From 8bf22039112e9895c067b2dc89e7a597fe6ad616 Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Fri, 13 Dec 2024 16:36:51 -0800 Subject: [PATCH 2/7] add ability to set marks in test cases, add combinatoric test for json schema generation --- pdm.lock | 2 +- pyproject.toml | 2 ++ src/numpydantic/testing/cases.py | 15 ++++++++++++- src/numpydantic/testing/helpers.py | 28 ++++++++++++++++++++++--- tests/conftest.py | 6 ++++-- tests/test_interface/conftest.py | 12 +++-------- tests/test_interface/test_interfaces.py | 12 +++++++++++ 7 files changed, 61 insertions(+), 16 deletions(-) diff --git a/pdm.lock b/pdm.lock index 8fa42d5..a2d8074 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "arrays", "dask", "dev", "docs", "hdf5", "tests", "video", "zarr"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:cc2b0fb32896c6df0ad747ddb5dee89af22f5c4c4643ee7a52db47fef30da936" +content_hash = "sha256:89ac87e811ecc42bf5117e9c9e4aa6a69011cb7c5c1a630fbfb2643b0045c526" [[metadata.targets]] requires_python = "~=3.9" diff --git a/pyproject.toml b/pyproject.toml index 282183e..36c7220 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,6 +129,8 @@ markers = [ "numpy: numpy interface", "video: video interface", "zarr: zarr interface", + "union: union dtypes", + "pipe_union: union dtypes specified with a pipe", ] [tool.black] diff --git a/src/numpydantic/testing/cases.py b/src/numpydantic/testing/cases.py index d0f44ee..042317d 100644 --- a/src/numpydantic/testing/cases.py +++ b/src/numpydantic/testing/cases.py @@ -143,27 +143,35 @@ DTYPE_CASES = [ dtype=np.uint32, passes=True, id="union-type-uint32", + marks={"union"}, ), ValidationCase( annotation_dtype=UNION_TYPE, dtype=np.float32, passes=True, id="union-type-float32", + marks={"union"}, ), ValidationCase( annotation_dtype=UNION_TYPE, dtype=np.uint64, passes=False, id="union-type-uint64", + marks={"union"}, ), ValidationCase( annotation_dtype=UNION_TYPE, dtype=np.float64, passes=False, id="union-type-float64", + marks={"union"}, ), ValidationCase( - annotation_dtype=UNION_TYPE, dtype=str, passes=False, id="union-type-str" + annotation_dtype=UNION_TYPE, + dtype=str, + passes=False, + id="union-type-str", + marks={"union"}, ), ] """ @@ -181,30 +189,35 @@ if YES_PIPE: dtype=np.uint32, passes=True, id="union-pipe-uint32", + marks={"union", "pipe_union"}, ), ValidationCase( annotation_dtype=UNION_PIPE, dtype=np.float32, passes=True, id="union-pipe-float32", + marks={"union", "pipe_union"}, ), ValidationCase( annotation_dtype=UNION_PIPE, dtype=np.uint64, passes=False, id="union-pipe-uint64", + marks={"union", "pipe_union"}, ), ValidationCase( annotation_dtype=UNION_PIPE, dtype=np.float64, passes=False, id="union-pipe-float64", + marks={"union", "pipe_union"}, ), ValidationCase( annotation_dtype=UNION_PIPE, dtype=str, passes=False, id="union-pipe-str", + marks={"union", "pipe_union"}, ), ] ) diff --git a/src/numpydantic/testing/helpers.py b/src/numpydantic/testing/helpers.py index b337e7d..f2d3c42 100644 --- a/src/numpydantic/testing/helpers.py +++ b/src/numpydantic/testing/helpers.py @@ -4,16 +4,19 @@ from functools import reduce from itertools import product from operator import ior from pathlib import Path -from typing import Generator, List, Literal, Optional, Tuple, Type, Union +from typing import TYPE_CHECKING, Generator, List, Literal, Optional, Tuple, Type, Union import numpy as np -from pydantic import BaseModel, ConfigDict, ValidationError, computed_field +from pydantic import BaseModel, ConfigDict, Field, ValidationError, computed_field from numpydantic import NDArray, Shape from numpydantic.dtype import Float from numpydantic.interface import Interface from numpydantic.types import DtypeType, NDArrayType +if TYPE_CHECKING: + from _pytest.mark.structures import MarkDecorator + class InterfaceCase(ABC): """ @@ -139,6 +142,8 @@ class ValidationCase(BaseModel): """The interface test case to generate and validate the array with""" path: Optional[Path] = None """The path to generate arrays into, if any.""" + marks: set[str] = Field(default_factory=set) + """pytest marks to set for this test case""" model_config = ConfigDict(arbitrary_types_allowed=True) @@ -179,6 +184,19 @@ class ValidationCase(BaseModel): return Model + @property + def pytest_marks(self) -> list["MarkDecorator"]: + """ + Instantiated pytest marks from :attr:`.ValidationCase.marks` + plus the interface name. + """ + import pytest + + marks = self.marks.copy() + if self.interface is not None: + marks.add(self.interface.interface.name) + return [getattr(pytest.mark, m) for m in marks] + def validate_case(self, path: Optional[Path] = None) -> bool: """ Whether the generated array correctly validated against the annotation, @@ -246,7 +264,10 @@ def merge_cases(*args: ValidationCase) -> ValidationCase: return args[0] dumped = [ - m.model_dump(exclude_unset=True, exclude={"model", "annotation"}) for m in args + m.model_dump( + exclude_unset=True, exclude={"model", "annotation", "pytest_marks"} + ) + for m in args ] # self_dump = self.model_dump(exclude_unset=True) @@ -263,6 +284,7 @@ def merge_cases(*args: ValidationCase) -> ValidationCase: merged = reduce(ior, dumped, {}) merged["passes"] = passes merged["id"] = ids + merged["marks"] = set().union(*[v.get("marks", set()) for v in dumped]) return ValidationCase.model_construct(**merged) diff --git a/tests/conftest.py b/tests/conftest.py index 669de7b..54eaa2d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,8 @@ def pytest_addoption(parser): @pytest.fixture( - scope="function", params=[pytest.param(c, id=c.id) for c in SHAPE_CASES] + scope="function", + params=[pytest.param(c, id=c.id, marks=c.pytest_marks) for c in SHAPE_CASES], ) def shape_cases(request, tmp_output_dir_func) -> ValidationCase: case: ValidationCase = request.param.model_copy() @@ -23,7 +24,8 @@ def shape_cases(request, tmp_output_dir_func) -> ValidationCase: @pytest.fixture( - scope="function", params=[pytest.param(c, id=c.id) for c in DTYPE_CASES] + scope="function", + params=[pytest.param(c, id=c.id, marks=c.pytest_marks) for c in DTYPE_CASES], ) def dtype_cases(request, tmp_output_dir_func) -> ValidationCase: case: ValidationCase = request.param.model_copy() diff --git a/tests/test_interface/conftest.py b/tests/test_interface/conftest.py index 0f1048a..6df8cc5 100644 --- a/tests/test_interface/conftest.py +++ b/tests/test_interface/conftest.py @@ -61,10 +61,7 @@ def interface_cases(request) -> InterfaceCase: @pytest.fixture( - params=( - pytest.param(p, id=p.id, marks=getattr(pytest.mark, p.interface.interface.name)) - for p in ALL_CASES - ) + params=(pytest.param(p, id=p.id, marks=p.pytest_marks) for p in ALL_CASES) ) def all_cases(interface_cases, request) -> ValidationCase: """ @@ -83,10 +80,7 @@ def all_cases(interface_cases, request) -> ValidationCase: @pytest.fixture( - params=( - pytest.param(p, id=p.id, marks=getattr(pytest.mark, p.interface.interface.name)) - for p in ALL_CASES_PASSING - ) + params=(pytest.param(p, id=p.id, marks=p.pytest_marks) for p in ALL_CASES_PASSING) ) def all_passing_cases(request) -> ValidationCase: """ @@ -132,7 +126,7 @@ def all_passing_cases_instance(all_passing_cases, tmp_output_dir_func): @pytest.fixture( params=( - pytest.param(p, id=p.id, marks=getattr(pytest.mark, p.interface.interface.name)) + pytest.param(p, id=p.id, marks=p.pytest_marks) for p in DTYPE_AND_INTERFACE_CASES_PASSING ) ) diff --git a/tests/test_interface/test_interfaces.py b/tests/test_interface/test_interfaces.py index a368aee..c77fb69 100644 --- a/tests/test_interface/test_interfaces.py +++ b/tests/test_interface/test_interfaces.py @@ -61,6 +61,18 @@ def test_interface_revalidate(all_passing_cases_instance): _ = type(all_passing_cases_instance)(array=all_passing_cases_instance.array) +@pytest.mark.json_schema +def test_interface_jsonschema(all_passing_cases_instance): + """ + All interfaces should be able to generate json schema + for all combinations of dtype and shape + + Note that this does not test for json schema correctness - + see ndarray tests for that + """ + _ = all_passing_cases_instance.model_json_schema() + + @pytest.mark.xfail def test_interface_rematch(interface_cases, tmp_output_dir_func): """ From a02d9b2d9c09bf2a89c69dbb91da202a38b06a5d Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Fri, 13 Dec 2024 17:12:36 -0800 Subject: [PATCH 3/7] add types to the ndarray type collection key --- src/numpydantic/vendor/nptyping/base_meta_classes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/numpydantic/vendor/nptyping/base_meta_classes.py b/src/numpydantic/vendor/nptyping/base_meta_classes.py index 337e629..76e9e47 100644 --- a/src/numpydantic/vendor/nptyping/base_meta_classes.py +++ b/src/numpydantic/vendor/nptyping/base_meta_classes.py @@ -120,7 +120,7 @@ class SubscriptableMeta(ABCMeta): new type is returned for every unique set of arguments. """ - _all_types: Dict[Tuple[type, Tuple[Any, ...]], type] = {} + _all_types: Dict[Tuple[type, Tuple[Any, ...], tuple[type, ...]], type] = {} _parameterized: bool = False @abstractmethod @@ -160,7 +160,7 @@ class SubscriptableMeta(ABCMeta): def _create_type( cls, args: Tuple[Any, ...], additional_values: Dict[str, Any] ) -> type: - key = (cls, args) + key = (cls, args, tuple(type(a) for a in args)) if key not in cls._all_types: cls._all_types[key] = type( cls.__name__, From 47d8039819b56bde14e47bae5710f49eda6ac050 Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Fri, 13 Dec 2024 17:17:47 -0800 Subject: [PATCH 4/7] simple enough fix - handle `typing.UnionType` in additional `dtype` metadata in json schema --- src/numpydantic/ndarray.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/numpydantic/ndarray.py b/src/numpydantic/ndarray.py index fa0fdc9..989dc58 100644 --- a/src/numpydantic/ndarray.py +++ b/src/numpydantic/ndarray.py @@ -204,9 +204,15 @@ class NDArray(NPTypingType, metaclass=NDArrayMeta): json_schema = handler(schema["metadata"]) json_schema = handler.resolve_ref_schema(json_schema) - if not isinstance(dtype, tuple) and dtype.__module__ not in ( - "builtins", - "typing", + if ( + not isinstance(dtype, tuple) + and dtype.__module__ + not in ( + "builtins", + "typing", + "types", + ) + and hasattr(dtype, "__name__") ): json_schema["dtype"] = ".".join([dtype.__module__, dtype.__name__]) From 0bd6c8809a1264c86d146af3582c1f19c14f1746 Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Fri, 13 Dec 2024 17:24:13 -0800 Subject: [PATCH 5/7] changelog --- docs/changelog.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 889c969..0deef0b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,18 @@ # Changelog +## Upcoming + +**Bugfix** +- [#38](https://github.com/p2p-ld/numpydantic/issues/38), [#39](https://github.com/p2p-ld/numpydantic/pull/39) - + - JSON Schema generation failed when the `dtype` was embedded from dtypes that lack a `__name__` attribute. + An additional check was added for presence of `__name__` when embedding. + - `NDArray` types were incorrectly cached s.t. pipe-union dtypes were considered equivalent to `Union[]` + dtypes. An additional tuple with the type of the args was added to the cache key to disambiguate them. + +**Testing** +- [#39](https://github.com/p2p-ld/numpydantic/pull/39) - Test that all combinations of shapes, dtypes, and interfaces + can generate JSON schema. + ## 1.* ### 1.6.* From 16afdca672020ddcfd7663dc83b28ac87ec6c487 Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Fri, 13 Dec 2024 17:24:48 -0800 Subject: [PATCH 6/7] changelog --- docs/changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.md b/docs/changelog.md index 0deef0b..92ee1c9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -12,6 +12,7 @@ **Testing** - [#39](https://github.com/p2p-ld/numpydantic/pull/39) - Test that all combinations of shapes, dtypes, and interfaces can generate JSON schema. +- [#39](https://github.com/p2p-ld/numpydantic/pull/39) - Add python 3.13 to the testing matrix. ## 1.* From a7b8b5f3d3125add58d60c0314f8591f868178d9 Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Fri, 13 Dec 2024 17:28:59 -0800 Subject: [PATCH 7/7] changelog & only test numpy <2 on 3.12 --- .github/workflows/tests.yml | 4 +++- docs/changelog.md | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 881ef98..d6ce738 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,8 +21,10 @@ jobs: python-version: "3.10" - numpy-version: "<2.0.0" python-version: "3.11" + # let's call python 3.12 the last version we're going to support with numpy <2 + # they don't provide wheels for <2 in 3.13 and beyond. - numpy-version: "<2.0.0" - python-version: "3.12" + python-version: "3.13" - platform: "macos-latest" python-version: "3.10" - platform: "macos-latest" diff --git a/docs/changelog.md b/docs/changelog.md index 92ee1c9..e2ddc40 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -13,6 +13,8 @@ - [#39](https://github.com/p2p-ld/numpydantic/pull/39) - Test that all combinations of shapes, dtypes, and interfaces can generate JSON schema. - [#39](https://github.com/p2p-ld/numpydantic/pull/39) - Add python 3.13 to the testing matrix. +- [#39](https://github.com/p2p-ld/numpydantic/pull/39) - Add an additional `marks` field to ValidationCase + for finer-grained control over running tests. ## 1.*