diff --git a/.github/publish-mastodon-template.md b/.github/publish-mastodon-template.md index 308374f11..02f628ae0 100644 --- a/.github/publish-mastodon-template.md +++ b/.github/publish-mastodon-template.md @@ -1,5 +1,5 @@ -New #xclim release: {{ .version }} πŸŽ‰ +New #xclim release: {{ .tag }} πŸŽ‰ -Latest source code available at: https://github.com/Ouranosinc/xclim/releases/tag/{{ .version }} +Latest source code available at: {{ .url }} Check out the docs for more information: https://xclim.readthedocs.io/en/stable/ diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index 2713d3e1c..861908194 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -8,6 +8,8 @@ on: - .* - .github/*/*.md - .github/*/*.yml + - .github/*.md + - .github/*.yml - CHANGELOG.rst - CI/*.in - CI/*.txt @@ -51,15 +53,15 @@ jobs: app-id: ${{ secrets.OURANOS_HELPER_BOT_ID }} private-key: ${{ secrets.OURANOS_HELPER_BOT_KEY }} - name: Checkout Repository - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: token: ${{ steps.token_generator.outputs.token }} - name: Set up Python3 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: "3.x" - name: Import GPG Key - uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 + uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6.2.0 with: gpg_private_key: ${{ secrets.OURANOS_HELPER_BOT_GPG_PRIVATE_KEY }} passphrase: ${{ secrets.OURANOS_HELPER_BOT_GPG_PRIVATE_KEY_PASSWORD }} diff --git a/.github/workflows/cache-cleaner.yml b/.github/workflows/cache-cleaner.yml index fc4462951..5fa3ee24b 100644 --- a/.github/workflows/cache-cleaner.yml +++ b/.github/workflows/cache-cleaner.yml @@ -25,7 +25,7 @@ jobs: objects.githubusercontent.com:443 - name: Checkout Repository - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Cleanup run: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 780b2c964..2f06fe3f5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -52,7 +52,7 @@ jobs: pypi.org:443 uploads.github.com:443 - name: Checkout Repository - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@1245696032ecf7d39f87d54daa406e22ddf769a8 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 280e97655..ad7af9083 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -26,7 +26,7 @@ jobs: github.com:443 - name: Checkout Repository - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Dependency Review - uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 + uses: actions/dependency-review-action@4081bf99e2866ebe428fc0477b69eb4fcda7220a # v4.4.0 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 359755ae4..4ae0707bd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -49,9 +49,9 @@ jobs: github.com:443 pypi.org:443 - name: Checkout Repository - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Python3 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: "3.x" cache: 'pip' @@ -59,7 +59,7 @@ jobs: run: | python -m pip install --require-hashes -r CI/requirements_ci.txt - name: Environment Caching - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: | ${{ matrix.testdata-cache }} @@ -95,9 +95,9 @@ jobs: pypi.org:443 raw.githubusercontent.com:443 - name: Checkout Repository - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Python${{ matrix.python-version }} - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: ${{ matrix.python-version }} cache: 'pip' @@ -105,7 +105,7 @@ jobs: run: | python -m pip install --require-hashes -r CI/requirements_ci.txt - name: Environment Caching - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: | ${{ matrix.testdata-cache }} @@ -193,14 +193,14 @@ jobs: pypi.org:443 raw.githubusercontent.com:443 - name: Checkout Repository - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install Eigen3 (SBCK) if: ${{ matrix.python-version == '3.11' && matrix.os == 'ubuntu-latest' }} run: | sudo apt-get update sudo apt-get install libeigen3-dev - name: Set up Python${{ matrix.python-version }} - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: ${{ matrix.python-version }} cache: 'pip' @@ -210,7 +210,7 @@ jobs: - name: Environment Caching # if prefetch is not in tox-env if: contains(matrix.tox-env, 'prefetch') == false - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: | ${{ matrix.testdata-cache }} @@ -267,7 +267,7 @@ jobs: raw.githubusercontent.com:443 repo.anaconda.com:443 - name: Checkout Repository - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Conda (Micromamba) with Python${{ matrix.python-version }} uses: mamba-org/setup-micromamba@617811f69075e3fd3ae68ca64220ad065877f246 # v2.0.0 with: @@ -280,7 +280,7 @@ jobs: run: | echo "micromamba: $(micromamba --version)" - name: Test Data Caching - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: | ${{ matrix.testdata-cache }} @@ -329,6 +329,6 @@ jobs: disable-sudo: true egress-policy: audit - name: Coveralls Finished - uses: coverallsapp/github-action@4cdef0b2dbe0c9aa26bed48edb868db738625e79 # v2.3.3 + uses: coverallsapp/github-action@cfd0633edbd2411b532b808ba7a8b5e04f76d2c8 # v2.3.4 with: parallel-finished: true diff --git a/.github/workflows/publish-mastodon.yml b/.github/workflows/publish-mastodon.yml index 2a4cd0e69..98a5df731 100644 --- a/.github/workflows/publish-mastodon.yml +++ b/.github/workflows/publish-mastodon.yml @@ -3,17 +3,7 @@ name: Publish Release Announcement to Mastodon on: release: types: - - published - workflow_dispatch: - inputs: - version-tag: - description: 'Version to announce' - required: true - type: string - dry-run: - description: 'Dry run' - default: true - type: boolean + - released permissions: contents: read @@ -35,25 +25,13 @@ jobs: github.com:443 - name: Checkout Repository - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - - - name: Current Version - if: ${{ !github.event.inputs.version-tag }} - run: | - CURRENT_VERSION="$(grep -E '__version__' xclim/__init__.py | cut -d ' ' -f3)" - echo "version=v${CURRENT_VERSION}" >> $GITHUB_ENV - - name: Set Version from Input - if: ${{ github.event.inputs.version-tag }} - run: | - echo "version=${{ github.event.inputs.version-tag }}" >> $GITHUB_ENV + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Get Release Description - if: ${{ !endsWith(env.current_version, '-dev') }} - id: get_release_description run: | # Fetch the release information using the GitHub API RELEASE_INFO=$(curl -sH "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ - "https://api.github.com/repos/${{ github.repository }}/releases/tags/${{ env.version }}") + "https://api.github.com/repos/${{ github.repository }}/releases/tags/${{ github.event.release.tag_name }}") # Extract the release description from the response RELEASE_DESCRIPTION=$(echo "$RELEASE_INFO" | jq -r .body) @@ -72,14 +50,14 @@ jobs: with: template: .github/publish-mastodon-template.md vars: | - version: ${{ env.version }} + tag: ${{ github.event.release.tag_name }} + url: https://github.com/Ouranosinc/xclim/releases/tag/${{ github.event.release.tag_name }} - name: Message Preview run: | echo "${{ steps.render_template.outputs.result }}${{ env.contributors }}" - name: Send toot to Mastodon - if: ${{ github.event.inputs.dry-run != 'true' || github.event_name == 'release' }} uses: cbrgm/mastodon-github-action@b26d62619432b20c2129edd86f07f7ede9797fc9 # v2.1.9 with: url: ${{ secrets.MASTODON_URL }} diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 62ae49a15..e257a4f20 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -26,11 +26,12 @@ jobs: files.pythonhosted.org:443 github.com:443 pypi.org:443 + ruf-repo-cdn.sigstore.dev:443 upload.pypi.org:443 - name: Checkout Repository - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Python3 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: "3.x" - name: Install CI libraries @@ -40,4 +41,4 @@ jobs: run: | python -m flit build - name: Publish distribution πŸ“¦ to PyPI - uses: pypa/gh-action-pypi-publish@f7600683efdcb7656dec5b29656edb7bc586e597 # v1.10.3 + uses: pypa/gh-action-pypi-publish@fb13cb306901256ace3dab689990e13a5550ffaa # v1.11.0 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index ce198d456..7965189a8 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -35,7 +35,7 @@ jobs: egress-policy: audit - name: Checkout Repository - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: persist-credentials: false diff --git a/.github/workflows/tag-testpypi.yml b/.github/workflows/tag-testpypi.yml index 60373dbad..ed02073b3 100644 --- a/.github/workflows/tag-testpypi.yml +++ b/.github/workflows/tag-testpypi.yml @@ -26,11 +26,12 @@ jobs: files.pythonhosted.org:443 github.com:443 pypi.org:443 + ruf-repo-cdn.sigstore.dev:443 test.pypi.org:443 - name: Checkout Repository - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Python3 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: "3.x" - name: Install CI libraries @@ -40,7 +41,7 @@ jobs: run: | python -m flit build - name: Publish distribution πŸ“¦ to Test PyPI - uses: pypa/gh-action-pypi-publish@f7600683efdcb7656dec5b29656edb7bc586e597 # v1.10.3 + uses: pypa/gh-action-pypi-publish@fb13cb306901256ace3dab689990e13a5550ffaa # v1.11.0 with: repository-url: https://test.pypi.org/legacy/ skip-existing: true diff --git a/.github/workflows/testdata-version.yml b/.github/workflows/testdata-version.yml index d8535a13f..20da4cbc6 100644 --- a/.github/workflows/testdata-version.yml +++ b/.github/workflows/testdata-version.yml @@ -30,7 +30,7 @@ jobs: api.github.com:443 github.com:443 - name: Checkout Repository - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Find xclim-testdata Tag and CI Testing Branch run: | XCLIM_TESTDATA_TAG="$( \ diff --git a/.github/workflows/upstream.yml b/.github/workflows/upstream.yml index 336d072c0..cbe84ef2f 100644 --- a/.github/workflows/upstream.yml +++ b/.github/workflows/upstream.yml @@ -54,7 +54,7 @@ jobs: raw.githubusercontent.com:443 repo.anaconda.com:443 - name: Checkout Repository - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 # Fetch all history for all branches and tags. - name: Setup Conda (Micromamba) with Python${{ matrix.python-version }} @@ -85,7 +85,7 @@ jobs: xclim show_version_info python -m pip check || true - name: Test Data Caching - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: | ${{ matrix.testdata-cache }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5a370c6b2..47c96bb8b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ default_language_version: repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.18.0 + rev: v3.19.0 hooks: - id: pyupgrade args: ['--py39-plus'] @@ -37,7 +37,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.0 + rev: v0.7.2 hooks: - id: ruff args: [ '--fix', '--show-fixes' ] @@ -65,7 +65,7 @@ repos: - id: nbqa-black additional_dependencies: [ 'black==24.10.0' ] - repo: https://github.com/kynan/nbstripout - rev: 0.7.1 + rev: 0.8.0 hooks: - id: nbstripout files: '.ipynb' @@ -99,7 +99,7 @@ repos: - id: codespell additional_dependencies: [ 'tomli' ] - repo: https://github.com/gitleaks/gitleaks - rev: v8.21.1 + rev: v8.21.2 hooks: - id: gitleaks - repo: https://github.com/python-jsonschema/check-jsonschema diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9cf5880f8..16cf6a9c6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,22 @@ Internal changes * `xclim` now uses a `src` layout for the codebase. Structure-dependent functions, documentation, and build commands have been adapted to reflect these changes. (:pull:`1971`). * Added a more robust `yamllint` configuration to ensure that all YAML files are linted consistently. (:pull:`1971`). +v0.53.2 (2024-10-31) +-------------------- +Contributors to this version: Γ‰ric Dupuis (:user:`coxipi`), Pascal Bourgault (:user:`aulemahal`), Trevor James Smith (:user:`Zeitsperre`). + +Breaking changes +^^^^^^^^^^^^^^^^ +* Due to a regression affecting symmetry of ``polyfit`` and ``polyval`` in `xarray`, `xclim` now requires `xarray>=2023.11.0,!=2024.10.0` (see: `pydata/xarray PR/9691 `_. (:pull:`1978`). + +Bug fixes +^^^^^^^^^ +* Fixed a bug where the units could be changed before a conversion of the magnitudes could occur. Conversion of units for multivariate ``DataArray`` is now properly handled in `sdba.TrainAdjust` and `sdba.Adjust`. (:pull:`1972`). +* Fixed a units formatting bug with indicators that output "delta" Celsius degrees. (:pull:`1973`). +* Corrected the ``"choices"`` of parameter ``op`` in the docstring of ``frost_free_spell_max_length``. (:pull:`1977`). +* Reorganised how ``Indicator`` subclasses can added arguments to the call signature. Injecting such arguments now works. For xclim's subclasses, this bug only affected the ``indexer`` argument of indicators subclassing ``xc.core.indicator.IndexingIndicator``. (:pull:`1981`). +* All-nan slices are now treated correctly in method `ExtremeValues`. (:issue:`1982`, :pull:`1983`). + v0.53.1 (2024-10-21) -------------------- Contributors to this version: Trevor James Smith (:user:`Zeitsperre`). diff --git a/CI/requirements_ci.in b/CI/requirements_ci.in index 648516b9e..dbff75495 100644 --- a/CI/requirements_ci.in +++ b/CI/requirements_ci.in @@ -1,7 +1,7 @@ -bump-my-version==0.28.0 +bump-my-version==0.28.1 deptry==0.20.0 -flit==3.9.0 -pip==24.2.0 +flit==3.10.1 +pip==24.3.1 pylint==3.3.1 -tox==4.23.0 +tox==4.23.2 tox-gh==1.4.4 diff --git a/CI/requirements_ci.txt b/CI/requirements_ci.txt index 61bc9eb86..14c8334ea 100644 --- a/CI/requirements_ci.txt +++ b/CI/requirements_ci.txt @@ -16,9 +16,9 @@ bracex==2.4 \ --hash=sha256:a27eaf1df42cf561fed58b7a8f3fdf129d1ea16a81e1fadd1d17989bc6384beb \ --hash=sha256:efdc71eff95eaff5e0f8cfebe7d01adf2c8637c8c92edaf63ef348c241a82418 # via wcmatch -bump-my-version==0.28.0 \ - --hash=sha256:cc84ace477022a4cc8c401ef5c035f2f752df45488be90ccb764a47f7de0e395 \ - --hash=sha256:ff3cb51bb15509ae8ebb8e8efa3eaa7c02209677f45457c8b007ef2f5bef7179 +bump-my-version==0.28.1 \ + --hash=sha256:df7fdb02a1b43c122a6714df6d1fe4efc7a1220b5638ca5a0eb3018813c1b222 \ + --hash=sha256:e608def5191baf505b6cde88bd679a0a95fc4cfeace4247adb60ac0f8a7e57ee # via -r CI/requirements_ci.in cachetools==5.5.0 \ --hash=sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292 \ @@ -167,13 +167,13 @@ filelock==3.16.1 \ # via # tox # virtualenv -flit==3.9.0 \ - --hash=sha256:076c3aaba5ac24cf0ad3251f910900d95a08218e6bcb26f21fef1036cc4679ca \ - --hash=sha256:d75edf5eb324da20d53570a6a6f87f51e606eee8384925cd66a90611140844c7 +flit==3.10.1 \ + --hash=sha256:9c6258ae76d218ce60f9e39a43ca42006a3abcc5c44ea6bb2a1daa13857a8f1a \ + --hash=sha256:d79c19c2caae73cc486d3d827af6a11c1a84b9efdfab8d9683b714ec8d1dc1f1 # via -r CI/requirements_ci.in -flit-core==3.9.0 \ - --hash=sha256:72ad266176c4a3fcfab5f2930d76896059851240570ce9a98733b658cb786eba \ - --hash=sha256:7aada352fb0c7f5538c4fafeddf314d3a6a92ee8e2b1de70482329e42de70301 +flit-core==3.10.1 \ + --hash=sha256:66e5b87874a0d6e39691f0e22f09306736b633548670ad3c09ec9db03c5662f7 \ + --hash=sha256:cb31a76e8b31ad3351bb89e531f64ef2b05d1e65bd939183250bf81ddf4922a8 # via flit idna==3.7 \ --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \ @@ -369,9 +369,9 @@ tomlkit==0.13.0 \ # via # bump-my-version # pylint -tox==4.23.0 \ - --hash=sha256:46da40afb660e46238c251280eb910bdaf00b390c7557c8e4bb611f422e9db12 \ - --hash=sha256:a6bd7d54231d755348d3c3a7b450b5bf6563833716d1299a1619587a1b77a3bf +tox==4.23.2 \ + --hash=sha256:452bc32bb031f2282881a2118923176445bac783ab97c874b8770ab4c3b76c38 \ + --hash=sha256:86075e00e555df6e82e74cfc333917f91ecb47ffbc868dcafbd2672e332f4a2c # via # -r CI/requirements_ci.in # tox-gh diff --git a/docs/notebooks/example/example.yml b/docs/notebooks/example/example.yml index 6b5201b5e..30cdb575c 100644 --- a/docs/notebooks/example/example.yml +++ b/docs/notebooks/example/example.yml @@ -12,10 +12,13 @@ variables: description: Precipitation flux on the outer surface of the forest standard_name: precipitation_flux_onto_canopy indicators: - RX1day: + RX1day_summer: base: rx1day cf_attrs: long_name: Highest 1-day precipitation amount + parameters: + indexer: + month: [5, 6, 7, 8, 9] context: hydro RX5day_canopy: base: max_n_day_precipitation_amount diff --git a/docs/notebooks/extendxclim.ipynb b/docs/notebooks/extendxclim.ipynb index 8293e3ba5..179bc200e 100644 --- a/docs/notebooks/extendxclim.ipynb +++ b/docs/notebooks/extendxclim.ipynb @@ -4,7 +4,8 @@ "cell_type": "code", "execution_count": null, "metadata": { - "nbsphinx": "hidden" + "nbsphinx": "hidden", + "tags": [] }, "outputs": [], "source": [ @@ -89,7 +90,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "import xarray as xr\n", @@ -214,7 +217,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "from xclim.core.indicator import registry\n", @@ -231,7 +236,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "display(Code(tg_mean_c.__doc__, language=\"rst\"))" @@ -249,7 +256,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "tg_mean_c.__module__ == xclim.atmos.tg_mean.__module__" @@ -265,7 +274,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# Passing module\n", @@ -277,7 +288,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# Conventional class inheritance, uses the current module name\n", @@ -319,7 +332,8 @@ "cell_type": "code", "execution_count": null, "metadata": { - "nbsphinx": "hidden" + "nbsphinx": "hidden", + "tags": [] }, "outputs": [], "source": [ @@ -338,7 +352,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# These variables were generated by a hidden cell above that syntax-colored them.\n", @@ -362,7 +378,7 @@ "\n", "\n", "\n", - "- `RX1day` is simply the same as `registry['RX1DAY']`, but with an updated `long_name`.\n", + "- `RX1day` is as `registry['RX1DAY']`, but with an updated `long_name` and an injected argument : its `indexer` arg is now set to only compute over may to september.\n", "- `RX5day_canopy` is based on `registry['MAX_N_DAY_PRECIPITATION_AMOUNT']`, changed the `long_name` and injects the `window` and `freq` arguments.\n", " * It also requests a different variable than the original indicator : `prveg` instead of `pr`. As xclim doesn't know about `prveg`, a definition is given in the `variables` section.\n", "- `R75pdays` is based on `registry['DAYS_OVER_PRECIP_THRESH']`, injects the `thresh` argument and changes the description of the `per` argument.\n", @@ -397,7 +413,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "from importlib.resources import files\n", @@ -431,7 +449,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "import xclim as xc\n", @@ -444,7 +464,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "docstring = f\"{example.__doc__}\\n---\\n\\n{xc.indicators.example.R99p.__doc__}\"\n", @@ -461,7 +483,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "from xclim.testing import open_dataset\n", @@ -505,7 +529,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "out = xr.merge(outs)\n", @@ -527,7 +553,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "from xclim.core.indicator import build_indicator_module, registry\n", @@ -565,7 +593,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "print(xc.indicators.awesome.__doc__)" @@ -575,7 +605,8 @@ "cell_type": "code", "execution_count": null, "metadata": { - "nbsphinx": "hidden" + "nbsphinx": "hidden", + "tags": [] }, "outputs": [], "source": [ @@ -597,7 +628,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.5" + "version": "3.12.6" }, "toc": { "base_numbering": 1, diff --git a/environment.yml b/environment.yml index 0a9e3f28f..a7d54a980 100644 --- a/environment.yml +++ b/environment.yml @@ -22,7 +22,7 @@ dependencies: - scikit-learn >=1.1.0 - scipy >=1.9.0 - statsmodels >=0.14.2 - - xarray >=2023.11.0 + - xarray >=2023.11.0,!=2024.10.0 - yamale >=5.0.0 # Extras - fastnanquantile >=0.0.2 diff --git a/pyproject.toml b/pyproject.toml index 29575f9ad..6ba537775 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ dependencies = [ "scikit-learn >=1.1.0", "scipy >=1.9.0", "statsmodels >=0.14.2", - "xarray >=2023.11.0", + "xarray >=2023.11.0,!=2024.10.0", "yamale >=5.0.0" ] @@ -61,7 +61,7 @@ dev = [ # Dev tools and testing "black[jupyter] ==24.10.0", "blackdoc ==0.3.9", - "bump-my-version ==0.28.0", + "bump-my-version ==0.28.1", "codespell ==2.3.0", "coverage[toml] >=7.5.0", "coveralls >=4.0.1", # coveralls is not yet compatible with Python 3.13 @@ -135,7 +135,7 @@ target-version = [ ] [tool.bumpversion] -current_version = "0.53.1" +current_version = "0.53.2" commit = true commit_args = "--no-verify" tag = false diff --git a/src/xclim/__init__.py b/src/xclim/__init__.py index d7ba1dfc8..2680845d2 100644 --- a/src/xclim/__init__.py +++ b/src/xclim/__init__.py @@ -13,7 +13,7 @@ __author__ = """Travis Logan""" __email__ = "logan.travis@ouranos.ca" -__version__ = "0.53.1" +__version__ = "0.53.2" with _resources.as_file(_resources.files("xclim.data")) as _module_data: diff --git a/src/xclim/core/indicator.py b/src/xclim/core/indicator.py index 3023eb906..1822d04c4 100644 --- a/src/xclim/core/indicator.py +++ b/src/xclim/core/indicator.py @@ -88,6 +88,8 @@ In the following, the section under `` is referred to as `data`. When creating indicators from a dictionary, with :py:meth:`Indicator.from_dict`, the input dict must follow the same structure of `data`. +Note that kwargs-like parameters like ``indexer`` must be injected as a dictionary (``param data`` above should be a dictionary). + When a module is built from a yaml file, the yaml is first validated against the schema (see xclim/data/schema.yml) using the YAMALE library (:cite:p:`lopker_yamale_2022`). See the "Extending xclim" notebook for more info. @@ -301,7 +303,7 @@ class Indicator(IndicatorRegistrar): Both are simply views of :py:attr:`xclim.core.indicator.Indicator._all_parameters`. Compared to their base `compute` function, indicators add the possibility of using dataset as input, - with the injected argument `ds` in the call signature. All arguments that were indicated + with the added argument `ds` in the call signature. All arguments that were indicated by the compute function to be variables (DataArrays) through annotations will be promoted to also accept strings that correspond to variable names in the `ds` dataset. @@ -425,12 +427,13 @@ def __new__(cls, **kwds): # noqa: C901 # title, abstract, references, notes, long_name kwds.setdefault(name, value) - # Inject parameters (subclasses can override or extend this through _injected_parameters) - for name, param in cls._injected_parameters(): + # Added parameters + # Subclasses can override or extend this through the classmethod _added_parameters + for name, param in cls._added_parameters(): if name in parameters: raise ValueError( f"Class {cls.__name__} can't wrap indices that have a `{name}`" - " argument as it conflicts with arguments it injects." + " argument as it conflicts with an argument it adds." ) parameters[name] = param else: # inherit parameters from base class @@ -529,8 +532,13 @@ def _parse_indice(compute, passed_parameters): # noqa: F841 return parameters, docmeta @classmethod - def _injected_parameters(cls): - """Create a list of tuples for arguments to inject, (name, Parameter).""" + def _added_parameters(cls): + """Create a list of tuples for arguments to add to the call signature (name, Parameter). + + These can't be in the compute function signature, the class is in charge of removing them + from the params passed to the compute function, likely through an override of + _preprocess_and_checks. + """ return [ ( "ds", @@ -853,7 +861,7 @@ def __call__(self, *args, **kwds): # Convert to output units outs = [ - convert_units_to(out, attrs["units"], self.context) + convert_units_to(out, attrs, self.context) for out, attrs in zip(outs, out_attrs, strict=False) ] @@ -949,7 +957,7 @@ def _preprocess_and_checks(self, das, params): def _get_compute_args(self, das, params): """Rename variables and parameters to match the compute function's names and split VAR_KEYWORD arguments.""" # Get correct variable names for the compute function. - # Exclude param without a mapping inside the compute functions (those injected by the indicator class) + # Exclude param without a mapping inside the compute functions (those added by the indicator class) args = {} for key, p in self._all_parameters.items(): if p.compute_name is not _empty: @@ -1534,18 +1542,24 @@ def _preprocess_and_checks(self, das, params): class IndexingIndicator(Indicator): - """Indicator that also injects "indexer" kwargs to subset the inputs before computation.""" - - def __init__(self, *args, **kwargs): - self._all_parameters["indexer"] = Parameter( - kind=InputKind.KWARGS, - description=( - "Indexing parameters to compute the indicator on a temporal " - "subset of the data. It accepts the same arguments as " - ":py:func:`xclim.indices.generic.select_time`." - ), - ) - super().__init__(*args, **kwargs) + """Indicator that also adds the "indexer" kwargs to subset the inputs before computation.""" + + @classmethod + def _added_parameters(cls): + """Create a list of tuples for arguments to add (name, Parameter).""" + return super()._added_parameters() + [ + ( + "indexer", + Parameter( + kind=InputKind.KWARGS, + description=( + "Indexing parameters to compute the indicator on a temporal " + "subset of the data. It accepts the same arguments as " + ":py:func:`xclim.indices.generic.select_time`." + ), + ), + ) + ] def _preprocess_and_checks(self, das: dict[str, DataArray], params: dict[str, Any]): """Perform parent's checks and also check if freq is allowed.""" @@ -1559,7 +1573,7 @@ def _preprocess_and_checks(self, das: dict[str, DataArray], params: dict[str, An class ResamplingIndicatorWithIndexing(ResamplingIndicator, IndexingIndicator): - """Resampling indicator that also injects "indexer" kwargs to subset the inputs before computation.""" + """Resampling indicator that also adds "indexer" kwargs to subset the inputs before computation.""" class Daily(ResamplingIndicator): diff --git a/src/xclim/indices/_threshold.py b/src/xclim/indices/_threshold.py index e58262718..dba665096 100644 --- a/src/xclim/indices/_threshold.py +++ b/src/xclim/indices/_threshold.py @@ -1426,8 +1426,8 @@ def frost_free_spell_max_length( Minimum number of days with temperatures above thresholds to qualify as a frost free day. freq : str Resampling frequency. - op : {"<", "<=", "lt", "le"} - Comparison operation. Default: "<". + op : {">", ">=", "gt", "ge"} + Comparison operation. Default: ">=". resample_before_rl : bool Determines if the resampling should take place before or after the run length encoding (or a similar algorithm) is applied to runs. diff --git a/src/xclim/sdba/_adjustment.py b/src/xclim/sdba/_adjustment.py index 431df0195..ed7b4e36c 100644 --- a/src/xclim/sdba/_adjustment.py +++ b/src/xclim/sdba/_adjustment.py @@ -778,10 +778,14 @@ def _fit_on_cluster(data, thresh, dist, cluster_thresh): def _extremes_train_1d(ref, hist, ref_params, *, q_thresh, cluster_thresh, dist, N): """Train for method ExtremeValues, only for 1D input along time.""" + # Fast-track, do nothing for all-nan slices + if all(np.isnan(ref)) or all(np.isnan(hist)): + return np.full(N, np.nan), np.full(N, np.nan), np.nan + # Find quantile q_thresh thresh = ( - np.quantile(ref[ref >= cluster_thresh], q_thresh) - + np.quantile(hist[hist >= cluster_thresh], q_thresh) + np.nanquantile(ref[ref >= cluster_thresh], q_thresh) + + np.nanquantile(hist[hist >= cluster_thresh], q_thresh) ) / 2 # Fit genpareto on cluster maximums on ref (if needed) and hist. diff --git a/src/xclim/sdba/adjustment.py b/src/xclim/sdba/adjustment.py index 4925e2949..5c8506071 100644 --- a/src/xclim/sdba/adjustment.py +++ b/src/xclim/sdba/adjustment.py @@ -180,12 +180,21 @@ def __convert_units_to(_input_da, _internal_dim, _internal_target): v: _inputs[0][_dim].attrs["_units"][iv] for iv, v in enumerate(_inputs[0][_dim].values) } - return ( - __convert_units_to( - _input_da, _internal_dim=_dim, _internal_target=_target + + # `__convert_units_to`` was changing the units of the 3rd dataset during the 2nd loop + # This explicit loop is designed to avoid this + _outputs = [] + original_units = list( + [_inp[_dim].attrs["_units"].copy() for _inp in _inputs] + ) + for _inp, units in zip(_inputs, original_units, strict=False): + _inp[_dim].attrs["_units"] = units + _outputs.append( + __convert_units_to( + _inp, _internal_dim=_dim, _internal_target=_target + ) ) - for _input_da in _inputs - ), _target + return _outputs, _target for dim, crd in inputs[0].coords.items(): if crd.attrs.get("is_variables"): diff --git a/tests/test_indicators.py b/tests/test_indicators.py index 997d2719b..d61b77bd7 100644 --- a/tests/test_indicators.py +++ b/tests/test_indicators.py @@ -286,6 +286,20 @@ def test_temp_unit_conversion(tas_series): np.testing.assert_array_almost_equal(txk, txc + 273.15) +def test_temp_diff_unit_conversion(tasmax_series, tasmin_series): + tx = tasmax_series(np.arange(365) + 1, start="2001-01-01") + tn = tasmin_series(np.arange(365), start="2001-01-01") + txC = convert_units_to(tx, "degC") + tnC = convert_units_to(tn, "degC") + + ind = xclim.atmos.daily_temperature_range.from_dict( + {"units": "degC"}, "dtr_degC", "test" + ) + out = ind(tasmax=txC, tasmin=tnC) + assert out.attrs["units"] == "degC" + assert out.attrs["units_metadata"] == "temperature: difference" + + def test_multiindicator(tas_series): tas = tas_series(np.arange(366), start="2000-01-01") tmin, tmax = multiTemp(tas, freq="YS") diff --git a/tests/test_modules.py b/tests/test_modules.py index c5f962414..37f5cbfcf 100644 --- a/tests/test_modules.py +++ b/tests/test_modules.py @@ -104,6 +104,12 @@ def test_custom_indices(open_dataset): example_path / "example.yml", name="ex4", mode="ignore" ) + # Check that indexer was added and injected correctly + assert "indexer" not in ex1.RX1day_summer.parameters + assert ex1.RX1day_summer.injected_parameters["indexer"] == { + "month": [5, 6, 7, 8, 9] + } + @pytest.mark.requires_docs def test_indicator_module_translations(): diff --git a/tests/test_sdba/test_adjustment.py b/tests/test_sdba/test_adjustment.py index f7d1c47f8..706bc5a29 100644 --- a/tests/test_sdba/test_adjustment.py +++ b/tests/test_sdba/test_adjustment.py @@ -725,6 +725,8 @@ def dist(ref, sim): assert (ref - scen).mean().tasmin < 5e-3 +# TODO: below we use `.adjust(scen,sim)`, but in the function signature, `sim` comes before +# are we testing the right thing below? class TestExtremeValues: @pytest.mark.parametrize( "c_thresh,q_thresh,frac,power", @@ -805,6 +807,19 @@ def test_real_data(self, open_dataset): new_scen = EX.adjust(scen, hist, frac=0.000000001) new_scen.load() + def test_nan_values(self): + times = xr.cftime_range("1990-01-01", periods=365, calendar="noleap") + ref = xr.DataArray( + np.arange(365), + dims=("time"), + coords={"time": times}, + attrs={"units": "mm/day"}, + ) + hist = (ref.copy() * np.nan).assign_attrs(ref.attrs) + EX = ExtremeValues.train(ref, hist, cluster_thresh="10 mm/day", q_thresh=0.9) + new_scen = EX.adjust(sim=hist, scen=ref) + assert new_scen.isnull().all() + class TestOTC: def test_compare_sbck(self, random, series):