From 9beda416eb712bc8a41e3822bfc01f9c12d73499 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Thu, 19 Sep 2024 10:40:41 +0100 Subject: [PATCH 1/8] Use pathlib in test_coding_standards. --- cf_units/tests/test_coding_standards.py | 27 ++++++++++++++----------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/cf_units/tests/test_coding_standards.py b/cf_units/tests/test_coding_standards.py index 98cf40e7..2ac81b8d 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 @@ -21,16 +20,20 @@ # 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) +DIR = Path(cf_units.__file__).resolve().parent +REPO_DIR = DIR.parent +DOCS_DIR = REPO_DIR / "doc" +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 = DOCS_DIR.glob("*") DOCS_DIRS = [ DOC_DIR for DOC_DIR in DOCS_DIRS - if os.path.basename(DOC_DIR) not in exclusion + if DOC_DIR.name not in exclusion ] @@ -74,7 +77,7 @@ def last_change_by_fname(): """ # Check the ".git" folder exists at the repo dir. - if not os.path.isdir(os.path.join(REPO_DIR, ".git")): + if not (REPO_DIR / ".git").is_dir(): raise ValueError("{} is not a git repository.".format(REPO_DIR)) # Call "git whatchanged" to get the details of all the files and when @@ -110,10 +113,10 @@ def test_license_headers(self): 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: From 357957f97e97bfc7b37c4d68504432461149d756 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Thu, 19 Sep 2024 14:44:09 +0100 Subject: [PATCH 2/8] Introduce test_python_versions, including deliberate rollbacks. --- .github/workflows/ci-tests.yml | 2 +- .github/workflows/ci-wheels.yml | 2 +- cf_units/tests/test_coding_standards.py | 97 ++++++++++++++++++++++--- tox.ini | 8 +- 4 files changed, 93 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index a4415530..fc5b888a 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -32,7 +32,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - version: [py310, py311] + version: [py39, py310, py311] include: - os: ubuntu-latest platform: linux diff --git a/.github/workflows/ci-wheels.yml b/.github/workflows/ci-wheels.yml index 2c10422d..313cb5c7 100644 --- a/.github/workflows/ci-wheels.yml +++ b/.github/workflows/ci-wheels.yml @@ -63,7 +63,7 @@ jobs: - name: "Building ${{ matrix.os }} (${{ matrix.arch }}) wheels" uses: pypa/cibuildwheel@v2.21.1 env: - CIBW_SKIP: "cp39-* cp313-* pp* *-musllinux*" + CIBW_SKIP: "cp312-* cp313-* pp* *-musllinux*" CIBW_ARCHS: ${{ matrix.arch }} CIBW_BUILD_FRONTEND: build CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 diff --git a/cf_units/tests/test_coding_standards.py b/cf_units/tests/test_coding_standards.py index 2ac81b8d..a4bf712d 100644 --- a/cf_units/tests/test_coding_standards.py +++ b/cf_units/tests/test_coding_standards.py @@ -23,18 +23,12 @@ DIR = Path(cf_units.__file__).resolve().parent REPO_DIR = DIR.parent DOCS_DIR = REPO_DIR / "doc" -DOCS_DIR = Path(cf_units.config.get_option( - "Resources", - "doc_dir", - default=DOCS_DIR -)) +DOCS_DIR = Path( + cf_units.config.get_option("Resources", "doc_dir", default=DOCS_DIR) +) exclusion = ["Makefile", "make.bat", "build"] DOCS_DIRS = DOCS_DIR.glob("*") -DOCS_DIRS = [ - DOC_DIR - for DOC_DIR in DOCS_DIRS - if DOC_DIR.name not in exclusion -] +DOCS_DIRS = [DOC_DIR for DOC_DIR in DOCS_DIRS if DOC_DIR.name not in exclusion] class TestLicenseHeaders: @@ -132,3 +126,86 @@ def test_license_headers(self): raise AssertionError( "There were license header failures. See stdout." ) + + +def test_python_versions(): + """Confirm alignment of ALL files listing supported Python versions.""" + supported = ["3.9", "3.10", "3.11"] + 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"#{10*' '}- 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() + + 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/tox.ini b/tox.ini index 547b4f7a..542f8d2d 100644 --- a/tox.ini +++ b/tox.ini @@ -2,9 +2,9 @@ requires = tox-conda tox-run-command - - -[testenv:py{310,311,312}-lock] + + +[testenv:py{39,310,311}-lock] allowlist_externals = cp changedir = @@ -32,7 +32,7 @@ commands = conda-lock --channel conda-forge --kind explicit --file {env:TMPFILE} --platform linux-64 --filename-template "{envname}-\{platform\}.txt" {posargs} -[testenv:py{310,311,312}-{linux,osx,win}-test] +[testenv:py{39,310,311}-{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 From 97af155a6078d23fa4d6ec6261f12010c8ceb4d1 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Thu, 19 Sep 2024 14:47:01 +0100 Subject: [PATCH 3/8] Roll forward Python to 10, 11, 12 in all files. --- .github/workflows/ci-locks.yml | 4 ++-- .github/workflows/ci-tests.yml | 6 +++--- .github/workflows/ci-wheels.yml | 2 +- cf_units/tests/test_coding_standards.py | 2 +- pyproject.toml | 2 +- setup.cfg | 2 +- tox.ini | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci-locks.yml b/.github/workflows/ci-locks.yml index 80689f54..27f8f77d 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 fc5b888a..a8bcd624 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -32,17 +32,17 @@ jobs: strategy: matrix: os: [ubuntu-latest] - version: [py39, py310, py311] + version: [py310, py311, py312] include: - os: ubuntu-latest platform: linux - os: ubuntu-latest - version: py311 + version: py312 posargs: "--cov-report=xml --cov" post-command: codecov # TODO: enable macos support # - os: macos-latest -# version: py311 +# version: py312 # platform: osx steps: - name: "Checkout" diff --git a/.github/workflows/ci-wheels.yml b/.github/workflows/ci-wheels.yml index 313cb5c7..2c10422d 100644 --- a/.github/workflows/ci-wheels.yml +++ b/.github/workflows/ci-wheels.yml @@ -63,7 +63,7 @@ jobs: - name: "Building ${{ matrix.os }} (${{ matrix.arch }}) wheels" uses: pypa/cibuildwheel@v2.21.1 env: - CIBW_SKIP: "cp312-* cp313-* pp* *-musllinux*" + CIBW_SKIP: "cp39-* cp313-* pp* *-musllinux*" CIBW_ARCHS: ${{ matrix.arch }} CIBW_BUILD_FRONTEND: build CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 diff --git a/cf_units/tests/test_coding_standards.py b/cf_units/tests/test_coding_standards.py index a4bf712d..b30ea208 100644 --- a/cf_units/tests/test_coding_standards.py +++ b/cf_units/tests/test_coding_standards.py @@ -130,7 +130,7 @@ def test_license_headers(self): def test_python_versions(): """Confirm alignment of ALL files listing supported Python versions.""" - supported = ["3.9", "3.10", "3.11"] + supported = ["3.10", "3.11", "3.12"] supported_strip = [ver.replace(".", "") for ver in supported] supported_latest = supported_strip[-1] 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 542f8d2d..fbc67069 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ requires = tox-run-command -[testenv:py{39,310,311}-lock] +[testenv:py{310,311,312}-lock] allowlist_externals = cp changedir = @@ -32,7 +32,7 @@ commands = conda-lock --channel conda-forge --kind explicit --file {env:TMPFILE} --platform linux-64 --filename-template "{envname}-\{platform\}.txt" {posargs} -[testenv:py{39,310,311}-{linux,osx,win}-test] +[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 From e8b9c4e16406c2980418d3da7f2787d17065f879 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Thu, 19 Sep 2024 14:52:22 +0100 Subject: [PATCH 4/8] Update test_python_versions for macos support. --- cf_units/tests/test_coding_standards.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cf_units/tests/test_coding_standards.py b/cf_units/tests/test_coding_standards.py index b30ea208..8060d022 100644 --- a/cf_units/tests/test_coding_standards.py +++ b/cf_units/tests/test_coding_standards.py @@ -191,10 +191,7 @@ def test_python_versions(): ), ( ci_tests_file, - ( - f"#{10*' '}- os: macos-latest\n" - f"#{12*' '}version: py{supported_latest}" - ), + (f"os: macos-latest\n" f"{12*' '}version: py{supported_latest}"), ), ] From a2d21a594b77f573e19b9029f3e9e882a49322dd Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Thu, 19 Sep 2024 15:02:08 +0100 Subject: [PATCH 5/8] Attempt better directory finding. --- cf_units/tests/test_coding_standards.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cf_units/tests/test_coding_standards.py b/cf_units/tests/test_coding_standards.py index 8060d022..2552b9fd 100644 --- a/cf_units/tests/test_coding_standards.py +++ b/cf_units/tests/test_coding_standards.py @@ -18,11 +18,8 @@ # 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 = Path(cf_units.__file__).resolve().parent -REPO_DIR = DIR.parent -DOCS_DIR = REPO_DIR / "doc" +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) ) From f1191f2065fa77a6eacee561b58996bfaf8111da Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Thu, 19 Sep 2024 15:11:49 +0100 Subject: [PATCH 6/8] Better handling if not running on a git repo. --- cf_units/tests/test_coding_standards.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/cf_units/tests/test_coding_standards.py b/cf_units/tests/test_coding_standards.py index 2552b9fd..5d20ca97 100644 --- a/cf_units/tests/test_coding_standards.py +++ b/cf_units/tests/test_coding_standards.py @@ -26,8 +26,10 @@ exclusion = ["Makefile", "make.bat", "build"] 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): @@ -67,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 (REPO_DIR / ".git").is_dir(): - 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( @@ -94,13 +92,7 @@ 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()): @@ -125,6 +117,7 @@ def test_license_headers(self): ) +@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"] From 000534ad9430c32f9dbba87fade01f14526c97ff Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Thu, 19 Sep 2024 16:29:07 +0100 Subject: [PATCH 7/8] Update conda_spec for Tox. --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index fbc67069..8a94fba7 100644 --- a/tox.ini +++ b/tox.ini @@ -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 = From ff48f02184b6c6b0eebb5827ca0639884a73e234 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Thu, 19 Sep 2024 17:40:01 +0100 Subject: [PATCH 8/8] Catch inconsistencies between tox header and conda_spec. --- cf_units/tests/test_coding_standards.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cf_units/tests/test_coding_standards.py b/cf_units/tests/test_coding_standards.py index 5d20ca97..e1a74d9d 100644 --- a/cf_units/tests/test_coding_standards.py +++ b/cf_units/tests/test_coding_standards.py @@ -191,6 +191,15 @@ def test_python_versions(): 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