diff --git a/.github/workflows/ci-additional.yaml b/.github/workflows/ci-additional.yaml index cb5c01c87ed..e0924a86bef 100644 --- a/.github/workflows/ci-additional.yaml +++ b/.github/workflows/ci-additional.yaml @@ -134,7 +134,7 @@ jobs: - name: Upload mypy coverage to Codecov uses: codecov/codecov-action@v5.5.2 with: - file: mypy_report/cobertura.xml + files: mypy_report/cobertura.xml flags: mypy env_vars: PYTHON_VERSION name: codecov-umbrella @@ -179,7 +179,7 @@ jobs: - name: Upload mypy coverage to Codecov uses: codecov/codecov-action@v5.5.2 with: - file: mypy_report/cobertura.xml + files: mypy_report/cobertura.xml flags: mypy-min env_vars: PYTHON_VERSION name: codecov-umbrella @@ -278,7 +278,7 @@ jobs: - name: Upload pyright coverage to Codecov uses: codecov/codecov-action@v5.5.2 with: - file: pyright_report/cobertura.xml + files: pyright_report/cobertura.xml flags: pyright env_vars: PYTHON_VERSION name: codecov-umbrella diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index deec9ddb519..c192d9b59e9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -170,7 +170,7 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: - file: ./coverage.xml + files: ./coverage.xml flags: unittests env_vars: RUNNER_OS,PYTHON_VERSION name: codecov-umbrella diff --git a/.github/workflows/upstream-dev-ci.yaml b/.github/workflows/upstream-dev-ci.yaml index 393246b4746..8a3019ff2c9 100644 --- a/.github/workflows/upstream-dev-ci.yaml +++ b/.github/workflows/upstream-dev-ci.yaml @@ -160,7 +160,7 @@ jobs: - name: Upload mypy coverage to Codecov uses: codecov/codecov-action@v5.5.2 with: - file: mypy_report/cobertura.xml + files: mypy_report/cobertura.xml flags: mypy env_vars: PYTHON_VERSION name: codecov-umbrella diff --git a/.gitignore b/.gitignore index c37948f226d..69c9c98bd64 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ pip-log.txt # Unit test / coverage reports .coverage .coverage.* +coverage.xml .tox nosetests.xml .cache @@ -47,6 +48,7 @@ nosetests.xml .testmon* .tmontmp/ .pytest_cache +pytest.xml dask-worker-space/ # asv environments diff --git a/pixi.toml b/pixi.toml index a55cc033335..2d9affbb2d4 100644 --- a/pixi.toml +++ b/pixi.toml @@ -205,7 +205,7 @@ pytest = "*" pytest-asyncio = "*" pytest-cov = "*" pytest-env = "*" -pytest-mypy-plugins = "*" +pytest-mypy-plugins = ">=4.0.0" pytest-reportlog = "*" pytest-timeout = "*" pytest-xdist = "*" @@ -250,13 +250,17 @@ linkcheck = { cmd = "make linkcheck", cwd = "doc", description = "Check URLs in [feature.typing.dependencies] -mypy = "==1.18.1" +mypy = "==1.19.1" pyright = "*" hypothesis = "*" lxml = "*" pandas-stubs = "<=2.3.3.251219" +scipy-stubs = ">=1.17.1.2" types-colorama = "*" types-docutils = "*" +types-decorator = "*" +types-networkx = "*" +types-openpyxl = "*" types-psutil = "*" types-Pygments = "*" types-python-dateutil = "*" @@ -264,7 +268,6 @@ types-pytz = "*" types-PyYAML = "*" types-requests = "*" types-setuptools = "*" -types-openpyxl = "*" typing_extensions = "*" pip = "*" diff --git a/pyproject.toml b/pyproject.toml index 0a3fb8996c1..a1feb10e8c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,38 +51,39 @@ viz = ["cartopy>=0.24", "matplotlib>=3.10", "nc-time-axis", "seaborn"] types = [ "pandas-stubs", "scipy-stubs", - "types-PyYAML", - "types-Pygments", "types-colorama", "types-decorator", "types-defusedxml", "types-docutils", "types-networkx", + "types-openpyxl", "types-pexpect", "types-psutil", "types-pycurl", - "types-openpyxl", + "types-Pygments", "types-python-dateutil", "types-pytz", + "types-PyYAML", "types-requests", "types-setuptools", + "types-xlrd", ] [dependency-groups] dev = [ "hypothesis", "jinja2", - "mypy==1.18.1", + "mypy==1.19.1", "pre-commit", "pytest", "pytest-cov", "pytest-env", - "pytest-mypy-plugins", + "pytest-mypy-plugins>=4.0.0", "pytest-timeout", "pytest-xdist", "pytest-asyncio", "pytz", - "ruff>=0.8.0", + "ruff>=0.15.0", "sphinx", "sphinx_autosummary_accessors", "xarray[complete,types]", @@ -158,7 +159,6 @@ module = [ "pooch.*", "pyarrow.*", "pydap.*", - "scipy.*", "seaborn.*", "setuptools", "sparse.*", @@ -176,6 +176,7 @@ module = [ [[tool.mypy.overrides]] check_untyped_defs = true module = [ + "xarray.backends.scipy_", "xarray.core.accessor_dt", "xarray.core.accessor_str", "xarray.structure.alignment", diff --git a/xarray/backends/common.py b/xarray/backends/common.py index 753c777649f..f2580ea2a43 100644 --- a/xarray/backends/common.py +++ b/xarray/backends/common.py @@ -12,6 +12,7 @@ Any, ClassVar, Self, + TypeAlias, TypeVar, Union, overload, @@ -381,7 +382,7 @@ def __exit__(self, exception_type, exception_value, traceback): self.close() -T_PathFileOrDataStore = ( +T_PathFileOrDataStore: TypeAlias = ( str | os.PathLike[Any] | ReadBuffer | bytes | memoryview | AbstractDataStore ) @@ -776,12 +777,7 @@ def __repr__(self) -> str: def open_dataset( self, - filename_or_obj: str - | os.PathLike[Any] - | ReadBuffer - | bytes - | memoryview - | AbstractDataStore, + filename_or_obj: T_PathFileOrDataStore, *, drop_variables: str | Iterable[str] | None = None, ) -> Dataset: @@ -793,12 +789,7 @@ def open_dataset( def guess_can_open( self, - filename_or_obj: str - | os.PathLike[Any] - | ReadBuffer - | bytes - | memoryview - | AbstractDataStore, + filename_or_obj: T_PathFileOrDataStore, ) -> bool: """ Backend open_dataset method used by Xarray in :py:func:`~xarray.open_dataset`. @@ -808,12 +799,7 @@ def guess_can_open( def open_datatree( self, - filename_or_obj: str - | os.PathLike[Any] - | ReadBuffer - | bytes - | memoryview - | AbstractDataStore, + filename_or_obj: T_PathFileOrDataStore, *, drop_variables: str | Iterable[str] | None = None, ) -> DataTree: @@ -827,12 +813,7 @@ def open_datatree( def open_groups_as_dict( self, - filename_or_obj: str - | os.PathLike[Any] - | ReadBuffer - | bytes - | memoryview - | AbstractDataStore, + filename_or_obj: T_PathFileOrDataStore, *, drop_variables: str | Iterable[str] | None = None, ) -> dict[str, Dataset]: diff --git a/xarray/backends/scipy_.py b/xarray/backends/scipy_.py index dffb5ffbfe8..9d5f33e8947 100644 --- a/xarray/backends/scipy_.py +++ b/xarray/backends/scipy_.py @@ -3,8 +3,8 @@ import gzip import io import os -from collections.abc import Iterable -from typing import TYPE_CHECKING, Any +from collections.abc import Iterable, Mapping +from typing import IO, TYPE_CHECKING, Any, Literal, TypeVar, overload import numpy as np @@ -26,6 +26,7 @@ ) from xarray.backends.store import StoreBackendEntrypoint from xarray.core import indexing +from xarray.core.types import Lock from xarray.core.utils import ( Frozen, FrozenDict, @@ -35,44 +36,55 @@ ) from xarray.core.variable import Variable -try: - from scipy.io import netcdf_file as netcdf_file_base -except ImportError: - netcdf_file_base = object # type: ignore[assignment,misc,unused-ignore] # scipy is optional - - if TYPE_CHECKING: import scipy.io from xarray.backends.common import AbstractDataStore + from xarray.backends.file_manager import FileManager from xarray.core.dataset import Dataset from xarray.core.types import ReadBuffer +T = TypeVar("T") +K = TypeVar("K") +V = TypeVar("V") HAS_NUMPY_2_0 = module_available("numpy", minversion="2.0.0.dev0") -def _decode_string(s): +@overload +def _decode_string(s: bytes) -> str: ... + + +@overload +def _decode_string(s: T) -> T: ... + + +def _decode_string(s: bytes | T) -> str | T: if isinstance(s, bytes): return s.decode("utf-8", "replace") return s -def _decode_attrs(d): +def _decode_attrs(d: Mapping[K, V]) -> dict[K, V]: # don't decode _FillValue from bytes -> unicode, because we want to ensure # that its type matches the data exactly return {k: v if k == "_FillValue" else _decode_string(v) for (k, v) in d.items()} class ScipyArrayWrapper(BackendArray): - def __init__(self, variable_name, datastore): + datastore: ScipyDataStore + variable_name: str + shape: tuple[int, ...] + dtype: np.dtype + + def __init__(self, variable_name: str, datastore: ScipyDataStore) -> None: self.datastore = datastore self.variable_name = variable_name array = self.get_variable().data self.shape = array.shape self.dtype = np.dtype(array.dtype.kind + str(array.dtype.itemsize)) - def get_variable(self, needs_lock=True): + def get_variable(self, needs_lock: bool = True) -> scipy.io.netcdf_variable: ds = self.datastore._manager.acquire(needs_lock) return ds.variables[self.variable_name] @@ -88,7 +100,7 @@ def __getitem__(self, key): # Copy data if the source file is mmapped. This makes things consistent # with the netCDF4 library by ensuring we can safely read arrays even # after closing associated files. - copy = self.datastore.ds.use_mmap + copy: bool | None = self.datastore.ds.use_mmap # adapt handling of copy-kwarg to numpy 2.0 # see https://github.com/numpy/numpy/issues/25916 @@ -110,39 +122,63 @@ def __setitem__(self, key, value): raise -# TODO: Make the scipy import lazy again after upstreaming these fixes. -class flush_only_netcdf_file(netcdf_file_base): - # scipy.io.netcdf_file.close() incorrectly closes file objects that - # were passed in as constructor arguments: - # https://github.com/scipy/scipy/issues/13905 - - # Instead of closing such files, only call flush(), which is - # equivalent as long as the netcdf_file object is not mmapped. - # This suffices to keep BytesIO objects open long enough to read - # their contents from to_netcdf(), but underlying files still get - # closed when the netcdf_file is garbage collected (via __del__), - # and will need to be fixed upstream in scipy. - def close(self): - if hasattr(self, "fp") and not self.fp.closed: - self.flush() - self.fp.seek(0) # allow file to be read again +# This is a dirty workaround to allow pickling of the flush_only_netcdf_file class. +# https://stackoverflow.com/questions/72766345/attributeerror-cant-pickle-local-object-in-multiprocessing +# TODO: Remove this after upstreaming the fixes to scipy. +class _PickleWorkaround: + flush_only_netcdf_file: type[scipy.io.netcdf_file] - def __del__(self): - # Remove the __del__ method, which in scipy is aliased to close(). - # These files need to be closed explicitly by xarray. - pass + @classmethod + def add_cls(cls, new_class: type[Any]) -> None: + setattr(cls, new_class.__name__, new_class) + new_class.__qualname__ = cls.__qualname__ + "." + new_class.__name__ -def _open_scipy_netcdf(filename, mode, mmap, version, flush_only=False): +def _open_scipy_netcdf( + filename: str | os.PathLike[Any] | IO[bytes], + mode: Literal["r", "w", "a"], + mmap: bool | None, + version: Literal[1, 2], + flush_only: bool = False, +) -> scipy.io.netcdf_file: import scipy.io - netcdf_file = flush_only_netcdf_file if flush_only else scipy.io.netcdf_file + # TODO: Remove this after upstreaming these fixes. + class flush_only_netcdf_file(scipy.io.netcdf_file): + # scipy.io.netcdf_file.close() incorrectly closes file objects that + # were passed in as constructor arguments: + # https://github.com/scipy/scipy/issues/13905 + + # Instead of closing such files, only call flush(), which is + # equivalent as long as the netcdf_file object is not mmapped. + # This suffices to keep BytesIO objects open long enough to read + # their contents from to_netcdf(), but underlying files still get + # closed when the netcdf_file is garbage collected (via __del__), + # and will need to be fixed upstream in scipy. + def close(self): + if hasattr(self, "fp") and not self.fp.closed: + self.flush() + self.fp.seek(0) # allow file to be read again + + def __del__(self): + # Remove the __del__ method, which in scipy is aliased to close(). + # These files need to be closed explicitly by xarray. + pass + + _PickleWorkaround.add_cls(flush_only_netcdf_file) + + netcdf_file = ( + _PickleWorkaround.flush_only_netcdf_file if flush_only else scipy.io.netcdf_file + ) # if the string ends with .gz, then gunzip and open as netcdf file if isinstance(filename, str) and filename.endswith(".gz"): try: return netcdf_file( - gzip.open(filename), mode=mode, mmap=mmap, version=version + gzip.open(filename), # type: ignore[arg-type] # not sure if gzip issue or scipy-stubs issue + mode=mode, + mmap=mmap, + version=version, ) except TypeError as e: # TODO: gzipped loading only works with NetCDF3 files. @@ -180,12 +216,22 @@ class ScipyDataStore(WritableCFDataStore): It only supports the NetCDF3 file-format. """ + lock: Lock + _manager: FileManager[scipy.io.netcdf_file] + def __init__( - self, filename_or_obj, mode="r", format=None, group=None, mmap=None, lock=None - ): + self, + filename_or_obj: T_PathFileOrDataStore, + mode: Literal["r", "w", "a"] = "r", + format: str | None = None, + group: str | None = None, + mmap: bool | None = None, + lock: Lock | Literal[False] | None = None, + ) -> None: if group is not None: raise ValueError("cannot save to a group with the scipy.io.netcdf backend") + version: Literal[1, 2] if format is None or format == "NETCDF3_64BIT": version = 2 elif format == "NETCDF3_CLASSIC": @@ -203,6 +249,7 @@ def __init__( filename_or_obj = io.BytesIO() source.getvalue = filename_or_obj.getbuffer + manager: FileManager if isinstance(filename_or_obj, str): # path manager = CachingFileManager( _open_scipy_netcdf, @@ -215,7 +262,7 @@ def __init__( # Note: checking for .seek matches the check for file objects # in scipy.io.netcdf_file scipy_dataset = _open_scipy_netcdf( - filename_or_obj, + filename_or_obj, # type: ignore[arg-type] # unsupported cases are caught above mode=mode, mmap=mmap, version=version, @@ -234,30 +281,30 @@ def __init__( def ds(self) -> scipy.io.netcdf_file: return self._manager.acquire() - def open_store_variable(self, name, var): + def open_store_variable(self, name: str, var: scipy.io.netcdf_variable) -> Variable: return Variable( var.dimensions, indexing.LazilyIndexedArray(ScipyArrayWrapper(name, self)), - _decode_attrs(var._attributes), + _decode_attrs(var._attributes), # type: ignore[attr-defined] # using private attribute ) - def get_variables(self): + def get_variables(self) -> Frozen[str, Variable]: return FrozenDict( (k, self.open_store_variable(k, v)) for k, v in self.ds.variables.items() ) - def get_attrs(self): - return Frozen(_decode_attrs(self.ds._attributes)) + def get_attrs(self) -> Frozen[str, Any]: + return Frozen(_decode_attrs(self.ds._attributes)) # type: ignore[attr-defined] # using private attribute - def get_dimensions(self): + def get_dimensions(self) -> Frozen[str, int | None]: return Frozen(self.ds.dimensions) - def get_encoding(self): + def get_encoding(self) -> dict[Literal["unlimited_dims"], set[str]]: return { "unlimited_dims": {k for k, v in self.ds.dimensions.items() if v is None} } - def set_dimension(self, name, length, is_unlimited=False): + def set_dimension(self, name: str, length: int, is_unlimited: bool = False) -> None: if name in self.ds.dimensions: raise ValueError( f"{type(self).__name__} does not support modifying dimensions" @@ -265,22 +312,26 @@ def set_dimension(self, name, length, is_unlimited=False): dim_length = length if not is_unlimited else None self.ds.createDimension(name, dim_length) - def _validate_attr_key(self, key): + def _validate_attr_key(self, key: Any) -> None: if not is_valid_nc3_name(key): raise ValueError("Not a valid attribute name") - def set_attribute(self, key, value): + def set_attribute(self, key: str, value: Any) -> None: self._validate_attr_key(key) value = encode_nc3_attr_value(value) setattr(self.ds, key, value) - def encode_variable(self, variable, name=None): + def encode_variable(self, variable: Variable, name: str | None = None) -> Variable: variable = encode_nc3_variable(variable, name=name) return variable def prepare_variable( - self, name, variable, check_encoding=False, unlimited_dims=None - ): + self, + name: str, + variable: Variable, + check_encoding: bool = False, + unlimited_dims: set[str] | None = None, + ) -> tuple[ScipyArrayWrapper, Any]: if ( check_encoding and variable.encoding @@ -295,7 +346,7 @@ def prepare_variable( # don't write the data yet; scipy.io.netcdf does not support incremental # writes. if name not in self.ds.variables: - self.ds.createVariable(name, data.dtype, variable.dims) + self.ds.createVariable(name, data.dtype, [str(v) for v in variable.dims]) scipy_var = self.ds.variables[name] for k, v in variable.attrs.items(): self._validate_attr_key(k) @@ -305,20 +356,15 @@ def prepare_variable( return target, data - def sync(self): + def sync(self) -> None: self.ds.sync() - def close(self): + def close(self) -> None: self._manager.close() def _normalize_filename_or_obj( - filename_or_obj: str - | os.PathLike[Any] - | ReadBuffer - | bytes - | memoryview - | AbstractDataStore, + filename_or_obj: T_PathFileOrDataStore, ) -> str | ReadBuffer | AbstractDataStore: if isinstance(filename_or_obj, bytes | memoryview): return io.BytesIO(filename_or_obj) @@ -381,18 +427,18 @@ def open_dataset( self, filename_or_obj: T_PathFileOrDataStore, *, - mask_and_scale=True, - decode_times=True, - concat_characters=True, - decode_coords=True, + mask_and_scale: bool = True, + decode_times: bool = True, + concat_characters: bool = True, + decode_coords: bool = True, drop_variables: str | Iterable[str] | None = None, - use_cftime=None, - decode_timedelta=None, - mode="r", - format=None, - group=None, - mmap=None, - lock=None, + use_cftime: bool | None = None, + decode_timedelta: bool | None = None, + mode: Literal["r", "w", "a"] = "r", + format: str | None = None, + group: str | None = None, + mmap: bool | None = None, + lock: Lock | Literal[False] | None = None, ) -> Dataset: filename_or_obj = _normalize_filename_or_obj(filename_or_obj) store = ScipyDataStore( diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index f40a39ba51a..f32d52ea3db 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -20,7 +20,7 @@ from importlib import import_module from io import BytesIO from pathlib import Path -from typing import TYPE_CHECKING, Any, Final, Literal, cast +from typing import TYPE_CHECKING, Any, Final, Literal, cast, overload from unittest.mock import patch import numpy as np @@ -149,6 +149,12 @@ WrapperStore = object # type: ignore[assignment,misc,unused-ignore] ZARR_FORMATS = [] +if TYPE_CHECKING: + from zarr.abc.store import Store as ZarrStoreABC + + from xarray import Variable + from xarray.backends.api import T_NetcdfEngine, T_NetcdfTypes + @pytest.fixture(scope="module", params=ZARR_FORMATS) def default_zarr_format(request) -> Generator[None, None]: @@ -236,9 +242,6 @@ def _check_compression_codec_available(codec: str | None) -> bool: dask_array_type = array_type("dask") -if TYPE_CHECKING: - from xarray.backends.api import T_NetcdfEngine, T_NetcdfTypes - def open_example_dataset(name, *args, **kwargs) -> Dataset: return open_dataset( @@ -4183,7 +4186,7 @@ async def test_async_load_multiple_variables(self) -> None: @pytest.mark.parametrize("cls_name", ["Variable", "DataArray", "Dataset"]) async def test_concurrent_load_multiple_objects( self, - cls_name, + cls_name: Literal["Variable", "DataArray", "Dataset"], ) -> None: N_OBJECTS = 5 N_LAZY_VARS = { @@ -4269,8 +4272,8 @@ async def test_concurrent_load_multiple_objects( ) async def test_indexing( self, - cls_name, - method, + cls_name: Literal["Variable", "DataArray", "Dataset"], + method: Literal["sel", "isel"], indexer, target_zarr_class, ) -> None: @@ -4369,9 +4372,27 @@ async def test_raise_on_older_zarr_version( await var.isel(**indexer).load_async() +@overload +def get_xr_obj(store: ZarrStoreABC, cls_name: Literal["Variable"]) -> Variable: ... + + +@overload +def get_xr_obj(store: ZarrStoreABC, cls_name: Literal["DataArray"]) -> DataArray: ... + + +@overload +def get_xr_obj(store: ZarrStoreABC, cls_name: Literal["Dataset"]) -> Dataset: ... + + +@overload def get_xr_obj( - store: zarr.abc.store.Store, cls_name: Literal["Variable", "DataArray", "Dataset"] -): + store: ZarrStoreABC, cls_name: Literal["Variable", "DataArray", "Dataset"] +) -> Variable | DataArray | Dataset: ... + + +def get_xr_obj( + store: ZarrStoreABC, cls_name: Literal["Variable", "DataArray", "Dataset"] +) -> Variable | DataArray | Dataset: ds = xr.open_zarr(store, consolidated=False, chunks=None) match cls_name: diff --git a/xarray/tests/test_dataarray_typing.yml b/xarray/tests/test_dataarray_typing.yml index f52b7c6b09f..aa49473b662 100644 --- a/xarray/tests/test_dataarray_typing.yml +++ b/xarray/tests/test_dataarray_typing.yml @@ -86,7 +86,7 @@ # Call to pipe missing argument for kwonly parameter `kwonly` da = DataArray().pipe(f, 42) out: | - main:7: error: No overload variant of "pipe" of "DataWithCoords" matches argument types "Callable[[DataArray, int, NamedArg(int, 'kwonly')], DataArray]", "int" [call-overload] + main:7: error: No overload variant of "pipe" of "DataWithCoords" matches argument types "def f(da: DataArray, arg: int, *, kwonly: int) -> DataArray", "int" [call-overload] main:7: note: Possible overload variants: main:7: note: def [P`2, T] pipe(self, func: Callable[[DataArray, **P], T], *args: P.args, **kwargs: P.kwargs) -> T main:7: note: def [T] pipe(self, func: tuple[Callable[..., T], str], *args: Any, **kwargs: Any) -> T @@ -101,7 +101,7 @@ # Call to pipe missing keyword for kwonly parameter `kwonly` da = DataArray().pipe(f, 42, 99) out: | - main:7: error: No overload variant of "pipe" of "DataWithCoords" matches argument types "Callable[[DataArray, int, NamedArg(int, 'kwonly')], DataArray]", "int", "int" [call-overload] + main:7: error: No overload variant of "pipe" of "DataWithCoords" matches argument types "def f(da: DataArray, arg: int, *, kwonly: int) -> DataArray", "int", "int" [call-overload] main:7: note: Possible overload variants: main:7: note: def [P`2, T] pipe(self, func: Callable[[DataArray, **P], T], *args: P.args, **kwargs: P.kwargs) -> T main:7: note: def [T] pipe(self, func: tuple[Callable[..., T], str], *args: Any, **kwargs: Any) -> T diff --git a/xarray/tests/test_dataset_typing.yml b/xarray/tests/test_dataset_typing.yml index 873cbdcb7ef..5198189935e 100644 --- a/xarray/tests/test_dataset_typing.yml +++ b/xarray/tests/test_dataset_typing.yml @@ -86,7 +86,7 @@ # Call to pipe missing argument for kwonly parameter `kwonly` ds = Dataset().pipe(f, 42) out: | - main:7: error: No overload variant of "pipe" of "DataWithCoords" matches argument types "Callable[[Dataset, int, NamedArg(int, 'kwonly')], Dataset]", "int" [call-overload] + main:7: error: No overload variant of "pipe" of "DataWithCoords" matches argument types "def f(ds: Dataset, arg: int, *, kwonly: int) -> Dataset", "int" [call-overload] main:7: note: Possible overload variants: main:7: note: def [P`2, T] pipe(self, func: Callable[[Dataset, **P], T], *args: P.args, **kwargs: P.kwargs) -> T main:7: note: def [T] pipe(self, func: tuple[Callable[..., T], str], *args: Any, **kwargs: Any) -> T @@ -101,7 +101,7 @@ # Call to pipe missing keyword for kwonly parameter `kwonly` ds = Dataset().pipe(f, 42, 99) out: | - main:7: error: No overload variant of "pipe" of "DataWithCoords" matches argument types "Callable[[Dataset, int, NamedArg(int, 'kwonly')], Dataset]", "int", "int" [call-overload] + main:7: error: No overload variant of "pipe" of "DataWithCoords" matches argument types "def f(ds: Dataset, arg: int, *, kwonly: int) -> Dataset", "int", "int" [call-overload] main:7: note: Possible overload variants: main:7: note: def [P`2, T] pipe(self, func: Callable[[Dataset, **P], T], *args: P.args, **kwargs: P.kwargs) -> T main:7: note: def [T] pipe(self, func: tuple[Callable[..., T], str], *args: Any, **kwargs: Any) -> T diff --git a/xarray/tests/test_datatree_typing.yml b/xarray/tests/test_datatree_typing.yml index 0c4b9118bd1..fa19515404d 100644 --- a/xarray/tests/test_datatree_typing.yml +++ b/xarray/tests/test_datatree_typing.yml @@ -86,7 +86,7 @@ # Call to pipe missing argument for kwonly parameter `kwonly` dt = DataTree().pipe(f, 42) out: | - main:7: error: No overload variant of "pipe" of "DataTree" matches argument types "Callable[[DataTree, int, NamedArg(int, 'kwonly')], DataTree]", "int" [call-overload] + main:7: error: No overload variant of "pipe" of "DataTree" matches argument types "def f(dt: DataTree, arg: int, *, kwonly: int) -> DataTree", "int" [call-overload] main:7: note: Possible overload variants: main:7: note: def [P`2, T] pipe(self, func: Callable[[DataTree, **P], T], *args: P.args, **kwargs: P.kwargs) -> T main:7: note: def [T] pipe(self, func: tuple[Callable[..., T], str], *args: Any, **kwargs: Any) -> T @@ -101,7 +101,7 @@ # Call to pipe missing keyword for kwonly parameter `kwonly` dt = DataTree().pipe(f, 42, 99) out: | - main:7: error: No overload variant of "pipe" of "DataTree" matches argument types "Callable[[DataTree, int, NamedArg(int, 'kwonly')], DataTree]", "int", "int" [call-overload] + main:7: error: No overload variant of "pipe" of "DataTree" matches argument types "def f(dt: DataTree, arg: int, *, kwonly: int) -> DataTree", "int", "int" [call-overload] main:7: note: Possible overload variants: main:7: note: def [P`2, T] pipe(self, func: Callable[[DataTree, **P], T], *args: P.args, **kwargs: P.kwargs) -> T main:7: note: def [T] pipe(self, func: tuple[Callable[..., T], str], *args: Any, **kwargs: Any) -> T diff --git a/xarray/tests/test_plugins.py b/xarray/tests/test_plugins.py index 5bb17c06eb2..b41d18ae418 100644 --- a/xarray/tests/test_plugins.py +++ b/xarray/tests/test_plugins.py @@ -279,7 +279,7 @@ def test_lazy_import() -> None: "numbagg", "pint", "pydap", - # "scipy", # TODO: xarray.backends.scipy_ is currently not lazy + "scipy", "sparse", "zarr", ]