From 8347eda7ffe1a943fc9a3376ec14c790b7358dd1 Mon Sep 17 00:00:00 2001 From: David Hassell Date: Fri, 10 Oct 2025 13:01:51 +0100 Subject: [PATCH] Remove support for Python 3.9 (EOL) --- .github/workflows/run-test-suite.yml | 6 +- Changelog.rst | 9 ++ cf/__init__.py | 2 +- cf/query.py | 187 +++++++++++++-------------- cf/test/test_Data.py | 4 +- docs/source/conf.py | 1 - docs/source/installation.rst | 4 +- setup.py | 1 - 8 files changed, 110 insertions(+), 104 deletions(-) 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",