Repository: geopandas/geopandas
Pull request #2351 (2022-02-12 20:41:28)

TST: allow TypeError in fillna() with pandas main
I have to admit I am not fully sure what has changed in https://github.com/pandas-dev/pandas/pull/45911 but as a result, `GeoDataFrame.fillna` completely circumvents our custom `GeometryArray.fillna`. But it still seem to work as before, just raising a different error (from `GeometryArray.__setitem__`), so I guess this is fine?



m-richards: It looks like a regression to me: 
https://github.com/pandas-dev/pandas/commit/769fc54897953d366d573d4e45cf13177a5f582b#diff-55001624a0932c1b6cee2e6ddb65dea85c1faf0dee84812c0ca0c32916a71438R1489-R1492

It seems that it's been refactored so that EABackedBlock.fillna() only calls the extensionarray fillna method for certain datetime types?

And further down in the method the `super().fillna(value, limit=limit, inplace=inplace, downcast=downcast)` call doesn't actually invoke the extension array at all - it just uses `where` to look for nans to fill, hence circumventing our code.

I think catching the typeerror is fine in the short term but we should probably see if this can be fixed on the pandas side? 

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/array.py b/geopandas/array.py
index cbfd5a6d..553fb69c 100644
--- a/geopandas/array.py
+++ b/geopandas/array.py
@@ -867,7 +867,14 @@ class GeometryArray(ExtensionArray):
     def x(self):
         """Return the x location of point geometries in a GeoSeries"""
         if (self.geom_type[~self.isna()] == "Point").all():
-            return vectorized.get_x(self.data)
+            empty = self.is_empty
+            if empty.any():
+                nonempty = ~empty
+                coords = np.full_like(nonempty, dtype=float, fill_value=np.nan)
+                coords[nonempty] = vectorized.get_x(self.data[nonempty])
+                return coords
+            else:
+                return vectorized.get_x(self.data)
         else:
             message = "x attribute access only provided for Point geometries"
             raise ValueError(message)
@@ -876,7 +883,14 @@ class GeometryArray(ExtensionArray):
     def y(self):
         """Return the y location of point geometries in a GeoSeries"""
         if (self.geom_type[~self.isna()] == "Point").all():
-            return vectorized.get_y(self.data)
+            empty = self.is_empty
+            if empty.any():
+                nonempty = ~empty
+                coords = np.full_like(nonempty, dtype=float, fill_value=np.nan)
+                coords[nonempty] = vectorized.get_y(self.data[nonempty])
+                return coords
+            else:
+                return vectorized.get_y(self.data)
         else:
             message = "y attribute access only provided for Point geometries"
             raise ValueError(message)
@@ -885,7 +899,14 @@ class GeometryArray(ExtensionArray):
     def z(self):
         """Return the z location of point geometries in a GeoSeries"""
         if (self.geom_type[~self.isna()] == "Point").all():
-            return vectorized.get_z(self.data)
+            empty = self.is_empty
+            if empty.any():
+                nonempty = ~empty
+                coords = np.full_like(nonempty, dtype=float, fill_value=np.nan)
+                coords[nonempty] = vectorized.get_z(self.data[nonempty])
+                return coords
+            else:
+                return vectorized.get_z(self.data)
         else:
             message = "z attribute access only provided for Point geometries"
             raise ValueError(message)
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
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
index 941b70fd..05475a96 100644
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -7,6 +7,7 @@ from pandas import DataFrame, Index, MultiIndex, Series
 from shapely.geometry import LinearRing, LineString, MultiPoint, Point, Polygon
 from shapely.geometry.collection import GeometryCollection
 from shapely.ops import unary_union
+from shapely import wkt
 
 from geopandas import GeoDataFrame, GeoSeries
 from geopandas.base import GeoPandasBase
@@ -72,6 +73,10 @@ class TestGeomMethods:
         self.landmarks = GeoSeries([self.esb, self.sol], crs="epsg:4326")
         self.pt2d = Point(-73.9847, 40.7484)
         self.landmarks_mixed = GeoSeries([self.esb, self.sol, self.pt2d], crs=4326)
+        self.pt_empty = wkt.loads("POINT EMPTY")
+        self.landmarks_mixed_empty = GeoSeries(
+            [self.esb, self.sol, self.pt2d, self.pt_empty], crs=4326
+        )
         self.l1 = LineString([(0, 0), (0, 1), (1, 1)])
         self.l2 = LineString([(0, 0), (1, 0), (1, 1), (0, 1)])
         self.g5 = GeoSeries([self.l1, self.l2])
@@ -623,6 +628,15 @@ class TestGeomMethods:
         expected_z = [30.3244, 31.2344, np.nan]
         assert_array_dtype_equal(expected_z, self.landmarks_mixed.geometry.z)
 
+    def test_xyz_points_empty(self):
+        expected_x = [-73.9847, -74.0446, -73.9847, np.nan]
+        expected_y = [40.7484, 40.6893, 40.7484, np.nan]
+        expected_z = [30.3244, 31.2344, np.nan, np.nan]
+
+        assert_array_dtype_equal(expected_x, self.landmarks_mixed_empty.geometry.x)
+        assert_array_dtype_equal(expected_y, self.landmarks_mixed_empty.geometry.y)
+        assert_array_dtype_equal(expected_z, self.landmarks_mixed_empty.geometry.z)
+
     def test_xyz_polygons(self):
         # accessing x attribute in polygon geoseries should raise an error
         with pytest.raises(ValueError):
diff --git a/geopandas/tests/test_pandas_methods.py b/geopandas/tests/test_pandas_methods.py
index 42c79de7..801e88a9 100644
--- a/geopandas/tests/test_pandas_methods.py
+++ b/geopandas/tests/test_pandas_methods.py
@@ -384,7 +384,7 @@ def test_fillna(s, df):
     df2["geometry"] = s2
     res = df2.fillna(Point(1, 1))
     assert_geodataframe_equal(res, df)
-    with pytest.raises(NotImplementedError):
+    with pytest.raises((NotImplementedError, TypeError)):  # GH2351
         df2.fillna(0)
 
     # allow non-geometry fill value if there are no missing values