Repository: geopandas/geopandas
Pull request #2366 (2022-02-27 12:41:08)

ENH: Add fast rectangle clipping option to `tools.clip`
Adds fast rectangle clipping to `tools.clip` by allowing `mask` to be a 4 element tuple (minx, miny, maxx, maxy). 

This leverages the `.clip_by_rect()` method, added to `GeoPandasBase` in https://github.com/geopandas/geopandas/pull/1928. Currently this PR bases off of #1928, so it might be nice to merge it first to get a nicer diff.

bstadlbauer: Hi @martinfleis!

Thanks for merging #1928, I've rebased this PR on main and it should be ready for reviewing. 

A note on testing: As most tests use a `mask` as as an input, I've rearranged them and put them into `TestClipWithSingleRectangleGdf`. This way, I could parameterize the `mask` parameter to run all tests with a regular `GeoDataframe` (Polygon) mask as well as with a `Tuple` (rectangle) mask. 

I've moved all tests which were not parameterizable: 
- `test_clip_line_keep_slivers`
- `test_clip_multipoly_keep_slivers`
- `test_clip_with_polygon`
- `test_clip_with_multipolygon`
- `test_clip_single_multipoly_no_extra_geoms`

below this class. 

Sadly, Github does not produce a nice diff of this, but if you happen to have PyCharm, you should see that there there are actually not that many changes to this file. 

bstadlbauer: @martinfleis: Changed all the things from the comments
martinfleis: It looks great, thanks for the changes!

I have now realised that we also need to update docstrings of `GeoSeries.clip` and `GeoDataFrame.clip` methods, to keep them in sync. Apart from that, I think it is ready.

Patch:
diff --git a/.github/workflows/release_to_pypi.yml b/.github/workflows/release_to_pypi.yml
index 92b47734..a5f390c4 100644
--- a/.github/workflows/release_to_pypi.yml
+++ b/.github/workflows/release_to_pypi.yml
@@ -19,11 +19,11 @@ jobs:
         with:
           python-version: "3.x"
 
-      - name: Build a binary wheel and a source tarball
+      - name: Build source and wheel distributions
         run: |
-          python -m pip install --upgrade pip
-          pip install setuptools wheel
-          python setup.py sdist bdist_wheel
+          python -m pip install --upgrade build twine
+          python -m build
+          twine check --strict dist/*
 
       - name: Publish distribution to PyPI
         uses: pypa/gh-action-pypi-publish@master
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 0ec40998..22f14602 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,12 +1,12 @@
 files: 'geopandas\/'
 repos:
--   repo: https://github.com/python/black
-    rev: 20.8b1
-    hooks:
-    -   id: black
-        language_version: python3
--   repo: https://gitlab.com/pycqa/flake8
-    rev: 3.8.3
-    hooks:
-    -   id: flake8
-        language: python_venv
+    - repo: https://github.com/psf/black
+      rev: 22.3.0
+      hooks:
+          - id: black
+            language_version: python3
+    - repo: https://gitlab.com/pycqa/flake8
+      rev: 3.8.3
+      hooks:
+          - id: flake8
+            language: python_venv
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 40fecd6b..76bbf2fb 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -13,7 +13,6 @@ In general, GeoPandas follows the conventions of the pandas project
 where applicable. Please read the [contributing
 guidelines](https://geopandas.readthedocs.io/en/latest/community/contributing.html).
 
-
 In particular, when submitting a pull request:
 
 - Install the requirements for the development environment (one can do this
@@ -39,7 +38,7 @@ is a great way to get started if you'd like to make a contribution.
 Style
 -----
 
-- GeoPandas supports Python 3.7+ only. The last version of GeoPandas
+- GeoPandas supports Python 3.8+ only. The last version of GeoPandas
   supporting Python 2 is 0.6.
 
 - GeoPandas follows [the PEP 8
diff --git a/README.md b/README.md
index b66c11f2..3fe6cd42 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,12 @@
-GeoPandas [![Actions Status](https://github.com/geopandas/geopandas/workflows/Tests/badge.svg)](https://github.com/geopandas/geopandas/actions?query=workflow%3ATests) [![Coverage Status](https://codecov.io/gh/geopandas/geopandas/branch/main/graph/badge.svg)](https://codecov.io/gh/geopandas/geopandas) [![Join the chat at https://gitter.im/geopandas/geopandas](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/geopandas/geopandas?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Binder](https://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/geopandas/geopandas/main) [![DOI](https://zenodo.org/badge/11002815.svg)](https://zenodo.org/badge/latestdoi/11002815)
-=========
+[![pypi](https://img.shields.io/pypi/v/geopandas.svg)](https://pypi.python.org/pypi/geopandas/)
+[![Actions Status](https://github.com/geopandas/geopandas/workflows/Tests/badge.svg)](https://github.com/geopandas/geopandas/actions?query=workflow%3ATests)
+[![Coverage Status](https://codecov.io/gh/geopandas/geopandas/branch/main/graph/badge.svg)](https://codecov.io/gh/geopandas/geopandas)
+[![Join the chat at https://gitter.im/geopandas/geopandas](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/geopandas/geopandas?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[![Binder](https://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/geopandas/geopandas/main)
+[![DOI](https://zenodo.org/badge/11002815.svg)](https://zenodo.org/badge/latestdoi/11002815)
+
+GeoPandas
+---------
 
 Python tools for geographic data
 
diff --git a/ci/envs/37-minimal.yaml b/ci/envs/38-minimal.yaml
similarity index 96%
rename from ci/envs/37-minimal.yaml
rename to ci/envs/38-minimal.yaml
index 9df7b6df..b697c63c 100644
--- a/ci/envs/37-minimal.yaml
+++ b/ci/envs/38-minimal.yaml
@@ -3,7 +3,7 @@ channels:
   - defaults
   - conda-forge
 dependencies:
-  - python=3.7
+  - python=3.8
   # required
   - numpy=1.18
   - pandas==1.0.5
diff --git a/ci/envs/37-pd11.yaml b/ci/envs/38-pd11-defaults.yaml
similarity index 96%
rename from ci/envs/37-pd11.yaml
rename to ci/envs/38-pd11-defaults.yaml
index a58c7ec5..eb524c2b 100644
--- a/ci/envs/37-pd11.yaml
+++ b/ci/envs/38-pd11-defaults.yaml
@@ -2,7 +2,7 @@ name: test
 channels:
   - defaults
 dependencies:
-  - python=3.7
+  - python=3.8
   # required
   - pandas=1.1
   - shapely
diff --git a/ci/envs/38-no-optional-deps.yaml b/ci/envs/39-no-optional-deps.yaml
similarity index 92%
rename from ci/envs/38-no-optional-deps.yaml
rename to ci/envs/39-no-optional-deps.yaml
index 7261cc6f..cb6f036a 100644
--- a/ci/envs/38-no-optional-deps.yaml
+++ b/ci/envs/39-no-optional-deps.yaml
@@ -2,7 +2,7 @@ name: test
 channels:
   - conda-forge
 dependencies:
-  - python=3.8
+  - python=3.9
   # required
   - pandas
   - shapely
diff --git a/doc/source/community/contributing.rst b/doc/source/community/contributing.rst
index 21444db8..1eb9520e 100644
--- a/doc/source/community/contributing.rst
+++ b/doc/source/community/contributing.rst
@@ -44,7 +44,7 @@ In particular, when submitting a pull request:
   imports when possible, and explicit relative imports for local
   imports when necessary in tests.
 
-- GeoPandas supports Python 3.7+ only. The last version of GeoPandas
+- GeoPandas supports Python 3.8+ only. The last version of GeoPandas
   supporting Python 2 is 0.6.
 
 
diff --git a/geopandas/_compat.py b/geopandas/_compat.py
index ab6edc24..8a68cdb9 100644
--- a/geopandas/_compat.py
+++ b/geopandas/_compat.py
@@ -149,7 +149,6 @@ if shapely_warning is not None and not SHAPELY_GE_20:
             )
             yield
 
-
 elif (Version(np.__version__) >= Version("1.21")) and not SHAPELY_GE_20:
 
     @contextlib.contextmanager
@@ -162,7 +161,6 @@ elif (Version(np.__version__) >= Version("1.21")) and not SHAPELY_GE_20:
             )
             yield
 
-
 else:
 
     @contextlib.contextmanager
diff --git a/geopandas/_config.py b/geopandas/_config.py
index 99a2fca4..cdfa5578 100644
--- a/geopandas/_config.py
+++ b/geopandas/_config.py
@@ -50,7 +50,7 @@ class Options(object):
         cls = self.__class__.__name__
         description = ""
         for key, option in self._options.items():
-            descr = u"{key}: {cur!r} [default: {default!r}]\n".format(
+            descr = "{key}: {cur!r} [default: {default!r}]\n".format(
                 key=key, cur=self._config[key], default=option.default_value
             )
             description += descr
@@ -58,7 +58,7 @@ class Options(object):
             if option.doc:
                 doc_text = "\n".join(textwrap.wrap(option.doc, width=70))
             else:
-                doc_text = u"No description available."
+                doc_text = "No description available."
             doc_text = textwrap.indent(doc_text, prefix="    ")
             description += doc_text + "\n"
         space = "\n  "
diff --git a/geopandas/explore.py b/geopandas/explore.py
index 7efab95d..3c0f500c 100644
--- a/geopandas/explore.py
+++ b/geopandas/explore.py
@@ -183,6 +183,16 @@ def _explore(
             Fill color. Defaults to the value of the color option
         fillOpacity : float (default 0.5)
             Fill opacity.
+        style_function : callable
+            Function mapping a GeoJson Feature to a style ``dict``.
+
+            * Style properties :func:`folium.vector_layers.path_options`
+            * GeoJson features :class:`GeoDataFrame.__geo_interface__`
+
+            e.g.::
+
+                lambda x: {"color":"red" if x["properties"]["gdp_md_est"]<10**6
+                                             else "blue"}
 
         Plus all supported by :func:`folium.vector_layers.path_options`. See the
         documentation of :class:`folium.features.GeoJson` for details.
@@ -367,7 +377,11 @@ GON (((180.00000 -16.06713, 180.00000...
                     "Cannot specify 'categories' when column has categorical dtype"
                 )
             categorical = True
-        elif gdf[column].dtype is np.dtype("O") or categories:
+        elif (
+            gdf[column].dtype is np.dtype("O")
+            or gdf[column].dtype is np.dtype(bool)
+            or categories
+        ):
             categorical = True
 
         nan_idx = pd.isna(gdf[column])
@@ -444,6 +458,17 @@ GON (((180.00000 -16.06713, 180.00000...
         style_kwds["fillOpacity"] = 0.5
     if "weight" not in style_kwds:
         style_kwds["weight"] = 2
+    if "style_function" in style_kwds:
+        style_kwds_function = style_kwds["style_function"]
+        if not callable(style_kwds_function):
+            raise ValueError("'style_function' has to be a callable")
+        style_kwds.pop("style_function")
+    else:
+
+        def _no_style(x):
+            return {}
+
+        style_kwds_function = _no_style
 
     # specify color
     if color is not None:
@@ -454,10 +479,14 @@ GON (((180.00000 -16.06713, 180.00000...
         ):  # use existing column
 
             def _style_color(x):
-                return {
+                base_style = {
                     "fillColor": x["properties"][color],
                     **style_kwds,
                 }
+                return {
+                    **base_style,
+                    **style_kwds_function(x),
+                }
 
             style_function = _style_color
         else:  # assign new column
@@ -476,27 +505,35 @@ GON (((180.00000 -16.06713, 180.00000...
             if not stroke_color:
 
                 def _style_column(x):
-                    return {
+                    base_style = {
                         "fillColor": x["properties"]["__folium_color"],
                         "color": x["properties"]["__folium_color"],
                         **style_kwds,
                     }
+                    return {
+                        **base_style,
+                        **style_kwds_function(x),
+                    }
 
                 style_function = _style_column
             else:
 
                 def _style_stroke(x):
-                    return {
+                    base_style = {
                         "fillColor": x["properties"]["__folium_color"],
                         "color": stroke_color,
                         **style_kwds,
                     }
+                    return {
+                        **base_style,
+                        **style_kwds_function(x),
+                    }
 
                 style_function = _style_stroke
     else:  # use folium default
 
         def _style_default(x):
-            return {**style_kwds}
+            return {**style_kwds, **style_kwds_function(x)}
 
         style_function = _style_default
 
@@ -810,6 +847,7 @@ def _explore_geoseries(
     marker_kwds={},
     style_kwds={},
     highlight_kwds={},
+    map_kwds={},
     **kwargs,
 ):
     """Interactive map based on GeoPandas and folium/leaflet.js
@@ -880,6 +918,17 @@ def _explore_geoseries(
             Fill color. Defaults to the value of the color option
         fillOpacity : float (default 0.5)
             Fill opacity.
+        style_function : callable
+            Function mapping a GeoJson Feature to a style ``dict``.
+
+            * Style properties :func:`folium.vector_layers.path_options`
+            * GeoJson features :class:`GeoSeries.__geo_interface__`
+
+            e.g.::
+
+                lambda x: {"color":"red" if x["properties"]["gdp_md_est"]<10**6
+                                             else "blue"}
+
 
         Plus all supported by :func:`folium.vector_layers.path_options`. See the
         documentation of :class:`folium.features.GeoJson` for details.
@@ -887,6 +936,9 @@ def _explore_geoseries(
     highlight_kwds : dict (default {})
         Style to be passed to folium highlight_function. Uses the same keywords
         as ``style_kwds``. When empty, defaults to ``{"fillOpacity": 0.75}``.
+    map_kwds : dict (default {})
+        Additional keywords to be passed to folium :class:`~folium.folium.Map`,
+        e.g. ``dragging``, or ``scrollWheelZoom``.
 
     **kwargs : dict
         Additional options to be passed on to the folium.
@@ -911,5 +963,6 @@ def _explore_geoseries(
         marker_kwds=marker_kwds,
         style_kwds=style_kwds,
         highlight_kwds=highlight_kwds,
+        map_kwds=map_kwds,
         **kwargs,
     )
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index bfd298d4..ab92ed73 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -633,7 +633,10 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
                 "geometry": shape(feature["geometry"]) if feature["geometry"] else None
             }
             # load properties
-            row.update(feature["properties"])
+            properties = feature["properties"]
+            if properties is None:
+                properties = {}
+            row.update(properties)
             rows.append(row)
         return GeoDataFrame(rows, columns=columns, crs=crs)
 
@@ -865,7 +868,7 @@ individually so that features may have different properties
         if not self.columns.is_unique:
             raise ValueError("GeoDataFrame cannot contain duplicated column names.")
 
-        properties_cols = self.columns.difference([self._geometry_column_name])
+        properties_cols = self.columns.drop(self._geometry_column_name)
 
         if len(properties_cols) > 0:
             # convert to object to get python scalars.
@@ -1024,6 +1027,16 @@ individually so that features may have different properties
         GeoDataFrame.to_file : write GeoDataFrame to file
         """
 
+        # Accept engine keyword for compatibility with pandas.DataFrame.to_parquet
+        # The only engine currently supported by GeoPandas is pyarrow, so no
+        # other engine should be specified.
+        engine = kwargs.pop("engine", "auto")
+        if engine not in ("auto", "pyarrow"):
+            raise ValueError(
+                f"GeoPandas only supports using pyarrow as the engine for "
+                f"to_parquet: {engine!r} passed instead."
+            )
+
         from geopandas.io.arrow import _to_parquet
 
         _to_parquet(self, path, compression=compression, index=index, **kwargs)
@@ -1808,7 +1821,7 @@ individually so that features may have different properties
             - append: Insert new values to the existing table.
         schema : string, optional
             Specify the schema. If None, use default schema: 'public'.
-        index : bool, default True
+        index : bool, default False
             Write DataFrame index as a column.
             Uses *index_label* as the column name in the table.
         index_label : string or sequence, default None
@@ -2093,17 +2106,21 @@ countries_w_city_data[countries_w_city_data["name_left"] == "Italy"]
         """Clip points, lines, or polygon geometries to the mask extent.
 
         Both layers must be in the same Coordinate Reference System (CRS).
-        The GeoDataFrame will be clipped to the full extent of the `mask` object.
+        The GeoDataFrame will be clipped to the full extent of the ``mask`` object.
 
         If there are multiple polygons in mask, data from the GeoDataFrame will be
         clipped to the total boundary of all polygons in mask.
 
         Parameters
         ----------
-        mask : GeoDataFrame, GeoSeries, (Multi)Polygon
-            Polygon vector layer used to clip `gdf`.
+        mask : GeoDataFrame, GeoSeries, (Multi)Polygon, list-like
+            Polygon vector layer used to clip the GeoDataFrame.
             The mask's geometry is dissolved into one geometric feature
-            and intersected with `gdf`.
+            and intersected with GeoDataFrame.
+            If the mask is list-like with four elements ``(minx, miny, maxx, maxy)``,
+            ``clip`` will use a faster rectangle clipping
+            (:meth:`~GeoSeries.clip_by_rect`), possibly leading to slightly different
+            results.
         keep_geom_type : boolean, default False
             If True, return only geometries of original type in case of intersection
             resulting in multiple geometry types or GeometryCollections.
@@ -2112,7 +2129,7 @@ countries_w_city_data[countries_w_city_data["name_left"] == "Italy"]
         Returns
         -------
         GeoDataFrame
-            Vector data (points, lines, polygons) from `gdf` clipped to
+            Vector data (points, lines, polygons) from the GeoDataFrame clipped to
             polygon boundary from mask.
 
         See also
diff --git a/geopandas/geoseries.py b/geopandas/geoseries.py
index 395a6a8b..eb9bd8e7 100644
--- a/geopandas/geoseries.py
+++ b/geopandas/geoseries.py
@@ -1288,10 +1288,14 @@ e": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": [3
 
         Parameters
         ----------
-        mask : GeoDataFrame, GeoSeries, (Multi)Polygon
+        mask : GeoDataFrame, GeoSeries, (Multi)Polygon, list-like
             Polygon vector layer used to clip `gdf`.
             The mask's geometry is dissolved into one geometric feature
-            and intersected with `gdf`.
+            and intersected with GeoSeries.
+            If the mask is list-like with four elements ``(minx, miny, maxx, maxy)``,
+            ``clip`` will use a faster rectangle clipping
+            (:meth:`~GeoSeries.clip_by_rect`), possibly leading to slightly different
+            results.
         keep_geom_type : boolean, default False
             If True, return only geometries of original type in case of intersection
             resulting in multiple geometry types or GeometryCollections.
diff --git a/geopandas/io/sql.py b/geopandas/io/sql.py
index 17e7c404..06c00b95 100644
--- a/geopandas/io/sql.py
+++ b/geopandas/io/sql.py
@@ -242,19 +242,30 @@ def _get_srid_from_crs(gdf):
 
     # Use geoalchemy2 default for srid
     # Note: undefined srid in PostGIS is 0
-    srid = -1
+    srid = None
     warning_msg = (
         "Could not parse CRS from the GeoDataFrame. "
-        + "Inserting data without defined CRS.",
+        "Inserting data without defined CRS."
     )
     if gdf.crs is not None:
         try:
-            srid = gdf.crs.to_epsg(min_confidence=25)
-            if srid is None:
-                srid = -1
-                warnings.warn(warning_msg, UserWarning, stacklevel=2)
+            for confidence in (100, 70, 25):
+                srid = gdf.crs.to_epsg(min_confidence=confidence)
+                if srid is not None:
+                    break
+                auth_srid = gdf.crs.to_authority(
+                    auth_name="ESRI", min_confidence=confidence
+                )
+                if auth_srid is not None:
+                    srid = int(auth_srid[1])
+                    break
         except Exception:
             warnings.warn(warning_msg, UserWarning, stacklevel=2)
+
+    if srid is None:
+        srid = -1
+        warnings.warn(warning_msg, UserWarning, stacklevel=2)
+
     return srid
 
 
@@ -272,7 +283,7 @@ def _convert_linearring_to_linestring(gdf, geom_name):
 
 
 def _convert_to_ewkb(gdf, geom_name, srid):
-    """Convert geometries to ewkb. """
+    """Convert geometries to ewkb."""
     if compat.USE_PYGEOS:
         from pygeos import set_srid, to_wkb
 
diff --git a/geopandas/tools/clip.py b/geopandas/tools/clip.py
index 27ff372b..c10b12ba 100644
--- a/geopandas/tools/clip.py
+++ b/geopandas/tools/clip.py
@@ -7,16 +7,23 @@ A module to clip vector data using GeoPandas.
 """
 import warnings
 
-from shapely.geometry import Polygon, MultiPolygon
+import pandas.api.types
+from shapely.geometry import Polygon, MultiPolygon, box
 
 from geopandas import GeoDataFrame, GeoSeries
 from geopandas.array import _check_crs, _crs_mismatch_warn
 
 
-def _clip_gdf_with_polygon(gdf, poly):
-    """Clip geometry to the polygon extent.
+def _mask_is_list_like_rectangle(mask):
+    return pandas.api.types.is_list_like(mask) and not isinstance(
+        mask, (GeoDataFrame, GeoSeries, Polygon, MultiPolygon)
+    )
 
-    Clip an input GeoDataFrame to the polygon extent of the poly
+
+def _clip_gdf_with_mask(gdf, mask):
+    """Clip geometry to the polygon/rectangle extent.
+
+    Clip an input GeoDataFrame to the polygon extent of the polygon
     parameter.
 
     Parameters
@@ -24,16 +31,22 @@ def _clip_gdf_with_polygon(gdf, poly):
     gdf : GeoDataFrame, GeoSeries
         Dataframe to clip.
 
-    poly : (Multi)Polygon
-        Reference polygon for clipping.
+    mask : (Multi)Polygon, list-like
+        Reference polygon/rectangle for clipping.
 
     Returns
     -------
     GeoDataFrame
         The returned GeoDataFrame is a clipped subset of gdf
-        that intersects with poly.
+        that intersects with polygon/rectangle.
     """
-    gdf_sub = gdf.iloc[gdf.sindex.query(poly, predicate="intersects")]
+    clipping_by_rectangle = _mask_is_list_like_rectangle(mask)
+    if clipping_by_rectangle:
+        intersection_polygon = box(*mask)
+    else:
+        intersection_polygon = mask
+
+    gdf_sub = gdf.iloc[gdf.sindex.query(intersection_polygon, predicate="intersects")]
 
     # For performance reasons points don't need to be intersected with poly
     non_point_mask = gdf_sub.geom_type != "Point"
@@ -45,14 +58,25 @@ def _clip_gdf_with_polygon(gdf, poly):
     # Clip the data with the polygon
     if isinstance(gdf_sub, GeoDataFrame):
         clipped = gdf_sub.copy()
-        clipped.loc[
-            non_point_mask, clipped._geometry_column_name
-        ] = gdf_sub.geometry.values[non_point_mask].intersection(poly)
+        if clipping_by_rectangle:
+            clipped.loc[
+                non_point_mask, clipped._geometry_column_name
+            ] = gdf_sub.geometry.values[non_point_mask].clip_by_rect(*mask)
+        else:
+            clipped.loc[
+                non_point_mask, clipped._geometry_column_name
+            ] = gdf_sub.geometry.values[non_point_mask].intersection(mask)
     else:
         # GeoSeries
         clipped = gdf_sub.copy()
-        clipped[non_point_mask] = gdf_sub.values[non_point_mask].intersection(poly)
+        if clipping_by_rectangle:
+            clipped[non_point_mask] = gdf_sub.values[non_point_mask].clip_by_rect(*mask)
+        else:
+            clipped[non_point_mask] = gdf_sub.values[non_point_mask].intersection(mask)
 
+    if clipping_by_rectangle:
+        # clip_by_rect might return empty geometry collections in edge cases
+        clipped = clipped[~clipped.is_empty]
     return clipped
 
 
@@ -60,19 +84,29 @@ def clip(gdf, mask, keep_geom_type=False):
     """Clip points, lines, or polygon geometries to the mask extent.
 
     Both layers must be in the same Coordinate Reference System (CRS).
-    The `gdf` will be clipped to the full extent of the clip object.
+    The ``gdf`` will be clipped to the full extent of the clip object.
 
-    If there are multiple polygons in mask, data from `gdf` will be
+    If there are multiple polygons in mask, data from ``gdf`` will be
     clipped to the total boundary of all polygons in mask.
 
+    If the ``mask`` is list-like with four elements ``(minx, miny, maxx, maxy)``, a
+    faster rectangle clipping algorithm will be used. Note that this can lead to
+    slightly different results in edge cases, e.g. if a line would be reduced to a
+    point, this point might not be returned.
+    The geometry is clipped in a fast but possibly dirty way. The output is not
+    guaranteed to be valid. No exceptions will be raised for topological errors.
+
     Parameters
     ----------
     gdf : GeoDataFrame or GeoSeries
         Vector layer (point, line, polygon) to be clipped to mask.
-    mask : GeoDataFrame, GeoSeries, (Multi)Polygon
-        Polygon vector layer used to clip `gdf`.
+    mask : GeoDataFrame, GeoSeries, (Multi)Polygon, list-like
+        Polygon vector layer used to clip ``gdf``.
         The mask's geometry is dissolved into one geometric feature
-        and intersected with `gdf`.
+        and intersected with ``gdf``.
+        If the mask is list-like with four elements ``(minx, miny, maxx, maxy)``,
+        ``clip`` will use a faster rectangle clipping (:meth:`~GeoSeries.clip_by_rect`),
+        possibly leading to slightly different results.
     keep_geom_type : boolean, default False
         If True, return only geometries of original type in case of intersection
         resulting in multiple geometry types or GeometryCollections.
@@ -81,7 +115,7 @@ def clip(gdf, mask, keep_geom_type=False):
     Returns
     -------
     GeoDataFrame or GeoSeries
-         Vector data (points, lines, polygons) from `gdf` clipped to
+         Vector data (points, lines, polygons) from ``gdf`` clipped to
          polygon boundary from mask.
 
     See also
@@ -110,10 +144,19 @@ def clip(gdf, mask, keep_geom_type=False):
             "'gdf' should be GeoDataFrame or GeoSeries, got {}".format(type(gdf))
         )
 
-    if not isinstance(mask, (GeoDataFrame, GeoSeries, Polygon, MultiPolygon)):
+    mask_is_list_like = _mask_is_list_like_rectangle(mask)
+    if (
+        not isinstance(mask, (GeoDataFrame, GeoSeries, Polygon, MultiPolygon))
+        and not mask_is_list_like
+    ):
+        raise TypeError(
+            "'mask' should be GeoDataFrame, GeoSeries,"
+            f"(Multi)Polygon or list-like, got {type(mask)}"
+        )
+
+    if mask_is_list_like and len(mask) != 4:
         raise TypeError(
-            "'mask' should be GeoDataFrame, GeoSeries or"
-            "(Multi)Polygon, got {}".format(type(mask))
+            "If 'mask' is list-like, it must have four values (minx, miny, maxx, maxy)"
         )
 
     if isinstance(mask, (GeoDataFrame, GeoSeries)):
@@ -122,6 +165,8 @@ def clip(gdf, mask, keep_geom_type=False):
 
     if isinstance(mask, (GeoDataFrame, GeoSeries)):
         box_mask = mask.total_bounds
+    elif mask_is_list_like:
+        box_mask = mask
     else:
         box_mask = mask.bounds
     box_gdf = gdf.total_bounds
@@ -132,11 +177,11 @@ def clip(gdf, mask, keep_geom_type=False):
         return gdf.iloc[:0]
 
     if isinstance(mask, (GeoDataFrame, GeoSeries)):
-        poly = mask.geometry.unary_union
+        combined_mask = mask.geometry.unary_union
     else:
-        poly = mask
+        combined_mask = mask
 
-    clipped = _clip_gdf_with_polygon(gdf, poly)
+    clipped = _clip_gdf_with_mask(gdf, combined_mask)
 
     if keep_geom_type:
         geomcoll_concat = (clipped.geom_type == "GeometryCollection").any()
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..9787c3bd
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools", "wheel"]
+build-backend = "setuptools.build_meta"
diff --git a/setup.cfg b/setup.cfg
index 475f2ced..dedd8f1e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,3 @@
-[bdist_wheel]
-universal = 1
-
 # See the docstring in versioneer.py for instructions. Note that you must
 # re-run 'versioneer.py setup' after changing this section, and commit the
 # resulting files.
diff --git a/setup.py b/setup.py
index ca1d0ec1..d12ea2f6 100644
--- a/setup.py
+++ b/setup.py
@@ -4,13 +4,16 @@
 """
 
 import os
+import sys
 
-try:
-    from setuptools import setup
-except ImportError:
-    from distutils.core import setup
+from setuptools import setup
 
-import versioneer
+# ensure the current directory is on sys.path so versioneer can be imported
+# when pip uses PEP 517/518 build rules.
+# https://github.com/python-versioneer/python-versioneer/issues/193
+sys.path.append(os.path.dirname(__file__))
+
+import versioneer  # noqa: E402
 
 LONG_DESCRIPTION = """GeoPandas is a project to add support for geographic data to
 `pandas`_ objects.
@@ -62,6 +65,7 @@ setup(
         "Source": "https://github.com/geopandas/geopandas",
     },
     long_description=LONG_DESCRIPTION,
+    long_description_content_type="text/x-rst",
     packages=[
         "geopandas",
         "geopandas.io",
@@ -71,7 +75,7 @@ setup(
         "geopandas.tools.tests",
     ],
     package_data={"geopandas": data_files},
-    python_requires=">=3.7",
+    python_requires=">=3.8",
     install_requires=INSTALL_REQUIRES,
     cmdclass=versioneer.get_cmdclass(),
 )


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 6e5e99e9..2a0da2bc 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -17,8 +17,8 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-python@v2
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v3
       - uses: pre-commit/action@v2.0.3
 
   Test:
@@ -35,28 +35,28 @@ jobs:
         postgis: [false]
         dev: [false]
         env:
-          - ci/envs/37-minimal.yaml
-          - ci/envs/38-no-optional-deps.yaml
-          - ci/envs/37-pd11.yaml
-          - ci/envs/37-latest-defaults.yaml
-          - ci/envs/37-latest-conda-forge.yaml
+          - ci/envs/38-minimal.yaml
+          - ci/envs/39-no-optional-deps.yaml
+          - ci/envs/38-pd11-defaults.yaml
+          - ci/envs/38-latest-defaults.yaml
           - ci/envs/38-latest-conda-forge.yaml
+          - ci/envs/39-pd12-conda-forge.yaml
           - ci/envs/39-latest-conda-forge.yaml
           - ci/envs/310-latest-conda-forge.yaml
         include:
-          - env: ci/envs/37-latest-conda-forge.yaml
+          - env: ci/envs/38-latest-conda-forge.yaml
             os: macos-latest
             postgis: false
             dev: false
-          - env: ci/envs/38-latest-conda-forge.yaml
+          - env: ci/envs/39-latest-conda-forge.yaml
             os: macos-latest
             postgis: false
             dev: false
-          - env: ci/envs/37-latest-conda-forge.yaml
+          - env: ci/envs/38-latest-conda-forge.yaml
             os: windows-latest
             postgis: false
             dev: false
-          - env: ci/envs/38-latest-conda-forge.yaml
+          - env: ci/envs/39-latest-conda-forge.yaml
             os: windows-latest
             postgis: false
             dev: false
@@ -65,7 +65,7 @@ jobs:
             dev: true
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
 
       - name: Setup Conda
         uses: conda-incubator/setup-miniconda@v2
@@ -106,7 +106,7 @@ jobs:
           pytest -v -r s -n auto --color=yes --cov=geopandas --cov-append --cov-report term-missing --cov-report xml geopandas/
 
       - name: Test with PostGIS
-        if: contains(matrix.env, '38-latest-conda-forge.yaml') && contains(matrix.os, 'ubuntu')
+        if: contains(matrix.env, '39-pd12-conda-forge.yaml') && contains(matrix.os, 'ubuntu')
         env:
           PGUSER: postgres
           PGPASSWORD: postgres
@@ -118,7 +118,7 @@ jobs:
           pytest -v -r s --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, '38-latest-conda-forge.yaml') && contains(matrix.os, 'ubuntu')
+        if: contains(matrix.env, '39-pd12-conda-forge.yaml') && contains(matrix.os, 'ubuntu')
         env:
           USE_PYGEOS: 1
         run: |
diff --git a/ci/envs/38-latest-conda-forge.yaml b/ci/envs/38-latest-conda-forge.yaml
index 6d2348f1..c6ea7126 100644
--- a/ci/envs/38-latest-conda-forge.yaml
+++ b/ci/envs/38-latest-conda-forge.yaml
@@ -4,7 +4,7 @@ channels:
 dependencies:
   - python=3.8
   # required
-  - pandas=1.2
+  - pandas
   - shapely
   - fiona
   - pyproj
@@ -23,12 +23,6 @@ dependencies:
   - xyzservices
   - scipy
   - geopy
-  # installed in tests.yaml, because not available on windows
-  # - postgis
   - SQLalchemy
-  - psycopg2
   - libspatialite
-  - geoalchemy2
   - pyarrow
-  # doctest testing
-  - pytest-doctestplus
diff --git a/ci/envs/37-latest-defaults.yaml b/ci/envs/38-latest-defaults.yaml
similarity index 96%
rename from ci/envs/37-latest-defaults.yaml
rename to ci/envs/38-latest-defaults.yaml
index 071d3eac..a4c4ef7e 100644
--- a/ci/envs/37-latest-defaults.yaml
+++ b/ci/envs/38-latest-defaults.yaml
@@ -2,7 +2,7 @@ name: test
 channels:
   - defaults
 dependencies:
-  - python=3.7
+  - python=3.8
   # required
   - pandas
   - shapely
diff --git a/ci/envs/37-latest-conda-forge.yaml b/ci/envs/39-pd12-conda-forge.yaml
similarity index 65%
rename from ci/envs/37-latest-conda-forge.yaml
rename to ci/envs/39-pd12-conda-forge.yaml
index dadd2c40..fc675bf9 100644
--- a/ci/envs/37-latest-conda-forge.yaml
+++ b/ci/envs/39-pd12-conda-forge.yaml
@@ -2,9 +2,9 @@ name: test
 channels:
   - conda-forge
 dependencies:
-  - python=3.7
+  - python=3.9
   # required
-  - pandas
+  - pandas=1.2
   - shapely
   - fiona
   - pyproj
@@ -23,6 +23,12 @@ dependencies:
   - xyzservices
   - scipy
   - geopy
+  # installed in tests.yaml, because not available on windows
+  # - postgis
   - SQLalchemy
+  - psycopg2
   - libspatialite
+  - geoalchemy2
   - pyarrow
+  # doctest testing
+  - pytest-doctestplus
diff --git a/geopandas/io/tests/generate_legacy_storage_files.py b/geopandas/io/tests/generate_legacy_storage_files.py
index cc39273e..9cf4a0cc 100644
--- a/geopandas/io/tests/generate_legacy_storage_files.py
+++ b/geopandas/io/tests/generate_legacy_storage_files.py
@@ -31,7 +31,7 @@ from shapely.geometry import Point
 
 
 def create_pickle_data():
-    """ create the pickle data """
+    """create the pickle data"""
 
     # custom geometry column name
     gdf_the_geom = geopandas.GeoDataFrame(
diff --git a/geopandas/io/tests/test_arrow.py b/geopandas/io/tests/test_arrow.py
index 1e701581..4c8d82c5 100644
--- a/geopandas/io/tests/test_arrow.py
+++ b/geopandas/io/tests/test_arrow.py
@@ -7,7 +7,7 @@ import pytest
 from pandas import DataFrame, read_parquet as pd_read_parquet
 from pandas.testing import assert_frame_equal
 import numpy as np
-from shapely.geometry import box
+from shapely.geometry import box, Point
 
 import geopandas
 from geopandas import GeoDataFrame, read_file, read_parquet, read_feather
@@ -23,6 +23,7 @@ from geopandas.io.arrow import (
     METADATA_VERSION,
 )
 from geopandas.testing import assert_geodataframe_equal, assert_geoseries_equal
+from geopandas.tests.util import mock
 
 
 # Skip all tests in this module if pyarrow is not available
@@ -172,6 +173,28 @@ def test_validate_metadata_invalid(metadata, error):
         _validate_metadata(metadata)
 
 
+def test_to_parquet_fails_on_invalid_engine(tmpdir):
+    df = GeoDataFrame(data=[[1, 2, 3]], columns=["a", "b", "a"], geometry=[Point(1, 1)])
+
+    with pytest.raises(
+        ValueError,
+        match=(
+            "GeoPandas only supports using pyarrow as the engine for "
+            "to_parquet: 'fastparquet' passed instead."
+        ),
+    ):
+        df.to_parquet(tmpdir / "test.parquet", engine="fastparquet")
+
+
+@mock.patch("geopandas.io.arrow._to_parquet")
+def test_to_parquet_does_not_pass_engine_along(mock_to_parquet):
+    df = GeoDataFrame(data=[[1, 2, 3]], columns=["a", "b", "a"], geometry=[Point(1, 1)])
+    df.to_parquet("", engine="pyarrow")
+    # assert that engine keyword is not passed through to _to_parquet (and thus
+    # parquet.write_table)
+    mock_to_parquet.assert_called_with(df, "", compression="snappy", index=None)
+
+
 # TEMPORARY: used to determine if pyarrow fails for roundtripping pandas data
 # without geometries
 def test_pandas_parquet_roundtrip1(tmpdir):
diff --git a/geopandas/io/tests/test_sql.py b/geopandas/io/tests/test_sql.py
index 33764477..62d832fd 100644
--- a/geopandas/io/tests/test_sql.py
+++ b/geopandas/io/tests/test_sql.py
@@ -458,6 +458,26 @@ class TestIO:
         ).fetchone()[0]
         assert target_srid == 0, "SRID should be 0, found %s" % target_srid
 
+    def test_write_postgis_with_esri_authority(self, engine_postgis, df_nybb):
+        """
+        Tests that GeoDataFrame can be written to PostGIS with ESRI Authority
+        CRS information (GH #2414).
+        """
+        engine = engine_postgis
+
+        table = "nybb"
+
+        # Write to db
+        df_nybb_esri = df_nybb.to_crs("ESRI:102003")
+        write_postgis(df_nybb_esri, con=engine, name=table, if_exists="replace")
+        # Validate that srid is 102003
+        target_srid = engine.execute(
+            "SELECT Find_SRID('{schema}', '{table}', '{geom_col}');".format(
+                schema="public", table=table, geom_col="geometry"
+            )
+        ).fetchone()[0]
+        assert target_srid == 102003, "SRID should be 102003, found %s" % target_srid
+
     def test_write_postgis_geometry_collection(
         self, engine_postgis, df_geom_collection
     ):
diff --git a/geopandas/tests/test_crs.py b/geopandas/tests/test_crs.py
index 04a4270a..93eb6b4b 100644
--- a/geopandas/tests/test_crs.py
+++ b/geopandas/tests/test_crs.py
@@ -1,5 +1,4 @@
 from packaging.version import Version
-import os
 
 import random
 
@@ -19,6 +18,7 @@ import pytest
 # pyproj 2.3.1 fixed a segfault for the case working in an environment with
 # 'init' dicts (https://github.com/pyproj4/pyproj/issues/415)
 PYPROJ_LT_231 = Version(pyproj.__version__) < Version("2.3.1")
+PYPROJ_GE_3 = Version(pyproj.__version__) >= Version("3.0.0")
 
 
 def _create_df(x, y=None, crs=None):
@@ -125,7 +125,9 @@ def test_transform2(epsg4326, epsg26918):
     # with PROJ >= 7, the transformation using EPSG code vs proj4 string is
     # slightly different due to use of grid files or not -> turn off network
     # to not use grid files at all for this test
-    os.environ["PROJ_NETWORK"] = "OFF"
+    if PYPROJ_GE_3:
+        pyproj.network.set_network_enabled(False)
+
     df = df_epsg26918()
     lonlat = df.to_crs(**epsg4326)
     utm = lonlat.to_crs(**epsg26918)
diff --git a/geopandas/tests/test_explore.py b/geopandas/tests/test_explore.py
index 73917a32..51493121 100644
--- a/geopandas/tests/test_explore.py
+++ b/geopandas/tests/test_explore.py
@@ -253,6 +253,14 @@ class TestExplore:
         with pytest.raises(ValueError, match="Cannot specify 'categories'"):
             df.explore("categorical", categories=["Brooklyn", "Staten Island"])
 
+    def test_bool(self):
+        df = self.nybb.copy()
+        df["bool"] = [True, False, True, False, True]
+        m = df.explore("bool")
+        out_str = self._fetch_map_string(m)
+        assert '"__folium_color":"#9edae5","bool":true' in out_str
+        assert '"__folium_color":"#1f77b4","bool":false' in out_str
+
     def test_column_values(self):
         """
         Check that the dataframe plot method returns same values with an
@@ -309,6 +317,37 @@ class TestExplore:
         m = self.world.explore(column="pop_est", style_kwds=dict(color="black"))
         assert '"color":"black"' in self._fetch_map_string(m)
 
+        # custom style_function - geopandas/issues/2350
+        m = self.world.explore(
+            style_kwds={
+                "style_function": lambda x: {
+                    "fillColor": "red"
+                    if x["properties"]["gdp_md_est"] < 10**6
+                    else "green",
+                    "color": "black"
+                    if x["properties"]["gdp_md_est"] < 10**6
+                    else "white",
+                }
+            }
+        )
+        # two lines with formatting instructions from style_function.
+        # make sure each passes test
+        assert all(
+            [
+                ('"fillColor":"green"' in t and '"color":"white"' in t)
+                or ('"fillColor":"red"' in t and '"color":"black"' in t)
+                for t in [
+                    "".join(line.split())
+                    for line in m._parent.render().split("\n")
+                    if "return" in line and "color" in line
+                ]
+            ]
+        )
+
+        # style function has to be callable
+        with pytest.raises(ValueError, match="'style_function' has to be a callable"):
+            self.world.explore(style_kwds={"style_function": "not callable"})
+
     def test_tooltip(self):
         """Test tooltip"""
         # default with no tooltip or popup
diff --git a/geopandas/tests/test_geodataframe.py b/geopandas/tests/test_geodataframe.py
index a5ec71bb..59f93592 100644
--- a/geopandas/tests/test_geodataframe.py
+++ b/geopandas/tests/test_geodataframe.py
@@ -619,7 +619,7 @@ class TestDataFrame:
         p3 = Point(3, 3)
         f3 = {
             "type": "Feature",
-            "properties": {"a": 2},
+            "properties": None,
             "geometry": p3.__geo_interface__,
         }
 
@@ -627,10 +627,103 @@ class TestDataFrame:
 
         result = df[["a", "b"]]
         expected = pd.DataFrame.from_dict(
-            [{"a": 0, "b": np.nan}, {"a": np.nan, "b": 1}, {"a": 2, "b": np.nan}]
+            [{"a": 0, "b": np.nan}, {"a": np.nan, "b": 1}, {"a": np.nan, "b": np.nan}]
         )
         assert_frame_equal(expected, result)
 
+    def test_from_features_empty_properties(self):
+        geojson_properties_object = """{
+          "type": "FeatureCollection",
+          "features": [
+            {
+              "type": "Feature",
+              "properties": {},
+              "geometry": {
+                "type": "Polygon",
+                "coordinates": [
+                  [
+                    [
+                      11.3456529378891,
+                      46.49461446367692
+                    ],
+                    [
+                      11.345674395561216,
+                      46.494097442978195
+                    ],
+                    [
+                      11.346918940544128,
+                      46.49385370294394
+                    ],
+                    [
+                      11.347616314888,
+                      46.4938352377453
+                    ],
+                    [
+                      11.347514390945435,
+                      46.49466985846028
+                    ],
+                    [
+                      11.3456529378891,
+                      46.49461446367692
+                    ]
+                  ]
+                ]
+              }
+            }
+          ]
+        }"""
+
+        geojson_properties_null = """{
+          "type": "FeatureCollection",
+          "features": [
+            {
+              "type": "Feature",
+              "properties": null,
+              "geometry": {
+                "type": "Polygon",
+                "coordinates": [
+                  [
+                    [
+                      11.3456529378891,
+                      46.49461446367692
+                    ],
+                    [
+                      11.345674395561216,
+                      46.494097442978195
+                    ],
+                    [
+                      11.346918940544128,
+                      46.49385370294394
+                    ],
+                    [
+                      11.347616314888,
+                      46.4938352377453
+                    ],
+                    [
+                      11.347514390945435,
+                      46.49466985846028
+                    ],
+                    [
+                      11.3456529378891,
+                      46.49461446367692
+                    ]
+                  ]
+                ]
+              }
+            }
+          ]
+        }"""
+
+        # geoJSON with empty properties
+        gjson_po = json.loads(geojson_properties_object)
+        gdf1 = GeoDataFrame.from_features(gjson_po)
+
+        # geoJSON with null properties
+        gjson_null = json.loads(geojson_properties_null)
+        gdf2 = GeoDataFrame.from_features(gjson_null)
+
+        assert_frame_equal(gdf1, gdf2)
+
     def test_from_features_geom_interface_feature(self):
         class Placemark(object):
             def __init__(self, geom, val):
@@ -659,7 +752,7 @@ class TestDataFrame:
         geometry = [Point(xy) for xy in zip(df["lon"], df["lat"])]
         gdf = GeoDataFrame(df, geometry=geometry)
         # from_features returns sorted columns
-        expected = gdf[["geometry", "lat", "lon", "name"]]
+        expected = gdf[["geometry", "name", "lat", "lon"]]
 
         # test FeatureCollection
         res = GeoDataFrame.from_features(gdf.__geo_interface__)
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
index 05e3d5c1..705b5eb1 100644
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -490,7 +490,7 @@ class TestGeomMethods:
         )
         assert_array_dtype_equal(expected, self.na_none.distance(self.p0))
 
-        expected = Series(np.array([np.sqrt(4 ** 2 + 4 ** 2), np.nan]), self.g6.index)
+        expected = Series(np.array([np.sqrt(4**2 + 4**2), np.nan]), self.g6.index)
         assert_array_dtype_equal(expected, self.g6.distance(self.na_none))
 
         expected = Series(np.array([np.nan, 0, 0, 0, 0, 0, np.nan, np.nan]), range(8))
diff --git a/geopandas/tests/test_op_output_types.py b/geopandas/tests/test_op_output_types.py
index 5e9b5d96..dd25ff91 100644
--- a/geopandas/tests/test_op_output_types.py
+++ b/geopandas/tests/test_op_output_types.py
@@ -88,8 +88,8 @@ def test_getitem(df):
     assert_object(df[["value1", "value2"]], pd.DataFrame)
     assert_object(df[[geo_name, "geometry2"]], GeoDataFrame, geo_name)
     assert_object(df[[geo_name]], GeoDataFrame, geo_name)
-    assert_object(df[["geometry2", "value1"]], GeoDataFrame, None, None)
-    assert_object(df[["geometry2"]], GeoDataFrame, None, None)
+    assert_object(df[["geometry2", "value1"]], GeoDataFrame, None)
+    assert_object(df[["geometry2"]], GeoDataFrame, None)
     assert_object(df[["value1"]], pd.DataFrame)
     # Series
     assert_object(df[geo_name], GeoSeries, geo_name)
@@ -102,12 +102,8 @@ def test_loc(df):
     assert_object(df.loc[:, ["value1", "value2"]], pd.DataFrame)
     assert_object(df.loc[:, [geo_name, "geometry2"]], GeoDataFrame, geo_name)
     assert_object(df.loc[:, [geo_name]], GeoDataFrame, geo_name)
-    # TODO: should this set the geometry column to geometry2 or return a DataFrame?
     assert_object(df.loc[:, ["geometry2", "value1"]], GeoDataFrame, None)
     assert_object(df.loc[:, ["geometry2"]], GeoDataFrame, None)
-    # Ideally this would mirror __getitem__ and below would be true
-    # assert_object(df.loc[:, ["geometry2", "value1"]], pd.DataFrame)
-    # assert_object(df.loc[:, ["geometry2"]], pd.DataFrame)
     assert_object(df.loc[:, ["value1"]], pd.DataFrame)
     # Series
     assert_object(df.loc[:, geo_name], GeoSeries, geo_name)
@@ -120,12 +116,8 @@ def test_iloc(df):
     assert_object(df.iloc[:, 0:2], pd.DataFrame)
     assert_object(df.iloc[:, 2:4], GeoDataFrame, geo_name)
     assert_object(df.iloc[:, [2]], GeoDataFrame, geo_name)
-    # TODO: should this set the geometry column to geometry2 or return a DataFrame?
     assert_object(df.iloc[:, [3, 0]], GeoDataFrame, None)
     assert_object(df.iloc[:, [3]], GeoDataFrame, None)
-    # Ideally this would mirror __getitem__ and below would be true
-    # assert_object(df.iloc[:, [3, 0]], pd.DataFrame)
-    # assert_object(df.iloc[:, [3]], pd.DataFrame)
     assert_object(df.iloc[:, [0]], pd.DataFrame)
     # Series
     assert_object(df.iloc[:, 2], GeoSeries, geo_name)
@@ -157,12 +149,8 @@ def test_reindex(df):
     assert_object(df.reindex(columns=[geo_name, "geometry2"]), GeoDataFrame, geo_name)
     assert_object(df.reindex(columns=[geo_name]), GeoDataFrame, geo_name)
     assert_object(df.reindex(columns=["new_col", geo_name]), GeoDataFrame, geo_name)
-    # TODO: should this set the geometry column to geometry2 or return a DataFrame?
     assert_object(df.reindex(columns=["geometry2", "value1"]), GeoDataFrame, None)
     assert_object(df.reindex(columns=["geometry2"]), GeoDataFrame, None)
-    # Ideally this would mirror __getitem__ and below would be true
-    # assert_object(df.reindex(columns=["geometry2", "value1"]), pd.DataFrame)
-    # assert_object(df.reindex(columns=["geometry2"]), pd.DataFrame)
     assert_object(df.reindex(columns=["value1"]), pd.DataFrame)
 
     # reindexing the rows always preserves the GeoDataFrame
@@ -181,12 +169,8 @@ def test_drop(df):
     assert_object(df.drop(columns=["value1", "value2"]), GeoDataFrame, geo_name)
     cols = ["value1", "value2", "geometry2"]
     assert_object(df.drop(columns=cols), GeoDataFrame, geo_name)
-    # TODO: should this set the geometry column to geometry2 or return a DataFrame?
     assert_object(df.drop(columns=[geo_name, "value2"]), GeoDataFrame, None)
     assert_object(df.drop(columns=["value1", "value2", geo_name]), GeoDataFrame, None)
-    # Ideally this would mirror __getitem__ and below would be true
-    # assert_object(df.drop(columns=[geo_name, "value2"]), pd.DataFrame)
-    # assert_object(df.drop(columns=["value1", "value2", geo_name]), pd.DataFrame)
     assert_object(df.drop(columns=["geometry2", "value2", geo_name]), pd.DataFrame)
 
 
@@ -226,7 +210,6 @@ def test_apply(df):
     assert_object(df[["geometry2", "value1"]].apply(identity, axis=1), pd.DataFrame)
 
     assert_object(df[["value1"]].apply(identity, axis=1), pd.DataFrame)
-    # if compat # https://github.com/pandas-dev/pandas/pull/30091
 
 
 def test_apply_axis1_secondary_geo_cols(df):
diff --git a/geopandas/tests/test_overlay.py b/geopandas/tests/test_overlay.py
index 14ea914a..166e9da5 100644
--- a/geopandas/tests/test_overlay.py
+++ b/geopandas/tests/test_overlay.py
@@ -14,6 +14,7 @@ from geopandas import _compat
 from geopandas.testing import assert_geodataframe_equal, assert_geoseries_equal
 import pytest
 
+
 DATA = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data", "overlay")
 
 
@@ -795,3 +796,79 @@ def test_no_intersection():
     expected = GeoDataFrame(columns=["foo", "bar", "geometry"])
     result = overlay(gdf1, gdf2, how="intersection")
     assert_geodataframe_equal(result, expected, check_index_type=False)
+
+
+class TestOverlayWikiExample:
+    def setup_method(self):
+        self.layer_a = GeoDataFrame(geometry=[box(0, 2, 6, 6)])
+
+        self.layer_b = GeoDataFrame(geometry=[box(4, 0, 10, 4)])
+
+        self.intersection = GeoDataFrame(geometry=[box(4, 2, 6, 4)])
+
+        self.union = GeoDataFrame(
+            geometry=[
+                box(4, 2, 6, 4),
+                Polygon([(4, 2), (0, 2), (0, 6), (6, 6), (6, 4), (4, 4), (4, 2)]),
+                Polygon([(10, 0), (4, 0), (4, 2), (6, 2), (6, 4), (10, 4), (10, 0)]),
+            ]
+        )
+
+        self.a_difference_b = GeoDataFrame(
+            geometry=[Polygon([(4, 2), (0, 2), (0, 6), (6, 6), (6, 4), (4, 4), (4, 2)])]
+        )
+
+        self.b_difference_a = GeoDataFrame(
+            geometry=[
+                Polygon([(10, 0), (4, 0), (4, 2), (6, 2), (6, 4), (10, 4), (10, 0)])
+            ]
+        )
+
+        self.symmetric_difference = GeoDataFrame(
+            geometry=[
+                Polygon([(4, 2), (0, 2), (0, 6), (6, 6), (6, 4), (4, 4), (4, 2)]),
+                Polygon([(10, 0), (4, 0), (4, 2), (6, 2), (6, 4), (10, 4), (10, 0)]),
+            ]
+        )
+
+        self.a_identity_b = GeoDataFrame(
+            geometry=[
+                box(4, 2, 6, 4),
+                Polygon([(4, 2), (0, 2), (0, 6), (6, 6), (6, 4), (4, 4), (4, 2)]),
+            ]
+        )
+
+        self.b_identity_a = GeoDataFrame(
+            geometry=[
+                box(4, 2, 6, 4),
+                Polygon([(10, 0), (4, 0), (4, 2), (6, 2), (6, 4), (10, 4), (10, 0)]),
+            ]
+        )
+
+    def test_intersection(self):
+        df_result = overlay(self.layer_a, self.layer_b, how="intersection")
+        assert df_result.geom_equals(self.intersection).bool()
+
+    def test_union(self):
+        df_result = overlay(self.layer_a, self.layer_b, how="union")
+        assert_geodataframe_equal(df_result, self.union)
+
+    def test_a_difference_b(self):
+        df_result = overlay(self.layer_a, self.layer_b, how="difference")
+        assert_geodataframe_equal(df_result, self.a_difference_b)
+
+    def test_b_difference_a(self):
+        df_result = overlay(self.layer_b, self.layer_a, how="difference")
+        assert_geodataframe_equal(df_result, self.b_difference_a)
+
+    def test_symmetric_difference(self):
+        df_result = overlay(self.layer_a, self.layer_b, how="symmetric_difference")
+        assert_geodataframe_equal(df_result, self.symmetric_difference)
+
+    def test_a_identity_b(self):
+        df_result = overlay(self.layer_a, self.layer_b, how="identity")
+        assert_geodataframe_equal(df_result, self.a_identity_b)
+
+    def test_b_identity_a(self):
+        df_result = overlay(self.layer_b, self.layer_a, how="identity")
+        assert_geodataframe_equal(df_result, self.b_identity_a)
diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py
index 39cf3de4..368a0440 100644
--- a/geopandas/tests/test_plotting.py
+++ b/geopandas/tests/test_plotting.py
@@ -1090,9 +1090,9 @@ class TestMapclassifyPlotting:
             )
         labels = [t.get_text() for t in ax.get_legend().get_texts()]
         expected = [
-            u"       140.00,    5217064.00",
-            u"   5217064.00,   19532732.33",
-            u"  19532732.33, 1379302771.00",
+            "       140.00,    5217064.00",
+            "   5217064.00,   19532732.33",
+            "  19532732.33, 1379302771.00",
         ]
         assert labels == expected
 
@@ -1125,7 +1125,7 @@ class TestMapclassifyPlotting:
             column="NEGATIVES", scheme="FISHER_JENKS", k=3, cmap="OrRd", legend=True
         )
         labels = [t.get_text() for t in ax.get_legend().get_texts()]
-        expected = [u"-10.00,  -3.41", u" -3.41,   3.30", u"  3.30,  10.00"]
+        expected = ["-10.00,  -3.41", " -3.41,   3.30", "  3.30,  10.00"]
         assert labels == expected
 
     def test_fmt(self):
@@ -1138,7 +1138,7 @@ class TestMapclassifyPlotting:
             legend_kwds={"fmt": "{:.0f}"},
         )
         labels = [t.get_text() for t in ax.get_legend().get_texts()]
-        expected = [u"-10,  -3", u" -3,   3", u"  3,  10"]
+        expected = ["-10,  -3", " -3,   3", "  3,  10"]
         assert labels == expected
 
     def test_interval(self):
@@ -1151,7 +1151,7 @@ class TestMapclassifyPlotting:
             legend_kwds={"interval": True},
         )
         labels = [t.get_text() for t in ax.get_legend().get_texts()]
-        expected = [u"[-10.00,  -3.41]", u"( -3.41,   3.30]", u"(  3.30,  10.00]"]
+        expected = ["[-10.00,  -3.41]", "( -3.41,   3.30]", "(  3.30,  10.00]"]
         assert labels == expected
 
     @pytest.mark.parametrize("scheme", ["FISHER_JENKS", "FISHERJENKS"])
diff --git a/geopandas/tests/test_sindex.py b/geopandas/tests/test_sindex.py
index 0b53a8d4..c36c77d1 100644
--- a/geopandas/tests/test_sindex.py
+++ b/geopandas/tests/test_sindex.py
@@ -174,6 +174,31 @@ class TestFrameSindex:
         subset2 = self.df[["A", "geom"]]
         assert subset2.sindex is original_index
 
+    def test_rebuild_on_update_inplace(self):
+        gdf = self.df.copy()
+        old_sindex = gdf.sindex
+        # sorting in place
+        gdf.sort_values("A", ascending=False, inplace=True)
+        # spatial index should be invalidated
+        assert not gdf.has_sindex
+        new_sindex = gdf.sindex
+        # and should be different
+        assert new_sindex is not old_sindex
+
+        # sorting should still have happened though
+        assert gdf.index.tolist() == [4, 3, 2, 1, 0]
+
+    @pytest.mark.skipif(not compat.PANDAS_GE_11, reason="fails on pd<1.1.0")
+    def test_update_inplace_no_rebuild(self):
+        gdf = self.df.copy()
+        old_sindex = gdf.sindex
+        gdf.rename(columns={"A": "AA"}, inplace=True)
+        # a rename shouldn't invalidate the index
+        assert gdf.has_sindex
+        # and the "new" should be the same
+        new_sindex = gdf.sindex
+        assert old_sindex is new_sindex
+
 
 # Skip to accommodate Shapely geometries being unhashable
 @pytest.mark.skip
diff --git a/geopandas/tools/tests/test_clip.py b/geopandas/tools/tests/test_clip.py
index 80767478..c643d5ac 100644
--- a/geopandas/tools/tests/test_clip.py
+++ b/geopandas/tools/tests/test_clip.py
@@ -14,6 +14,7 @@ from shapely.geometry import (
     LinearRing,
     GeometryCollection,
     MultiPoint,
+    box,
 )
 
 import geopandas
@@ -22,9 +23,20 @@ from geopandas import GeoDataFrame, GeoSeries, clip
 from geopandas.testing import assert_geodataframe_equal, assert_geoseries_equal
 import pytest
 
+from geopandas.tools.clip import _mask_is_list_like_rectangle
 
 pytestmark = pytest.mark.skip_no_sindex
 pandas_133 = Version(pd.__version__) == Version("1.3.3")
+mask_variants_single_rectangle = [
+    "single_rectangle_gdf",
+    "single_rectangle_gdf_list_bounds",
+    "single_rectangle_gdf_tuple_bounds",
+    "single_rectangle_gdf_array_bounds",
+]
+mask_variants_large_rectangle = [
+    "larger_single_rectangle_gdf",
+    "larger_single_rectangle_gdf_bounds",
+]
 
 
 @pytest.fixture
@@ -62,6 +74,24 @@ def single_rectangle_gdf():
     return gdf
 
 
+@pytest.fixture
+def single_rectangle_gdf_tuple_bounds(single_rectangle_gdf):
+    """Bounds of the created single rectangle"""
+    return tuple(single_rectangle_gdf.total_bounds)
+
+
+@pytest.fixture
+def single_rectangle_gdf_list_bounds(single_rectangle_gdf):
+    """Bounds of the created single rectangle"""
+    return list(single_rectangle_gdf.total_bounds)
+
+
+@pytest.fixture
+def single_rectangle_gdf_array_bounds(single_rectangle_gdf):
+    """Bounds of the created single rectangle"""
+    return single_rectangle_gdf.total_bounds
+
+
 @pytest.fixture
 def larger_single_rectangle_gdf():
     """Create a slightly larger rectangle for clipping.
@@ -75,6 +105,12 @@ def larger_single_rectangle_gdf():
     return gdf
 
 
+@pytest.fixture
+def larger_single_rectangle_gdf_bounds(larger_single_rectangle_gdf):
+    """Bounds of the created single rectangle"""
+    return tuple(larger_single_rectangle_gdf.total_bounds)
+
+
 @pytest.fixture
 def buffered_locations(point_gdf):
     """Buffer points to create a multi-polygon."""
@@ -174,19 +210,11 @@ def test_not_gdf(single_rectangle_gdf):
     with pytest.raises(TypeError):
         clip((2, 3), single_rectangle_gdf)
     with pytest.raises(TypeError):
-        clip(single_rectangle_gdf, (2, 3))
-
-
-def test_returns_gdf(point_gdf, single_rectangle_gdf):
-    """Test that function returns a GeoDataFrame (or GDF-like) object."""
-    out = clip(point_gdf, single_rectangle_gdf)
-    assert isinstance(out, GeoDataFrame)
-
-
-def test_returns_series(point_gdf, single_rectangle_gdf):
-    """Test that function returns a GeoSeries if GeoSeries is passed."""
-    out = clip(point_gdf.geometry, single_rectangle_gdf)
-    assert isinstance(out, GeoSeries)
+        clip(single_rectangle_gdf, "foobar")
+    with pytest.raises(TypeError):
+        clip(single_rectangle_gdf, (1, 2, 3))
+    with pytest.raises(TypeError):
+        clip(single_rectangle_gdf, (1, 2, 3, 4, 5))
 
 
 def test_non_overlapping_geoms():
@@ -203,50 +231,175 @@ def test_non_overlapping_geoms():
     assert_geoseries_equal(out2, GeoSeries(crs=unit_gdf.crs))
 
 
-def test_clip_points(point_gdf, single_rectangle_gdf):
-    """Test clipping a points GDF with a generic polygon geometry."""
-    clip_pts = clip(point_gdf, single_rectangle_gdf)
-    pts = np.array([[2, 2], [3, 4], [9, 8]])
-    exp = GeoDataFrame([Point(xy) for xy in pts], columns=["geometry"], crs="EPSG:3857")
-    assert_geodataframe_equal(clip_pts, exp)
-
-
-def test_clip_points_geom_col_rename(point_gdf, single_rectangle_gdf):
-    """Test clipping a points GDF with a generic polygon geometry."""
-    point_gdf_geom_col_rename = point_gdf.rename_geometry("geometry2")
-    clip_pts = clip(point_gdf_geom_col_rename, single_rectangle_gdf)
-    pts = np.array([[2, 2], [3, 4], [9, 8]])
-    exp = GeoDataFrame(
-        [Point(xy) for xy in pts],
-        columns=["geometry2"],
-        crs="EPSG:3857",
-        geometry="geometry2",
-    )
-    assert_geodataframe_equal(clip_pts, exp)
-
-
-def test_clip_poly(buffered_locations, single_rectangle_gdf):
-    """Test clipping a polygon GDF with a generic polygon geometry."""
-    clipped_poly = clip(buffered_locations, single_rectangle_gdf)
-    assert len(clipped_poly.geometry) == 3
-    assert all(clipped_poly.geom_type == "Polygon")
-
-
-def test_clip_poly_geom_col_rename(buffered_locations, single_rectangle_gdf):
-    """Test clipping a polygon GDF with a generic polygon geometry."""
-
-    poly_gdf_geom_col_rename = buffered_locations.rename_geometry("geometry2")
-    clipped_poly = clip(poly_gdf_geom_col_rename, single_rectangle_gdf)
-    assert len(clipped_poly.geometry) == 3
-    assert "geometry" not in clipped_poly.keys()
-    assert "geometry2" in clipped_poly.keys()
+@pytest.mark.parametrize("mask_fixture_name", mask_variants_single_rectangle)
+class TestClipWithSingleRectangleGdf:
+    @pytest.fixture
+    def mask(self, mask_fixture_name, request):
+        return request.getfixturevalue(mask_fixture_name)
+
+    def test_returns_gdf(self, point_gdf, mask):
+        """Test that function returns a GeoDataFrame (or GDF-like) object."""
+        out = clip(point_gdf, mask)
+        assert isinstance(out, GeoDataFrame)
+
+    def test_returns_series(self, point_gdf, mask):
+        """Test that function returns a GeoSeries if GeoSeries is passed."""
+        out = clip(point_gdf.geometry, mask)
+        assert isinstance(out, GeoSeries)
+
+    def test_clip_points(self, point_gdf, mask):
+        """Test clipping a points GDF with a generic polygon geometry."""
+        clip_pts = clip(point_gdf, mask)
+        pts = np.array([[2, 2], [3, 4], [9, 8]])
+        exp = GeoDataFrame(
+            [Point(xy) for xy in pts], columns=["geometry"], crs="EPSG:3857"
+        )
+        assert_geodataframe_equal(clip_pts, exp)
+
+    def test_clip_points_geom_col_rename(self, point_gdf, mask):
+        """Test clipping a points GDF with a generic polygon geometry."""
+        point_gdf_geom_col_rename = point_gdf.rename_geometry("geometry2")
+        clip_pts = clip(point_gdf_geom_col_rename, mask)
+        pts = np.array([[2, 2], [3, 4], [9, 8]])
+        exp = GeoDataFrame(
+            [Point(xy) for xy in pts],
+            columns=["geometry2"],
+            crs="EPSG:3857",
+            geometry="geometry2",
+        )
+        assert_geodataframe_equal(clip_pts, exp)
+
+    def test_clip_poly(self, buffered_locations, mask):
+        """Test clipping a polygon GDF with a generic polygon geometry."""
+        clipped_poly = clip(buffered_locations, mask)
+        assert len(clipped_poly.geometry) == 3
+        assert all(clipped_poly.geom_type == "Polygon")
+
+    def test_clip_poly_geom_col_rename(self, buffered_locations, mask):
+        """Test clipping a polygon GDF with a generic polygon geometry."""
+
+        poly_gdf_geom_col_rename = buffered_locations.rename_geometry("geometry2")
+        clipped_poly = clip(poly_gdf_geom_col_rename, mask)
+        assert len(clipped_poly.geometry) == 3
+        assert "geometry" not in clipped_poly.keys()
+        assert "geometry2" in clipped_poly.keys()
+
+    def test_clip_poly_series(self, buffered_locations, mask):
+        """Test clipping a polygon GDF with a generic polygon geometry."""
+        clipped_poly = clip(buffered_locations.geometry, mask)
+        assert len(clipped_poly) == 3
+        assert all(clipped_poly.geom_type == "Polygon")
+
+    @pytest.mark.xfail(pandas_133, reason="Regression in pandas 1.3.3 (GH #2101)")
+    def test_clip_multipoly_keep_geom_type(self, multi_poly_gdf, mask):
+        """Test a multi poly object where the return includes a sliver.
+        Also the bounds of the object should == the bounds of the clip object
+        if they fully overlap (as they do in these fixtures)."""
+        clipped = clip(multi_poly_gdf, mask, keep_geom_type=True)
+        expected_bounds = (
+            mask if _mask_is_list_like_rectangle(mask) else mask.total_bounds
+        )
+        assert np.array_equal(clipped.total_bounds, expected_bounds)
+        # Assert returned data is a not geometry collection
+        assert (clipped.geom_type.isin(["Polygon", "MultiPolygon"])).all()
+
+    def test_clip_multiline(self, multi_line, mask):
+        """Test that clipping a multiline feature with a poly returns expected
+        output."""
+        clipped = clip(multi_line, mask)
+        assert clipped.geom_type[0] == "MultiLineString"
+
+    def test_clip_multipoint(self, multi_point, mask):
+        """Clipping a multipoint feature with a polygon works as expected.
+        should return a geodataframe with a single multi point feature"""
+        clipped = clip(multi_point, mask)
+        assert clipped.geom_type[0] == "MultiPoint"
+        assert hasattr(clipped, "attr")
+        # All points should intersect the clip geom
+        assert len(clipped) == 2
+        clipped_mutltipoint = MultiPoint(
+            [
+                Point(2, 2),
+                Point(3, 4),
+                Point(9, 8),
+            ]
+        )
+        assert clipped.iloc[0].geometry.wkt == clipped_mutltipoint.wkt
+        shape_for_points = (
+            box(*mask) if _mask_is_list_like_rectangle(mask) else mask.unary_union
+        )
+        assert all(clipped.intersects(shape_for_points))
+
+    def test_clip_lines(self, two_line_gdf, mask):
+        """Test what happens when you give the clip_extent a line GDF."""
+        clip_line = clip(two_line_gdf, mask)
+        assert len(clip_line.geometry) == 2
+
+    def test_mixed_geom(self, mixed_gdf, mask):
+        """Test clipping a mixed GeoDataFrame"""
+        clipped = clip(mixed_gdf, mask)
+        assert (
+            clipped.geom_type[0] == "Point"
+            and clipped.geom_type[1] == "Polygon"
+            and clipped.geom_type[2] == "LineString"
+        )
 
+    def test_mixed_series(self, mixed_gdf, mask):
+        """Test clipping a mixed GeoSeries"""
+        clipped = clip(mixed_gdf.geometry, mask)
+        assert (
+            clipped.geom_type[0] == "Point"
+            and clipped.geom_type[1] == "Polygon"
+            and clipped.geom_type[2] == "LineString"
+        )
 
-def test_clip_poly_series(buffered_locations, single_rectangle_gdf):
-    """Test clipping a polygon GDF with a generic polygon geometry."""
-    clipped_poly = clip(buffered_locations.geometry, single_rectangle_gdf)
-    assert len(clipped_poly) == 3
-    assert all(clipped_poly.geom_type == "Polygon")
+    def test_clip_warning_no_extra_geoms(self, buffered_locations, mask):
+        """Test a user warning is provided if no new geometry types are found."""
+        with pytest.warns(UserWarning):
+            clip(buffered_locations, mask, True)
+            warnings.warn(
+                "keep_geom_type was called when no extra geometry types existed.",
+                UserWarning,
+            )
+
+    def test_clip_with_line_extra_geom(self, sliver_line, mask):
+        """When the output of a clipped line returns a geom collection,
+        and keep_geom_type is True, no geometry collections should be returned."""
+        clipped = clip(sliver_line, mask, keep_geom_type=True)
+        assert len(clipped.geometry) == 1
+        # Assert returned data is a not geometry collection
+        assert not (clipped.geom_type == "GeometryCollection").any()
+
+    def test_clip_no_box_overlap(self, pointsoutside_nooverlap_gdf, mask):
+        """Test clip when intersection is empty and boxes do not overlap."""
+        clipped = clip(pointsoutside_nooverlap_gdf, mask)
+        assert len(clipped) == 0
+
+    def test_clip_box_overlap(self, pointsoutside_overlap_gdf, mask):
+        """Test clip when intersection is empty and boxes do overlap."""
+        clipped = clip(pointsoutside_overlap_gdf, mask)
+        assert len(clipped) == 0
+
+    def test_warning_extra_geoms_mixed(self, mixed_gdf, mask):
+        """Test the correct warnings are raised if keep_geom_type is
+        called on a mixed GDF"""
+        with pytest.warns(UserWarning):
+            clip(mixed_gdf, mask, keep_geom_type=True)
+
+    def test_warning_geomcoll(self, geomcol_gdf, mask):
+        """Test the correct warnings are raised if keep_geom_type is
+        called on a GDF with GeometryCollection"""
+        with pytest.warns(UserWarning):
+            clip(geomcol_gdf, mask, keep_geom_type=True)
+
+
+def test_clip_line_keep_slivers(sliver_line, single_rectangle_gdf):
+    """Test the correct output if a point is returned
+    from a line only geometry type."""
+    clipped = clip(sliver_line, single_rectangle_gdf)
+    # Assert returned data is a geometry collection given sliver geoms
+    assert "Point" == clipped.geom_type[0]
+    assert "LineString" == clipped.geom_type[1]
 
 
 @pytest.mark.xfail(pandas_133, reason="Regression in pandas 1.3.3 (GH #2101)")
@@ -260,93 +413,9 @@ def test_clip_multipoly_keep_slivers(multi_poly_gdf, single_rectangle_gdf):
     assert "GeometryCollection" in clipped.geom_type[0]
 
 
-@pytest.mark.xfail(pandas_133, reason="Regression in pandas 1.3.3 (GH #2101)")
-def test_clip_multipoly_keep_geom_type(multi_poly_gdf, single_rectangle_gdf):
-    """Test a multi poly object where the return includes a sliver.
-    Also the bounds of the object should == the bounds of the clip object
-    if they fully overlap (as they do in these fixtures)."""
-    clipped = clip(multi_poly_gdf, single_rectangle_gdf, keep_geom_type=True)
-    assert np.array_equal(clipped.total_bounds, single_rectangle_gdf.total_bounds)
-    # Assert returned data is a not geometry collection
-    assert (clipped.geom_type == "Polygon").any()
-
-
-def test_clip_single_multipoly_no_extra_geoms(
-    buffered_locations, larger_single_rectangle_gdf
-):
-    """When clipping a multi-polygon feature, no additional geom types
-    should be returned."""
-    multi = buffered_locations.dissolve(by="type").reset_index()
-    clipped = clip(multi, larger_single_rectangle_gdf)
-    assert clipped.geom_type[0] == "Polygon"
-
-
-def test_clip_multiline(multi_line, single_rectangle_gdf):
-    """Test that clipping a multiline feature with a poly returns expected output."""
-    clipped = clip(multi_line, single_rectangle_gdf)
-    assert clipped.geom_type[0] == "MultiLineString"
-
-
-def test_clip_multipoint(single_rectangle_gdf, multi_point):
-    """Clipping a multipoint feature with a polygon works as expected.
-    should return a geodataframe with a single multi point feature"""
-    clipped = clip(multi_point, single_rectangle_gdf)
-    assert clipped.geom_type[0] == "MultiPoint"
-    assert hasattr(clipped, "attr")
-    # All points should intersect the clip geom
-    assert len(clipped) == 2
-    clipped_mutltipoint = MultiPoint(
-        [
-            Point(2, 2),
-            Point(3, 4),
-            Point(9, 8),
-        ]
-    )
-    assert clipped.iloc[0].geometry.wkt == clipped_mutltipoint.wkt
-    assert all(clipped.intersects(single_rectangle_gdf.unary_union))
-
-
-def test_clip_lines(two_line_gdf, single_rectangle_gdf):
-    """Test what happens when you give the clip_extent a line GDF."""
-    clip_line = clip(two_line_gdf, single_rectangle_gdf)
-    assert len(clip_line.geometry) == 2
-
-
-def test_clip_with_multipolygon(buffered_locations, single_rectangle_gdf):
-    """Test clipping a polygon with a multipolygon."""
-    multi = buffered_locations.dissolve(by="type").reset_index()
-    clipped = clip(single_rectangle_gdf, multi)
-    assert clipped.geom_type[0] == "Polygon"
-
-
-def test_mixed_geom(mixed_gdf, single_rectangle_gdf):
-    """Test clipping a mixed GeoDataFrame"""
-    clipped = clip(mixed_gdf, single_rectangle_gdf)
-    assert (
-        clipped.geom_type[0] == "Point"
-        and clipped.geom_type[1] == "Polygon"
-        and clipped.geom_type[2] == "LineString"
-    )
-
-
-def test_mixed_series(mixed_gdf, single_rectangle_gdf):
-    """Test clipping a mixed GeoSeries"""
-    clipped = clip(mixed_gdf.geometry, single_rectangle_gdf)
-    assert (
-        clipped.geom_type[0] == "Point"
-        and clipped.geom_type[1] == "Polygon"
-        and clipped.geom_type[2] == "LineString"
-    )
-
-
-def test_clip_warning_no_extra_geoms(buffered_locations, single_rectangle_gdf):
-    """Test a user warning is provided if no new geometry types are found."""
-    with pytest.warns(UserWarning):
-        clip(buffered_locations, single_rectangle_gdf, True)
-        warnings.warn(
-            "keep_geom_type was called when no extra geometry types existed.",
-            UserWarning,
-        )
+def test_warning_crs_mismatch(point_gdf, single_rectangle_gdf):
+    with pytest.warns(UserWarning, match="CRS mismatch between the CRS"):
+        clip(point_gdf, single_rectangle_gdf.to_crs(4326))
 
 
 def test_clip_with_polygon(single_rectangle_gdf):
@@ -361,50 +430,23 @@ def test_clip_with_polygon(single_rectangle_gdf):
     assert_geodataframe_equal(clipped, exp)
 
 
-def test_clip_with_line_extra_geom(single_rectangle_gdf, sliver_line):
-    """When the output of a clipped line returns a geom collection,
-    and keep_geom_type is True, no geometry collections should be returned."""
-    clipped = clip(sliver_line, single_rectangle_gdf, keep_geom_type=True)
-    assert len(clipped.geometry) == 1
-    # Assert returned data is a not geometry collection
-    assert not (clipped.geom_type == "GeometryCollection").any()
-
-
-def test_clip_line_keep_slivers(single_rectangle_gdf, sliver_line):
-    """Test the correct output if a point is returned
-    from a line only geometry type."""
-    clipped = clip(sliver_line, single_rectangle_gdf)
-    # Assert returned data is a geometry collection given sliver geoms
-    assert "Point" == clipped.geom_type[0]
-    assert "LineString" == clipped.geom_type[1]
-
-
-def test_clip_no_box_overlap(pointsoutside_nooverlap_gdf, single_rectangle_gdf):
-    """Test clip when intersection is empty and boxes do not overlap."""
-    clipped = clip(pointsoutside_nooverlap_gdf, single_rectangle_gdf)
-    assert len(clipped) == 0
-
-
-def test_clip_box_overlap(pointsoutside_overlap_gdf, single_rectangle_gdf):
-    """Test clip when intersection is empty and boxes do overlap."""
-    clipped = clip(pointsoutside_overlap_gdf, single_rectangle_gdf)
-    assert len(clipped) == 0
-
-
-def test_warning_extra_geoms_mixed(single_rectangle_gdf, mixed_gdf):
-    """Test the correct warnings are raised if keep_geom_type is
-    called on a mixed GDF"""
-    with pytest.warns(UserWarning):
-        clip(mixed_gdf, single_rectangle_gdf, keep_geom_type=True)
-
-
-def test_warning_geomcoll(single_rectangle_gdf, geomcol_gdf):
-    """Test the correct warnings are raised if keep_geom_type is
-    called on a GDF with GeometryCollection"""
-    with pytest.warns(UserWarning):
-        clip(geomcol_gdf, single_rectangle_gdf, keep_geom_type=True)
+def test_clip_with_multipolygon(buffered_locations, single_rectangle_gdf):
+    """Test clipping a polygon with a multipolygon."""
+    multi = buffered_locations.dissolve(by="type").reset_index()
+    clipped = clip(single_rectangle_gdf, multi)
+    assert clipped.geom_type[0] == "Polygon"
 
 
-def test_warning_crs_mismatch(point_gdf, single_rectangle_gdf):
-    with pytest.warns(UserWarning, match="CRS mismatch between the CRS"):
-        clip(point_gdf, single_rectangle_gdf.to_crs(4326))
+@pytest.mark.parametrize(
+    "mask_fixture_name",
+    mask_variants_large_rectangle,
+)
+def test_clip_single_multipoly_no_extra_geoms(
+    buffered_locations, mask_fixture_name, request
+):
+    """When clipping a multi-polygon feature, no additional geom types
+    should be returned."""
+    masks = request.getfixturevalue(mask_fixture_name)
+    multi = buffered_locations.dissolve(by="type").reset_index()
+    clipped = clip(multi, masks)
+    assert clipped.geom_type[0] == "Polygon"
diff --git a/geopandas/tools/tests/test_sjoin.py b/geopandas/tools/tests/test_sjoin.py
index c14367c2..80a9972d 100644
--- a/geopandas/tools/tests/test_sjoin.py
+++ b/geopandas/tools/tests/test_sjoin.py
@@ -767,14 +767,14 @@ class TestNearest:
                 [Point(1, 1), Point(0.25, 1)],
                 [0, 1],
                 [1, 0],
-                [math.sqrt(0.25 ** 2 + 1), 0],
+                [math.sqrt(0.25**2 + 1), 0],
             ),
             (
                 [Point(0, 0), Point(1, 1)],
                 [Point(-10, -10), Point(100, 100)],
                 [0, 1],
                 [0, 0],
-                [math.sqrt(10 ** 2 + 10 ** 2), math.sqrt(11 ** 2 + 11 ** 2)],
+                [math.sqrt(10**2 + 10**2), math.sqrt(11**2 + 11**2)],
             ),
             (
                 [Point(0, 0), Point(1, 1)],
@@ -788,7 +788,7 @@ class TestNearest:
                 [Point(1.1, 1.1), Point(0, 0)],
                 [0, 1, 2],
                 [1, 0, 1],
-                [0, np.sqrt(0.1 ** 2 + 0.1 ** 2), 0],
+                [0, np.sqrt(0.1**2 + 0.1**2), 0],
             ),
         ],
     )
@@ -852,21 +852,21 @@ class TestNearest:
                 [Point(-10, -10), Point(100, 100)],
                 [0, 1],
                 [0, 1],
-                [math.sqrt(10 ** 2 + 10 ** 2), math.sqrt(99 ** 2 + 99 ** 2)],
+                [math.sqrt(10**2 + 10**2), math.sqrt(99**2 + 99**2)],
             ),
             (
                 [Point(0, 0), Point(1, 1)],
                 [Point(x, y) for x, y in zip(np.arange(10), np.arange(10))],
                 [0, 1] + [1] * 8,
                 list(range(10)),
-                [0, 0] + [np.sqrt(x ** 2 + x ** 2) for x in np.arange(1, 9)],
+                [0, 0] + [np.sqrt(x**2 + x**2) for x in np.arange(1, 9)],
             ),
             (
                 [Point(0, 0), Point(1, 1), Point(0, 0)],
                 [Point(1.1, 1.1), Point(0, 0)],
                 [1, 0, 2],
                 [0, 1, 1],
-                [np.sqrt(0.1 ** 2 + 0.1 ** 2), 0, 0],
+                [np.sqrt(0.1**2 + 0.1**2), 0, 0],
             ),
         ],
     )