Repository: geopandas/geopandas
Pull request #3267 (2024-04-26 07:08:59)

Fix error message when specifying IO engine that is not available
While testing https://github.com/geopandas/geopandas/issues/3260#issuecomment-2078302687 I noticed I got a confusing error because I actually had no fiona installed, not because fiona was failing to write to bytes:

```
In [3]: data.to_file(outpath, layer="geometry", driver="GPKG", engine="fiona")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[3], line 3
      1 if outpath.exists():
      2     outpath.unlink()
----> 3 data.to_file(outpath, layer="geometry", driver="GPKG", engine="fiona")

File ~/scipy/repos/geopandas/geopandas/geodataframe.py:1349, in GeoDataFrame.to_file(self, filename, driver, schema, index, **kwargs)
   1257 """Write the ``GeoDataFrame`` to a file.
   1258 
   1259 By default, an ESRI shapefile is written, but any OGR data source
   (...)
   1345 
   1346 """
   1347 from geopandas.io.file import _to_file
-> 1349 _to_file(self, filename, driver, schema, index, **kwargs)

File ~/scipy/repos/geopandas/geopandas/io/file.py:668, in _to_file(df, filename, driver, schema, index, mode, crs, engine, **kwargs)
    666     _to_file_pyogrio(df, filename, driver, schema, crs, mode, **kwargs)
    667 elif engine == "fiona":
--> 668     _to_file_fiona(df, filename, driver, schema, crs, mode, **kwargs)
    669 else:
    670     raise ValueError(f"unknown engine '{engine}'")

File ~/scipy/repos/geopandas/geopandas/io/file.py:690, in _to_file_fiona(df, filename, driver, schema, crs, mode, **kwargs)
    687 else:
    688     crs = df.crs
--> 690 with fiona_env():
    691     crs_wkt = None
    692     try:

TypeError: 'NoneType' object is not callable
```



Patch:
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index f25187bf..90426d17 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -16,7 +16,7 @@ from geopandas.geoseries import GeoSeries
 import geopandas.io
 from geopandas.explore import _explore
 from ._decorator import doc
-from ._compat import HAS_PYPROJ
+from ._compat import HAS_PYPROJ, PANDAS_GE_30
 
 
 def _geodataframe_constructor_with_fallback(*args, **kwargs):
@@ -314,7 +314,10 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
         if inplace:
             frame = self
         else:
-            frame = self.copy()
+            if PANDAS_GE_30:
+                frame = self.copy(deep=False)
+            else:
+                frame = self.copy()
 
         geo_column_name = self._geometry_column_name
 
@@ -2004,7 +2007,7 @@ properties': {'col1': 'name1'}, 'geometry': {'type': 'Point', 'coordinates': (1.
 
     # overrides the pandas astype method to ensure the correct return type
     # should be removable when pandas 1.4 is dropped
-    def astype(self, dtype, copy=True, errors="raise", **kwargs):
+    def astype(self, dtype, copy=None, errors="raise", **kwargs):
         """
         Cast a pandas object to a specified dtype ``dtype``.
         Returns a GeoDataFrame when the geometry column is kept as geometries,
@@ -2014,7 +2017,12 @@ properties': {'col1': 'name1'}, 'geometry': {'type': 'Point', 'coordinates': (1.
         -------
         GeoDataFrame or DataFrame
         """
-        df = super().astype(dtype, copy=copy, errors=errors, **kwargs)
+        if not PANDAS_GE_30 and copy is None:
+            copy = True
+        if copy is not None:
+            kwargs["copy"] = copy
+
+        df = super().astype(dtype, errors=errors, **kwargs)
 
         try:
             geoms = df[self._geometry_column_name]
diff --git a/geopandas/io/file.py b/geopandas/io/file.py
index aff36857..be00fea9 100644
--- a/geopandas/io/file.py
+++ b/geopandas/io/file.py
@@ -82,7 +82,7 @@ def _import_pyogrio():
 
 
 def _check_fiona(func):
-    if fiona is None:
+    if not fiona:
         raise ImportError(
             f"the {func} requires the 'fiona' package, but it is not installed or does "
             f"not import correctly.\nImporting fiona resulted in: {fiona_import_error}"
@@ -90,7 +90,7 @@ def _check_fiona(func):
 
 
 def _check_pyogrio(func):
-    if pyogrio is None:
+    if not pyogrio:
         raise ImportError(
             f"the {func} requires the 'pyogrio' package, but it is not installed "
             "or does not import correctly."
diff --git a/geopandas/tools/overlay.py b/geopandas/tools/overlay.py
index c7477edc..af3386e9 100644
--- a/geopandas/tools/overlay.py
+++ b/geopandas/tools/overlay.py
@@ -6,6 +6,7 @@ import pandas as pd
 
 from geopandas import GeoDataFrame, GeoSeries
 from geopandas.array import _check_crs, _crs_mismatch_warn
+from geopandas._compat import PANDAS_GE_30
 
 
 def _ensure_geometry_column(df):
@@ -14,12 +15,15 @@ def _ensure_geometry_column(df):
     If another column with that name exists, it will be dropped.
     """
     if not df._geometry_column_name == "geometry":
-        if "geometry" in df.columns:
-            df.drop("geometry", axis=1, inplace=True)
-        df.rename(
-            columns={df._geometry_column_name: "geometry"}, copy=False, inplace=True
-        )
-        df.set_geometry("geometry", inplace=True)
+        if PANDAS_GE_30:
+            if "geometry" in df.columns:
+                df = df.drop("geometry", axis=1)
+            df = df.rename_geometry("geometry")
+        else:
+            if "geometry" in df.columns:
+                df.drop("geometry", axis=1, inplace=True)
+            df.rename_geometry("geometry", inplace=True)
+    return df
 
 
 def _overlay_intersection(df1, df2):
@@ -111,8 +115,8 @@ def _overlay_symmetric_diff(df1, df2):
     dfdiff1["__idx2"] = np.nan
     dfdiff2["__idx1"] = np.nan
     # ensure geometry name (otherwise merge goes wrong)
-    _ensure_geometry_column(dfdiff1)
-    _ensure_geometry_column(dfdiff2)
+    dfdiff1 = _ensure_geometry_column(dfdiff1)
+    dfdiff2 = _ensure_geometry_column(dfdiff2)
     # combine both 'difference' dataframes
     dfsym = dfdiff1.merge(
         dfdiff2, on=["__idx1", "__idx2"], how="outer", suffixes=("_1", "_2")
diff --git a/geopandas/tools/sjoin.py b/geopandas/tools/sjoin.py
index 20bbbc7f..bd3aaae6 100644
--- a/geopandas/tools/sjoin.py
+++ b/geopandas/tools/sjoin.py
@@ -422,7 +422,11 @@ def _frame_join(
     new_index = pd.RangeIndex(len(l_idx))
     left = left_df._reindex_with_indexers({0: (new_index, l_idx)})
     right = right_df._reindex_with_indexers({0: (new_index, r_idx)})
-    joined = pd.concat([left, right], axis=1, copy=False)
+    if PANDAS_GE_30:
+        kwargs = {}
+    else:
+        kwargs = dict(copy=False)
+    joined = pd.concat([left, right], axis=1, **kwargs)
 
     if how in ("inner", "left"):
         joined = _restore_index(joined, left_index, left_index_original)


Test patch:
diff --git a/geopandas/io/tests/test_file.py b/geopandas/io/tests/test_file.py
index efd09ad5..19ba6005 100644
--- a/geopandas/io/tests/test_file.py
+++ b/geopandas/io/tests/test_file.py
@@ -874,7 +874,7 @@ def test_read_file__columns_empty(engine, naturalearth_lowres):
     assert gdf.columns.tolist() == ["geometry"]
 
 
-@pytest.mark.skipif(FIONA_GE_19, reason="test for fiona < 1.9")
+@pytest.mark.skipif(FIONA_GE_19 or not fiona, reason="test for fiona < 1.9")
 def test_read_file__columns_old_fiona(naturalearth_lowres):
     with pytest.raises(NotImplementedError):
         geopandas.read_file(
@@ -1306,6 +1306,26 @@ def test_option_io_engine(nybb_filename):
         geopandas.options.io_engine = None
 
 
+@pytest.mark.skipif(pyogrio, reason="test for pyogrio not installed")
+def test_error_engine_unavailable_pyogrio(tmp_path, df_points, file_path):
+
+    with pytest.raises(ImportError, match="the 'read_file' function requires"):
+        geopandas.read_file(file_path, engine="pyogrio")
+
+    with pytest.raises(ImportError, match="the 'to_file' method requires"):
+        df_points.to_file(tmp_path / "test.gpkg", engine="pyogrio")
+
+
+@pytest.mark.skipif(fiona, reason="test for fiona not installed")
+def test_error_engine_unavailable_fiona(tmp_path, df_points, file_path):
+
+    with pytest.raises(ImportError, match="the 'read_file' function requires"):
+        geopandas.read_file(file_path, engine="fiona")
+
+    with pytest.raises(ImportError, match="the 'to_file' method requires"):
+        df_points.to_file(tmp_path / "test.gpkg", engine="fiona")
+
+
 @PYOGRIO_MARK
 def test_list_layers(df_points, tmpdir):
     tempfilename = os.path.join(str(tmpdir), "dataset.gpkg")