Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
File renamed without changes.
3 changes: 3 additions & 0 deletions tests/strategies/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import sgrid, time

__all__ = ["sgrid", "time"]
58 changes: 58 additions & 0 deletions tests/strategies/time.py
Original file line number Diff line number Diff line change
@@ -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)
14 changes: 7 additions & 7 deletions tests/utils/test_sgrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -204,30 +204,30 @@ 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)
assert parsed == grid


@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)
assert parsed == grid


@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)
Expand All @@ -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)
Expand Down
59 changes: 6 additions & 53 deletions tests/utils/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,62 +6,15 @@
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

@VeckoTheGecko VeckoTheGecko Apr 28, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import alias is common convention for Hypothesis (and packages that provide hypothesis strategies).

import hypothesis.strategies as st
import xarray.testing.strategies as xrst

# and here
import tests.strategies as pst # <-- parcels strategies

from parcels._core.utils.time import (
TimeInterval,
_get_cf_attrs,
maybe_convert_python_timedelta_to_numpy,
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",
Expand All @@ -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)
Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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"]
Loading