diff --git a/test/__init__.py b/test/__init__.py index 38f8254907a..29d4ac8db9b 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -83,7 +83,8 @@ def assertArrayEqual(self, a1, a2): def assertDatasetEqual(self, d1, d2): # this method is functionally equivalent to `assert d1 == d2`, but it # checks each aspect of equality separately for easier debugging - self.assertEqual(sorted(d1.variables), sorted(d2.variables)) + self.assertEqual(sorted(d1.variables, key=str), + sorted(d2.variables, key=str)) for k in d1: v1 = d1.variables[k] v2 = d2.variables[k] @@ -93,14 +94,16 @@ def assertDatasetIdentical(self, d1, d2): # this method is functionally equivalent to `assert d1.identical(d2)`, # but it checks each aspect of equality separately for easier debugging assert utils.dict_equiv(d1.attrs, d2.attrs), (d1.attrs, d2.attrs) - self.assertEqual(sorted(d1.variables), sorted(d2.variables)) + self.assertEqual(sorted(d1.variables, key=str), + sorted(d2.variables, key=str)) for k in d1: v1 = d1.variables[k] v2 = d2.variables[k] assert v1.identical(v2), (v1, v2) def assertDatasetAllClose(self, d1, d2, rtol=1e-05, atol=1e-08): - self.assertEqual(sorted(d1.variables), sorted(d2.variables)) + self.assertEqual(sorted(d1.variables, key=str), + sorted(d2.variables, key=str)) for k in d1: v1 = d1.variables[k] v2 = d2.variables[k] diff --git a/test/test_backends.py b/test/test_backends.py index c235ff4b929..6c8a3a9ca47 100644 --- a/test/test_backends.py +++ b/test/test_backends.py @@ -76,6 +76,11 @@ def test_roundtrip_test_data(self): actual = self.roundtrip(expected) self.assertDatasetAllClose(expected, actual) + def test_roundtrip_None_variable(self): + expected = Dataset({None: (('x', 'y'), [[0, 1], [2, 3]])}) + actual = self.roundtrip(expected) + self.assertDatasetAllClose(expected, actual) + def test_roundtrip_string_data(self): expected = Dataset({'x': ('t', ['abc', 'def'])}) actual = self.roundtrip(expected) diff --git a/test/test_data_array.py b/test/test_data_array.py index 8f1065a5f8d..6bfaaa45ec9 100644 --- a/test/test_data_array.py +++ b/test/test_data_array.py @@ -229,6 +229,21 @@ def test_dataset_math(self): 'x': sim['x']})['tmin'] self.assertDataArrayEqual(actual, expected) + def test_math_name(self): + # Verify that name is preserved only when it can be done unambiguously. + # The rule (copied from pandas.Series) is keep the current name only if + # the other object has no name attribute and this object isn't a + # coordinate; otherwise reset to None. + ds = self.ds + a = self.dv + self.assertEqual((+a).name, 'foo') + self.assertEqual((a + 0).name, 'foo') + self.assertIs((a + a.rename(None)).name, None) + self.assertIs((a + a).name, None) + self.assertIs((+ds['x']).name, None) + self.assertIs((ds['x'] + 0).name, None) + self.assertIs((a + ds['x']).name, None) + def test_coord_math(self): ds = Dataset({'x': ('x', 1 + np.arange(3))}) expected = ds.copy() @@ -381,3 +396,8 @@ def test_to_and_from_series(self): self.assertEqual('foo', actual.name) # test roundtrip self.assertDataArrayIdentical(self.dv, DataArray.from_series(actual)) + # test name is None + actual.name = None + expected_da = self.dv.rename(None) + self.assertDataArrayIdentical(expected_da, + DataArray.from_series(actual)) diff --git a/test/test_dataset.py b/test/test_dataset.py index 8aa602bd76f..9c7476b9cad 100644 --- a/test/test_dataset.py +++ b/test/test_dataset.py @@ -171,6 +171,10 @@ def test_equals_and_identical(self): self.assertFalse(data.equals(data2)) self.assertTrue(data != data2) + data = create_test_data(seed=42).rename({'var1': None}) + self.assertTrue(data.equals(data)) + self.assertTrue(data.identical(data)) + def test_attrs(self): data = create_test_data(seed=42) data.attrs = {'foobar': 'baz'} diff --git a/xray/backends/common.py b/xray/backends/common.py index 9284daf83e4..d53832b7bf0 100644 --- a/xray/backends/common.py +++ b/xray/backends/common.py @@ -2,6 +2,21 @@ from xray.pycompat import iteritems +NONE_VAR_NAME = '__values__' + + +def _encode_variable_name(name): + if name is None: + name = NONE_VAR_NAME + return name + + +def _decode_variable_name(name): + if name == NONE_VAR_NAME: + name = None + return name + + class AbstractDataStore(object): def open_store_variable(self, v): raise NotImplementedError @@ -12,7 +27,8 @@ def store_variables(self): @property def variables(self): - return FrozenOrderedDict((k, self.open_store_variable(v)) + return FrozenOrderedDict((_decode_variable_name(k), + self.open_store_variable(v)) for k, v in iteritems(self.store_variables)) def sync(self): @@ -39,7 +55,7 @@ def set_attributes(self, attributes): def set_variables(self, variables): for vn, v in iteritems(variables): - self.set_variable(vn, v) + self.set_variable(_encode_variable_name(vn), v) def set_necessary_dimensions(self, variable): for d, l in zip(variable.dimensions, variable.shape): diff --git a/xray/common.py b/xray/common.py index 6e262fffd06..669930ce765 100644 --- a/xray/common.py +++ b/xray/common.py @@ -131,7 +131,10 @@ def _wrap_indent(text, start='', length=None): def array_repr(arr): - name_str = ('%r ' % arr.name) if hasattr(arr, 'name') else '' + if hasattr(arr, 'name') and arr.name is not None: + name_str = '%r ' % arr.name + else: + name_str = '' dim_summary = ', '.join('%s: %s' % (k, v) for k, v in zip(arr.dimensions, arr.shape)) summary = [''% (type(arr).__name__, name_str, dim_summary)] diff --git a/xray/data_array.py b/xray/data_array.py index dff43651689..74df9800217 100644 --- a/xray/data_array.py +++ b/xray/data_array.py @@ -345,7 +345,8 @@ def rename(self, new_name_or_name_dict): -------- Dataset.rename """ - if isinstance(new_name_or_name_dict, basestring): + if (isinstance(new_name_or_name_dict, basestring) + or new_name_or_name_dict is None): new_name = new_name_or_name_dict name_dict = {self.name: new_name} else: @@ -597,10 +598,9 @@ def from_series(cls, series): with NaN). Thus this operation should be the inverse of the `to_series` method. """ - name = series.name if series.name is not None else 'values' - df = pd.DataFrame({name: series}) + df = pd.DataFrame({series.name: series}) ds = xray.Dataset.from_dataframe(df) - return ds[name] + return ds[series.name] def equals(self, other): """True if two DataArrays have the same dimensions, coordinates and @@ -638,29 +638,22 @@ def identical(self, other): def _select_coordinates(self): return xray.Dataset(self.coordinates) - def _refocus(self, new_var, name=None): - """Returns a copy of this DataArray's dataset with this - DataArray's focus variable replaced by `new_var`. - - If `new_var` is a DataArray, its contents will be merged in. - """ - if not hasattr(new_var, 'dimensions'): - new_var = variable.Variable(self.variable.dimensions, new_var) + def __array_wrap__(self, obj, context=None): + new_var = self.variable.__array_wrap__(obj, context) ds = self._select_coordinates() - if name is None: - name = self.name + '_' + if (self.name,) == self.dimensions: + # use a new name for coordinate variables + name = None + else: + name = self.name ds[name] = new_var return ds[name] - def __array_wrap__(self, obj, context=None): - return self._refocus(self.variable.__array_wrap__(obj, context)) - @staticmethod def _unary_op(f): @functools.wraps(f) def func(self, *args, **kwargs): - return self._refocus(f(self.variable, *args, **kwargs), - self.name + '_' + f.__name__) + return self.__array_wrap__(f(self.variable, *args, **kwargs)) return func def _check_coordinates_compat(self, other): @@ -682,11 +675,13 @@ def func(self, other): if hasattr(other, 'coordinates'): ds.merge(other.coordinates, inplace=True) other_array = getattr(other, 'variable', other) - other_name = getattr(other, 'name', 'other') - name = self.name + '_' + f.__name__ + '_' + other_name + if hasattr(other, 'name') or (self.name,) == self.dimensions: + name = None + else: + name = self.name ds[name] = (f(self.variable, other_array) - if not reflexive - else f(other_array, self.variable)) + if not reflexive + else f(other_array, self.variable)) return ds[name] return func diff --git a/xray/dataset.py b/xray/dataset.py index 43ce6168d76..09cba63b3b5 100644 --- a/xray/dataset.py +++ b/xray/dataset.py @@ -231,6 +231,14 @@ def _assert_compat_valid(compat): "'identical'" % compat) +def _item0_str(items): + """Key function for use in sorted on a list of variables. + + This is useful because None is not comparable to strings in Python 3. + """ + return str(items[0]) + + def as_dataset(obj): """Cast the given object to a Dataset. @@ -469,8 +477,10 @@ def equals(self, other): return (len(self) == len(other) and all(k1 == k2 and v1.equals(v2) for (k1, v1), (k2, v2) - in zip(sorted(self.variables.items()), - sorted(other.variables.items())))) + in zip(sorted(self.variables.items(), + key=_item0_str), + sorted(other.variables.items(), + key=_item0_str)))) except (TypeError, AttributeError): return False @@ -489,8 +499,10 @@ def identical(self, other): and len(self) == len(other) and all(k1 == k2 and v1.identical(v2) for (k1, v1), (k2, v2) - in zip(sorted(self.variables.items()), - sorted(other.variables.items())))) + in zip(sorted(self.variables.items(), + key=_item0_str), + sorted(other.variables.items(), + key=_item0_str)))) except (TypeError, AttributeError): return False