Repository: geopandas/geopandas
Pull request #3078 (2023-11-17 16:18:38)

BUG: fix getting row as Series for CoW
With CoW enabled, the index is not necessarily an identical object (in python `is` sence), but it's still identical from pandas' point of view, and pandas has a `Index.is_()` method to check for this. 

This fixes a few of the failures on the dev build (where we test with CoW enabled)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b6b2106b..1f0c63be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,8 @@ Bug fixes:
 - Fix `GeoDataFrame.merge()` incorrectly returning a `DataFrame` instead of a
   `GeoDataFrame` when the `suffixes` argument is applied to the active
   geometry column (#2933).
+- Fix bug in `pandas.concat` CRS consistency checking where CRS differing by WKT 
+  whitespace only were treated as incompatible (#3023)
 
 ## Version 0.14.1 (Nov 11, 2023)
 
diff --git a/geopandas/array.py b/geopandas/array.py
index e22ee9d7..c83ff5e0 100644
--- a/geopandas/array.py
+++ b/geopandas/array.py
@@ -1593,15 +1593,20 @@ def _get_common_crs(arr_seq):
     # mask out all None arrays with no crs (most likely auto generated by pandas
     # from concat with missing column)
     arr_seq = [ga for ga in arr_seq if not (ga.isna().all() and ga.crs is None)]
-
-    crs_set = {arr.crs for arr in arr_seq}
-    crs_not_none = [crs for crs in crs_set if crs is not None]
+    # determine unique crs without using a set, because CRS hash can be different
+    # for objects with the same CRS
+    unique_crs = []
+    for arr in arr_seq:
+        if arr.crs not in unique_crs:
+            unique_crs.append(arr.crs)
+
+    crs_not_none = [crs for crs in unique_crs if crs is not None]
     names = [crs.name for crs in crs_not_none]
 
     if len(crs_not_none) == 0:
         return None
     if len(crs_not_none) == 1:
-        if len(crs_set) != 1:
+        if len(unique_crs) != 1:
             warnings.warn(
                 "CRS not set for some of the concatenation inputs. "
                 f"Setting output's CRS as {names[0]} "
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index e7a6e4f0..8fcbc6c2 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -1612,7 +1612,7 @@ individually so that features may have different properties
               checking the identity of the index)
             """
             srs = pd.Series(*args, **kwargs)
-            is_row_proxy = srs.index is self.columns
+            is_row_proxy = srs.index.is_(self.columns)
             if is_geometry_type(srs) and not is_row_proxy:
                 srs = GeoSeries(srs)
             return srs
diff --git a/geopandas/geoseries.py b/geopandas/geoseries.py
index e0cdf905..44db62ed 100644
--- a/geopandas/geoseries.py
+++ b/geopandas/geoseries.py
@@ -152,8 +152,6 @@ class GeoSeries(GeoPandasBase, Series):
 
     """
 
-    _metadata = ["name"]
-
     def __init__(self, data=None, index=None, crs: Optional[Any] = None, **kwargs):
         if hasattr(data, "crs") and crs:
             if not data.crs:
@@ -619,6 +617,14 @@ class GeoSeries(GeoPandasBase, Series):
     def _constructor(self):
         return _geoseries_constructor_with_fallback
 
+    def _constructor_from_mgr(self, mgr, axes):
+        assert isinstance(mgr, SingleBlockManager)
+
+        if not isinstance(mgr.blocks[0].dtype, GeometryDtype):
+            return Series._from_mgr(mgr, axes)
+
+        return GeoSeries._from_mgr(mgr, axes)
+
     @property
     def _constructor_expanddim(self):
         return _geoseries_expanddim


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 40a43feb..8473773d 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -88,6 +88,13 @@ jobs:
         run: |
           pytest -v -r a -n auto --color=yes --cov=geopandas --cov-append --cov-report term-missing --cov-report xml geopandas/
 
+      - name: Test with Copy-on-Write
+        if: ${{ always() && matrix.dev }}
+        env:
+          PANDAS_COPY_ON_WRITE: '1'
+        run: |
+          pytest -v -r a -n auto --color=yes --cov=geopandas --cov-append --cov-report term-missing --cov-report xml geopandas/
+
       - name: Test with PostGIS
         if: (contains(matrix.env, '310-pd20-conda-forge.yaml') || contains(matrix.env, '311-latest-conda-forge.yaml'))
           && contains(matrix.os, 'ubuntu')
diff --git a/geopandas/tests/test_merge.py b/geopandas/tests/test_merge.py
index 2ae1c9f8..7832f1f4 100644
--- a/geopandas/tests/test_merge.py
+++ b/geopandas/tests/test_merge.py
@@ -1,6 +1,7 @@
 import warnings
 
 import pandas as pd
+import pyproj
 import pytest
 from geopandas.testing import assert_geodataframe_equal
 from pandas.testing import assert_index_equal
@@ -133,6 +134,38 @@ class TestMerging:
             partial_none_case.iloc[0] = None
             pd.concat([single_geom_col, partial_none_case])
 
+    def test_concat_axis0_crs_wkt_mismatch(self):
+        # https://github.com/geopandas/geopandas/issues/326#issuecomment-1727958475
+        wkt_template = """GEOGCRS["WGS 84",
+        ENSEMBLE["World Geodetic System 1984 ensemble",
+        MEMBER["World Geodetic System 1984 (Transit)"],
+        MEMBER["World Geodetic System 1984 (G730)"],
+        MEMBER["World Geodetic System 1984 (G873)"],
+        MEMBER["World Geodetic System 1984 (G1150)"],
+        MEMBER["World Geodetic System 1984 (G1674)"],
+        MEMBER["World Geodetic System 1984 (G1762)"],
+        MEMBER["World Geodetic System 1984 (G2139)"],
+        ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]],
+        ENSEMBLEACCURACY[2.0]],PRIMEM["Greenwich",0,
+        ANGLEUNIT["degree",0.0174532925199433]],CS[ellipsoidal,2],
+        AXIS["geodetic latitude (Lat)",north,ORDER[1],
+        ANGLEUNIT["degree",0.0174532925199433]],
+        AXIS["geodetic longitude (Lon)",east,ORDER[2],
+        ANGLEUNIT["degree",0.0174532925199433]],
+        USAGE[SCOPE["Horizontal component of 3D system."],
+        AREA["World.{}"],BBOX[-90,-180,90,180]],ID["EPSG",4326]]"""
+        wkt_v1 = wkt_template.format("")
+        wkt_v2 = wkt_template.format(" ")  # add additional whitespace
+        crs1 = pyproj.CRS.from_wkt(wkt_v1)
+        crs2 = pyproj.CRS.from_wkt(wkt_v2)
+        # pyproj crs __hash__ based on WKT strings means these are distinct in a
+        # set are but equal by equality
+        assert len({crs1, crs2}) == 2
+        assert crs1 == crs2
+        expected = pd.concat([self.gdf, self.gdf]).set_crs(crs1)
+        res = pd.concat([self.gdf.set_crs(crs1), self.gdf.set_crs(crs2)])
+        assert_geodataframe_equal(expected, res)
+
     def test_concat_axis1(self):
         res = pd.concat([self.gdf, self.df], axis=1)