diff --git a/xarray/core/merge.py b/xarray/core/merge.py index ca4e29b600d..dff00804f8f 100644 --- a/xarray/core/merge.py +++ b/xarray/core/merge.py @@ -501,7 +501,7 @@ def merge_attrs(variable_attrs, combine_attrs): if combine_attrs == "drop": return {} elif combine_attrs == "override": - return variable_attrs[0] + return dict(variable_attrs[0]) elif combine_attrs == "no_conflicts": result = dict(variable_attrs[0]) for attrs in variable_attrs[1:]: diff --git a/xarray/tests/test_merge.py b/xarray/tests/test_merge.py index 0994ed9a485..10e619f84aa 100644 --- a/xarray/tests/test_merge.py +++ b/xarray/tests/test_merge.py @@ -1,328 +1,44 @@ -import numpy as np import pytest - import xarray as xr -from xarray.core import dtypes, merge -from xarray.core.merge import MergeError -from xarray.testing import assert_identical - -from . import raises_regex -from .test_dataset import create_test_data - - -class TestMergeInternals: - def test_broadcast_dimension_size(self): - actual = merge.broadcast_dimension_size( - [xr.Variable("x", [1]), xr.Variable("y", [2, 1])] - ) - assert actual == {"x": 1, "y": 2} - - actual = merge.broadcast_dimension_size( - [xr.Variable(("x", "y"), [[1, 2]]), xr.Variable("y", [2, 1])] - ) - assert actual == {"x": 1, "y": 2} - - with pytest.raises(ValueError): - merge.broadcast_dimension_size( - [xr.Variable(("x", "y"), [[1, 2]]), xr.Variable("y", [2])] - ) - - -class TestMergeFunction: - def test_merge_arrays(self): - data = create_test_data() - actual = xr.merge([data.var1, data.var2]) - expected = data[["var1", "var2"]] - assert actual.identical(expected) - - def test_merge_datasets(self): - data = create_test_data() - - actual = xr.merge([data[["var1"]], data[["var2"]]]) - expected = data[["var1", "var2"]] - assert actual.identical(expected) - - actual = xr.merge([data, data]) - assert actual.identical(data) - - def test_merge_dataarray_unnamed(self): - data = xr.DataArray([1, 2], dims="x") - with raises_regex(ValueError, "without providing an explicit name"): - xr.merge([data]) - - def test_merge_arrays_attrs_default(self): - var1_attrs = {"a": 1, "b": 2} - var2_attrs = {"a": 1, "c": 3} - expected_attrs = {} - - data = create_test_data() - data.var1.attrs = var1_attrs - data.var2.attrs = var2_attrs - actual = xr.merge([data.var1, data.var2]) - expected = data[["var1", "var2"]] - expected.attrs = expected_attrs - assert actual.identical(expected) - - @pytest.mark.parametrize( - "combine_attrs, var1_attrs, var2_attrs, expected_attrs, expect_exception", - [ - ( - "no_conflicts", - {"a": 1, "b": 2}, - {"a": 1, "c": 3}, - {"a": 1, "b": 2, "c": 3}, - False, - ), - ("no_conflicts", {"a": 1, "b": 2}, {}, {"a": 1, "b": 2}, False), - ("no_conflicts", {}, {"a": 1, "c": 3}, {"a": 1, "c": 3}, False), - ( - "no_conflicts", - {"a": 1, "b": 2}, - {"a": 4, "c": 3}, - {"a": 1, "b": 2, "c": 3}, - True, - ), - ("drop", {"a": 1, "b": 2}, {"a": 1, "c": 3}, {}, False), - ("identical", {"a": 1, "b": 2}, {"a": 1, "b": 2}, {"a": 1, "b": 2}, False), - ("identical", {"a": 1, "b": 2}, {"a": 1, "c": 3}, {"a": 1, "b": 2}, True), - ( - "override", - {"a": 1, "b": 2}, - {"a": 4, "b": 5, "c": 3}, - {"a": 1, "b": 2}, - False, - ), - ], - ) - def test_merge_arrays_attrs( - self, combine_attrs, var1_attrs, var2_attrs, expected_attrs, expect_exception - ): - data = create_test_data() - data.var1.attrs = var1_attrs - data.var2.attrs = var2_attrs - if expect_exception: - with raises_regex(MergeError, "combine_attrs"): - actual = xr.merge([data.var1, data.var2], combine_attrs=combine_attrs) - else: - actual = xr.merge([data.var1, data.var2], combine_attrs=combine_attrs) - expected = data[["var1", "var2"]] - expected.attrs = expected_attrs - assert actual.identical(expected) - - def test_merge_dicts_simple(self): - actual = xr.merge([{"foo": 0}, {"bar": "one"}, {"baz": 3.5}]) - expected = xr.Dataset({"foo": 0, "bar": "one", "baz": 3.5}) - assert actual.identical(expected) - - def test_merge_dicts_dims(self): - actual = xr.merge([{"y": ("x", [13])}, {"x": [12]}]) - expected = xr.Dataset({"x": [12], "y": ("x", [13])}) - assert actual.identical(expected) - - def test_merge_error(self): - ds = xr.Dataset({"x": 0}) - with pytest.raises(xr.MergeError): - xr.merge([ds, ds + 1]) - - def test_merge_alignment_error(self): - ds = xr.Dataset(coords={"x": [1, 2]}) - other = xr.Dataset(coords={"x": [2, 3]}) - with raises_regex(ValueError, "indexes .* not equal"): - xr.merge([ds, other], join="exact") - - def test_merge_wrong_input_error(self): - with raises_regex(TypeError, "objects must be an iterable"): - xr.merge([1]) - ds = xr.Dataset(coords={"x": [1, 2]}) - with raises_regex(TypeError, "objects must be an iterable"): - xr.merge({"a": ds}) - with raises_regex(TypeError, "objects must be an iterable"): - xr.merge([ds, 1]) - - def test_merge_no_conflicts_single_var(self): - ds1 = xr.Dataset({"a": ("x", [1, 2]), "x": [0, 1]}) - ds2 = xr.Dataset({"a": ("x", [2, 3]), "x": [1, 2]}) - expected = xr.Dataset({"a": ("x", [1, 2, 3]), "x": [0, 1, 2]}) - assert expected.identical(xr.merge([ds1, ds2], compat="no_conflicts")) - assert expected.identical(xr.merge([ds2, ds1], compat="no_conflicts")) - assert ds1.identical(xr.merge([ds1, ds2], compat="no_conflicts", join="left")) - assert ds2.identical(xr.merge([ds1, ds2], compat="no_conflicts", join="right")) - expected = xr.Dataset({"a": ("x", [2]), "x": [1]}) - assert expected.identical( - xr.merge([ds1, ds2], compat="no_conflicts", join="inner") - ) - - with pytest.raises(xr.MergeError): - ds3 = xr.Dataset({"a": ("x", [99, 3]), "x": [1, 2]}) - xr.merge([ds1, ds3], compat="no_conflicts") - - with pytest.raises(xr.MergeError): - ds3 = xr.Dataset({"a": ("y", [2, 3]), "y": [1, 2]}) - xr.merge([ds1, ds3], compat="no_conflicts") - - def test_merge_no_conflicts_multi_var(self): - data = create_test_data() - data1 = data.copy(deep=True) - data2 = data.copy(deep=True) - - expected = data[["var1", "var2"]] - actual = xr.merge([data1.var1, data2.var2], compat="no_conflicts") - assert expected.identical(actual) - - data1["var1"][:, :5] = np.nan - data2["var1"][:, 5:] = np.nan - data1["var2"][:4, :] = np.nan - data2["var2"][4:, :] = np.nan - del data2["var3"] - - actual = xr.merge([data1, data2], compat="no_conflicts") - assert data.equals(actual) - - def test_merge_no_conflicts_preserve_attrs(self): - data = xr.Dataset({"x": ([], 0, {"foo": "bar"})}) - actual = xr.merge([data, data]) - assert data.identical(actual) - - def test_merge_no_conflicts_broadcast(self): - datasets = [xr.Dataset({"x": ("y", [0])}), xr.Dataset({"x": np.nan})] - actual = xr.merge(datasets) - expected = xr.Dataset({"x": ("y", [0])}) - assert expected.identical(actual) - - datasets = [xr.Dataset({"x": ("y", [np.nan])}), xr.Dataset({"x": 0})] - actual = xr.merge(datasets) - assert expected.identical(actual) - - -class TestMergeMethod: - def test_merge(self): - data = create_test_data() - ds1 = data[["var1"]] - ds2 = data[["var3"]] - expected = data[["var1", "var3"]] - actual = ds1.merge(ds2) - assert expected.identical(actual) - - actual = ds2.merge(ds1) - assert expected.identical(actual) - - actual = data.merge(data) - assert data.identical(actual) - actual = data.reset_coords(drop=True).merge(data) - assert data.identical(actual) - actual = data.merge(data.reset_coords(drop=True)) - assert data.identical(actual) - - with pytest.raises(ValueError): - ds1.merge(ds2.rename({"var3": "var1"})) - with raises_regex(ValueError, "should be coordinates or not"): - data.reset_coords().merge(data) - with raises_regex(ValueError, "should be coordinates or not"): - data.merge(data.reset_coords()) - - def test_merge_broadcast_equals(self): - ds1 = xr.Dataset({"x": 0}) - ds2 = xr.Dataset({"x": ("y", [0, 0])}) - actual = ds1.merge(ds2) - assert ds2.identical(actual) - - actual = ds2.merge(ds1) - assert ds2.identical(actual) - - actual = ds1.copy() - actual.update(ds2) - assert ds2.identical(actual) - - ds1 = xr.Dataset({"x": np.nan}) - ds2 = xr.Dataset({"x": ("y", [np.nan, np.nan])}) - actual = ds1.merge(ds2) - assert ds2.identical(actual) - - def test_merge_compat(self): - ds1 = xr.Dataset({"x": 0}) - ds2 = xr.Dataset({"x": 1}) - for compat in ["broadcast_equals", "equals", "identical", "no_conflicts"]: - with pytest.raises(xr.MergeError): - ds1.merge(ds2, compat=compat) - - ds2 = xr.Dataset({"x": [0, 0]}) - for compat in ["equals", "identical"]: - with raises_regex(ValueError, "should be coordinates or not"): - ds1.merge(ds2, compat=compat) - - ds2 = xr.Dataset({"x": ((), 0, {"foo": "bar"})}) - with pytest.raises(xr.MergeError): - ds1.merge(ds2, compat="identical") - - with raises_regex(ValueError, "compat=.* invalid"): - ds1.merge(ds2, compat="foobar") - - assert ds1.identical(ds1.merge(ds2, compat="override")) - - def test_merge_auto_align(self): - ds1 = xr.Dataset({"a": ("x", [1, 2]), "x": [0, 1]}) - ds2 = xr.Dataset({"b": ("x", [3, 4]), "x": [1, 2]}) - expected = xr.Dataset( - {"a": ("x", [1, 2, np.nan]), "b": ("x", [np.nan, 3, 4])}, {"x": [0, 1, 2]} - ) - assert expected.identical(ds1.merge(ds2)) - assert expected.identical(ds2.merge(ds1)) - - expected = expected.isel(x=slice(2)) - assert expected.identical(ds1.merge(ds2, join="left")) - assert expected.identical(ds2.merge(ds1, join="right")) - - expected = expected.isel(x=slice(1, 2)) - assert expected.identical(ds1.merge(ds2, join="inner")) - assert expected.identical(ds2.merge(ds1, join="inner")) - - @pytest.mark.parametrize("fill_value", [dtypes.NA, 2, 2.0, {"a": 2, "b": 1}]) - def test_merge_fill_value(self, fill_value): - ds1 = xr.Dataset({"a": ("x", [1, 2]), "x": [0, 1]}) - ds2 = xr.Dataset({"b": ("x", [3, 4]), "x": [1, 2]}) - if fill_value == dtypes.NA: - # if we supply the default, we expect the missing value for a - # float array - fill_value_a = fill_value_b = np.nan - elif isinstance(fill_value, dict): - fill_value_a = fill_value["a"] - fill_value_b = fill_value["b"] - else: - fill_value_a = fill_value_b = fill_value - - expected = xr.Dataset( - {"a": ("x", [1, 2, fill_value_a]), "b": ("x", [fill_value_b, 3, 4])}, - {"x": [0, 1, 2]}, - ) - assert expected.identical(ds1.merge(ds2, fill_value=fill_value)) - assert expected.identical(ds2.merge(ds1, fill_value=fill_value)) - assert expected.identical(xr.merge([ds1, ds2], fill_value=fill_value)) - - def test_merge_no_conflicts(self): - ds1 = xr.Dataset({"a": ("x", [1, 2]), "x": [0, 1]}) - ds2 = xr.Dataset({"a": ("x", [2, 3]), "x": [1, 2]}) - expected = xr.Dataset({"a": ("x", [1, 2, 3]), "x": [0, 1, 2]}) - - assert expected.identical(ds1.merge(ds2, compat="no_conflicts")) - assert expected.identical(ds2.merge(ds1, compat="no_conflicts")) - - assert ds1.identical(ds1.merge(ds2, compat="no_conflicts", join="left")) - - assert ds2.identical(ds1.merge(ds2, compat="no_conflicts", join="right")) - - expected2 = xr.Dataset({"a": ("x", [2]), "x": [1]}) - assert expected2.identical(ds1.merge(ds2, compat="no_conflicts", join="inner")) - - with pytest.raises(xr.MergeError): - ds3 = xr.Dataset({"a": ("x", [99, 3]), "x": [1, 2]}) - ds1.merge(ds3, compat="no_conflicts") - - with pytest.raises(xr.MergeError): - ds3 = xr.Dataset({"a": ("y", [2, 3]), "y": [1, 2]}) - ds1.merge(ds3, compat="no_conflicts") - - def test_merge_dataarray(self): - ds = xr.Dataset({"a": 0}) - da = xr.DataArray(data=1, name="b") - - assert_identical(ds.merge(da), xr.merge([ds, da])) +from xarray.core.merge import merge_attrs + +def test_merge_override_attrs_copy(): + # Test that override mode creates a copy of attrs, not a reference + attrs1 = {'a': 'b', 'b': 'c'} + attrs2 = {'a': 'c', 'd': 'e'} + + # Test with datasets + xds1 = xr.Dataset(attrs=attrs1) + xds2 = xr.Dataset(attrs=attrs2) + + xds3 = xr.merge([xds1, xds2], combine_attrs='override') + + # Modify the merged dataset's attrs + original_a1 = xds1.attrs['a'] + xds3.attrs['a'] = 'd' + + # The original dataset should not be affected + assert xds1.attrs['a'] == original_a1, "Original dataset attrs were modified" + assert xds3.attrs['a'] == 'd', "Merged dataset attrs not properly set" + + # The attrs should be independent objects + assert xds3.attrs is not xds1.attrs, "Attrs are not independent copies" + + # Test the merge_attrs function directly + merged_attrs = merge_attrs([attrs1, attrs2], 'override') + assert merged_attrs == attrs1 + assert merged_attrs is not attrs1, "merge_attrs should return a copy, not a reference" + + # Modify the merged attrs and ensure original is unchanged + merged_attrs['a'] = 'modified' + assert attrs1['a'] == 'b', "Original attrs were modified by changes to merged attrs" + +def test_merge_override_attrs_empty(): + # Test override with empty list + result = merge_attrs([], 'override') + assert result is None + + # Test override with single empty dict + result = merge_attrs([{}], 'override') + assert result == {} + assert isinstance(result, dict) diff --git a/xarray/xarray/tests/test_merge_attrs.py b/xarray/xarray/tests/test_merge_attrs.py new file mode 100644 index 00000000000..4f69b11d915 --- /dev/null +++ b/xarray/xarray/tests/test_merge_attrs.py @@ -0,0 +1,44 @@ +import pytest +import xarray as xr + +def test_merge_override_attrs_copy(): + """Test that merge with combine_attrs='override' creates a copy of attrs""" + # Create two datasets with different attrs + xds1 = xr.Dataset(attrs={'a': 'b', 'b': 'c'}) + xds2 = xr.Dataset(attrs={'a': 'c', 'd': 'e'}) + + # Store original attrs values + original_a1 = xds1.attrs['a'] + original_a2 = xds2.attrs['a'] + + # Merge with override + xds3 = xr.merge([xds1, xds2], combine_attrs='override') + + # Verify initial merge result + assert xds3.attrs['a'] == 'b' # Should take value from first dataset + assert xds3.attrs['b'] == 'c' + assert xds3.attrs['d'] == 'e' + + # Modify the merged dataset's attrs + xds3.attrs['a'] = 'd' + + # Verify that original datasets are unchanged + assert xds1.attrs['a'] == original_a1, 'Original dataset attrs were modified' + assert xds2.attrs['a'] == original_a2, 'Second dataset attrs were modified' + assert xds1.attrs['a'] == 'b', 'First dataset attr value changed' + + # Verify that attrs are independent (not the same object) + assert xds3.attrs is not xds1.attrs, 'Attrs are not independent copies' + + # Test with empty inputs + assert xr.core.merge.merge_attrs([], 'override') == {} + assert xr.core.merge.merge_attrs([{}], 'override') == {} + + # Test with single input + single_attrs = {'x': 'y', 'z': 'w'} + result = xr.core.merge.merge_attrs([single_attrs], 'override') + assert result == single_attrs + assert result is not single_attrs, 'Single input should still create a copy' + result['x'] = 'modified' + assert single_attrs['x'] == 'y', 'Original single attrs were modified' +