Skip to content

Commit e544e0d

Browse files
author
Joe Hamman
authored
Add netcdftime as an optional dependency. (#1920)
* rework imports to support using netcdftime package when netcdf4-python is not installed * temporary travis build for netcdftime dev * flake8 and rework import logic * add missing netcdftime environment * fix typo in unidata * cython too * use conda-forge * require netcdf4 for tests that read/write
1 parent be27319 commit e544e0d

File tree

8 files changed

+69
-26
lines changed

8 files changed

+69
-26
lines changed

.travis.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ matrix:
4545
env: CONDA_ENV=py36-rasterio1.0alpha
4646
- python: 3.6
4747
env: CONDA_ENV=py36-zarr-dev
48+
- python: 3.6
49+
env: CONDA_ENV=py36-netcdftime-dev
4850
- python: 3.5
4951
env: CONDA_ENV=docs
5052
allow_failures:
@@ -73,6 +75,8 @@ matrix:
7375
env: CONDA_ENV=py36-rasterio1.0alpha
7476
- python: 3.6
7577
env: CONDA_ENV=py36-zarr-dev
78+
- python: 3.6
79+
env: CONDA_ENV=py36-netcdftime-dev
7680

7781
before_install:
7882
- if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name: test_env
2+
channels:
3+
- conda-forge
4+
dependencies:
5+
- python=3.6
6+
- pytest
7+
- flake8
8+
- numpy
9+
- pandas
10+
- netcdftime
11+
- pip:
12+
- coveralls
13+
- pytest-cov

doc/installing.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ For netCDF and IO
2525
- `pynio <https://www.pyngl.ucar.edu/Nio.shtml>`__: for reading GRIB and other
2626
geoscience specific file formats
2727
- `zarr <http://zarr.readthedocs.io/>`__: for chunked, compressed, N-dimensional arrays.
28+
- `netcdftime <https://github.com/Unidata/netcdftime>`__: recommended if you
29+
want to encode/decode datetimes for non-standard calendars.
2830

2931
For accelerating xarray
3032
~~~~~~~~~~~~~~~~~~~~~~~

doc/whats-new.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ Enhancements
9393
- Speed of reindexing/alignment with dask array is orders of magnitude faster
9494
when inserting missing values (:issue:`1847`).
9595
By `Stephan Hoyer <https://github.com/shoyer>`_.
96+
- Add ``netcdftime`` as an optional dependency of xarray. This allows for
97+
encoding/decoding of datetimes with non-standard calendars without the
98+
netCDF4 dependency (:issue:`1084`).
99+
By `Joe Hamman <https://github.com/jhamman>`_.
96100

97101
.. _Zarr: http://zarr.readthedocs.io/
98102

xarray/backends/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ def open_mfdataset(paths, chunks=None, concat_dim=_CONCAT_DIM_DEFAULT,
443443
lock=None, data_vars='all', coords='different', **kwargs):
444444
"""Open multiple files as a single dataset.
445445
446-
Requires dask to be installed. See documentation for details on dask [1].
446+
Requires dask to be installed. See documentation for details on dask [1].
447447
Attributes from the first dataset file are used for the combined dataset.
448448
449449
Parameters

xarray/coding/times.py

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,26 @@
4040
'milliseconds', 'microseconds'])
4141

4242

43+
def _import_netcdftime():
44+
'''
45+
helper function handle the transition to netcdftime as a stand-alone
46+
package
47+
'''
48+
try:
49+
# Try importing netcdftime directly
50+
import netcdftime as nctime
51+
if not hasattr(nctime, 'num2date'):
52+
# must have gotten an old version from netcdf4-python
53+
raise ImportError
54+
except ImportError:
55+
# in netCDF4 the num2date/date2num function are top-level api
56+
try:
57+
import netCDF4 as nctime
58+
except ImportError:
59+
raise ImportError("Failed to import netcdftime")
60+
return nctime
61+
62+
4363
def _netcdf_to_numpy_timeunit(units):
4464
units = units.lower()
4565
if not units.endswith('s'):
@@ -59,23 +79,23 @@ def _unpack_netcdf_time_units(units):
5979
return delta_units, ref_date
6080

6181

62-
def _decode_datetime_with_netcdf4(num_dates, units, calendar):
63-
import netCDF4 as nc4
82+
def _decode_datetime_with_netcdftime(num_dates, units, calendar):
83+
nctime = _import_netcdftime()
6484

65-
dates = np.asarray(nc4.num2date(num_dates, units, calendar))
85+
dates = np.asarray(nctime.num2date(num_dates, units, calendar))
6686
if (dates[np.nanargmin(num_dates)].year < 1678 or
6787
dates[np.nanargmax(num_dates)].year >= 2262):
6888
warnings.warn('Unable to decode time axis into full '
6989
'numpy.datetime64 objects, continuing using dummy '
70-
'netCDF4.datetime objects instead, reason: dates out'
90+
'netcdftime.datetime objects instead, reason: dates out'
7191
' of range', SerializationWarning, stacklevel=3)
7292
else:
7393
try:
7494
dates = nctime_to_nptime(dates)
7595
except ValueError as e:
7696
warnings.warn('Unable to decode time axis into full '
7797
'numpy.datetime64 objects, continuing using '
78-
'dummy netCDF4.datetime objects instead, reason:'
98+
'dummy netcdftime.datetime objects instead, reason:'
7999
'{0}'.format(e), SerializationWarning, stacklevel=3)
80100
return dates
81101

@@ -111,15 +131,15 @@ def decode_cf_datetime(num_dates, units, calendar=None):
111131
numpy array of date time objects.
112132
113133
For standard (Gregorian) calendars, this function uses vectorized
114-
operations, which makes it much faster than netCDF4.num2date. In such a
134+
operations, which makes it much faster than netcdftime.num2date. In such a
115135
case, the returned array will be of type np.datetime64.
116136
117137
Note that time unit in `units` must not be smaller than microseconds and
118138
not larger than days.
119139
120140
See also
121141
--------
122-
netCDF4.num2date
142+
netcdftime.num2date
123143
"""
124144
num_dates = np.asarray(num_dates)
125145
flat_num_dates = num_dates.ravel()
@@ -137,7 +157,7 @@ def decode_cf_datetime(num_dates, units, calendar=None):
137157
ref_date = pd.Timestamp(ref_date)
138158
except ValueError:
139159
# ValueError is raised by pd.Timestamp for non-ISO timestamp
140-
# strings, in which case we fall back to using netCDF4
160+
# strings, in which case we fall back to using netcdftime
141161
raise OutOfBoundsDatetime
142162

143163
# fixes: https://github.com/pydata/pandas/issues/14068
@@ -155,9 +175,8 @@ def decode_cf_datetime(num_dates, units, calendar=None):
155175
ref_date).values
156176

157177
except (OutOfBoundsDatetime, OverflowError):
158-
dates = _decode_datetime_with_netcdf4(flat_num_dates.astype(np.float),
159-
units,
160-
calendar)
178+
dates = _decode_datetime_with_netcdftime(
179+
flat_num_dates.astype(np.float), units, calendar)
161180

162181
return dates.reshape(num_dates.shape)
163182

@@ -215,7 +234,7 @@ def infer_timedelta_units(deltas):
215234

216235

217236
def nctime_to_nptime(times):
218-
"""Given an array of netCDF4.datetime objects, return an array of
237+
"""Given an array of netcdftime.datetime objects, return an array of
219238
numpy.datetime64 objects of the same size"""
220239
times = np.asarray(times)
221240
new = np.empty(times.shape, dtype='M8[ns]')
@@ -235,20 +254,20 @@ def _cleanup_netcdf_time_units(units):
235254
return units
236255

237256

238-
def _encode_datetime_with_netcdf4(dates, units, calendar):
239-
"""Fallback method for encoding dates using netCDF4-python.
257+
def _encode_datetime_with_netcdftime(dates, units, calendar):
258+
"""Fallback method for encoding dates using netcdftime.
240259
241260
This method is more flexible than xarray's parsing using datetime64[ns]
242261
arrays but also slower because it loops over each element.
243262
"""
244-
import netCDF4 as nc4
263+
nctime = _import_netcdftime()
245264

246265
if np.issubdtype(dates.dtype, np.datetime64):
247266
# numpy's broken datetime conversion only works for us precision
248267
dates = dates.astype('M8[us]').astype(datetime)
249268

250269
def encode_datetime(d):
251-
return np.nan if d is None else nc4.date2num(d, units, calendar)
270+
return np.nan if d is None else nctime.date2num(d, units, calendar)
252271

253272
return np.vectorize(encode_datetime)(dates)
254273

@@ -268,7 +287,7 @@ def encode_cf_datetime(dates, units=None, calendar=None):
268287
269288
See also
270289
--------
271-
netCDF4.date2num
290+
netcdftime.date2num
272291
"""
273292
dates = np.asarray(dates)
274293

@@ -283,7 +302,7 @@ def encode_cf_datetime(dates, units=None, calendar=None):
283302
delta, ref_date = _unpack_netcdf_time_units(units)
284303
try:
285304
if calendar not in _STANDARD_CALENDARS or dates.dtype.kind == 'O':
286-
# parse with netCDF4 instead
305+
# parse with netcdftime instead
287306
raise OutOfBoundsDatetime
288307
assert dates.dtype == 'datetime64[ns]'
289308

@@ -293,7 +312,7 @@ def encode_cf_datetime(dates, units=None, calendar=None):
293312
num = (dates - ref_date) / time_delta
294313

295314
except (OutOfBoundsDatetime, OverflowError):
296-
num = _encode_datetime_with_netcdf4(dates, units, calendar)
315+
num = _encode_datetime_with_netcdftime(dates, units, calendar)
297316

298317
num = cast_to_int_if_safe(num)
299318
return (num, units, calendar)

xarray/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def _importorskip(modname, minversion=None):
6868
has_netCDF4, requires_netCDF4 = _importorskip('netCDF4')
6969
has_h5netcdf, requires_h5netcdf = _importorskip('h5netcdf')
7070
has_pynio, requires_pynio = _importorskip('Nio')
71+
has_netcdftime, requires_netcdftime = _importorskip('netcdftime')
7172
has_dask, requires_dask = _importorskip('dask')
7273
has_bottleneck, requires_bottleneck = _importorskip('bottleneck')
7374
has_rasterio, requires_rasterio = _importorskip('rasterio')

xarray/tests/test_conventions.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
from xarray.core import utils, indexing
1414
from xarray.testing import assert_identical
1515
from . import (
16-
TestCase, requires_netCDF4, unittest, raises_regex, IndexerMaker,
17-
assert_array_equal)
16+
TestCase, requires_netCDF4, requires_netcdftime, unittest, raises_regex,
17+
IndexerMaker, assert_array_equal)
1818
from .test_backends import CFEncodedDataTest
1919
from xarray.core.pycompat import iteritems
2020
from xarray.backends.memory import InMemoryDataStore
@@ -181,7 +181,7 @@ def test_decode_cf_with_conflicting_fill_missing_value():
181181
assert_identical(actual, expected)
182182

183183

184-
@requires_netCDF4
184+
@requires_netcdftime
185185
class TestEncodeCFVariable(TestCase):
186186
def test_incompatible_attributes(self):
187187
invalid_vars = [
@@ -237,7 +237,7 @@ def test_multidimensional_coordinates(self):
237237
assert 'coordinates' not in attrs
238238

239239

240-
@requires_netCDF4
240+
@requires_netcdftime
241241
class TestDecodeCF(TestCase):
242242
def test_dataset(self):
243243
original = Dataset({
@@ -303,7 +303,7 @@ def test_invalid_time_units_raises_eagerly(self):
303303
with raises_regex(ValueError, 'unable to decode time'):
304304
decode_cf(ds)
305305

306-
@requires_netCDF4
306+
@requires_netcdftime
307307
def test_dataset_repr_with_netcdf4_datetimes(self):
308308
# regression test for #347
309309
attrs = {'units': 'days since 0001-01-01', 'calendar': 'noleap'}
@@ -316,7 +316,7 @@ def test_dataset_repr_with_netcdf4_datetimes(self):
316316
ds = decode_cf(Dataset({'time': ('time', [0, 1], attrs)}))
317317
assert '(time) datetime64[ns]' in repr(ds)
318318

319-
@requires_netCDF4
319+
@requires_netcdftime
320320
def test_decode_cf_datetime_transition_to_invalid(self):
321321
# manually create dataset with not-decoded date
322322
from datetime import datetime

0 commit comments

Comments
 (0)