From c15a8449062e9e24bffc03942664e4d5f74bbbc4 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:34:10 +0800 Subject: [PATCH 1/9] Remove netCDF4 comment --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9fdc121af..5a5e19d1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "cftime >=1.6.3", "numpy >=2.1.0", "dask >=2024.5.1", - "netCDF4 >=1.7.2", # TODO: should we use h5netcdf here instead of netCDF4? + "netCDF4 >=1.7.2", "zarr >=3", "tqdm >=4.50.0", "xarray >=2024.5.0", From dcdb0e4a06ac75e58e55dcf45d0695217b789c88 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:39:03 +0800 Subject: [PATCH 2/9] Remove comment about `pytest docs/examples` This folder of scripts doesnt exist in v4 --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5a5e19d1d..55fcbb45e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,6 @@ local_scheme = "no-local-version" [tool.pytest.ini_options] addopts = ["--strict-config", "--strict-markers"] xfail_strict = true -# testpaths = ["tests", "docs/examples"] # TODO v4: Re-enable once examples are back/fixed testpaths = ["tests"] python_files = ["test_*.py", "example_*.py", "*tutorial*"] minversion = "7" From 9ec86847b4a15daa004047d766daa96101d1d669 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:47:22 +0800 Subject: [PATCH 3/9] Move test_reprs.py to v4 test suite --- pixi.toml | 3 ++ tests-v3/test_reprs.py | 115 ----------------------------------------- tests/test_reprs.py | 36 +++++++++++++ 3 files changed, 39 insertions(+), 115 deletions(-) delete mode 100644 tests-v3/test_reprs.py create mode 100644 tests/test_reprs.py diff --git a/pixi.toml b/pixi.toml index a87fff64f..f9f3e5706 100644 --- a/pixi.toml +++ b/pixi.toml @@ -83,6 +83,9 @@ pytest-cov = "*" pytest-reportlog = "*" pytest-timeout = "*" +[feature.test.pypi-dependencies] +re-assert = "*" + [feature.test.tasks] tests = { cmd = "pytest", description = "Run the test suite." } tests-flaky = { cmd = "pytest -m 'flaky' --run-flaky-tests", description = "Run only flaky tests." } diff --git a/tests-v3/test_reprs.py b/tests-v3/test_reprs.py deleted file mode 100644 index 18407e583..000000000 --- a/tests-v3/test_reprs.py +++ /dev/null @@ -1,115 +0,0 @@ -import re -from datetime import timedelta -from typing import Any - -import numpy as np - -import parcels -from parcels import Grid, ParticleFile, Variable -from parcels.grid import RectilinearGrid -from tests.utils import create_fieldset_unit_mesh, create_simple_pset - - -def assert_simple_repr(class_: type, kwargs: dict[str, Any]): - """Test that the repr of an object contains all the arguments. This only works for objects where the repr matches the calling signature.""" - obj = class_(**kwargs) - obj_repr = repr(obj) - - for param in kwargs.keys(): - assert param in obj_repr - # skip `assert repr(value) in obj_repr` as this is not always true if init does processing on the value - assert class_.__name__ in obj_repr - - -def valid_indentation(s: str) -> bool: - """Make sure that all lines in string is indented with a multiple of 4 spaces.""" - if s.startswith(" "): - return False - - lines = s.split("\n") - for line in lines: - line = re.sub("^( {4})+", "", line) - if line.startswith(" "): - return False - return True - - -def test_check_indentation(): - valid = """ -test - test -test - test - test - test""" - assert valid_indentation(valid) - invalid = """ -test - test - invalid! -""" - assert not valid_indentation(invalid) - - -def test_particletype_repr(): - kwargs = dict(pclass=parcels.Particle) - assert_simple_repr(parcels.particle.ParticleType, kwargs) - - -def test_grid_repr(): - """Test arguments are in the repr of a Grid object""" - kwargs = dict(lon=np.array([1, 2, 3]), lat=np.array([4, 5, 6]), time=None, mesh="spherical") - assert_simple_repr(Grid, kwargs) - - -def test_variable_repr(): - """Test arguments are in the repr of the Variable object.""" - kwargs = dict(name="test", dtype=np.float32, initial=0, to_write=False) - assert_simple_repr(Variable, kwargs) - - -def test_rectilineargrid_repr(): - """ - Test arguments are in the repr of a RectilinearGrid object. - - Mainly to test inherited repr is correct. - """ - kwargs = dict(lon=np.array([1, 2, 3]), lat=np.array([4, 5, 6]), time=None, mesh="spherical") - assert_simple_repr(RectilinearGrid, kwargs) - - -def test_particlefile_repr(): - pset = create_simple_pset() - kwargs = dict( - name="file.zarr", particleset=pset, outputdt=timedelta(hours=1), chunks=None, create_new_zarrfile=False - ) - assert_simple_repr(ParticleFile, kwargs) - - -def test_field_repr(): - field = create_fieldset_unit_mesh().U - assert valid_indentation(repr(field)) - - -def test_vectorfield_repr(): - field = create_fieldset_unit_mesh().UV - assert isinstance(field, parcels.VectorField) - assert valid_indentation(repr(field)) - - -def test_fieldset_repr(): - fieldset = create_fieldset_unit_mesh() - assert valid_indentation(repr(fieldset)) - - -def test_particleset_repr(): - pset = create_simple_pset() - valid_indentation(repr(pset)) - - pset = create_simple_pset(n=15) - valid_indentation(repr(pset)) - - -def capture(s): - with open("file.txt", "a") as f: - f.write(s) diff --git a/tests/test_reprs.py b/tests/test_reprs.py new file mode 100644 index 000000000..04679addf --- /dev/null +++ b/tests/test_reprs.py @@ -0,0 +1,36 @@ +import numpy as np +from re_assert import Matches + +from parcels import Particle, ParticleFile, ParticleSet + + +def test_particlefile_repr(tmp_parquet): + pfile_repr = repr(ParticleFile(tmp_parquet, outputdt=np.timedelta64(1, "s"))) + match = Matches( + r"""\ + path : .* + outputdt : 1.0 + metadata : .*""", + ) + match.assert_matches(pfile_repr) + # assert_simple_repr(ParticleFile, kwargs) + + +def test_field_repr(fieldset): + Matches(r"Field\(name=.*, model=.*\)").assert_matches(repr(fieldset.U)) + + +def test_vectorfield_repr(fieldset): + Matches(r"\<.*VectorField object at.*\>").assert_matches(repr(fieldset.UV)) + + +def test_xgrid_repr(fieldset): + Matches(r"\<.*XGrid object at.*\>").assert_matches(repr(fieldset.U.grid)) + + +def test_fieldset_repr(fieldset): + Matches(r"\<.*FieldSet object at.*\>").assert_matches(repr(fieldset)) + + +def test_particleset_repr(fieldset): + Matches(r"\<.*ParticleSet object at.*\>").assert_matches(repr(ParticleSet(fieldset, pclass=Particle))) From c9b502ff0fc3f753bdcd809e94b541f365544074 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:48:44 +0800 Subject: [PATCH 4/9] Small fixes to reprs --- src/parcels/_core/field.py | 2 +- src/parcels/_repr_utils.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/parcels/_core/field.py b/src/parcels/_core/field.py index 888cdcca3..ded5993ed 100644 --- a/src/parcels/_core/field.py +++ b/src/parcels/_core/field.py @@ -116,7 +116,7 @@ def time_interval(self): # TODO PR: Remove in favour of referencing model time_ return self.model.time_interval def __repr__(self): - return f"Field(name={self.name}, model={self.model})" + return f"Field(name={self.name!r}, model={self.model})" @property def interp_method(self): diff --git a/src/parcels/_repr_utils.py b/src/parcels/_repr_utils.py index 9e2c828c5..a64dc103a 100644 --- a/src/parcels/_repr_utils.py +++ b/src/parcels/_repr_utils.py @@ -138,8 +138,7 @@ def particlefile_repr(pfile: Any) -> str: out = f"""<{type(pfile).__name__}> path : {pfile.path} outputdt : {pfile.outputdt!r} - metadata : -{_format_list_items_multiline(pfile.metadata, level=2, with_brackets=False)} + metadata : {_format_list_items_multiline(pfile.metadata, level=2, with_brackets=False)} """ return textwrap.dedent(out).strip() From c8cb78051987ad1af69870478359349e47791e5e Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:49:01 +0800 Subject: [PATCH 5/9] Remove mentions of legacy folders --- docs/conf.py | 1 - pyproject.toml | 5 ----- 2 files changed, 6 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index feb8b5f4c..75852a08e 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -121,7 +121,6 @@ "_build", "jupyter_execute", "**.ipynb_checkpoints", - "user_guide/examples_v3", ".jupyter_cache", ] diff --git a/pyproject.toml b/pyproject.toml index 55fcbb45e..9b2cbbdf0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,11 +100,6 @@ select = [ "NPY201", # numpy 2 deprecations ] -exclude = [ - "tests-v3/**", - 'docs/user_guide/examples_v3/**', -] # TODO v4: Remove once folders are gone - ignore = [ # # Rules intentionally excluded # line too long (82 > 79 characters) From a2fb915a0fb6d292f7dae7aaae1a9e91a194be72 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:04:31 +0800 Subject: [PATCH 6/9] Remove outdated comment This now works with xdim=1 and ydim=1 (when it comes to ingestion of SGRID, and when testing alongside `test_moving_eddy`). xref https://github.com/Parcels-code/Parcels/pull/2135/changes#r2256749529 --- src/parcels/_datasets/structured/generated.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parcels/_datasets/structured/generated.py b/src/parcels/_datasets/structured/generated.py index 7eba61f55..05ad96dd6 100644 --- a/src/parcels/_datasets/structured/generated.py +++ b/src/parcels/_datasets/structured/generated.py @@ -91,7 +91,7 @@ def radial_rotation_dataset(xdim=200, ydim=200): # Define 2D flat, square field ) -def moving_eddy_dataset(xdim=2, ydim=2): # TODO check if this also works with xdim=1, ydim=1 +def moving_eddy_dataset(xdim=2, ydim=2): """Create a dataset with an eddy moving in time. Note that there is no spatial variation in the flow.""" f, u_0, u_g = 1.0e-4, 0.3, 0.04 # Some constants From 2328eac7da93387dcf9cf8fd6ddc4ab7b110f601 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:07:41 +0800 Subject: [PATCH 7/9] Remove outdated comment --- src/parcels/_repr_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/parcels/_repr_utils.py b/src/parcels/_repr_utils.py index a64dc103a..1386761dc 100644 --- a/src/parcels/_repr_utils.py +++ b/src/parcels/_repr_utils.py @@ -44,7 +44,6 @@ def fieldset_repr(fieldset: FieldSet) -> str: return textwrap.dedent(out).strip() -# TODO add land_value here after HG #2451 is merged def field_repr(field: Field, level: int = 0) -> str: """Return a pretty repr for Field""" with xr.set_options(display_expand_data=False): From 920d457251b0fdbf775bcded93797cdf16215b3d Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:07:48 +0800 Subject: [PATCH 8/9] Update TODO comment --- src/parcels/_typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parcels/_typing.py b/src/parcels/_typing.py index dd98afc44..7f1540e82 100644 --- a/src/parcels/_typing.py +++ b/src/parcels/_typing.py @@ -60,7 +60,7 @@ def _validate_against_pure_literal(value, typing_literal): Can't be used with ``Literal[...] | None`` etc. as its not a pure literal. """ - # TODO remove once https://github.com/pydata/xarray/issues/11209 is resolved - Xarray objects don't work normally in `in` statements + # Xarray objects don't work normally in `in` statements - see https://github.com/pydata/xarray/issues/11209 (this is unlikely to be resolved anytime soon) if _is_xarray_object(value): raise ValueError(f"Invalid input type {type(value)}") From b04db584abb56ed310b13432fe6e51f883db70e0 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:15:58 +0800 Subject: [PATCH 9/9] Split test_reprs.py into respective files --- tests/test_field.py | 9 +++++++++ tests/test_fieldset.py | 5 +++++ tests/test_particlefile.py | 15 ++++++++++++++- tests/test_particleset.py | 5 +++++ tests/test_reprs.py | 36 ------------------------------------ tests/test_xgrid.py | 5 +++++ 6 files changed, 38 insertions(+), 37 deletions(-) delete mode 100644 tests/test_reprs.py diff --git a/tests/test_field.py b/tests/test_field.py index 3050aba89..924335a86 100644 --- a/tests/test_field.py +++ b/tests/test_field.py @@ -2,6 +2,7 @@ import numpy as np import pytest +from re_assert import Matches from parcels import Field, VectorField from parcels._core.fieldset import FieldSet @@ -39,6 +40,14 @@ def test_field_init_param_types(): Field(name="while", model=model) +def test_field_repr(fieldset): + Matches(r"Field\(name=.*, model=.*\)").assert_matches(repr(fieldset.U)) + + +def test_vectorfield_repr(fieldset): + Matches(r"\<.*VectorField object at.*\>").assert_matches(repr(fieldset.UV)) + + # TODO: Move to test_model.py ? def test_field_init_fail_on_float_time_dim(): """Test that accessing time_interval fails when dataset has float time dimension. diff --git a/tests/test_fieldset.py b/tests/test_fieldset.py index b1b6b16f6..f83f8b126 100644 --- a/tests/test_fieldset.py +++ b/tests/test_fieldset.py @@ -7,6 +7,7 @@ import pandas as pd import pytest import xarray as xr +from re_assert import Matches import parcels.tutorial import tests @@ -40,6 +41,10 @@ def test_fieldset_init_wrong_types(): FieldSet([1.0, 2.0, 3.0]) +def test_fieldset_repr(fieldset): + Matches(r"\<.*FieldSet object at.*\>").assert_matches(repr(fieldset)) + + def test_fieldset_add_context(fieldset): fieldset.add_context("test_context", 1.0) assert fieldset.test_context == 1.0 diff --git a/tests/test_particlefile.py b/tests/test_particlefile.py index 38645de8d..39646a51d 100755 --- a/tests/test_particlefile.py +++ b/tests/test_particlefile.py @@ -9,11 +9,13 @@ import pyarrow.parquet as pq import pytest import xarray as xr +from re_assert import Matches import parcels.tutorial from parcels import ( Field, FieldSet, + Particle, ParticleFile, ParticleSet, ParticleSetWarning, @@ -21,7 +23,7 @@ Variable, convert, ) -from parcels._core.particle import Particle, get_default_particle +from parcels._core.particle import get_default_particle from parcels._core.particlefile import get_schema from parcels._core.utils.time import TimeInterval, timedelta_to_float from parcels._datasets.structured.generated import peninsula_dataset @@ -30,6 +32,17 @@ from tests.common_kernels import DoNothing +def test_particlefile_repr(tmp_parquet): + pfile_repr = repr(ParticleFile(tmp_parquet, outputdt=np.timedelta64(1, "s"))) + match = Matches( + r"""\ + path : .* + outputdt : 1.0 + metadata : .*""", + ) + match.assert_matches(pfile_repr) + + def test_metadata(fieldset, tmp_parquet): pset = ParticleSet(fieldset, pclass=Particle, x=0, y=0) diff --git a/tests/test_particleset.py b/tests/test_particleset.py index d0721b7ed..5166c6c8c 100644 --- a/tests/test_particleset.py +++ b/tests/test_particleset.py @@ -5,6 +5,7 @@ import numpy as np import pytest import xarray as xr +from re_assert import Matches from parcels import ( FieldSet, @@ -35,6 +36,10 @@ def test_create_empty_pset(fieldset): assert pset.size == 0 +def test_particleset_repr(fieldset): + Matches(r"\<.*ParticleSet object at.*\>").assert_matches(repr(ParticleSet(fieldset, pclass=Particle))) + + @pytest.mark.parametrize("offset", [0, 1, 200]) def test_pset_with_pids(fieldset, offset, npart=100): lon = np.linspace(0, 1, npart) diff --git a/tests/test_reprs.py b/tests/test_reprs.py deleted file mode 100644 index 04679addf..000000000 --- a/tests/test_reprs.py +++ /dev/null @@ -1,36 +0,0 @@ -import numpy as np -from re_assert import Matches - -from parcels import Particle, ParticleFile, ParticleSet - - -def test_particlefile_repr(tmp_parquet): - pfile_repr = repr(ParticleFile(tmp_parquet, outputdt=np.timedelta64(1, "s"))) - match = Matches( - r"""\ - path : .* - outputdt : 1.0 - metadata : .*""", - ) - match.assert_matches(pfile_repr) - # assert_simple_repr(ParticleFile, kwargs) - - -def test_field_repr(fieldset): - Matches(r"Field\(name=.*, model=.*\)").assert_matches(repr(fieldset.U)) - - -def test_vectorfield_repr(fieldset): - Matches(r"\<.*VectorField object at.*\>").assert_matches(repr(fieldset.UV)) - - -def test_xgrid_repr(fieldset): - Matches(r"\<.*XGrid object at.*\>").assert_matches(repr(fieldset.U.grid)) - - -def test_fieldset_repr(fieldset): - Matches(r"\<.*FieldSet object at.*\>").assert_matches(repr(fieldset)) - - -def test_particleset_repr(fieldset): - Matches(r"\<.*ParticleSet object at.*\>").assert_matches(repr(ParticleSet(fieldset, pclass=Particle))) diff --git a/tests/test_xgrid.py b/tests/test_xgrid.py index c22437281..2d28d4aef 100644 --- a/tests/test_xgrid.py +++ b/tests/test_xgrid.py @@ -5,6 +5,7 @@ import pytest import xarray as xr from numpy.testing import assert_allclose +from re_assert import Matches from parcels import FieldSet from parcels._core.index_search import ( @@ -56,6 +57,10 @@ def test_grid_init_param_types(ds): XGrid.from_dataset(ds, mesh="invalid") +def test_xgrid_repr(fieldset): + Matches(r"\<.*XGrid object at.*\>").assert_matches(repr(fieldset.U.grid)) + + @pytest.mark.parametrize("ds, attr, expected", test_cases) def test_xgrid_properties_ground_truth(ds, attr, expected): grid = FieldSet.from_sgrid_conventions(ds, mesh="flat").data_g.grid