diff --git a/.github/workflows/run-test-suite.yml b/.github/workflows/run-test-suite.yml
index 64a5bc9478..b9cbb5fe6c 100644
--- a/.github/workflows/run-test-suite.yml
+++ b/.github/workflows/run-test-suite.yml
@@ -25,7 +25,7 @@ jobs:
# Skip older ubuntu-16.04 & macos-10.15 to save usage resource
os: [ubuntu-latest, macos-latest]
# Note: keep versions quoted as strings else 3.10 taken as 3.1, etc.
- python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
# Run on new and old(er) versions of the distros we support (Linux, Mac OS)
runs-on: ${{ matrix.os }}
@@ -138,7 +138,7 @@ jobs:
# For one job only, generate a coverage report:
- name: Upload coverage report to Codecov
- # Get coverage from only one job (choose with Ubuntu Python 3.8 as
+ # Get coverage from only one job (choose with Ubuntu Python 3.10 as
# representative). Note that we could use a separate workflow
# to setup Codecov reports, but given the amount of steps required to
# install including dependencies via conda, that a separate workflow
@@ -147,7 +147,7 @@ jobs:
# passing at least for that job, avoiding useless coverage reports.
uses: codecov/codecov-action@v3
if: |
- matrix.os == "ubuntu-latest" && matrix.python-version == "3.9"
+ matrix.os == "ubuntu-latest" && matrix.python-version == "3.10"
with:
file: |
${{ github.workspace }}/main/cf/test/cf_coverage_reports/coverage.xml
diff --git a/Changelog.rst b/Changelog.rst
index d6ab07043d..b8de197a02 100644
--- a/Changelog.rst
+++ b/Changelog.rst
@@ -1,3 +1,12 @@
+Version NEXTVERSION
+--------------
+
+**2025-10-??**
+
+* Python 3.9 support removed
+ (https://github.com/NCAS-CMS/cf-python/issues/896)
+* Changed dependency: ``Python>=3.10.0``
+
Version 3.18.1
--------------
diff --git a/cf/__init__.py b/cf/__init__.py
index 40187ed1e6..12ba5969e9 100644
--- a/cf/__init__.py
+++ b/cf/__init__.py
@@ -235,7 +235,7 @@
f"Got {scipy.__version__} at {scipy.__file__}"
)
-_minimum_vn = "3.9.0"
+_minimum_vn = "3.10.0"
if Version(python_version()) < Version(_minimum_vn):
raise ValueError(
f"Bad python version: cf requires python>={_minimum_vn}. "
diff --git a/cf/query.py b/cf/query.py
index 268ca7b6d0..320bcbc92c 100644
--- a/cf/query.py
+++ b/cf/query.py
@@ -959,122 +959,119 @@ def _evaluate(self, x, parent_attr):
operator = self._operator
value = self._value
- # TODO: Once Python 3.9 is no longer supported, this is a good
- # candidate for PEP 622 – Structural Pattern Matching
- # (https://peps.python.org/pep-0622)
+ match operator:
+ case "gt":
+ _gt = getattr(x, "__query_gt__", None)
+ if _gt is not None:
+ return _gt(value)
- if operator == "gt":
- _gt = getattr(x, "__query_gt__", None)
- if _gt is not None:
- return _gt(value)
+ return x > value
- return x > value
+ case "wi":
+ _wi = getattr(x, "__query_wi__", None)
+ if _wi is not None:
+ return _wi(value, self.open_lower, self.open_upper)
- if operator == "wi":
- _wi = getattr(x, "__query_wi__", None)
- if _wi is not None:
- return _wi(value, self.open_lower, self.open_upper)
+ if self.open_lower:
+ lower_bound = x > value[0]
+ else:
+ lower_bound = x >= value[0]
- if self.open_lower:
- lower_bound = x > value[0]
- else:
- lower_bound = x >= value[0]
+ if self.open_upper:
+ upper_bound = x < value[1]
+ else:
+ upper_bound = x <= value[1]
- if self.open_upper:
- upper_bound = x < value[1]
- else:
- upper_bound = x <= value[1]
+ return lower_bound & upper_bound
- return lower_bound & upper_bound
+ case "eq":
+ try:
+ return bool(value.search(x))
+ except AttributeError:
+ return x == value
+ except TypeError:
+ raise ValueError(
+ "Can't perform regular expression search on a "
+ f"non-string: {x!r}"
+ )
- if operator == "eq":
- try:
- return bool(value.search(x))
- except AttributeError:
- return x == value
- except TypeError:
- raise ValueError(
- "Can't perform regular expression search on a "
- f"non-string: {x!r}"
- )
+ case "isclose":
+ rtol = self.rtol
+ atol = self.atol
+ if atol is None:
+ atol = cf_atol().value
- if operator == "isclose":
- rtol = self.rtol
- atol = self.atol
- if atol is None:
- atol = cf_atol().value
+ if rtol is None:
+ rtol = cf_rtol().value
- if rtol is None:
- rtol = cf_rtol().value
+ _isclose = getattr(x, "__query_isclose__", None)
+ if _isclose is not None:
+ return _isclose(value, rtol, atol)
- _isclose = getattr(x, "__query_isclose__", None)
- if _isclose is not None:
- return _isclose(value, rtol, atol)
+ return np.isclose(x, value, rtol=rtol, atol=atol)
- return np.isclose(x, value, rtol=rtol, atol=atol)
-
- if operator == "ne":
- try:
- return not bool(value.search(x))
- except AttributeError:
- return x != value
- except TypeError:
- raise ValueError(
- "Can't perform regular expression search on a "
- f"non-string: {x!r}"
- )
+ case "ne":
+ try:
+ return not bool(value.search(x))
+ except AttributeError:
+ return x != value
+ except TypeError:
+ raise ValueError(
+ "Can't perform regular expression search on a "
+ f"non-string: {x!r}"
+ )
- if operator == "lt":
- _lt = getattr(x, "__query_lt__", None)
- if _lt is not None:
- return _lt(value)
+ case "lt":
+ _lt = getattr(x, "__query_lt__", None)
+ if _lt is not None:
+ return _lt(value)
- return x < value
+ return x < value
- if operator == "le":
- _le = getattr(x, "__query_le__", None)
- if _le is not None:
- return _le(value)
+ case "le":
+ _le = getattr(x, "__query_le__", None)
+ if _le is not None:
+ return _le(value)
- return x <= value
+ return x <= value
- if operator == "ge":
- _ge = getattr(x, "__query_ge_", None)
- if _ge is not None:
- return _ge(value)
+ case "ge":
+ _ge = getattr(x, "__query_ge_", None)
+ if _ge is not None:
+ return _ge(value)
- return x >= value
+ return x >= value
- if operator == "wo":
- _wo = getattr(x, "__query_wo__", None)
- if _wo is not None:
- return _wo(value)
+ case "wo":
+ _wo = getattr(x, "__query_wo__", None)
+ if _wo is not None:
+ return _wo(value)
- return (x < value[0]) | (x > value[1])
+ return (x < value[0]) | (x > value[1])
- if operator == "set":
- if isinstance(x, str):
- for v in value:
- try:
- if v.search(x):
- return True
- except AttributeError:
- if x == v:
- return True
+ case "set":
+ if isinstance(x, str):
+ for v in value:
+ try:
+ if v.search(x):
+ return True
+ except AttributeError:
+ if x == v:
+ return True
- return False
- else:
- _set = getattr(x, "__query_set__", None)
- if _set is not None:
- return _set(value)
-
- i = iter(value)
- v = next(i)
- out = x == v
- for v in i:
- out |= x == v
-
- return out
+ return False
+ else:
+ _set = getattr(x, "__query_set__", None)
+ if _set is not None:
+ return _set(value)
+
+ i = iter(value)
+ v = next(i)
+ out = x == v
+ for v in i:
+ out |= x == v
+
+ return out
def inspect(self):
"""Inspect the object for debugging.
diff --git a/cf/test/test_Data.py b/cf/test/test_Data.py
index 5918fb79b6..2300a62e47 100644
--- a/cf/test/test_Data.py
+++ b/cf/test/test_Data.py
@@ -4592,7 +4592,9 @@ def test_Data_masked_values(self):
d = cf.Data(array)
e = d.masked_values(1.1)
ea = e.array
- a = np.ma.masked_values(array, 1.1, rtol=cf.rtol(), atol=cf.atol())
+ a = np.ma.masked_values(
+ array, 1.1, rtol=float(cf.rtol()), atol=float(cf.atol())
+ )
self.assertTrue(np.isclose(ea, a).all())
self.assertTrue((ea.mask == a.mask).all())
self.assertIsNone(d.masked_values(1.1, inplace=True))
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 294b46dca0..945eec8118 100755
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -22,7 +22,6 @@
import cf
-
print("\ncf environment:")
print("-----------------")
cf.environment()
diff --git a/docs/source/installation.rst b/docs/source/installation.rst
index 40cec52247..cba73e8d26 100644
--- a/docs/source/installation.rst
+++ b/docs/source/installation.rst
@@ -32,7 +32,7 @@ Windows Subsystem for Linux (WSL)
**Python versions**
-------------------
-The cf package is only for Python 3.9 or newer.
+The cf package is only for Python 3.10 or newer.
----
@@ -209,7 +209,7 @@ installed, which:
Required
^^^^^^^^
-* `Python `_, 3.9.0 or newer.
+* `Python `_, 3.10.0 or newer.
* `numpy `_, versions 2.0.0 or newer.
diff --git a/setup.py b/setup.py
index f82bd9182d..66064ede44 100755
--- a/setup.py
+++ b/setup.py
@@ -299,7 +299,6 @@ def compile():
"Operating System :: MacOS",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",