diff --git a/.copier-answers.yml b/.copier-answers.yml index d4f5feb..6f93fb1 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,10 +1,12 @@ # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY -_commit: v0.4.1 +_commit: v0.14.4 _src_path: gl:znicholls/copier-core-python-repository +conda_release: true email: zebedee.nicholls@climate-energy-college.org include_cli: false name: Zebedee Nicholls notebook_based_docs: true +package_manager: uv pandas_doctests: false plot_dependencies: false project_description_short: Handling of units related to simple climate modelling. diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index aac7b63..4fd0ef1 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -1,45 +1,29 @@ -name: "Setup Python and pdm" -description: "setup Python and pdm with caches" +name: "Setup Python and uv" +description: "setup Python and uv" inputs: python-version: description: "Python version to use" required: true - pdm-dependency-install-flags: - description: "Flags to pass to pdm when running `pdm install`" + uv-dependency-install-flags: + description: "Flags to pass to uv when running `uv install`" required: true - run-pdm-install: - description: "Should we run the pdm install steps" + run-uv-install: + description: "Should we run the uv install steps" required: false default: true runs: using: "composite" steps: - - name: Write file with install flags - shell: bash - run: | - echo "${{ inputs.pdm-dependency-install-flags }}" > pdm-install-flags.txt - - name: Setup PDM - id: setup-pdm - uses: pdm-project/setup-pdm@v4.1 + - name: Setup uv + id: setup-uv + uses: astral-sh/setup-uv@v4 with: + version: "0.8.8" python-version: ${{ inputs.python-version }} - cache: true - cache-dependency-path: | - ./pdm.lock - ./pdm-install-flags.txt - name: Install dependencies shell: bash - if: ${{ (inputs.run-pdm-install == 'true') && (steps.setup-pdm.outputs.cache-hit != 'true') }} - run: | - pdm install --no-self ${{ inputs.pdm-dependency-install-flags }} - # Now run same command but let the package install too - - name: Install package - shell: bash - # To ensure that the package is always installed, this step is run even if the cache was hit - if: ${{ inputs.run-pdm-install == 'true' }} + if: ${{ (inputs.run-uv-install == 'true') }} run: | - pdm install ${{ inputs.pdm-dependency-install-flags }} - pdm run which python - pdm run python --version # Check python version just in case + uv sync ${{ inputs.uv-dependency-install-flags }} diff --git a/.github/workflows/bump.yaml b/.github/workflows/bump.yaml index d962689..673acc4 100644 --- a/.github/workflows/bump.yaml +++ b/.github/workflows/bump.yaml @@ -5,24 +5,28 @@ on: inputs: bump_rule: type: choice - description: How to bump the project's version (see https://github.com/carstencodes/pdm-bump#usage) + description: How to bump the project's version (see https://docs.astral.sh/uv/reference/cli/#uv-version) options: - - no-pre-release - # no micro because we always sit on a pre-release in main, - # so we would always use no-pre-release instead of micro - # - micro + - patch - minor - major - - "pre-release --pre alpha" - - "pre-release --pre beta" - - "pre-release --pre release-candidate" + - stable + - alpha + - beta + - rc + - post + - dev required: true jobs: bump_version: name: "Bump version and create changelog" if: "!startsWith(github.event.head_commit.message, 'bump:')" - runs-on: ubuntu-latest + strategy: + matrix: + os: [ "ubuntu-latest" ] + python-version: [ "3.11" ] + runs-on: "${{ matrix.os }}" env: CI_COMMIT_EMAIL: "ci-runner@openscm-units.invalid" steps: @@ -32,36 +36,27 @@ jobs: fetch-depth: 0 token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" - - name: Setup PDM - uses: pdm-project/setup-pdm@v4 - with: - python-version: "3.9" - - - name: Install pdm-bump - run: | - pdm self add pdm-bump - - uses: ./.github/actions/setup with: python-version: ${{ matrix.python-version }} - pdm-dependency-install-flags: "-G dev" + uv-dependency-install-flags: "--all-extras --group dev" - name: Create bump and changelog run: | git config --global user.name "$GITHUB_ACTOR" git config --global user.email "$CI_COMMIT_EMAIL" - BASE_VERSION=`sed -ne 's/^version = "\([0-9\.a]*\)"/\1/p' pyproject.toml` + BASE_VERSION=`sed -ne 's/^version = "\([0-9\.post]*\)"/\1/p' pyproject.toml` echo "Bumping from version $BASE_VERSION" # Bump - pdm bump ${{ github.event.inputs.bump_rule }} + uv version --bump ${{ github.event.inputs.bump_rule }} - NEW_VERSION=`sed -ne 's/^version = "\([0-9\.a]*\)"/\1/p' pyproject.toml` + NEW_VERSION=`sed -ne 's/^version = "\([0-9\.]*\)"/\1/p' pyproject.toml` echo "Bumping to version $NEW_VERSION" # Build CHANGELOG - pdm run towncrier build --yes --version v$NEW_VERSION + uv run towncrier build --yes --version v$NEW_VERSION # Commit, tag and push git commit -a -m "bump: version $BASE_VERSION -> $NEW_VERSION" @@ -70,12 +65,12 @@ jobs: # Bump to alpha (so that future commits do not have the same # version as the tagged commit) - BASE_VERSION=`sed -ne 's/^version = "\([0-9\.a]*\)"/\1/p' pyproject.toml` + BASE_VERSION=`sed -ne 's/^version = "\([0-9\.]*\)"/\1/p' pyproject.toml` # Bump to pre-release of next version - pdm bump pre-release --pre alpha + uv version --bump post - NEW_VERSION=`sed -ne 's/^version = "\([0-9\.a]*\)"/\1/p' pyproject.toml` + NEW_VERSION=`sed -ne 's/^version = "\([0-9\.post]*\)"/\1/p' pyproject.toml` echo "Bumping version $BASE_VERSION > $NEW_VERSION" # Commit and push diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 532cae9..57b9bce 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,49 +9,71 @@ on: jobs: mypy: if: ${{ !github.event.pull_request.draft }} - runs-on: ubuntu-latest + strategy: + matrix: + os: [ "ubuntu-latest" ] + python-version: [ "3.9" ] + runs-on: "${{ matrix.os }}" steps: - name: Check out repository uses: actions/checkout@v4 - uses: ./.github/actions/setup with: - python-version: "3.11" - pdm-dependency-install-flags: "-G :all" + python-version: ${{ matrix.python-version }} + uv-dependency-install-flags: "--all-extras --group dev" - name: mypy run: | - MYPYPATH=stubs pdm run mypy src + MYPYPATH=stubs uv run mypy src docs: if: ${{ !github.event.pull_request.draft }} - runs-on: ubuntu-latest + strategy: + matrix: + os: [ "ubuntu-latest" ] + python-version: [ "3.11" ] + runs-on: "${{ matrix.os }}" steps: - name: Check out repository uses: actions/checkout@v4 - uses: ./.github/actions/setup with: - python-version: "3.11" - pdm-dependency-install-flags: "-G docs -G :all" + python-version: ${{ matrix.python-version }} + uv-dependency-install-flags: "--all-extras --group docs" - name: docs run: | - pdm run mkdocs build --strict + uv run mkdocs build --strict - uses: ./.github/actions/setup with: python-version: "3.11" - pdm-dependency-install-flags: "-G docs -G :all -G dev" + uv-dependency-install-flags: "--all-extras --group docs --group dev" - name: docs-with-changelog run: | # Check CHANGELOG will build too - pdm run towncrier build --yes - pdm run mkdocs build --strict + uv run towncrier build --yes + uv run mkdocs build --strict # Just in case, undo the staged changes git restore --staged . && git restore . + urls: + if: ${{ !github.event.pull_request.draft }} + runs-on: "ubuntu-latest" + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: check-urls-are-valid + uses: lycheeverse/lychee-action@v2 + with: + # Exclude local links + # and the template link in pyproject.toml + args: "--exclude 'file://' --exclude '^https://github\\.com/openscm/openscm-units/pull/\\{issue\\}$' ." + tests: strategy: fail-fast: false matrix: os: [ "ubuntu-latest" ] - python-version: [ "3.9", "3.10", "3.11" ] + # Test against all security and bugfix versions: https://devguide.python.org/versions/ + python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] runs-on: "${{ matrix.os }}" defaults: run: @@ -72,65 +94,167 @@ jobs: # when people try to run without installing optional dependencies, # we should add a CI step that runs the tests without optional dependencies too. # We don't have that right now, because we're not sure this pain point exists. - pdm-dependency-install-flags: "-G :all -G tests" + uv-dependency-install-flags: "--all-extras --group tests" - name: Run tests run: | - pdm run pytest -r a -v src tests --doctest-modules --cov=src --cov-report=term-missing --cov-report=xml - pdm run coverage report + uv run pytest -r a -v src tests --doctest-modules --doctest-report ndiff --cov=src --cov-report=term-missing --cov-report=xml + uv run coverage report - name: Upload coverage reports to Codecov with GitHub Action uses: codecov/codecov-action@v4.2.0 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - imports-without-extras: + tests-resolution-strategies: strategy: fail-fast: false matrix: + # Only test on ubuntu here for now. + # We could consider doing this on different platforms too, + # although that probably belongs better with the PyPI tests. os: [ "ubuntu-latest" ] - python-version: [ "3.9", "3.10", "3.11" ] + # Tests with lowest direct resolution. + # We don't do lowest because that is essentially testing + # whether downstream dependencies + # have set their minimum support dependencies correctly, + # which isn't our problem to solve. + resolution-strategy: [ "lowest-direct" ] + # Only test against the oldest supported python version + # because python is itself a direct dependency + # (so we're testing against the lowest direct python too). + python-version: [ "3.9" ] runs-on: "${{ matrix.os }}" + defaults: + run: + # This might be needed for Windows + # and doesn't seem to affect unix-based systems so we include it. + # If you have better proof of whether this is needed or not, + # feel free to update. + shell: bash steps: - name: Check out repository uses: actions/checkout@v4 - - uses: ./.github/actions/setup + - name: Setup uv + id: setup-uv + uses: astral-sh/setup-uv@v4 with: + version: "0.8.8" python-version: ${{ matrix.python-version }} - pdm-dependency-install-flags: "--prod --without :all" + - name: Create venv + run: | + uv venv --seed + - name: Install dependencies + run: | + uv pip install --requirements requirements-only-tests-locked.txt + uv pip compile --python ${{ matrix.python-version }} --resolution ${{ matrix.resolution-strategy }} --all-extras pyproject.toml -o requirements-tmp.txt + uv pip install --requirements requirements-tmp.txt . + - name: Run tests + run: | + uv run --no-sync pytest tests -r a -v + + tests-without-extras: + # Run the tests without installing extras. + # This is just a test to make sure to avoid + # breaking our test PyPI install workflow. + strategy: + fail-fast: false + matrix: + os: [ "ubuntu-latest" ] + # Just test against one Python version, this is just a helper. + # The real work happens in the test PyPI install + python-version: [ "3.11" ] + runs-on: "${{ matrix.os }}" + defaults: + run: + # This might be needed for Windows + # and doesn't seem to affect unix-based systems so we include it. + # If you have better proof of whether this is needed or not, + # feel free to update. + shell: bash + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Set up Python "${{ matrix.python-version }}" + id: setup-python + uses: actions/setup-python@v4 + with: + python-version: "${{ matrix.python-version }}" + - name: Install + run: | + pip install --upgrade pip wheel + pip install --no-deps . + pip install -r requirements-locked.txt + pip install -r requirements-only-tests-min-locked.txt + - name: Run tests + run: | + pytest tests -r a -vv tests + + imports-without-extras: + strategy: + fail-fast: false + matrix: + os: [ "ubuntu-latest" ] + # Test against all security and bugfix versions: https://devguide.python.org/versions/ + python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] + runs-on: "${{ matrix.os }}" + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Set up Python "${{ matrix.python-version }}" + id: setup-python + uses: actions/setup-python@v4 + with: + python-version: "${{ matrix.python-version }}" + - name: Install + run: | + pip install --upgrade pip wheel + pip install . - name: Check importable without extras - run: pdm run python scripts/test-install.py + run: python scripts/test-install.py check-build: - runs-on: ubuntu-latest + strategy: + matrix: + os: [ "ubuntu-latest" ] + python-version: [ "3.11" ] + runs-on: "${{ matrix.os }}" steps: - name: Check out repository uses: actions/checkout@v4 - - name: Setup PDM - uses: pdm-project/setup-pdm@v4 + - name: Setup uv + id: setup-uv + uses: astral-sh/setup-uv@v4 with: - python-version: "3.9" - pdm-dependency-install-flags: "not-used" - run-pdm-install: false + version: "0.8.8" + python-version: ${{ matrix.python-version }} - name: Build package run: | - pdm build + uv run python scripts/add-locked-targets-to-pyproject-toml.py + cat pyproject.toml + uv build + # Just in case, undo the changes to `pyproject.toml` + git restore --staged . && git restore . - name: Check build run: | tar -tvf dist/openscm_units-*.tar.gz --wildcards '*openscm_units/py.typed' tar -tvf dist/openscm_units-*.tar.gz --wildcards 'openscm_units-*/LICENCE' check-dependency-licences: - runs-on: ubuntu-latest + strategy: + matrix: + os: [ "ubuntu-latest" ] + python-version: [ "3.11" ] + runs-on: "${{ matrix.os }}" steps: - name: Check out repository uses: actions/checkout@v4 - uses: ./.github/actions/setup with: - python-version: "3.9" - pdm-dependency-install-flags: "-G dev" + python-version: ${{ matrix.python-version }} + uv-dependency-install-flags: "--group dev" - name: Check licences of dependencies shell: bash run: | TEMP_FILE=$(mktemp) - pdm export --prod > $TEMP_FILE - pdm run liccheck -r $TEMP_FILE -R licence-check.txt + uv export --no-dev > $TEMP_FILE + uv run liccheck -r $TEMP_FILE -R licence-check.txt cat licence-check.txt diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 3133d00..e31361c 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -15,7 +15,11 @@ jobs: # https://docs.pypi.org/trusted-publishers/adding-a-publisher/#github-actions # You can comment this line out if you don't want it. environment: deploy - runs-on: ubuntu-latest + strategy: + matrix: + os: [ "ubuntu-latest" ] + python-version: [ "3.11" ] + runs-on: "${{ matrix.os }}" permissions: # this permission is mandatory for trusted publishing with PyPI id-token: write @@ -24,10 +28,16 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Setup PDM - uses: pdm-project/setup-pdm@v4 + - name: Setup uv + id: setup-uv + uses: astral-sh/setup-uv@v4 with: - python-version: "3.9" + version: "0.8.8" + python-version: ${{ matrix.python-version }} - name: Publish to PyPI run: | - pdm publish + uv run python scripts/add-locked-targets-to-pyproject-toml.py + uv build + uv publish + # Just in case, undo the changes to `pyproject.toml` + git restore --staged . && git restore . diff --git a/.github/workflows/install-conda.yaml b/.github/workflows/install-conda.yaml index a156c64..58fea6c 100644 --- a/.github/workflows/install-conda.yaml +++ b/.github/workflows/install-conda.yaml @@ -2,7 +2,7 @@ # We make sure that we run the tests that apply to the version we installed, # rather than the latest tests in main. # The reason we do this, is that we want this workflow to test -# that installing from PyPI/conda leads to a correct installation. +# that installing from conda/mamba leads to a correct installation. # If we tested against main, the tests could fail # because the tests from main require the new features in main to pass. name: Test installation conda @@ -18,29 +18,23 @@ on: jobs: test-micromamba-installation: name: Test (micro)mamba install ${{ matrix.install-target }} (${{ matrix.python-version }}, ${{ matrix.os }}) - runs-on: "${{ matrix.os }}" strategy: fail-fast: false matrix: - # # There is an issue on windows, one for another day - # os: ["ubuntu-latest", "macos-latest", "windows-latest"] - os: ["ubuntu-latest", "macos-latest"] - python-version: [ "3.9", "3.10", "3.11" ] - # Check both the 'library' and the 'application' (i.e. locked package) + os: ["ubuntu-latest", "macos-latest", "windows-latest"] + # Test against all security and bugfix versions: https://devguide.python.org/versions/ + python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] + # Check both 'library' install and the 'application' (i.e. locked) install install-target: ["openscm-units", "openscm-units-locked"] - + runs-on: "${{ matrix.os }}" steps: - # While we wait for conda-forge release - # (https://github.com/conda-forge/staged-recipes/pull/26986) - # specify pins by hand - name: Setup (micro)mamba and install package uses: mamba-org/setup-micromamba@v1 with: environment-name: test-mamba-install create-args: >- python=${{ matrix.python-version }} - -c conda-forge - ${{ matrix.install-target }} + -c conda-forge ${{ matrix.install-target }} init-shell: bash - name: Get version shell: bash -leo pipefail {0} @@ -48,6 +42,10 @@ jobs: INSTALLED_VERSION=`python -c 'import openscm_units; print(f"v{openscm_units.__version__}")'` echo $INSTALLED_VERSION echo "INSTALLED_VERSION=$INSTALLED_VERSION" >> $GITHUB_ENV + - name: Check installed version environment variable + shell: bash -leo pipefail {0} + run: | + echo "${{ env.INSTALLED_VERSION }}" - name: Checkout repository uses: actions/checkout@v4 with: @@ -60,8 +58,24 @@ jobs: - name: Install pytest and other test dependencies shell: bash -leo pipefail {0} run: | - micromamba install pytest pytest-regressions + pip install -r requirements-only-tests-min-locked.txt + # micromamba install pytest pytest-regressions + - name: Run tests + shell: bash -leo pipefail {0} + run: | + # Can't run doctests here because the paths are different. + # This only runs with minimum test dependencies installed. + # So this is really just a smoke test, + # rather than a super thorough integration test. + # You will have to make sure that your tests run + # without all the extras installed for this to pass. + pytest tests -r a -vv tests + - name: Install all test dependencies + shell: bash -leo pipefail {0} + run: | + pip install -r requirements-only-tests-locked.txt - name: Run tests shell: bash -leo pipefail {0} run: | - pytest tests -r a -vv -s + # Can't run doctests here because the paths are different. + pytest tests -r a -vv tests diff --git a/.github/workflows/install-pypi.yaml b/.github/workflows/install-pypi.yaml index badc605..b74c55a 100644 --- a/.github/workflows/install-pypi.yaml +++ b/.github/workflows/install-pypi.yaml @@ -2,7 +2,7 @@ # We make sure that we run the tests that apply to the version we installed, # rather than the latest tests in main. # The reason we do this, is that we want this workflow to test -# that installing from PyPI/conda leads to a correct installation. +# that installing from PyPI leads to a correct installation. # If we tested against main, the tests could fail # because the tests from main require the new features in main to pass. name: Test installation PyPI @@ -18,14 +18,15 @@ on: jobs: test-pypi-install: name: Test PyPI install ${{ matrix.install-target }} (${{ matrix.python-version }}, ${{ matrix.os }}) - runs-on: "${{ matrix.os }}" strategy: fail-fast: false matrix: os: ["ubuntu-latest", "macos-latest", "windows-latest"] - python-version: [ "3.9", "3.10", "3.11" ] + # Test against all security and bugfix versions: https://devguide.python.org/versions/ + python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] # Check both 'library' install and the 'application' (i.e. locked) install install-target: ["openscm-units", "openscm-units[locked]"] + runs-on: "${{ matrix.os }}" steps: - name: Set up Python "${{ matrix.python-version }}" id: setup-python @@ -65,10 +66,22 @@ jobs: run: | which python python scripts/test-install.py - - name: Install pytest + - name: Install min test dependencies run: | - pip install pytest + pip install -r requirements-only-tests-min-locked.txt - name: Run tests run: | - # Can't run doctests here because the paths are different + # Can't run doctests here because the paths are different. + # This only runs with minimum test dependencies installed. + # So this is really just a smoke test, + # rather than a super thorough integration test. + # You will have to make sure that your tests run + # without all the extras installed for this to pass. + pytest tests -r a -vv tests + - name: Install all test dependencies + run: | + pip install -r requirements-only-tests-locked.txt + - name: Run tests with extra test dependencies installed + run: | + # Can't run doctests here because the paths are different. pytest tests -r a -vv tests diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 60bc054..d5b27ca 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -11,30 +11,39 @@ defaults: jobs: draft-release: name: Create draft release - runs-on: ubuntu-latest + strategy: + matrix: + os: [ "ubuntu-latest" ] + python-version: [ "3.11" ] + runs-on: "${{ matrix.os }}" steps: - name: Check out repository uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Setup PDM - uses: pdm-project/setup-pdm@v4 + - name: Setup uv + id: setup-uv + uses: astral-sh/setup-uv@v4 with: - python-version: "3.9" + version: "0.8.8" + python-version: ${{ matrix.python-version }} - name: Add version to environment run: | - PROJECT_VERSION=`sed -ne 's/^version = "\([0-9\.a]*\)"/\1/p' pyproject.toml` + PROJECT_VERSION=`sed -ne 's/^version = "\([0-9\.]*\)"/\1/p' pyproject.toml` echo "PROJECT_VERSION=$PROJECT_VERSION" >> $GITHUB_ENV - name: Build package for PyPI run: | - pdm build + uv run python scripts/add-locked-targets-to-pyproject-toml.py + uv build + # Just in case, undo the changes to `pyproject.toml` + git restore --staged . && git restore . - name: Generate Release Notes run: | echo "" >> ".github/release_template.md" echo "## Changelog" >> ".github/release_template.md" echo "" >> ".github/release_template.md" - pdm add typer - pdm run python scripts/changelog-to-release-template.py >> ".github/release_template.md" + uv add typer + uv run python scripts/changelog-to-release-template.py >> ".github/release_template.md" echo "" >> ".github/release_template.md" echo "## Changes" >> ".github/release_template.md" echo "" >> ".github/release_template.md" diff --git a/.github/workflows/test-upstream-latest.yaml b/.github/workflows/test-upstream-latest.yaml new file mode 100644 index 0000000..0d51ee4 --- /dev/null +++ b/.github/workflows/test-upstream-latest.yaml @@ -0,0 +1,58 @@ +name: Test against upstream latest + +on: + workflow_dispatch: + schedule: + # * is a special character in YAML so you have to quote this string + # This means At 03:00 on Wednesday. + # see https://crontab.guru/#0_0_*_*_3 + - cron: '0 3 * * 3' + +jobs: + tests-upstream-latest: + strategy: + fail-fast: false + matrix: + # Only test on ubuntu here for now. + # We could consider doing this on different platforms too, + # but this is mainly a warning for us of what is coming, + # rather than a super robust dive. + os: [ "ubuntu-latest" ] + # Test against all bugfix versions: https://devguide.python.org/versions/ + # as they are latest and ones most likely to support new features + python-version: [ "3.12", "3.13" ] + runs-on: "${{ matrix.os }}" + defaults: + run: + # This might be needed for Windows + # and doesn't seem to affect unix-based systems so we include it. + # If you have better proof of whether this is needed or not, + # feel free to update. + shell: bash + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Setup uv + id: setup-uv + uses: astral-sh/setup-uv@v4 + with: + version: "0.8.8" + python-version: ${{ matrix.python-version }} + # Often you need a step like this for e.g. numpy, scipy, pandas + - name: Setup compilation dependencies + run: | + echo "python${{ matrix.python-version }}-dev" + sudo add-apt-repository ppa:deadsnakes/ppa -y + sudo apt update + sudo apt install -y "python${{ matrix.python-version }}-dev" + - name: Create venv + run: | + uv venv --seed + - name: Install dependencies + run: | + uv pip install --requirements requirements-only-tests-locked.txt --requirements requirements-only-tests-min-locked.txt + uv pip install --requirements pyproject.toml --all-extras . + uv pip install --requirements requirements-upstream-dev.txt + - name: Run tests + run: | + uv run --no-sync pytest tests -r a -v diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fa3c69a..7e3fe83 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,12 +4,12 @@ ci: autoupdate_schedule: quarterly autoupdate_branch: pre-commit-autoupdate # Currently network access isn't supported in the pre-commit CI product. - skip: [pdm-lock-check, pdm-export, pdm-sync] + skip: [uv-sync, uv-lock, uv-export] # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: "v4.5.0" + rev: "v6.0.0" hooks: - id: check-added-large-files - id: check-ast @@ -33,24 +33,30 @@ repos: language: fail files: "\\.rej$" - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: "v0.1.8" + rev: "v0.8.4" hooks: - id: ruff args: [ --fix, --exit-non-zero-on-fix ] - id: ruff-format - - repo: https://github.com/pdm-project/pdm - rev: 2.17.0 + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.5.21 hooks: - # Check that the lock file is up to date. - # We need the pdm lock file too - # so that we can build locked version of the package. - - id: pdm-lock-check - args: ["--dev", "-G", ":all", "--strategy", "inherit_metadata" ] + - id: uv-sync + - id: uv-lock + name: uv-lock-check + args: ["--check"] # Put requirements.txt files in the repo too - - id: pdm-export + - id: uv-export name: export-requirements - args: ["-o", "requirements-locked.txt", "--without-hashes", "--prod"] - - id: pdm-export + args: ["-o", "requirements-locked.txt", "--no-hashes", "--no-dev", "--no-emit-project"] + - id: uv-export name: export-requirements-docs - args: ["-o", "requirements-docs-locked.txt", "--without-hashes", "-G", ":all", "-G", "docs"] - - id: pdm-sync + args: ["-o", "requirements-docs-locked.txt", "--no-hashes", "--no-dev", "--no-emit-project", "--all-extras", "--group", "docs"] + - id: uv-export + name: export-requirements-only-tests-min + args: ["-o", "requirements-only-tests-min-locked.txt", "--no-hashes", "--no-dev", "--no-emit-project", "--only-group", "tests-min"] + - id: uv-export + name: export-requirements-only-tests + args: ["-o", "requirements-only-tests-locked.txt", "--no-hashes", "--no-dev", "--no-emit-project", "--only-group", "tests"] + # # Not released yet + # - id: uv-sync diff --git a/Makefile b/Makefile index 469d465..5382d7b 100644 --- a/Makefile +++ b/Makefile @@ -24,22 +24,21 @@ help: ## print short description of each target .PHONY: checks checks: ## run all the linting checks of the codebase - @echo "=== pre-commit ==="; pdm run pre-commit run --all-files || echo "--- pre-commit failed ---" >&2; \ - echo "=== mypy ==="; MYPYPATH=stubs pdm run mypy src || echo "--- mypy failed ---" >&2; \ + @echo "=== pre-commit ==="; uv run pre-commit run --all-files || echo "--- pre-commit failed ---" >&2; \ + echo "=== mypy ==="; MYPYPATH=stubs uv run mypy src || echo "--- mypy failed ---" >&2; \ echo "======" .PHONY: ruff-fixes ruff-fixes: ## fix the code using ruff # format before and after checking so that the formatted stuff is checked and # the fixed stuff is formatted - pdm run ruff format src tests scripts docs - pdm run ruff check src tests scripts docs --fix - pdm run ruff format src tests scripts docs - + uv run ruff format src tests scripts docs + uv run ruff check src tests scripts docs --fix + uv run ruff format src tests scripts docs .PHONY: test test: ## run the tests - pdm run pytest src tests -r a -v --doctest-modules --cov=src + uv run pytest src tests -r a -v --doctest-modules --doctest-report ndiff --cov=src # Note on code coverage and testing: # You must specify cov=src. @@ -56,30 +55,29 @@ test: ## run the tests .PHONY: docs docs: ## build the docs - pdm run mkdocs build + uv run mkdocs build .PHONY: docs-strict docs-strict: ## build the docs strictly (e.g. raise an error on warnings, this most closely mirrors what we do in the CI) - pdm run mkdocs build --strict + uv run mkdocs build --strict .PHONY: docs-serve docs-serve: ## serve the docs locally - pdm run mkdocs serve + uv run mkdocs serve .PHONY: changelog-draft changelog-draft: ## compile a draft of the next changelog - pdm run towncrier build --draft --version draft + uv run towncrier build --draft --version draft .PHONY: licence-check licence-check: ## Check that licences of the dependencies are suitable # Will likely fail on Windows, but Makefiles are in general not Windows # compatible so we're not too worried - pdm export --without=tests --without=docs --without=dev > $(TEMP_FILE) - pdm run liccheck -r $(TEMP_FILE) -R licence-check.txt + uv export --no-dev > $(TEMP_FILE) + uv run liccheck -r $(TEMP_FILE) -R licence-check.txt rm -f $(TEMP_FILE) .PHONY: virtual-environment virtual-environment: ## update virtual environment, create a new one if it doesn't already exist - pdm lock --dev --group :all --strategy inherit_metadata - pdm install --dev --group :all - pdm run pre-commit install + uv sync --all-extras --group all-dev + uv run pre-commit install diff --git a/README.md b/README.md index 3d051fb..02d1cd4 100644 --- a/README.md +++ b/README.md @@ -116,12 +116,12 @@ The locked version of OpenSCM-Units can be installed with ### For developers -For development, we rely on [pdm](https://pdm-project.org/en/latest/) +For development, we rely on [uv](https://docs.astral.sh/uv/) for all our dependency management. -To get started, you will need to make sure that pdm is installed -([instructions here](https://pdm-project.org/en/latest/#installation), -although we found that installing with [pipx](https://pipx.pypa.io/stable/installation/) -worked perfectly for us). +To get started, you will need to make sure that uv is installed +([instructions here](https://docs.astral.sh/uv/getting-started/installation/) +(we found that the self-managed install was best, +particularly for upgrading uv later). For all of our work, we use our `Makefile`. You can read the instructions out and run the commands by hand if you wish, @@ -139,6 +139,6 @@ For the rest of our developer docs, please see [development][development]. ## Original template This project was generated from this template: -[copier core python repository](https://gitlab.com/znicholls/copier-core-python-repository). +[copier core python repository](https://gitlab.com/openscm/copier-core-python-repository). [copier](https://copier.readthedocs.io/en/stable/) is used to manage and distribute this template. diff --git a/docs/NAVIGATION.md b/docs/NAVIGATION.md index 5a1fd94..6c248fb 100644 --- a/docs/NAVIGATION.md +++ b/docs/NAVIGATION.md @@ -10,7 +10,7 @@ See https://oprypin.github.io/mkdocs-literate-nav/ - [Tutorials](tutorials/index.md) - [Basic demonstration](tutorials/basic-demonstration.py) - [Further background](further-background/index.md) - - [Design principles](further-background/design-principles.py) + - [Dependency pinning and testing](further-background/dependency-pinning-and-testing.md) - [Development](development.md) - [API reference](api/openscm_units/) - [Changelog](changelog.md) diff --git a/docs/development.md b/docs/development.md index 83a5fbd..e7612d9 100644 --- a/docs/development.md +++ b/docs/development.md @@ -48,8 +48,8 @@ should change depending on the updates to the code base. Releasing is semi-automated via a CI job. The CI job requires the type of version bump that will be performed to be manually specified. -See the pdm-bump docs for the -[list of available bump rules](https://github.com/carstencodes/pdm-bump#usage). +See the `uv version` docs (specifically the `--bump` flag) for the +[list of available bump rules](https://docs.astral.sh/uv/reference/cli/#uv-version). ### Standard process @@ -57,7 +57,7 @@ The steps required are the following: 1. Bump the version: manually trigger the "bump" workflow from the main branch (see here: [bump workflow](https://github.com/openscm/openscm-units/actions/workflows/bump.yaml)). - A valid "bump_rule" (see [pdm-bump's docs](https://github.com/carstencodes/pdm-bump#usage)) + A valid "bump_rule" (see [uv's docs](https://docs.astral.sh/uv/reference/cli/#uv-version)) will need to be specified. This will then trigger a draft release. @@ -70,11 +70,28 @@ The steps required are the following: This triggers a release to PyPI (which you can then add to the release if you want). +1. Go to your conda feedstock repository + (likely something like https://github.com/conda-forge/openscm-units-feedstock) + and make a new merge request that updates your `recipe/meta.yaml` file + to point to the newly released version on PyPI. + + - If you have updated any dependencies, copy these across to your `recipe/meta.yaml` file. + - If you are releasing a locked version on conda too, + you can generate the pins for your lock file with `scripts/print-conda-recipe-pins.py`. + 1. That's it, release done, make noise on social media of choice, do whatever else 1. Enjoy the newly available version +#### Further details + +We use [uv's build backend](https://docs.astral.sh/uv/concepts/build-backend) for building our project +and `scripts/add-locked-targets-to-pyproject-toml.py` +to provide locked extra groups for our package. +Including locked extra groups is why we run `scripts/add-locked-targets-to-pyproject-toml.py` +before any step related to building the package in the CI. + ## Read the Docs Our documentation is hosted by [Read the Docs (RtD)](https://www.readthedocs.org/), diff --git a/docs/further-background/dependency-pinning-and-testing.md b/docs/further-background/dependency-pinning-and-testing.md new file mode 100644 index 0000000..9b428a5 --- /dev/null +++ b/docs/further-background/dependency-pinning-and-testing.md @@ -0,0 +1,138 @@ +# Dependency pinning and associated testing strategy + + +Here we explain our dependency pinning and associated testing strategy. +This will help you, as a user, to know what to expect +and what your options are. +As a developer, these docs can also be helpful to understand +the overall philosophy and thinking. + +## Dependency pinning + +We use lower-bound pinning. +In other words, we pin the lowest supported version of the packages on which we depend. +As a user, this helps you get a working install +while giving you freedom to use newer versions, should you wish. + +We don't use upper-bound pins. +The reason is that we have had bad experiences with upper-bound pinning. +In the majority of cases, new releases do not cause issues +so pinning simply forces users to workaround overly strict pins[^1] +(which can be done, see +[working around incorrectly set pins][working-around-incorrectly-set-pins]). +The tradeoff with this approach is that you run the risk that, +if a dependency releases a breaking change, +the function provided by our package may break too. + +[^1]: + Yes, if the entire world followed semantic versioning perfectly, + we could use upper-bound pins for the next major version with more confidence + but that isn't the current state of the ecosystem. + Even if it were, we still think this would result in unnecessary pins + in many cases because many major releases are still compatible + because most packages don't use the entire API of their dependencies. + +### Working around incorrectly set pins + +Despite our best efforts, it is possible that we will set our pins incorrectly. +Part of this is because we simply cannot test all possible combinations of package installs +(see [testing strategy][testing-strategy]), +so we might miss valid/invalid combinations. + +If we set our pins incorrectly and you need to effectively overwrite them, +unfortunately there is currently no universal solution. +There has been quite some discussion, +see e.g. [this issue](https://github.com/pypa/pip/issues/8076), +but no universal resolution. + +However, for some environment managers, there is a solution. +This comes in the form of dependency overrides, +which allow you to override a package's stated dependencies +(essentially fixing them on the fly, +rather than having to fix them upstream). +Here are the docs for the package managers that we know support this: + +- [uv dependency overrides](https://docs.astral.sh/uv/concepts/resolution/#dependency-overrides). +- [pdm dependency overrides](https://pdm-project.org/latest/usage/dependency/#dependency-overrides). + +We do not know if this strategy can be used for packaging. +For example, you are building package A. +This depends on version 2 of package B and version 1 of package C. +However, version 1 of package C (incorrectly) says +that it is only compatible with version 1 of package B. +We are not sure if the dependency overrides +can be used to release a version of package A +that can be relased to and installed from PyPI. +If this is the situation you are in and you would like a resolution, +please comment on [this issue](https://gitlab.com/openscm/copier-core-python-repository/-/issues/4). + +## Testing strategy + +We test against multiple python versions in our CI. +These tests run with the latest compatible versions of our dependencies +and a 'full' installation, i.e. with all optional dependencies too. +This gives us the best possible coverage of our code base +against the latest compatible version of all our possible dependencies. + +In an attempt to anticipate changes to the API's of our key dependencies, +we also test against the latest unreleased version of our key dependencies once a week. +As a user, this probably won't matter too much, +except that it should reduce the chance +that a new release of one of our dependencies breaks our package +without us knowing in advance and being able to set a pin in anticipation. +As a developer, this is important to be aware of, +so we can anticipate changes as early as possible. + +We additionally test with the lowest/oldest compatible versions of our direct dependencies. +This includes Python, i.e. these tests are only run +with the lowest/oldest version of Python compatible with our project. +This is because Python is itself a dependency of our project +and newer versions of Python tend to not work +with the lowest/oldest versions of our direct dependencies. +These tests ensure that our minimum supported versions are actually supported +(if they are all installed simultaneously, +see the next paragraph for why this caveat matters). +As a note for developers, +the key trick to making this work is to use `uv pip compile` +rather than `uv run` (or similar) in the CI. +The reason is that `uv pip compile` +allows you to install dependencies for a very specific combination of things, +which is different to `uv`'s normal 'all-at-once' environment handling +(for more details, see [here](https://github.com/astral-sh/uv/issues/10774#issuecomment-2601925564)). + +We do not test the combinations in between lowest-supported and latest, +e.g. the oldest compatible version of package A +with the newest compatiable version of package B. +The reason for this is simply combinatorics, +it is generally not feasible +for us to test all possible combinations of our dependencies' versions. + +We also don't test with the oldest versions of our dependencies' dependencies. +We don't do this because, in practice, +all that such tests actually test is +whether our dependencies have set their minimum support dependencies correctly, +which isn't our problem to solve. + +Once a week, we also test what happens when a user installs from PyPI on the 'happy path'. +In other words, they do `pip install openscm-units`. +We check that such an install passes all the tests that don't require extras +(for developers, this is why we have `tests-min` and `tests-full` dev dependency groups, +they allow us to test a truly minimal testing environment, +separate from any extras we install to get full coverage). +Finally, we also check the installation of the locked versions of the package, +i.e. installation with `pip install 'openscm-units[locked]'`. +These tests give us the greatest coverage of Python versions and operating systems +and help alert us to places where users may face issues. +Having said that, these tests do require 30 separate jobs, +which is why we don't run them in CI. + +Through this combination of CI testing and installation testing, +we get a pretty good coverage of the different ways in which our package can be used. +It is not perfect, largely because the combinatorics don't allow for testing everything. +If we find a particular, key, use case failing often, +then we would happily discuss whether this should be included in the CI too, +to catch issues in advance of use. diff --git a/docs/further-background/design-principles.py b/docs/further-background/design-principles.py index 808333f..24ea1be 100644 --- a/docs/further-background/design-principles.py +++ b/docs/further-background/design-principles.py @@ -5,7 +5,7 @@ # extension: .py # format_name: percent # format_version: '1.3' -# jupytext_version: 1.16.2 +# jupytext_version: 1.18.1 # kernelspec: # display_name: Python 3 (ipykernel) # language: python diff --git a/docs/gen_doc_stubs.py b/docs/gen_doc_stubs.py index 490cdbc..0c8c490 100644 --- a/docs/gen_doc_stubs.py +++ b/docs/gen_doc_stubs.py @@ -4,17 +4,18 @@ This script can also be run directly to actually write out those files, as a preview. -All credit to the creators of: +Credit to the creators of: https://oprypin.github.io/mkdocs-gen-files/ and the docs at: https://mkdocstrings.github.io/crystal/quickstart/migrate.html +and the maintainers of: +https://github.com/mkdocstrings/python """ from __future__ import annotations import importlib import pkgutil -from collections.abc import Iterable from pathlib import Path import mkdocs_gen_files @@ -29,6 +30,9 @@ class PackageInfo: """ Package information used to help us auto-generate the docs + + Not stricly needed anymore now that mkdocstrings-python has a summary option, + but being kept in case we need something like this pattern again. """ full_name: str @@ -36,17 +40,17 @@ class PackageInfo: summary: str -def write_subpackage_pages(package: object) -> tuple[PackageInfo, ...]: +def write_subpackage_pages(subpackage: object) -> tuple[PackageInfo, ...]: """ - Write pages for the sub-packages of a package + Write pages for the sub-packages of a module """ - sub_packages = [] - for _, name, is_pkg in pkgutil.walk_packages(package.__path__): - subpackage_full_name = package.__name__ + "." + name - sub_package_info = write_module_page(subpackage_full_name) - sub_packages.append(sub_package_info) + sub_sub_packages = [] + for _, name, is_pkg in pkgutil.walk_packages(subpackage.__path__): + subpackage_full_name = subpackage.__name__ + "." + name + sub_package_info = write_package_page(subpackage_full_name) + sub_sub_packages.append(sub_package_info) - return tuple(sub_packages) + return tuple(sub_sub_packages) def get_write_file(package_full_name: str) -> Path: @@ -60,50 +64,16 @@ def get_write_file(package_full_name: str) -> Path: return write_file -def create_sub_packages_table(sub_packages: Iterable[PackageInfo]) -> str: - """Create the table summarising the sub-packages""" - links = [f"[{sp.stem}][{sp.full_name}]" for sp in sub_packages] - sub_package_header = "Sub-package" - sub_package_width = max([len(v) for v in [sub_package_header, *links]]) - - descriptions = [sp.summary for sp in sub_packages] - description_header = "Description" - description_width = max([len(v) for v in [description_header, *descriptions]]) - - sp_column = [sub_package_header, *links] - description_column = [description_header, *descriptions] - - sub_packages_table_l = [] - for i, (sub_package_value, description) in enumerate( - zip(sp_column, description_column) - ): - sp_padded = sub_package_value.ljust(sub_package_width) - desc_padded = description.ljust(description_width) - - line = f"| {sp_padded} | {desc_padded} |" - sub_packages_table_l.append(line) - - if i == 0: - underline = f"| {'-'*sub_package_width} | {'-'*description_width} |" - sub_packages_table_l.append(underline) - - sub_packages_table = "\n".join(sub_packages_table_l) - return sub_packages_table - - -def write_module_page( +def write_package_page( package_full_name: str, ) -> PackageInfo: """ - Write the docs pages for a module/package + Write the docs pages for a package (or sub-package) """ package = importlib.import_module(package_full_name) if hasattr(package, "__path__"): - sub_packages = write_subpackage_pages(package) - - else: - sub_packages = None + write_subpackage_pages(package) package_name = package_full_name.split(".")[-1] @@ -116,10 +86,6 @@ def write_module_page( with mkdocs_gen_files.open(write_file, "w") as fh: fh.write(f"# {package_full_name}\n") - if sub_packages: - fh.write("\n") - fh.write(f"{create_sub_packages_table(sub_packages)}\n") - fh.write("\n") fh.write(f"::: {package_full_name}") @@ -132,6 +98,6 @@ def write_module_page( return PackageInfo(package_full_name, package_name, summary) -write_module_page(PACKAGE_NAME_ROOT) +write_package_page(PACKAGE_NAME_ROOT) with mkdocs_gen_files.open(ROOT_DIR / PACKAGE_NAME_ROOT / "NAVIGATION.md", "w") as fh: fh.writelines(nav.build_literate_nav()) diff --git a/docs/how-to-guides/custom-conversions.py b/docs/how-to-guides/custom-conversions.py index fd740d2..feec8ab 100644 --- a/docs/how-to-guides/custom-conversions.py +++ b/docs/how-to-guides/custom-conversions.py @@ -5,7 +5,7 @@ # extension: .py # format_name: percent # format_version: '1.3' -# jupytext_version: 1.15.2 +# jupytext_version: 1.18.1 # kernelspec: # display_name: Python 3 (ipykernel) # language: python diff --git a/docs/javascripts/katex.js b/docs/javascripts/katex.js new file mode 100644 index 0000000..f7fd704 --- /dev/null +++ b/docs/javascripts/katex.js @@ -0,0 +1,10 @@ +document$.subscribe(({ body }) => { + renderMathInElement(body, { + delimiters: [ + { left: "$$", right: "$$", display: true }, + { left: "$", right: "$", display: false }, + { left: "\\(", right: "\\)", display: false }, + { left: "\\[", right: "\\]", display: true } + ], + }) +}) diff --git a/docs/tutorials/basic-demonstration.py b/docs/tutorials/basic-demonstration.py index 6cc4874..e45963a 100644 --- a/docs/tutorials/basic-demonstration.py +++ b/docs/tutorials/basic-demonstration.py @@ -5,7 +5,7 @@ # extension: .py # format_name: percent # format_version: '1.3' -# jupytext_version: 1.14.5 +# jupytext_version: 1.18.1 # kernelspec: # display_name: Python 3 (ipykernel) # language: python diff --git a/mkdocs.yml b/mkdocs.yml index 914aa41..1379aea 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -62,11 +62,13 @@ plugins: - remove_input # Docstring generation - mkdocstrings: + # See https://analog-garage.github.io/mkdocstrings-python-xref/1.6.0/ + default_handler: python_xref enable_inventory: true handlers: - python: + python_xref: paths: [src] - import: + inventories: # Cross-ref helpers (lots included here, remove what you don't want) - https://www.attrs.org/en/stable/objects.inv - https://unidata.github.io/cftime/objects.inv @@ -74,6 +76,7 @@ plugins: - https://loguru.readthedocs.io/en/latest/objects.inv - https://matplotlib.org/stable/objects.inv - https://ncdata.readthedocs.io/en/stable/objects.inv + - https://numpy.org/doc/stable/objects.inv - https://openscm-units.readthedocs.io/en/stable/objects.inv - https://pandas.pydata.org/docs/objects.inv - https://pint.readthedocs.io/en/stable/objects.inv @@ -82,12 +85,35 @@ plugins: - https://docs.scipy.org/doc/scipy/objects.inv - https://scitools-iris.readthedocs.io/en/stable/objects.inv - https://scmdata.readthedocs.io/en/stable/objects.inv + # # Not available for tqdm + # # https://github.com/tqdm/tqdm/issues/705 + # - https://tqdm.github.io/objects.inv - https://validators.readthedocs.io/en/stable/objects.inv - http://xarray.pydata.org/en/stable/objects.inv options: + # It turns out that xref does this check without considering config. + # As a result, every docstring is parsed as a google-style docstring. + # As docstrings are only parsed once, this results in rendering failures. + # Hence, turn off the checks here. + # mkdocs_autorefs catches anything which doesn't end up being a correct reference. + # The fix might be as simple as changing + # `self.collect(ref, PythonOptions())` + # to `self.collect(ref, self.get_options({}))` + # in the python-xref source, but it's hard to predict the side effects + # so we haven't bothered with that route. + check_crossrefs: false docstring_style: numpy - show_root_heading: true + relative_crossrefs: true + separate_signature: true + show_root_heading: false + show_signature_annotations: true show_source: true + signature_crossrefs: true + summary: + attributes: true + classes: true + functions: true + modules: true # https://squidfunk.github.io/mkdocs-material/plugins/search/ - search # Add clickable sections to the sidebar @@ -97,6 +123,10 @@ plugins: markdown_extensions: # https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown/#attribute-lists - attr_list + - footnotes + # https://squidfunk.github.io/mkdocs-material/reference/math/#katex-mkdocsyml + - pymdownx.arithmatex: + generic: true # Allow admonitions, useful for deprecation warnings # https://facelessuser.github.io/pymdown-extensions/extensions/blocks/plugins/admonition/ - pymdownx.blocks.admonition @@ -125,6 +155,14 @@ markdown_extensions: - toc: permalink: "#" +extra_javascript: + - javascripts/katex.js + - https://unpkg.com/katex@0/dist/katex.min.js + - https://unpkg.com/katex@0/dist/contrib/auto-render.min.js + +extra_css: + - https://unpkg.com/katex@0/dist/katex.min.css + watch: - README.md # Auto-generate if `src` changes (because this changes API docs) diff --git a/pyproject.toml b/pyproject.toml index ae1d259..424a610 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ { name = "Robert Gieseke", email = "rob.g@web.de" }, { name = "Sven Willner", email = "sven.willer@gmail.com" }, ] -license = {text = "BSD-3-Clause"} +license = { text = "BSD-3-Clause" } requires-python = ">=3.9" dependencies = [ "pint", @@ -19,61 +19,106 @@ dependencies = [ ] readme = "README.md" classifiers = [ + # Full list: https://pypi.org/classifiers/ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + ## If you apply an OSI-approved licence, you should uncomment the below "License :: OSI Approved :: BSD License", + "Natural Language :: English", "Operating System :: OS Independent", + "Programming Language :: Python", + "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", + "Programming Language :: Python :: 3.13", + "Typing :: Typed", ] -[build-system] -requires = [ - "pdm-backend", - "pdm-build-locked", -] -build-backend = "pdm.backend" +[project.urls] +Homepage = "https://openscm-units.readthedocs.io" +Documentation = "https://openscm-units.readthedocs.io" +Changelog = "https://openscm-units.readthedocs.io/en/stable/changelog" +Repository = "https://github.com/openscm/openscm-units" +Issues = "https://github.com/openscm/openscm-units/issues" -[tool.pdm] -[tool.pdm.build] -locked = true -includes = [ - "src/openscm_units", - "LICENCE", -] -[tool.pdm.dev-dependencies] +[project.optional-dependencies] + +[dependency-groups] + dev = [ - "pre-commit>=3.8.0", - "mypy>=1.11", - "towncrier>=24.8.0", - "liccheck>=0.9.2", - # Required for liccheck - "setuptools>=74.1.2", - "pip>=24.2", + # Key dependencies + # ---------------- + "liccheck==0.9.2", + "mypy>=1.14.0", + # Required for liccheck, see https://github.com/dhatim/python-license-check/pull/113 + "pip>=24.3.1", + "pre-commit>=4.0.1", "pandas-stubs>=2.2.2.240807", + "towncrier>=24.8.0", "tomli>=2.0.2", "typer>=0.12.5", ] docs = [ - "attrs>=24.0.0", - "mkdocs>=1.6.0", - "mkdocs-autorefs>=1.2.0", + # Key dependencies + # ---------------- + "attrs>=25.3.0", + "mkdocs-autorefs>=1.4.2", "mkdocs-gen-files>=0.5.0", - "mkdocs-literate-nav>=0.6.1", - "mkdocs-material>=9.5.34", - "mkdocs-section-index>=0.3.9", - "mkdocstrings[python]>=0.25.0", - "pymdown-extensions>=10.9", + "mkdocs-literate-nav>=0.6.2", + "mkdocs-material>=9.6.16", + "mkdocs-section-index>=0.3.10", + "mkdocs>=1.6.1", + "mkdocstrings-python-xref>=1.16.3", + "mkdocstrings-python>=1.16.12", + "pymdown-extensions>=10.16.1", + "ruff==0.12.8", + + # Key dependencies for notebook_based_docs + # ---------------------------------------- + "jupyterlab>=4.4.5", + "jupytext>=1.17.2", "mkdocs-jupyter>=0.25.1", - "jupyterlab>=4.2.0", - "jupytext>=1.16.3", + ] +# For minimum test dependencies. +# These are used when running our minimum PyPI install tests. +tests-min = [ + # Key dependencies + # ---------------- + "pytest>=8.3.4", +] +# Full test dependencies. +tests-full = [ + # Key dependencies + # ---------------- + "pytest-cov>=6.0.0", +] +# Test dependencies +# (partly split because liccheck uses toml, +# which doesn't support inhomogeneous arrays). tests = [ - "pytest>=8.3.3", - "coverage>=7.6.0", - "pytest-cov>=5.0.0", + {include-group = "tests-min"}, + {include-group = "tests-full"}, +] +all-dev = [ + {include-group = "dev"}, + {include-group = "docs"}, + {include-group = "tests"}, +] + +[build-system] +requires = ["uv_build>=0.8.7,<0.9.0"] +build-backend = "uv_build" + +[tool.uv.build-backend] +source-include = [ + "src/openscm_units", + "LICENCE", ] @@ -89,6 +134,10 @@ skip_empty = true show_missing = true exclude_also = [ "if TYPE_CHECKING", + # Type overloading lines + "@overload", + "\\.\\.\\.", + ] [tool.mypy] @@ -193,13 +242,16 @@ authorized_licenses = [ "bsd", "bsd license", "BSD 3-Clause", + "BSD-3-Clause", "CC0", "apache", "apache 2.0", "apache software", "apache software license", "Apache License, Version 2.0", + "CMU License (MIT-CMU)", "Historical Permission Notice and Disclaimer (HPND)", + "isc", "isc license", "isc license (iscl)", "gnu lgpl", @@ -210,8 +262,10 @@ authorized_licenses = [ "mit", "mit license", "Mozilla Public License 2.0 (MPL 2.0)", + "psf-2.0", "python software foundation", "python software foundation license", + "The Unlicense (Unlicense)", "zpl 2.1", ] unauthorized_licenses = [ diff --git a/requirements-docs-locked.txt b/requirements-docs-locked.txt index 711d0ea..084f41b 100644 --- a/requirements-docs-locked.txt +++ b/requirements-docs-locked.txt @@ -1,138 +1,156 @@ -# This file is @generated by PDM. -# Please do not edit it manually. - -anyio==4.6.2.post1 -appdirs==1.4.4 -appnope==0.1.4; platform_system == "Darwin" -argon2-cffi==23.1.0 -argon2-cffi-bindings==21.2.0 -arrow==1.3.0 -asttokens==2.4.1 -async-lru==2.0.4 -attrs==24.2.0 -babel==2.16.0 -beautifulsoup4==4.12.3 -bleach==6.1.0 -certifi==2024.8.30 -cffi==1.17.1 -charset-normalizer==3.4.0 -click==8.1.7 +# This file was autogenerated by uv via the following command: +# uv export -o requirements-docs-locked.txt --no-hashes --no-dev --no-emit-project --all-extras --group docs +anyio==4.12.0 +appnope==0.1.4 ; sys_platform == 'darwin' +argon2-cffi==25.1.0 +argon2-cffi-bindings==25.1.0 +arrow==1.4.0 +asttokens==3.0.1 +async-lru==2.0.5 +attrs==25.4.0 +babel==2.17.0 +backrefs==6.1 +beautifulsoup4==4.14.3 +bleach==6.2.0 ; python_full_version < '3.10' +bleach==6.3.0 ; python_full_version >= '3.10' +certifi==2025.11.12 +cffi==2.0.0 +charset-normalizer==3.4.4 +click==8.1.8 ; python_full_version < '3.10' +click==8.3.1 ; python_full_version >= '3.10' colorama==0.4.6 -comm==0.2.2 -debugpy==1.8.7 -decorator==5.1.1 +comm==0.2.3 +debugpy==1.8.19 +decorator==5.2.1 defusedxml==0.7.1 -exceptiongroup==1.2.2; python_version < "3.11" -executing==2.1.0 -fastjsonschema==2.20.0 +exceptiongroup==1.3.1 ; python_full_version < '3.11' +executing==2.2.1 +fastjsonschema==2.21.2 flexcache==0.3 -flexparser==0.3.1 +flexparser==0.4 fqdn==1.5.1 ghp-import==2.1.0 globalwarmingpotentials==0.11.1 -griffe==1.4.1 -h11==0.14.0 -httpcore==1.0.6 -httpx==0.27.2 -idna==3.10 -importlib-metadata==8.5.0; python_version < "3.10" -ipykernel==6.29.5 -ipython==8.18.1 +griffe==1.14.0 ; python_full_version < '3.10' +griffe==1.15.0 ; python_full_version >= '3.10' +h11==0.16.0 +httpcore==1.0.9 +httpx==0.28.1 +idna==3.11 +importlib-metadata==8.7.0 ; python_full_version < '3.10' +ipykernel==6.31.0 +ipython==8.18.1 ; python_full_version < '3.10' +ipython==8.37.0 ; python_full_version == '3.10.*' +ipython==9.8.0 ; python_full_version >= '3.11' +ipython-pygments-lexers==1.1.1 ; python_full_version >= '3.11' isoduration==20.11.0 -jedi==0.19.1 -jinja2==3.1.5 -json5==0.9.25 +jedi==0.19.2 +jinja2==3.1.6 +json5==0.12.1 jsonpointer==3.0.0 -jsonschema==4.23.0 -jsonschema-specifications==2024.10.1 -jsonschema[format-nongpl]==4.23.0 -jupyter-client==8.6.3 -jupyter-core==5.7.2 -jupyter-events==0.10.0 -jupyter-lsp==2.2.5 -jupyter-server==2.14.2 +jsonschema==4.25.1 +jsonschema-specifications==2025.9.1 +jupyter-client==8.6.3 ; python_full_version < '3.10' +jupyter-client==8.7.0 ; python_full_version >= '3.10' +jupyter-core==5.8.1 ; python_full_version < '3.10' +jupyter-core==5.9.1 ; python_full_version >= '3.10' +jupyter-events==0.12.0 +jupyter-lsp==2.3.0 +jupyter-server==2.17.0 jupyter-server-terminals==0.5.3 -jupyterlab==4.4.8 +jupyterlab==4.5.1 jupyterlab-pygments==0.3.0 -jupyterlab-server==2.27.3 -jupytext==1.16.4 -markdown==3.7 -markdown-it-py==3.0.0 -markupsafe==3.0.1 -matplotlib-inline==0.1.7 -mdit-py-plugins==0.4.2 +jupyterlab-server==2.28.0 +jupytext==1.18.1 +lark==1.3.1 +markdown==3.9 ; python_full_version < '3.10' +markdown==3.10 ; python_full_version >= '3.10' +markdown-it-py==3.0.0 ; python_full_version < '3.10' +markdown-it-py==4.0.0 ; python_full_version >= '3.10' +markupsafe==3.0.3 +matplotlib-inline==0.2.1 +mdit-py-plugins==0.4.2 ; python_full_version < '3.10' +mdit-py-plugins==0.5.0 ; python_full_version >= '3.10' mdurl==0.1.2 mergedeep==1.3.4 -mistune==3.0.2 +mistune==3.1.4 mkdocs==1.6.1 -mkdocs-autorefs==1.2.0 -mkdocs-gen-files==0.5.0 +mkdocs-autorefs==1.4.3 +mkdocs-gen-files==0.6.0 mkdocs-get-deps==0.2.0 mkdocs-jupyter==0.25.1 -mkdocs-literate-nav==0.6.1 -mkdocs-material==9.5.41 +mkdocs-literate-nav==0.6.2 +mkdocs-material==9.7.1 mkdocs-material-extensions==1.3.1 -mkdocs-section-index==0.3.9 -mkdocstrings==0.26.2 -mkdocstrings-python==1.12.1 -mkdocstrings[python]==0.26.2 -nbclient==0.10.0 -nbconvert==7.16.4 +mkdocs-section-index==0.3.10 +mkdocstrings==0.30.1 ; python_full_version < '3.10' +mkdocstrings==1.0.0 ; python_full_version >= '3.10' +mkdocstrings-python==1.18.2 ; python_full_version < '3.10' +mkdocstrings-python==1.19.0 ; python_full_version >= '3.10' +mkdocstrings-python-xref==1.16.4 +nbclient==0.10.2 +nbconvert==7.16.6 nbformat==5.10.4 nest-asyncio==1.6.0 notebook-shim==0.2.4 -numpy==2.0.2 -overrides==7.7.0 -packaging==24.1 +numpy==2.0.2 ; python_full_version < '3.10' +numpy==2.2.6 ; python_full_version == '3.10.*' +numpy==2.3.5 ; python_full_version >= '3.11' +overrides==7.7.0 ; python_full_version < '3.12' +packaging==25.0 paginate==0.5.7 -pandas==2.2.3 +pandas==2.3.3 pandocfilters==1.5.1 -parso==0.8.4 +parso==0.8.5 pathspec==0.12.1 -pexpect==4.9.0; sys_platform != "win32" -pint==0.24.3 -platformdirs==4.3.6 -prometheus-client==0.21.0 -prompt-toolkit==3.0.48 -psutil==6.0.0 -ptyprocess==0.7.0; os_name != "nt" or sys_platform != "win32" +pexpect==4.9.0 ; (python_full_version < '3.10' and sys_platform == 'emscripten') or (sys_platform != 'emscripten' and sys_platform != 'win32') +pint==0.24.4 ; python_full_version < '3.11' +pint==0.25.2 ; python_full_version >= '3.11' +platformdirs==4.4.0 ; python_full_version < '3.10' +platformdirs==4.5.1 ; python_full_version >= '3.10' +prometheus-client==0.23.1 +prompt-toolkit==3.0.52 +psutil==7.1.3 +ptyprocess==0.7.0 ; (python_full_version < '3.10' and sys_platform == 'emscripten') or os_name != 'nt' or (sys_platform != 'emscripten' and sys_platform != 'win32') pure-eval==0.2.3 -pycparser==2.22 -pygments==2.18.0 -pymdown-extensions==10.11.2 +pycparser==2.23 ; implementation_name != 'PyPy' +pygments==2.19.2 +pymdown-extensions==10.19.1 python-dateutil==2.9.0.post0 -python-json-logger==2.0.7 -pytz==2024.2 -pywin32==308; sys_platform == "win32" and platform_python_implementation != "PyPy" -pywinpty==2.0.13; os_name == "nt" -pyyaml==6.0.2 -pyyaml-env-tag==0.1 -pyzmq==26.2.0 -referencing==0.35.1 -regex==2024.9.11 -requests==2.32.3 +python-json-logger==4.0.0 +pytz==2025.2 +pywin32==311 ; python_full_version < '3.10' and platform_python_implementation != 'PyPy' and sys_platform == 'win32' +pywinpty==3.0.2 ; os_name == 'nt' +pyyaml==6.0.3 +pyyaml-env-tag==1.1 +pyzmq==27.1.0 +referencing==0.36.2 ; python_full_version < '3.10' +referencing==0.37.0 ; python_full_version >= '3.10' +requests==2.32.5 rfc3339-validator==0.1.4 rfc3986-validator==0.1.1 -rpds-py==0.20.0 +rfc3987-syntax==1.1.0 +rpds-py==0.27.1 ; python_full_version < '3.10' +rpds-py==0.30.0 ; python_full_version >= '3.10' +ruff==0.12.8 send2trash==1.8.3 -setuptools==75.2.0 -six==1.16.0 -sniffio==1.3.1 -soupsieve==2.6 +setuptools==80.9.0 +six==1.17.0 +soupsieve==2.8.1 stack-data==0.6.3 terminado==0.18.1 -tinycss2==1.3.0 -tomli==2.0.2 -tornado==6.4.1 +tinycss2==1.4.0 +tomli==2.3.0 ; python_full_version < '3.11' +tornado==6.5.4 traitlets==5.14.3 -types-python-dateutil==2.9.0.20241003 -typing-extensions==4.12.2 -tzdata==2024.2 +typing-extensions==4.15.0 +tzdata==2025.3 uri-template==1.3.0 -urllib3==2.2.3 -watchdog==5.0.3 -wcwidth==0.2.13 -webcolors==24.8.0 +urllib3==2.6.2 +watchdog==6.0.0 +wcwidth==0.2.14 +webcolors==24.11.1 ; python_full_version < '3.10' +webcolors==25.10.0 ; python_full_version >= '3.10' webencodings==0.5.1 -websocket-client==1.8.0 -zipp==3.20.2; python_version < "3.10" +websocket-client==1.9.0 +zipp==3.23.0 ; python_full_version < '3.10' diff --git a/requirements-locked.txt b/requirements-locked.txt index 7ed959e..6eff534 100644 --- a/requirements-locked.txt +++ b/requirements-locked.txt @@ -1,15 +1,18 @@ -# This file is @generated by PDM. -# Please do not edit it manually. - -appdirs==1.4.4 +# This file was autogenerated by uv via the following command: +# uv export -o requirements-locked.txt --no-hashes --no-dev --no-emit-project flexcache==0.3 -flexparser==0.3.1 +flexparser==0.4 globalwarmingpotentials==0.11.1 -numpy==2.0.2 -pandas==2.2.3 -pint==0.24.3 +numpy==2.0.2 ; python_full_version < '3.10' +numpy==2.2.6 ; python_full_version == '3.10.*' +numpy==2.3.5 ; python_full_version >= '3.11' +pandas==2.3.3 +pint==0.24.4 ; python_full_version < '3.11' +pint==0.25.2 ; python_full_version >= '3.11' +platformdirs==4.4.0 ; python_full_version < '3.10' +platformdirs==4.5.1 ; python_full_version >= '3.10' python-dateutil==2.9.0.post0 -pytz==2024.2 -six==1.16.0 -typing-extensions==4.12.2 -tzdata==2024.2 +pytz==2025.2 +six==1.17.0 +typing-extensions==4.15.0 +tzdata==2025.3 diff --git a/requirements-only-tests-locked.txt b/requirements-only-tests-locked.txt new file mode 100644 index 0000000..f958a17 --- /dev/null +++ b/requirements-only-tests-locked.txt @@ -0,0 +1,16 @@ +# This file was autogenerated by uv via the following command: +# uv export -o requirements-only-tests-locked.txt --no-hashes --no-dev --no-emit-project --only-group tests +colorama==0.4.6 ; sys_platform == 'win32' +coverage==7.10.7 ; python_full_version < '3.10' +coverage==7.13.0 ; python_full_version >= '3.10' +exceptiongroup==1.3.1 ; python_full_version < '3.11' +iniconfig==2.1.0 ; python_full_version < '3.10' +iniconfig==2.3.0 ; python_full_version >= '3.10' +packaging==25.0 +pluggy==1.6.0 +pygments==2.19.2 +pytest==8.4.2 ; python_full_version < '3.10' +pytest==9.0.2 ; python_full_version >= '3.10' +pytest-cov==7.0.0 +tomli==2.3.0 ; python_full_version <= '3.11' +typing-extensions==4.15.0 ; python_full_version < '3.11' diff --git a/requirements-only-tests-min-locked.txt b/requirements-only-tests-min-locked.txt new file mode 100644 index 0000000..6621ff0 --- /dev/null +++ b/requirements-only-tests-min-locked.txt @@ -0,0 +1,13 @@ +# This file was autogenerated by uv via the following command: +# uv export -o requirements-only-tests-min-locked.txt --no-hashes --no-dev --no-emit-project --only-group tests-min +colorama==0.4.6 ; sys_platform == 'win32' +exceptiongroup==1.3.1 ; python_full_version < '3.11' +iniconfig==2.1.0 ; python_full_version < '3.10' +iniconfig==2.3.0 ; python_full_version >= '3.10' +packaging==25.0 +pluggy==1.6.0 +pygments==2.19.2 +pytest==8.4.2 ; python_full_version < '3.10' +pytest==9.0.2 ; python_full_version >= '3.10' +tomli==2.3.0 ; python_full_version < '3.11' +typing-extensions==4.15.0 ; python_full_version < '3.11' diff --git a/requirements-upstream-dev.txt b/requirements-upstream-dev.txt new file mode 100644 index 0000000..f7c22b0 --- /dev/null +++ b/requirements-upstream-dev.txt @@ -0,0 +1,8 @@ +# Upstream packages can be listed in here. +# The basic pattern is +# @git+ +# e.g. +# pandas@git+https://github.com/pandas-dev/pandas + +# Pint default branch +pint@git+https://github.com/hgrecco/pint diff --git a/scripts/add-locked-targets-to-pyproject-toml.py b/scripts/add-locked-targets-to-pyproject-toml.py new file mode 100644 index 0000000..96514ab --- /dev/null +++ b/scripts/add-locked-targets-to-pyproject-toml.py @@ -0,0 +1,85 @@ +""" +Add locked targets to `pyproject.toml` + +This adds a "locked" target +plus a "