From 72606f740a72d056b07e90e7486942ce6797bbfc Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Tue, 9 Oct 2018 10:34:21 +0200 Subject: [PATCH 01/27] Integration of ECMWF cfgrib driver to read GRIB files into xarray. --- xarray/backends/__init__.py | 2 + xarray/backends/api.py | 3 + xarray/backends/cfgrib_.py | 125 ++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 xarray/backends/cfgrib_.py diff --git a/xarray/backends/__init__.py b/xarray/backends/__init__.py index a2f0d79a6d1..9b9e04d9346 100644 --- a/xarray/backends/__init__.py +++ b/xarray/backends/__init__.py @@ -5,6 +5,7 @@ """ from .common import AbstractDataStore from .file_manager import FileManager, CachingFileManager, DummyFileManager +from .cfgrib_ import CfGribDataStore from .memory import InMemoryDataStore from .netCDF4_ import NetCDF4DataStore from .pydap_ import PydapDataStore @@ -18,6 +19,7 @@ 'AbstractDataStore', 'FileManager', 'CachingFileManager', + 'CfGribDataStore', 'DummyFileManager', 'InMemoryDataStore', 'NetCDF4DataStore', diff --git a/xarray/backends/api.py b/xarray/backends/api.py index 65112527045..f52c32a6d61 100644 --- a/xarray/backends/api.py +++ b/xarray/backends/api.py @@ -296,6 +296,9 @@ def maybe_decode_store(store, lock=False): elif engine == 'pseudonetcdf': store = backends.PseudoNetCDFDataStore.open( filename_or_obj, lock=lock, **backend_kwargs) + elif engine == 'cfgrib': + store = backends.CfGribDataStore.from_path( + filename_or_obj, lock=lock, **backend_kwargs) else: raise ValueError('unrecognized engine for open_dataset: %r' % engine) diff --git a/xarray/backends/cfgrib_.py b/xarray/backends/cfgrib_.py new file mode 100644 index 00000000000..4299de63974 --- /dev/null +++ b/xarray/backends/cfgrib_.py @@ -0,0 +1,125 @@ +# +# Copyright 2017-2018 European Centre for Medium-Range Weather Forecasts (ECMWF). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Authors: +# Alessandro Amici - B-Open - https://bopen.eu +# + +from __future__ import absolute_import, division, print_function + +import numpy as np + +from . import common +from .. import core + + +class WrapGrib(common.BackendArray): + def __init__(self, backend_array): + self.backend_array = backend_array + + def __getattr__(self, item): + return getattr(self.backend_array, item) + + def __getitem__(self, item): + key, np_inds = core.indexing.decompose_indexer( + item, self.shape, core.indexing.IndexingSupport.OUTER_1VECTOR) + + array = self.backend_array[key.tuple] + + if len(np_inds.tuple) > 0: + array = core.indexing.NumpyIndexingAdapter(array)[np_inds] + + return array + + +FLAVOURS = { + 'eccodes': { + 'dataset': { + 'encode_time': False, + 'encode_vertical': False, + 'encode_geography': False, + }, + }, + 'ecmwf': { + 'variable_map': {}, + }, + 'cds': { + 'variable_map': { + 'number': 'realization', + 'time': 'forecast_reference_time', + 'valid_time': 'time', + 'step': 'leadtime', + 'air_pressure': 'plev', + 'latitude': 'lat', + 'longitude': 'lon', + }, + }, +} + + +class CfGribDataStore(common.AbstractDataStore): + """ + Implements the ``xr.AbstractDataStore`` read-only API for a GRIB file. + """ + def __init__(self, ds, variable_map={}, lock=None): + self.ds = ds + self.variable_map = variable_map.copy() + self.lock = lock + + @classmethod + def from_path(cls, path, lock=None, flavour_name='ecmwf', errors='ignore', **kwargs): + import cfgrib + flavour = FLAVOURS[flavour_name].copy() + config = flavour.pop('dataset', {}).copy() + config.update(kwargs) + ds = cfgrib.open_file(path, errors=errors, **config) + return cls(ds=ds, lock=lock, **flavour) + + def open_store_variable(self, name, var): + if isinstance(var.data, np.ndarray): + data = var.data + else: + data = core.indexing.LazilyOuterIndexedArray(WrapGrib(var.data)) + + dimensions = tuple(self.variable_map.get(dim, dim) for dim in var.dimensions) + attrs = var.attributes + + # the coordinates attributes need a special treatment + if 'coordinates' in attrs: + coordinates = [self.variable_map.get(d, d) for d in attrs['coordinates'].split()] + attrs['coordinates'] = ' '.join(coordinates) + + encoding = self.ds.encoding.copy() + encoding['original_shape'] = var.data.shape + + return core.variable.Variable(dimensions, data, attrs, encoding) + + def get_variables(self): + variables = [] + for k, v in self.ds.variables.items(): + variables.append((self.variable_map.get(k, k), self.open_store_variable(k, v))) + return core.utils.FrozenOrderedDict(variables) + + def get_attrs(self): + return core.utils.FrozenOrderedDict(self.ds.attributes) + + def get_dimensions(self): + return core.utils.FrozenOrderedDict((self.variable_map.get(d, d), s) + for d, s in self.ds.dimensions.items()) + + def get_encoding(self): + encoding = {} + encoding['unlimited_dims'] = {k for k, v in self.ds.dimensions.items() if v is None} + return encoding From 71fcbe757b12d93fea95b89e40b4ff59cc992734 Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Tue, 9 Oct 2018 21:49:27 +0200 Subject: [PATCH 02/27] Remove all coordinate renaming from the cfgrib backend. --- xarray/backends/cfgrib_.py | 36 +++++++----------------------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/xarray/backends/cfgrib_.py b/xarray/backends/cfgrib_.py index 4299de63974..580c18372fe 100644 --- a/xarray/backends/cfgrib_.py +++ b/xarray/backends/cfgrib_.py @@ -52,20 +52,7 @@ def __getitem__(self, item): 'encode_geography': False, }, }, - 'ecmwf': { - 'variable_map': {}, - }, - 'cds': { - 'variable_map': { - 'number': 'realization', - 'time': 'forecast_reference_time', - 'valid_time': 'time', - 'step': 'leadtime', - 'air_pressure': 'plev', - 'latitude': 'lat', - 'longitude': 'lon', - }, - }, + 'ecmwf': {}, } @@ -73,13 +60,12 @@ class CfGribDataStore(common.AbstractDataStore): """ Implements the ``xr.AbstractDataStore`` read-only API for a GRIB file. """ - def __init__(self, ds, variable_map={}, lock=None): + def __init__(self, ds, lock=False): self.ds = ds - self.variable_map = variable_map.copy() self.lock = lock @classmethod - def from_path(cls, path, lock=None, flavour_name='ecmwf', errors='ignore', **kwargs): + def from_path(cls, path, lock=False, flavour_name='ecmwf', errors='ignore', **kwargs): import cfgrib flavour = FLAVOURS[flavour_name].copy() config = flavour.pop('dataset', {}).copy() @@ -93,31 +79,23 @@ def open_store_variable(self, name, var): else: data = core.indexing.LazilyOuterIndexedArray(WrapGrib(var.data)) - dimensions = tuple(self.variable_map.get(dim, dim) for dim in var.dimensions) + dimensions = var.dimensions attrs = var.attributes - # the coordinates attributes need a special treatment - if 'coordinates' in attrs: - coordinates = [self.variable_map.get(d, d) for d in attrs['coordinates'].split()] - attrs['coordinates'] = ' '.join(coordinates) - encoding = self.ds.encoding.copy() encoding['original_shape'] = var.data.shape return core.variable.Variable(dimensions, data, attrs, encoding) def get_variables(self): - variables = [] - for k, v in self.ds.variables.items(): - variables.append((self.variable_map.get(k, k), self.open_store_variable(k, v))) - return core.utils.FrozenOrderedDict(variables) + return core.utils.FrozenOrderedDict((k, self.open_store_variable(k, v)) + for k, v in self.ds.variables.items()) def get_attrs(self): return core.utils.FrozenOrderedDict(self.ds.attributes) def get_dimensions(self): - return core.utils.FrozenOrderedDict((self.variable_map.get(d, d), s) - for d, s in self.ds.dimensions.items()) + return core.utils.FrozenOrderedDict(self.ds.dimensions.items()) def get_encoding(self): encoding = {} From 6faa7b9cbc6028dfd23bd52187f9e13a71f7ab7a Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Tue, 9 Oct 2018 22:05:56 +0200 Subject: [PATCH 03/27] Move flavour selection to `cfgrib.Dataset.from_path`. --- xarray/backends/cfgrib_.py | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/xarray/backends/cfgrib_.py b/xarray/backends/cfgrib_.py index 580c18372fe..5d39b0448a9 100644 --- a/xarray/backends/cfgrib_.py +++ b/xarray/backends/cfgrib_.py @@ -44,18 +44,6 @@ def __getitem__(self, item): return array -FLAVOURS = { - 'eccodes': { - 'dataset': { - 'encode_time': False, - 'encode_vertical': False, - 'encode_geography': False, - }, - }, - 'ecmwf': {}, -} - - class CfGribDataStore(common.AbstractDataStore): """ Implements the ``xr.AbstractDataStore`` read-only API for a GRIB file. @@ -65,13 +53,9 @@ def __init__(self, ds, lock=False): self.lock = lock @classmethod - def from_path(cls, path, lock=False, flavour_name='ecmwf', errors='ignore', **kwargs): + def from_path(cls, path, lock=False, **backend_kwargs): import cfgrib - flavour = FLAVOURS[flavour_name].copy() - config = flavour.pop('dataset', {}).copy() - config.update(kwargs) - ds = cfgrib.open_file(path, errors=errors, **config) - return cls(ds=ds, lock=lock, **flavour) + return cls(ds=cfgrib.open_file(path, **backend_kwargs), lock=lock) def open_store_variable(self, name, var): if isinstance(var.data, np.ndarray): From 1469a0ebb227ddda94937e55505d2eda28302b03 Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Tue, 9 Oct 2018 22:22:51 +0200 Subject: [PATCH 04/27] Sync xarray backend import style with xarray. --- xarray/backends/cfgrib_.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/xarray/backends/cfgrib_.py b/xarray/backends/cfgrib_.py index 5d39b0448a9..d746d8a1bad 100644 --- a/xarray/backends/cfgrib_.py +++ b/xarray/backends/cfgrib_.py @@ -21,11 +21,13 @@ import numpy as np -from . import common -from .. import core +from .. import Variable +from ..core import indexing +from ..core.utils import Frozen, FrozenOrderedDict +from .common import AbstractDataStore, BackendArray -class WrapGrib(common.BackendArray): +class CfGribArrayWrapper(BackendArray): def __init__(self, backend_array): self.backend_array = backend_array @@ -33,18 +35,18 @@ def __getattr__(self, item): return getattr(self.backend_array, item) def __getitem__(self, item): - key, np_inds = core.indexing.decompose_indexer( - item, self.shape, core.indexing.IndexingSupport.OUTER_1VECTOR) + key, np_inds = indexing.decompose_indexer( + item, self.shape, indexing.IndexingSupport.OUTER_1VECTOR) array = self.backend_array[key.tuple] if len(np_inds.tuple) > 0: - array = core.indexing.NumpyIndexingAdapter(array)[np_inds] + array = indexing.NumpyIndexingAdapter(array)[np_inds] return array -class CfGribDataStore(common.AbstractDataStore): +class CfGribDataStore(AbstractDataStore): """ Implements the ``xr.AbstractDataStore`` read-only API for a GRIB file. """ @@ -61,7 +63,7 @@ def open_store_variable(self, name, var): if isinstance(var.data, np.ndarray): data = var.data else: - data = core.indexing.LazilyOuterIndexedArray(WrapGrib(var.data)) + data = indexing.LazilyOuterIndexedArray(CfGribArrayWrapper(var.data)) dimensions = var.dimensions attrs = var.attributes @@ -69,17 +71,17 @@ def open_store_variable(self, name, var): encoding = self.ds.encoding.copy() encoding['original_shape'] = var.data.shape - return core.variable.Variable(dimensions, data, attrs, encoding) + return Variable(dimensions, data, attrs, encoding) def get_variables(self): - return core.utils.FrozenOrderedDict((k, self.open_store_variable(k, v)) - for k, v in self.ds.variables.items()) + return FrozenOrderedDict((k, self.open_store_variable(k, v)) + for k, v in self.ds.variables.items()) def get_attrs(self): - return core.utils.FrozenOrderedDict(self.ds.attributes) + return Frozen(self.ds.attributes) def get_dimensions(self): - return core.utils.FrozenOrderedDict(self.ds.dimensions.items()) + return Frozen(self.ds.dimensions) def get_encoding(self): encoding = {} From 12811e87b9b44647d2542f0648d2543d437ba95f Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Tue, 9 Oct 2018 23:28:49 +0200 Subject: [PATCH 05/27] Make use of the new xarray.backends.FileCachingManager. --- xarray/backends/api.py | 2 +- xarray/backends/cfgrib_.py | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/xarray/backends/api.py b/xarray/backends/api.py index f52c32a6d61..8816de39a4f 100644 --- a/xarray/backends/api.py +++ b/xarray/backends/api.py @@ -297,7 +297,7 @@ def maybe_decode_store(store, lock=False): store = backends.PseudoNetCDFDataStore.open( filename_or_obj, lock=lock, **backend_kwargs) elif engine == 'cfgrib': - store = backends.CfGribDataStore.from_path( + store = backends.CfGribDataStore( filename_or_obj, lock=lock, **backend_kwargs) else: raise ValueError('unrecognized engine for open_dataset: %r' diff --git a/xarray/backends/cfgrib_.py b/xarray/backends/cfgrib_.py index d746d8a1bad..68c9f801292 100644 --- a/xarray/backends/cfgrib_.py +++ b/xarray/backends/cfgrib_.py @@ -25,6 +25,7 @@ from ..core import indexing from ..core.utils import Frozen, FrozenOrderedDict from .common import AbstractDataStore, BackendArray +from .file_manager import CachingFileManager class CfGribArrayWrapper(BackendArray): @@ -50,14 +51,15 @@ class CfGribDataStore(AbstractDataStore): """ Implements the ``xr.AbstractDataStore`` read-only API for a GRIB file. """ - def __init__(self, ds, lock=False): - self.ds = ds + def __init__(self, filename, lock=False, **backend_kwargs): + import cfgrib self.lock = lock + self._manager = CachingFileManager( + cfgrib.open_file, filename, lock=lock, mode='r', **backend_kwargs) - @classmethod - def from_path(cls, path, lock=False, **backend_kwargs): - import cfgrib - return cls(ds=cfgrib.open_file(path, **backend_kwargs), lock=lock) + @property + def ds(self): + return self._manager.acquire() def open_store_variable(self, name, var): if isinstance(var.data, np.ndarray): @@ -65,13 +67,10 @@ def open_store_variable(self, name, var): else: data = indexing.LazilyOuterIndexedArray(CfGribArrayWrapper(var.data)) - dimensions = var.dimensions - attrs = var.attributes - encoding = self.ds.encoding.copy() encoding['original_shape'] = var.data.shape - return Variable(dimensions, data, attrs, encoding) + return Variable(var.dimensions, data, var.attributes, encoding) def get_variables(self): return FrozenOrderedDict((k, self.open_store_variable(k, v)) @@ -87,3 +86,6 @@ def get_encoding(self): encoding = {} encoding['unlimited_dims'] = {k for k, v in self.ds.dimensions.items() if v is None} return encoding + + def close(self): + self._manager.close() From a4409b657b99d5f9ad4df4df3956adc2a49599fe Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Wed, 10 Oct 2018 00:37:35 +0200 Subject: [PATCH 06/27] Add just-in-case locking for ecCodes. --- xarray/backends/cfgrib_.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/xarray/backends/cfgrib_.py b/xarray/backends/cfgrib_.py index 68c9f801292..d7e49952a2f 100644 --- a/xarray/backends/cfgrib_.py +++ b/xarray/backends/cfgrib_.py @@ -26,6 +26,11 @@ from ..core.utils import Frozen, FrozenOrderedDict from .common import AbstractDataStore, BackendArray from .file_manager import CachingFileManager +from .locks import ensure_lock, SerializableLock + +# FIXME: Add a dedicated lock just in case, even if ecCodes is supposed to be thread-safe in most +# circumstances. See: https://confluence.ecmwf.int/display/ECC/Frequently+Asked+Questions +ECCODES_LOCK = SerializableLock() class CfGribArrayWrapper(BackendArray): @@ -51,11 +56,14 @@ class CfGribDataStore(AbstractDataStore): """ Implements the ``xr.AbstractDataStore`` read-only API for a GRIB file. """ - def __init__(self, filename, lock=False, **backend_kwargs): + def __init__(self, filename, lock=None, **backend_kwargs): import cfgrib - self.lock = lock + if lock is None: + lock = ECCODES_LOCK + self.lock = ensure_lock(lock) + backend_kwargs['filter_by_keys'] = tuple(backend_kwargs.get('filter_by_keys', {}).items()) self._manager = CachingFileManager( - cfgrib.open_file, filename, lock=lock, mode='r', **backend_kwargs) + cfgrib.open_file, filename, lock=lock, mode='r', kwargs=backend_kwargs) @property def ds(self): From 80b8788636aab9f6e7dc8aa9c54c4e7c1308b806 Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Wed, 10 Oct 2018 08:43:11 +0200 Subject: [PATCH 07/27] Explicitly assign attributes to CfGribArrayWrapper --- xarray/backends/cfgrib_.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/xarray/backends/cfgrib_.py b/xarray/backends/cfgrib_.py index d7e49952a2f..b7c335362c7 100644 --- a/xarray/backends/cfgrib_.py +++ b/xarray/backends/cfgrib_.py @@ -34,17 +34,16 @@ class CfGribArrayWrapper(BackendArray): - def __init__(self, backend_array): - self.backend_array = backend_array - - def __getattr__(self, item): - return getattr(self.backend_array, item) + def __init__(self, array): + self.shape = array.shape + self.dtype = array.dtype + self.backend_array = array def __getitem__(self, item): key, np_inds = indexing.decompose_indexer( item, self.shape, indexing.IndexingSupport.OUTER_1VECTOR) - array = self.backend_array[key.tuple] + array = self.array[key.tuple] if len(np_inds.tuple) > 0: array = indexing.NumpyIndexingAdapter(array)[np_inds] From 9dfd6602a350516f00f7eb675864b8d865cfa41d Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Wed, 10 Oct 2018 08:56:23 +0200 Subject: [PATCH 08/27] Add missing locking in CfGribArrayWrapper and use explicit_indexing_adapter. --- xarray/backends/cfgrib_.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/xarray/backends/cfgrib_.py b/xarray/backends/cfgrib_.py index b7c335362c7..878e9f43d86 100644 --- a/xarray/backends/cfgrib_.py +++ b/xarray/backends/cfgrib_.py @@ -34,21 +34,19 @@ class CfGribArrayWrapper(BackendArray): - def __init__(self, array): + def __init__(self, datastore, array): + self.datastore = datastore self.shape = array.shape self.dtype = array.dtype - self.backend_array = array + self.array = array - def __getitem__(self, item): - key, np_inds = indexing.decompose_indexer( - item, self.shape, indexing.IndexingSupport.OUTER_1VECTOR) + def __getitem__(self, key): + return indexing.explicit_indexing_adapter( + key, self.shape, indexing.IndexingSupport.BASIC, self._getitem) - array = self.array[key.tuple] - - if len(np_inds.tuple) > 0: - array = indexing.NumpyIndexingAdapter(array)[np_inds] - - return array + def _getitem(self, key): + with self.datastore.lock: + return self.array[key] class CfGribDataStore(AbstractDataStore): @@ -72,7 +70,7 @@ def open_store_variable(self, name, var): if isinstance(var.data, np.ndarray): data = var.data else: - data = indexing.LazilyOuterIndexedArray(CfGribArrayWrapper(var.data)) + data = indexing.LazilyOuterIndexedArray(CfGribArrayWrapper(self, var.data)) encoding = self.ds.encoding.copy() encoding['original_shape'] = var.data.shape From edc4e85e8bdb81b2531a11b40ddab4b382576cbc Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Wed, 10 Oct 2018 21:45:45 +0200 Subject: [PATCH 09/27] Add a comment about the ugly work-around needed for filter_by_keys. --- xarray/backends/cfgrib_.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/xarray/backends/cfgrib_.py b/xarray/backends/cfgrib_.py index 878e9f43d86..5eca0f7759d 100644 --- a/xarray/backends/cfgrib_.py +++ b/xarray/backends/cfgrib_.py @@ -58,7 +58,11 @@ def __init__(self, filename, lock=None, **backend_kwargs): if lock is None: lock = ECCODES_LOCK self.lock = ensure_lock(lock) - backend_kwargs['filter_by_keys'] = tuple(backend_kwargs.get('filter_by_keys', {}).items()) + + # NOTE: filter_by_keys is a dict, but CachingFileManager only accepts hashable types + if 'filter_by_keys' in backend_kwargs: + backend_kwargs['filter_by_keys'] = tuple(backend_kwargs['filter_by_keys'].items()) + self._manager = CachingFileManager( cfgrib.open_file, filename, lock=lock, mode='r', kwargs=backend_kwargs) From 9b5335a4499c638e5360bdf58d1295a56c1f9083 Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Wed, 10 Oct 2018 22:18:15 +0200 Subject: [PATCH 10/27] Declare correct indexing support. --- xarray/backends/cfgrib_.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/backends/cfgrib_.py b/xarray/backends/cfgrib_.py index 5eca0f7759d..28042e8f1a0 100644 --- a/xarray/backends/cfgrib_.py +++ b/xarray/backends/cfgrib_.py @@ -42,7 +42,7 @@ def __init__(self, datastore, array): def __getitem__(self, key): return indexing.explicit_indexing_adapter( - key, self.shape, indexing.IndexingSupport.BASIC, self._getitem) + key, self.shape, indexing.IndexingSupport.OUTER, self._getitem) def _getitem(self, key): with self.datastore.lock: From 485a40963771a2dc88b6b1aa1277c49a7d86653f Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Sun, 14 Oct 2018 15:16:26 +0200 Subject: [PATCH 11/27] Add TestCfGrib test class. --- xarray/tests/__init__.py | 1 + xarray/tests/data/example.grib | Bin 0 -> 5232 bytes xarray/tests/test_backends.py | 19 ++++++++++++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 xarray/tests/data/example.grib diff --git a/xarray/tests/__init__.py b/xarray/tests/__init__.py index 5f724dd6713..56ecfa30c4d 100644 --- a/xarray/tests/__init__.py +++ b/xarray/tests/__init__.py @@ -77,6 +77,7 @@ def LooseVersion(vstring): has_zarr, requires_zarr = _importorskip('zarr', minversion='2.2') has_np113, requires_np113 = _importorskip('numpy', minversion='1.13.0') has_iris, requires_iris = _importorskip('iris') +has_cfgrib, requires_cfgrib = _importorskip('cfgrib') # some special cases has_scipy_or_netCDF4 = has_scipy or has_netCDF4 diff --git a/xarray/tests/data/example.grib b/xarray/tests/data/example.grib new file mode 100644 index 0000000000000000000000000000000000000000..596a54d98a022568e41ebe83eedb94d6561a09a6 GIT binary patch literal 5232 zcmds)F=*6K9LB%2*DFP(hah%v2vSiux84}6cR5do;&g+>41(0j&DM5!4#FuNI*4@W zkins2Q7Cj1p-}ExXme)=7f%Oq5Uk+PL5csDt1rpLkQeN|YYpEe?_M5Be)s?0_j2Z~ z>sMVDeQ0m*>2OAa3uK@Te$+JH%};bT^ag#1NfPN$ugtwxblP=xw9V_ z@A%Vsfe+RCGjRDB@A7y1@c6ZUa82r@Cm5g|-K$vG2BYH$@62ZDsc?DFnt;%=9A+5q zGtBPsAz~Y&%}gNUEWIN$e}J9pAX_vb^i#)f3r!2(n4sbf2{Nyiy(25nN69922z_~j z2E(e2hLM&`ksEK`F#hI7v-k?^L`ThrrXl29LcN8p1=RDY$<(+pj?6qCCDSSp+N@xG z88sUlD{09TxiOBc@Ht8rM0i()GYw}O%{+#6UlQ|7ksJMN^lm7!v3M<-jduW{rxSRX z!_*#Tj3BT~f@o^o7)MqbjV8zo9YVi=9Rp1R-|e(yirg4SR-TKJ-EKkXtA&PzH49Gq zn(?2a`>F5GvydCj;@jvv8v=xOR#1O{Er!~D+L|eHV;q@zF-oT8A+(vtdKI+_HkQ+p zDRN^RS>aQZEQqjM0+r3#N3(_D5nsK_MtVA#_r~!#fSvAGse@!oz#>5PTJQ$Z`(;}) zWZlU|S+dy#G8<^a5ud=Kh6Guc+;~`Ighg;Ky~>>fge1tiv{HAH`SXm<`2S+YrxI#b z1?D|0a-+bah76e+H~s_JbAUvADajPMF^=qBLd~dDgk^hW8wG@9YR1iR_$xJCG$tEE zE45@c(mXo`*%&61%SNghRqm)~W0>qwf*Xm6L^WFi^ni|z8~u3}Ci|bVF*O;@Fj}UM TK{kfTq_dIcnFvN|a5w%2m#N8p literal 0 HcmV?d00001 diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index 75aaba718c8..a8d360065e6 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -33,7 +33,7 @@ has_dask, has_netCDF4, has_scipy, network, raises_regex, requires_cftime, requires_dask, requires_h5netcdf, requires_netCDF4, requires_pathlib, requires_pseudonetcdf, requires_pydap, requires_pynio, requires_rasterio, - requires_scipy, requires_scipy_or_netCDF4, requires_zarr) + requires_scipy, requires_scipy_or_netCDF4, requires_zarr, requires_cfgrib) from .test_dataset import create_test_data try: @@ -2463,6 +2463,23 @@ def test_weakrefs(self): assert_identical(actual, expected) +@requires_cfgrib +class TestCfGrib(object): + + def test_read(self): + with open_example_dataset('example.grib', engine='cfgrib') as ds: + assert ds.dims == {'number': 2, 'time': 3, 'air_pressure': 2, 'latitude': 3, 'longitude': 4} + assert list(ds.data_vars) == ['z', 't'] + assert ds['z'].min() == 12660. + + def test_open_filter_by_keys(self): + kwargs = {'filter_by_keys': {'shortName': 't'}} + with open_example_dataset('example.grib', engine='cfgrib', backend_kwargs=kwargs) as ds: + assert ds.dims == {'number': 2, 'time': 3, 'air_pressure': 2, 'latitude': 3, 'longitude': 4} + assert list(ds.data_vars) == ['t'] + assert ds['t'].min() == 231. + + @requires_pseudonetcdf @pytest.mark.filterwarnings('ignore:IOAPI_ISPH is assumed to be 6370000') class TestPseudoNetCDFFormat(object): From 81f18c2272c3039c14992d4e1795564041bb1cc8 Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Sun, 14 Oct 2018 15:17:41 +0200 Subject: [PATCH 12/27] cfgrib doesn't store a file reference so no need for CachingFileManager. --- xarray/backends/cfgrib_.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/xarray/backends/cfgrib_.py b/xarray/backends/cfgrib_.py index 28042e8f1a0..43ca08102fd 100644 --- a/xarray/backends/cfgrib_.py +++ b/xarray/backends/cfgrib_.py @@ -25,7 +25,6 @@ from ..core import indexing from ..core.utils import Frozen, FrozenOrderedDict from .common import AbstractDataStore, BackendArray -from .file_manager import CachingFileManager from .locks import ensure_lock, SerializableLock # FIXME: Add a dedicated lock just in case, even if ecCodes is supposed to be thread-safe in most @@ -63,12 +62,7 @@ def __init__(self, filename, lock=None, **backend_kwargs): if 'filter_by_keys' in backend_kwargs: backend_kwargs['filter_by_keys'] = tuple(backend_kwargs['filter_by_keys'].items()) - self._manager = CachingFileManager( - cfgrib.open_file, filename, lock=lock, mode='r', kwargs=backend_kwargs) - - @property - def ds(self): - return self._manager.acquire() + self.ds = cfgrib.open_file(filename, mode='r', **backend_kwargs) def open_store_variable(self, name, var): if isinstance(var.data, np.ndarray): @@ -95,6 +89,3 @@ def get_encoding(self): encoding = {} encoding['unlimited_dims'] = {k for k, v in self.ds.dimensions.items() if v is None} return encoding - - def close(self): - self._manager.close() From 5dedb3fc65fce12e36e395d532875d2f69328f2f Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Sun, 14 Oct 2018 16:40:34 +0200 Subject: [PATCH 13/27] Add cfgrib testing to Travis-CI. --- ci/requirements-py36.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ci/requirements-py36.yml b/ci/requirements-py36.yml index fd63fe26130..fc272984237 100644 --- a/ci/requirements-py36.yml +++ b/ci/requirements-py36.yml @@ -21,8 +21,10 @@ dependencies: - bottleneck - zarr - pseudonetcdf>=3.0.1 + - eccodes - pip: - coveralls - pytest-cov - pydap - lxml + - cfgrib From 831ae4f27bff2d3d2adcdfe08721abb58464df14 Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Sun, 14 Oct 2018 16:50:10 +0200 Subject: [PATCH 14/27] Naming. --- xarray/tests/test_backends.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index a8d360065e6..3fe13c2ee67 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -2472,7 +2472,7 @@ def test_read(self): assert list(ds.data_vars) == ['z', 't'] assert ds['z'].min() == 12660. - def test_open_filter_by_keys(self): + def test_read_filter_by_keys(self): kwargs = {'filter_by_keys': {'shortName': 't'}} with open_example_dataset('example.grib', engine='cfgrib', backend_kwargs=kwargs) as ds: assert ds.dims == {'number': 2, 'time': 3, 'air_pressure': 2, 'latitude': 3, 'longitude': 4} From 6372e6eec6f962af8e8d64f5afe632c0296d87c3 Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Sun, 14 Oct 2018 17:06:55 +0200 Subject: [PATCH 15/27] Fix line lengths and get to 100% coverage. --- xarray/backends/cfgrib_.py | 22 ++++++++++++++-------- xarray/tests/test_backends.py | 11 ++++++++--- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/xarray/backends/cfgrib_.py b/xarray/backends/cfgrib_.py index 43ca08102fd..23b7761a450 100644 --- a/xarray/backends/cfgrib_.py +++ b/xarray/backends/cfgrib_.py @@ -1,5 +1,5 @@ # -# Copyright 2017-2018 European Centre for Medium-Range Weather Forecasts (ECMWF). +# Copyright 2017-2018 European Centre for Medium-Range Weather Forecasts. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,8 +27,9 @@ from .common import AbstractDataStore, BackendArray from .locks import ensure_lock, SerializableLock -# FIXME: Add a dedicated lock just in case, even if ecCodes is supposed to be thread-safe in most -# circumstances. See: https://confluence.ecmwf.int/display/ECC/Frequently+Asked+Questions +# FIXME: Add a dedicated lock, even if ecCodes is supposed to be thread-safe +# in most circumstances. See: +# https://confluence.ecmwf.int/display/ECC/Frequently+Asked+Questions ECCODES_LOCK = SerializableLock() @@ -58,9 +59,11 @@ def __init__(self, filename, lock=None, **backend_kwargs): lock = ECCODES_LOCK self.lock = ensure_lock(lock) - # NOTE: filter_by_keys is a dict, but CachingFileManager only accepts hashable types + # NOTE: filter_by_keys is a dict, but CachingFileManager only accepts + # hashable types. if 'filter_by_keys' in backend_kwargs: - backend_kwargs['filter_by_keys'] = tuple(backend_kwargs['filter_by_keys'].items()) + filter_by_keys_items = backend_kwargs['filter_by_keys'].items() + backend_kwargs['filter_by_keys'] = tuple(filter_by_keys_items) self.ds = cfgrib.open_file(filename, mode='r', **backend_kwargs) @@ -68,7 +71,8 @@ def open_store_variable(self, name, var): if isinstance(var.data, np.ndarray): data = var.data else: - data = indexing.LazilyOuterIndexedArray(CfGribArrayWrapper(self, var.data)) + wrapped_array = CfGribArrayWrapper(self, var.data) + data = indexing.LazilyOuterIndexedArray(wrapped_array) encoding = self.ds.encoding.copy() encoding['original_shape'] = var.data.shape @@ -86,6 +90,8 @@ def get_dimensions(self): return Frozen(self.ds.dimensions) def get_encoding(self): - encoding = {} - encoding['unlimited_dims'] = {k for k, v in self.ds.dimensions.items() if v is None} + dims = self.get_dimensions() + encoding = { + 'unlimited_dims': {k for k, v in dims.items() if v is None}, + } return encoding diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index 3fe13c2ee67..a274b41c424 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -2467,15 +2467,20 @@ def test_weakrefs(self): class TestCfGrib(object): def test_read(self): + expected = {'number': 2, 'time': 3, 'air_pressure': 2, 'latitude': 3, + 'longitude': 4} with open_example_dataset('example.grib', engine='cfgrib') as ds: - assert ds.dims == {'number': 2, 'time': 3, 'air_pressure': 2, 'latitude': 3, 'longitude': 4} + assert ds.dims == expected assert list(ds.data_vars) == ['z', 't'] assert ds['z'].min() == 12660. def test_read_filter_by_keys(self): kwargs = {'filter_by_keys': {'shortName': 't'}} - with open_example_dataset('example.grib', engine='cfgrib', backend_kwargs=kwargs) as ds: - assert ds.dims == {'number': 2, 'time': 3, 'air_pressure': 2, 'latitude': 3, 'longitude': 4} + expected = {'number': 2, 'time': 3, 'air_pressure': 2, 'latitude': 3, + 'longitude': 4} + with open_example_dataset('example.grib', engine='cfgrib', + backend_kwargs=kwargs) as ds: + assert ds.dims == expected assert list(ds.data_vars) == ['t'] assert ds['t'].min() == 231. From 8e9b2e37bb07e42c808717fea1d82a2a267c66d8 Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Sun, 14 Oct 2018 20:58:52 +0200 Subject: [PATCH 16/27] Add reference to *cfgrib* engine in inline docs. --- xarray/backends/api.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/xarray/backends/api.py b/xarray/backends/api.py index 8816de39a4f..3fb7338e171 100644 --- a/xarray/backends/api.py +++ b/xarray/backends/api.py @@ -162,7 +162,8 @@ def open_dataset(filename_or_obj, group=None, decode_cf=True, decode_coords : bool, optional If True, decode the 'coordinates' attribute to identify coordinates in the resulting dataset. - engine : {'netcdf4', 'scipy', 'pydap', 'h5netcdf', 'pynio', 'pseudonetcdf'}, optional + engine : {'netcdf4', 'scipy', 'pydap', 'h5netcdf', 'pynio', 'cfgrib', + 'pseudonetcdf'}, optional Engine to use when reading files. If not provided, the default engine is chosen based on available dependencies, with a preference for 'netcdf4'. @@ -359,7 +360,8 @@ def open_dataarray(filename_or_obj, group=None, decode_cf=True, decode_coords : bool, optional If True, decode the 'coordinates' attribute to identify coordinates in the resulting dataset. - engine : {'netcdf4', 'scipy', 'pydap', 'h5netcdf', 'pynio'}, optional + engine : {'netcdf4', 'scipy', 'pydap', 'h5netcdf', 'pynio', 'cfgrib'}, + optional Engine to use when reading files. If not provided, the default engine is chosen based on available dependencies, with a preference for 'netcdf4'. @@ -489,7 +491,8 @@ def open_mfdataset(paths, chunks=None, concat_dim=_CONCAT_DIM_DEFAULT, of all non-null values. preprocess : callable, optional If provided, call this function on each dataset prior to concatenation. - engine : {'netcdf4', 'scipy', 'pydap', 'h5netcdf', 'pynio'}, optional + engine : {'netcdf4', 'scipy', 'pydap', 'h5netcdf', 'pynio', 'cfgrib'}, + optional Engine to use when reading files. If not provided, the default engine is chosen based on available dependencies, with a preference for 'netcdf4'. From 07b946909249e4df667ee12c137b06d7bf3367a2 Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Sun, 14 Oct 2018 21:21:57 +0200 Subject: [PATCH 17/27] First cut of the documentation. --- doc/installing.rst | 4 +++- doc/io.rst | 17 +++++++++++++++++ doc/whats-new.rst | 6 +++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/doc/installing.rst b/doc/installing.rst index eb74eb7162b..64751eea637 100644 --- a/doc/installing.rst +++ b/doc/installing.rst @@ -34,7 +34,9 @@ For netCDF and IO - `rasterio `__: for reading GeoTiffs and other gridded raster datasets. - `iris `__: for conversion to and from iris' - Cube objects. + Cube objects +- `cfgrib `__: for reading GRIB files via the + *ECMWF ecCodes* library. For accelerating xarray ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/io.rst b/doc/io.rst index 093ee773e15..cfe552d1d54 100644 --- a/doc/io.rst +++ b/doc/io.rst @@ -635,6 +635,23 @@ For example: Not all native zarr compression and filtering options have been tested with xarray. +.. _io.cfgrib: + +GRIB format via cfgrib +---------------------- + +xarray supports reading GRIB files via ECMWF cfgrib_ python driver and ecCodes_ +C-library, if they are installed. To open GRIB file supply ``engine='cfgrib'`` +to :py:func:`~xarray.open_dataset`: + +We recommend installing ecCodes via conda:: + + conda install -c conda-forge eccodes + pip install cfgrib + +.. _cfgrib: https://github.com/ecmwf/cfgrib +.. _ecCodes: https://confluence.ecmwf.int/display/ECC/ecCodes+Home + .. _io.pynio: Formats supported by PyNIO diff --git a/doc/whats-new.rst b/doc/whats-new.rst index c8ae5ac43c8..31771020c84 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -72,7 +72,11 @@ Enhancements :py:meth:`~xarray.DataArray.interp`, and :py:meth:`~xarray.Dataset.interp`. By `Spencer Clark `_ - +- Added a new backend for the GRIB file format based on ECMWF *cfgrib* + python driver and *ecCodes* C-library. (:issue:`2475`) + By `Alessandro Amici `_, + sponsored by `ECMWF `_. + Bug fixes ~~~~~~~~~ From 340720a4e68dc4638e2d99cf7fd8d3191927e4f1 Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Mon, 15 Oct 2018 00:16:44 +0200 Subject: [PATCH 18/27] Tentative test cfgrib under dask.distributed. --- xarray/tests/test_distributed.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/xarray/tests/test_distributed.py b/xarray/tests/test_distributed.py index 7c77a62d3c9..764d2c7d4b5 100644 --- a/xarray/tests/test_distributed.py +++ b/xarray/tests/test_distributed.py @@ -20,12 +20,12 @@ import xarray as xr from xarray.backends.locks import HDF5_LOCK, CombinedLock from xarray.tests.test_backends import (ON_WINDOWS, create_tmp_file, - create_tmp_geotiff) + create_tmp_geotiff, open_example_dataset) from xarray.tests.test_dataset import create_test_data from . import ( assert_allclose, has_h5netcdf, has_netCDF4, requires_rasterio, has_scipy, - requires_zarr, raises_regex) + requires_zarr, requires_cfgrib, raises_regex) # this is to stop isort throwing errors. May have been easier to just use # `isort:skip` in retrospect @@ -142,6 +142,18 @@ def test_dask_distributed_rasterio_integration_test(loop): assert_allclose(actual, expected) +@requires_cfgrib +def test_dask_distributed_cfgrib_integration_test(loop): + with cluster() as (s, [a, b]): + with Client(s['address'], loop=loop) as c: + ds = open_example_dataset('example.grib', engine='cfgrib', chunks={'time': 1}) + da_grib = ds['t'] + assert isinstance(da_grib.data, da.Array) + actual = da_grib.compute() + assert actual.dims == ('number', 'time', 'air_pressure', 'latitude', 'longitude') + assert actual.coords['time'].size == 3 + + @pytest.mark.skipif(distributed.__version__ <= '1.19.3', reason='Need recent distributed version to clean up get') @gen_cluster(client=True, timeout=None) From 4d84f708fb5f1f8a1712fb5bc9a1ab7fb4a23588 Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Mon, 15 Oct 2018 00:25:05 +0200 Subject: [PATCH 19/27] Better integration test. --- xarray/tests/test_distributed.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/xarray/tests/test_distributed.py b/xarray/tests/test_distributed.py index 764d2c7d4b5..9af1ca02408 100644 --- a/xarray/tests/test_distributed.py +++ b/xarray/tests/test_distributed.py @@ -146,12 +146,14 @@ def test_dask_distributed_rasterio_integration_test(loop): def test_dask_distributed_cfgrib_integration_test(loop): with cluster() as (s, [a, b]): with Client(s['address'], loop=loop) as c: - ds = open_example_dataset('example.grib', engine='cfgrib', chunks={'time': 1}) - da_grib = ds['t'] - assert isinstance(da_grib.data, da.Array) - actual = da_grib.compute() - assert actual.dims == ('number', 'time', 'air_pressure', 'latitude', 'longitude') - assert actual.coords['time'].size == 3 + with open_example_dataset('example.grib', + engine='cfgrib', + chunks={'time': 1}) as ds: + with open_example_dataset('example.grib', + engine='cfgrib') as expected: + assert isinstance(ds['t'].data, da.Array) + actual = ds.compute() + assert_allclose(actual, expected) @pytest.mark.skipif(distributed.__version__ <= '1.19.3', From 0b027db7ef3bd90ec7a3f22c74772fed1f21e241 Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Mon, 15 Oct 2018 11:27:52 +0200 Subject: [PATCH 20/27] Remove explicit copyright and license boilerplate to harmonise with other files. --- xarray/backends/cfgrib_.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/xarray/backends/cfgrib_.py b/xarray/backends/cfgrib_.py index 23b7761a450..fbe39bcdb1f 100644 --- a/xarray/backends/cfgrib_.py +++ b/xarray/backends/cfgrib_.py @@ -1,22 +1,3 @@ -# -# Copyright 2017-2018 European Centre for Medium-Range Weather Forecasts. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Authors: -# Alessandro Amici - B-Open - https://bopen.eu -# - from __future__ import absolute_import, division, print_function import numpy as np From a4ead545f1a19ad62802a16284817c04d739dd79 Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Mon, 15 Oct 2018 11:34:37 +0200 Subject: [PATCH 21/27] Add a usage example. --- doc/io.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/io.rst b/doc/io.rst index cfe552d1d54..2363146ce0b 100644 --- a/doc/io.rst +++ b/doc/io.rst @@ -641,9 +641,13 @@ GRIB format via cfgrib ---------------------- xarray supports reading GRIB files via ECMWF cfgrib_ python driver and ecCodes_ -C-library, if they are installed. To open GRIB file supply ``engine='cfgrib'`` +C-library, if they are installed. To open a GRIB file supply ``engine='cfgrib'`` to :py:func:`~xarray.open_dataset`: +.. ipython:: python + + ds_grib = xr.open_dataset('example.grib', engine='cfgrib') + We recommend installing ecCodes via conda:: conda install -c conda-forge eccodes From ec80d86e43d418c26349dbfb785d11f030027a70 Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Mon, 15 Oct 2018 15:59:38 +0200 Subject: [PATCH 22/27] Fix code style. --- xarray/tests/test_distributed.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xarray/tests/test_distributed.py b/xarray/tests/test_distributed.py index 9af1ca02408..1837a0fe4ef 100644 --- a/xarray/tests/test_distributed.py +++ b/xarray/tests/test_distributed.py @@ -20,7 +20,8 @@ import xarray as xr from xarray.backends.locks import HDF5_LOCK, CombinedLock from xarray.tests.test_backends import (ON_WINDOWS, create_tmp_file, - create_tmp_geotiff, open_example_dataset) + create_tmp_geotiff, + open_example_dataset) from xarray.tests.test_dataset import create_test_data from . import ( From f30b7d0bbc71756be8d768c76caf7330b3e62afa Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Tue, 16 Oct 2018 12:43:35 +0200 Subject: [PATCH 23/27] Fix doc style. --- xarray/backends/cfgrib_.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xarray/backends/cfgrib_.py b/xarray/backends/cfgrib_.py index fbe39bcdb1f..c0a7c025606 100644 --- a/xarray/backends/cfgrib_.py +++ b/xarray/backends/cfgrib_.py @@ -9,8 +9,8 @@ from .locks import ensure_lock, SerializableLock # FIXME: Add a dedicated lock, even if ecCodes is supposed to be thread-safe -# in most circumstances. See: -# https://confluence.ecmwf.int/display/ECC/Frequently+Asked+Questions +# in most circumstances. See: +# https://confluence.ecmwf.int/display/ECC/Frequently+Asked+Questions ECCODES_LOCK = SerializableLock() From 223d25c16d4e3394051426ad7532888f2a9561b6 Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Wed, 17 Oct 2018 08:57:30 +0200 Subject: [PATCH 24/27] Fix docs testing. The example.grib file is not accessible. --- doc/io.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/io.rst b/doc/io.rst index 2363146ce0b..9e7a94e2646 100644 --- a/doc/io.rst +++ b/doc/io.rst @@ -645,6 +645,7 @@ C-library, if they are installed. To open a GRIB file supply ``engine='cfgrib'`` to :py:func:`~xarray.open_dataset`: .. ipython:: python + :verbatim: ds_grib = xr.open_dataset('example.grib', engine='cfgrib') From bbf01e30088010fe334fbc3ee815719e4122b03f Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Wed, 17 Oct 2018 09:00:21 +0200 Subject: [PATCH 25/27] Fix merge in docs. --- doc/whats-new.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 2b5696ec3a7..03404e7e6b6 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -71,7 +71,7 @@ Enhancements :py:meth:`~xarray.Dataset.differentiate`, :py:meth:`~xarray.DataArray.interp`, and :py:meth:`~xarray.Dataset.interp`. - By `Spencer Clark `_ + By `Spencer Clark `_. - Added a new backend for the GRIB file format based on ECMWF *cfgrib* python driver and *ecCodes* C-library. (:issue:`2475`) By `Alessandro Amici `_, From da2b9ddca940b627168781dfc14594c1367010f0 Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Wed, 17 Oct 2018 09:00:42 +0200 Subject: [PATCH 26/27] Fix merge in docs. --- doc/whats-new.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 03404e7e6b6..61da801badb 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -77,8 +77,6 @@ Enhancements By `Alessandro Amici `_, sponsored by `ECMWF `_. - By `Spencer Clark `_. - Bug fixes ~~~~~~~~~ From eda96a495844fdff53a0da9a35e56a3f3e575267 Mon Sep 17 00:00:00 2001 From: Alessandro Amici Date: Wed, 17 Oct 2018 17:52:04 +0200 Subject: [PATCH 27/27] Fix doc style. --- doc/io.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/io.rst b/doc/io.rst index 9e7a94e2646..e841e665308 100644 --- a/doc/io.rst +++ b/doc/io.rst @@ -644,10 +644,10 @@ xarray supports reading GRIB files via ECMWF cfgrib_ python driver and ecCodes_ C-library, if they are installed. To open a GRIB file supply ``engine='cfgrib'`` to :py:func:`~xarray.open_dataset`: -.. ipython:: python +.. ipython:: :verbatim: - ds_grib = xr.open_dataset('example.grib', engine='cfgrib') + In [1]: ds_grib = xr.open_dataset('example.grib', engine='cfgrib') We recommend installing ecCodes via conda::