Repository: geopandas/geopandas
Pull request #2568 (2022-09-26 19:37:48)

DOC/MAINT: GeoPandas roadmap draft
This is a proposal of a roadmap for the GeoPandas project (not covering sub-projects) resulting from the [discussions](https://hackmd.io/pq8MqmtxTFqqaiayFuuEYQ) during the GeoPandas community calls. 

Feel free to open a discussion on a removal of individual goals or moving them between the milestones. If you have another suggestion on what would you like to see on a roadmap, feel free to suggest it here or open a separate issue for a discussion (not everything needs to be included in the first version but we need one for NumFocus in about 2 weeks).

I would like to merge the first version in relatively soon, ideally during the first week of October or sooner.

cc @geopandas/collaborators 

martinfleis: @brendan-ward thanks for the thorough proofreading! I've added the suggested I/O bit.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4253ca1d..adfde9c5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,8 @@ Development version
 
 New features and improvements:
 
-  - Add where filter to ``read_file`` (#2552)
+- Added ``normalize()`` method from shapely to GeoSeries/GeoDataframe (#2537)
+- Add where filter to ``read_file`` (#2552)
 
 Deprecations and compatibility notes:
 
diff --git a/doc/source/about.md b/doc/source/about.md
index 2e2f0616..34cd6a3d 100644
--- a/doc/source/about.md
+++ b/doc/source/about.md
@@ -6,11 +6,11 @@
 :hidden:
 
 Team <about/team>
+Roadmap <about/roadmap>
 Citing <about/citing>
 Logo <about/logo>
 ```
 
-
 GeoPandas is an open source project to add support for geographic data to pandas objects. It
 currently implements `GeoSeries` and `GeoDataFrame` types which are subclasses of
 `pandas.Series` and `pandas.DataFrame` respectively. GeoPandas objects can act on
@@ -25,7 +25,7 @@ under the liberal terms of the BSD-3-Clause license.
 
 ```{container} button
 
-{doc}`Team <about/team>`
+{doc}`Team <about/team>` {doc}`Roadmap <about/roadmap>`
 {doc}`Citing <about/citing>` {doc}`Logo <about/logo>`
 ```
 
@@ -49,5 +49,3 @@ development.
 - **2014**: GeoPandas 0.1.0 released
 - **2020**: GeoPandas became [NumFOCUS Affiliated
   Project](https://numfocus.org/sponsored-projects/affiliated-projects)
-
-
diff --git a/doc/source/about/roadmap.md b/doc/source/about/roadmap.md
index a4ab6d98..298a926a 100644
--- a/doc/source/about/roadmap.md
+++ b/doc/source/about/roadmap.md
@@ -1,5 +1,104 @@
 # Roadmap
 
+This page provides an overview of the strategic goals for development of GeoPandas. Some
+of the tasks may happen sooner given the appropriate funding, other later with no
+specified date, and some may not happen at all if the implementation proves to be
+against the will of the community or face technical issues preventing their inclusion in
+the code base.
+
+The current roadmap is divided into two milestones. The first milestone aims at a
+release of the first major version of GeoPandas, while the second milestone is a
+longer-term vision covering enhancements that should happen in subsequent releases.
+
 ## Roadmap for GeoPandas 1.0
 
-WIP
+### Fully vectorized geometry engine
+
+GeoPandas uses `shapely` as its geometry engine, based on scalar geometries, requiring a
+loop-based implementation of most GeoPandas methods. That comes at a significant
+performance cost, which is being resolved in shapely 2.0, a new major release resulting
+from a complete rewrite of the internals using the vectorized implementation prototyped
+in the `PyGEOS` project. At this moment, GeoPandas supports `shapely<2.0`,
+`shapely>=2.0`, and `PyGEOS` as possible geometry engines, which causes friction in the
+development process and uneven performance on the user side based on what geometry
+engine the user happens to be using.
+
+GeoPandas 1.0 will require `shapely>=2.0` and deprecate both older shapely and `PyGEOS`
+engines. This change should simplify the code base allowing more manageable maintenance
+and a lower barrier to entry for new contributors.
+
+### Feature parity with shapely
+
+Even though GeoPandas uses shapely as the geometry engine, not all its functions are
+exposed at a GeoPandas level. This has resulted in a less convenient API and a need to
+switch between `GeoSeries` objects and lists or arrays of geometries, potentially
+risking the data loss or corruption as the CRS is not included in such operations. In
+the first phase, all element-wise operations (e.g. `segmentize`, or
+`minimum_bounding_circle`) should be exposed as `GeoSeries` methods. The feature parity
+should be reached in the second phase, covering all relevant functions.
+
+### Clarity of the API
+
+The first version of the GeoPandas API is nearly ten years old. The PyData ecosystem has
+significantly changed in the meantime, and some of the early decisions may no longer be
+future-proof. Ahead of GeoPandas 1.0, the API will be revised to ensure that all the
+necessary deprecations occur before the major release to provide the stability of the
+API for the coming years.
+
+### Pruned dependencies
+
+GeoPandas offers functionality for every step of a typical geospatial workflow, from
+reading of the GIS file formats to geometry operations and handling of Coordinate
+Reference Systems (CRS) and transformation of geometries between them. However, GIS I/O
+depends on a relatively heavy C++ library `GDAL` and CRS management on another C++
+library `PROJ`, even though not every application based on GeoPandas is necessarily
+geospatial. GeoPandas 1.0 should eliminate the hard dependency on both `GDAL` and `PROJ`
+and offer the basic capability of a GeoDataFrame with a minimal set of dependencies
+limited to `pandas` and `shapely`.
+
+## Beyond GeoPandas 1.0
+
+Additional work is planned for a longer time frame, stretching beyond GeoPandas 1.0
+without a specific target release.
+
+### S2 geometry engine
+
+The geometry engine used in GeoPandas is `shapely`, which serves as a Python API for
+`GEOS`. It means that all geometry operations in GeoPandas are planar, using (possibly)
+projected coordinate reference systems. Some applications focusing on the global context
+may find planar operations limiting as they come with troubles around anti-meridian and
+poles. One solution is an implementation of a spherical geometry engine, namely `S2`,
+that should eliminate these limitations and offer an alternative to `GEOS`.
+
+The GeoPandas community is currently working together with the R-spatial community that
+has already exposed `S2` in an R counterpart of GeoPandas `sf` on Python bindings for
+`S2`, that should be used as a secondary geometry engine in GeoPandas.
+
+### Lighter-weight geospatial I/O
+
+In order to support lighter-weight installations of GeoPandas that do not depend on
+heavier and difficult to install libraries such as GDAL, additional I/O libraries should
+be developed and integrated into GeoPandas as optional dependencies.  These should be
+simpler to install and not require binary dependencies, which would lower the barrier to
+entry for GeoPandas users that need basic I/O support for a limited number of GIS
+formats such as ESRI Shapefiles or GeoPackages.
+
+### Prepared geometries
+
+GeoPandas is using spatial indexing for the operations that may benefit from it. Further
+performance gains can be achieved using prepared geometries. Preparation creates a
+spatial index of individual line segments of geometries, greatly enhancing the speed of
+spatial predicates like `intersects` or `contains`. Given that the preparation has
+become less computationally expensive in `shapely` 2.0, GeoPandas should expose the
+preparation to the user but, more importantly, use smart automatic geometry preparation
+under the hood.
+
+### Static plotting improvements
+
+GeoPandas currently covers a broad range of geospatial tasks, from data exploration to
+advanced analysis. However, one moment may tempt the user to use different software -
+plotting. GeoPandas can create static maps based on ``matplotlib``, but they are a bit
+basic at the moment. It isn't straightforward to generate a complex map in a
+production-quality which can go straight to an academic journal or an infographic. We
+want to change this and remove barriers which we currently have and make it simple to
+create beautiful maps.
diff --git a/doc/source/docs/reference/geoseries.rst b/doc/source/docs/reference/geoseries.rst
index 1965d115..4ad48ca7 100644
--- a/doc/source/docs/reference/geoseries.rst
+++ b/doc/source/docs/reference/geoseries.rst
@@ -87,6 +87,7 @@ Constructive Methods and Attributes
    GeoSeries.convex_hull
    GeoSeries.envelope
    GeoSeries.simplify
+   GeoSeries.normalize
 
 Affine transformations
 ----------------------
diff --git a/geopandas/_vectorized.py b/geopandas/_vectorized.py
index 90b6dd1a..e9646c6a 100644
--- a/geopandas/_vectorized.py
+++ b/geopandas/_vectorized.py
@@ -519,9 +519,9 @@ def is_ring(data):
         for geom in data:
             if geom is None:
                 results.append(False)
-            elif geom.type == "Polygon":
+            elif geom.geom_type == "Polygon":
                 results.append(geom.exterior.is_ring)
-            elif geom.type in ["LineString", "LinearRing"]:
+            elif geom.geom_type in ["LineString", "LinearRing"]:
                 results.append(geom.is_ring)
             else:
                 results.append(False)
@@ -796,12 +796,13 @@ def clip_by_rect(data, xmin, ymin, xmax, ymax):
         return pygeos.clip_by_rect(data, xmin, ymin, xmax, ymax)
     else:
         clipped_geometries = np.empty(len(data), dtype=object)
-        clipped_geometries[:] = [
-            shapely.ops.clip_by_rect(s, xmin, ymin, xmax, ymax)
-            if s is not None
-            else None
-            for s in data
-        ]
+        with compat.ignore_shapely2_warnings():
+            clipped_geometries[:] = [
+                shapely.ops.clip_by_rect(s, xmin, ymin, xmax, ymax)
+                if s is not None
+                else None
+                for s in data
+            ]
         return clipped_geometries
 
 
@@ -961,13 +962,17 @@ def normalize(data):
         return shapely.normalize(data)
     elif compat.USE_PYGEOS:
         return pygeos.normalize(data)
+    elif compat.SHAPELY_GE_18:
+        out = np.empty(len(data), dtype=object)
+        with compat.ignore_shapely2_warnings():
+            out[:] = [geom.normalize() if geom is not None else None for geom in data]
     else:
         out = np.empty(len(data), dtype=object)
         with compat.ignore_shapely2_warnings():
             out[:] = [
                 _shapely_normalize(geom) if geom is not None else None for geom in data
             ]
-        return out
+    return out
 
 
 def project(data, other, normalized=False):
diff --git a/geopandas/array.py b/geopandas/array.py
index e30e4a83..c2065565 100644
--- a/geopandas/array.py
+++ b/geopandas/array.py
@@ -511,6 +511,9 @@ class GeometryArray(ExtensionArray):
     def representative_point(self):
         return GeometryArray(vectorized.representative_point(self.data), crs=self.crs)
 
+    def normalize(self):
+        return GeometryArray(vectorized.normalize(self.data), crs=self.crs)
+
     #
     # Binary predicates
     #
diff --git a/geopandas/base.py b/geopandas/base.py
index ad272df4..b5a44502 100644
--- a/geopandas/base.py
+++ b/geopandas/base.py
@@ -692,6 +692,39 @@ GeometryCollection
         """
         return _delegate_geo_method("representative_point", self)
 
+    def normalize(self):
+        """Returns a ``GeoSeries`` of normalized geometries to normal form (or canonical form).
+
+        This method orders the coordinates, rings of a polygon and parts of
+        multi geometries consistently. Typically useful for testing purposes
+        (for example in combination with `equals_exact`).
+
+        Examples
+        --------
+
+        >>> from shapely.geometry import Polygon, LineString, Point
+        >>> s = geopandas.GeoSeries(
+        ...     [
+        ...         Polygon([(0, 0), (1, 1), (0, 1)]),
+        ...         LineString([(0, 0), (1, 1), (1, 0)]),
+        ...         Point(0, 0),
+        ...     ],
+        ...     crs='EPSG:3857'
+        ... )
+        >>> s
+        0    POLYGON ((0.000 0.000, 1.000 1.000, 0.000 1.00...
+        1    LINESTRING (0.000 0.000, 1.000 1.000, 1.000 0....
+        2                                  POINT (0.000 0.000)
+        dtype: geometry
+
+        >>> s.normalize()
+        0    POLYGON ((0.000 0.000, 0.000 1.000, 1.000 1.00...
+        1    LINESTRING (0.000 0.000, 1.000 1.000, 1.000 0....
+        2                                  POINT (0.000 0.000)
+        dtype: geometry
+        """
+        return _delegate_geo_method("normalize", self)
+
     #
     # Reduction operations that return a Shapely geometry
     #
diff --git a/geopandas/geoseries.py b/geopandas/geoseries.py
index 8dc054e5..cbb4f402 100644
--- a/geopandas/geoseries.py
+++ b/geopandas/geoseries.py
@@ -9,6 +9,7 @@ from pandas.core.internals import SingleBlockManager
 from pyproj import CRS
 import shapely
 from shapely.geometry.base import BaseGeometry
+from shapely.geometry import GeometryCollection
 
 from geopandas.base import GeoPandasBase, _delegate_property
 from geopandas.plotting import plot_series
@@ -788,7 +789,7 @@ class GeoSeries(GeoPandasBase, Series):
         GeoSeries.isna : detect missing values
         """
         if value is None:
-            value = BaseGeometry()
+            value = GeometryCollection() if compat.SHAPELY_GE_20 else BaseGeometry()
         return super().fillna(value=value, method=method, inplace=inplace, **kwargs)
 
     def __contains__(self, other):
@@ -924,7 +925,7 @@ class GeoSeries(GeoPandasBase, Series):
         index = []
         geometries = []
         for idx, s in self.geometry.items():
-            if s.type.startswith("Multi") or s.type == "GeometryCollection":
+            if s.geom_type.startswith("Multi") or s.geom_type == "GeometryCollection":
                 geoms = s.geoms
                 idxs = [(idx, i) for i in range(len(geoms))]
             else:
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
index 7677d569..4145307f 100644
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -48,7 +48,7 @@ def _flatten_multi_geoms(geoms, prefix="Multi"):
         return geoms, np.arange(len(geoms))
 
     for ix, geom in enumerate(geoms):
-        if geom is not None and geom.type.startswith(prefix) and not geom.is_empty:
+        if geom is not None and geom.geom_type.startswith(prefix) and not geom.is_empty:
             for poly in geom.geoms:
                 components.append(poly)
                 component_index.append(ix)
@@ -435,7 +435,7 @@ def plot_series(
     expl_color = np.take(color, multiindex, axis=0) if color_given else color
     expl_series = geopandas.GeoSeries(geoms)
 
-    geom_types = expl_series.type
+    geom_types = expl_series.geom_type
     poly_idx = np.asarray((geom_types == "Polygon") | (geom_types == "MultiPolygon"))
     line_idx = np.asarray(
         (geom_types == "LineString")
@@ -834,7 +834,7 @@ GON (((-122.84000 49.00000, -120.0000...
     nan_idx = np.take(nan_idx, multiindex, axis=0)
     expl_series = geopandas.GeoSeries(geoms)
 
-    geom_types = expl_series.type
+    geom_types = expl_series.geom_type
     poly_idx = np.asarray((geom_types == "Polygon") | (geom_types == "MultiPolygon"))
     line_idx = np.asarray(
         (geom_types == "LineString")
diff --git a/geopandas/tools/overlay.py b/geopandas/tools/overlay.py
index 15133eff..ead7d812 100644
--- a/geopandas/tools/overlay.py
+++ b/geopandas/tools/overlay.py
@@ -35,7 +35,7 @@ def _overlay_intersection(df1, df2):
         right = df2.geometry.take(idx2)
         right.reset_index(drop=True, inplace=True)
         intersections = left.intersection(right)
-        poly_ix = intersections.type.isin(["Polygon", "MultiPolygon"])
+        poly_ix = intersections.geom_type.isin(["Polygon", "MultiPolygon"])
         intersections.loc[poly_ix] = intersections[poly_ix].buffer(0)
 
         # only keep actual intersecting geometries
@@ -92,7 +92,7 @@ def _overlay_difference(df1, df2):
         )
         new_g.append(new)
     differences = GeoSeries(new_g, index=df1.index, crs=df1.crs)
-    poly_ix = differences.type.isin(["Polygon", "MultiPolygon"])
+    poly_ix = differences.geom_type.isin(["Polygon", "MultiPolygon"])
     differences.loc[poly_ix] = differences[poly_ix].buffer(0)
     geom_diff = differences[~differences.is_empty].copy()
     dfdiff = df1[~differences.is_empty].copy()
diff --git a/geopandas/tools/util.py b/geopandas/tools/util.py
index c42c921a..5d4c507e 100644
--- a/geopandas/tools/util.py
+++ b/geopandas/tools/util.py
@@ -32,8 +32,8 @@ def collect(x, multi=False):
     # must be the same. If there is more than one element,
     # they cannot be Multi*, i.e., can't pass in combination of
     # Point and MultiPoint... or even just MultiPoint
-    t = x[0].type
-    if not all(g.type == t for g in x):
+    t = x[0].geom_type
+    if not all(g.geom_type == t for g in x):
         raise ValueError("Geometry type must be homogeneous")
     if len(x) > 1 and t.startswith("Multi"):
         raise ValueError("Cannot collect {0}. Must have single geometries".format(t))


Test patch:
diff --git a/geopandas/testing.py b/geopandas/testing.py
index 72ba71d8..c47e07f3 100644
--- a/geopandas/testing.py
+++ b/geopandas/testing.py
@@ -180,9 +180,9 @@ def assert_geoseries_equal(
     assert left.index.equals(right.index), "index: %s != %s" % (left.index, right.index)
 
     if check_geom_type:
-        assert (left.type == right.type).all(), "type: %s != %s" % (
-            left.type,
-            right.type,
+        assert (left.geom_type == right.geom_type).all(), "type: %s != %s" % (
+            left.geom_type,
+            right.geom_type,
         )
 
     if normalize:
diff --git a/geopandas/tests/test_dissolve.py b/geopandas/tests/test_dissolve.py
index d957bdf8..dd0eafc9 100644
--- a/geopandas/tests/test_dissolve.py
+++ b/geopandas/tests/test_dissolve.py
@@ -1,3 +1,5 @@
+import warnings
+
 import numpy as np
 import pandas as pd
 
@@ -289,7 +291,7 @@ def test_dissolve_dropna():
 )
 def test_dissolve_dropna_warn(nybb_polydf):
     # No warning with default params
-    with pytest.warns(None) as record:
+    with warnings.catch_warnings(record=True) as record:
         nybb_polydf.dissolve()
 
     for r in record:
@@ -308,7 +310,7 @@ def test_dissolve_multi_agg(nybb_polydf, merged_shapes):
     merged_shapes[("BoroCode", "max")] = [5, 2]
     merged_shapes[("BoroName", "count")] = [3, 2]
 
-    with pytest.warns(None) as record:
+    with warnings.catch_warnings(record=True) as record:
         test = nybb_polydf.dissolve(
             by="manhattan_bronx",
             aggfunc={
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
index dbefd4b6..d29932dc 100644
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -1,4 +1,5 @@
 import string
+import warnings
 
 import numpy as np
 from numpy.testing import assert_array_equal
@@ -672,6 +673,15 @@ class TestGeomMethods:
         with pytest.warns(UserWarning, match="Geometry is in a geographic CRS"):
             self.g4.centroid
 
+    def test_normalize(self):
+        polygon = Polygon([(0, 0), (1, 1), (0, 1)])
+        linestring = LineString([(0, 0), (1, 1), (1, 0)])
+        point = Point(0, 0)
+        series = GeoSeries([polygon, linestring, point])
+        polygon2 = Polygon([(0, 0), (0, 1), (1, 1)])
+        expected = GeoSeries([polygon2, linestring, point])
+        assert_geoseries_equal(series.normalize(), expected)
+
     def test_convex_hull(self):
         # the convex hull of a square should be the same as the square
         squares = GeoSeries([self.sq for i in range(3)])
@@ -858,7 +868,7 @@ class TestGeomMethods:
         with pytest.warns(UserWarning, match="Geometry is in a geographic CRS"):
             self.g4.buffer(1)
 
-        with pytest.warns(None) as record:
+        with warnings.catch_warnings(record=True) as record:
             # do not warn for 0
             self.g4.buffer(0)
 
diff --git a/geopandas/tests/test_geoseries.py b/geopandas/tests/test_geoseries.py
index c511c3f3..d4b1425c 100644
--- a/geopandas/tests/test_geoseries.py
+++ b/geopandas/tests/test_geoseries.py
@@ -3,6 +3,7 @@ import os
 import random
 import shutil
 import tempfile
+import warnings
 
 import numpy as np
 from numpy.testing import assert_array_equal
@@ -11,6 +12,7 @@ from pandas.testing import assert_index_equal
 
 from pyproj import CRS
 from shapely.geometry import (
+    GeometryCollection,
     LineString,
     MultiLineString,
     MultiPoint,
@@ -121,13 +123,13 @@ class TestSeries:
         # Test that warning is not issued when operating on aligned series
         a1, a2 = self.a1.align(self.a2)
 
-        with pytest.warns(None) as warnings:
+        with warnings.catch_warnings(record=True) as record:
             a1.contains(a2)  # _series_op, explicitly aligned
             self.g1.intersects(self.g2)  # _series_op, implicitly aligned
             a2.union(a1)  # _geo_op, explicitly aligned
             self.g2.intersection(self.g1)  # _geo_op, implicitly aligned
 
-        user_warnings = [w for w in warnings if w.category is UserWarning]
+        user_warnings = [w for w in record if w.category is UserWarning]
         assert not user_warnings, user_warnings[0].message
 
     def test_geom_equals(self):
@@ -384,7 +386,7 @@ class TestSeries:
 
 @pytest.mark.filterwarnings("ignore::UserWarning")
 def test_missing_values():
-    s = GeoSeries([Point(1, 1), None, np.nan, BaseGeometry(), Polygon()])
+    s = GeoSeries([Point(1, 1), None, np.nan, GeometryCollection(), Polygon()])
 
     # construction -> missing values get normalized to None
     assert s[1] is None
@@ -509,7 +511,7 @@ class TestConstructor:
             np.array([], dtype="float64"),
             np.array([], dtype="str"),
         ]:
-            with pytest.warns(None) as record:
+            with warnings.catch_warnings(record=True) as record:
                 s = GeoSeries(arr)
             assert not record
             assert isinstance(s, GeoSeries)
diff --git a/geopandas/tests/test_pandas_methods.py b/geopandas/tests/test_pandas_methods.py
index 90a4aa9b..b29ce854 100644
--- a/geopandas/tests/test_pandas_methods.py
+++ b/geopandas/tests/test_pandas_methods.py
@@ -302,8 +302,10 @@ def test_to_csv(df):
     assert df.to_csv(index=False) == exp
 
 
+@pytest.mark.filterwarnings(
+    "ignore:Dropping of nuisance columns in DataFrame reductions"
+)
 def test_numerical_operations(s, df):
-
     # df methods ignore the geometry column
     exp = pd.Series([3, 4], index=["value1", "value2"])
     assert_series_equal(df.sum(), exp)
diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py
index 3a3c9c38..771eac82 100644
--- a/geopandas/tests/test_plotting.py
+++ b/geopandas/tests/test_plotting.py
@@ -620,7 +620,7 @@ class TestPolygonPlotting:
 
         t3 = Polygon([(2, 0), (3, 0), (3, 1)])
         df_nan = GeoDataFrame({"geometry": t3, "values": [np.nan]})
-        self.df3 = self.df.append(df_nan)
+        self.df3 = pd.concat([self.df, df_nan])
 
     def test_single_color(self):
 
@@ -888,7 +888,7 @@ class TestColorParamArray:
             color += ["red", "green", "blue"]
 
         self.gdf = GeoDataFrame({"geometry": geom, "color_rgba": color})
-        self.mgdf = self.gdf.dissolve(self.gdf.type)
+        self.mgdf = self.gdf.dissolve(self.gdf.geom_type)
 
     def test_color_single(self):
         ax = self.gdf.plot(color=self.gdf["color_rgba"])
diff --git a/geopandas/tests/test_sindex.py b/geopandas/tests/test_sindex.py
index eaa21220..75bfe05a 100644
--- a/geopandas/tests/test_sindex.py
+++ b/geopandas/tests/test_sindex.py
@@ -75,6 +75,7 @@ class TestSeriesSindex:
         s = GeoSeries([t1, t2, sq])
         assert s.sindex.size == 3
 
+    @pytest.mark.filterwarnings("ignore:The series.append method is deprecated")
     def test_polygons_append(self):
         t1 = Polygon([(0, 0), (1, 0), (1, 1)])
         t2 = Polygon([(0, 0), (1, 1), (0, 1)])
diff --git a/geopandas/tests/test_testing.py b/geopandas/tests/test_testing.py
index f85286da..1783dd23 100644
--- a/geopandas/tests/test_testing.py
+++ b/geopandas/tests/test_testing.py
@@ -1,3 +1,5 @@
+import warnings
+
 import numpy as np
 
 from shapely.geometry import Point, Polygon
@@ -125,7 +127,7 @@ def test_ignore_crs_mismatch():
 
     # assert that with `check_crs=False` the assert passes, and also does not
     # generate any warning from comparing both geometries with different crs
-    with pytest.warns(None) as record:
+    with warnings.catch_warnings(record=True) as record:
         assert_geodataframe_equal(df1, df2, check_crs=False)
 
     assert len(record) == 0
diff --git a/geopandas/tests/util.py b/geopandas/tests/util.py
index 83f09e89..a6641371 100644
--- a/geopandas/tests/util.py
+++ b/geopandas/tests/util.py
@@ -34,7 +34,7 @@ def validate_boro_df(df, case_sensitive=False):
     else:
         for col in columns:
             assert col.lower() in (dfcol.lower() for dfcol in df.columns)
-    assert Series(df.geometry.type).dropna().eq("MultiPolygon").all()
+    assert Series(df.geometry.geom_type).dropna().eq("MultiPolygon").all()
 
 
 def get_srid(df):
diff --git a/geopandas/tools/tests/test_sjoin.py b/geopandas/tools/tests/test_sjoin.py
index 953a1fa9..313a5a6e 100644
--- a/geopandas/tools/tests/test_sjoin.py
+++ b/geopandas/tools/tests/test_sjoin.py
@@ -404,7 +404,7 @@ class TestSpatialJoinNYBB:
         df = sjoin(self.pointdf, self.polydf, how="left")
         assert df.shape == (21, 8)
         for i, row in df.iterrows():
-            assert row.geometry.type == "Point"
+            assert row.geometry.geom_type == "Point"
         assert "pointattr1" in df.columns
         assert "BoroCode" in df.columns
 
@@ -415,9 +415,9 @@ class TestSpatialJoinNYBB:
         assert df.shape == (12, 8)
         assert df.shape == df2.shape
         for i, row in df.iterrows():
-            assert row.geometry.type == "MultiPolygon"
+            assert row.geometry.geom_type == "MultiPolygon"
         for i, row in df2.iterrows():
-            assert row.geometry.type == "MultiPolygon"
+            assert row.geometry.geom_type == "MultiPolygon"
 
     def test_sjoin_inner(self):
         df = sjoin(self.pointdf, self.polydf, how="inner")
@@ -531,9 +531,9 @@ class TestSpatialJoinNYBB:
     def test_sjoin_empty_geometries(self):
         # https://github.com/geopandas/geopandas/issues/944
         empty = GeoDataFrame(geometry=[GeometryCollection()] * 3)
-        df = sjoin(self.pointdf.append(empty), self.polydf, how="left")
+        df = sjoin(pd.concat([self.pointdf, empty]), self.polydf, how="left")
         assert df.shape == (24, 8)
-        df2 = sjoin(self.pointdf, self.polydf.append(empty), how="left")
+        df2 = sjoin(self.pointdf, pd.concat([self.polydf, empty]), how="left")
         assert df2.shape == (21, 8)
 
     @pytest.mark.parametrize("predicate", ["intersects", "within", "contains"])