Repository: regionmask/regionmask

Issue:
drop support for pygeos and shapely < 2.0
Dropping pygeos and shapely 1.8 will allow considerable internal cleanups - looking forward to this. Upstream packages may move faster here (e.g. geopandas) but I'll probably wait until it's "allowed by NEP29" (not sure entirely when that will be, though).



```
Package           Required             Policy               Status
----------------- -------------------- -------------------- ------
shapely           1.8     (2021-10-25) 1.8     (2021-10-25) =
```


Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/regionmask/regionmask/pull/521?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.71%. Comparing base [(`e74cb22`)](https://app.codecov.io/gh/regionmask/regionmask/commit/e74cb22e976925ccd6c8ecaac8a9bfaadab44574?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask) to head [(`cc07cef`)](https://app.codecov.io/gh/regionmask/regionmask/pull/521?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask).
> Report is 12 commits behind head on main.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #521      +/-   ##
==========================================
- Coverage   99.79%   99.71%   -0.08%     
==========================================
  Files          17       17              
  Lines        1435     1406      -29     
==========================================
- Hits         1432     1402      -30     
- Misses          3        4       +1     
```

| [Flag](https://app.codecov.io/gh/regionmask/regionmask/pull/521/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask) | Coverage Δ | |
|---|---|---|
| [bare-minimum](https://app.codecov.io/gh/regionmask/regionmask/pull/521/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask) | `?` | |
| [min-all-deps](https://app.codecov.io/gh/regionmask/regionmask/pull/521/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask) | `?` | |
| [shapely_lt_2](https://app.codecov.io/gh/regionmask/regionmask/pull/521/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask) | `?` | |
| [unittests](https://app.codecov.io/gh/regionmask/regionmask/pull/521/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask) | `99.71% <ø> (-0.08%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask#carryforward-flags-in-the-pull-request-comment) to find out more.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/regionmask/regionmask/pull/521?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask).

mathause: Turns out cartopy v0.22 is required for shapely v2.0+. This is the most recent version of cartopy; which puts me a bit in a bind... Maybe at least wait for the new release of cartopy which should be soonish.
mathause: policy-wise it should be possible to require cartopy 0.22 in about 3 months (so around August 2024), which is most likely before the next release

Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 5d3452a..e485cfb 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -31,9 +31,6 @@ jobs:
           - env: "min-all-deps"
             os: "ubuntu-latest"
             python-version: "3.9"
-          - env: "shapely_lt_2"
-            os: "ubuntu-latest"
-            python-version: "3.10"
     steps:
       - uses: actions/checkout@v4
         with:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 6b84e07..9e44b99 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -18,9 +18,10 @@ Breaking Changes
 ~~~~~~~~~~~~~~~~
 
 - Drop support for pygeos, which was replaced by shapely v2.0 (:pull:`519`).
+- Require shapely v2.0+ (:pull:`521`).
 - Bumped minimum rasterio version to v1.3 (:issue:`347`).
 
-- The minimum versions of some dependencies were changed (:pull:`541`):
+- The minimum versions of some dependencies were changed (:pull:`541`, :pull:`546`, and :pull:`521`):
 
   ============ ===== =====
   Package      Old   New
@@ -33,7 +34,7 @@ Breaking Changes
   pandas       1.3   2.0
   pooch        1.4   1.7
   rasterio     1.2   1.3
-  shapely      1.8   1.8
+  shapely      1.8   2.0
   xarray       0.20  2023.07
   ============ ===== =====
 
diff --git a/ci/requirements/bare-minimum.yml b/ci/requirements/bare-minimum.yml
index b978665..50dac88 100644
--- a/ci/requirements/bare-minimum.yml
+++ b/ci/requirements/bare-minimum.yml
@@ -9,7 +9,7 @@ dependencies:
   - packaging
   - pooch
   - rasterio
-  - shapely=1.8 # fixing shapely for now
+  - shapely
   - xarray
 # for testing
   - pytest
diff --git a/ci/requirements/min-all-deps.yml b/ci/requirements/min-all-deps.yml
index 15d1dc5..2873375 100644
--- a/ci/requirements/min-all-deps.yml
+++ b/ci/requirements/min-all-deps.yml
@@ -15,7 +15,7 @@ dependencies:
   # pyogrio makes environment solving difficult
   - pyogrio=0.5
   - rasterio=1.3
-  - shapely=1.8
+  - shapely=2.0
   - xarray=2023.7
 # for testing
   - pytest
diff --git a/ci/requirements/shapely_lt_2.yml b/ci/requirements/shapely_lt_2.yml
deleted file mode 100644
index 162d466..0000000
--- a/ci/requirements/shapely_lt_2.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-name: regionmask-tests
-channels:
-  - conda-forge
-  - nodefaults
-dependencies:
-  - cartopy
-  - geopandas
-  - matplotlib-base
-  - numpy
-  - packaging
-  - pooch
-  - rasterio
-  - shapely<2
-  - xarray
-# for testing
-  - pytest
-  - pytest-cov
-  - pytest-timeout
diff --git a/docs/source/installation.rst b/docs/source/installation.rst
index 2bdd7ab..5a5eb95 100644
--- a/docs/source/installation.rst
+++ b/docs/source/installation.rst
@@ -10,7 +10,7 @@ Required dependencies
 - `packaging <https://packaging.pypa.io/en/latest/>`__ (23.1 or later)
 - `pooch <https://www.fatiando.org/pooch/latest/>`__ (1.7 or later)
 - `rasterio <https://rasterio.readthedocs.io/>`__ (1.3 or later)
-- `shapely <http://toblerity.org/shapely/>`__ (1.8 or later)
+- `shapely <https://shapely.readthedocs.io/en/stable>`__ (2.0 or later)
 - `xarray <http://xarray.pydata.org/>`__ (2023.07 or later)
 
 Optional dependencies
diff --git a/regionmask/core/mask.py b/regionmask/core/mask.py
index 107b974..6c07c82 100644
--- a/regionmask/core/mask.py
+++ b/regionmask/core/mask.py
@@ -3,7 +3,6 @@ import warnings
 import numpy as np
 import shapely
 import xarray as xr
-from packaging.version import Version
 
 from regionmask.core.coords import _get_coords
 from regionmask.core.utils import (
@@ -18,8 +17,6 @@ from regionmask.core.utils import (
     unpackbits,
 )
 
-has_shapely_2 = Version(shapely.__version__) > Version("2.0b1")
-
 _MASK_DOCSTRING_TEMPLATE = """\
 create a {nd} {qualifier} mask of a set of regions for the given lat/ lon grid
 
@@ -236,8 +233,6 @@ def _mask(
 
     if method is None:
         method = _determine_method(lon_arr, lat_arr)
-    elif method == "shapely" and has_shapely_2:
-        method = "shapely_2"
     elif method == "rasterize":
         method = _determine_method(lon_arr, lat_arr)
         if "rasterize" not in method:
@@ -254,9 +249,6 @@ def _mask(
     elif method == "shapely":
         mask_func = _mask_shapely
         kwargs = {"is_unstructured": is_unstructured}
-    elif method == "shapely_2":
-        mask_func = _mask_shapely_v2
-        kwargs = {"is_unstructured": is_unstructured}
 
     mask = mask_func(lon_arr, lat_arr, polygons, numbers=numbers, as_3D=as_3D, **kwargs)
 
@@ -555,9 +547,6 @@ def _determine_method(lon, lat):
         else:
             return "rasterize_split"
 
-    if has_shapely_2:
-        return "shapely_2"
-
     return "shapely"
 
 
@@ -606,7 +595,7 @@ def _mask_edgepoints_shapely(
     as_3D=False,
 ):
 
-    import shapely.vectorized as shp_vect
+    import shapely.vectorized
 
     LON, LAT, shape = _get_LON_LAT_shape(
         lon, lat, numbers, is_unstructured=is_unstructured, as_3D=as_3D
@@ -649,17 +638,17 @@ def _mask_edgepoints_shapely(
 
     if as_3D:
         for i, polygon in enumerate(polygons):
-            sel = shp_vect.contains(polygon, LON, LAT)
+            sel = shapely.vectorized.contains(polygon, LON, LAT)
             mask[i, idx[sel]] = True
     else:
         for i, polygon in enumerate(polygons):
-            sel = shp_vect.contains(polygon, LON, LAT)
+            sel = shapely.vectorized.contains(polygon, LON, LAT)
             mask[idx[sel]] = numbers[i]
 
     return mask.reshape(shape)
 
 
-def _mask_shapely_v2(
+def _mask_shapely(
     lon, lat, polygons, numbers, fill=np.nan, is_unstructured=False, as_3D=False
 ):
     """create a mask using shapely.STRtree"""
@@ -691,37 +680,6 @@ def _mask_shapely_v2(
     return out.reshape(shape)
 
 
-def _mask_shapely(
-    lon, lat, polygons, numbers, fill=np.nan, is_unstructured=False, as_3D=False
-):
-    """create a mask using shapely.vectorized.contains"""
-
-    import shapely.vectorized
-
-    lon, lat = _parse_input(lon, lat, polygons, fill, numbers)
-
-    LON, LAT, shape = _get_LON_LAT_shape(
-        lon, lat, numbers, is_unstructured=is_unstructured, as_3D=as_3D
-    )
-    out = _get_out(shape, fill, as_3D=as_3D)
-
-    # add a tiny offset to get a consistent edge behaviour
-    LON = LON - 1 * 10**-8
-    LAT = LAT - 1 * 10**-10
-
-    if as_3D:
-        for i, polygon in enumerate(polygons):
-            sel = shapely.vectorized.contains(polygon, LON, LAT)
-            out[i, sel] = True
-
-    else:
-        for i, polygon in enumerate(polygons):
-            sel = shapely.vectorized.contains(polygon, LON, LAT)
-            out[sel] = numbers[i]
-
-    return out.reshape(shape)
-
-
 def _parse_input(lon, lat, coords, fill, numbers):
 
     lon = np.asarray(lon)
diff --git a/requirements.txt b/requirements.txt
index 238630c..c991ee0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,5 +7,5 @@ numpy >= 1.24
 packaging >= 23.1
 pooch >= 1.7
 rasterio >= 1.3
-shapely >= 1.8
+shapely >= 2.0
 xarray >= 2023.7
diff --git a/setup.cfg b/setup.cfg
index bb3cd7d..d0115ef 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -33,7 +33,7 @@ install_requires =
     packaging >= 23.1
     pooch >= 1.7
     rasterio >= 1.3
-    shapely >= 1.8
+    shapely >= 2.0
     xarray >= 2023.07
 
 


Test patch:
diff --git a/regionmask/tests/__init__.py b/regionmask/tests/__init__.py
index ad704d1..27ca2bf 100644
--- a/regionmask/tests/__init__.py
+++ b/regionmask/tests/__init__.py
@@ -30,4 +30,3 @@ def assert_no_warnings():
 has_cartopy, requires_cartopy = _importorskip("cartopy")
 has_cf_xarray, requires_cf_xarray = _importorskip("cf_xarray")
 has_matplotlib, requires_matplotlib = _importorskip("matplotlib")
-has_shapely_2, requires_shapely_2 = _importorskip("shapely", minversion="2.0b1")
diff --git a/regionmask/tests/test_mask.py b/regionmask/tests/test_mask.py
index 4383126..3e529c1 100644
--- a/regionmask/tests/test_mask.py
+++ b/regionmask/tests/test_mask.py
@@ -13,11 +13,10 @@ from regionmask.core.mask import (
     _mask_rasterize,
     _mask_rasterize_no_offset,
     _mask_shapely,
-    _mask_shapely_v2,
     _transform_from_latlon,
 )
 from regionmask.core.utils import _wrapAngle, create_lon_lat_dataarray_from_bounds
-from regionmask.tests import assert_no_warnings, has_shapely_2, requires_shapely_2
+from regionmask.tests import assert_no_warnings
 from regionmask.tests.utils import (
     dummy_ds,
     dummy_region,
@@ -30,7 +29,6 @@ from regionmask.tests.utils import (
 MASK_FUNCS = [
     _mask_rasterize,
     _mask_shapely,
-    pytest.param(_mask_shapely_v2, marks=requires_shapely_2),
 ]
 
 MASK_METHODS = [
@@ -873,17 +871,11 @@ def test_rasterize_on_split_lon_asymmetric():
     xr.testing.assert_equal(result, expected)
 
 
-if has_shapely_2:
-    METHOD_IRREGULAR = "shapely_2"
-else:
-    METHOD_IRREGULAR = "shapely"
-
-
 METHODS = {
     0: "rasterize",
     1: "rasterize_flip",
     2: "rasterize_split",
-    3: METHOD_IRREGULAR,
+    3: "shapely",  # METHOD_IRREGULAR
 }
 
 equal = np.arange(0.5, 360)
diff --git a/regionmask/tests/test_segmentize.py b/regionmask/tests/test_segmentize.py
index a382c8f..223824f 100644
--- a/regionmask/tests/test_segmentize.py
+++ b/regionmask/tests/test_segmentize.py
@@ -2,7 +2,6 @@ import numpy as np
 import pytest
 
 from regionmask.core.plot import _get_tolerance, segmentize
-from regionmask.tests import requires_shapely_2
 
 
 def test_get_tolerance():
@@ -94,7 +93,6 @@ COORDS = (
 )
 
 
-@requires_shapely_2
 @pytest.mark.parametrize("coords", COORDS)
 @pytest.mark.parametrize("tolerance", [0.3, 1.0, 2.7852])
 def test_segmentize_shapely(coords, tolerance):