Repository: geopandas/geopandas
Pull request #3378 (2024-07-12 18:21:35)

BUG:  incorrect warning in read_file for pyogrio when layer keyword is provided
Bug is described in issue #3377

Solution is to pass down the layer argument from gpd.read_file to pyogrio.read_info to prevent the unwaranted warning.



FredericF: Hello @m-richards, I added a test. 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 51217980..f37f386f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,18 @@
 
 Bug fixes:
 
+- Fix unspecified layer warning being emitted while reading multilayer datasets, even
+  when layer is specified when using the mask or bbox keywords (#3378).
 - Properly support named aggregations over a geometry column in `GroupBy.agg` (#3368).
+- Support GeoDataFrame constructor receiving arguments to `geometry` which are not
+  (Geo)Series, but instead should be interpreted as column names, like Enums (#3384).
+- Fix regression where constructing a GeoSeries from a pd.Series with GeometryDtype values 
+  failed when `crs` was provided(#3383).
+
+Deprecations and compatibility notes:
+
+- The `GeoSeries.select` method wrapping the pandas `Series.select` method has been removed.
+  The upstream method no longer exists in all supported version of pandas (#3394).
 
 ## Version 1.0.1 (July 2, 2024)
 
diff --git a/doc/source/conf.py b/doc/source/conf.py
index da5107a5..2f55e987 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -246,6 +246,13 @@ html_additional_pages = {page[0]: "redirect.html" for page in moved_pages}
 
 html_context = {"redirects": {old: new for old, new in moved_pages}}
 
+# Define the canonical URL if you are using a custom domain on Read the Docs
+html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "")
+
+# Tell Jinja2 templates the build is running on Read the Docs
+if os.environ.get("READTHEDOCS", "") == "True":
+    html_context["READTHEDOCS"] = True
+
 # If false, no module index is generated.
 # html_domain_indices = True
 
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index d2e889b4..a054b576 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -201,7 +201,10 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
             ):
                 raise ValueError(crs_mismatch_error)
 
-            if hasattr(geometry, "name") and geometry.name not in ("geometry", None):
+            if isinstance(geometry, pd.Series) and geometry.name not in (
+                "geometry",
+                None,
+            ):
                 # __init__ always creates geometry col named "geometry"
                 # rename as `set_geometry` respects the given series name
                 geometry = geometry.rename("geometry")
@@ -1327,7 +1330,8 @@ properties': {'col1': 'name1'}, 'geometry': {'type': 'Point', 'coordinates': (1.
             If ``False``, the index(es) will not be written to the file.
             If ``None``, the index(ex) will be included as columns in the file
             output except `RangeIndex` which is stored as metadata only.
-        compression : {'snappy', 'gzip', 'brotli', None}, default 'snappy'
+        compression : {'snappy', 'gzip', 'brotli', 'lz4', 'zstd', None}, \
+default 'snappy'
             Name of the compression to use. Use ``None`` for no compression.
         geometry_encoding : {'WKB', 'geoarrow'}, default 'WKB'
             The encoding to use for the geometry columns. Defaults to "WKB"
diff --git a/geopandas/geoseries.py b/geopandas/geoseries.py
index ea30f61d..4eaa4d3a 100644
--- a/geopandas/geoseries.py
+++ b/geopandas/geoseries.py
@@ -164,7 +164,7 @@ class GeoSeries(GeoPandasBase, Series):
                 # make a copy to avoid setting CRS to passed GeometryArray
                 data = data.copy()
             else:
-                if not data.crs == crs:
+                if not data_crs == crs:
                     raise ValueError(
                         "CRS mismatch between CRS of the passed geometries "
                         "and 'crs'. Use 'GeoSeries.set_crs(crs, "
@@ -734,10 +734,6 @@ class GeoSeries(GeoPandasBase, Series):
     def take(self, *args, **kwargs):
         return self._wrapped_pandas_method("take", *args, **kwargs)
 
-    @doc(pd.Series)
-    def select(self, *args, **kwargs):
-        return self._wrapped_pandas_method("select", *args, **kwargs)
-
     @doc(pd.Series)
     def apply(self, func, convert_dtype: Optional[bool] = None, args=(), **kwargs):
         if convert_dtype is not None:
diff --git a/geopandas/io/file.py b/geopandas/io/file.py
index 37aa3038..af6f3bce 100644
--- a/geopandas/io/file.py
+++ b/geopandas/io/file.py
@@ -228,9 +228,9 @@ def _read_file(
         Keyword args to be passed to the engine, and can be used to write
         to multi-layer data, store data within archives (zip files), etc.
         In case of the "pyogrio" engine, the keyword arguments are passed to
-        `pyogrio.write_dataframe`. In case of the "fiona" engine, the keyword
+        `pyogrio.read_dataframe`. In case of the "fiona" engine, the keyword
         arguments are passed to fiona.open`. For more information on possible
-        keywords, type: ``import pyogrio; help(pyogrio.write_dataframe)``.
+        keywords, type: ``import pyogrio; help(pyogrio.read_dataframe)``.
 
 
     Examples
@@ -481,7 +481,7 @@ def _read_file_pyogrio(path_or_bytes, bbox=None, mask=None, rows=None, **kwargs)
 
     if bbox is not None:
         if isinstance(bbox, (GeoDataFrame, GeoSeries)):
-            crs = pyogrio.read_info(path_or_bytes).get("crs")
+            crs = pyogrio.read_info(path_or_bytes, layer=kwargs.get("layer")).get("crs")
             if isinstance(path_or_bytes, IOBase):
                 path_or_bytes.seek(0)
 
@@ -494,7 +494,7 @@ def _read_file_pyogrio(path_or_bytes, bbox=None, mask=None, rows=None, **kwargs)
     if mask is not None:
         # NOTE: mask cannot be used at same time as bbox keyword
         if isinstance(mask, (GeoDataFrame, GeoSeries)):
-            crs = pyogrio.read_info(path_or_bytes).get("crs")
+            crs = pyogrio.read_info(path_or_bytes, layer=kwargs.get("layer")).get("crs")
             if isinstance(path_or_bytes, IOBase):
                 path_or_bytes.seek(0)
 
@@ -526,7 +526,7 @@ def _read_file_pyogrio(path_or_bytes, bbox=None, mask=None, rows=None, **kwargs)
             stacklevel=3,
         )
         ignore_fields = kwargs.pop("ignore_fields")
-        fields = pyogrio.read_info(path_or_bytes)["fields"]
+        fields = pyogrio.read_info(path_or_bytes, layer=kwargs.get("layer"))["fields"]
         include_fields = [col for col in fields if col not in ignore_fields]
         kwargs["columns"] = include_fields
     elif "include_fields" in kwargs:


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 4e419a47..2cbc1083 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -88,7 +88,7 @@ jobs:
 
       - name: Test
         run: |
-          pytest -v -r fEs -n auto --color=yes --cov=geopandas --cov-append --cov-report term-missing --cov-report xml geopandas/
+          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'))
@@ -102,7 +102,7 @@ jobs:
           conda install postgis -c conda-forge
           sh ci/scripts/setup_postgres.sh
           set -o pipefail
-          pytest -v -r fEs --color=yes --cov=geopandas --cov-append --cov-report term-missing --cov-report xml geopandas/io/tests/test_sql.py | tee /dev/stderr | if grep SKIPPED >/dev/null;then echo "TESTS SKIPPED, FAILING" && exit 1;fi
+          pytest -v -r a --color=yes --cov=geopandas --cov-append --cov-report term-missing --cov-report xml geopandas/io/tests/test_sql.py | tee /dev/stderr | if grep SKIPPED >/dev/null;then echo "TESTS SKIPPED, FAILING" && exit 1;fi
 
       - name: Test docstrings
         if: contains(matrix.env, '310-pd20-conda-forge.yaml') && contains(matrix.os, 'ubuntu')
diff --git a/geopandas/io/tests/test_arrow.py b/geopandas/io/tests/test_arrow.py
index 9078191d..c10220a1 100644
--- a/geopandas/io/tests/test_arrow.py
+++ b/geopandas/io/tests/test_arrow.py
@@ -466,7 +466,9 @@ def test_column_order(tmpdir, file_format, naturalearth_lowres):
     assert_geodataframe_equal(result, df[custom_column_order[1:]])
 
 
-@pytest.mark.parametrize("compression", ["snappy", "gzip", "brotli", None])
+@pytest.mark.parametrize(
+    "compression", ["snappy", "gzip", "brotli", "lz4", "zstd", None]
+)
 def test_parquet_compression(compression, tmpdir, naturalearth_lowres):
     """Using compression options should not raise errors, and should
     return identical GeoDataFrame.
@@ -1020,7 +1022,6 @@ def test_read_parquet_geoarrow(geometry_type):
     ["point", "linestring", "polygon", "multipoint", "multilinestring", "multipolygon"],
 )
 def test_geoarrow_roundtrip(tmp_path, geometry_type):
-
     df = geopandas.read_parquet(
         DATA_PATH
         / "arrow"
diff --git a/geopandas/io/tests/test_file.py b/geopandas/io/tests/test_file.py
index 014b1f3c..b710f1f4 100644
--- a/geopandas/io/tests/test_file.py
+++ b/geopandas/io/tests/test_file.py
@@ -5,6 +5,7 @@ import os
 import pathlib
 import shutil
 import tempfile
+import warnings
 from collections import OrderedDict
 from packaging.version import Version
 
@@ -1075,6 +1076,31 @@ def test_read_file_mask_gdf_mismatched_crs(df_nybb, engine, nybb_filename):
     assert filtered_df_shape == (2, 5)
 
 
+@pytest.mark.skipif(not HAS_PYPROJ, reason="pyproj not installed")
+def test_read_file_multi_layer_with_layer_arg_no_warning(tmp_path, engine):
+    # While reading a file with multiple layers, if the layer is properly
+    # specified, a "Specify layer" warning should not be emitted
+    data1 = {"geometry": [Point(1, 2), Point(2, 1)]}
+    gdf = geopandas.GeoDataFrame(data1, crs="EPSG:4326")
+    file_path = tmp_path / "out.gpkg"
+    gdf.to_file(file_path, layer="layer1", engine=engine)
+    gdf.to_file(file_path, layer="layer2", engine=engine)
+
+    with warnings.catch_warnings(record=True) as captured:
+        warnings.simplefilter("always")
+        read_file(file_path, bbox=gdf, layer="layer1", engine=engine)
+        read_file(file_path, mask=gdf, layer="layer1", engine=engine)
+        specify_layer_warnings = [
+            warning
+            for warning in captured
+            if warning.category == UserWarning
+            and "specify layer parameter" in str(warning.message).lower()
+        ]
+        assert (
+            len(specify_layer_warnings) == 0
+        ), "'Specify layer parameter' warning was raised, but the layer was specified."
+
+
 def test_read_file_bbox_mask_not_allowed(engine, nybb_filename):
     bbox = (
         1031051.7879884212,
diff --git a/geopandas/tests/test_geodataframe.py b/geopandas/tests/test_geodataframe.py
index c9d74b1f..c710b262 100644
--- a/geopandas/tests/test_geodataframe.py
+++ b/geopandas/tests/test_geodataframe.py
@@ -2,6 +2,7 @@ import json
 import os
 import shutil
 import tempfile
+from enum import Enum
 
 import numpy as np
 import pandas as pd
@@ -1489,6 +1490,19 @@ class TestConstructor:
         df[other_df.columns] = other_df
         assert_geodataframe_equal(df, expected)
 
+    def test_geometry_colname_enum(self):
+        # ensure that other classes to geometry arg in GeoDataFrame
+        # with `name` attribute are not assumed to be (Geo)Series
+        class Fruit(Enum):
+            apple = 1
+            pear = 2
+
+        df = pd.DataFrame(
+            {Fruit.apple: [1, 2], Fruit.pear: GeoSeries.from_xy([1, 2], [3, 4])}
+        )
+        res = GeoDataFrame(df, geometry=Fruit.pear)
+        assert res.active_geometry_name == Fruit.pear
+
 
 @pytest.mark.skipif(not compat.HAS_PYPROJ, reason="pyproj not available")
 def test_geodataframe_crs():
diff --git a/geopandas/tests/test_geoseries.py b/geopandas/tests/test_geoseries.py
index 39590176..4d2e2b54 100644
--- a/geopandas/tests/test_geoseries.py
+++ b/geopandas/tests/test_geoseries.py
@@ -673,6 +673,16 @@ class TestConstructor:
         assert gs.crs is None
         assert result.crs == "EPSG:4326"
 
+        # https://github.com/geopandas/geopandas/issues/3382
+        s2 = pd.Series(gs.set_crs("EPSG:4326"))
+        result = GeoSeries(s2, crs=4326)
+        assert result.crs == "EPSG:4326"
+        with pytest.raises(
+            ValueError,
+            match="CRS mismatch between CRS of the passed geometries and 'crs'",
+        ):
+            GeoSeries(s2, crs=4283)
+
     def test_copy(self):
         # default is to copy with CoW / pandas 3+
         arr = np.array([Point(x, x) for x in range(3)], dtype=object)