Repository: geopandas/geopandas

Issue:
BUG: .attrs not preserved
Pandas `DataFrame.attrs` dictionary is lost after slicing (and likely some other operations).

Copying @jorisvandenbossche comments from gitter

> Those attrs are passed through using the `__finalize__` mechanism (so the same as for `_metadata` actually)
but we override `__finalize__`
>
> The pandas implementation is here: https://github.com/pandas-dev/pandas/blob/9cb372376fc78aa66e2559de919592007b74cfaa/pandas/core/generic.py#L5324-L5359
> and pandas added several types of functionality to that recently (to preserve `attrs` but also `flags`)
>
> In the GeoPandas implementation, we have some custom logic for merge and concat (to use the metadata of the first object)
but would need to think that through if this is still needed with the "crs on the array level" change



This works (pandas.DataFrame)

```python
>>> df = pd.DataFrame({'a': range(10)})                                                                                                                                                                       
>>> df.attrs['name'] = "my_name"
>>> df.iloc[:5].attrs
{'name': 'my_name'}
```

This does not (geopandas.GeoDataFrame)

```python
>>>df = gpd.GeoDataFrame({'a': range(10)})                                                                                                                                                                       
>>> df.attrs['name'] = "my_name"
>>> df.iloc[:5].attrs
{}
```

See https://gitter.im/geopandas/geopandas?at=5f85673f02e81701b01b6126 for a relevant discussion.


Discussion:
So for reference, the current geopandas implementation of `GeoDataFrame.__finalize__` is here:

https://github.com/geopandas/geopandas/blob/7044aa479dbae72c14dfb647a5a782a01cff81d1/geopandas/geodataframe.py#L1148-L1162

Resolving patch:
diff --git a/geopandas/_compat.py b/geopandas/_compat.py
--- a/geopandas/_compat.py
+++ b/geopandas/_compat.py
@@ -11,8 +11,9 @@
 # -----------------------------------------------------------------------------
 
 PANDAS_GE_025 = str(pd.__version__) >= LooseVersion("0.25.0")
-PANDAS_GE_10 = str(pd.__version__) >= LooseVersion("0.26.0.dev")
+PANDAS_GE_10 = str(pd.__version__) >= LooseVersion("1.0.0")
 PANDAS_GE_11 = str(pd.__version__) >= LooseVersion("1.1.0")
+PANDAS_GE_12 = str(pd.__version__) >= LooseVersion("1.2.0")
 
 
 # -----------------------------------------------------------------------------
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -894,6 +894,8 @@ def _constructor(self):
 
     def __finalize__(self, other, method=None, **kwargs):
         """propagate metadata from other to self """
+        self = super().__finalize__(other, method=method, **kwargs)
+
         # merge operation: using metadata of the left object
         if method == "merge":
             for name in self._metadata:
@@ -902,9 +904,6 @@ def __finalize__(self, other, method=None, **kwargs):
         elif method == "concat":
             for name in self._metadata:
                 object.__setattr__(self, name, getattr(other.objs[0], name, None))
-        else:
-            for name in self._metadata:
-                object.__setattr__(self, name, getattr(other, name, None))
 
         return self
 


Test patch:
diff --git a/geopandas/tests/test_pandas_methods.py b/geopandas/tests/test_pandas_methods.py
--- a/geopandas/tests/test_pandas_methods.py
+++ b/geopandas/tests/test_pandas_methods.py
@@ -9,7 +9,7 @@
 
 import geopandas
 from geopandas import GeoDataFrame, GeoSeries
-from geopandas._compat import PANDAS_GE_025, PANDAS_GE_11
+import geopandas._compat as compat
 from geopandas.array import from_shapely
 
 from geopandas.testing import assert_geodataframe_equal, assert_geoseries_equal
@@ -71,7 +71,7 @@ def test_repr_all_missing():
 def test_repr_empty():
     # https://github.com/geopandas/geopandas/issues/1195
     s = GeoSeries([])
-    if PANDAS_GE_025:
+    if compat.PANDAS_GE_025:
         # repr with correct name fixed in pandas 0.25
         assert repr(s) == "GeoSeries([], dtype: geometry)"
     else:
@@ -455,7 +455,7 @@ def test_groupby(df):
 
     # applying on the geometry column
     res = df.groupby("value2")["geometry"].apply(lambda x: x.cascaded_union)
-    if PANDAS_GE_11:
+    if compat.PANDAS_GE_11:
         exp = GeoSeries(
             [shapely.geometry.MultiPoint([(0, 0), (2, 2)]), Point(1, 1)],
             index=pd.Index([1, 2], name="value2"),
@@ -518,3 +518,44 @@ def test_apply_convert_dtypes_keyword(s):
     # ensure the convert_dtypes keyword is accepted
     res = s.apply(lambda x: x, convert_dtype=True, args=())
     assert_geoseries_equal(res, s)
+
+
+@pytest.mark.skipif(not compat.PANDAS_GE_10, reason="attrs introduced in pandas 1.0")
+def test_preserve_attrs(df):
+    # https://github.com/geopandas/geopandas/issues/1654
+    df.attrs["name"] = "my_name"
+    attrs = {"name": "my_name"}
+    assert df.attrs == attrs
+
+    # preserve attrs in indexing operations
+    for subset in [df[:2], df[df["value1"] > 2], df[["value2", "geometry"]]]:
+        assert df.attrs == attrs
+
+    # preserve attrs in methods
+    df2 = df.reset_index()
+    assert df2.attrs == attrs
+
+
+@pytest.mark.skipif(not compat.PANDAS_GE_12, reason="attrs introduced in pandas 1.0")
+def test_preserve_flags(df):
+    # https://github.com/geopandas/geopandas/issues/1654
+    df = df.set_flags(allows_duplicate_labels=False)
+    assert df.flags.allows_duplicate_labels is False
+
+    # preserve flags in indexing operations
+    for subset in [df[:2], df[df["value1"] > 2], df[["value2", "geometry"]]]:
+        assert df.flags.allows_duplicate_labels is False
+
+    # preserve attrs in methods
+    df2 = df.reset_index()
+    assert df2.flags.allows_duplicate_labels is False
+
+    # it is honored for operations that introduce duplicate labels
+    with pytest.raises(ValueError):
+        df.reindex([0, 0, 1])
+
+    with pytest.raises(ValueError):
+        df[["value1", "value1", "geometry"]]
+
+    with pytest.raises(ValueError):
+        pd.concat([df, df])