Repository: geopandas/geopandas
Pull request #2961 (2023-07-15 05:16:10)

DOC: Update dissolve docstring to document options for the "by" parameter
Description: This pull request updates the docstring for the dissolve function in the geopandas package to accurately document the options for the by parameter.

Changes Made: Modified the docstring to indicate that the by parameter can accept either a string or a list-like object.

Please review and merge this pull request. Thank you for your attention to this matter.


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/doc/source/getting_started/install.rst b/doc/source/getting_started/install.rst
index 60531708..23883b41 100644
--- a/doc/source/getting_started/install.rst
+++ b/doc/source/getting_started/install.rst
@@ -270,4 +270,4 @@ More specifically, whether the speedups are used or not is determined by:
 
 .. _packaging: https://packaging.pypa.io/en/latest/
 
-.. _pointpats: https://pointpats.readthedocs.io/en/latest/
\ No newline at end of file
+.. _pointpats: https://pysal.org/pointpats/
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index ed4a3606..1224a061 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
@@ -1696,9 +1705,12 @@ individually so that features may have different properties
 
         Parameters
         ----------
-        by : string, default None
-            Column whose values define groups to be dissolved. If None,
-            whole GeoDataFrame is considered a single group.
+        by : str or list-like, default None
+            Column(s) whose values define the groups to be dissolved. If None,
+            the entire GeoDataFrame is considered as a single group. If a list-like
+            object is provided, the values in the list are treated as categorical
+            labels, and polygons will be combined based on the equality of
+            these categorical labels.
         aggfunc : function or string, default "first"
             Aggregation function for manipulation of data associated
             with each group. Passed to pandas `groupby.agg` method.
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