From 5b5cb1ce7eb1ef94b628a373a78d0c8af5e14d34 Mon Sep 17 00:00:00 2001 From: Martin Yeo <40734014+trexfeathers@users.noreply.github.com> Date: Fri, 20 Sep 2024 11:30:39 +0100 Subject: [PATCH] Fully support Python 3.12 (#461) * Use pathlib in test_coding_standards. * Introduce test_python_versions, including deliberate rollbacks. * Roll forward Python to 10, 11, 12 in all files. * Update test_python_versions for macos support. * Attempt better directory finding. * Better handling if not running on a git repo. * Update conda_spec for Tox. * Catch inconsistencies between tox header and conda_spec. --- .github/workflows/ci-locks.yml | 4 +- .github/workflows/ci-tests.yml | 6 +- cf_units/tests/test_coding_standards.py | 132 +++++++++++++++++++----- pyproject.toml | 2 +- setup.cfg | 2 +- tox.ini | 10 +- 6 files changed, 116 insertions(+), 40 deletions(-) diff --git a/.github/workflows/ci-locks.yml b/.github/workflows/ci-locks.yml index a0fde215..952b3433 100644 --- a/.github/workflows/ci-locks.yml +++ b/.github/workflows/ci-locks.yml @@ -27,7 +27,7 @@ jobs: ENV_NAME: "ci-locks" strategy: matrix: - lock: [py39-lock, py310-lock, py311-lock] + lock: [py310-lock, py311-lock, py312-lock] steps: - name: "Checkout" uses: actions/checkout@v4 @@ -54,7 +54,7 @@ jobs: miniforge-version: latest channels: conda-forge,defaults activate-environment: ${{ env.ENV_NAME }} - auto-update-conda: true + auto-update-conda: true use-only-tar-bz2: true - name: "Conda environment cache" diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 1d6c8833..f8ac9424 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -32,17 +32,17 @@ jobs: strategy: matrix: os: [ubuntu-latest] - version: [py310, py311] + version: [py310, py311, py312] gitpath-prepend: [""] include: - os: ubuntu-latest platform: linux - os: ubuntu-latest - version: py311 + version: py312 posargs: "--cov-report=xml --cov" post-command: codecov - os: macos-latest - version: py311 + version: py312 platform: osx # On macos, the up-to-date git may not be first on the path # N.B. setting includes a final ":", to simplify the path setting command diff --git a/cf_units/tests/test_coding_standards.py b/cf_units/tests/test_coding_standards.py index 98cf40e7..e1a74d9d 100644 --- a/cf_units/tests/test_coding_standards.py +++ b/cf_units/tests/test_coding_standards.py @@ -3,11 +3,10 @@ # This file is part of cf-units and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -import os import subprocess from datetime import datetime from fnmatch import fnmatch -from glob import glob +from pathlib import Path import pytest @@ -19,21 +18,18 @@ # See LICENSE in the root of the repository for full licensing details.""" -# Guess cf_units repo directory of cf_units - realpath is used to mitigate -# against Python finding the cf_units package via a symlink. -DIR = os.path.realpath(os.path.dirname(cf_units.__file__)) -REPO_DIR = os.path.dirname(DIR) -DOCS_DIR = os.path.join(REPO_DIR, "doc") -DOCS_DIR = cf_units.config.get_option("Resources", "doc_dir", default=DOCS_DIR) +REPO_DIR = Path(__file__).resolve().parents[2] +DOCS_DIR = REPO_DIR / "docs" +DOCS_DIR = Path( + cf_units.config.get_option("Resources", "doc_dir", default=DOCS_DIR) +) exclusion = ["Makefile", "make.bat", "build"] -DOCS_DIRS = glob(os.path.join(DOCS_DIR, "*")) -DOCS_DIRS = [ - DOC_DIR - for DOC_DIR in DOCS_DIRS - if os.path.basename(DOC_DIR) not in exclusion -] +DOCS_DIRS = DOCS_DIR.glob("*") +DOCS_DIRS = [DOC_DIR for DOC_DIR in DOCS_DIRS if DOC_DIR.name not in exclusion] +IS_GIT_REPO = (REPO_DIR / ".git").is_dir() +@pytest.mark.skipif(not IS_GIT_REPO, reason="Not a git repository.") class TestLicenseHeaders: @staticmethod def whatchanged_parse(whatchanged_output): @@ -73,10 +69,6 @@ def last_change_by_fname(): or cannot be found by subprocess, an IOError may also be raised. """ - # Check the ".git" folder exists at the repo dir. - if not os.path.isdir(os.path.join(REPO_DIR, ".git")): - raise ValueError("{} is not a git repository.".format(REPO_DIR)) - # Call "git whatchanged" to get the details of all the files and when # they were last changed. output = subprocess.check_output( @@ -100,20 +92,14 @@ def test_license_headers(self): "cf_units/_udunits2_parser/parser/*", ) - try: - last_change_by_fname = self.last_change_by_fname() - except ValueError: - # Caught the case where this is not a git repo. - return pytest.skip( - "cf_units installation did not look like a " "git repo." - ) + last_change_by_fname = self.last_change_by_fname() failed = False for fname, last_change in sorted(last_change_by_fname.items()): - full_fname = os.path.join(REPO_DIR, fname) + full_fname = REPO_DIR / fname if ( - full_fname.endswith(".py") - and os.path.isfile(full_fname) + full_fname.suffix == ".py" + and full_fname.is_file() and not any(fnmatch(fname, pat) for pat in exclude_patterns) ): with open(full_fname) as fh: @@ -129,3 +115,93 @@ def test_license_headers(self): raise AssertionError( "There were license header failures. See stdout." ) + + +@pytest.mark.skipif(not IS_GIT_REPO, reason="Not a git repository.") +def test_python_versions(): + """Confirm alignment of ALL files listing supported Python versions.""" + supported = ["3.10", "3.11", "3.12"] + supported_strip = [ver.replace(".", "") for ver in supported] + supported_latest = supported_strip[-1] + + workflows_dir = REPO_DIR / ".github" / "workflows" + + # Places that are checked: + pyproject_toml_file = REPO_DIR / "pyproject.toml" + setup_cfg_file = REPO_DIR / "setup.cfg" + tox_file = REPO_DIR / "tox.ini" + ci_locks_file = workflows_dir / "ci-locks.yml" + ci_tests_file = workflows_dir / "ci-tests.yml" + ci_wheels_file = workflows_dir / "ci-wheels.yml" + + text_searches: list[tuple[Path, str]] = [ + ( + pyproject_toml_file, + "target-version = [" + + ", ".join([f'"py{p}"' for p in supported_strip]) + + "]", + ), + ( + setup_cfg_file, + "\n ".join( + [ + f"Programming Language :: Python :: {ver}" + for ver in supported + ] + ), + ), + ( + tox_file, + "[testenv:py{" + ",".join(supported_strip) + "}-lock]", + ), + ( + tox_file, + "[testenv:py{" + + ",".join(supported_strip) + + "}-{linux,osx,win}-test]", + ), + ( + ci_locks_file, + "lock: [" + + ", ".join([f"py{p}-lock" for p in supported_strip]) + + "]", + ), + ( + ci_tests_file, + ( + f"os: [ubuntu-latest]\n" + f"{8*' '}version: [" + + ", ".join([f"py{p}" for p in supported_strip]) + + "]" + ), + ), + ( + ci_tests_file, + (f"os: ubuntu-latest\n" f"{12*' '}version: py{supported_latest}"), + ), + ( + ci_tests_file, + (f"os: macos-latest\n" f"{12*' '}version: py{supported_latest}"), + ), + ] + + # This routine will not check for file existence first - if files are + # being added/removed we want developers to be aware that this test will + # need to be updated. + for path, search in text_searches: + assert search in path.read_text() + + tox_text = tox_file.read_text() + for version in supported_strip: + # A fairly lazy implementation, but should catch times when the section + # header does not match the conda_spec for the `tests` section. + # (Note that Tox does NOT provide its own helpful error in these cases). + py_version = f"py{version}" + assert tox_text.count(f" {py_version}-") == 3 + assert tox_text.count(f"{py_version}-lock") == 3 + + ci_wheels_text = ci_wheels_file.read_text() + (cibw_line,) = [ + line for line in ci_wheels_text.splitlines() if "CIBW_SKIP" in line + ] + assert all([p not in cibw_line for p in supported_strip]) diff --git a/pyproject.toml b/pyproject.toml index 3fd0732b..77faa797 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ local_scheme = "dirty-tag" [tool.black] line-length = 79 -target-version = ["py39", "py310", "py311"] +target-version = ["py310", "py311", "py312"] include = '\.pyi?$' exclude = ''' ( diff --git a/setup.cfg b/setup.cfg index f12fd640..9face95d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,9 +6,9 @@ classifiers = License :: OSI Approved :: BSD License Operating System :: OS Independent 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 Topic :: Scientific/Engineering description = Units of measure as required by the Climate and Forecast (CF) metadata conventions download_url = https://github.com/SciTools/cf-units diff --git a/tox.ini b/tox.ini index 547b4f7a..8a94fba7 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,8 @@ requires = tox-conda tox-run-command - - + + [testenv:py{310,311,312}-lock] allowlist_externals = cp @@ -34,15 +34,15 @@ commands = [testenv:py{310,311,312}-{linux,osx,win}-test] conda_spec = - py39-linux: {toxinidir}{/}requirements{/}locks{/}py39-lock-linux-64.txt py310-linux: {toxinidir}{/}requirements{/}locks{/}py310-lock-linux-64.txt py311-linux: {toxinidir}{/}requirements{/}locks{/}py311-lock-linux-64.txt - py39-osx: {toxinidir}{/}requirements{/}locks{/}py39-lock-osx-64.txt + py312-linux: {toxinidir}{/}requirements{/}locks{/}py312-lock-linux-64.txt py310-osx: {toxinidir}{/}requirements{/}locks{/}py310-lock-osx-64.txt py311-osx: {toxinidir}{/}requirements{/}locks{/}py311-lock-osx-64.txt - py39-win: {toxinidir}{/}requirements{/}locks{/}py39-lock-win-64.txt + py312-osx: {toxinidir}{/}requirements{/}locks{/}py312-lock-osx-64.txt py310-win: {toxinidir}{/}requirements{/}locks{/}py310-lock-win-64.txt py311-win: {toxinidir}{/}requirements{/}locks{/}py311-lock-win-64.txt + py312-win: {toxinidir}{/}requirements{/}locks{/}py312-lock-win-64.txt description = Perform cf-units unit/integration tests. passenv =