diff --git a/xarray/backends/scipy_.py b/xarray/backends/scipy_.py index 9d5f33e8947..8031073e94d 100644 --- a/xarray/backends/scipy_.py +++ b/xarray/backends/scipy_.py @@ -127,11 +127,18 @@ def __setitem__(self, key, value): # TODO: Remove this after upstreaming the fixes to scipy. class _PickleWorkaround: flush_only_netcdf_file: type[scipy.io.netcdf_file] + _initialized: bool = False @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__ + # Only set the class once to ensure pickling stability. + # Each call to _open_scipy_netcdf creates a new class, but we only need + # the first one. Subsequent calls would overwrite the class definition, + # breaking pickle's class-identity check for instances created earlier. + if not cls._initialized: + setattr(cls, new_class.__name__, new_class) + new_class.__qualname__ = cls.__qualname__ + "." + new_class.__name__ + cls._initialized = True def _open_scipy_netcdf( diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index 4e08b71260b..3fd7798d86e 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -2523,6 +2523,14 @@ def test_pickle_open_dataset_from_bytes(self) -> None: with pickle.loads(pickle.dumps(roundtrip)) as unpickled: assert_identical(unpickled, original) + def test_pickle_open_dataset_after_multiple_opens(self) -> None: + original = Dataset({"foo": ("x", [1, 2, 3])}) + netcdf_bytes = bytes(original.to_netcdf(engine=self.engine)) + with open_dataset(netcdf_bytes, engine=self.engine) as ds1: + with open_dataset(netcdf_bytes, engine=self.engine): + with pickle.loads(pickle.dumps(ds1)) as unpickled: + assert_identical(unpickled, original) + def test_compute_false(self) -> None: original = create_test_data() with pytest.raises(