Skip to content

Commit 6a7f15a

Browse files
authored
Merge branch 'main' into regrid-weights-chunks
2 parents 3a97a1f + ed814fa commit 6a7f15a

13 files changed

Lines changed: 149 additions & 118 deletions

.github/workflows/run-test-suite.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
# Skip older ubuntu-16.04 & macos-10.15 to save usage resource
2626
os: [ubuntu-latest, macos-latest]
2727
# Note: keep versions quoted as strings else 3.10 taken as 3.1, etc.
28-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
28+
python-version: ["3.10", "3.11", "3.12", "3.13"]
2929

3030
# Run on new and old(er) versions of the distros we support (Linux, Mac OS)
3131
runs-on: ${{ matrix.os }}
@@ -138,7 +138,7 @@ jobs:
138138
139139
# For one job only, generate a coverage report:
140140
- name: Upload coverage report to Codecov
141-
# Get coverage from only one job (choose with Ubuntu Python 3.8 as
141+
# Get coverage from only one job (choose with Ubuntu Python 3.10 as
142142
# representative). Note that we could use a separate workflow
143143
# to setup Codecov reports, but given the amount of steps required to
144144
# install including dependencies via conda, that a separate workflow
@@ -147,7 +147,7 @@ jobs:
147147
# passing at least for that job, avoiding useless coverage reports.
148148
uses: codecov/codecov-action@v3
149149
if: |
150-
matrix.os == "ubuntu-latest" && matrix.python-version == "3.9"
150+
matrix.os == "ubuntu-latest" && matrix.python-version == "3.10"
151151
with:
152152
file: |
153153
${{ github.workspace }}/main/cf/test/cf_coverage_reports/coverage.xml

Changelog.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
Version NEXTVERSION
22
--------------
33

4-
**2025-??-??**
4+
**2025-10-??**
55

6+
* Python 3.9 support removed
7+
(https://github.com/NCAS-CMS/cf-python/issues/896)
68
* Allow regridding for very large grids. New keyword parameter to
79
`cf.Field.regrids` and `cf.Field.regridc`: ``dst_grid_partitions``
810
(https://github.com/NCAS-CMS/cf-python/issues/878)
11+
* Changed dependency: ``Python>=3.10.0``
912

1013
----
1114

RELEASE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
$ find cf/ -type f | xargs sed -i 's/NEXTVERSION/X.Y.Z/g'
1515
```
1616

17+
- [ ] Edit the name of the `NEXTVERSION` milestone in GitHub to be the
18+
upcoming version `<VN>` (replacing `<VN>` appropriately,
19+
e.g. `3.18.0`). Then close the new `<VN>` milestone, and create a
20+
new `NEXTVERSION` milestone.
21+
1722
- [ ] Change the version and date in `cf/__init__.py` (`__version__` and
1823
`__date__` variables)
1924

cf/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@
235235
f"Got {scipy.__version__} at {scipy.__file__}"
236236
)
237237

238-
_minimum_vn = "3.9.0"
238+
_minimum_vn = "3.10.0"
239239
if Version(python_version()) < Version(_minimum_vn):
240240
raise ValueError(
241241
f"Bad python version: cf requires python>={_minimum_vn}. "

cf/query.py

Lines changed: 92 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -959,122 +959,119 @@ def _evaluate(self, x, parent_attr):
959959
operator = self._operator
960960
value = self._value
961961

962-
# TODO: Once Python 3.9 is no longer supported, this is a good
963-
# candidate for PEP 622 – Structural Pattern Matching
964-
# (https://peps.python.org/pep-0622)
962+
match operator:
963+
case "gt":
964+
_gt = getattr(x, "__query_gt__", None)
965+
if _gt is not None:
966+
return _gt(value)
965967

966-
if operator == "gt":
967-
_gt = getattr(x, "__query_gt__", None)
968-
if _gt is not None:
969-
return _gt(value)
968+
return x > value
970969

971-
return x > value
970+
case "wi":
971+
_wi = getattr(x, "__query_wi__", None)
972+
if _wi is not None:
973+
return _wi(value, self.open_lower, self.open_upper)
972974

973-
if operator == "wi":
974-
_wi = getattr(x, "__query_wi__", None)
975-
if _wi is not None:
976-
return _wi(value, self.open_lower, self.open_upper)
975+
if self.open_lower:
976+
lower_bound = x > value[0]
977+
else:
978+
lower_bound = x >= value[0]
977979

978-
if self.open_lower:
979-
lower_bound = x > value[0]
980-
else:
981-
lower_bound = x >= value[0]
980+
if self.open_upper:
981+
upper_bound = x < value[1]
982+
else:
983+
upper_bound = x <= value[1]
982984

983-
if self.open_upper:
984-
upper_bound = x < value[1]
985-
else:
986-
upper_bound = x <= value[1]
985+
return lower_bound & upper_bound
987986

988-
return lower_bound & upper_bound
987+
case "eq":
988+
try:
989+
return bool(value.search(x))
990+
except AttributeError:
991+
return x == value
992+
except TypeError:
993+
raise ValueError(
994+
"Can't perform regular expression search on a "
995+
f"non-string: {x!r}"
996+
)
989997

990-
if operator == "eq":
991-
try:
992-
return bool(value.search(x))
993-
except AttributeError:
994-
return x == value
995-
except TypeError:
996-
raise ValueError(
997-
"Can't perform regular expression search on a "
998-
f"non-string: {x!r}"
999-
)
998+
case "isclose":
999+
rtol = self.rtol
1000+
atol = self.atol
1001+
if atol is None:
1002+
atol = cf_atol().value
10001003

1001-
if operator == "isclose":
1002-
rtol = self.rtol
1003-
atol = self.atol
1004-
if atol is None:
1005-
atol = cf_atol().value
1004+
if rtol is None:
1005+
rtol = cf_rtol().value
10061006

1007-
if rtol is None:
1008-
rtol = cf_rtol().value
1007+
_isclose = getattr(x, "__query_isclose__", None)
1008+
if _isclose is not None:
1009+
return _isclose(value, rtol, atol)
10091010

1010-
_isclose = getattr(x, "__query_isclose__", None)
1011-
if _isclose is not None:
1012-
return _isclose(value, rtol, atol)
1011+
return np.isclose(x, value, rtol=rtol, atol=atol)
10131012

1014-
return np.isclose(x, value, rtol=rtol, atol=atol)
1015-
1016-
if operator == "ne":
1017-
try:
1018-
return not bool(value.search(x))
1019-
except AttributeError:
1020-
return x != value
1021-
except TypeError:
1022-
raise ValueError(
1023-
"Can't perform regular expression search on a "
1024-
f"non-string: {x!r}"
1025-
)
1013+
case "ne":
1014+
try:
1015+
return not bool(value.search(x))
1016+
except AttributeError:
1017+
return x != value
1018+
except TypeError:
1019+
raise ValueError(
1020+
"Can't perform regular expression search on a "
1021+
f"non-string: {x!r}"
1022+
)
10261023

1027-
if operator == "lt":
1028-
_lt = getattr(x, "__query_lt__", None)
1029-
if _lt is not None:
1030-
return _lt(value)
1024+
case "lt":
1025+
_lt = getattr(x, "__query_lt__", None)
1026+
if _lt is not None:
1027+
return _lt(value)
10311028

1032-
return x < value
1029+
return x < value
10331030

1034-
if operator == "le":
1035-
_le = getattr(x, "__query_le__", None)
1036-
if _le is not None:
1037-
return _le(value)
1031+
case "le":
1032+
_le = getattr(x, "__query_le__", None)
1033+
if _le is not None:
1034+
return _le(value)
10381035

1039-
return x <= value
1036+
return x <= value
10401037

1041-
if operator == "ge":
1042-
_ge = getattr(x, "__query_ge_", None)
1043-
if _ge is not None:
1044-
return _ge(value)
1038+
case "ge":
1039+
_ge = getattr(x, "__query_ge_", None)
1040+
if _ge is not None:
1041+
return _ge(value)
10451042

1046-
return x >= value
1043+
return x >= value
10471044

1048-
if operator == "wo":
1049-
_wo = getattr(x, "__query_wo__", None)
1050-
if _wo is not None:
1051-
return _wo(value)
1045+
case "wo":
1046+
_wo = getattr(x, "__query_wo__", None)
1047+
if _wo is not None:
1048+
return _wo(value)
10521049

1053-
return (x < value[0]) | (x > value[1])
1050+
return (x < value[0]) | (x > value[1])
10541051

1055-
if operator == "set":
1056-
if isinstance(x, str):
1057-
for v in value:
1058-
try:
1059-
if v.search(x):
1060-
return True
1061-
except AttributeError:
1062-
if x == v:
1063-
return True
1052+
case "set":
1053+
if isinstance(x, str):
1054+
for v in value:
1055+
try:
1056+
if v.search(x):
1057+
return True
1058+
except AttributeError:
1059+
if x == v:
1060+
return True
10641061

1065-
return False
1066-
else:
1067-
_set = getattr(x, "__query_set__", None)
1068-
if _set is not None:
1069-
return _set(value)
1070-
1071-
i = iter(value)
1072-
v = next(i)
1073-
out = x == v
1074-
for v in i:
1075-
out |= x == v
1076-
1077-
return out
1062+
return False
1063+
else:
1064+
_set = getattr(x, "__query_set__", None)
1065+
if _set is not None:
1066+
return _set(value)
1067+
1068+
i = iter(value)
1069+
v = next(i)
1070+
out = x == v
1071+
for v in i:
1072+
out |= x == v
1073+
1074+
return out
10781075

10791076
def inspect(self):
10801077
"""Inspect the object for debugging.

cf/test/test_Field.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import atexit
22
import datetime
33
import faulthandler
4+
from importlib.util import find_spec
45
import itertools
56
import os
67
import re
@@ -1152,6 +1153,8 @@ def test_Field_insert_dimension(self):
11521153
with self.assertRaises(ValueError):
11531154
f.insert_dimension(1, "qwerty")
11541155

1156+
@unittest.skipUnless(
1157+
find_spec("matplotlib"), "matplotlib required but not installed")
11551158
def test_Field_indices(self):
11561159
f = cf.read(self.filename)[0]
11571160

cf/test/test_RegridOperator.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,30 @@
11
import datetime
22
import faulthandler
3+
from importlib.util import find_spec
34
import unittest
45

56
faulthandler.enable() # to debug seg faults and timeouts
67

78
import cf
89

910

11+
# ESMF renamed its Python module to `esmpy` at ESMF version 8.4.0. Allow
12+
# either for now for backwards compatibility.
13+
esmpy_imported = False
14+
# Note: here only need esmpy for cf under-the-hood code, not in test
15+
# directly, so no need to actually import esmpy, just test it is there.
16+
if find_spec("esmpy") or find_spec("ESMF"):
17+
esmpy_imported = True
18+
19+
1020
class RegridOperatorTest(unittest.TestCase):
11-
src = cf.example_field(0)
12-
dst = cf.example_field(1)
13-
r = src.regrids(dst, "linear", return_operator=True)
1421

22+
def setUp(self):
23+
src = cf.example_field(0)
24+
dst = cf.example_field(1)
25+
self.r = src.regrids(dst, "linear", return_operator=True)
26+
27+
@unittest.skipUnless(esmpy_imported, "Requires esmpy/ESMF package.")
1528
def test_RegridOperator_attributes(self):
1629
self.assertEqual(self.r.coord_sys, "spherical")
1730
self.assertEqual(self.r.method, "linear")
@@ -39,6 +52,7 @@ def test_RegridOperator_attributes(self):
3952
self.assertIsNone(self.r.dst_z)
4053
self.assertFalse(self.r.ln_z)
4154

55+
@unittest.skipUnless(esmpy_imported, "Requires esmpy/ESMF package.")
4256
def test_RegridOperator_copy(self):
4357
self.assertIsInstance(self.r.copy(), self.r.__class__)
4458

cf/test/test_read_write.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import inspect
55
import os
66
import shutil
7+
import shutil
78
import subprocess
89
import tempfile
910
import unittest
@@ -642,6 +643,8 @@ def test_read_write_unlimited(self):
642643
self.assertTrue(domain_axes["domainaxis0"].nc_is_unlimited())
643644
self.assertTrue(domain_axes["domainaxis2"].nc_is_unlimited())
644645

646+
@unittest.skipUnless(
647+
shutil.which("ncdump"), "ncdump required - install nco")
645648
def test_read_CDL(self):
646649
subprocess.run(
647650
" ".join(["ncdump", self.filename, ">", tmpfile]),
@@ -703,6 +706,8 @@ def test_read_CDL(self):
703706
with self.assertRaises(Exception):
704707
cf.read("test_read_write.py")
705708

709+
@unittest.skipUnless(
710+
shutil.which("ncdump"), "ncdump required - install nco")
706711
def test_read_cdl_string(self):
707712
"""Test the cf.read 'cdl_string' keyword."""
708713
f = cf.read("example_field_0.nc")[0]
@@ -876,6 +881,8 @@ def test_read_url(self):
876881
f = cf.read(remote)
877882
self.assertEqual(len(f), 1)
878883

884+
@unittest.skipUnless(
885+
shutil.which("ncdump"), "ncdump required - install nco")
879886
def test_read_dataset_type(self):
880887
"""Test the cf.read 'dataset_type' keyword."""
881888
# netCDF dataset

cf/test/test_regrid_featureType.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,6 @@
2727
atol = 2e-12
2828
rtol = 0
2929

30-
meshloc = {
31-
"face": esmpy.MeshLoc.ELEMENT,
32-
"node": esmpy.MeshLoc.NODE,
33-
}
34-
3530

3631
def esmpy_regrid(coord_sys, method, src, dst, **kwargs):
3732
"""Helper function that regrids one dimension of Field data using
@@ -44,6 +39,11 @@ def esmpy_regrid(coord_sys, method, src, dst, **kwargs):
4439
Regridded numpy masked array.
4540
4641
"""
42+
meshloc = {
43+
"face": esmpy.MeshLoc.ELEMENT,
44+
"node": esmpy.MeshLoc.NODE,
45+
}
46+
4747
esmpy_regrid = cf.regrid.regrid(
4848
coord_sys,
4949
src,

0 commit comments

Comments
 (0)