diff --git a/pixi.toml b/pixi.toml index b75ff1ae8..08c92f64d 100644 --- a/pixi.toml +++ b/pixi.toml @@ -55,7 +55,7 @@ tqdm = "4.50.*" xarray = "2025.8.*" pandas = "2.2.*" pyarrow = "20.0.*" -uxarray = "2026.04.1" +uxarray = "==2026.04.1" dask = "2024.6.*" zarr = "3.0.*" xgcm = { version = "0.9.*", channel = "conda-forge" } diff --git a/tests/test_datasets.py b/tests/datasets/test_structured.py similarity index 100% rename from tests/test_datasets.py rename to tests/datasets/test_structured.py diff --git a/tests/strategies/__init__.py b/tests/strategies/__init__.py new file mode 100644 index 000000000..5a8e17c88 --- /dev/null +++ b/tests/strategies/__init__.py @@ -0,0 +1,3 @@ +from . import sgrid, time + +__all__ = ["sgrid", "time"] diff --git a/tests/strategies/time.py b/tests/strategies/time.py new file mode 100644 index 000000000..ba398a57d --- /dev/null +++ b/tests/strategies/time.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from datetime import datetime + +import numpy as np +from cftime import datetime as cftime_datetime +from hypothesis import strategies as st + +from parcels._core.utils.time import ( + TimeInterval, +) + +cf_calendar = st.sampled_from( + [ + "gregorian", + "proleptic_gregorian", + "365_day", + "360_day", + "julian", + "366_day", + np.datetime64, + datetime, + np.timedelta64, + ] +) + + +@st.composite +def np_timedelta64(draw): + """Strategy for generating np.timedelta64 objects.""" + return np.timedelta64(draw(st.integers(1, 60 * 60 * 24 * 100 * 365)), "s") + + +@st.composite +def datetime_various(draw, calendar=None): + if calendar is None: + calendar = draw(cf_calendar) + if calendar is np.timedelta64: + return draw(np_timedelta64()) + + year = draw(st.integers(1900, 2100)) + month = draw(st.integers(1, 12)) + day = draw(st.integers(1, 28)) + if calendar is datetime: + return datetime(year, month, day) + if calendar is np.datetime64: + return np.datetime64(datetime(year, month, day)) + + return cftime_datetime(year, month, day, calendar=calendar) + + +@st.composite +def time_interval(draw, left=None, calendar=None): + if left is None: + left = draw(datetime_various(calendar=calendar)) + right = left + draw(np_timedelta64()) + + return TimeInterval(left, right) diff --git a/tests/utils/test_sgrid.py b/tests/utils/test_sgrid.py index 78339948f..d48cb1253 100644 --- a/tests/utils/test_sgrid.py +++ b/tests/utils/test_sgrid.py @@ -6,8 +6,8 @@ import xgcm from hypothesis import assume, example, given +import tests.strategies as pst from parcels._core.utils import sgrid -from tests.strategies import sgrid as sgrid_strategies def create_example_grid2dmetadata(with_vertical_dimensions: bool, with_node_coordinates: bool): @@ -183,7 +183,7 @@ def dummy_comodo_3d_ds() -> xr.Dataset: sgrid.FaceNodePadding("edge2", "node2", sgrid.Padding.LOW), ) ) -@given(sgrid_strategies.mappings) +@given(pst.sgrid.mappings) def test_edge_node_mapping_metadata_roundtrip(edge_node_padding): serialized = sgrid.dump_mappings(edge_node_padding) parsed = sgrid.load_mappings(serialized) @@ -204,7 +204,7 @@ def test_load_dump_mappings(input_, expected): @example(grid2dmetadata) -@given(sgrid_strategies.grid2Dmetadata()) +@given(pst.sgrid.grid2Dmetadata()) def test_Grid2DMetadata_roundtrip(grid: sgrid.Grid2DMetadata): attrs = grid.to_attrs() parsed = sgrid.Grid2DMetadata.from_attrs(attrs) @@ -212,14 +212,14 @@ def test_Grid2DMetadata_roundtrip(grid: sgrid.Grid2DMetadata): @example(grid3dmetadata) -@given(sgrid_strategies.grid3Dmetadata()) +@given(pst.sgrid.grid3Dmetadata()) def test_Grid3DMetadata_roundtrip(grid: sgrid.Grid3DMetadata): attrs = grid.to_attrs() parsed = sgrid.Grid3DMetadata.from_attrs(attrs) assert parsed == grid -@given(sgrid_strategies.grid_metadata) +@given(pst.sgrid.grid_metadata) def test_parse_grid_attrs(grid: sgrid.AttrsSerializable): attrs = grid.to_attrs() parsed = sgrid.parse_grid_attrs(attrs) @@ -227,7 +227,7 @@ def test_parse_grid_attrs(grid: sgrid.AttrsSerializable): @example(grid2dmetadata) -@given(sgrid_strategies.grid2Dmetadata()) +@given(pst.sgrid.grid2Dmetadata()) def test_parse_sgrid_2d(grid_metadata: sgrid.Grid2DMetadata): """Test the ingestion of datasets in XGCM to ensure that it matches the SGRID metadata provided""" ds = dummy_sgrid_2d_ds(grid_metadata) @@ -249,7 +249,7 @@ def test_parse_sgrid_2d(grid_metadata: sgrid.Grid2DMetadata): assert coords[sgrid.SGRID_PADDING_TO_XGCM_POSITION[obj.padding]] == obj.node -@given(sgrid_strategies.grid3Dmetadata()) +@given(pst.sgrid.grid3Dmetadata()) def test_parse_sgrid_3d(grid_metadata: sgrid.Grid3DMetadata): """Test the ingestion of datasets in XGCM to ensure that it matches the SGRID metadata provided""" ds = dummy_sgrid_3d_ds(grid_metadata) diff --git a/tests/utils/test_time.py b/tests/utils/test_time.py index 26cc39c1c..67d7a4f8e 100644 --- a/tests/utils/test_time.py +++ b/tests/utils/test_time.py @@ -6,8 +6,8 @@ import pytest from cftime import datetime as cftime_datetime from hypothesis import given -from hypothesis import strategies as st +import tests.strategies as pst # parcels strategies from parcels._core.utils.time import ( TimeInterval, _get_cf_attrs, @@ -15,53 +15,6 @@ timedelta_to_float, ) -calendar_strategy = st.sampled_from( - [ - "gregorian", - "proleptic_gregorian", - "365_day", - "360_day", - "julian", - "366_day", - np.datetime64, - datetime, - np.timedelta64, - ] -) - - -@st.composite -def np_timedelta64_strategy(draw): - """Strategy for generating np.timedelta64 objects.""" - return np.timedelta64(draw(st.integers(1, 60 * 60 * 24 * 100 * 365)), "s") - - -@st.composite -def datetime_strategy(draw, calendar=None): - if calendar is None: - calendar = draw(calendar_strategy) - if calendar is np.timedelta64: - return draw(np_timedelta64_strategy()) - - year = draw(st.integers(1900, 2100)) - month = draw(st.integers(1, 12)) - day = draw(st.integers(1, 28)) - if calendar is datetime: - return datetime(year, month, day) - if calendar is np.datetime64: - return np.datetime64(datetime(year, month, day)) - - return cftime_datetime(year, month, day, calendar=calendar) - - -@st.composite -def time_interval_strategy(draw, left=None, calendar=None): - if left is None: - left = draw(datetime_strategy(calendar=calendar)) - right = left + draw(np_timedelta64_strategy()) - - return TimeInterval(left, right) - @pytest.mark.parametrize( "left,right", @@ -83,7 +36,7 @@ def test_time_interval_initialization(left, right): TimeInterval(right, left) -@given(time_interval_strategy()) +@given(pst.time.time_interval()) def test_time_interval_contains(interval): left = 0 right = timedelta_to_float(interval.right - interval.left) @@ -94,12 +47,12 @@ def test_time_interval_contains(interval): assert interval.is_all_time_in_interval(middle) -@given(time_interval_strategy(calendar="365_day"), time_interval_strategy(calendar="365_day")) +@given(pst.time.time_interval(calendar="365_day"), pst.time.time_interval(calendar="365_day")) def test_time_interval_intersection_commutative(interval1, interval2): assert interval1.intersection(interval2) == interval2.intersection(interval1) -@given(time_interval_strategy()) +@given(pst.time.time_interval()) def test_time_interval_intersection_with_self(interval): assert interval.intersection(interval) == interval @@ -111,7 +64,7 @@ def test_time_interval_repr(): assert repr(interval) == expected -@given(time_interval_strategy()) +@given(pst.time.time_interval()) def test_time_interval_equality(interval): assert interval == interval @@ -222,7 +175,7 @@ def test_timedelta_to_float_exceptions(): timedelta_to_float("invalid_type") -@given(datetime_strategy()) +@given(pst.time.datetime_various()) def test_datetime_get_cf_attrs(dt): attrs = _get_cf_attrs(dt) assert "seconds" in attrs["units"]