Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Contributors to this version: Éric Dupuis (:user:`coxipi`), Trevor James Smith
Breaking changes
^^^^^^^^^^^^^^^^
* `lmoments3` is now listed as a dependency to the extras recipe. This dependency is not installed by default with `xclim` and must be explicitly requested with ``$ pip install "xclim[extras]"``, if desired. (:pull:`2269`).
* `numpy`-related `RuntimeWarnings` for invalid operations are noisier when running calculations via the ``xclim.indices`` and muted by default for ``xclim.indicators``. This change is made to ensure that users who perform indice calculations can be made more aware of potential inconsistencies in their source datasets (:issue:`2277`, :pull:`2276`).

Bug fixes
^^^^^^^^^
Expand All @@ -18,6 +19,9 @@ Internal changes
^^^^^^^^^^^^^^^^
* Replaced the ``tox.ini`` file with a ``tox.toml`` file and simplified the conditionals for environment selection. (:pull:`2269`).
* Removed `python-coveralls` from the `tox`-only dependencies (abandoned software / not supported for Python 3.13 or 3.14) and added the `coverallsapp/github-action` step to PyPI/tox builds on CI. (:pull:`2269`).
* The testing suite has been updated to support `pytest >=9.0` (:pull:`2276`):
* The configuration in `tox.toml` now uses the new TOML conventions.
* `--strict-config` and `--strict-markers` have been replaced with the new `--strict` mode. For more information, refer to the `pytest documentation <https://docs.pytest.org/en/stable/reference/reference.html#confval-strict>`_.

v0.59.1 (2025-10-31)
--------------------
Expand Down
10 changes: 5 additions & 5 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ dependencies:
- coverage >=7.5.0
- deptry >=0.23.0
- distributed >=2.0
- flake8 >=7.2.0
- flake8-rst-docstrings >=0.3.1
- flake8 >=7.3.0
- flake8-rst-docstrings >=0.4.0
- flit >=3.11.0,<4.0
- furo >=2023.9.10
- h5netcdf >=1.5.0
Expand All @@ -57,7 +57,7 @@ dependencies:
- pre-commit >=3.7
- pybtex >=0.24.0
- pylint >=3.3.1
- pytest >=8.0.0
- pytest >=9.0.0
- pytest-cov >=5.0.0
- pytest-socket >=0.6.0
- pytest-timeout >=2.4.0
Expand All @@ -72,10 +72,10 @@ dependencies:
- sphinxcontrib-bibtex
- sphinxcontrib-svg2pdfconverter
- tokenize-rt >=5.2.0
- tox >=4.25.0
- tox >=4.31.0
- tox-gh >=1.5.0
- vulture =2.14
- xdoctest >=1.1.5
- xdoctest >=1.3.0
- yamllint >=1.35.1
# Temporary Fixes
- importlib-metadata <8.7.0 # Issues with dask >=2025.5.1 on Python3.11
39 changes: 19 additions & 20 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ dev = [
"codespell >=2.4.1",
"coverage[toml] >=7.5.0",
"deptry >=0.23.0",
"flake8 >=7.2.0",
"flake8-rst-docstrings >=0.3.1",
"flake8 >=7.3.0",
"flake8-rst-docstrings >=0.4.0",
"h5netcdf >=1.5.0",
"h5py >=3.12.1",
"ipython >=8.10.0",
Expand All @@ -75,17 +75,17 @@ dev = [
"pooch >=1.8.0",
"pre-commit >=3.7",
"pylint >=3.3.1",
"pytest >=8.0.0",
"pytest >=9.0.0",
"pytest-cov >=5.0.0",
"pytest-socket >=0.6.0",
"pytest-timeout >=2.4.0",
"pytest-xdist[psutil] >=3.2",
"ruff >=0.13.3",
"tokenize-rt >=5.2.0",
"tox >=4.25.0",
"tox >=4.31.0",
"tox-gh >=1.5.0",
"vulture >=2.14",
"xdoctest >=1.1.5",
"xdoctest >=1.3.0",
"yamllint >=1.35.1"
]
docs = [
Expand Down Expand Up @@ -263,34 +263,33 @@ override_SS05 = [
'^Statistics '
]

[tool.pytest.ini_options]
minversion = "7.0"
[tool.pytest]
minversion = "9.0"
addopts = [
"-ra",
"--color=yes",
"--numprocesses=0",
"--maxprocesses=8",
"--dist=worksteal",
"--strict-config",
"--strict-markers"
]
log_cli_level = "INFO"
norecursedirs = ["docs/notebooks/*"]
filterwarnings = ["ignore::UserWarning"]
testpaths = [
"tests"
]
pythonpath = [
"src"
"--strict"
]
doctest_optionflags = ["NORMALIZE_WHITESPACE", "IGNORE_EXCEPTION_DETAIL", "NUMBER", "ELLIPSIS"]
filterwarnings = ["ignore::UserWarning"]
log_cli_level = "INFO"
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"requires_docs: mark tests that can only be run with documentation present (deselect with '-m \"not requires_docs\"')",
"requires_internet: mark tests that require internet access (deselect with '-m \"not requires_internet\"')"
]
session_timeout = 900
timeout = 300
norecursedirs = ["docs/notebooks/*"]
pythonpath = [
"src"
]
session_timeout = "900" # must be string representing seconds (this may soon change)
testpaths = [
"tests"
]
timeout = "300" # must be string representing seconds (this may soon change)
timeout_method = "thread"
xfail_strict = true

Expand Down
3 changes: 1 addition & 2 deletions src/xclim/analog.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,8 +579,7 @@ def kldiv(x: np.ndarray, y: np.ndarray, *, k: int | Sequence[int] = 1) -> float
# The 0th nearest neighbour of x[i] in x is x[i] itself.
# Hence, we take the k'th + 1, which in 0-based indexing is given by
# index k.
with np.errstate(divide="ignore"):
ki_calc = -np.log(r[:, ki] / s[:, ki - 1]).sum() * d / nx + np.log(ny / (nx - 1.0))
ki_calc = -np.log(r[:, ki] / s[:, ki - 1]).sum() * d / nx + np.log(ny / (nx - 1.0))
out.append(ki_calc)

if mk:
Expand Down
1 change: 1 addition & 0 deletions src/xclim/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ def cli(ctx, **kwargs): # numpydoc ignore=PR01
if not kwargs["verbose"]:
warnings.simplefilter("ignore", FutureWarning)
warnings.simplefilter("ignore", DeprecationWarning)
warnings.simplefilter("ignore", RuntimeWarning)

if kwargs["version"]:
click.echo(f"xclim {xc.__version__}")
Expand Down
2 changes: 1 addition & 1 deletion src/xclim/core/indicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -894,7 +894,7 @@ def __call__(self, *args, **kwds):

# get mappings where keys are the actual compute function's argument names
args = self._get_compute_args(das, params)
with xarray.set_options(keep_attrs=False):
with xarray.set_options(keep_attrs=False), np.errstate(divide="ignore", invalid="ignore"):
outs = self.compute(**args)

if isinstance(outs, DataArray):
Expand Down
2 changes: 1 addition & 1 deletion src/xclim/ensembles/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def create_ensemble(

dim = xr.IndexVariable("realization", list(realizations), attrs={"axis": "E"})

ens = xr.concat(ds, dim)
ens = xr.concat(ds, dim, compat="no_conflicts", join="outer")
for var_name, var in ds[0].variables.items():
ens[var_name].attrs.update(**var.attrs)
ens.attrs.update(**ds[0].attrs)
Expand Down
15 changes: 7 additions & 8 deletions src/xclim/indices/fire/_cffwis.py
Original file line number Diff line number Diff line change
Expand Up @@ -848,14 +848,13 @@ def _fire_weather_calc( # noqa: C901 # pylint: disable=R0912, R0915
ind_prevs["DMC"],
)
if "FFMC" in outputs:
with np.errstate(divide="ignore", invalid="ignore"):
out["FFMC"][..., it] = _fine_fuel_moisture_code(
tas[..., it],
pr[..., it],
ws[..., it],
rh[..., it],
ind_prevs["FFMC"],
)
out["FFMC"][..., it] = _fine_fuel_moisture_code(
tas[..., it],
pr[..., it],
ws[..., it],
rh[..., it],
ind_prevs["FFMC"],
)
if "ISI" in outputs:
out["ISI"][..., it] = initial_spread_index(ws[..., it], out["FFMC"][..., it])
if "BUI" in outputs:
Expand Down
17 changes: 6 additions & 11 deletions src/xclim/indices/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,8 @@ def _fitfunc_1d(arr, *, dist, nparams, method, **fitkwargs):

# Estimate parameters
if method in ["ML", "MLE"]:
with np.errstate(invalid="ignore"):
with np.errstate(divide="ignore"):
args, kwargs = _fit_start(x, dist.name, **fitkwargs)
params = dist.fit(x, *args, method="mle", **kwargs, **fitkwargs)
args, kwargs = _fit_start(x, dist.name, **fitkwargs)
params = dist.fit(x, *args, method="mle", **kwargs, **fitkwargs)
elif method == "MM":
params = dist.fit(x, method="mm", **fitkwargs)
elif method in ["MSE", "MPS"]:
Expand All @@ -63,8 +61,8 @@ def _fitfunc_1d(arr, *, dist, nparams, method, **fitkwargs):
for i, arg in enumerate(args):
guess[param_info[i]] = arg

fitresult = scipy.stats.fit(dist=dist, data=x, method="mse", guess=guess, **fitkwargs)
params = fitresult.params
fit_result = scipy.stats.fit(dist=dist, data=x, method="mse", guess=guess, **fitkwargs)
params = fit_result.params
elif method == "PWM":
# lmoments3 will raise an error if only dist.numargs + 2 values are provided
if len(x) <= dist.numargs + 2:
Expand Down Expand Up @@ -618,11 +616,8 @@ def _fit_start(x, dist: str, **fitkwargs: Any) -> tuple[tuple, dict]:
x_pos = x_pos[x_pos > 0]
# MLE estimation
log_x_pos = np.log(x_pos)
# ignore invalid values occurring in the log calculations
with np.errstate(invalid="ignore"), warnings.catch_warnings():
shape0 = log_x_pos.std()
warnings.filterwarnings("ignore", message="Mean of empty slice.", category=RuntimeWarning)
scale0 = np.exp(log_x_pos.mean())
shape0 = log_x_pos.std()
scale0 = np.exp(log_x_pos.mean())
kwargs = {"scale": scale0, "loc": loc0}
return (shape0,), kwargs

Expand Down
8 changes: 7 additions & 1 deletion tests/test_analog.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,13 @@ def test_spatial_analogs(method, open_dataset):
target = data.sel(lat=46.1875, lon=-72.1875, time=slice("1970", "1990"))
candidates = data.sel(time=slice("1970", "1990"))

out = xca.spatial_analogs(target, candidates, method=method)
# Ensure division by 0 operation is announced for kldiv
if method == "kldiv":
with pytest.warns(RuntimeWarning):
out = xca.spatial_analogs(target, candidates, method=method)
else:
out = xca.spatial_analogs(target, candidates, method=method)

# Special case since scikit-learn updated to 1.2.0 (and again at 1.3)
if method == "friedman_rafsky":
diss[method][42, 105] = np.nan
Expand Down
4 changes: 2 additions & 2 deletions tests/test_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def test_tas_temperature_flags(self, vars_dropped, flags, tas_series, tasmax_ser

def test_pr_precipitation_flags(self, pr_series):
# Pint <=0.24.4 has precision errors : (1 / 3600 / 24 kg m-2 s-1 = 0.999999998 mm/d )
pytest.importorskip("pint", minversion="0.25")
pytest.importorskip("pint", minversion="0.25", reason="Precipitation flag calculations require newer `pint`")
pr = pr_series(np.zeros(365), start="1971-01-01")
pr += 1 / 3600 / 24
pr[0:7] += 10 / 3600 / 24
Expand All @@ -58,7 +58,7 @@ def test_pr_precipitation_flags(self, pr_series):

def test_suspicious_pr_data(self, pr_series):
# Pint <=0.24.4 has precision errors : (1 / 3600 / 24 kg m-2 s-1 = 0.999999998 mm/d )
pytest.importorskip("pint", minversion="0.25")
pytest.importorskip("pint", minversion="0.25", reason="Precipitation flag calculations require newer `pint`")
bad_pr = pr_series(np.zeros(365), start="1971-01-01")

# Add some strangeness
Expand Down
26 changes: 19 additions & 7 deletions tests/test_indices.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from __future__ import annotations

import calendar
import warnings

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -474,7 +473,7 @@ def test_huglin_index(self, method, end_date, freq, values, cap_value, open_data
else:
if method == "icclim":
# The 'icclim' method is an alias for 'huglin'
with pytest.warns(DeprecationWarning):
with pytest.warns(DeprecationWarning) as record:
# find lat implicitly
hi = xci.huglin_index(
tasmax=tasmax,
Expand All @@ -484,6 +483,7 @@ def test_huglin_index(self, method, end_date, freq, values, cap_value, open_data
freq=freq,
cap_value=cap_value,
)
assert "Method 'icclim' is deprecated. Use 'stepwise' instead" in record[0].message.args[0]
else:
# find lat implicitly
hi = xci.huglin_index(
Expand Down Expand Up @@ -744,7 +744,7 @@ def test_standardized_precipitation_index(
self, freq, window, dist, method, values, diff_tol, open_dataset, no_numbagg
):
if method == "ML" and freq == "D" and Version(__numpy_version__) < Version("2.0.0"):
pytest.skip("Skipping SPI/ML/D on older numpy")
pytest.skip("Skipping SPI/ML/D for numpy below v2.0")

# change `dist` to a lmoments3 object if needed
if method == "PWM":
Expand Down Expand Up @@ -882,7 +882,7 @@ def test_standardized_precipitation_evapotranspiration_index(
self, freq, window, dist, method, values, diff_tol, open_dataset
):
if method == "ML" and freq == "D" and Version(__numpy_version__) < Version("2.0.0"):
pytest.skip("Skipping SPI/ML/D on older numpy")
pytest.skip("Skipping SPI/ML/D for numpy below v2.0")

ds = open_dataset("sdba/CanESM2_1950-2100.nc").isel(location=1).sel(time=slice("1950", "2000"))
pr = ds.pr
Expand Down Expand Up @@ -1168,7 +1168,7 @@ def test_standardized_streamflow_index(
)
def test_standardized_groundwater_index(self, freq, window, dist, method, values, diff_tol, open_dataset):
if method == "ML" and freq == "D" and Version(__numpy_version__) < Version("2.0.0"):
pytest.skip("Skipping SPI/ML/D on older numpy")
pytest.skip("Skipping SPI/ML/D for numpy below v2.0")
ds = open_dataset("Raven/gwl_obs.nc")
gwl0 = ds.gwl

Expand All @@ -1178,8 +1178,20 @@ def test_standardized_groundwater_index(self, freq, window, dist, method, values
fitkwargs = {}
if method == "APP":
fitkwargs["floc"] = 0
with warnings.catch_warnings():
warnings.filterwarnings("ignore", message="Degrees of freedom <= 0 for slice")

if freq == "MS" and dist == "lognorm" and method == "ML":
with pytest.warns(RuntimeWarning) as record:
params = standardized_index_fit_params(
gwl_cal,
freq=freq,
window=window,
dist=dist,
method=method,
fitkwargs=fitkwargs,
zero_inflated=True,
)
assert "Degrees of freedom <= 0 for slice" in record[0].message.args[0]
else:
params = standardized_index_fit_params(
gwl_cal,
freq=freq,
Expand Down
6 changes: 4 additions & 2 deletions tests/test_precip.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,8 @@ def get_snowfall(cls, nimbus):
(
xr.open_dataset(nimbus.fetch(cls.pr_file), engine="h5netcdf"),
xr.open_dataset(nimbus.fetch(cls.tasmin_file), engine="h5netcdf"),
)
),
compat="no_conflicts",
)
return convert.snowfall_approximation(dnr.pr, tas=dnr.tasmin, thresh="-0.5 degC", method="binary")

Expand Down Expand Up @@ -702,7 +703,8 @@ def get_snowfall(cls, nimbus):
(
xr.open_dataset(nimbus.fetch(cls.pr_file), engine="h5netcdf"),
xr.open_dataset(nimbus.fetch(cls.tasmin_file), engine="h5netcdf"),
)
),
compat="no_conflicts",
)
return convert.snowfall_approximation(dnr.pr, tas=dnr.tasmin, thresh="-0.5 degC", method="binary")

Expand Down
13 changes: 10 additions & 3 deletions tests/test_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,13 +285,20 @@ def test_not_enough_unique_values(
):
lmom = pytest.importorskip("lmoments3.distr")
lm3dc = getattr(lmom, lm3_dist_map[dist])
time = xr.date_range("2000-01-01", "2000-12-31", freq="M")
# Some distributions have no parameter but we want to ensure we have at least one
time = xr.date_range("2000-01-01", "2000-12-31", freq="ME")
# Some distributions have no parameter, but we want to ensure we have at least one
# unique value
unique_values = np.arange(lm3dc.numargs or 1)

da = xr.DataArray(np.random.choice(unique_values, time.size), coords=dict(time=("time", time)))
out = stats.fit(da, dist=lm3dc, method="PWM").compute()
with pytest.warns() as record:
out = stats.fit(da, dist=lm3dc, method="PWM").compute()

if dist in ["expon", "gumbel_r", "norm"]:
assert record[0].category is UserWarning
elif dist in ["gamma", "genextreme", "genpareto", "pearson3", "weibull_min"]:
assert record[0].category is RuntimeWarning

assert out.isnull().all()


Expand Down
5 changes: 1 addition & 4 deletions tox.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ deps = [
"lmoments3 >=1.0.7",
"h5netcdf >=1.5.0",
"netcdf4",
"pytest >=8.0.0",
"pytest >=9.0.0",
"xsdba >=0.4.0"
]
commands = [
Expand All @@ -28,9 +28,6 @@ commands = [

[env.doctests]
labels = ["doctests"]
deps = [
"pytest >=9.0"
]
description = "Run doctests with pytest under {basepython}"
commands = [
["python", "-c", "from xclim.testing.utils import run_doctests; run_doctests()"]
Expand Down
Loading