Repository: geopandas/geopandas
Pull request #3080 (2023-11-20 10:19:12)

COMPAT: add pandas blockmanager alternative apis for _constructor like things
xref #3060

With [6b4857e](https://github.com/geopandas/geopandas/pull/3080/commits/6b4857ec8d1bcccb52984d2af1b9685349d37ed7) we are down to a mere 34992 warnings (commits after are fixes for old pandas).

With [5d7d146](https://github.com/geopandas/geopandas/pull/3080/commits/5d7d14672255a6fdededc1b92416f8011adfd450) we are down to 346

[482c989](https://github.com/geopandas/geopandas/pull/3080/commits/482c98926c37d6d2dab7c4c4fd950815a81a50d0) is a bit more convoluted than the others. Originally I didn't think we needed this, but there are warnings about this in e.g. test_clip.py (and with it we are down to 311). Since we have quite some logic for `GeoSeries._constructor_expanddim`, I opted to directly construct the dataframe (and fortunately we can actually do this in the DataFrame case as the `Series._name` being undefined as part of `from_mgr` is not an issue)

Would not surprise me if this is not the 'right' way of doing some of this, I've kept some tabs on some of the relevant pandas PRs but not across it all.


Patch:
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index 8f161b46..7025ad0d 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -1575,6 +1575,12 @@ individually so that features may have different properties
     def _constructor(self):
         return _geodataframe_constructor_with_fallback
 
+    def _constructor_from_mgr(self, mgr, axes):
+        # analogous logic to _geodataframe_constructor_with_fallback
+        if not any(isinstance(block.dtype, GeometryDtype) for block in mgr.blocks):
+            return pd.DataFrame._from_mgr(mgr, axes)
+        return GeoDataFrame._from_mgr(mgr, axes)
+
     @property
     def _constructor_sliced(self):
         def _geodataframe_constructor_sliced(*args, **kwargs):
@@ -1601,6 +1607,13 @@ individually so that features may have different properties
 
         return _geodataframe_constructor_sliced
 
+    def _constructor_sliced_from_mgr(self, mgr, axes):
+        is_row_proxy = mgr.index.is_(self.columns)
+
+        if isinstance(mgr.blocks[0].dtype, GeometryDtype) and not is_row_proxy:
+            return GeoSeries._from_mgr(mgr, axes)
+        return Series._from_mgr(mgr, axes)
+
     def __finalize__(self, other, method=None, **kwargs):
         """propagate metadata from other to self"""
         self = super().__finalize__(other, method=method, **kwargs)
diff --git a/geopandas/geoseries.py b/geopandas/geoseries.py
index 3f105cba..53c684d2 100644
--- a/geopandas/geoseries.py
+++ b/geopandas/geoseries.py
@@ -51,20 +51,16 @@ def _geoseries_constructor_with_fallback(
         return Series(data=data, index=index, **kwargs)
 
 
-def _geoseries_expanddim(data=None, *args, **kwargs):
+def _expanddim_logic(df):
+    """Shared logic for _constructor_expanddim and _constructor_from_mgr_expanddim."""
     from geopandas import GeoDataFrame
 
-    # pd.Series._constructor_expanddim == pd.DataFrame
-    df = pd.DataFrame(data, *args, **kwargs)
-    geo_col_name = None
-    if isinstance(data, GeoSeries):
-        # pandas default column name is 0, keep convention
-        geo_col_name = data.name if data.name is not None else 0
-
-    if df.shape[1] == 1:
-        geo_col_name = df.columns[0]
-
     if (df.dtypes == "geometry").sum() > 0:
+        if df.shape[1] == 1:
+            geo_col_name = df.columns[0]
+        else:
+            geo_col_name = None
+
         if geo_col_name is None or not is_geometry_type(df[geo_col_name]):
             df = GeoDataFrame(df)
             df._geometry_column_name = None
@@ -74,6 +70,13 @@ def _geoseries_expanddim(data=None, *args, **kwargs):
     return df
 
 
+def _geoseries_expanddim(data=None, *args, **kwargs):
+    # pd.Series._constructor_expanddim == pd.DataFrame, we start
+    # with that then specialize.
+    df = pd.DataFrame(data, *args, **kwargs)
+    return _expanddim_logic(df)
+
+
 class GeoSeries(GeoPandasBase, Series):
     """
     A Series object designed to store shapely geometry objects.
@@ -629,6 +632,10 @@ class GeoSeries(GeoPandasBase, Series):
     def _constructor_expanddim(self):
         return _geoseries_expanddim
 
+    def _constructor_expanddim_from_mgr(self, mgr, axes):
+        df = pd.DataFrame._from_mgr(mgr, axes)
+        return _expanddim_logic(df)
+
     def _wrapped_pandas_method(self, mtd, *args, **kwargs):
         """Wrap a generic pandas method to ensure it returns a GeoSeries"""
         val = getattr(super(), mtd)(*args, **kwargs)


Test patch:
diff --git a/geopandas/tests/test_merge.py b/geopandas/tests/test_merge.py
index 7832f1f4..0f0b942e 100644
--- a/geopandas/tests/test_merge.py
+++ b/geopandas/tests/test_merge.py
@@ -3,6 +3,8 @@ import warnings
 import pandas as pd
 import pyproj
 import pytest
+
+from geopandas._compat import PANDAS_GE_21
 from geopandas.testing import assert_geodataframe_equal
 from pandas.testing import assert_index_equal
 
@@ -178,10 +180,18 @@ class TestMerging:
         # https://github.com/geopandas/geopandas/issues/1230
         # Expect that concat should fail gracefully if duplicate column names belonging
         # to geometry columns are introduced.
-        expected_err = (
-            "GeoDataFrame does not support multiple columns using the geometry"
-            " column name 'geometry'"
-        )
+        if PANDAS_GE_21:
+            # _constructor_from_mgr changes mean we now get the concat specific error
+            # message in this case too
+            expected_err = (
+                "Concat operation has resulted in multiple columns using the geometry "
+                "column name 'geometry'."
+            )
+        else:
+            expected_err = (
+                "GeoDataFrame does not support multiple columns using the geometry"
+                " column name 'geometry'"
+            )
         with pytest.raises(ValueError, match=expected_err):
             pd.concat([self.gdf, self.gdf], axis=1)