kdtree 0.16__tar.gz → 1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
kdtree-1.0/MANIFEST.in ADDED
@@ -0,0 +1,7 @@
1
+ include AUTHORS
2
+ include LICENSE
3
+ include readme.md
4
+ include test.py
5
+ include kdtree.pyi
6
+ include test_typing.py
7
+ include typing_sample.py
kdtree-1.0/PKG-INFO ADDED
@@ -0,0 +1,42 @@
1
+ Metadata-Version: 2.4
2
+ Name: kdtree
3
+ Version: 1.0
4
+ Summary: A Python implementation of a kd-tree
5
+ Home-page: https://github.com/stefankoegl/kdtree
6
+ Download-URL: http://pypi.python.org/packages/source/k/kdtree/kdtree-1.0.tar.gz
7
+ Author: Stefan Kögl
8
+ Author-email: stefan@skoegl.net
9
+ License: ISC license
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: ISC License (ISCL)
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Programming Language :: Python :: 3.15
22
+ Classifier: Programming Language :: Python :: Implementation :: CPython
23
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
24
+ Classifier: Topic :: Software Development :: Libraries
25
+ Classifier: Topic :: Utilities
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.11
28
+ License-File: LICENSE
29
+ License-File: AUTHORS
30
+ Dynamic: author
31
+ Dynamic: author-email
32
+ Dynamic: classifier
33
+ Dynamic: description
34
+ Dynamic: download-url
35
+ Dynamic: home-page
36
+ Dynamic: license
37
+ Dynamic: license-file
38
+ Dynamic: requires-python
39
+ Dynamic: summary
40
+
41
+ This package provides a simple implementation of a kd-tree in Python.
42
+ https://en.wikipedia.org/wiki/K-d_tree
@@ -0,0 +1,42 @@
1
+ Metadata-Version: 2.4
2
+ Name: kdtree
3
+ Version: 1.0
4
+ Summary: A Python implementation of a kd-tree
5
+ Home-page: https://github.com/stefankoegl/kdtree
6
+ Download-URL: http://pypi.python.org/packages/source/k/kdtree/kdtree-1.0.tar.gz
7
+ Author: Stefan Kögl
8
+ Author-email: stefan@skoegl.net
9
+ License: ISC license
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: ISC License (ISCL)
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Programming Language :: Python :: 3.15
22
+ Classifier: Programming Language :: Python :: Implementation :: CPython
23
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
24
+ Classifier: Topic :: Software Development :: Libraries
25
+ Classifier: Topic :: Utilities
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.11
28
+ License-File: LICENSE
29
+ License-File: AUTHORS
30
+ Dynamic: author
31
+ Dynamic: author-email
32
+ Dynamic: classifier
33
+ Dynamic: description
34
+ Dynamic: download-url
35
+ Dynamic: home-page
36
+ Dynamic: license
37
+ Dynamic: license-file
38
+ Dynamic: requires-python
39
+ Dynamic: summary
40
+
41
+ This package provides a simple implementation of a kd-tree in Python.
42
+ https://en.wikipedia.org/wiki/K-d_tree
@@ -2,10 +2,13 @@ AUTHORS
2
2
  LICENSE
3
3
  MANIFEST.in
4
4
  kdtree.py
5
+ kdtree.pyi
5
6
  readme.md
6
7
  setup.cfg
7
8
  setup.py
8
9
  test.py
10
+ test_typing.py
11
+ typing_sample.py
9
12
  kdtree.egg-info/PKG-INFO
10
13
  kdtree.egg-info/SOURCES.txt
11
14
  kdtree.egg-info/dependency_links.txt
@@ -1,7 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
3
 
4
- """A Python implemntation of a kd-tree
4
+ """A Python implementation of a kd-tree
5
5
 
6
6
  This package provides a simple implementation of a kd-tree in Python.
7
7
  https://en.wikipedia.org/wiki/K-d_tree
@@ -17,7 +17,7 @@ from collections import deque
17
17
  from functools import wraps
18
18
 
19
19
  __author__ = u'Stefan Kögl <stefan@skoegl.net>'
20
- __version__ = '0.16'
20
+ __version__ = '1.0'
21
21
  __website__ = 'https://github.com/stefankoegl/kdtree'
22
22
  __license__ = 'ISC license'
23
23
 
@@ -44,7 +44,7 @@ class Node(object):
44
44
  >>> Node( 1, left=Node(2) ).is_leaf
45
45
  False
46
46
  """
47
- return (not self.data) or \
47
+ return (self.data is None) or \
48
48
  (all(not bool(c) for c, p in self.children))
49
49
 
50
50
 
@@ -175,10 +175,10 @@ class Node(object):
175
175
  __bool__ = __nonzero__
176
176
 
177
177
  def __eq__(self, other):
178
- if isinstance(other, tuple):
179
- return self.data == other
180
- else:
178
+ if isinstance(other, Node):
181
179
  return self.data == other.data
180
+ else:
181
+ return self.data == other
182
182
 
183
183
  def __hash__(self):
184
184
  return id(self)
@@ -190,7 +190,7 @@ def require_axis(f):
190
190
  @wraps(f)
191
191
  def _wrapper(self, *args, **kwargs):
192
192
  if None in (self.axis, self.sel_axis):
193
- raise ValueError('%(func_name) requires the node %(node)s '
193
+ raise ValueError('%(func_name)s requires the node %(node)s '
194
194
  'to have an axis and a sel_axis function' %
195
195
  dict(func_name=f.__name__, node=repr(self)))
196
196
 
@@ -229,10 +229,10 @@ class KDNode(Node):
229
229
  Users should call add() only to the topmost tree.
230
230
  """
231
231
 
232
+ check_dimensionality([point], dimensions=self.dimensions)
233
+
232
234
  current = self
233
235
  while True:
234
- check_dimensionality([point], dimensions=current.dimensions)
235
-
236
236
  # Adding has hit an empty leaf-node, add here
237
237
  if current.data is None:
238
238
  current.data = point
@@ -294,33 +294,49 @@ class KDNode(Node):
294
294
 
295
295
  If there are multiple points matching "point", only one is removed. The
296
296
  optional "node" parameter is used for checking the identity, once the
297
- removeal candidate is decided."""
297
+ removal candidate is decided."""
298
+
299
+ return self._remove_one(point, node)[0]
300
+
301
+
302
+ def _remove_one(self, point, node):
303
+ """ Removes at most one matching node from the (sub)tree
304
+
305
+ Returns a (new-subtree-root, removed) tuple, where removed indicates
306
+ whether a node has been removed. """
298
307
 
299
308
  # Recursion has reached an empty leaf node, nothing here to delete
300
309
  if not self:
301
- return
310
+ return self, False
302
311
 
303
312
  # Recursion has reached the node to be deleted
304
313
  if self.should_remove(point, node):
305
- return self._remove(point)
314
+ return self._remove(point), True
306
315
 
307
316
  # Remove direct subnode
308
317
  if self.left and self.left.should_remove(point, node):
309
318
  self.left = self.left._remove(point)
319
+ return self, True
310
320
 
311
321
  elif self.right and self.right.should_remove(point, node):
312
322
  self.right = self.right._remove(point)
323
+ return self, True
313
324
 
314
- # Recurse to subtrees
325
+ # Recurse to subtrees. If the point lies on the splitting plane, both
326
+ # subtrees may contain it, but only one node must be removed
315
327
  if point[self.axis] <= self.data[self.axis]:
316
328
  if self.left:
317
- self.left = self.left.remove(point, node)
329
+ self.left, removed = self.left._remove_one(point, node)
330
+ if removed:
331
+ return self, True
318
332
 
319
333
  if point[self.axis] >= self.data[self.axis]:
320
334
  if self.right:
321
- self.right = self.right.remove(point, node)
335
+ self.right, removed = self.right._remove_one(point, node)
336
+ if removed:
337
+ return self, True
322
338
 
323
- return self
339
+ return self, False
324
340
 
325
341
 
326
342
  @require_axis
@@ -376,7 +392,10 @@ class KDNode(Node):
376
392
  Returns the (possibly new) root of the rebalanced tree
377
393
  """
378
394
 
379
- return create([x.data for x in self.inorder()])
395
+ points = [x.data for x in self.inorder()]
396
+ axis = self.axis if self.axis is not None else 0
397
+ return create(points, dimensions=self.dimensions, axis=axis,
398
+ sel_axis=self.sel_axis)
380
399
 
381
400
 
382
401
  def axis_dist(self, point, axis):
@@ -387,13 +406,17 @@ class KDNode(Node):
387
406
  return math.pow(self.data[axis] - point[axis], 2)
388
407
 
389
408
 
390
- def dist(self, point):
409
+ def dist(self, point, axis=None):
391
410
  """
392
411
  Squared distance between the current Node
393
412
  and the given point
394
413
  """
395
- r = range(self.dimensions)
396
- return sum([self.axis_dist(point, i) for i in r])
414
+ if axis is None:
415
+ axes = range(self.dimensions)
416
+ else:
417
+ axes = [axis]
418
+
419
+ return sum([self.axis_dist(point, i) for i in axes])
397
420
 
398
421
 
399
422
  def search_knn(self, point, k, dist=None):
@@ -406,7 +429,9 @@ class KDNode(Node):
406
429
  distances.
407
430
 
408
431
  dist is a distance function, expecting two points and returning a
409
- distance value. Distance values can be any compareable type.
432
+ distance value. dist should expect an optional `axis` parameter. If
433
+ given, the distance on the specified axis should be calculated.
434
+ Distance values must support comparison and unary negation.
410
435
 
411
436
  The result is an ordered list of (node, distance) tuples.
412
437
  """
@@ -414,21 +439,23 @@ class KDNode(Node):
414
439
  if k < 1:
415
440
  raise ValueError("k must be greater than 0.")
416
441
 
417
- if dist is None:
418
- get_dist = lambda n: n.dist(point)
419
- else:
420
- get_dist = lambda n: dist(n.data, point)
442
+ def get_dist(n, axis=None):
443
+ if dist is None:
444
+ return n.dist(point, axis=axis)
445
+ else:
446
+ return dist(n.data, point, axis=axis)
421
447
 
422
448
  results = []
423
449
 
424
- self._search_node(point, k, results, get_dist, itertools.count())
450
+ self._search_node(point, k, results, get_dist, itertools.count(),
451
+ prune=dist is None)
425
452
 
426
453
  # We sort the final result by the distance in the tuple
427
454
  # (<KdNode>, distance).
428
455
  return [(node, -d) for d, _, node in sorted(results, reverse=True)]
429
456
 
430
457
 
431
- def _search_node(self, point, k, results, get_dist, counter):
458
+ def _search_node(self, point, k, results, get_dist, counter, prune=True):
432
459
  if not self:
433
460
  return
434
461
 
@@ -446,32 +473,33 @@ class KDNode(Node):
446
473
  heapq.heapreplace(results, item)
447
474
  else:
448
475
  heapq.heappush(results, item)
476
+
449
477
  # get the splitting plane
450
478
  split_plane = self.data[self.axis]
451
- # get the squared distance between the point and the splitting plane
452
- # (squared since all distances are squared).
453
- plane_dist = point[self.axis] - split_plane
454
- plane_dist2 = plane_dist * plane_dist
479
+ # get the distance between the point and the splitting plane
480
+ plane_dist = get_dist(self, axis=self.axis)
455
481
 
456
482
  # Search the side of the splitting plane that the point is in
457
483
  if point[self.axis] < split_plane:
458
484
  if self.left is not None:
459
- self.left._search_node(point, k, results, get_dist, counter)
485
+ self.left._search_node(point, k, results, get_dist, counter,
486
+ prune=prune)
460
487
  else:
461
488
  if self.right is not None:
462
- self.right._search_node(point, k, results, get_dist, counter)
489
+ self.right._search_node(point, k, results, get_dist, counter,
490
+ prune=prune)
463
491
 
464
492
  # Search the other side of the splitting plane if it may contain
465
493
  # points closer than the farthest point in the current results.
466
- if -plane_dist2 > results[0][0] or len(results) < k:
494
+ if not prune or -plane_dist > results[0][0] or len(results) < k:
467
495
  if point[self.axis] < self.data[self.axis]:
468
496
  if self.right is not None:
469
497
  self.right._search_node(point, k, results, get_dist,
470
- counter)
498
+ counter, prune=prune)
471
499
  else:
472
500
  if self.left is not None:
473
501
  self.left._search_node(point, k, results, get_dist,
474
- counter)
502
+ counter, prune=prune)
475
503
 
476
504
 
477
505
  @require_axis
@@ -484,10 +512,14 @@ class KDNode(Node):
484
512
  with this location will be returned (not its neighbor).
485
513
 
486
514
  dist is a distance function, expecting two points and returning a
487
- distance value. Distance values can be any compareable type.
515
+ distance value. dist should expect an optional `axis` parameter. If
516
+ given, the distance on the specified axis should be calculated.
517
+ Distance values must support comparison and unary negation.
488
518
 
489
519
  The result is a (node, distance) tuple.
490
520
  """
521
+ if not self:
522
+ raise ValueError("tree is empty")
491
523
 
492
524
  return next(iter(self.search_knn(point, 1, dist)), None)
493
525
 
@@ -499,28 +531,36 @@ class KDNode(Node):
499
531
  nodeDist = get_dist(self)
500
532
 
501
533
  if nodeDist < dist:
502
- results.append(self.data)
534
+ results.append(self.data)
503
535
 
504
536
  # get the splitting plane
505
537
  split_plane = self.data[self.axis]
538
+ # dist is compared against squared distances, so the distance to the
539
+ # splitting plane has to be squared as well
540
+ plane_dist = self.axis_dist(point, self.axis)
506
541
 
507
- # Search the side of the splitting plane that the point is in
508
- if point[self.axis] <= split_plane + dist:
542
+ # Search each side of the splitting plane that may contain points
543
+ # within the given distance
544
+ if point[self.axis] <= split_plane or plane_dist < dist:
509
545
  if self.left is not None:
510
546
  self.left._search_nn_dist(point, dist, results, get_dist)
511
- if point[self.axis] >= split_plane - dist:
547
+ if point[self.axis] >= split_plane or plane_dist < dist:
512
548
  if self.right is not None:
513
549
  self.right._search_nn_dist(point, dist, results, get_dist)
514
550
 
515
551
 
516
552
  @require_axis
517
- def search_nn_dist(self, point, distance, best=None):
553
+ def search_nn_dist(self, point, distance):
518
554
  """
519
555
  Search the n nearest nodes of the given point which are within given
520
556
  distance
521
557
 
522
558
  point must be a location, not a node. A list containing the n nearest
523
559
  nodes to the point within the distance will be returned.
560
+
561
+ distance is the threshold distance to filter nodes.
562
+ Note: KDNode.dist() returns squared distance, so the comparison will
563
+ be done using squared_distance < distance.
524
564
  """
525
565
 
526
566
  results = []
@@ -529,6 +569,51 @@ class KDNode(Node):
529
569
  self._search_nn_dist(point, distance, results, get_dist)
530
570
  return results
531
571
 
572
+ @require_axis
573
+ def range_search(self, top_left, bottom_right):
574
+ """
575
+ Search all nodes in or on the hyperrectangle defined by the points top_left and bottom_right.
576
+
577
+ Note: the list returned is not in any particular order.
578
+ """
579
+
580
+ invalid_rect = False
581
+ if len(top_left) != len(bottom_right) or len(top_left) != self.dimensions:
582
+ invalid_rect = True
583
+ else:
584
+ for i in range(len(top_left)):
585
+ if top_left[i] > bottom_right[i]:
586
+ invalid_rect = True
587
+ break
588
+
589
+ if invalid_rect:
590
+ raise ValueError("invalid rectangle")
591
+
592
+ results = []
593
+ stack = [self]
594
+
595
+ while(stack):
596
+ node = stack.pop()
597
+
598
+ data = node.data
599
+ axis = node.axis
600
+
601
+ if data is None:
602
+ continue
603
+
604
+ in_range = True
605
+ for d in range(0, len(data)):
606
+ in_range &= (data[d] >= top_left[d]) and (data[d] <= bottom_right[d])
607
+
608
+ if in_range:
609
+ results.append(node)
610
+
611
+ if data[axis] >= top_left[axis] and node.left:
612
+ stack.append(node.left)
613
+ if data[axis] <= bottom_right[axis] and node.right:
614
+ stack.append(node.right)
615
+
616
+ return results
532
617
 
533
618
  @require_axis
534
619
  def is_valid(self):
@@ -606,8 +691,8 @@ def create(point_list=None, dimensions=None, axis=0, sel_axis=None):
606
691
  median = len(point_list) // 2
607
692
 
608
693
  loc = point_list[median]
609
- left = create(point_list[:median], dimensions, sel_axis(axis))
610
- right = create(point_list[median + 1:], dimensions, sel_axis(axis))
694
+ left = create(point_list[:median], dimensions, sel_axis(axis), sel_axis)
695
+ right = create(point_list[median + 1:], dimensions, sel_axis(axis), sel_axis)
611
696
  return KDNode(loc, left, right, axis=axis, sel_axis=sel_axis, dimensions=dimensions)
612
697
 
613
698
 
@@ -651,7 +736,8 @@ def visualize(tree, max_level=100, node_width=10, left_padding=5):
651
736
  in_level = 0
652
737
  level = 0
653
738
 
654
- for node in level_order(tree, include_all=True):
739
+ # level_order() is infinite with include_all; the loop ends with break
740
+ for node in level_order(tree, include_all=True): # pragma: no branch
655
741
 
656
742
  if in_level == 0:
657
743
  print()
kdtree-1.0/kdtree.pyi ADDED
@@ -0,0 +1,165 @@
1
+ """Type stubs for the kdtree module
2
+
3
+ kdtree is distributed as a single-file module. Type information is therefore
4
+ shipped as this stub file, which is installed next to kdtree.py.
5
+ """
6
+
7
+ from collections.abc import Callable, Collection, Iterable, Iterator, Sequence
8
+ from typing import Any, Generic, ParamSpec, Protocol, Self, TypeVar, overload
9
+
10
+ __author__: str
11
+ __version__: str
12
+ __website__: str
13
+ __license__: str
14
+
15
+ class _Point(Protocol):
16
+ """Anything that can be used as a point: it has a length and its
17
+ coordinates can be accessed by index"""
18
+
19
+ def __len__(self) -> int: ...
20
+ def __getitem__(self, index: int, /) -> Any: ...
21
+
22
+ _T = TypeVar("_T")
23
+ _P = TypeVar("_P", bound=_Point)
24
+ _Q = TypeVar("_Q", bound=_Point)
25
+ _P_contra = TypeVar("_P_contra", contravariant=True)
26
+ _Q_contra = TypeVar("_Q_contra", contravariant=True)
27
+ _D_co = TypeVar("_D_co", covariant=True)
28
+ _N = TypeVar("_N", bound=Node[Any])
29
+ _D = TypeVar("_D", bound=_Distance)
30
+ _R = TypeVar("_R")
31
+ _Params = ParamSpec("_Params")
32
+
33
+ class _Distance(Protocol):
34
+ """A distance value: search_knn() negates distances and compares them"""
35
+
36
+ def __neg__(self) -> Self: ...
37
+ def __lt__(self, other: Any, /) -> bool: ...
38
+ def __gt__(self, other: Any, /) -> bool: ...
39
+
40
+ class _DistFunc(Protocol[_P_contra, _Q_contra, _D_co]):
41
+ """A distance function, always called as dist(a, b, axis=axis)
42
+
43
+ a is a point stored in the tree, b is the point that is searched for."""
44
+
45
+ def __call__(
46
+ self, a: _P_contra, b: _Q_contra, /, *, axis: int | None
47
+ ) -> _D_co: ...
48
+
49
+ class Node(Generic[_T]):
50
+ data: _T | None
51
+ left: Self | None
52
+ right: Self | None
53
+ def __init__(
54
+ self,
55
+ data: _T | None = None,
56
+ left: Self | None = None,
57
+ right: Self | None = None,
58
+ ) -> None: ...
59
+ @property
60
+ def is_leaf(self) -> bool: ...
61
+ def preorder(self) -> Iterator[Self]: ...
62
+ def inorder(self) -> Iterator[Self]: ...
63
+ def postorder(self) -> Iterator[Self]: ...
64
+ @property
65
+ def children(self) -> Iterator[tuple[Self, int]]: ...
66
+ def set_child(self, index: int, child: Self | None) -> None: ...
67
+ def height(self) -> int: ...
68
+ def get_child_pos(self, child: Node[Any]) -> int | None: ...
69
+ def __nonzero__(self) -> bool: ...
70
+ def __bool__(self) -> bool: ...
71
+ def __eq__(self, other: object) -> bool: ...
72
+ def __hash__(self) -> int: ...
73
+
74
+ def require_axis(f: Callable[_Params, _R]) -> Callable[_Params, _R]: ...
75
+
76
+ class KDNode(Node[_P]):
77
+ axis: int | None
78
+ sel_axis: Callable[[int], int] | None
79
+ dimensions: int | None
80
+ def __init__(
81
+ self,
82
+ data: _P | None = None,
83
+ left: Self | None = None,
84
+ right: Self | None = None,
85
+ axis: int | None = None,
86
+ sel_axis: Callable[[int], int] | None = None,
87
+ dimensions: int | None = None,
88
+ ) -> None: ...
89
+ def add(self, point: _P) -> Self: ...
90
+ def create_subnode(self, data: _P | None) -> Self: ...
91
+ def find_replacement(self) -> tuple[Self | None, Self]: ...
92
+ def should_remove(self, point: _Point, node: KDNode[Any] | None) -> bool: ...
93
+ def remove(self, point: _Point, node: KDNode[Any] | None = None) -> Self: ...
94
+ @property
95
+ def is_balanced(self) -> bool: ...
96
+ def rebalance(self) -> KDNode[_P]: ...
97
+ def axis_dist(self, point: _Point, axis: int) -> float: ...
98
+ def dist(self, point: _Point, axis: int | None = None) -> float: ...
99
+ @overload
100
+ def search_knn(
101
+ self, point: _Point, k: int, dist: None = None
102
+ ) -> list[tuple[Self, float]]: ...
103
+ @overload
104
+ def search_knn(
105
+ self, point: _Q, k: int, dist: _DistFunc[_P, _Q, _D]
106
+ ) -> list[tuple[Self, _D]]: ...
107
+ @overload
108
+ def search_nn(self, point: _Point, dist: None = None) -> tuple[Self, float]: ...
109
+ @overload
110
+ def search_nn(
111
+ self, point: _Q, dist: _DistFunc[_P, _Q, _D]
112
+ ) -> tuple[Self, _D]: ...
113
+ def search_nn_dist(self, point: _Point, distance: float) -> list[_P]: ...
114
+ def range_search(self, top_left: _Point, bottom_right: _Point) -> list[Self]: ...
115
+ def is_valid(self) -> bool: ...
116
+ def extreme_child(
117
+ self,
118
+ sel_func: Callable[..., Any],
119
+ axis: int,
120
+ ) -> tuple[Self, Self | None] | tuple[None, None]: ...
121
+
122
+ @overload
123
+ def create(
124
+ point_list: None = None,
125
+ *,
126
+ dimensions: int,
127
+ axis: int = 0,
128
+ sel_axis: Callable[[int], int] | None = None,
129
+ ) -> KDNode[Any]: ...
130
+ @overload
131
+ def create(
132
+ point_list: None,
133
+ dimensions: int,
134
+ axis: int = 0,
135
+ sel_axis: Callable[[int], int] | None = None,
136
+ ) -> KDNode[Any]: ...
137
+ @overload
138
+ def create(
139
+ point_list: Sequence[_P],
140
+ dimensions: int | None = None,
141
+ axis: int = 0,
142
+ sel_axis: Callable[[int], int] | None = None,
143
+ ) -> KDNode[_P]: ...
144
+ @overload
145
+ def create(
146
+ # without dimensions, they are taken from point_list[0]. Iterators are
147
+ # not supported, as the dimensionality check consumes them
148
+ point_list: Collection[_P],
149
+ dimensions: int,
150
+ axis: int = 0,
151
+ sel_axis: Callable[[int], int] | None = None,
152
+ ) -> KDNode[_P]: ...
153
+ @overload
154
+ def check_dimensionality(
155
+ point_list: Sequence[_Point], dimensions: int | None = None
156
+ ) -> int: ...
157
+ @overload
158
+ def check_dimensionality(point_list: Iterable[_Point], dimensions: int) -> int: ...
159
+ def level_order(tree: _N, include_all: bool = False) -> Iterator[_N]: ...
160
+ def visualize(
161
+ tree: Node[Any],
162
+ max_level: int = 100,
163
+ node_width: int = 10,
164
+ left_padding: int = 5,
165
+ ) -> None: ...
@@ -1,4 +1,4 @@
1
- A simple kd-tree in Python [![Build Status](https://travis-ci.org/stefankoegl/kdtree.png?branch=master)](https://travis-ci.org/stefankoegl/kdtree)
1
+ A simple kd-tree in Python [![Tests](https://github.com/stefankoegl/kdtree/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/stefankoegl/kdtree/actions/workflows/tests.yml)
2
2
  ==========================
3
3
 
4
4
  The kdtree package can construct, modify and search
@@ -8,7 +8,7 @@ The kdtree package can construct, modify and search
8
8
  * Repository: https://github.com/stefankoegl/kdtree.git
9
9
  * Documentation: https://python-kdtree.readthedocs.org/
10
10
  * PyPI: https://pypi.python.org/pypi/kdtree
11
- * Travis-CI: https://travis-ci.org/stefankoegl/kdtree
11
+ * GitHub Actions: https://github.com/stefankoegl/kdtree/actions
12
12
  * Coveralls: https://coveralls.io/r/stefankoegl/kdtree
13
13
 
14
14
 
@@ -55,7 +55,7 @@ Usage
55
55
 
56
56
  # Find the nearest node to the location (1, 2, 3)
57
57
  >>> tree.search_nn( (1, 2, 3) )
58
- <KDNode - (2, 3, 4)>
58
+ (<KDNode - (2, 3, 4)>, 3.0)
59
59
 
60
60
  # Add a point to make the tree more interesting
61
61
  >>> tree.add( (10, 2, 1) )
@@ -73,7 +73,7 @@ Usage
73
73
  # Take the right subtree of the root
74
74
  >>> subtree = tree.right
75
75
 
76
- # and detatch it
76
+ # and detach it
77
77
  >>> tree.right = None
78
78
  >>> kdtree.visualize(tree)
79
79
 
@@ -122,6 +122,21 @@ Usage
122
122
 
123
123
  (2, 3, 4)
124
124
 
125
+ ### Type hints
126
+
127
+ Type information is provided in the stub file `kdtree.pyi`, which is installed
128
+ next to `kdtree.py` and, for mypy, as the stub-only package `kdtree-stubs`.
129
+ Trees are generic in the type of their points:
130
+
131
+ ```python
132
+ tree = kdtree.create([(1, 2), (3, 4)]) # KDNode[tuple[int, int]]
133
+ node, dist = tree.search_nn((1, 1)) # KDNode[tuple[int, int]], float
134
+ ```
135
+
136
+ The classes are only generic for type checkers; at runtime write
137
+ annotations like `kdtree.KDNode[tuple[int, int]]` as strings or use
138
+ `from __future__ import annotations`.
139
+
125
140
  ### Adding a payload
126
141
 
127
142
  Indexing a dict by a pair of floats is not a good idea, since there might be unexpected precision errors.