Repository: regionmask/regionmask

Issue:
regionmask assumes data is in lat/ lon format
It is currently not possible to mask data that is not in lat/ lon format. Due to the `is_180` check.

It could be good to allow this if the regions and the x & y coordinates both share the same projection (!= WSG84).

Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/regionmask/regionmask/pull/255?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask) Report
> Merging [#255](https://codecov.io/gh/regionmask/regionmask/pull/255?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask) (a473eac) into [master](https://codecov.io/gh/regionmask/regionmask/commit/ca7a6cf04d91ea32678acb08bbfdbe4fc6507547?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask) (ca7a6cf) will **decrease** coverage by `11.70%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/regionmask/regionmask/pull/255/graphs/tree.svg?width=650&height=150&src=pr&token=WI9PSJZIB7&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask)](https://codecov.io/gh/regionmask/regionmask/pull/255?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask)

```diff
@@             Coverage Diff             @@
##           master     #255       +/-   ##
===========================================
- Coverage   98.94%   87.24%   -11.71%     
===========================================
  Files          14       14               
  Lines        1045     1011       -34     
===========================================
- Hits         1034      882      -152     
- Misses         11      129      +118     
```

| Flag | Coverage Δ | |
|---|---|---|
| py36-bare-minimum | `87.24% <100.00%> (ø)` | |
| py36-min-all-deps | `?` | |
| unittests | `87.24% <100.00%> (-11.71%)` | :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.

| [Impacted Files](https://codecov.io/gh/regionmask/regionmask/pull/255?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask) | Coverage Δ | |
|---|---|---|
| [regionmask/core/regions.py](https://codecov.io/gh/regionmask/regionmask/pull/255/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask#diff-cmVnaW9ubWFzay9jb3JlL3JlZ2lvbnMucHk=) | `96.57% <ø> (-0.95%)` | :arrow_down: |
| [regionmask/core/\_geopandas.py](https://codecov.io/gh/regionmask/regionmask/pull/255/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask#diff-cmVnaW9ubWFzay9jb3JlL19nZW9wYW5kYXMucHk=) | `97.26% <100.00%> (-0.04%)` | :arrow_down: |
| [regionmask/core/mask.py](https://codecov.io/gh/regionmask/regionmask/pull/255/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask#diff-cmVnaW9ubWFzay9jb3JlL21hc2sucHk=) | `99.50% <100.00%> (+<0.01%)` | :arrow_up: |
| [regionmask/core/utils.py](https://codecov.io/gh/regionmask/regionmask/pull/255/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask#diff-cmVnaW9ubWFzay9jb3JlL3V0aWxzLnB5) | `100.00% <100.00%> (ø)` | |
| [regionmask/defined\_regions/natural\_earth.py](https://codecov.io/gh/regionmask/regionmask/pull/255/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask#diff-cmVnaW9ubWFzay9kZWZpbmVkX3JlZ2lvbnMvbmF0dXJhbF9lYXJ0aC5weQ==) | `41.58% <0.00%> (-57.50%)` | :arrow_down: |
| [regionmask/core/plot.py](https://codecov.io/gh/regionmask/regionmask/pull/255/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask#diff-cmVnaW9ubWFzay9jb3JlL3Bsb3QucHk=) | `63.43% <0.00%> (-36.57%)` | :arrow_down: |
| [regionmask/core/formatting.py](https://codecov.io/gh/regionmask/regionmask/pull/255/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask#diff-cmVnaW9ubWFzay9jb3JlL2Zvcm1hdHRpbmcucHk=) | `65.62% <0.00%> (-31.25%)` | :arrow_down: |
| [regionmask/defined\_regions/\_ar6.py](https://codecov.io/gh/regionmask/regionmask/pull/255/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask#diff-cmVnaW9ubWFzay9kZWZpbmVkX3JlZ2lvbnMvX2FyNi5weQ==) | `100.00% <0.00%> (ø)` | |
| ... and [1 more](https://codecov.io/gh/regionmask/regionmask/pull/255/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/regionmask/regionmask/pull/255?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/regionmask/regionmask/pull/255?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask). Last update [ca7a6cf...a473eac](https://codecov.io/gh/regionmask/regionmask/pull/255?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=regionmask).


Patch:
diff --git a/docs/notebooks/method.ipynb b/docs/notebooks/method.ipynb
index a729c73..20657eb 100644
--- a/docs/notebooks/method.ipynb
+++ b/docs/notebooks/method.ipynb
@@ -34,7 +34,7 @@
    "cell_type": "raw",
    "metadata": {},
    "source": [
-    ".. note:: From version 0.5 ``regionmask`` treats points on the region borders differently and also considers poygon interiors (holes), e.g. the Caspian Sea in ``natural_earth.land_110`` region."
+    ".. note:: From version 0.5 `regionmask` treats points on the region borders differently and also considers poygon interiors (holes), e.g. the Caspian Sea in the `natural_earth.land_110` region."
    ]
   },
   {
@@ -296,8 +296,20 @@
    "source": [
     "### Points at -180°E (0°E) and -90°N\n",
     "\n",
-    "The described edge behaviour leads to a consistent treatment of points on the border. However, gridpoints at -180°E (or 0°E) and -90°N would *never* fall in any region.\n",
-    "\n",
+    "The described edge behaviour leads to a consistent treatment of points on the border. However, gridpoints at -180°E (or 0°E) and -90°N would *never* fall in any region."
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "metadata": {},
+   "source": [
+    ".. note:: From version 0.8 this applies only if ``wrap_lon`` is *not* set to ``False``. If wrap_lon is set to False `regionmask` assumes the coordinates are not lat and lon coordinates."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
     "We exemplify this with a region spanning the whole globe and a coarse longitude/ latidude grid:"
    ]
   },
@@ -331,12 +343,10 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "mask_global = region_global.mask(lon, lat)\n",
+    "# setting `wrap_lon=False` turns this feature off\n",
+    "mask_global_nontreat = region_global.mask(lon, lat, wrap_lon=False)\n",
     "\n",
-    "# we need to manually create the mask\n",
-    "mask_global_nontreat = mask_global.copy()\n",
-    "mask_global_nontreat[-1, :] = np.NaN\n",
-    "mask_global_nontreat[:, 0] = np.NaN"
+    "mask_global = region_global.mask(lon, lat)"
    ]
   },
   {
@@ -547,7 +557,7 @@
  ],
  "metadata": {
   "kernelspec": {
-   "display_name": "Python 3",
+   "display_name": "Python 3 (ipykernel)",
    "language": "python",
    "name": "python3"
   },
@@ -561,7 +571,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.7.6"
+   "version": "3.8.6"
   }
  },
  "nbformat": 4,
diff --git a/docs/whats_new.rst b/docs/whats_new.rst
index 55cef68..adc248b 100644
--- a/docs/whats_new.rst
+++ b/docs/whats_new.rst
@@ -16,23 +16,29 @@ v0.8.0 (unreleased)
 Breaking Changes
 ~~~~~~~~~~~~~~~~
 
- - Updates to :py:meth:`Regions.plot` and :py:meth:`Regions.plot_regions` (:pull:`246`):
-
-   - Deprecated all positional arguments (keword arguments only).
-   - The ``regions`` keyword was deprecated. Subset regions before plotting, i.e.
-     use ``r[regions].plot()`` instead of ``r.plot(regions=regions)``. This will allow
-     to remove a argument from the methods.
- - Updates to :py:meth:`Regions.plot` (:pull:`246`):
-
-   - Added ``lw=0`` to the default ``ocean_kws`` and ``land_kws`` to avoid overlap with
-     the coastlines.
-   - Renamed the ``proj`` keyword to ``projection`` for consistency with cartopy.
-   - Renamed the ``coastlines`` keyword to ``add_coastlines`` for consistency with other
-     keywords (e.g. ``add_land``).
+- Points at *exactly* -180°E (or 0°E) and -90°N are no longer special cased if
+  ``wrap_lon=False`` when creating a mask - see :doc:`methods<notebooks/method>` for
+  details (:issue:`151`).
+- Updates to :py:meth:`Regions.plot` and :py:meth:`Regions.plot_regions` (:pull:`246`):
+
+  - Deprecated all positional arguments (keword arguments only).
+  - The ``regions`` keyword was deprecated. Subset regions before plotting, i.e.
+    use ``r[regions].plot()`` instead of ``r.plot(regions=regions)``. This will allow
+    to remove a argument from the methods.
+- Updates to :py:meth:`Regions.plot` (:pull:`246`):
+
+  - Added ``lw=0`` to the default ``ocean_kws`` and ``land_kws`` to avoid overlap with
+    the coastlines.
+  - Renamed the ``proj`` keyword to ``projection`` for consistency with cartopy.
+  - Renamed the ``coastlines`` keyword to ``add_coastlines`` for consistency with other
+    keywords (e.g. ``add_land``).
 
 Enhancements
 ~~~~~~~~~~~~
 
+- Can now create masks for regions with arbitrary coordinates e.g. for coordinate reference
+  systems that are not lat/ lon based by setting ``wrap_lon=False`` (:issue:`151`).
+
 Deprecations
 ~~~~~~~~~~~~
 
@@ -42,6 +48,9 @@ New regions
 Bug Fixes
 ~~~~~~~~~
 
+- :py:meth:`Regions.mask` (and all other ``mask`` methods and functions) no longer raise
+  an error for regions that exceed 360° latitude if ``wrap_lon=False``. This was most
+  likely a regression from :pull:`48` (:issue:`151`).
 - Raise a ValueError if the input coordinates (lat and lon) have wrong number of dimensions
   or shape (:pull:`245`, :issue:`242`).
 
@@ -158,8 +167,8 @@ v0.6.2 (19.01.2021)
 -------------------
 
 This is a minor bugfix release that corrects a problem occurring only in python 2.7 which
-could lead to wrong coordinates of 3D masks derived with :py:attr:`Regions.mask_3D` and
-:py:attr:`mask_3D_geopandas`.
+could lead to wrong coordinates of 3D masks derived with :py:meth:`Regions.mask_3D` and
+:py:func:`mask_3D_geopandas`.
 
 Bug Fixes
 ~~~~~~~~~
@@ -168,8 +177,8 @@ Bug Fixes
   previously not always the case. Either when creating regions with unsorted numbers
   in python 3.6 and higher (e.g. ``Regions([poly2, poly1], [2, 1])``) or when indexing
   regions in python 2.7 (e.g. ``regionmask.defined_regions.ar6.land[[30, 31, 32]]`` sorts
-  the regions as 32, 30, 31). This can lead to problems for :py:attr:`Regions.mask_3D` and
-  :py:attr:`mask_3D_geopandas` (:issue:`200`).
+  the regions as 32, 30, 31). This can lead to problems for :py:meth:`Regions.mask_3D` and
+  :py:func:`mask_3D_geopandas` (:issue:`200`).
 
 v0.6.1 (19.08.2020)
 -------------------
@@ -207,7 +216,7 @@ Breaking Changes
 Enhancements
 ~~~~~~~~~~~~
 
-- Create 3D boolean masks using :py:attr:`Regions.mask_3D` and :py:attr:`mask_3D_geopandas`
+- Create 3D boolean masks using :py:meth:`Regions.mask_3D` and :py:func:`mask_3D_geopandas`
   - see the :doc:`tutorial on 3D masks<notebooks/mask_3D>` (:issue:`4`, :issue:`73`).
 - Create regions from geopandas/ shapefiles :py:attr:`from_geopandas`
   (:pull:`101` by `Aaron Spring <https://github.com/aaronspring>`_).
diff --git a/regionmask/core/_geopandas.py b/regionmask/core/_geopandas.py
index 663e61d..6173f8b 100644
--- a/regionmask/core/_geopandas.py
+++ b/regionmask/core/_geopandas.py
@@ -3,7 +3,6 @@ import numpy as np
 from ..defined_regions.natural_earth import _maybe_get_column
 from .mask import _inject_mask_docstring, _mask_2D, _mask_3D
 from .regions import Regions
-from .utils import _is_180
 
 
 def _check_duplicates(data, name):
@@ -138,7 +137,7 @@ def _prepare_gdf_for_mask(geodataframe, method, numbers):
 
     lon_min = geodataframe.bounds["minx"].min()
     lon_max = geodataframe.bounds["maxx"].max()
-    is_180 = _is_180(lon_min, lon_max)
+    lon_bounds = [lon_min, lon_max]
 
     polygons = geodataframe.geometry.tolist()
 
@@ -149,7 +148,7 @@ def _prepare_gdf_for_mask(geodataframe, method, numbers):
     else:
         numbers = geodataframe.index.values
 
-    return polygons, is_180, numbers
+    return polygons, lon_bounds, numbers
 
 
 def mask_geopandas(
@@ -163,13 +162,13 @@ def mask_geopandas(
     wrap_lon=None,
 ):
 
-    polygons, is_180, numbers = _prepare_gdf_for_mask(
+    polygons, lon_bounds, numbers = _prepare_gdf_for_mask(
         geodataframe, method=method, numbers=numbers
     )
 
     return _mask_2D(
         outlines=polygons,
-        regions_is_180=is_180,
+        lon_bounds=lon_bounds,
         numbers=numbers,
         lon_or_obj=lon_or_obj,
         lat=lat,
@@ -195,13 +194,13 @@ def mask_3D_geopandas(
     wrap_lon=None,
 ):
 
-    polygons, is_180, numbers = _prepare_gdf_for_mask(
+    polygons, lon_bounds, numbers = _prepare_gdf_for_mask(
         geodataframe, method=method, numbers=numbers
     )
 
     mask_3D = _mask_3D(
         outlines=polygons,
-        regions_is_180=is_180,
+        lon_bounds=lon_bounds,
         numbers=numbers,
         lon_or_obj=lon_or_obj,
         lat=lat,
diff --git a/regionmask/core/mask.py b/regionmask/core/mask.py
index ab5b50a..7b004bf 100644
--- a/regionmask/core/mask.py
+++ b/regionmask/core/mask.py
@@ -37,11 +37,16 @@ wrap_lon : bool | 180 | 360, optional
     Whether to wrap the longitude around, inferred automatically.
     If the regions and the provided longitude do not have the same
     base (i.e. one is -180..180 and the other 0..360) one of them
-    must be wrapped. This can be achieved with wrap_lon.
-    If wrap_lon is None autodetects whether the longitude needs to be
-    wrapped. If wrap_lon is False, nothing is done. If wrap_lon is True,
-    longitude data is wrapped to 360 if its minimum is smaller
-    than 0 and wrapped to 180 if its maximum is larger than 180.
+    must be wrapped. This can be achieved with wrap_lon:
+
+    - ``None``: autodetects whether the longitude needs to be wrapped
+    - ``False``: nothing is done. This is useful when working with coordinates that are
+      not in lat/ lon format.
+    - ``True``: longitude data is wrapped to `[0, 360[` if its minimum is smaller than 0
+      and wrapped to `[-180, 180[` if its maximum is larger than 180.
+    - ``180``: Wraps longitude coordinates to `[-180, 180[`
+    - ``360``: Wraps longitude coordinates to `[0, 360[`
+
 
 Returns
 -------
@@ -90,7 +95,7 @@ def _inject_mask_docstring(is_3D, gp_method):
 
 def _mask(
     outlines,
-    regions_is_180,
+    lon_bounds,
     numbers,
     lon_or_obj,
     lat=None,
@@ -115,13 +120,18 @@ def _mask(
 
     # automatically detect whether wrapping is necessary
     if wrap_lon is None:
-        grid_is_180 = _is_180(lon.min(), lon.max())
+        msg_add = "Set `wrap_lon=False` to skip this check."
+        grid_is_180 = _is_180(lon.min(), lon.max(), msg_add=msg_add)
 
-        wrap_lon = not regions_is_180 == grid_is_180
+        regions_is_180 = _is_180(*lon_bounds, msg_add=msg_add)
+
+        wrap_lon_ = not regions_is_180 == grid_is_180
+    else:
+        wrap_lon_ = wrap_lon
 
     lon_orig = lon.copy()
-    if wrap_lon:
-        lon = _wrapAngle(lon, wrap_lon)
+    if wrap_lon_:
+        lon = _wrapAngle(lon, wrap_lon_)
 
     if method not in (None, "rasterize", "shapely"):
         msg = "Method must be None or one of 'rasterize' and 'shapely'."
@@ -144,8 +154,10 @@ def _mask(
     elif method == "shapely":
         mask = _mask_shapely(lon, lat, outlines, numbers=numbers)
 
-    # we need to treat the points at -180°E/0°E and -90°N
-    mask = _mask_edgepoints_shapely(mask, lon, lat, outlines, numbers)
+    # not False required
+    if wrap_lon is not False:
+        # treat the points at -180°E/0°E and -90°N
+        mask = _mask_edgepoints_shapely(mask, lon, lat, outlines, numbers)
 
     # create an xr.DataArray
     if lon.ndim == 1:
@@ -158,7 +170,7 @@ def _mask(
 
 def _mask_2D(
     outlines,
-    regions_is_180,
+    lon_bounds,
     numbers,
     lon_or_obj,
     lat=None,
@@ -170,7 +182,7 @@ def _mask_2D(
 
     mask = _mask(
         outlines=outlines,
-        regions_is_180=regions_is_180,
+        lon_bounds=lon_bounds,
         numbers=numbers,
         lon_or_obj=lon_or_obj,
         lat=lat,
@@ -189,7 +201,7 @@ def _mask_2D(
 
 def _mask_3D(
     outlines,
-    regions_is_180,
+    lon_bounds,
     numbers,
     lon_or_obj,
     lat=None,
@@ -202,7 +214,7 @@ def _mask_3D(
 
     mask = _mask(
         outlines=outlines,
-        regions_is_180=regions_is_180,
+        lon_bounds=lon_bounds,
         numbers=numbers,
         lon_or_obj=lon_or_obj,
         lat=lat,
diff --git a/regionmask/core/regions.py b/regionmask/core/regions.py
index 6954399..d5e1efe 100644
--- a/regionmask/core/regions.py
+++ b/regionmask/core/regions.py
@@ -288,7 +288,7 @@ class Regions:
 
         return _mask_2D(
             outlines=self.polygons,
-            regions_is_180=self.lon_180,
+            lon_bounds=self.bounds_global[::2],
             numbers=self.numbers,
             lon_or_obj=lon_or_obj,
             lat=lat,
@@ -313,7 +313,7 @@ class Regions:
 
         mask_3D = _mask_3D(
             outlines=self.polygons,
-            regions_is_180=self.lon_180,
+            lon_bounds=self.bounds_global[::2],
             numbers=self.numbers,
             lon_or_obj=lon_or_obj,
             lat=lat,
diff --git a/regionmask/core/utils.py b/regionmask/core/utils.py
index 6c3d8ac..65cb86a 100644
--- a/regionmask/core/utils.py
+++ b/regionmask/core/utils.py
@@ -59,7 +59,7 @@ def _sanitize_names_abbrevs(numbers, values, default):
 
 
 def _wrapAngle360(lon):
-    """wrap angle to [0, 360[."""
+    """wrap angle to `[0, 360[`."""
     lon = np.array(lon)
     return np.mod(lon, 360)
 
@@ -68,7 +68,7 @@ def _wrapAngle360(lon):
 
 
 def _wrapAngle180(lon):
-    """wrap angle to [-180,180[."""
+    """wrap angle to `[-180, 180[`."""
     lon = np.array(lon)
     sel = (lon < -180) | (180 <= lon)
     lon[sel] = _wrapAngle360(lon[sel] + 180) - 180
@@ -89,12 +89,7 @@ def _wrapAngle(lon, wrap_lon=True):
     new_lon = lon
 
     if wrap_lon is True:
-        if lon.min() < 0 and lon.max() > 180:
-            msg = (
-                "lon has both data that is larger than 180 and "
-                "smaller than 0. Cannot infer the transformation."
-            )
-            raise RuntimeError(msg)
+        _is_180(lon.min(), lon.max(), msg_add="Cannot infer the transformation.")
 
     wl = int(wrap_lon)
 
@@ -107,8 +102,7 @@ def _wrapAngle(lon, wrap_lon=True):
     # check if they are still unique
     if new_lon.ndim == 1:
         if new_lon.shape != np.unique(new_lon).shape:
-            msg = "There are equal longitude coordinates (when wrapped)!"
-            raise IndexError(msg)
+            raise ValueError("There are equal longitude coordinates (when wrapped)!")
 
     return new_lon
 
@@ -116,13 +110,13 @@ def _wrapAngle(lon, wrap_lon=True):
 # -----------------------------------------------------------------------------
 
 
-def _is_180(lon_min, lon_max):
+def _is_180(lon_min, lon_max, msg_add=""):
 
     lon_min = np.round(lon_min, 6)
     lon_max = np.round(lon_max, 6)
 
     if (lon_min < 0) and (lon_max > 180):
-        msg = "lon has both data that is larger than 180 and smaller than 0."
+        msg = "lon has both data that is larger than 180 and smaller than 0. " + msg_add
         raise ValueError(msg)
 
     return lon_max <= 180


Test patch:
diff --git a/regionmask/tests/test_geopandas.py b/regionmask/tests/test_geopandas.py
index 2997481..5bcfd03 100644
--- a/regionmask/tests/test_geopandas.py
+++ b/regionmask/tests/test_geopandas.py
@@ -2,6 +2,7 @@ import geopandas as gp
 import numpy as np
 import pandas as pd
 import pytest
+import shapely
 import xarray as xr
 
 from regionmask import Regions, from_geopandas, mask_3D_geopandas, mask_geopandas
@@ -282,6 +283,27 @@ def test_mask_3D_geopandas_warns_empty(geodataframe_clean, drop):
     assert np.all(np.equal(result.lon.values, [10]))
 
 
+@pytest.mark.parametrize("func", [mask_geopandas, mask_3D_geopandas])
+def test_wrap_lon_maybe_error(func):
+
+    # regions that exceed 360° longitude
+    p = shapely.geometry.Polygon([[-180, 0], [-180, 10], [360, 10], [360, 0]])
+    gs = gp.GeoSeries(p, index=[1])
+    # lons that exceed 360° longitude
+    lon = np.arange(-175, 360, 2.5)
+    lat = np.arange(10, 1, -3)
+
+    mask = func(gs, lon, lat, wrap_lon=False)
+
+    # the region index is 1 -> thus this works for 2D and 3D masks
+    assert (mask == 1).all()
+    np.testing.assert_equal(lon, mask.lon)
+    np.testing.assert_equal(lat, mask.lat)
+
+    with pytest.raises(ValueError, match="Set `wrap_lon=False` to skip this check."):
+        func(gs, lon, lat)
+
+
 @pytest.mark.parametrize("func", [mask_geopandas, mask_3D_geopandas])
 def test_mask_geopandas_wrong_input(func):
 
diff --git a/regionmask/tests/test_mask.py b/regionmask/tests/test_mask.py
index 6faabae..6516608 100644
--- a/regionmask/tests/test_mask.py
+++ b/regionmask/tests/test_mask.py
@@ -195,6 +195,45 @@ def test_mask_wrap(method):
     assert np.allclose(result, expected, equal_nan=True)
 
 
+@pytest.mark.parametrize("meth", ["mask", "mask_3D"])
+def test_wrap_lon_no_error_wrap_lon_false(meth):
+
+    # regions that exceed 360° longitude
+    r = Regions([[[-180, 0], [-180, 10], [360, 10], [360, 0]]], numbers=[1])
+
+    # lons that exceed 360° longitude
+    lon = np.arange(-175, 360, 2.5)
+    lat = np.arange(10, 1, -3)
+
+    mask = getattr(r, meth)(lon, lat, wrap_lon=False)
+
+    # the region index is 1 -> thus this works for 2D and 3D masks
+    assert (mask == 1).all()
+    np.testing.assert_equal(lon, mask.lon)
+    np.testing.assert_equal(lat, mask.lat)
+
+    # -180° is not special cased (no _mask_edgepoints_shapely)
+    lon = [-180]
+    mask = getattr(r, meth)(lon, lat, wrap_lon=False)
+    assert (mask != 1).all()
+    np.testing.assert_equal(lon, mask.lon)
+    np.testing.assert_equal(lat, mask.lat)
+
+
+@pytest.mark.parametrize("meth", ["mask", "mask_3D"])
+def test_wrap_lon_error_wrap_lon(meth):
+
+    # regions that exceed 360° longitude
+    r = Regions([[[-180, 0], [-180, 10], [360, 10], [360, 0]]])
+
+    # lons that exceed 360° longitude
+    lon = np.arange(-180, 360, 2.5)
+    lat = np.arange(10, 1, -3)
+
+    with pytest.raises(ValueError, match="Set `wrap_lon=False` to skip this check."):
+        getattr(r, meth)(lon, lat)
+
+
 @pytest.mark.parametrize("method", ["rasterize", "shapely"])
 def test_mask_autowrap(method):
 
@@ -677,54 +716,14 @@ def test_inject_mask_docstring():
 
     result = _inject_mask_docstring(True, True)
 
-    expected = """\
-create a 3D float mask of a set of regions for the given lat/ lon grid
-
-Parameters
-----------
-geodataframe : GeoDataFrame or GeoSeries
-    Object providing the region definitions (outlines).
-lon_or_obj : object or array_like
-    Can either be a longitude array and then ``lat`` needs to be
-    given. Or an object where the longitude and latitude can be
-    retrived as: ``lon = lon_or_obj[lon_name]`` and
-    ``lat = lon_or_obj[lat_name]``
-lat : array_like, optional
-    If ``lon_or_obj`` is a longitude array, the latitude needs to be
-    specified here.
-drop : boolean, optional
-    If True (default) drops slices where all elements are False (i.e no
-    gridpoints are contained in a region). If False returns one slice per
-    region.
-lon_name : str, optional
-    Name of longitude in ``lon_or_obj``. Default: "lon".
-lat_name : str, optional
-    Name of latgitude in ``lon_or_obj``. Default: "lat"
-numbers : str, optional
-    Name of the column to use for numbering the regions.
-    This column must not have duplicates. If None (default),
-    takes ``geodataframe.index.values``.
-method : "rasterize" | "shapely", optional
-    Method used to determine whether a gridpoint lies in a region.
-    Both methods should lead to the same result. If None (default)
-    autoselects the method depending on the grid spacing.
-wrap_lon : bool | 180 | 360, optional
-    Whether to wrap the longitude around, inferred automatically.
-    If the regions and the provided longitude do not have the same
-    base (i.e. one is -180..180 and the other 0..360) one of them
-    must be wrapped. This can be achieved with wrap_lon.
-    If wrap_lon is None autodetects whether the longitude needs to be
-    wrapped. If wrap_lon is False, nothing is done. If wrap_lon is True,
-    longitude data is wrapped to 360 if its minimum is smaller
-    than 0 and wrapped to 180 if its maximum is larger than 180.
-
-Returns
--------
-mask_3D : float xarray.DataArray
-
-References
-----------
-See https://regionmask.readthedocs.io/en/stable/notebooks/method.html
-"""
-
-    assert result == expected
+    assert "3D" in result
+    assert "2D" not in result
+    assert "drop :" in result
+    assert "geodataframe" in result
+
+    result = _inject_mask_docstring(False, False)
+
+    assert "2D" in result
+    assert "3D" not in result
+    assert "drop :" not in result
+    assert "geodataframe" not in result
diff --git a/regionmask/tests/test_utils.py b/regionmask/tests/test_utils.py
index 6d241d2..9afa8db 100644
--- a/regionmask/tests/test_utils.py
+++ b/regionmask/tests/test_utils.py
@@ -86,6 +86,9 @@ def test_is_180():
     with pytest.raises(ValueError, match="lon has both data that is larger than 180"):
         _is_180(-1, 181)
 
+    with pytest.raises(ValueError, match="and smaller than 0. Additional text."):
+        _is_180(-1, 181, "Additional text.")
+
 
 @pytest.mark.parametrize("lon_vals", [(-161, -29, 2), (-180, 181, 2)])
 @pytest.mark.parametrize("lat_vals", [(75, 13, -2), (90, -91, -2)])
diff --git a/regionmask/tests/test_wrapAngle.py b/regionmask/tests/test_wrapAngle.py
index d7eba9b..10c7581 100644
--- a/regionmask/tests/test_wrapAngle.py
+++ b/regionmask/tests/test_wrapAngle.py
@@ -54,9 +54,9 @@ def test__wrapAngle():
 
     assert np.allclose(result, expected)
 
-    raises(RuntimeError, _wrapAngle, [-1, 181])
+    raises(ValueError, _wrapAngle, [-1, 181])
 
-    raises(IndexError, _wrapAngle, [0, 0])
+    raises(ValueError, _wrapAngle, [0, 0])
 
     # test explicit argument
     assert _wrapAngle(-1, 180) == -1