Repository: Toblerity/Shapely
Pull request #1180 (2021-08-21 17:11:30)

Add migration guide with explanation how to update your code for Shapely 1.8 / 2.0
In preparation of a next (final?) Shapely 1.8 release, I wrote up a more detailed page on the warnings included in Shapely 1.8 (and upcoming changes in 2.0), and how to update your code to get rid of the warnings / prepare it for Shapely 2.0.


jorisvandenbossche: I should probably still add some more content about putting geometries in numpy arrays, cfr https://github.com/Toblerity/Shapely/issues/1096

Patch:
diff --git a/CHANGES.txt b/CHANGES.txt
index 8b12c78..14db836 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,8 +1,24 @@
 Changes
 =======
 
-1.8a2 (TBD)
------------
+1.8a3 (2021-08-24)
+------------------
+
+Deprecations:
+
+The STRtree class deprecation warnings have been removed. The class in 2.0.0
+will be backwards compatible with the class in 1.8.0.
+
+Bug fixes:
+
+- The __array_interface__ raises only AttributeError, all other exceptions are
+  deprecated starting with Numpy 1.21 (#1173).
+- The STRtree class now uses a pair of item, geom sequences internally instead
+  of a dict (#1177).
+
+
+1.8a2 (2021-07-15)
+------------------
 
 Python version support:
 
@@ -31,13 +47,18 @@ Bug fixes:
   geometry are avoided by never passing an empty geometry (#1134).
 - Python's builtin super() is now used only as described in PEP 3135 (#1109).
 - Only load conda GEOS dll if it exists (on Windows) (#1108).
+- Add /opt/homebrew/lib to the list of directories to be searched for the GEOS
+  shared library.
+- Added new library search path to assist app creation with cx_Freeze.
 
 1.8a1 (2021-03-03)
 ------------------
 
 Shapely 1.8.0 will be a transitional version. There are a few bug fixes and new
 features, but it is mainly about warning of the upcoming changes in 2.0.0.
-Several more pre-releases before 1.8.0 are expected.
+Several more pre-releases before 1.8.0 are expected. See the migration guide
+to Shapely 1.8 / 2.0 for more details on how to update your code
+(https://shapely.readthedocs.io/en/latest/migration.html).
 
 Python version support:
 
@@ -50,7 +71,6 @@ version 2.0.0.
 
 - ops.cascaded_union
 - geometry .empty()
-- geometry .to_wkb() and .to_wkt()
 - geometry .ctypes and .__array_interface__
 - multi-part geometry .__len__
 
@@ -62,6 +82,9 @@ not yet decided, but will be decided before 1.8.0 is released.
 Deprecation warnings will be emitted in 1.8a1 when any of these features are
 used.
 
+The deprecated .to_wkb() and .to_wkt() methods on the geometry objects have
+been removed.
+
 New features:
 
 - Add a normalize() method to geometry classes, exposing the GEOSNormalize
diff --git a/docs/conf.py b/docs/conf.py
index dd3ec47..43a6e3b 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -33,6 +33,7 @@ from shapely.ops import unary_union
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
 extensions = [
     'matplotlib.sphinxext.plot_directive',
+    'sphinx.ext.intersphinx',
     'sphinx.ext.autodoc',
     #'sphinx.ext.pngmath', # <----- pick one, not both
     'sphinx.ext.mathjax', # <--/
@@ -209,3 +210,11 @@ latex_documents = [
 
 # If false, no module index is generated.
 #latex_use_modindex = True
+
+
+#  --Options for sphinx extensions -----------------------------------------------
+
+# connect docs in other projects
+intersphinx_mapping = {
+    'numpy': ('https://numpy.org/doc/stable/', None),
+}
diff --git a/docs/index.rst b/docs/index.rst
index e509d6a..5742ecb 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -11,6 +11,7 @@ Documentation Contents
    The Project <project>
    User Manual <manual>
    API Documentation <modules>
+   migration
 
 Indices and tables
 ==================
diff --git a/docs/manual.rst b/docs/manual.rst
index ca2638a..78d7aaf 100644
--- a/docs/manual.rst
+++ b/docs/manual.rst
@@ -2787,6 +2787,8 @@ All of Shapely's geometry types are supported by these functions.
   >>> wkt.dumps(pt, trim=True)
   'POINT (0 0)'
 
+.. _array-interface:
+
 Numpy and Python Arrays
 -----------------------
 
diff --git a/docs/migration.rst b/docs/migration.rst
new file mode 100644
index 0000000..30c6a76
--- /dev/null
+++ b/docs/migration.rst
@@ -0,0 +1,340 @@
+.. _migration:
+
+==============================
+Migrating to Shapely 1.8 / 2.0
+==============================
+
+Shapely 1.8.0 is a transitional version introducing several warnings in
+preparation of the upcoming changes in 2.0.0.
+
+Shapely 2.0.0 will be a major release with a refactor of the internals with
+considerable performance improvements (based on the developments in the
+`PyGEOS <https://github.com/pygeos/pygeos>`__ package), along with several
+breaking changes.
+
+This guide gives an overview of the most important changes with details
+on what will change in 2.0.0, how we warn for this in 1.8.0, and how
+you can update your code to be future-proof.
+
+For more background, see
+`RFC 1: Roadmap for Shapely 2.0 <https://github.com/shapely/shapely-rfc/pull/1>`__.
+
+.. contents:: Table of Contents
+
+
+Geometry objects will become immutable
+======================================
+
+Geometry objects will become immutable in version 2.0.0.
+
+In Shapely 1.x, some of the geometry classes are mutable, meaning that you
+can change their coordinates in-place. Illustrative code::
+
+    >>> from shapely.geometry import LineString
+    >>> line = LineString([(0,0), (2, 2)])
+    >>> print(line)
+    LINESTRING (0 0, 2 2)
+
+    >>> line.coords = [(0, 0), (10, 0), (10, 10)]
+    >>> print(line)
+    LINESTRING (0 0, 10 0, 10 10)
+
+In Shapely 1.8, this will start raising a warning::
+
+    >>> line.coords = [(0, 0), (10, 0), (10, 10)]
+    ShapelyDeprecationWarning: Setting the 'coords' to mutate a Geometry
+    in place is deprecated, and will not be possible any more in Shapely 2.0
+
+and starting with version 2.0.0, all geometry objects will become immutable.
+As a consequence, they will also become hashable and therefore usable as, for
+example, dictionary keys.
+
+**How do I update my code?** There is no direct alternative for mutating the
+coordinates of an existing geometry, except for creating a new geometry
+object with the new coordinates.
+
+
+Setting custom attributes
+-------------------------
+
+Another consequence of the geometry objects becoming immutable is that
+assigning custom attributes, which currently works, will no longer be possible.
+
+Currently you can do::
+
+    >>> line.name = "my_geometry"
+    >>> line.name
+    'my_geometry'
+
+This doesn't raise a warning in Shapely 1.8, but will start raising an
+AttributeError in Shapely 2.0.
+
+
+Multi-part geometries will no longer be "sequences" (length, iterable, indexable)
+=================================================================================
+
+In Shapely 1.x, multi-part geometries (MultiPoint, MultiLineString,
+MultiPolygon and GeometryCollection) implement a part of the "sequence"
+python interface (making them list-like). This means you can iterate through
+the object to get the parts, index into the object to get a specific part,
+and ask for the number of parts with the ``len()`` method.
+
+Some examples of this with Shapely 1.x:
+
+    >>> from shapely.geometry import Point, MultiPoint
+    >>> mp = MultiPoint([(1, 1), (2, 2), (3, 3)])
+    >>> print(mp)
+    MULTIPOINT (1 1, 2 2, 3 3)
+    >>> for part in mp:
+    ...     print(part)
+    POINT (1 1)
+    POINT (2 2)
+    POINT (3 3)
+    >>> print(mp[1])
+    POINT (2 2)
+    >>> len(mp)
+    3
+    >>> list(mp)
+    [<shapely.geometry.point.Point at 0x7f2e0912bf10>,
+     <shapely.geometry.point.Point at 0x7f2e09fed820>,
+     <shapely.geometry.point.Point at 0x7f2e09fed4c0>]
+
+Starting with Shapely 1.8, all the examples above will start raising a
+deprecation warning. For example:
+
+    >>> for part in mp:
+    ...     print(part)
+    ShapelyDeprecationWarning: Iteration over multi-part geometries is deprecated
+    and will be removed in Shapely 2.0. Use the `geoms` property to access the
+    constituent parts of a multi-part geometry.
+    POINT (1 1)
+    POINT (2 2)
+    POINT (3 3)
+
+In Shapely 2.0, all those examples will raise an error.
+
+**How do I update my code?** To access the geometry parts of a multi-part
+geometry, you can use the ``.geoms`` attribute, as the warning indicates.
+
+The examples above can be updated to::
+
+    >>> for part in mp.geoms:
+    ...     print(part)
+    POINT (1 1)
+    POINT (2 2)
+    POINT (3 3)
+    >>> print(mp.geoms[1])
+    POINT (2 2)
+    >>> len(mp.geoms)
+    3
+    >>> list(mp.geoms)
+    [<shapely.geometry.point.Point at 0x7f2e0912bf10>,
+     <shapely.geometry.point.Point at 0x7f2e09fed820>,
+     <shapely.geometry.point.Point at 0x7f2e09fed4c0>]
+
+The single-part geometries (Point, LineString, Polygon) already didn't
+support those features, and for those classes there is no change in behaviour
+for this aspect.
+
+
+Interopability with NumPy and the array interface
+=================================================
+
+Conversion of the coordinates to (NumPy) arrays
+-----------------------------------------------
+
+Shapely provides an array interface to have easy access to the coordinates as,
+for example, NumPy arrays (:ref:`manual section <array-interface>`).
+
+A small example::
+
+    >>> line = LineString([(0, 0), (1, 1), (2, 2)])
+    >>> import numpy as np
+    >>> np.asarray(line)
+    array([[0., 0.],
+           [1., 1.],
+           [2., 2.]])
+
+In addition, there are also the explicit ``array_interface()`` method and
+``ctypes`` attribute to get access to the coordinates as array data:
+
+    >>> line.ctypes
+    <shapely.geometry.linestring.c_double_Array_6 at 0x7f75261eb740>
+    >>> line.array_interface()
+    {'version': 3,
+     'typestr': '<f8',
+     'data': <shapely.geometry.linestring.c_double_Array_6 at 0x7f752664ae40>,
+     'shape': (3, 2)}
+
+This functionality is available for Point, LineString, LinearRing and MultiPoint.
+
+For more robust interoperability with NumPy, this array interface will be removed
+from those geometry classes, and limited to the ``coords``. 
+
+Starting with Shapely 1.8, converting a geometry object to a NumPy array
+directly will start raising a warning::
+
+    >>> np.asarray(line)
+    ShapelyDeprecationWarning: The array interface is deprecated and will no longer
+    work in Shapely 2.0. Convert the '.coords' to a NumPy array instead.
+    array([[0., 0.],
+           [1., 1.],
+           [2., 2.]])
+
+**How do I update my code?** To convert a geometry to a NumPy array, you can
+convert the ``.coords`` attribute instead::
+
+    >>> line.coords
+    <shapely.coords.CoordinateSequence at 0x7f2e09e88d60>
+    >>> np.array(line.coords)
+    array([[0., 0.],
+           [1., 1.],
+           [2., 2.]])
+
+The ``array_interface()`` method and ``ctypes`` attribute will be removed in
+Shapely 2.0, but since Shapely will start requiring NumPy as a dependency,
+you can use NumPy or its array interface directly. Check the NumPy docs on
+the :py:attr:`ctypes <numpy:numpy.ndarray.ctypes>` attribute
+or the :ref:`array interface <numpy:arrays.interface>` for more details.
+
+Creating NumPy arrays of geometry objects
+-----------------------------------------
+
+Shapely geometry objects can be stored in NumPy arrays using the ``object``
+dtype. In general, one could create such an array from a list of geometries
+as follows::
+
+    >>> from shapely.geometry import Point
+    >>> arr = np.array([Point(0, 0), Point(1, 1), Point(2, 2)])
+    >>> arr
+    array([<shapely.geometry.point.Point object at 0x7fb798407cd0>,
+           <shapely.geometry.point.Point object at 0x7fb7982831c0>,
+           <shapely.geometry.point.Point object at 0x7fb798283b80>],
+          dtype=object)
+
+The above works for point geometries, but because in Shapely 1.x, some
+geometry types are sequence-like (see above), NumPy can try to "unpack" them
+when creating an array. Therefore, for more robust creation of a NumPy array
+from a list of geometries, it's generally recommended to this in a two-step
+way (first creating an empty array and then filling it)::
+
+    geoms = [Point(0, 0), Point(1, 1), Point(2, 2)]
+    arr = np.empty(len(geoms), dtype="object")
+    arr[:] = geoms
+
+This code snippet results in the same array as the example above, and works
+for all geometry types and Shapely/NumPy versions. 
+
+However, starting with Shapely 1.8, the above code will show deprecation
+warnings that cannot be avoided (depending on the geometry type, NumPy tries
+to access the array interface of the objects or check if an object is
+iterable or has a length, and those operations are all deprecated now. The
+end result is still correct, but the warnings appear nonetheless).
+Specifically in this case, it is fine to ignore those warnings (and the only
+way to make them go away)::
+
+    import warnings
+    from shapely.errors import ShapelyDeprecationWarning
+
+    geoms = [Point(0, 0), Point(1, 1), Point(2, 2)]
+    arr = np.empty(len(geoms), dtype="object")
+
+    with warnings.catch_warnings():
+        warnings.filterwarnings("ignore", category=ShapelyDeprecationWarning)
+        arr[:] = geoms
+
+In Shapely 2.0, the geometry objects will no longer be sequence like and
+those deprecation warnings will be removed (and thus the ``filterwarnings``
+will no longer be necessary), and creation of NumPy arrays will generally be
+more robust.
+
+If you maintain code that depends on Shapely, and you want to have it work
+with multiple versions of Shapely, the above code snippet provides a context
+manager that can be copied into your project::
+
+    import contextlib
+    import shapely
+    import warnings
+    from distutils.version import LooseVersion
+
+    SHAPELY_GE_20 = str(shapely.__version__) >= LooseVersion("2.0")
+
+    try:
+        from shapely.errors import ShapelyDeprecationWarning as shapely_warning
+    except ImportError:
+        shapely_warning = None
+
+    if shapely_warning is not None and not SHAPELY_GE_20:
+        @contextlib.contextmanager
+        def ignore_shapely2_warnings():
+            with warnings.catch_warnings():
+                warnings.filterwarnings("ignore", category=shapely_warning)
+                yield
+    else:
+        @contextlib.contextmanager
+        def ignore_shapely2_warnings():
+            yield
+
+This can then be used when creating NumPy arrays (be careful to *only* use it
+for this specific purpose, and not generally suppress those warnings)::
+
+    geoms = [...]
+    arr = np.empty(len(geoms), dtype="object")
+    with ignore_shapely2_warnings():
+        arr[:] = geoms
+
+
+Consistent creation of empty geometries
+=======================================
+
+Shapely 1.x is inconsistent in creating empty geometries between various
+creation methods. A small example for an empty Polygon geometry::
+
+    # Using an empty constructor results in a GeometryCollection
+    >>> from shapely.geometry import Polygon
+    >>> g1 = Polygon()
+    >>> type(g1)
+    <class 'shapely.geometry.polygon.Polygon'>
+    >>> g1.wkt
+    GEOMETRYCOLLECTION EMPTY
+
+    # Converting from WKT gives a correct empty polygon
+    >>> from shapely import wkt
+    >>> g2 = wkt.loads("POLYGON EMPTY")
+    >>> type(g2)
+    <class 'shapely.geometry.polygon.Polygon'>
+    >>> g2.wkt
+    POLYGON EMPTY
+
+Shapely 1.8 does not yet change this inconsistent behaviour, but starting
+with Shapely 2.0, the different methods will always consistently give an
+empty geometry object of the correct type, instead of using an empty
+GeometryCollection as "generic" empty geometry object.
+
+**How do I update my code?** Those cases that will change don't raise a
+warning, but you will need to update your code if you rely on the fact that
+empty geometry objects are of the GeometryCollection type. Use the
+``.is_empty`` attribute for robustly checking if a geometry object is an
+empty geometry.
+
+In addition, the WKB serialization methods will start supporting empty
+Points (using ``"POINT (NaN NaN)"`` to represent an empty point).
+
+
+Other deprecated functionality
+==============================
+
+There are some other various functions and methods deprecated in Shapely 1.8
+as well:
+
+- The adapters to create geometry-like proxy objects with coordinates stored
+  outside Shapely geometries are deprecated and will be removed in Shapely
+  2.0 (e.g. created using ``asShape()``). They have little to no benefit
+  compared to the normal geometry classes, as thus you can convert to your
+  data to a normal geometry object instead. Use the ``shape()`` function
+  instead to convert a GeoJSON-like dict to a Shapely geometry.
+
+- The ``empty()`` method on a geometry object is deprecated.
+
+- The ``shapely.ops.cascaded_union`` function is deprecated. Use
+  ``shapely.ops.unary_union`` instead, which internally already uses a cascaded union operation for better performance.
diff --git a/shapely/__init__.py b/shapely/__init__.py
index 4110415..d4b1f6d 100644
--- a/shapely/__init__.py
+++ b/shapely/__init__.py
@@ -1,1 +1,1 @@
-__version__ = "1.8a2"
+__version__ = "1.8a3"
diff --git a/shapely/_buildcfg.py b/shapely/_buildcfg.py
index 086539b..789b0cc 100644
--- a/shapely/_buildcfg.py
+++ b/shapely/_buildcfg.py
@@ -181,8 +181,10 @@ elif sys.platform == 'darwin':
                 "/Library/Frameworks/GEOS.framework/Versions/Current/GEOS",
                 # macports
                 '/opt/local/lib/libgeos_c.dylib',
-                # homebrew
+                # homebrew Intel
                 '/usr/local/lib/libgeos_c.dylib',
+                # homebrew Apple Silicon
+                '/opt/homebrew/lib/libgeos_c.dylib',
             ]
         lgeos = load_dll('geos_c', fallbacks=alt_paths)
 
diff --git a/shapely/geometry/base.py b/shapely/geometry/base.py
index 2a5eacb..be5ff0c 100644
--- a/shapely/geometry/base.py
+++ b/shapely/geometry/base.py
@@ -309,7 +309,7 @@ class BaseGeometry:
     @property
     def __array_interface__(self):
         """Provide the Numpy array protocol."""
-        raise NotImplementedError
+        raise AttributeError("Not implemented")
 
     # Coordinate access
     # -----------------
diff --git a/shapely/geos.py b/shapely/geos.py
index fe73419..799412d 100644
--- a/shapely/geos.py
+++ b/shapely/geos.py
@@ -131,9 +131,11 @@ elif sys.platform == 'darwin':
                     os.environ['RESOURCEPATH'], '..', 'Frameworks',
                     'libgeos_c.dylib')]
             except KeyError:
-                # binary from pyinstaller
                 alt_paths = [
-                    os.path.join(sys.executable, 'libgeos_c.dylib')]
+                    # binary from pyinstaller
+                    os.path.join(sys.executable, 'libgeos_c.dylib'),
+                    # .app from cx_Freeze
+                    os.path.join(os.path.dirname(sys.executable), 'libgeos_c.1.dylib')]
                 if hasattr(sys, '_MEIPASS'):
                     alt_paths.append(
                         os.path.join(sys._MEIPASS, 'libgeos_c.1.dylib'))
@@ -143,8 +145,10 @@ elif sys.platform == 'darwin':
                 "/Library/Frameworks/GEOS.framework/Versions/Current/GEOS",
                 # macports
                 '/opt/local/lib/libgeos_c.dylib',
-                # homebrew
+                # homebrew Intel
                 '/usr/local/lib/libgeos_c.dylib',
+                # homebrew Apple Silicon
+                '/opt/homebrew/lib/libgeos_c.dylib',
             ]
         _lgeos = load_dll('geos_c', fallbacks=alt_paths)
 
diff --git a/shapely/strtree.py b/shapely/strtree.py
index 30c79a5..aba92c3 100644
--- a/shapely/strtree.py
+++ b/shapely/strtree.py
@@ -22,9 +22,7 @@ import ctypes
 import logging
 from typing import Any, ItemsView, Iterable, Iterator, Optional, Sequence, Tuple, Union
 import sys
-from warnings import warn
 
-from shapely.errors import ShapelyDeprecationWarning
 from shapely.geometry.base import BaseGeometry
 from shapely.geos import lgeos
 
@@ -99,49 +97,34 @@ class STRtree:
         items: Iterable[Any] = None,
         node_capacity: int = 10,
     ):
-        warn(
-            "STRtree will be changed in 2.0.0. The exact API is not yet decided, but will be documented before 1.8.0",
-            ShapelyDeprecationWarning,
-            stacklevel=2,
-        )
-        self._tree = None
         self.node_capacity = node_capacity
-        self._rev = {
-            item: geom
-            for geom, item in self._iterinitdata(geoms, items)
-            if not geom.is_empty
-        }
-        if self._rev:
-            self._init_tree(self._rev.items())
-
-    def _iterinitdata(
-        self,
-        geoms: Iterable[BaseGeometry], items: Optional[Iterable[BaseGeometry]],
-    ) -> Iterator[Tuple[BaseGeometry, Any]]:
-        if items is not None:
-            for geom, item in zip(geoms, items):
-                if isinstance(geom, BaseGeometry):
-                    yield (geom, item)
+
+        # Keep references to geoms
+        self._geoms = list(geoms)
+        # Default enumeration index to store in the tree
+        self._idxs = list(range(len(self._geoms)))
+
+        # handle items
+        self._has_custom_items = items is not None
+        if not self._has_custom_items:
+            items = self._idxs
+        self._items = items
+
+        # initialize GEOS STRtree
+        self._tree = lgeos.GEOSSTRtree_create(self.node_capacity)
+        i = 0
+        for idx, geom in zip(self._idxs, self._geoms):
+            # filter empty geometries out of the input
+            if geom is not None and not geom.is_empty:
+                lgeos.GEOSSTRtree_insert(self._tree, geom._geom, ctypes.py_object(idx))
+                i += 1
+        self._n_geoms = i
+
+    def __reduce__(self):
+        if self._has_custom_items:
+            return STRtree, (self._geoms, self._items)
         else:
-            for enum_idx, geom in enumerate(geoms):
-                if isinstance(geom, BaseGeometry):
-                    yield (geom, enum_idx)
-
-    def _init_tree(self, rev_initdata: ItemsView[Any, BaseGeometry]):
-        if rev_initdata:
-            self._tree = lgeos.GEOSSTRtree_create(self.node_capacity)
-            for item, geom in rev_initdata:
-                lgeos.GEOSSTRtree_insert(self._tree, geom._geom, ctypes.py_object(item))
-
-    def __getstate__(self):
-        state = self.__dict__.copy()
-        del state["_tree"]
-        return state
-
-    def __setstate__(self, state):
-        self.__dict__.update(state)
-        if self._rev:
-            self._init_tree(self._rev.items())
+            return STRtree, (self._geoms, )
 
     def __del__(self):
         if self._tree is not None:
@@ -152,6 +135,19 @@ class STRtree:
 
             self._tree = None
 
+    def _query(self, geom):
+        if self._n_geoms == 0:
+            return []
+
+        result = []
+
+        def callback(item, userdata):
+            idx = ctypes.cast(item, ctypes.py_object).value
+            result.append(idx)
+
+        lgeos.GEOSSTRtree_query(self._tree, geom._geom, lgeos.GEOSQueryCallback(callback), None)
+        return result
+
     def query_items(self, geom: BaseGeometry) -> Sequence[Any]:
         """Query for nodes which intersect the geom's envelope to get
         stored items.
@@ -197,19 +193,11 @@ class STRtree:
         ['POINT (2 2)']
 
         """
-        if self._tree is None or not self._rev:
-            return []
-
-        result = []
-
-        def callback(item, userdata):
-            idx = ctypes.cast(item, ctypes.py_object).value
-            result.append(idx)
-
-        lgeos.GEOSSTRtree_query(
-            self._tree, geom._geom, lgeos.GEOSQueryCallback(callback), None
-        )
-        return result
+        result = self._query(geom)
+        if self._has_custom_items:
+            return [self._items[i] for i in result]
+        else:
+            return result
 
     def query_geoms(self, geom: BaseGeometry) -> Sequence[BaseGeometry]:
         """Query for nodes which intersect the geom's envelope to get
@@ -225,8 +213,8 @@ class STRtree:
         An array or list of geometry objects.
 
         """
-        items = self.query_items(geom)
-        return [self._rev[idx] for idx in items]
+        result = self._query(geom)
+        return [self._geoms[i] for i in result]
 
     def query(self, geom: BaseGeometry) -> Sequence[BaseGeometry]:
         """Query for nodes which intersect the geom's envelope to get
@@ -247,6 +235,34 @@ class STRtree:
         """
         return self.query_geoms(geom)
 
+    def _nearest(self, geom, exclusive):
+        envelope = geom.envelope
+
+        def callback(item1, item2, distance, userdata):
+            try:
+                callback_userdata = ctypes.cast(userdata, ctypes.py_object).value
+                idx = ctypes.cast(item1, ctypes.py_object).value
+                geom2 = ctypes.cast(item2, ctypes.py_object).value
+                dist = ctypes.cast(distance, ctypes.POINTER(ctypes.c_double))
+                if callback_userdata["exclusive"] and self._geoms[idx].equals(geom2):
+                    dist[0] = sys.float_info.max
+                else:
+                    lgeos.GEOSDistance(self._geoms[idx]._geom, geom2._geom, dist)
+                
+                return 1
+            except Exception:
+                log.exception("Caught exception")
+                return 0
+
+        item = lgeos.GEOSSTRtree_nearest_generic(
+            self._tree,
+            ctypes.py_object(geom),
+            envelope._geom,
+            lgeos.GEOSDistanceCallback(callback),
+            ctypes.py_object({"exclusive": exclusive}),
+        )
+        return ctypes.cast(item, ctypes.py_object).value
+
     def nearest_item(
         self, geom: BaseGeometry, exclusive: bool = False
     ) -> Union[Any, None]:
@@ -285,35 +301,14 @@ class STRtree:
         'POINT (0 0)'
 
         """
-        if self._tree is None or not self._rev:
+        if self._n_geoms == 0:
             return None
 
-        envelope = geom.envelope
-
-        def callback(item1, item2, distance, userdata):
-            try:
-                callback_userdata = ctypes.cast(userdata, ctypes.py_object).value
-                idx = ctypes.cast(item1, ctypes.py_object).value
-                geom2 = ctypes.cast(item2, ctypes.py_object).value
-                dist = ctypes.cast(distance, ctypes.POINTER(ctypes.c_double))
-                if callback_userdata["exclusive"] and self._rev[idx].equals(geom2):
-                    dist[0] = sys.float_info.max
-                else:
-                    lgeos.GEOSDistance(self._rev[idx]._geom, geom2._geom, dist)
-                return 1
-            except Exception:
-                log.exception("Caught exception")
-                return 0
-
-        item = lgeos.GEOSSTRtree_nearest_generic(
-            self._tree,
-            ctypes.py_object(geom),
-            envelope._geom,
-            lgeos.GEOSDistanceCallback(callback),
-            ctypes.py_object({"exclusive": exclusive}),
-        )
-        result = ctypes.cast(item, ctypes.py_object).value
-        return result
+        result = self._nearest(geom, exclusive)
+        if self._has_custom_items:
+            return self._items[result]
+        else:
+            return result
 
     def nearest_geom(
         self, geom: BaseGeometry, exclusive: bool = False
@@ -337,11 +332,8 @@ class STRtree:
         version 2.0.
 
         """
-        item = self.nearest_item(geom, exclusive=exclusive)
-        if item is None:
-            return None
-        else:
-            return self._rev[item]
+        result = self._nearest(geom, exclusive)
+        return self._geoms[result]
 
     def nearest(
         self, geom: BaseGeometry, exclusive: bool = False


Test patch:
diff --git a/tests/test_dlls.py b/tests/test_dlls.py
index ca6fb07..c71da8e 100644
--- a/tests/test_dlls.py
+++ b/tests/test_dlls.py
@@ -13,8 +13,9 @@ class LoadingTestCase(unittest.TestCase):
     def test_fallbacks(self):
         load_dll('geos_c', fallbacks=[
             os.path.join(sys.prefix, "lib", "libgeos_c.dylib"), # anaconda (Mac OS X)
-            '/opt/local/lib/libgeos_c.dylib',  # MacPorts
-            '/usr/local/lib/libgeos_c.dylib',  # homebrew (Mac OS X)
+            '/opt/local/lib/libgeos_c.dylib',     # MacPorts
+            '/usr/local/lib/libgeos_c.dylib',     # homebrew (Mac OS X)
+            '/opt/homebrew/lib/libgeos_c.dylib',  # homebrew (macOS)
             os.path.join(sys.prefix, "lib", "libgeos_c.so"), # anaconda (Linux)
             'libgeos_c.so.1',
             'libgeos_c.so'])
diff --git a/tests/test_emptiness.py b/tests/test_emptiness.py
index 348266d..33e654f 100644
--- a/tests/test_emptiness.py
+++ b/tests/test_emptiness.py
@@ -69,6 +69,16 @@ class EmptinessTestCase(unittest.TestCase):
         self.assertTrue(LinearRing(empty_generator()).is_empty)
 
 
+@shapely20_deprecated
+@pytest.mark.filterwarnings("error:An exception was ignored")  # NumPy 1.21
+def test_numpy_object_array():
+    np = pytest.importorskip("numpy")
+
+    geoms = [BaseGeometry(), EmptyGeometry()]
+    arr = np.empty(2, object)
+    arr[:] = geoms
+
+
 def test_shape_empty():
     empty_mp = MultiPolygon()
     empty_json = mapping(empty_mp)
diff --git a/tests/test_strtree.py b/tests/test_strtree.py
index e1402cc..4ecda52 100644
--- a/tests/test_strtree.py
+++ b/tests/test_strtree.py
@@ -6,7 +6,6 @@ import sys
 
 import pytest
 
-from shapely.errors import ShapelyDeprecationWarning
 from shapely.geometry import Point, Polygon, box
 from shapely.geos import geos_version
 from shapely import strtree
@@ -27,8 +26,7 @@ empty = wkt.loads("GEOMETRYCOLLECTION EMPTY")
     [(Point(2, 2).buffer(0.99), 1), (Point(2, 2).buffer(1.0), 3)],
 )
 def test_query(geoms, query_geom, num_results):
-    with pytest.warns(ShapelyDeprecationWarning):
-        tree = STRtree(geoms)
+    tree = STRtree(geoms)
     results = tree.query(query_geom)
     assert len(results) == num_results
 
@@ -41,8 +39,7 @@ def test_query(geoms, query_geom, num_results):
 )
 def test_query_enumeration_idx(geoms, query_geom, expected):
     """Store enumeration idx"""
-    with pytest.warns(ShapelyDeprecationWarning):
-        tree = STRtree(geoms, range(len(geoms)))
+    tree = STRtree(geoms, range(len(geoms)))
     results = tree.query_items(query_geom)
     assert sorted(results) == sorted(expected)
 
@@ -56,8 +53,7 @@ def test_query_enumeration_idx(geoms, query_geom, expected):
 )
 def test_query_items(geoms, items, query_geom, expected):
     """Store enumeration idx"""
-    with pytest.warns(ShapelyDeprecationWarning):
-        tree = STRtree(geoms, items)
+    tree = STRtree(geoms, items)
     results = tree.query_items(query_geom)
     expected = [items[idx] for idx in expected] if items is not None else expected
     assert sorted(results) == sorted(expected)
@@ -74,8 +70,7 @@ def test_query_items(geoms, items, query_geom, expected):
     ],
 )
 def test_query_items_with_empty(tree_geometry, geometry, expected):
-    with pytest.warns(ShapelyDeprecationWarning):
-        tree = STRtree(tree_geometry)
+    tree = STRtree(tree_geometry)
     assert tree.query_items(geometry) == expected
 
 
@@ -87,8 +82,7 @@ def test_insert_empty_geometry():
     """
     empty = Polygon()
     geoms = [empty]
-    with pytest.warns(ShapelyDeprecationWarning):
-        tree = STRtree(geoms)
+    tree = STRtree(geoms)
     query = Polygon([(0, 0), (1, 1), (2, 0), (0, 0)])
     results = tree.query(query)
     assert len(results) == 0
@@ -103,8 +97,7 @@ def test_query_empty_geometry():
     empty = Polygon()
     point = Point(1, 0.5)
     geoms = [empty, point]
-    with pytest.warns(ShapelyDeprecationWarning):
-        tree = STRtree(geoms)
+    tree = STRtree(geoms)
     query = Polygon([(0, 0), (1, 1), (2, 0), (0, 0)])
     results = tree.query(query)
     assert len(results) == 1
@@ -117,8 +110,7 @@ def test_references():
     empty = Polygon()
     point = Point(1, 0.5)
     geoms = [empty, point]
-    with pytest.warns(ShapelyDeprecationWarning):
-        tree = STRtree(geoms)
+    tree = STRtree(geoms)
 
     empty = None
     point = None
@@ -132,8 +124,7 @@ def test_references():
 
 @requires_geos_342
 def test_safe_delete():
-    with pytest.warns(ShapelyDeprecationWarning):
-        tree = STRtree([])
+    tree = STRtree([])
 
     _lgeos = strtree.lgeos
     strtree.lgeos = None
@@ -148,8 +139,7 @@ def test_pickle_persistence():
     """
     Don't crash trying to use unpickled GEOS handle.
     """
-    with pytest.warns(ShapelyDeprecationWarning):
-        tree = STRtree([Point(i, i).buffer(0.1) for i in range(3)], range(3))
+    tree = STRtree([Point(i, i).buffer(0.1) for i in range(3)], range(3))
 
     pickled_strtree = pickle.dumps(tree)
     unpickle_script_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "unpickle-strtree.py")
@@ -175,8 +165,7 @@ def test_pickle_persistence():
 )
 @pytest.mark.parametrize("query_geom", [Point(0, 0.4)])
 def test_nearest_geom(geoms, query_geom):
-    with pytest.warns(ShapelyDeprecationWarning):
-        tree = STRtree(geoms)
+    tree = STRtree(geoms)
     result = tree.nearest(query_geom)
     assert result.geom_type == "Point"
     assert result.x == 0.0
@@ -197,22 +186,19 @@ def test_nearest_geom(geoms, query_geom):
 @pytest.mark.parametrize("items", [list(range(1, 4)), list("abc")])
 @pytest.mark.parametrize("query_geom", [Point(0, 0.4)])
 def test_nearest_item(geoms, items, query_geom):
-    with pytest.warns(ShapelyDeprecationWarning):
-        tree = STRtree(geoms, items)
+    tree = STRtree(geoms, items)
     assert tree.nearest_item(query_geom) == items[0]
 
 
 @pytest.mark.parametrize(["geoms", "items"], [([], None), ([], [])])
 def test_nearest_empty(geoms, items):
-    with pytest.warns(ShapelyDeprecationWarning):
-        tree = STRtree(geoms, items)
+    tree = STRtree(geoms, items)
     assert tree.nearest_item(None) is None
 
 
 @pytest.mark.parametrize(["geoms", "items"], [([], None), ([], [])])
 def test_nearest_items(geoms, items):
-    with pytest.warns(ShapelyDeprecationWarning):
-        tree = STRtree(geoms, items)
+    tree = STRtree(geoms, items)
     assert tree.nearest_item(None) is None
 
 
@@ -230,6 +216,5 @@ def test_nearest_items(geoms, items):
 @pytest.mark.parametrize("items", [list(range(1, 4)), list("abc")])
 @pytest.mark.parametrize("query_geom", [Point(0, 0.5)])
 def test_nearest_item_exclusive(geoms, items, query_geom):
-    with pytest.warns(ShapelyDeprecationWarning):
-        tree = STRtree(geoms, items)
+    tree = STRtree(geoms, items)
     assert tree.nearest_item(query_geom, exclusive=True) != items[0]