Repository: geopandas/geopandas

Issue:
ENH: Improve error message for attempting to write multiple geometry columns using `to_file`
xref #2565

```python
In [16]: gdf = gpd.GeoDataFrame({"a":[1,2]}, geometry=[Point(1,1), Point(1,2)])

In [17]: gdf['geom2'] = gdf.geometry

In [18]: gdf.to_file("tmp.gpkg")
-----------------------------------
File ...\venv\lib\site-packages\geopandas\io\file.py:596, in infer_schema.<locals>.convert_type(column, in_type)
    594     out_type = types[str(in_type)]
    595 else:
--> 596     out_type = type(np.zeros(1, in_type).item()).__name__
    597 if out_type == "long":
    598     out_type = "int"

TypeError: Cannot interpret '<geopandas.array.GeometryDtype object at 0x0000025FA88908E0>' as a data type

```



Discussion:
martinfleis: I have revived this.

Not sure if it needs a changelog note?

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8bf10b95..14041dff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,9 +16,12 @@ New features and improvements:
 - Added ``exclusive`` parameter to ``sjoin_nearest`` method for Shapely >= 2.0 (#2877)
 
 Bug fixes:
+- Fix ambiguous error when GeoDataFrame is initialised with a column called "crs" (#2944)
 
 - Fix a color assignment in ``explore`` when using ``UserDefined`` bins (#2923)
 - ``assert_geodataframe_equal`` now handles GeoDataFrames with no active geometry (#2498)
+- Fix bug in `apply` with `axis=1` where the given user defined function returns nested 
+  data in the geometry column (#2959)
 
 ## Version 0.13.2 (Jun 6, 2023)
 
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index ed4a3606..41991a7f 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -3,6 +3,7 @@ import warnings
 
 import numpy as np
 import pandas as pd
+import shapely.errors
 from pandas import DataFrame, Series
 from pandas.core.accessor import CachedAccessor
 
@@ -19,6 +20,11 @@ from geopandas.explore import _explore
 from . import _compat as compat
 from ._decorator import doc
 
+if compat.SHAPELY_GE_18:
+    geometry_type_error = shapely.errors.GeometryTypeError
+else:
+    geometry_type_error = ValueError
+
 
 def _geodataframe_constructor_with_fallback(*args, **kwargs):
     """
@@ -1529,7 +1535,10 @@ individually so that features may have different properties
             if pd.api.types.is_scalar(value) or isinstance(value, BaseGeometry):
                 value = [value] * self.shape[0]
             try:
-                crs = getattr(self, "crs", None)
+                if self._geometry_column_name is not None:
+                    crs = getattr(self, "crs", None)
+                else:  # don't use getattr, because a col "crs" might exist
+                    crs = None
                 value = _ensure_geometry(value, crs=crs)
                 if key == "geometry":
                     self._persist_old_default_geometry_colname()
@@ -1613,7 +1622,7 @@ individually so that features may have different properties
                     # not enough info about func to preserve CRS
                     result = _ensure_geometry(result)
 
-                except TypeError:
+                except (TypeError, geometry_type_error):
                     pass
 
         return result
diff --git a/geopandas/io/file.py b/geopandas/io/file.py
index bcf311c1..8832e402 100644
--- a/geopandas/io/file.py
+++ b/geopandas/io/file.py
@@ -565,6 +565,15 @@ def _to_file(
             stacklevel=3,
         )
 
+    if (df.dtypes == "geometry").sum() > 1:
+        raise ValueError(
+            "GeoDataFrame contains multiple geometry columns but GeoDataFrame.to_file "
+            "supports only a single geometry column. Use a GeoDataFrame.to_parquet or "
+            "GeoDataFrame.to_feather, drop additional geometry columns or convert them "
+            "to a supported format like a well-known text (WKT) using "
+            "`GeoSeries.to_wkt()`.",
+        )
+
     if mode not in ("w", "a"):
         raise ValueError(f"'mode' should be one of 'w' or 'a', got '{mode}' instead")
 


Test patch:
diff --git a/geopandas/io/tests/test_file.py b/geopandas/io/tests/test_file.py
index 67930b9d..9d80cea9 100644
--- a/geopandas/io/tests/test_file.py
+++ b/geopandas/io/tests/test_file.py
@@ -1134,3 +1134,9 @@ def test_write_read_file(test_file, engine):
     df_json = geopandas.read_file(test_file, engine=engine)
     assert_geodataframe_equal(gdf, df_json, check_crs=True)
     os.remove(os.path.expanduser(test_file))
+
+
+def test_multiple_geom_cols_error(tmpdir, df_nybb):
+    df_nybb["geom2"] = df_nybb.geometry
+    with pytest.raises(ValueError, match="GeoDataFrame contains multiple geometry"):
+        df_nybb.to_file(os.path.join(str(tmpdir), "boros.gpkg"))
diff --git a/geopandas/tests/test_geodataframe.py b/geopandas/tests/test_geodataframe.py
index 009cdbc8..8dcc9381 100644
--- a/geopandas/tests/test_geodataframe.py
+++ b/geopandas/tests/test_geodataframe.py
@@ -1469,3 +1469,11 @@ def test_geodataframe_crs_nonrepresentable_json(crs):
     ):
         gdf_geojson = json.loads(gdf.to_json())
     assert "crs" not in gdf_geojson
+
+
+def test_geodataframe_crs_colname():
+    # https://github.com/geopandas/geopandas/issues/2942
+    gdf = GeoDataFrame({"crs": [1], "geometry": [Point(1, 1)]})
+    assert gdf.crs is None
+    assert gdf["crs"].iloc[0] == 1
+    assert getattr(gdf, "crs") is None
diff --git a/geopandas/tests/test_pandas_methods.py b/geopandas/tests/test_pandas_methods.py
index c10a4183..2f569b4d 100644
--- a/geopandas/tests/test_pandas_methods.py
+++ b/geopandas/tests/test_pandas_methods.py
@@ -752,6 +752,11 @@ def test_df_apply_returning_series(df):
     result = df.apply(lambda x: None, axis=1)
     assert result.dtype == "object"
 
+    # https://github.com/geopandas/geopandas/issues/2889
+    # contrived case such that `from_shapely` receives an array of geodataframes
+    res = df.apply(lambda row: df.geometry.to_frame(), axis=1)
+    assert res.dtype == "object"
+
 
 def test_df_apply_geometry_dtypes(df):
     # https://github.com/geopandas/geopandas/issues/1852