Repository: pydata/xarray

Issue:
Better error message for setting encoding["units"] 
<!-- Please include a self-contained copy-pastable example that generates the issue if possible

-->

**What happened**:

Setting invalid units for time axis encoding results in an exception ``AttributeError: 'NoneType' object has no attribute 'groups'``

**What you expected to happen**:

It should say "invalid time units", like this (see commented out line below) ``ValueError: invalid time units: days after 1/3/2000``

**Minimal Complete Verifiable Example**:

```python
# Put your MCVE code here
import pandas as pd
import xarray as xr
ds = xr.Dataset(data_vars={'v': (('t',), [0,])},
                coords={'t': [pd.Timestamp(2000, 1, 1)]})
ds.t.encoding['units'] = 'days since Big Bang'
#ds.t.encoding['units'] = 'days after 1/3/2000'
ds.to_netcdf('/tmp/x.nc', mode='w')

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-16-0d97a797be64> in <module>
      5 ds.t.encoding['units'] = 'days since Big Bang'
      6 #ds.t.encoding['units'] = 'days after 1/3/2000'
----> 7 ds.to_netcdf('/tmp/x.nc', mode='w')

~/miniconda3/envs/xarray/lib/python3.9/site-packages/xarray-0.0.0-py3.9.egg/xarray/core/dataset.py in to_netcdf(self, path, mode, format, group, engine, encoding, unlimited_dims, compute, invalid_netcdf)
   1752         from ..backends.api import to_netcdf
   1753 
-> 1754         return to_netcdf(
   1755             self,
   1756             path,

~/miniconda3/envs/xarray/lib/python3.9/site-packages/xarray-0.0.0-py3.9.egg/xarray/backends/api.py in to_netcdf(dataset, path_or_file, mode, format, group, engine, encoding, unlimited_dims, compute, multifile, invalid_netcdf)
   1066         # TODO: allow this work (setting up the file for writing array data)
   1067         # to be parallelized with dask
-> 1068         dump_to_store(
   1069             dataset, store, writer, encoding=encoding, unlimited_dims=unlimited_dims
   1070         )

~/miniconda3/envs/xarray/lib/python3.9/site-packages/xarray-0.0.0-py3.9.egg/xarray/backends/api.py in dump_to_store(dataset, store, writer, encoder, encoding, unlimited_dims)
   1113         variables, attrs = encoder(variables, attrs)
   1114 
-> 1115     store.store(variables, attrs, check_encoding, writer, unlimited_dims=unlimited_dims)
   1116 
   1117 

~/miniconda3/envs/xarray/lib/python3.9/site-packages/xarray-0.0.0-py3.9.egg/xarray/backends/common.py in store(self, variables, attributes, check_encoding_set, writer, unlimited_dims)
    261             writer = ArrayWriter()
    262 
--> 263         variables, attributes = self.encode(variables, attributes)
    264 
    265         self.set_attributes(attributes)

~/miniconda3/envs/xarray/lib/python3.9/site-packages/xarray-0.0.0-py3.9.egg/xarray/backends/common.py in encode(self, variables, attributes)
    350         # All NetCDF files get CF encoded by default, without this attempting
    351         # to write times, for example, would fail.
--> 352         variables, attributes = cf_encoder(variables, attributes)
    353         variables = {k: self.encode_variable(v) for k, v in variables.items()}
    354         attributes = {k: self.encode_attribute(v) for k, v in attributes.items()}

~/miniconda3/envs/xarray/lib/python3.9/site-packages/xarray-0.0.0-py3.9.egg/xarray/conventions.py in cf_encoder(variables, attributes)
    841     _update_bounds_encoding(variables)
    842 
--> 843     new_vars = {k: encode_cf_variable(v, name=k) for k, v in variables.items()}
    844 
    845     # Remove attrs from bounds variables (issue #2921)

~/miniconda3/envs/xarray/lib/python3.9/site-packages/xarray-0.0.0-py3.9.egg/xarray/conventions.py in <dictcomp>(.0)
    841     _update_bounds_encoding(variables)
    842 
--> 843     new_vars = {k: encode_cf_variable(v, name=k) for k, v in variables.items()}
    844 
    845     # Remove attrs from bounds variables (issue #2921)

~/miniconda3/envs/xarray/lib/python3.9/site-packages/xarray-0.0.0-py3.9.egg/xarray/conventions.py in encode_cf_variable(var, needs_copy, name)
    267         variables.UnsignedIntegerCoder(),
    268     ]:
--> 269         var = coder.encode(var, name=name)
    270 
    271     # TODO(shoyer): convert all of these to use coders, too:

~/miniconda3/envs/xarray/lib/python3.9/site-packages/xarray-0.0.0-py3.9.egg/xarray/coding/times.py in encode(self, variable, name)
    510             variable
    511         ):
--> 512             (data, units, calendar) = encode_cf_datetime(
    513                 data, encoding.pop("units", None), encoding.pop("calendar", None)
    514             )

~/miniconda3/envs/xarray/lib/python3.9/site-packages/xarray-0.0.0-py3.9.egg/xarray/coding/times.py in encode_cf_datetime(dates, units, calendar)
    448         units = infer_datetime_units(dates)
    449     else:
--> 450         units = _cleanup_netcdf_time_units(units)
    451 
    452     if calendar is None:

~/miniconda3/envs/xarray/lib/python3.9/site-packages/xarray-0.0.0-py3.9.egg/xarray/coding/times.py in _cleanup_netcdf_time_units(units)
    399 
    400 def _cleanup_netcdf_time_units(units):
--> 401     delta, ref_date = _unpack_netcdf_time_units(units)
    402     try:
    403         units = "{} since {}".format(delta, format_timestamp(ref_date))

~/miniconda3/envs/xarray/lib/python3.9/site-packages/xarray-0.0.0-py3.9.egg/xarray/coding/times.py in _unpack_netcdf_time_units(units)
    130 
    131     delta_units, ref_date = [s.strip() for s in matches.groups()]
--> 132     ref_date = _ensure_padded_year(ref_date)
    133 
    134     return delta_units, ref_date

~/miniconda3/envs/xarray/lib/python3.9/site-packages/xarray-0.0.0-py3.9.egg/xarray/coding/times.py in _ensure_padded_year(ref_date)
    105     # appropriately
    106     matches_start_digits = re.match(r"(\d+)(.*)", ref_date)
--> 107     ref_year, everything_else = [s for s in matches_start_digits.groups()]
    108     ref_date_padded = "{:04d}{}".format(int(ref_year), everything_else)
    109 

AttributeError: 'NoneType' object has no attribute 'groups'
```
**Anything else we need to know?**:
Are there  detail specifications for the valid units string? Setting it to 1/3/2000 surprised me (my locale: LANG=en_CA.UTF-8).
**Environment**:
Latest code from github: xarray version 0.0.0
<details><summary>Output of <tt>xr.show_versions()</tt></summary>
INSTALLED VERSIONS
------------------
commit: None
python: 3.9.2 | packaged by conda-forge | (default, Feb 21 2021, 05:02:46) 
[GCC 9.3.0]
python-bits: 64
OS: Linux
OS-release: 5.10.11-200.fc33.x86_64
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: en_CA.UTF-8
LOCALE: en_CA.UTF-8
libhdf5: 1.10.6
libnetcdf: 4.8.0

xarray: 0.0.0
pandas: 1.2.4
numpy: 1.20.2
scipy: None
netCDF4: 1.5.6
pydap: None
h5netcdf: None
h5py: None
Nio: None
zarr: 2.7.1
cftime: 1.4.1
nc_time_axis: None
PseudoNetCDF: None
rasterio: None
cfgrib: None
iris: None
bottleneck: None
dask: 2021.04.0
distributed: 2021.04.0
matplotlib: None
cartopy: None
seaborn: None
numbagg: None
pint: None
setuptools: 49.6.0.post20210108
pip: 21.0.1
conda: None
pytest: None
IPython: 7.22.0
sphinx: None
<!-- Paste the output here xr.show_versions() here -->


</details>


Discussion:
pep8speaks: Hello @gcaria! Thanks for updating this PR. We checked the lines you've touched for [PEP 8](https://www.python.org/dev/peps/pep-0008) issues, and found:



* In the file [`xarray/tests/test_coding_times.py`](https://github.com/pydata/xarray/blob/6d3eaa6fa4d1c1d2155128313b94798c3b56f519/xarray/tests/test_coding_times.py):

> [Line 832:1](https://github.com/pydata/xarray/blob/6d3eaa6fa4d1c1d2155128313b94798c3b56f519/xarray/tests/test_coding_times.py#L832): [E302](https://duckduckgo.com/?q=pep8%20E302) expected 2 blank lines, found 1


gcaria: Done :) Since it was very little effort, I've also added a test for the other related error raising.

mathause: Sorry I wasn't entirely clear - I meant "the tests belong in this general vicinity" - could you put them in their own function? You can also give yourself credit by adding something to whats-new (up to you).

Patch:
diff --git a/HOW_TO_RELEASE.md b/HOW_TO_RELEASE.md
index e30b05c4..e526cd7e 100644
--- a/HOW_TO_RELEASE.md
+++ b/HOW_TO_RELEASE.md
@@ -1,4 +1,4 @@
-# How to issue an xarray release in 20 easy steps
+# How to issue an xarray release in 16 easy steps
 
 Time required: about an hour.
 
@@ -57,51 +57,30 @@ upstream        https://github.com/pydata/xarray (push)
       pytest
       ```
  9. Check that the ReadTheDocs build is passing.
-10. Tag the release:
-      ```sh
-      git tag -a v{0.X.Y} -m 'v{0.X.Y}'
-      ```
-11. Ensure the dependencies for building are installed:
-      ```sh
-      pip install setuptools-scm twine wheel
-      ```
-12. Build source and binary wheels for PyPI:
-      ```sh
-      git clean -xdf  # This removes any untracked files!
-      git restore -SW .  # This removes any tracked changes!
-      python setup.py bdist_wheel sdist
-      ```
-13. Use twine to check the package build:
-      ```sh
-      twine check dist/xarray-{0.X.Y}*
-      ```
-14. Use twine to register and upload the release on PyPI. Be careful, you can't
-    take this back!
-      ```sh
-      twine upload dist/xarray-{0.X.Y}*
-      ```
-    You will need to be listed as a package owner at
-    <https://pypi.python.org/pypi/xarray> for this to work.
-15. Push your changes to master:
-      ```sh
-      git push upstream master
-      git push upstream --tags
-      ```
-16. Update the stable branch (used by ReadTheDocs) and switch back to master:
+10. Issue the release on GitHub. Click on "Draft a new release" at
+    <https://github.com/pydata/xarray/releases>. Type in the version number (with a "v")
+    and paste the release summary in the notes.
+11. This should automatically trigger an upload of the new build to PyPI via GitHub Actions.
+    Check this has run [here](https://github.com/pydata/xarray/actions/workflows/pypi-release.yaml),
+    and that the version number you expect is displayed [on PyPI](https://pypi.org/project/xarray/)
+12. Update the stable branch (used by ReadTheDocs) and switch back to master:
      ```sh
       git switch stable
       git rebase master
       git push --force upstream stable
       git switch master
      ```
+    You may need to first fetch it with `git fetch upstream`,
+    and check out a local version with `git checkout -b stable upstream/stable`.
+
     It's OK to force push to `stable` if necessary. (We also update the stable
     branch with `git cherry-pick` for documentation only fixes that apply the
     current released version.)
-17. Add a section for the next release {0.X.Y+1} to doc/whats-new.rst:
+13. Add a section for the next release {0.X.Y+1} to doc/whats-new.rst:
      ```rst
-     .. _whats-new.{0.X.Y+1}:
+     .. _whats-new.0.X.Y+1:
 
-     v{0.X.Y+1} (unreleased)
+     v0.X.Y+1 (unreleased)
      ---------------------
 
      New Features
@@ -128,19 +107,17 @@ upstream        https://github.com/pydata/xarray (push)
      ~~~~~~~~~~~~~~~~
 
      ```
-18. Commit your changes and push to master again:
+14. Commit your changes and push to master again:
       ```sh
       git commit -am 'New whatsnew section'
       git push upstream master
       ```
     You're done pushing to master!
-19. Issue the release on GitHub. Click on "Draft a new release" at
-    <https://github.com/pydata/xarray/releases>. Type in the version number
-    and paste the release summary in the notes.
-20. Update the docs. Login to <https://readthedocs.org/projects/xray/versions/>
+
+15. Update the docs. Login to <https://readthedocs.org/projects/xray/versions/>
     and switch your new release tag (at the bottom) from "Inactive" to "Active".
     It should now build automatically.
-21. Issue the release announcement to mailing lists & Twitter. For bug fix releases, I
+16. Issue the release announcement to mailing lists & Twitter. For bug fix releases, I
     usually only email xarray@googlegroups.com. For major/feature releases, I will email a broader
     list (no more than once every 3-6 months):
       - pydata@googlegroups.com
@@ -151,6 +128,7 @@ upstream        https://github.com/pydata/xarray (push)
 
     Google search will turn up examples of prior release announcements (look for
     "ANN xarray").
+    Some of these groups require you to be subscribed in order to email them.
 
 <!-- markdownlint-enable MD013 -->
 
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index d07a2741..d3ab043e 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -27,6 +27,8 @@ New Features
 - :py:meth:`Dataset.interp` now allows interpolation with non-numerical datatypes,
   such as booleans, instead of dropping them. (:issue:`4761` :pull:`5008`).
   By `Jimmy Westling <https://github.com/illviljan>`_.
+- Raise more informative error when decoding time variables with invalid reference dates.
+  (:issue:`5199`, :pull:`5288`). By `Giacomo Caria <https://github.com/gcaria>`_.
 
 Breaking changes
 ~~~~~~~~~~~~~~~~
@@ -54,7 +56,11 @@ Internal Changes
 - Explicit indexes refactor: add an ``xarray.Index`` base class and
   ``Dataset.xindexes`` / ``DataArray.xindexes`` properties. Also rename
   ``PandasIndexAdapter`` to ``PandasIndex``, which now inherits from
-  ``xarray.Index`` (:pull:`5102`). By `Benoit Bovy <https://github.com/benbovy>`_.
+  ``xarray.Index`` (:pull:`5102`).
+  By `Benoit Bovy <https://github.com/benbovy>`_.
+- Updated the release guide for developers. Now accounts for actions that are automated via github
+  actions. (:pull:`5274`).
+  By `Tom Nicholas <https://github.com/TomNicholas>`_.
 
 .. _whats-new.0.18.0:
 
diff --git a/xarray/coding/times.py b/xarray/coding/times.py
index 9f5d1f87..f62a3961 100644
--- a/xarray/coding/times.py
+++ b/xarray/coding/times.py
@@ -104,6 +104,8 @@ def _ensure_padded_year(ref_date):
     # No four-digit strings, assume the first digits are the year and pad
     # appropriately
     matches_start_digits = re.match(r"(\d+)(.*)", ref_date)
+    if not matches_start_digits:
+        raise ValueError(f"invalid reference date for time units: {ref_date}")
     ref_year, everything_else = [s for s in matches_start_digits.groups()]
     ref_date_padded = "{:04d}{}".format(int(ref_year), everything_else)
 


Test patch:
diff --git a/xarray/tests/test_coding_times.py b/xarray/tests/test_coding_times.py
index 67ffbf14..9f8ccf2e 100644
--- a/xarray/tests/test_coding_times.py
+++ b/xarray/tests/test_coding_times.py
@@ -823,6 +823,12 @@ def test_encode_cf_datetime_overflow(shape):
     roundtrip = decode_cf_datetime(num, units, calendar)
     np.testing.assert_array_equal(dates, roundtrip)
 
+    with pytest.raises(ValueError, match="invalid time units"):
+        encode_cf_datetime(dates, units="days after 2000-01-01")
+
+    with pytest.raises(ValueError, match="invalid reference date"):
+        encode_cf_datetime(dates, units="days since NO_YEAR")
+
 
 def test_encode_cf_datetime_pandas_min():
     # GH 2623