Repository: geopandas/geopandas
Pull request #2100 (2021-09-05 11:07:15)

DEP: Switch CRS mismatch future warnings to errors
This addresses the 3 places (GeoDataFrame twice and GeoSeries) where supplying crs which conflict with the underlying data was raising a FutureWarning, and switches these over to ValueErrors. Have only changed these as they are marked with `#TODO: raise error in 0.9 or 0.10`, but there are a number of other FutureWarnings throughout the code (the first of which is something that should change in this branch too):
```bash
grep -rw . -e 'FutureWarning' --exclude *.pyc | cut -d: -f1 | uniq -c
      1 ./doc/source/docs/user_guide/projections.rst
      2 ./geopandas/array.py
      1 ./geopandas/base.py
      2 ./geopandas/geodataframe.py
      5 ./geopandas/geoseries.py
      4 ./geopandas/plotting.py
      1 ./geopandas/sindex.py
      3 ./geopandas/tests/test_array.py
      4 ./geopandas/tests/test_crs.py
      2 ./geopandas/tests/test_geom_methods.py
      4 ./geopandas/tests/test_geoseries.py
      1 ./geopandas/tests/test_sindex.py
      3 ./geopandas/tools/crs.py
      2 ./geopandas/tools/sjoin.py
      2 ./geopandas/tools/tests/test_sjoin.py
      3 ./geopandas/tools/tests/test_tools.py
      1 ./geopandas/_vectorized.py
```
Not sure if there is a desire to address some of these now as well (or at least least thing about how soon they should happen).

There is perhaps some sense in dealing with other crs related future warnings all in the same release? Going through the list very quickly, these seem to fit:
- `geodataframe.py` in both `__init__` and `crs`
```python
warnings.warn(
                "Assigning CRS to a GeoDataFrame without a geometry column is now "
                "deprecated and will not be supported in the future.",
                FutureWarning,
                stacklevel=2,
            )
```
- `tools/crs.py`
```python
"explicit_crs_from_epsg is deprecated. "
        "You can set the epsg on the GeoDataFrame (gdf) using gdf.crs=epsg",
```
```python
"epsg_from_crs is deprecated. "
        "You can get the epsg code from GeoDataFrame (gdf) "
        "using gdf.crs.to_epsg()",
```

In addition, the GeoSeries warning below has been for just over 2 years now, and seems like a pretty logical change to make.
```
"""\
    You are passing non-geometry data to the GeoSeries constructor. Currently,
    it falls back to returning a pandas Series. But in the future, we will start
    to raise a TypeError instead."""
```




m-richards: > Regarding other changes, I think that anything that has a warning since geopandas 0.8 or older can be turned into an error. If you decide to go for it, can you split it to individual thematic PRs so it is easier to track these deprecations?

Sure, I'm happy to split them out, I might start with an issue listing all the existing future warnings and the PRs can target specific ones.

Patch:
diff --git a/doc/source/docs/user_guide/projections.rst b/doc/source/docs/user_guide/projections.rst
index c36fbc3a..8a9896ae 100644
--- a/doc/source/docs/user_guide/projections.rst
+++ b/doc/source/docs/user_guide/projections.rst
@@ -109,8 +109,8 @@ GeoPandas 0.8 implements support for different projections assigned to different
 columns of the same GeoDataFrame. The projection is now stored together with geometries per column (directly
 on the GeometryArray level).
 
-Note that if GeometryArray has assigned projection, it is preferred over the
-projection passed to GeoSeries or GeoDataFrame during the creation:
+Note that if GeometryArray has an assigned projection, it cannot be overridden by an another inconsistent
+projection during the creation of a GeoSeries or GeoDataFrame:
 
 .. code-block:: python
 
@@ -121,18 +121,12 @@ projection passed to GeoSeries or GeoDataFrame during the creation:
    - Lat[north]: Geodetic latitude (degree)
    - Lon[east]: Geodetic longitude (degree)
    ...
-   >>> GeoSeries(array, crs=3395).crs  # crs=3395 is ignored as array already has CRS
-   FutureWarning: CRS mismatch between CRS of the passed geometries and 'crs'. Use 'GeoDataFrame.set_crs(crs, allow_override=True)' to overwrite CRS or 'GeoDataFrame.to_crs(crs)' to reproject geometries. CRS mismatch will raise an error in the future versions of GeoPandas.
+   >>> GeoSeries(array, crs=4326)  # crs=4326 is okay, as it matches the existing CRS
+   >>> GeoSeries(array, crs=3395)  # crs=3395 is forbidden as array already has CRS
+   ValueError: CRS mismatch between CRS of the passed geometries and 'crs'. Use 'GeoSeries.set_crs(crs, allow_override=True)' to overwrite CRS or 'GeoSeries.to_crs(crs)' to reproject geometries.
        GeoSeries(array, crs=3395).crs
 
-   <Geographic 2D CRS: EPSG:4326>
-   Name: WGS 84
-   Axis Info [ellipsoidal]:
-   - Lat[north]: Geodetic latitude (degree)
-   - Lon[east]: Geodetic longitude (degree)
-   ...
-
-If you want to overwrite projection, you can then assign it to the GeoSeries
+If you want to overwrite the projection, you can then assign it to the GeoSeries
 manually or re-project geometries to the target projection using either
 ``GeoSeries.set_crs(epsg=3395, allow_override=True)`` or
 ``GeoSeries.to_crs(epsg=3395)``.
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index 788e6274..87562e7c 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -61,18 +61,12 @@ def _ensure_geometry(data, crs=None):
             return out
 
 
-def _crs_mismatch_warning():
-    # TODO: raise error in 0.9 or 0.10.
-    warnings.warn(
-        "CRS mismatch between CRS of the passed geometries "
-        "and 'crs'. Use 'GeoDataFrame.set_crs(crs, "
-        "allow_override=True)' to overwrite CRS or "
-        "'GeoDataFrame.to_crs(crs)' to reproject geometries. "
-        "CRS mismatch will raise an error in the future versions "
-        "of GeoPandas.",
-        FutureWarning,
-        stacklevel=3,
-    )
+crs_mismatch_error = (
+    "CRS mismatch between CRS of the passed geometries "
+    "and 'crs'. Use 'GeoDataFrame.set_crs(crs, "
+    "allow_override=True)' to overwrite CRS or "
+    "'GeoDataFrame.to_crs(crs)' to reproject geometries. "
+)
 
 
 class GeoDataFrame(GeoPandasBase, DataFrame):
@@ -150,9 +144,7 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
         if geometry is None and isinstance(data, GeoDataFrame):
             self._geometry_column_name = data._geometry_column_name
             if crs is not None and data.crs != crs:
-                _crs_mismatch_warning()
-                # TODO: raise error in 0.9 or 0.10.
-            return
+                raise ValueError(crs_mismatch_error)
 
         if geometry is None and "geometry" in self.columns:
             # Check for multiple columns with name "geometry". If there are,
@@ -173,8 +165,7 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
                     and crs
                     and not self["geometry"].values.crs == crs
                 ):
-                    _crs_mismatch_warning()
-                    # TODO: raise error in 0.9 or 0.10.
+                    raise ValueError(crs_mismatch_error)
                 self["geometry"] = _ensure_geometry(self["geometry"].values, crs)
             except TypeError:
                 pass
@@ -193,8 +184,8 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
                 and crs
                 and not geometry.crs == crs
             ):
-                _crs_mismatch_warning()
-                # TODO: raise error in 0.9 or 0.10.
+                raise ValueError(crs_mismatch_error)
+
             self.set_geometry(geometry, inplace=True)
 
         if geometry is None and crs:
@@ -488,7 +479,7 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
         GeoDataFrame
 
         """
-        dataframe = super().from_dict(data, **kwargs)
+        dataframe = DataFrame.from_dict(data, **kwargs)
         return GeoDataFrame(dataframe, geometry=geometry, crs=crs)
 
     @classmethod
diff --git a/geopandas/geoseries.py b/geopandas/geoseries.py
index 9be068b2..2452bedd 100644
--- a/geopandas/geoseries.py
+++ b/geopandas/geoseries.py
@@ -143,17 +143,12 @@ class GeoSeries(GeoPandasBase, Series):
                 data = data.copy()
             else:
                 if not data.crs == crs:
-                    warnings.warn(
+                    raise ValueError(
                         "CRS mismatch between CRS of the passed geometries "
-                        "and 'crs'. Use 'GeoDataFrame.set_crs(crs, "
+                        "and 'crs'. Use 'GeoSeries.set_crs(crs, "
                         "allow_override=True)' to overwrite CRS or "
                         "'GeoSeries.to_crs(crs)' to reproject geometries. "
-                        "CRS mismatch will raise an error in the future versions "
-                        "of GeoPandas.",
-                        FutureWarning,
-                        stacklevel=2,
                     )
-                    # TODO: raise error in 0.9 or 0.10.
 
         if isinstance(data, SingleBlockManager):
             if isinstance(data.blocks[0].dtype, GeometryDtype):


Test patch:
diff --git a/geopandas/tests/test_crs.py b/geopandas/tests/test_crs.py
index b7be6663..2b995e47 100644
--- a/geopandas/tests/test_crs.py
+++ b/geopandas/tests/test_crs.py
@@ -200,7 +200,10 @@ class TestGeometryArrayCRS:
         assert s.crs == self.osgb
         assert s.values.crs == self.osgb
 
-        with pytest.warns(FutureWarning):
+        with pytest.raises(
+            ValueError,
+            match="CRS mismatch between CRS of the passed geometries and 'crs'",
+        ):
             s = GeoSeries(arr, crs=4326)
         assert s.crs == self.osgb
 
@@ -219,17 +222,15 @@ class TestGeometryArrayCRS:
         assert df.geometry.crs == self.osgb
         assert df.geometry.values.crs == self.osgb
 
-        # different passed CRS than array CRS is ignored
-        with pytest.warns(FutureWarning, match="CRS mismatch"):
+        # different passed CRS than array CRS is now an error
+        match_str = "CRS mismatch between CRS of the passed geometries and 'crs'"
+        with pytest.raises(ValueError, match=match_str):
             df = GeoDataFrame(geometry=s, crs=4326)
-        assert df.crs == self.osgb
-        assert df.geometry.crs == self.osgb
-        assert df.geometry.values.crs == self.osgb
-        with pytest.warns(FutureWarning, match="CRS mismatch"):
+        with pytest.raises(ValueError, match=match_str):
             GeoDataFrame(geometry=s, crs=4326)
-        with pytest.warns(FutureWarning, match="CRS mismatch"):
+        with pytest.raises(ValueError, match=match_str):
             GeoDataFrame({"data": [1, 2], "geometry": s}, crs=4326)
-        with pytest.warns(FutureWarning, match="CRS mismatch"):
+        with pytest.raises(ValueError, match=match_str):
             GeoDataFrame(df, crs=4326).crs
 
         # manually change CRS