Skip to content

Commit 082c9b2

Browse files
committed
docs: overhaul pyqtree docstrings + add loose type annotations
1 parent dcfd709 commit 082c9b2

File tree

5 files changed

+120
-44
lines changed

5 files changed

+120
-44
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "fastquadtree"
3-
version = "1.3.1"
3+
version = "1.3.2"
44
edition = "2021"
55

66
[lib]

docs/api/pyqtree.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# fastquadtree.pyqtree
1+
# fastquadtree.pyqtree.Index
22
::: fastquadtree.pyqtree.Index
33
options:
44
inherited_members: true

pysrc/fastquadtree/pyqtree.py

Lines changed: 47 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55

66
from __future__ import annotations
77

8+
from collections.abc import Iterable
89
from operator import itemgetter
9-
from typing import Any, Tuple
10+
from typing import Any, SupportsFloat, Tuple
1011

1112
from ._native import RectQuadTree
1213

@@ -34,18 +35,18 @@ def gather_objs(objs, ids, chunk=2048):
3435

3536
class Index:
3637
"""
37-
The class below is taken from the pyqtree package, but the implementation
38+
The interface of the class below is taken from the pyqtree package, but the implementation
3839
has been modified to use the fastquadtree package as a backend instead of
3940
the original pure-python implementation.
4041
Based on the benchmarks, this gives a overall performance boost of 6.514x.
4142
See the benchmark section of the docs for more details and the latest numbers.
4243
43-
Original docstring from pyqtree follows:
44-
The top spatial index to be created by the user. Once created it can be
45-
populated with geographically placed members that can later be tested for
46-
intersection with a user inputted geographic bounding box. Note that the
47-
index can be iterated through in a for-statement, which loops through all
48-
all the quad instances and lets you access their properties.
44+
Index is the top-level class for creating and using a quadtree spatial index
45+
with the original pyqtree interface. If you are not migrating from pyqtree,
46+
consider using the RectQuadTree class for detailed control and better performance.
47+
48+
This class wraps a RectQuadTree instance and provides methods to insert items with bounding boxes,
49+
remove items, and query for items intersecting a given bounding box.
4950
5051
Example usage:
5152
```python
@@ -65,32 +66,36 @@ class Index:
6566

6667
def __init__(
6768
self,
68-
bbox=None,
69-
x=None,
70-
y=None,
71-
width=None,
72-
height=None,
73-
max_items=MAX_ITEMS,
74-
max_depth=MAX_DEPTH,
69+
bbox: Iterable[SupportsFloat] | None = None,
70+
x: float | int | None = None,
71+
y: float | int | None = None,
72+
width: float | int | None = None,
73+
height: float | int | None = None,
74+
max_items: int = MAX_ITEMS,
75+
max_depth: int = MAX_DEPTH,
7576
):
7677
"""
7778
Initiate by specifying either 1) a bbox to keep track of, or 2) with an xy centerpoint and a width and height.
7879
79-
Parameters:
80-
- **bbox**: The coordinate system bounding box of the area that the quadtree should
80+
Args:
81+
bbox: The coordinate system bounding box of the area that the quadtree should
8182
keep track of, as a 4-length sequence (xmin,ymin,xmax,ymax)
82-
- **x**:
83+
x:
8384
The x center coordinate of the area that the quadtree should keep track of.
84-
- **y**
85+
y:
8586
The y center coordinate of the area that the quadtree should keep track of.
86-
- **width**:
87+
width:
8788
How far from the xcenter that the quadtree should look when keeping track.
88-
- **height**:
89+
height:
8990
How far from the ycenter that the quadtree should look when keeping track
90-
- **max_items** (optional): The maximum number of items allowed per quad before splitting
91-
up into four new subquads. Default is 10.
92-
- **max_depth** (optional): The maximum levels of nested subquads, after which no more splitting
91+
max_items (optional): The maximum number of items allowed per quad before splitting
92+
up into four new subquads. Default is 10.
93+
max_depth (optional): The maximum levels of nested subquads, after which no more splitting
9394
occurs and the bottommost quad nodes may grow indefinately. Default is 20.
95+
96+
Note:
97+
Either the bbox argument must be set, or the x, y, width, and height
98+
arguments must be set.
9499
"""
95100
if bbox is not None:
96101
x1, y1, x2, y2 = bbox
@@ -114,15 +119,15 @@ def __init__(
114119
self._free = []
115120
self._item_to_id = {}
116121

117-
def insert(self, item: Any, bbox): # pyright: ignore[reportIncompatibleMethodOverride]
122+
def insert(self, item: Any, bbox: Iterable[SupportsFloat]):
118123
"""
119124
Inserts an item into the quadtree along with its bounding box.
120125
121-
Parameters:
122-
- **item**: The item to insert into the index, which will be returned by the intersection method
123-
- **bbox**: The spatial bounding box tuple of the item, with four members (xmin,ymin,xmax,ymax)
126+
Args:
127+
item: The item to insert into the index, which will be returned by the intersection method
128+
bbox: The spatial bounding box tuple of the item, with four members (xmin,ymin,xmax,ymax)
124129
"""
125-
if type(bbox) is list: # Handle list input
130+
if type(bbox) is not tuple: # Handle non-tuple input
126131
bbox = tuple(bbox)
127132

128133
if self._free:
@@ -134,36 +139,37 @@ def insert(self, item: Any, bbox): # pyright: ignore[reportIncompatibleMethodOv
134139
self._qt.insert(rid, bbox)
135140
self._item_to_id[id(item)] = rid
136141

137-
def remove(self, item, bbox):
142+
def remove(self, item: Any, bbox: Iterable[SupportsFloat]):
138143
"""
139144
Removes an item from the quadtree.
140145
141-
Parameters:
142-
- **item**: The item to remove from the index
143-
- **bbox**: The spatial bounding box tuple of the item, with four members (xmin,ymin,xmax,ymax)
146+
Args:
147+
item: The item to remove from the index
148+
bbox: The spatial bounding box tuple of the item, with four members (xmin,ymin,xmax,ymax)
144149
145-
Both parameters need to exactly match the parameters provided to the insert method.
150+
Note:
151+
Both parameters need to exactly match the parameters provided to the insert method.
146152
"""
147-
if type(bbox) is list: # Handle list input
153+
if type(bbox) is not tuple: # Handle non-tuple input
148154
bbox = tuple(bbox)
149155

150156
rid = self._item_to_id.pop(id(item))
151157
self._qt.delete(rid, bbox)
152158
self._objects[rid] = None
153159
self._free.append(rid)
154160

155-
def intersect(self, bbox):
161+
def intersect(self, bbox: Iterable[SupportsFloat]) -> list:
156162
"""
157-
Intersects an input boundingbox rectangle with all of the items
163+
Intersects an input bounding box rectangle with all of the items
158164
contained in the quadtree.
159165
160-
Parameters:
161-
- **bbox**: A spatial bounding box tuple with four members (xmin,ymin,xmax,ymax)
166+
Args:
167+
bbox: A spatial bounding box tuple with four members (xmin,ymin,xmax,ymax)
162168
163169
Returns:
164-
- A list of inserted items whose bounding boxes intersect with the input bbox.
170+
A list of inserted items whose bounding boxes intersect with the input bbox.
165171
"""
166-
if type(bbox) is list: # Handle list input
172+
if type(bbox) is not tuple: # Handle non-tuple input
167173
bbox = tuple(bbox)
168174
result = self._qt.query_ids(bbox)
169175
# result = [id1, id2, ...]

tests/test_pyqtree_shim_compat.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,3 +401,73 @@ def test_insert_list_and_tuple_equivalence():
401401
# Both objects should be present
402402
results = idx.intersect((0.0, 0.0, 100.0, 100.0))
403403
assert set(results) == {obj1, obj2}
404+
405+
406+
def test_insert_non_list_non_tuple_iterator():
407+
"""Test that any iterable (not just list/tuple) works for bbox in insert."""
408+
idx = FQTIndex(bbox=WORLD)
409+
410+
obj1, box1 = "obj1", (10.0, 10.0, 20.0, 20.0)
411+
412+
obj2 = "obj2"
413+
414+
def obj2_box2_iterator():
415+
yield 30.0
416+
yield 30.0
417+
yield 40.0
418+
yield 40.0
419+
420+
# Insert using tuple
421+
idx.insert(obj1, box1)
422+
423+
# Insert using range iterator
424+
idx.insert(obj2, obj2_box2_iterator())
425+
426+
# Both objects should be present
427+
results = idx.intersect((0.0, 0.0, 100.0, 100.0))
428+
assert set(results) == {obj1, obj2}
429+
430+
# Try Range
431+
obj3 = "obj3"
432+
box3_range = range(50, 54) # 50, 51, 52, 53
433+
idx.insert(obj3, box3_range)
434+
results = idx.intersect((0.0, 0.0, 100.0, 100.0))
435+
assert set(results) == {obj1, obj2, obj3}
436+
437+
438+
def test_insert_fails_on_tuple_too_long():
439+
"""Test that insert fails when bbox tuple is too long."""
440+
idx = FQTIndex(bbox=WORLD)
441+
442+
obj1 = "obj1"
443+
box1 = (10.0, 10.0, 20.0, 20.0, 30.0) # This should fail
444+
445+
with pytest.raises(ValueError):
446+
idx.insert(obj1, box1)
447+
448+
449+
def non_tuple_intersect_and_non_tuple_remove_handled():
450+
"""Test that intersect and remove accept any iterable (not just list/tuple)."""
451+
idx = FQTIndex(bbox=WORLD)
452+
453+
obj1, box1 = "obj1", (10.0, 10.0, 20.0, 20.0)
454+
455+
idx.insert(obj1, box1)
456+
457+
def query_iterator():
458+
yield 15.0
459+
yield 15.0
460+
yield 25.0
461+
yield 25.0
462+
463+
results = idx.intersect(query_iterator())
464+
assert results == [obj1]
465+
466+
def remove_box_iterator():
467+
yield 10.0
468+
yield 10.0
469+
yield 20.0
470+
yield 20.0
471+
472+
idx.remove(obj1, remove_box_iterator())
473+
assert idx.intersect((0.0, 0.0, 100.0, 100.0)) == []

0 commit comments

Comments
 (0)