Repository: geopandas/geopandas

Issue:
Consistency of predicate argument
I have realised that with changes to sindex in 0.8.0 we have introduced inconsistency in terminology. While in `sjoin`, we have an attribute `op`, in `sindex.query` we use `predicate`. Both mean the same thing and are used for the same purpose. 

Not sure what the best option would be, but I quite like literal `predicate` as I always found `op` a bit cryptic (btw, what the `o` stands for? overlay? as overlay predicate?). One way could be to have both as synonyms or to have only `predicate` but silently accept `op` as well (until some deprecation or just keep that). But even I was confused by the current situation while trying to use `op` in query.

cc @adriangb 

Discussion:
adriangb: Where are we with this @martinfleis ? Do you want me to resolve conflicts and fix anything else?
adriangb: Hey @martinfleis, I made the doc edits and swapped the `UserWarning -> FutureWarning`.

I also realized that there's 2 edge cases that we weren't checking for:
- Both `op` and `predicate` are specified. I added a `UserWarning` for this that is only emitted if `predicate!="intersects"` (i.e. not the default) _and_ `op!=predicate`. This also includes the deprecation message. Let me know if you'd rather this be a `FutureWarning`.
- Extra `**kwargs` are specified. This will now raise a `ValueError` that lists out the extra kwargs.
adriangb: @brendan-ward bumping again, hoping this doesn't get left out of the next release
martinfleis: @adriangb can you make sure this is mergeable (no conflicts, green CI)?
adriangb: yeah let me resolve conflicts
adriangb: Conflicts resolved. CI failures look like the same Matplotlib issue as #2066
adriangb: All tests are passing now w/ master merged in, and I believe I've addressed all of the feedback. Please ping me if there's anything else needed @brendan-ward @martinfleis 

Patch:
diff --git a/doc/source/docs/user_guide/mergingdata.rst b/doc/source/docs/user_guide/mergingdata.rst
index 6edc2d7c..2343571f 100644
--- a/doc/source/docs/user_guide/mergingdata.rst
+++ b/doc/source/docs/user_guide/mergingdata.rst
@@ -91,24 +91,24 @@ In a Spatial Join, two geometry objects are merged based on their spatial relati
 
    # Execute spatial join
 
-   cities_with_country = geopandas.sjoin(cities, countries, how="inner", op='intersects')
+   cities_with_country = geopandas.sjoin(cities, countries, how="inner", predicate='intersects')
    cities_with_country.head()
 
 
 Sjoin Arguments
 ~~~~~~~~~~~~~~~~
 
-:func:`sjoin` has two core arguments: ``how`` and ``op``.
+:func:`sjoin` has two core arguments: ``how`` and ``predicate``.
 
-**op**
+**predicate**
 
-The ``op`` argument specifies how ``geopandas`` decides whether or not to join the attributes of one
+The ``predicate`` argument specifies how ``geopandas`` decides whether or not to join the attributes of one
 object to another, based on their geometric relationship.
 
-The values for ``op`` correspond to the names of geometric binary predicates and depend on the spatial
+The values for ``predicate`` correspond to the names of geometric binary predicates and depend on the spatial
 index implementation.
 
-The default spatial index in ``geopandas`` currently supports the following values for ``op`` which are
+The default spatial index in ``geopandas`` currently supports the following values for ``predicate`` which are
 defined in the
 `Shapely documentation <http://shapely.readthedocs.io/en/latest/manual.html#binary-predicates>`__:
 
diff --git a/geopandas/geoseries.py b/geopandas/geoseries.py
index 928f08c3..3f540df3 100644
--- a/geopandas/geoseries.py
+++ b/geopandas/geoseries.py
@@ -187,6 +187,12 @@ class GeoSeries(GeoPandasBase, Series):
             kwargs.pop("dtype", None)
             # Use Series constructor to handle input data
             with compat.ignore_shapely2_warnings():
+                # suppress additional warning from pandas for empty data
+                # (will always give object dtype instead of float dtype in the future,
+                # making the `if s.empty: s = s.astype(object)` below unnecessary)
+                warnings.filterwarnings(
+                    "ignore", "The default dtype for empty Series", FutureWarning
+                )
                 s = pd.Series(data, index=index, name=name, **kwargs)
             # prevent trying to convert non-geometry objects
             if s.dtype != object:
diff --git a/geopandas/tools/geocoding.py b/geopandas/tools/geocoding.py
index 7743d1a4..bba8bb65 100644
--- a/geopandas/tools/geocoding.py
+++ b/geopandas/tools/geocoding.py
@@ -123,8 +123,12 @@ def _query(data, forward, provider, throttle_time, **kwargs):
     from geopy.geocoders.base import GeocoderQueryError
     from geopy.geocoders import get_geocoder_for_service
 
-    if not isinstance(data, pd.Series):
-        data = pd.Series(data)
+    if forward:
+        if not isinstance(data, pd.Series):
+            data = pd.Series(data)
+    else:
+        if not isinstance(data, geopandas.GeoSeries):
+            data = geopandas.GeoSeries(data)
 
     if isinstance(provider, str):
         provider = get_geocoder_for_service(provider)
diff --git a/geopandas/tools/sjoin.py b/geopandas/tools/sjoin.py
index e1a4b8d7..ca6f921e 100644
--- a/geopandas/tools/sjoin.py
+++ b/geopandas/tools/sjoin.py
@@ -7,7 +7,13 @@ from geopandas.array import _check_crs, _crs_mismatch_warn
 
 
 def sjoin(
-    left_df, right_df, how="inner", op="intersects", lsuffix="left", rsuffix="right"
+    left_df,
+    right_df,
+    how="inner",
+    predicate="intersects",
+    lsuffix="left",
+    rsuffix="right",
+    **kwargs,
 ):
     """Spatial join of two GeoDataFrames.
 
@@ -24,11 +30,12 @@ def sjoin(
         * 'right': use keys from right_df; retain only right_df geometry column
         * 'inner': use intersection of keys from both dfs; retain only
           left_df geometry column
-    op : string, default 'intersects'
+    predicate : string, default 'intersects'
         Binary predicate. Valid values are determined by the spatial index used.
         You can check the valid values in left_df or right_df as
         ``left_df.sindex.valid_query_predicates`` or
         ``right_df.sindex.valid_query_predicates``
+        Replaces deprecated ``op`` parameter.
     lsuffix : string, default 'left'
         Suffix to apply to overlapping column names (left GeoDataFrame).
     rsuffix : string, default 'right'
@@ -84,6 +91,30 @@ stria    AUT    416600.0
     Every operation in GeoPandas is planar, i.e. the potential third
     dimension is not taken into account.
     """
+    if "op" in kwargs:
+        op = kwargs.pop("op")
+        deprecation_message = (
+            "The `op` parameter is deprecated and will be removed"
+            " in a future release. Please use the `predicate` parameter"
+            " instead."
+        )
+        if predicate != "intersects" and op != predicate:
+            override_message = (
+                "A non-default value for `predicate` was passed"
+                f' (got `predicate="{predicate}"`'
+                f' in combination with `op="{op}"`).'
+                " The value of `predicate` will be overriden by the value of `op`,"
+                " , which may result in unexpected behavior."
+                f"\n{deprecation_message}"
+            )
+            warnings.warn(override_message, UserWarning, stacklevel=4)
+        else:
+            warnings.warn(deprecation_message, FutureWarning, stacklevel=4)
+        predicate = op
+    if kwargs:
+        first = next(iter(kwargs.keys()))
+        raise TypeError(f"sjoin() got an unexpected keyword argument '{first}'")
+
     _basic_checks(left_df, right_df, how, lsuffix, rsuffix)
 
     box_left_gdf = left_df.total_bounds
@@ -106,7 +137,7 @@ stria    AUT    416600.0
         copy_df = _frame_join(indices, left_df, right_df, how, lsuffix, rsuffix)
         return copy_df.iloc[:0]
 
-    indices = _geom_predicate_query(left_df, right_df, op)
+    indices = _geom_predicate_query(left_df, right_df, predicate)
 
     joined = _frame_join(indices, left_df, right_df, how, lsuffix, rsuffix)
 
@@ -163,14 +194,14 @@ def _basic_checks(left_df, right_df, how, lsuffix, rsuffix):
         )
 
 
-def _geom_predicate_query(left_df, right_df, op):
+def _geom_predicate_query(left_df, right_df, predicate):
     """Compute geometric comparisons and get matching indices.
 
     Parameters
     ----------
     left_df : GeoDataFrame
     right_df : GeoDataFrame
-    op : string
+    predicate : string
         Binary predicate to query.
 
     Returns
@@ -185,7 +216,10 @@ def _geom_predicate_query(left_df, right_df, op):
         warnings.filterwarnings(
             "ignore", "Generated spatial index is empty", FutureWarning
         )
-        if op == "within":
+
+        original_predicate = predicate
+
+        if predicate == "within":
             # within is implemented as the inverse of contains
             # contains is a faster predicate
             # see discussion at https://github.com/geopandas/geopandas/pull/1421
@@ -195,7 +229,6 @@ def _geom_predicate_query(left_df, right_df, op):
         else:
             # all other predicates are symmetric
             # keep them the same
-            predicate = op
             sindex = right_df.sindex
             input_geoms = left_df.geometry
 
@@ -205,7 +238,8 @@ def _geom_predicate_query(left_df, right_df, op):
     else:
         # when sindex is empty / has no valid geometries
         indices = pd.DataFrame(columns=["_key_left", "_key_right"], dtype=float)
-    if op == "within":
+
+    if original_predicate == "within":
         # within is implemented as the inverse of contains
         # flip back the results
         indices = indices.rename(


Test patch:
diff --git a/geopandas/testing.py b/geopandas/testing.py
index 66c2c289..0c897302 100644
--- a/geopandas/testing.py
+++ b/geopandas/testing.py
@@ -12,12 +12,20 @@ from geopandas import _vectorized
 
 def _isna(this):
     """isna version that works for both scalars and (Geo)Series"""
-    if hasattr(this, "isna"):
-        return this.isna()
-    elif hasattr(this, "isnull"):
-        return this.isnull()
-    else:
-        return pd.isnull(this)
+    with warnings.catch_warnings():
+        # GeoSeries.isna will raise a warning about no longer returning True
+        # for empty geometries. This helper is used below always in combination
+        # with an is_empty check to preserve behaviour, and thus we ignore the
+        # warning here to avoid it bubbling up to the user
+        warnings.filterwarnings(
+            "ignore", r"GeoSeries.isna\(\) previously returned", UserWarning
+        )
+        if hasattr(this, "isna"):
+            return this.isna()
+        elif hasattr(this, "isnull"):
+            return this.isnull()
+        else:
+            return pd.isnull(this)
 
 
 def _geom_equals_mask(this, that):
diff --git a/geopandas/tests/test_array.py b/geopandas/tests/test_array.py
index 3770ca27..e7633c06 100644
--- a/geopandas/tests/test_array.py
+++ b/geopandas/tests/test_array.py
@@ -447,7 +447,18 @@ def test_binary_geo_scalar(attr):
 
 
 @pytest.mark.parametrize(
-    "attr", ["is_closed", "is_valid", "is_empty", "is_simple", "has_z", "is_ring"]
+    "attr",
+    [
+        "is_closed",
+        "is_valid",
+        "is_empty",
+        "is_simple",
+        "has_z",
+        # for is_ring we raise a warning about the value for Polygon changing
+        pytest.param(
+            "is_ring", marks=pytest.mark.filterwarnings("ignore:is_ring:FutureWarning")
+        ),
+    ],
 )
 def test_unary_predicates(attr):
     na_value = False
@@ -484,6 +495,8 @@ def test_unary_predicates(attr):
     assert result.tolist() == expected
 
 
+# for is_ring we raise a warning about the value for Polygon changing
+@pytest.mark.filterwarnings("ignore:is_ring:FutureWarning")
 def test_is_ring():
     g = [
         shapely.geometry.LinearRing([(0, 0), (1, 1), (1, -1)]),
diff --git a/geopandas/tests/test_extension_array.py b/geopandas/tests/test_extension_array.py
index b7db16a4..1c37bdc5 100644
--- a/geopandas/tests/test_extension_array.py
+++ b/geopandas/tests/test_extension_array.py
@@ -23,6 +23,7 @@ from pandas.tests.extension import base as extension_tests
 import shapely.geometry
 
 from geopandas.array import GeometryArray, GeometryDtype, from_shapely
+from geopandas._compat import ignore_shapely2_warnings
 
 import pytest
 
@@ -48,7 +49,8 @@ def dtype():
 
 def make_data():
     a = np.empty(100, dtype=object)
-    a[:] = [shapely.geometry.Point(i, i) for i in range(100)]
+    with ignore_shapely2_warnings():
+        a[:] = [shapely.geometry.Point(i, i) for i in range(100)]
     ga = from_shapely(a)
     return ga
 
@@ -299,7 +301,8 @@ class TestInterface(extension_tests.BaseInterfaceTests):
         result = np.array(data, dtype=object)
         # expected = np.array(list(data), dtype=object)
         expected = np.empty(len(data), dtype=object)
-        expected[:] = list(data)
+        with ignore_shapely2_warnings():
+            expected[:] = list(data)
         assert_array_equal(result, expected)
 
     def test_contains(self, data, data_missing):
@@ -404,6 +407,11 @@ def all_arithmetic_operators(request):
     return request.param
 
 
+# an inherited test from pandas creates a Series from a list of geometries, which
+# triggers the warning from Shapely, out of control of GeoPandas, so ignoring here
+@pytest.mark.filterwarnings(
+    "ignore:The array interface is deprecated and will no longer work in Shapely 2.0"
+)
 class TestArithmeticOps(extension_tests.BaseArithmeticOpsTests):
     @pytest.mark.skip(reason="not applicable")
     def test_divmod_series_array(self, data, data_for_twos):
@@ -414,6 +422,11 @@ class TestArithmeticOps(extension_tests.BaseArithmeticOpsTests):
         pass
 
 
+# an inherited test from pandas creates a Series from a list of geometries, which
+# triggers the warning from Shapely, out of control of GeoPandas, so ignoring here
+@pytest.mark.filterwarnings(
+    "ignore:The array interface is deprecated and will no longer work in Shapely 2.0"
+)
 class TestComparisonOps(extension_tests.BaseComparisonOpsTests):
     def _compare_other(self, s, data, op_name, other):
         op = getattr(operator, op_name.strip("_"))
diff --git a/geopandas/tests/test_geocode.py b/geopandas/tests/test_geocode.py
index f2daf9db..4b1c8ccb 100644
--- a/geopandas/tests/test_geocode.py
+++ b/geopandas/tests/test_geocode.py
@@ -134,7 +134,7 @@ def test_bad_provider_reverse():
     from geopy.exc import GeocoderNotFound
 
     with pytest.raises(GeocoderNotFound):
-        reverse_geocode(["cambridge, ma"], "badprovider")
+        reverse_geocode([Point(0, 0)], "badprovider")
 
 
 def test_forward(locations, points):
diff --git a/geopandas/tests/test_geodataframe.py b/geopandas/tests/test_geodataframe.py
index 0b9e247a..cbfebbd5 100644
--- a/geopandas/tests/test_geodataframe.py
+++ b/geopandas/tests/test_geodataframe.py
@@ -15,6 +15,7 @@ from shapely.geometry import Point
 import geopandas
 from geopandas import GeoDataFrame, GeoSeries, read_file
 from geopandas.array import GeometryArray, GeometryDtype, from_shapely
+from geopandas._compat import ignore_shapely2_warnings
 
 from geopandas.testing import assert_geodataframe_equal, assert_geoseries_equal
 from geopandas.tests.util import PACKAGE_DIR, validate_boro_df
@@ -367,6 +368,9 @@ class TestDataFrame:
         assert len(data["features"]) == 5
         assert "id" in data["features"][0].keys()
 
+    @pytest.mark.filterwarnings(
+        "ignore:Geometry column does not contain geometry:UserWarning"
+    )
     def test_to_json_geom_col(self):
         df = self.df.copy()
         df["geom"] = df["geometry"]
@@ -872,7 +876,8 @@ class TestConstructor:
             "B": np.arange(3.0),
             "geometry": [Point(x, x) for x in range(3)],
         }
-        a = np.array([data["A"], data["B"], data["geometry"]], dtype=object).T
+        with ignore_shapely2_warnings():
+            a = np.array([data["A"], data["B"], data["geometry"]], dtype=object).T
 
         df = GeoDataFrame(a, columns=["A", "B", "geometry"])
         check_geodataframe(df)
@@ -887,7 +892,8 @@ class TestConstructor:
             "geometry": [Point(x, x) for x in range(3)],
         }
         gpdf = GeoDataFrame(data)
-        pddf = pd.DataFrame(data)
+        with ignore_shapely2_warnings():
+            pddf = pd.DataFrame(data)
         check_geodataframe(gpdf)
         assert type(pddf) == pd.DataFrame
 
@@ -917,7 +923,8 @@ class TestConstructor:
 
         gpdf = GeoDataFrame(data, geometry="other_geom")
         check_geodataframe(gpdf, "other_geom")
-        pddf = pd.DataFrame(data)
+        with ignore_shapely2_warnings():
+            pddf = pd.DataFrame(data)
 
         for df in [gpdf, pddf]:
             res = GeoDataFrame(df, geometry="other_geom")
@@ -1003,7 +1010,8 @@ class TestConstructor:
     def test_overwrite_geometry(self):
         # GH602
         data = pd.DataFrame({"geometry": [1, 2, 3], "col1": [4, 5, 6]})
-        geoms = pd.Series([Point(i, i) for i in range(3)])
+        with ignore_shapely2_warnings():
+            geoms = pd.Series([Point(i, i) for i in range(3)])
         # passed geometry kwarg should overwrite geometry column in data
         res = GeoDataFrame(data, geometry=geoms)
         assert_geoseries_equal(res.geometry, GeoSeries(geoms))
@@ -1028,6 +1036,6 @@ class TestConstructor:
 
 
 def test_geodataframe_crs():
-    gdf = GeoDataFrame()
+    gdf = GeoDataFrame(columns=["geometry"])
     gdf.crs = "IGNF:ETRS89UTM28"
     assert gdf.crs.to_authority() == ("IGNF", "ETRS89UTM28")
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
index a617dc3a..895693b3 100644
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -245,13 +245,15 @@ class TestGeomMethods:
                 "intersection", self.all_none, self.g1, self.empty
             )
 
-        assert len(self.g0.intersection(self.g9, align=True) == 8)
+        with pytest.warns(UserWarning, match="The indices .+ different"):
+            assert len(self.g0.intersection(self.g9, align=True) == 8)
         assert len(self.g0.intersection(self.g9, align=False) == 7)
 
     def test_union_series(self):
         self._test_binary_topological("union", self.sq, self.g1, self.g2)
 
-        assert len(self.g0.union(self.g9, align=True) == 8)
+        with pytest.warns(UserWarning, match="The indices .+ different"):
+            assert len(self.g0.union(self.g9, align=True) == 8)
         assert len(self.g0.union(self.g9, align=False) == 7)
 
     def test_union_polygon(self):
@@ -260,7 +262,8 @@ class TestGeomMethods:
     def test_symmetric_difference_series(self):
         self._test_binary_topological("symmetric_difference", self.sq, self.g3, self.g4)
 
-        assert len(self.g0.symmetric_difference(self.g9, align=True) == 8)
+        with pytest.warns(UserWarning, match="The indices .+ different"):
+            assert len(self.g0.symmetric_difference(self.g9, align=True) == 8)
         assert len(self.g0.symmetric_difference(self.g9, align=False) == 7)
 
     def test_symmetric_difference_poly(self):
@@ -273,7 +276,8 @@ class TestGeomMethods:
         expected = GeoSeries([GeometryCollection(), self.t2])
         self._test_binary_topological("difference", expected, self.g1, self.g2)
 
-        assert len(self.g0.difference(self.g9, align=True) == 8)
+        with pytest.warns(UserWarning, match="The indices .+ different"):
+            assert len(self.g0.difference(self.g9, align=True) == 8)
         assert len(self.g0.difference(self.g9, align=False) == 7)
 
     def test_difference_poly(self):
@@ -365,7 +369,8 @@ class TestGeomMethods:
         assert_array_dtype_equal(expected, self.g0.contains(self.t1))
 
         expected = [False, True, True, True, True, True, False, False]
-        assert_array_dtype_equal(expected, self.g0.contains(self.g9, align=True))
+        with pytest.warns(UserWarning, match="The indices .+ different"):
+            assert_array_dtype_equal(expected, self.g0.contains(self.g9, align=True))
 
         expected = [False, False, True, False, False, False, False]
         assert_array_dtype_equal(expected, self.g0.contains(self.g9, align=False))
@@ -389,7 +394,8 @@ class TestGeomMethods:
         assert_array_dtype_equal(expected, self.crossed_lines.crosses(self.l3))
 
         expected = [False] * 8
-        assert_array_dtype_equal(expected, self.g0.crosses(self.g9, align=True))
+        with pytest.warns(UserWarning, match="The indices .+ different"):
+            assert_array_dtype_equal(expected, self.g0.crosses(self.g9, align=True))
 
         expected = [False] * 7
         assert_array_dtype_equal(expected, self.g0.crosses(self.g9, align=False))
@@ -399,7 +405,8 @@ class TestGeomMethods:
         assert_array_dtype_equal(expected, self.g0.disjoint(self.t1))
 
         expected = [False] * 8
-        assert_array_dtype_equal(expected, self.g0.disjoint(self.g9, align=True))
+        with pytest.warns(UserWarning, match="The indices .+ different"):
+            assert_array_dtype_equal(expected, self.g0.disjoint(self.g9, align=True))
 
         expected = [False, False, False, False, True, False, False]
         assert_array_dtype_equal(expected, self.g0.disjoint(self.g9, align=False))
@@ -436,7 +443,8 @@ class TestGeomMethods:
             index=range(8),
         )
 
-        assert_array_dtype_equal(expected, self.g0.relate(self.g9, align=True))
+        with pytest.warns(UserWarning, match="The indices .+ different"):
+            assert_array_dtype_equal(expected, self.g0.relate(self.g9, align=True))
 
         expected = Series(
             [
@@ -462,7 +470,8 @@ class TestGeomMethods:
         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))
-        assert_array_dtype_equal(expected, self.g0.distance(self.g9, align=True))
+        with pytest.warns(UserWarning, match="The indices .+ different"):
+            assert_array_dtype_equal(expected, self.g0.distance(self.g9, align=True))
 
         val = self.g0.iloc[4].distance(self.g9.iloc[4])
         expected = Series(np.array([0, 0, 0, 0, val, np.nan, np.nan]), self.g0.index)
@@ -489,7 +498,8 @@ class TestGeomMethods:
         assert_array_dtype_equal(expected, self.g0.intersects(self.empty_poly))
 
         expected = [False, True, True, True, True, True, False, False]
-        assert_array_dtype_equal(expected, self.g0.intersects(self.g9, align=True))
+        with pytest.warns(UserWarning, match="The indices .+ different"):
+            assert_array_dtype_equal(expected, self.g0.intersects(self.g9, align=True))
 
         expected = [True, True, True, True, False, False, False]
         assert_array_dtype_equal(expected, self.g0.intersects(self.g9, align=False))
@@ -502,7 +512,8 @@ class TestGeomMethods:
         assert_array_dtype_equal(expected, self.g4.overlaps(self.t1))
 
         expected = [False] * 8
-        assert_array_dtype_equal(expected, self.g0.overlaps(self.g9, align=True))
+        with pytest.warns(UserWarning, match="The indices .+ different"):
+            assert_array_dtype_equal(expected, self.g0.overlaps(self.g9, align=True))
 
         expected = [False] * 7
         assert_array_dtype_equal(expected, self.g0.overlaps(self.g9, align=False))
@@ -512,7 +523,8 @@ class TestGeomMethods:
         assert_array_dtype_equal(expected, self.g0.touches(self.t1))
 
         expected = [False] * 8
-        assert_array_dtype_equal(expected, self.g0.touches(self.g9, align=True))
+        with pytest.warns(UserWarning, match="The indices .+ different"):
+            assert_array_dtype_equal(expected, self.g0.touches(self.g9, align=True))
 
         expected = [True, False, False, True, False, False, False]
         assert_array_dtype_equal(expected, self.g0.touches(self.g9, align=False))
@@ -525,7 +537,8 @@ class TestGeomMethods:
         assert_array_dtype_equal(expected, self.g0.within(self.sq))
 
         expected = [False, True, True, True, True, True, False, False]
-        assert_array_dtype_equal(expected, self.g0.within(self.g9, align=True))
+        with pytest.warns(UserWarning, match="The indices .+ different"):
+            assert_array_dtype_equal(expected, self.g0.within(self.g9, align=True))
 
         expected = [False, True, False, False, False, False, False]
         assert_array_dtype_equal(expected, self.g0.within(self.g9, align=False))
@@ -542,7 +555,8 @@ class TestGeomMethods:
         assert_series_equal(res, exp)
 
         expected = [False, True, True, True, True, True, False, False]
-        assert_array_dtype_equal(expected, self.g0.covers(self.g9, align=True))
+        with pytest.warns(UserWarning, match="The indices .+ different"):
+            assert_array_dtype_equal(expected, self.g0.covers(self.g9, align=True))
 
         expected = [False, False, True, False, False, False, False]
         assert_array_dtype_equal(expected, self.g0.covers(self.g9, align=False))
@@ -562,7 +576,8 @@ class TestGeomMethods:
         assert_series_equal(res, exp)
 
         expected = [False, True, True, True, True, True, False, False]
-        assert_array_dtype_equal(expected, self.g0.covered_by(self.g9, align=True))
+        with pytest.warns(UserWarning, match="The indices .+ different"):
+            assert_array_dtype_equal(expected, self.g0.covered_by(self.g9, align=True))
 
         expected = [False, True, False, False, False, False, False]
         assert_array_dtype_equal(expected, self.g0.covered_by(self.g9, align=False))
@@ -575,6 +590,8 @@ class TestGeomMethods:
         expected = Series(np.array([False] * len(self.g1)), self.g1.index)
         self._test_unary_real("is_empty", expected, self.g1)
 
+    # for is_ring we raise a warning about the value for Polygon changing
+    @pytest.mark.filterwarnings("ignore:is_ring:FutureWarning")
     def test_is_ring(self):
         expected = Series(np.array([True] * len(self.g1)), self.g1.index)
         self._test_unary_real("is_ring", expected, self.g1)
@@ -688,7 +705,8 @@ class TestGeomMethods:
 
         s = GeoSeries([Point(2, 2), Point(0.5, 0.5)], index=[1, 2])
         expected = Series([np.nan, 2.0, np.nan])
-        assert_series_equal(self.g5.project(s), expected)
+        with pytest.warns(UserWarning, match="The indices .+ different"):
+            assert_series_equal(self.g5.project(s), expected)
 
         expected = Series([2.0, 0.5], index=self.g5.index)
         assert_series_equal(self.g5.project(s, align=False), expected)
diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py
index d7a2d6a1..ff1879d1 100644
--- a/geopandas/tests/test_plotting.py
+++ b/geopandas/tests/test_plotting.py
@@ -516,7 +516,7 @@ class TestLineStringPlotting:
             self.df.plot(linestyle=ls, linewidth=1),
             self.df.plot(column="values", linestyle=ls, linewidth=1),
         ]:
-            np.testing.assert_array_equal(exp_ls, ax.collections[0].get_linestyle())
+            assert exp_ls == ax.collections[0].get_linestyle()
 
     def test_style_kwargs_linewidth(self):
         # single
@@ -947,7 +947,7 @@ class TestNonuniformGeometryPlotting:
             self.series.plot(linestyles=ls, linewidth=1),
             self.df.plot(linestyles=ls, linewidth=1),
         ]:
-            np.testing.assert_array_equal(exp_ls, ax.collections[0].get_linestyle())
+            assert exp_ls == ax.collections[0].get_linestyle()
 
     def test_style_kwargs_linewidth(self):
         # single
diff --git a/geopandas/tools/tests/test_sjoin.py b/geopandas/tools/tests/test_sjoin.py
index b18f6d4a..5f0ad322 100644
--- a/geopandas/tools/tests/test_sjoin.py
+++ b/geopandas/tools/tests/test_sjoin.py
@@ -113,6 +113,40 @@ class TestSpatialJoin:
         with pytest.warns(UserWarning, match="CRS mismatch between the CRS"):
             sjoin(df1, df2)
 
+    @pytest.mark.parametrize("dfs", ["default-index"], indirect=True)
+    @pytest.mark.parametrize("op", ["intersects", "contains", "within"])
+    def test_deprecated_op_param(self, dfs, op):
+        _, df1, df2, _ = dfs
+        with pytest.warns(FutureWarning, match="`op` parameter is deprecated"):
+            sjoin(df1, df2, op=op)
+
+    @pytest.mark.parametrize("dfs", ["default-index"], indirect=True)
+    @pytest.mark.parametrize("op", ["intersects", "contains", "within"])
+    @pytest.mark.parametrize("predicate", ["contains", "within"])
+    def test_deprecated_op_param_nondefault_predicate(self, dfs, op, predicate):
+        _, df1, df2, _ = dfs
+        match = "use the `predicate` parameter instead"
+        if op != predicate:
+            warntype = UserWarning
+            match = (
+                "`predicate` will be overriden by the value of `op`"
+                + r"(.|\s)*"
+                + match
+            )
+        else:
+            warntype = FutureWarning
+        with pytest.warns(warntype, match=match):
+            sjoin(df1, df2, predicate=predicate, op=op)
+
+    @pytest.mark.parametrize("dfs", ["default-index"], indirect=True)
+    def test_unknown_kwargs(self, dfs):
+        _, df1, df2, _ = dfs
+        with pytest.raises(
+            TypeError,
+            match=r"sjoin\(\) got an unexpected keyword argument 'extra_param'",
+        ):
+            sjoin(df1, df2, extra_param="test")
+
     @pytest.mark.parametrize(
         "dfs",
         [
@@ -124,13 +158,14 @@ class TestSpatialJoin:
         ],
         indirect=True,
     )
-    @pytest.mark.parametrize("op", ["intersects", "contains", "within"])
-    def test_inner(self, op, dfs):
+    @pytest.mark.parametrize("predicate", ["intersects", "contains", "within"])
+    @pytest.mark.parametrize("predicate_kw", ["predicate", "op"])
+    def test_inner(self, predicate, predicate_kw, dfs):
         index, df1, df2, expected = dfs
 
-        res = sjoin(df1, df2, how="inner", op=op)
+        res = sjoin(df1, df2, how="inner", **{predicate_kw: predicate})
 
-        exp = expected[op].dropna().copy()
+        exp = expected[predicate].dropna().copy()
         exp = exp.drop("geometry_y", axis=1).rename(columns={"geometry_x": "geometry"})
         exp[["df1", "df2"]] = exp[["df1", "df2"]].astype("int64")
         if index == "default-index":
@@ -167,20 +202,20 @@ class TestSpatialJoin:
         ],
         indirect=True,
     )
-    @pytest.mark.parametrize("op", ["intersects", "contains", "within"])
-    def test_left(self, op, dfs):
+    @pytest.mark.parametrize("predicate", ["intersects", "contains", "within"])
+    def test_left(self, predicate, dfs):
         index, df1, df2, expected = dfs
 
-        res = sjoin(df1, df2, how="left", op=op)
+        res = sjoin(df1, df2, how="left", predicate=predicate)
 
         if index in ["default-index", "string-index"]:
-            exp = expected[op].dropna(subset=["index_left"]).copy()
+            exp = expected[predicate].dropna(subset=["index_left"]).copy()
         elif index == "named-index":
-            exp = expected[op].dropna(subset=["df1_ix"]).copy()
+            exp = expected[predicate].dropna(subset=["df1_ix"]).copy()
         elif index == "multi-index":
-            exp = expected[op].dropna(subset=["level_0_x"]).copy()
+            exp = expected[predicate].dropna(subset=["level_0_x"]).copy()
         elif index == "named-multi-index":
-            exp = expected[op].dropna(subset=["df1_ix1"]).copy()
+            exp = expected[predicate].dropna(subset=["df1_ix1"]).copy()
         exp = exp.drop("geometry_y", axis=1).rename(columns={"geometry_x": "geometry"})
         exp["df1"] = exp["df1"].astype("int64")
         if index == "default-index":
@@ -218,14 +253,14 @@ class TestSpatialJoin:
             }
         )
         not_in = geopandas.GeoDataFrame({"col1": [1], "geometry": [Point(-0.5, 0.5)]})
-        empty = sjoin(not_in, polygons, how="left", op="intersects")
+        empty = sjoin(not_in, polygons, how="left", predicate="intersects")
         assert empty.index_right.isnull().all()
-        empty = sjoin(not_in, polygons, how="right", op="intersects")
+        empty = sjoin(not_in, polygons, how="right", predicate="intersects")
         assert empty.index_left.isnull().all()
-        empty = sjoin(not_in, polygons, how="inner", op="intersects")
+        empty = sjoin(not_in, polygons, how="inner", predicate="intersects")
         assert empty.empty
 
-    @pytest.mark.parametrize("op", ["intersects", "contains", "within"])
+    @pytest.mark.parametrize("predicate", ["intersects", "contains", "within"])
     @pytest.mark.parametrize(
         "empty",
         [
@@ -233,7 +268,7 @@ class TestSpatialJoin:
             GeoDataFrame(geometry=GeoSeries()),
         ],
     )
-    def test_join_with_empty(self, op, empty):
+    def test_join_with_empty(self, predicate, empty):
         # Check joins with empty geometry columns/dataframes.
         polygons = geopandas.GeoDataFrame(
             {
@@ -244,11 +279,11 @@ class TestSpatialJoin:
                 ],
             }
         )
-        result = sjoin(empty, polygons, how="left", op=op)
+        result = sjoin(empty, polygons, how="left", predicate=predicate)
         assert result.index_right.isnull().all()
-        result = sjoin(empty, polygons, how="right", op=op)
+        result = sjoin(empty, polygons, how="right", predicate=predicate)
         assert result.index_left.isnull().all()
-        result = sjoin(empty, polygons, how="inner", op=op)
+        result = sjoin(empty, polygons, how="inner", predicate=predicate)
         assert result.empty
 
     @pytest.mark.parametrize("dfs", ["default-index", "string-index"], indirect=True)
@@ -272,20 +307,20 @@ class TestSpatialJoin:
         ],
         indirect=True,
     )
-    @pytest.mark.parametrize("op", ["intersects", "contains", "within"])
-    def test_right(self, op, dfs):
+    @pytest.mark.parametrize("predicate", ["intersects", "contains", "within"])
+    def test_right(self, predicate, dfs):
         index, df1, df2, expected = dfs
 
-        res = sjoin(df1, df2, how="right", op=op)
+        res = sjoin(df1, df2, how="right", predicate=predicate)
 
         if index in ["default-index", "string-index"]:
-            exp = expected[op].dropna(subset=["index_right"]).copy()
+            exp = expected[predicate].dropna(subset=["index_right"]).copy()
         elif index == "named-index":
-            exp = expected[op].dropna(subset=["df2_ix"]).copy()
+            exp = expected[predicate].dropna(subset=["df2_ix"]).copy()
         elif index == "multi-index":
-            exp = expected[op].dropna(subset=["level_0_y"]).copy()
+            exp = expected[predicate].dropna(subset=["level_0_y"]).copy()
         elif index == "named-multi-index":
-            exp = expected[op].dropna(subset=["df2_ix1"]).copy()
+            exp = expected[predicate].dropna(subset=["df2_ix1"]).copy()
         exp = exp.drop("geometry_x", axis=1).rename(columns={"geometry_y": "geometry"})
         exp["df2"] = exp["df2"].astype("int64")
         if index == "default-index":
@@ -310,7 +345,7 @@ class TestSpatialJoin:
             exp.index.names = df2.index.names
 
         # GH 1364 fix of behaviour was done in pandas 1.1.0
-        if op == "within" and str(pd.__version__) >= LooseVersion("1.1.0"):
+        if predicate == "within" and str(pd.__version__) >= LooseVersion("1.1.0"):
             exp = exp.sort_index()
 
         assert_frame_equal(res, exp, check_index_type=False)
@@ -367,21 +402,21 @@ class TestSpatialJoinNYBB:
         df = sjoin(self.pointdf, self.polydf, how="inner")
         assert df.shape == (11, 8)
 
-    def test_sjoin_op(self):
+    def test_sjoin_predicate(self):
         # points within polygons
-        df = sjoin(self.pointdf, self.polydf, how="left", op="within")
+        df = sjoin(self.pointdf, self.polydf, how="left", predicate="within")
         assert df.shape == (21, 8)
         assert df.loc[1]["BoroName"] == "Staten Island"
 
         # points contain polygons? never happens so we should have nulls
-        df = sjoin(self.pointdf, self.polydf, how="left", op="contains")
+        df = sjoin(self.pointdf, self.polydf, how="left", predicate="contains")
         assert df.shape == (21, 8)
         assert np.isnan(df.loc[1]["Shape_Area"])
 
-    def test_sjoin_bad_op(self):
+    def test_sjoin_bad_predicate(self):
         # AttributeError: 'Point' object has no attribute 'spandex'
         with pytest.raises(ValueError):
-            sjoin(self.pointdf, self.polydf, how="left", op="spandex")
+            sjoin(self.pointdf, self.polydf, how="left", predicate="spandex")
 
     def test_sjoin_duplicate_column_name(self):
         pointdf2 = self.pointdf.rename(columns={"pointattr1": "Shape_Area"})
@@ -480,14 +515,14 @@ class TestSpatialJoinNYBB:
         df2 = sjoin(self.pointdf, self.polydf.append(empty), how="left")
         assert df2.shape == (21, 8)
 
-    @pytest.mark.parametrize("op", ["intersects", "within", "contains"])
-    def test_sjoin_no_valid_geoms(self, op):
+    @pytest.mark.parametrize("predicate", ["intersects", "within", "contains"])
+    def test_sjoin_no_valid_geoms(self, predicate):
         """Tests a completely empty GeoDataFrame."""
         empty = GeoDataFrame(geometry=[], crs=self.pointdf.crs)
-        assert sjoin(self.pointdf, empty, how="inner", op=op).empty
-        assert sjoin(self.pointdf, empty, how="right", op=op).empty
-        assert sjoin(empty, self.pointdf, how="inner", op=op).empty
-        assert sjoin(empty, self.pointdf, how="left", op=op).empty
+        assert sjoin(self.pointdf, empty, how="inner", predicate=predicate).empty
+        assert sjoin(self.pointdf, empty, how="right", predicate=predicate).empty
+        assert sjoin(empty, self.pointdf, how="inner", predicate=predicate).empty
+        assert sjoin(empty, self.pointdf, how="left", predicate=predicate).empty
 
     def test_empty_sjoin_return_duplicated_columns(self):
 
@@ -513,6 +548,6 @@ class TestSpatialJoinNaturalEarth:
         countries = self.world[["geometry", "name"]]
         countries = countries.rename(columns={"name": "country"})
         cities_with_country = sjoin(
-            self.cities, countries, how="inner", op="intersects"
+            self.cities, countries, how="inner", predicate="intersects"
         )
         assert cities_with_country.shape == (172, 4)