[Git][debian-gis-team/python-rtree][upstream] New upstream version 1.4.0

Bas Couwenberg (@sebastic) gitlab at salsa.debian.org
Thu Mar 6 04:31:38 GMT 2025



Bas Couwenberg pushed to branch upstream at Debian GIS Project / python-rtree


Commits:
e25d6645 by Bas Couwenberg at 2025-03-06T05:18:23+01:00
New upstream version 1.4.0
- - - - -


26 changed files:

- .github/workflows/deploy.yml
- .github/workflows/test.yml
- .gitignore
- .pre-commit-config.yaml
- .readthedocs.yaml
- CHANGES.rst
- DEPENDENCIES.txt
- README.md
- benchmarks/benchmarks.py
- docs/source/class.rst
- docs/source/install.rst
- docs/source/performance.rst
- environment.yml
- pyproject.toml
- rtree/__init__.py
- rtree/core.py
- rtree/finder.py
- rtree/index.py
- ci/install_libspatialindex.bat → scripts/install_libspatialindex.bat
- ci/install_libspatialindex.bash → scripts/install_libspatialindex.sh
- scripts/repair_wheel.py
- setup.py
- tests/conftest.py
- tests/test_index.py
- tests/test_tpr.py
- tox.ini


Changes:

=====================================
.github/workflows/deploy.yml
=====================================
@@ -18,40 +18,29 @@ jobs:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        os: [windows-latest, ubuntu-latest, macos-latest]
+        os:
+          - windows-latest
+          - ubuntu-latest
+          - ubuntu-24.04-arm
+          - macos-latest
 
     steps:
     - uses: actions/checkout at v4
 
-    - name: Set up QEMU
-      if: runner.os == 'Linux'
-      uses: docker/setup-qemu-action at v3
-      with:
-        platforms: arm64
-
     - uses: actions/setup-python at v5
       name: Install Python
       with:
         python-version: '3.11'
 
-    - name: Setup
-      run: pip install wheel
-
     - uses: ilammy/msvc-dev-cmd at v1
       if: startsWith(matrix.os, 'windows')
 
-    - name: Run Windows Preinstall Build
-      if: startsWith(matrix.os, 'windows')
-      run: |
-        choco install vcpython27 -f -y
-        ci\install_libspatialindex.bat
-
     - name: Build wheels
-      uses: pypa/cibuildwheel at v2.19.2
+      uses: pypa/cibuildwheel at v2.23.0
 
     - uses: actions/upload-artifact at v4
       with:
-        name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
+        name: cibw-wheels-${{ matrix.os }}
         path: ./wheelhouse/*.whl
 
   build_sdist:


=====================================
.github/workflows/test.yml
=====================================
@@ -4,21 +4,12 @@ on:
   push:
     branches:
       - master
-    paths:
-      - '.github/workflows/test.yml'
   pull_request:
   workflow_dispatch:
   schedule:
     - cron: '0 6 * * 1'
 
 jobs:
-  pre-commit:
-    runs-on: ubuntu-latest
-    steps:
-    - uses: actions/checkout at v4
-    - uses: actions/setup-python at v5
-    - uses: pre-commit/action at v3.0.1
-
   conda:
     name: Conda ${{ matrix.python-version }} - ${{ matrix.os }}
     defaults:
@@ -26,12 +17,12 @@ jobs:
         shell: bash -l {0}
     runs-on: ${{ matrix.os }}
     strategy:
-      fail-fast: true
+      fail-fast: false
       matrix:
         os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
-        python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
-        # test oldesst and newest libspatialindex versions
-        sidx-version: ['1.8.5', '2.0.0']
+        # test oldest and newest versions of python and libspatialindex
+        python-version: ['3.9', '3.13']
+        sidx-version: ['1.8.5', '2.1.0']
         exclude:
           - os: 'macos-latest'
           - sidx-version: '1.8.5'
@@ -43,16 +34,15 @@ jobs:
         channels: conda-forge
         auto-update-conda: true
         python-version: ${{ matrix.python-version }}
+
     - name: Setup
-      run: |
-          conda install -c conda-forge numpy libspatialindex=${{ matrix.sidx-version }} -y
+      run: conda install -c conda-forge numpy pytest libspatialindex=${{ matrix.sidx-version }} -y
+
     - name: Install
-      run: |
-        pip install -e .
+      run: pip install -e .
+
     - name: Test with pytest
-      run: |
-        pip install pytest
-        python -m pytest --doctest-modules rtree tests
+      run: pytest --import-mode=importlib -Werror -v --doctest-modules rtree tests
 
   ubuntu:
     name: Ubuntu Python ${{ matrix.python-version }}
@@ -61,25 +51,26 @@ jobs:
         shell: bash -l {0}
     runs-on: ubuntu-latest
     strategy:
-      fail-fast: true
+      fail-fast: false
       matrix:
-        python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
+        python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
 
     steps:
     - uses: actions/checkout at v4
     - uses: actions/setup-python at v5
       name: Install Python
       with:
-        python-version: '3.11'
+        python-version: ${{ matrix.python-version }}
+        allow-prereleases: true
+
     - name: Setup
       run: |
-          sudo apt install libspatialindex-c6 python3-pip
-          python3 -m pip install --upgrade pip
-          python3 -m pip install setuptools numpy pytest
+          sudo apt-get -y install libspatialindex-c6
+          pip install --upgrade pip
+          pip install numpy pytest
 
     - name: Build
-      run: |
-        python3 -m pip install --user .
+      run: pip install --user .
+
     - name: Test with pytest
-      run: |
-        python3 -m pytest --doctest-modules rtree tests
+      run: pytest --import-mode=importlib -Werror -v --doctest-modules rtree tests


=====================================
.gitignore
=====================================
@@ -1,4 +1,4 @@
-Rtree.egg-info/
+*.egg-info/
 *.pyc
 docs/build
 build/


=====================================
.pre-commit-config.yaml
=====================================
@@ -2,26 +2,26 @@ ci:
   autoupdate_schedule: quarterly
 repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.6.0
+    rev: v5.0.0
     hooks:
     - id: check-yaml
     - id: end-of-file-fixer
     - id: trailing-whitespace
   - repo: https://github.com/python-jsonschema/check-jsonschema
-    rev: 0.27.3
+    rev: 0.31.2
     hooks:
     - id: check-github-workflows
       args: ["--verbose"]
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.5.0
+    rev: v0.9.9
     hooks:
-    # Run the linter.
+    # Run the linter
     - id: ruff
       args: [ --fix ]
-    # Run the formatter.
+    # Run the formatter
     - id: ruff-format
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v1.5.1
+    rev: v1.15.0
     hooks:
     - id: mypy
       exclude: 'docs/.'


=====================================
.readthedocs.yaml
=====================================
@@ -9,9 +9,9 @@ version: 2
 build:
    apt_packages:
       - libspatialindex-dev
-   os: ubuntu-20.04
+   os: ubuntu-lts-latest
    tools:
-      python: "3.11"
+      python: latest
 
 # Build documentation in the docs/source directory with Sphinx
 sphinx:


=====================================
CHANGES.rst
=====================================
@@ -1,3 +1,11 @@
+1.4.0: 2025-03-06
+=================
+
+- Python 3.9+ is now required (:PR:`321`)
+- Add support for array-based bulk insert with NumPy (:PR:`340` by :user:`FreddieWitherden`)
+- Upgrade binary wheels with libspatialindex-2.1.0 (:PR:`353`)
+- Rename project and other build components to "rtree" (:PR:`350`)
+
 1.3.0: 2024-07-10
 =================
 


=====================================
DEPENDENCIES.txt
=====================================
@@ -1,4 +1,4 @@
-- python 3.8+
+- python 3.9+
 - setuptools
 - libspatialindex C library 1.8.5+:
   https://libspatialindex.org/


=====================================
README.md
=====================================
@@ -1,7 +1,7 @@
 # Rtree: Spatial indexing for Python
 
 ![Build](https://github.com/Toblerity/rtree/workflows/Build/badge.svg)
-[![PyPI version](https://badge.fury.io/py/Rtree.svg)](https://badge.fury.io/py/Rtree)
+[![PyPI version](https://badge.fury.io/py/rtree.svg)](https://badge.fury.io/py/rtree)
 
 
 Rtree is a [ctypes](https://docs.python.org/3/library/ctypes.html) Python wrapper of [libspatialindex](https://libspatialindex.org/) that provides a


=====================================
benchmarks/benchmarks.py
=====================================
@@ -164,7 +164,7 @@ hits = disk_index.intersection(bbox, objects="raw")
     t = timeit.Timer(
         stmt=s, setup="from __main__ import points, disk_index, bbox, insert_object"
     )
-    print("Disk-based Rtree Intersection " "without Item() wrapper (objects='raw'):")
+    print("Disk-based Rtree Intersection without Item() wrapper (objects='raw'):")
     result = list(disk_index.intersection(bbox, objects="raw"))
     print(len(result), "raw hits")
     print(f"{1e6 * t.timeit(number=TEST_TIMES) / TEST_TIMES:.2f} usec/pass")


=====================================
docs/source/class.rst
=====================================
@@ -4,7 +4,7 @@ Class Documentation
 ------------------------------------------------------------------------------
 
 .. autoclass:: rtree.index.Index
-    :members: __init__, insert, intersection, nearest, delete, bounds, count, close, dumps, loads
+    :members: __init__, insert, intersection, intersection_v, nearest, nearest_v, delete, bounds, count, close, dumps, loads
 
 .. autoclass:: rtree.index.Property
     :members:


=====================================
docs/source/install.rst
=====================================
@@ -22,7 +22,7 @@ ensure that applications can find it at startup time.
 
 Rtree can be easily installed via pip::
 
-  $ pip install Rtree
+  $ pip install rtree
 
 or by running in a local source directory::
 
@@ -39,8 +39,8 @@ The Windows DLLs of `libspatialindex`_ are pre-compiled in
 windows installers that are available from `PyPI`_.  Installation on Windows
 is as easy as::
 
-  pip install Rtree
+  pip install rtree
 
 
-.. _`PyPI`: https://pypi.org/project/Rtree/
+.. _`PyPI`: https://pypi.org/project/rtree/
 .. _`libspatialindex`: https://libspatialindex.org


=====================================
docs/source/performance.rst
=====================================
@@ -80,4 +80,5 @@ Use the correct query method
 
 Use :py:meth:`~rtree.index.Index.count` if you only need a count and
 :py:meth:`~rtree.index.Index.intersection` if you only need the ids.
-Otherwise, lots of data may potentially be copied.
+Otherwise, lots of data may potentially be copied.  If possible also
+make use of the bulk query methods suffixed with `_v`.


=====================================
environment.yml
=====================================
@@ -3,5 +3,5 @@ channels:
 - defaults
 - conda-forge
 dependencies:
-- python>=3.8
+- python>=3.9
 - libspatialindex>=1.8.5


=====================================
pyproject.toml
=====================================
@@ -3,7 +3,7 @@ requires = ["setuptools>=61", "wheel"]
 build-backend = "setuptools.build_meta"
 
 [project]
-name = "Rtree"
+name = "rtree"
 authors = [
     {name = "Sean Gillies", email = "sean.gillies at gmail.com"},
 ]
@@ -13,7 +13,7 @@ maintainers = [
 ]
 description = "R-Tree spatial index for Python GIS"
 readme = "README.md"
-requires-python = ">=3.8"
+requires-python = ">=3.9"
 keywords = ["gis", "spatial", "index", "r-tree"]
 license = {text = "MIT"}
 classifiers = [
@@ -23,11 +23,11 @@ classifiers = [
     "License :: OSI Approved :: MIT License",
     "Operating System :: OS Independent",
     "Programming Language :: Python :: 3",
-    "Programming Language :: Python :: 3.8",
     "Programming Language :: Python :: 3.9",
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.12",
+    "Programming Language :: Python :: 3.13",
     "Topic :: Scientific/Engineering :: GIS",
     "Topic :: Database",
 ]
@@ -49,28 +49,28 @@ version = {attr = "rtree.__version__"}
 rtree = ["py.typed"]
 
 [tool.cibuildwheel]
-build = "cp38-*"
+build = "cp39-*"
 build-verbosity = 3
+before-all = "pip install wheel"
 repair-wheel-command = "python scripts/repair_wheel.py -w {dest_dir} {wheel}"
 test-requires = "tox"
 test-command = "tox --conf {project} --installpkg {wheel}"
 test-skip = [
-    "*aarch64",  # slow!
     "*-macosx_arm64",
 ]
 
 [tool.cibuildwheel.linux]
-archs = ["auto", "aarch64"]
+archs = ["auto"]
 before-build = [
     "yum install -y cmake libffi-devel",
-    "bash {project}/ci/install_libspatialindex.bash",
+    "sh {project}/scripts/install_libspatialindex.sh",
 ]
 
 [[tool.cibuildwheel.overrides]]
 select = "*-musllinux*"
 before-build = [
     "apk add cmake libffi-dev",
-    "bash {project}/ci/install_libspatialindex.bash",
+    "sh {project}/scripts/install_libspatialindex.sh",
 ]
 
 [tool.cibuildwheel.macos]
@@ -78,11 +78,14 @@ archs = ["x86_64", "arm64"]
 environment = { MACOSX_DEPLOYMENT_TARGET="10.9" }
 before-build = [
     "brew install coreutils cmake",
-    "bash {project}/ci/install_libspatialindex.bash",
+    "sh {project}/scripts/install_libspatialindex.sh",
 ]
 
 [tool.cibuildwheel.windows]
 archs = ["AMD64"]
+before-build = [
+    "call {project}\\scripts\\install_libspatialindex.bat",
+]
 
 [tool.coverage.report]
 # Ignore warnings for overloads


=====================================
rtree/__init__.py
=====================================
@@ -7,6 +7,6 @@ hyperrectangular intersection queries.
 
 from __future__ import annotations
 
-__version__ = "1.3.0"
+__version__ = "1.4.0"
 
 from .index import Index, Rtree  # noqa


=====================================
rtree/core.py
=====================================
@@ -125,6 +125,23 @@ rt.Index_CreateWithStream.argtypes = [ctypes.c_void_p, NEXTFUNC]
 rt.Index_CreateWithStream.restype = ctypes.c_void_p
 rt.Index_CreateWithStream.errcheck = check_void  # type: ignore
 
+try:
+    rt.Index_CreateWithArray.argtypes = [
+        ctypes.c_void_p,
+        ctypes.c_uint64,
+        ctypes.c_uint32,
+        ctypes.c_uint64,
+        ctypes.c_uint64,
+        ctypes.c_uint64,
+        ctypes.c_void_p,
+        ctypes.c_void_p,
+        ctypes.c_void_p,
+    ]
+    rt.Index_CreateWithArray.restype = ctypes.c_void_p
+    rt.Index_CreateWithArray.errcheck = check_void  # type: ignore
+except AttributeError:
+    pass
+
 rt.Index_Destroy.argtypes = [ctypes.c_void_p]
 rt.Index_Destroy.restype = None
 rt.Index_Destroy.errcheck = check_void_done  # type: ignore
@@ -221,6 +238,44 @@ rt.Index_NearestNeighbors_id.argtypes = [
 rt.Index_NearestNeighbors_id.restype = ctypes.c_int
 rt.Index_NearestNeighbors_id.errcheck = check_return  # type: ignore
 
+try:
+    rt.Index_NearestNeighbors_id_v.argtypes = [
+        ctypes.c_void_p,
+        ctypes.c_int64,
+        ctypes.c_int64,
+        ctypes.c_uint32,
+        ctypes.c_uint64,
+        ctypes.c_uint64,
+        ctypes.c_uint64,
+        ctypes.c_void_p,
+        ctypes.c_void_p,
+        ctypes.c_void_p,
+        ctypes.c_void_p,
+        ctypes.c_void_p,
+        ctypes.POINTER(ctypes.c_int64),
+    ]
+    rt.Index_NearestNeighbors_id_v.restype = ctypes.c_int
+    rt.Index_NearestNeighbors_id_v.errcheck = check_return  # type: ignore
+
+    rt.Index_Intersects_id_v.argtypes = [
+        ctypes.c_void_p,
+        ctypes.c_int64,
+        ctypes.c_uint32,
+        ctypes.c_uint64,
+        ctypes.c_uint64,
+        ctypes.c_uint64,
+        ctypes.c_void_p,
+        ctypes.c_void_p,
+        ctypes.c_void_p,
+        ctypes.c_void_p,
+        ctypes.POINTER(ctypes.c_int64),
+    ]
+    rt.Index_Intersects_id_v.restype = ctypes.c_int
+    rt.Index_Intersects_id_v.errcheck = check_return  # type: ignore
+except AttributeError:
+    pass
+
+
 rt.Index_GetLeaves.argtypes = [
     ctypes.c_void_p,
     ctypes.POINTER(ctypes.c_uint32),


=====================================
rtree/finder.py
=====================================
@@ -77,7 +77,7 @@ def load() -> ctypes.CDLL:
                 if pkg_files is not None:
                     for file in pkg_files:  # type: ignore
                         if (
-                            file.parent.name == "Rtree.libs"
+                            file.parent.name == "rtree.libs"
                             and file.stem.startswith("libspatialindex")
                             and ".so" in file.suffixes
                         ):


=====================================
rtree/index.py
=====================================
@@ -6,7 +6,8 @@ import os.path
 import pickle
 import pprint
 import warnings
-from typing import Any, Iterator, Literal, Sequence, overload
+from collections.abc import Iterator, Sequence
+from typing import Any, Literal, overload
 
 from . import core
 from .exceptions import RTreeError
@@ -206,20 +207,26 @@ class Index:
         self.interleaved = bool(kwargs.get("interleaved", True))
 
         stream = None
+        arrays = None
         basename = None
         storage = None
         if args:
             if isinstance(args[0], str) or isinstance(args[0], bytes):
                 # they sent in a filename
                 basename = args[0]
-                # they sent in a filename, stream
+                # they sent in a filename, stream or filename, buffers
                 if len(args) > 1:
-                    stream = args[1]
+                    if isinstance(args[1], tuple):
+                        arrays = args[1]
+                    else:
+                        stream = args[1]
             elif isinstance(args[0], ICustomStorage):
                 storage = args[0]
                 # they sent in a storage, stream
                 if len(args) > 1:
                     stream = args[1]
+            elif isinstance(args[0], tuple):
+                arrays = args[0]
             else:
                 stream = args[0]
 
@@ -271,6 +278,18 @@ class Index:
         if stream and self.properties.type == RT_RTree:
             self._exception = None
             self.handle = self._create_idx_from_stream(stream)
+            if self._exception:
+                raise self._exception
+        elif arrays and self.properties.type == RT_RTree:
+            self._exception = None
+
+            try:
+                self.handle = self._create_idx_from_array(*arrays)
+            except NameError:
+                raise NotImplementedError(
+                    "libspatialindex >= 2.1 needed for bulk insert"
+                )
+
             if self._exception:
                 raise self._exception
         else:
@@ -278,6 +297,8 @@ class Index:
             if stream:  # Bulk insert not supported, so add one by one
                 for item in stream:
                     self.insert(*item)
+            elif arrays:
+                raise NotImplementedError("Bulk insert only supported for RTrees")
 
     def get_size(self) -> int:
         warnings.warn(
@@ -296,7 +317,7 @@ class Index:
             return 0
 
     def __repr__(self) -> str:
-        return f"rtree.index.Index(bounds={self.bounds}, size={self.get_size()})"
+        return f"rtree.index.Index(bounds={self.bounds}, size={len(self)})"
 
     def __getstate__(self) -> dict[str, Any]:
         state = self.__dict__.copy()
@@ -330,42 +351,38 @@ class Index:
     def get_coordinate_pointers(
         self, coordinates: Sequence[float]
     ) -> tuple[float, float]:
-        try:
-            iter(coordinates)
-        except TypeError:
-            raise TypeError("Bounds must be a sequence")
         dimension = self.properties.dimension
+        coordinates = list(coordinates)
 
-        mins = ctypes.c_double * dimension
-        maxs = ctypes.c_double * dimension
-
-        if not self.interleaved:
-            coordinates = Index.interleave(coordinates)
+        arr = ctypes.c_double * dimension
+        mins = arr()
 
-        # it's a point make it into a bbox. [x, y] => [x, y, x, y]
+        # Point
         if len(coordinates) == dimension:
-            coordinates = *coordinates, *coordinates
+            mins[:] = coordinates
+            maxs = mins
+        # Bounding box
+        else:
+            maxs = arr()
 
-        if len(coordinates) != dimension * 2:
-            raise RTreeError(
-                "Coordinates must be in the form "
-                "(minx, miny, maxx, maxy) or (x, y) for 2D indexes"
-            )
+            # Interleaved box
+            if self.interleaved:
+                p = coordinates[:dimension]
+                q = coordinates[dimension:]
+            # Non-interleaved box
+            else:
+                p = coordinates[::2]
+                q = coordinates[1::2]
 
-        # so here all coords are in the form:
-        # [xmin, ymin, zmin, xmax, ymax, zmax]
-        for i in range(dimension):
-            if not coordinates[i] <= coordinates[i + dimension]:
+            mins[:] = p
+            maxs[:] = q
+
+            if not p <= q:
                 raise RTreeError(
                     "Coordinates must not have minimums more than maximums"
                 )
 
-        p_mins = mins(*[ctypes.c_double(coordinates[i]) for i in range(dimension)])
-        p_maxs = maxs(
-            *[ctypes.c_double(coordinates[i + dimension]) for i in range(dimension)]
-        )
-
-        return (p_mins, p_maxs)
+        return mins, maxs
 
     @staticmethod
     def _get_time_doubles(times):
@@ -1029,6 +1046,162 @@ class Index:
 
         return self._get_ids(it, p_num_results.contents.value)
 
+    def intersection_v(self, mins, maxs):
+        """Bulk intersection query for obtaining the ids of entries
+        which intersect with the provided bounding boxes.  The return
+        value is a tuple consisting of two 1D NumPy arrays: one of
+        intersecting ids and another containing the counts for each
+        bounding box.
+
+        :param mins: A NumPy array of shape `(n, d)` containing the
+            minima to query.
+
+        :param maxs: A NumPy array of shape `(n, d)` containing the
+            maxima to query.
+        """
+        import numpy as np
+
+        assert mins.shape == maxs.shape
+        assert mins.strides == maxs.strides
+
+        # Cast
+        mins = mins.astype(np.float64)
+        maxs = maxs.astype(np.float64)
+
+        # Extract counts
+        n, d = mins.shape
+
+        # Compute strides
+        d_i_stri = mins.strides[0] // mins.itemsize
+        d_j_stri = mins.strides[1] // mins.itemsize
+
+        ids = np.empty(2 * n, dtype=np.int64)
+        counts = np.empty(n, dtype=np.uint64)
+        nr = ctypes.c_int64(0)
+        offn, offi = 0, 0
+
+        while True:
+            core.rt.Index_Intersects_id_v(
+                self.handle,
+                n - offn,
+                d,
+                len(ids),
+                d_i_stri,
+                d_j_stri,
+                mins[offn:].ctypes.data,
+                maxs[offn:].ctypes.data,
+                ids[offi:].ctypes.data,
+                counts[offn:].ctypes.data,
+                ctypes.byref(nr),
+            )
+
+            # If we got the expected nuber of results then return
+            if nr.value == n - offn:
+                return ids[: counts.sum()], counts
+            # Otherwise, if our array is too small then resize
+            else:
+                offi += counts[offn : offn + nr.value].sum()
+                offn += nr.value
+
+                ids = ids.resize(2 * len(ids), refcheck=False)
+
+    def nearest_v(
+        self,
+        mins,
+        maxs,
+        num_results=1,
+        max_dists=None,
+        strict=False,
+        return_max_dists=False,
+    ):
+        """Bulk ``k``-nearest query for the given bounding boxes.  The
+        return value is a tuple consisting of, by default, two 1D NumPy
+        arrays: one of intersecting ids and another containing the
+        counts for each bounding box.
+
+        :param mins: A NumPy array of shape `(n, d)` containing the
+            minima to query.
+
+        :param maxs: A NumPy array of shape `(n, d)` containing the
+            maxima to query.
+
+        :param num_results: The maximum number of neighbors to return
+            for each bounding box.  If there are multiple equidistant
+            furthest neighbors then, by default, they are *all*
+            returned.  Hence, the actual number of results can be
+            greater than requested.
+
+        :param max_dists: Optional; a NumPy array of shape `(n,)`
+            containing the maximum distance to consider for each
+            bounding box.
+
+        :param strict: If True then each point will never return more
+            than `num_results` even in cases of equidistant furthest
+            neighbors.
+
+        :param return_max_dists: If True, the distance of the furthest
+            neighbor for each bounding box will also be returned.
+        """
+        import numpy as np
+
+        assert mins.shape == maxs.shape
+        assert mins.strides == maxs.strides
+
+        # Cast
+        mins = mins.astype(np.float64)
+        maxs = maxs.astype(np.float64)
+
+        # Extract counts
+        n, d = mins.shape
+
+        # Compute strides
+        d_i_stri = mins.strides[0] // mins.itemsize
+        d_j_stri = mins.strides[1] // mins.itemsize
+
+        ids = np.empty(n * num_results, dtype=np.int64)
+        counts = np.empty(n, dtype=np.uint64)
+        nr = ctypes.c_int64(0)
+        offn, offi = 0, 0
+
+        if max_dists is not None:
+            assert len(max_dists) == n
+
+            dists = max_dists.astype(np.float64).copy()
+        elif return_max_dists:
+            dists = np.zeros(n)
+        else:
+            dists = None
+
+        while True:
+            core.rt.Index_NearestNeighbors_id_v(
+                self.handle,
+                num_results if not strict else -num_results,
+                n - offn,
+                d,
+                len(ids),
+                d_i_stri,
+                d_j_stri,
+                mins[offn:].ctypes.data,
+                maxs[offn:].ctypes.data,
+                ids[offi:].ctypes.data,
+                counts[offn:].ctypes.data,
+                dists[offn:].ctypes.data if dists is not None else None,
+                ctypes.byref(nr),
+            )
+
+            # If we got the expected nuber of results then return
+            if nr.value == n - offn:
+                if return_max_dists:
+                    return ids[: counts.sum()], counts, dists
+                else:
+                    return ids[: counts.sum()], counts
+            # Otherwise, if our array is too small then resize
+            else:
+                offi += counts[offn : offn + nr.value].sum()
+                offn += nr.value
+
+                ids = ids.resize(2 * len(ids), refcheck=False)
+
     def _nearestTP(self, coordinates, velocities, times, num_results=1, objects=False):
         p_mins, p_maxs = self.get_coordinate_pointers(coordinates)
         pv_mins, pv_maxs = self.get_coordinate_pointers(velocities)
@@ -1230,16 +1403,14 @@ class Index:
                 return -1
 
             if self.interleaved:
-                coordinates = Index.deinterleave(coordinates)
-
-            # this code assumes the coords are not interleaved.
-            # xmin, xmax, ymin, ymax, zmin, zmax
-            for i in range(dimension):
-                mins[i] = coordinates[i * 2]
-                maxs[i] = coordinates[(i * 2) + 1]
+                mins[:] = coordinates[:dimension]
+                maxs[:] = coordinates[dimension:]
+            else:
+                mins[:] = coordinates[::2]
+                maxs[:] = coordinates[1::2]
 
-            p_mins[0] = ctypes.cast(mins, ctypes.POINTER(ctypes.c_double))
-            p_maxs[0] = ctypes.cast(maxs, ctypes.POINTER(ctypes.c_double))
+            p_mins[0] = mins
+            p_maxs[0] = maxs
 
             # set the dimension
             p_dimension[0] = dimension
@@ -1255,6 +1426,36 @@ class Index:
         stream = core.NEXTFUNC(py_next_item)
         return IndexStreamHandle(self.properties.handle, stream)
 
+    def _create_idx_from_array(self, ibuf, minbuf, maxbuf):
+        assert len(ibuf) == len(minbuf)
+        assert len(ibuf) == len(maxbuf)
+        assert minbuf.strides == maxbuf.strides
+
+        # Cast
+        ibuf = ibuf.astype(int)
+        minbuf = minbuf.astype(float)
+        maxbuf = maxbuf.astype(float)
+
+        # Extract counts
+        n, d = minbuf.shape
+
+        # Compute strides
+        i_stri = ibuf.strides[0] // 8
+        d_i_stri = minbuf.strides[0] // 8
+        d_j_stri = minbuf.strides[1] // 8
+
+        return IndexArrayHandle(
+            self.properties.handle,
+            n,
+            d,
+            i_stri,
+            d_i_stri,
+            d_j_stri,
+            ibuf.ctypes.data,
+            minbuf.ctypes.data,
+            maxbuf.ctypes.data,
+        )
+
     def leaves(self):
         leaf_node_count = ctypes.c_uint32()
         p_leafsizes = ctypes.pointer(ctypes.c_uint32())
@@ -1436,6 +1637,14 @@ class IndexStreamHandle(IndexHandle):
     _create = core.rt.Index_CreateWithStream
 
 
+try:
+
+    class IndexArrayHandle(IndexHandle):
+        _create = core.rt.Index_CreateWithArray
+except AttributeError:
+    pass
+
+
 class PropertyHandle(Handle):
     _create = core.rt.IndexProperty_Create
     _destroy = core.rt.IndexProperty_Destroy
@@ -1485,6 +1694,13 @@ class Property:
             if v is not None:
                 setattr(self, k, v)
 
+        # Consistency checks
+        if "near_minimum_overlap_factor" not in state:
+            nmof = self.near_minimum_overlap_factor
+            ilc = min(self.index_capacity, self.leaf_capacity)
+            if nmof >= ilc:
+                self.near_minimum_overlap_factor = ilc // 3 + 1
+
     def __getstate__(self) -> dict[Any, Any]:
         return self.as_dict()
 
@@ -1509,9 +1725,15 @@ class Property:
         return pprint.pformat(self.as_dict())
 
     def get_index_type(self) -> int:
-        return core.rt.IndexProperty_GetIndexType(self.handle)
+        try:
+            return self._type
+        except AttributeError:
+            type = core.rt.IndexProperty_GetIndexType(self.handle)
+            self._type: int = type
+            return type
 
     def set_index_type(self, value: int) -> None:
+        self._type = value
         return core.rt.IndexProperty_SetIndexType(self.handle, value)
 
     type = property(get_index_type, set_index_type)
@@ -1530,11 +1752,17 @@ class Property:
     :data:`RT_Linear`, :data:`RT_Quadratic`, and :data:`RT_Star`"""
 
     def get_dimension(self) -> int:
-        return core.rt.IndexProperty_GetDimension(self.handle)
+        try:
+            return self._dimension
+        except AttributeError:
+            dim = core.rt.IndexProperty_GetDimension(self.handle)
+            self._dimension: int = dim
+            return dim
 
     def set_dimension(self, value: int) -> None:
         if value <= 0:
             raise RTreeError("Negative or 0 dimensional indexes are not allowed")
+        self._dimension = value
         return core.rt.IndexProperty_SetDimension(self.handle, value)
 
     dimension = property(get_dimension, set_dimension)


=====================================
ci/install_libspatialindex.bat → scripts/install_libspatialindex.bat
=====================================
@@ -1,6 +1,6 @@
 python -c "import sys; print(sys.version)"
 
-set SIDX_VERSION=2.0.0
+set SIDX_VERSION=2.1.0
 
 curl -LO --retry 5 --retry-max-time 120 "https://github.com/libspatialindex/libspatialindex/archive/%SIDX_VERSION%.zip"
 


=====================================
ci/install_libspatialindex.bash → scripts/install_libspatialindex.sh
=====================================
@@ -1,9 +1,9 @@
-#!/bin/bash
+#!/bin/sh
 set -xe
 
 # A simple script to install libspatialindex from a Github Release
-VERSION=2.0.0
-SHA256=8caa4564c4592824acbf63a2b883aa2d07e75ccd7e9bf64321c455388a560579
+VERSION=2.1.0
+SHA256=86aa0925dd151ff9501a5965c4f8d7fb3dcd8accdc386a650dbdd62660399926
 
 # where to copy resulting files
 # this has to be run before `cd`-ing anywhere
@@ -32,7 +32,12 @@ rm -f $VERSION.zip
 curl -LOs --retry 5 --retry-max-time 120 https://github.com/libspatialindex/libspatialindex/archive/${VERSION}.zip
 
 # check the file hash
-echo "${SHA256}  ${VERSION}.zip" | sha256sum -c -
+if [ "$(uname)" = "Darwin" ]
+then
+    echo "${SHA256}  ${VERSION}.zip" | shasum -a 256 -c -
+else
+    echo "${SHA256}  ${VERSION}.zip" | sha256sum -c -
+fi
 
 rm -rf "libspatialindex-${VERSION}"
 unzip -q $VERSION
@@ -43,7 +48,7 @@ cd build
 
 printenv
 
-if [ "$(uname)" == "Darwin" ]; then
+if [ "$(uname)" = "Darwin" ]; then
     CMAKE_ARGS="-D CMAKE_OSX_ARCHITECTURES=${ARCHFLAGS##* } \
                 -D CMAKE_INSTALL_RPATH=@loader_path"
 fi


=====================================
scripts/repair_wheel.py
=====================================
@@ -45,7 +45,7 @@ def main():
         # use the platform specific repair tool first
         if os_ == "linux":
             # use path from cibuildwheel which allows auditwheel to create
-            # Rtree.libs/libspatialindex-*.so.*
+            # rtree.libs/libspatialindex-*.so.*
             cibw_lib_path = "/project/rtree/lib"
             if os.environ.get("LD_LIBRARY_PATH"):  # append path
                 os.environ["LD_LIBRARY_PATH"] += f"{os.pathsep}{cibw_lib_path}"
@@ -96,7 +96,7 @@ def main():
 
         if os_ == "linux":
             # This is auditwheel's libs, which needs post-processing
-            libs_dir = unpackdir / "Rtree.libs"
+            libs_dir = unpackdir / "rtree.libs"
             lsidx_list = list(libs_dir.glob("libspatialindex*.so*"))
             assert len(lsidx_list) == 1, list(libs_dir.iterdir())
             lsidx = lsidx_list[0]


=====================================
setup.py
=====================================
@@ -57,7 +57,7 @@ class InstallPlatlib(install):  # type: ignore[misc]
 
 # See pyproject.toml for other project metadata
 setup(
-    name="Rtree",
+    name="rtree",
     distclass=BinaryDistribution,
     cmdclass={"bdist_wheel": bdist_wheel, "install": InstallPlatlib},
 )


=====================================
tests/conftest.py
=====================================
@@ -2,11 +2,14 @@ from __future__ import annotations
 
 import os
 import shutil
-from typing import Iterator
+from collections.abc import Iterator
 
+import numpy
 import py
 import pytest
 
+import rtree
+
 data_files = ["boxes_15x15.data"]
 
 
@@ -17,3 +20,12 @@ def temporary_working_directory(tmpdir: py.path.local) -> Iterator[None]:
         shutil.copy(filename, str(tmpdir))
     with tmpdir.as_cwd():
         yield
+
+
+def pytest_report_header(config):
+    """Header for pytest."""
+    vers = [
+        f"SIDX version: {rtree.core.rt.SIDX_Version().decode()}",
+        f"NumPy version: {numpy.__version__}",
+    ]
+    return "\n".join(vers)


=====================================
tests/test_index.py
=====================================
@@ -5,7 +5,7 @@ import pickle
 import sys
 import tempfile
 import unittest
-from typing import Iterator
+from collections.abc import Iterator
 
 import numpy as np
 import pytest
@@ -240,7 +240,7 @@ class IndexIntersection(IndexTestCase):
         self.assertTrue(0 in self.idx.intersection((0, 0, 60, 60)))
         hits = list(self.idx.intersection((0, 0, 60, 60)))
 
-        self.assertTrue(len(hits), 10)
+        self.assertEqual(len(hits), 10)
         self.assertEqual(hits, [0, 4, 16, 27, 35, 40, 47, 50, 76, 80])
 
     def test_objects(self) -> None:
@@ -436,14 +436,14 @@ class IndexSerialization(unittest.TestCase):
             idx.add(i, coords)
 
         hits = list(idx.intersection((0, 0, 60, 60)))
-        self.assertTrue(len(hits), 10)
+        self.assertEqual(len(hits), 10)
         self.assertEqual(hits, [0, 4, 16, 27, 35, 40, 47, 50, 76, 80])
         del idx
 
         # Check we can reopen the index and get the same results
         idx2 = index.Index(tname, properties=p)
         hits = list(idx2.intersection((0, 0, 60, 60)))
-        self.assertTrue(len(hits), 10)
+        self.assertEqual(len(hits), 10)
         self.assertEqual(hits, [0, 4, 16, 27, 35, 40, 47, 50, 76, 80])
 
     @pytest.mark.skipif(not sys.maxsize > 2**32, reason="Fails on 32bit systems")
@@ -465,7 +465,7 @@ class IndexSerialization(unittest.TestCase):
             tname, data_gen(interleaved=False), properties=p, interleaved=False
         )
         hits1 = sorted(list(idx.intersection((0, 60, 0, 60))))
-        self.assertTrue(len(hits1), 10)
+        self.assertEqual(len(hits1), 10)
         self.assertEqual(hits1, [0, 4, 16, 27, 35, 40, 47, 50, 76, 80])
 
         leaves = idx.leaves()
@@ -591,7 +591,7 @@ class IndexSerialization(unittest.TestCase):
         )
 
         hits2 = sorted(list(idx.intersection((0, 60, 0, 60), objects=True)))
-        self.assertTrue(len(hits2), 10)
+        self.assertEqual(len(hits2), 10)
         self.assertEqual(hits2[0].object, 42)
 
     def test_overwrite(self) -> None:
@@ -846,15 +846,11 @@ class IndexCustomStorage(unittest.TestCase):
         """Reopening custom index storage works as expected"""
 
         storage = DictStorage()
-        settings = index.Property()
-        settings.writethrough = True
-        settings.buffering_capacity = 1
-
-        r1 = index.Index(storage, properties=settings, overwrite=True)
+        r1 = index.Index(storage, overwrite=True)
         r1.add(555, (2, 2))
         del r1
         self.assertTrue(storage.hasData)
 
-        r2 = index.Index(storage, properly=settings, overwrite=False)
+        r2 = index.Index(storage, overwrite=False)
         count = r2.count((0, 0, 10, 10))
         self.assertEqual(count, 1)


=====================================
tests/test_tpr.py
=====================================
@@ -3,8 +3,9 @@ from __future__ import annotations
 import os
 import unittest
 from collections import defaultdict, namedtuple
+from collections.abc import Iterator
 from math import ceil
-from typing import Any, Iterator
+from typing import Any
 
 import numpy as np
 from numpy.random import default_rng


=====================================
tox.ini
=====================================
@@ -1,7 +1,7 @@
 [tox]
 requires =
     tox>=4
-env_list = py{38,39,310,311,312}
+env_list = py{39,310,311,312,313}
 
 [testenv]
 description = run unit tests



View it on GitLab: https://salsa.debian.org/debian-gis-team/python-rtree/-/commit/e25d66454c78db34491de4241745c8b4c57c2df4

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/python-rtree/-/commit/e25d66454c78db34491de4241745c8b4c57c2df4
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20250306/1848ee27/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list