Repository: scipy/scipy
Pull request: ENH: static typing for qhull
<!-- 
Thanks for contributing a pull request! Please ensure that
your PR satisfies the checklist before submitting:
http://scipy.github.io/devdocs/dev/contributor/development_workflow.html#checklist-before-submitting-a-pr

Also, please name and describe your PR as you would write a
commit message:
https://docs.scipy.org/doc/numpy/dev/development_workflow.html#writing-the-commit-message

Note that we are a team of volunteers; we appreciate your
patience during the review process.

Again, thanks for contributing!
-->

#### Reference issue
<!--Example: Closes gh-WXYZ.-->

Closes https://github.com/scipy/scipy/issues/13299: static typing for qhull.pyx

#### What does this implement/fix?
<!--Please explain your changes.-->

- Add `qhull.pyi` stub file to declare types for classes/functions in `qhull.pyx`
- imports numpy.typing if possible, otherwise `numpy.typing.ArrayLike` falls back to `Any`

#### Additional information
<!--Any additional information you think is important.-->

- Unable to import `__future__ annotations` in Cython, so we need stub files
- I tried to type all functions that were possible, the only thing I think I missed was the `qhull_options` argument of the constructor for `_QhullUser`s because it has dynamic type during runtime (str and bytes)
- `mypy` appears to parse the file:
```python
import numpy as np
from scipy.spatial import ConvexHull
ConvexHull([]).add_points(np.random.rand(30, 2), 'not a bool')

# mypy output: test.py:3: error: Argument 2 to "add_points" of "ConvexHull" has incompatible type "str"; expected "bool"  [arg-type]
```

Problem statement:
Typing for scipy.spatial
**Is your feature request related to a problem? Please describe.**
It would be great to have typing for spatial (e.g. Voronoi) as some fields are undocumented (e.g. ridge_dict), and some are not detected by IDE introspection (e.g. vertices).

**Describe the solution you'd like**
Add typing to `scipy/spatial/qhull.py` *-or-* add stubs if the `qhull.py` heavily depends on C code.

**Describe alternatives you've considered**
Add typing stubs outside of scipy - that would be time-consuming and hard to maintain if spatial (or qhull) change or add API.

**Additional context (e.g. screenshots)**
(none)

Hints from issue discussion:
There is no `qhull.py`, it's Cython code. Adding type annotations directly in Cython code seems to be possible (http://docs.cython.org/en/latest/src/tutorial/pure.html#pep-484-type-annotations), but it seems a little unusual and I'd expect some problems like conflicts with the regular way of typing parameters in Cython. 

If someone wants to try the inline thing please do, but otherwise adding type stubs is the way to go. A PR for that would be very welcome. Annotations should use `numpy.typing` as discussed in https://github.com/scipy/scipy/pull/10844#issuecomment-752194177.

Commit message: TYPE: don't ignore qhull missing imports

Patch to review:
diff --git a/mypy.ini b/mypy.ini
index bb4f43a997e9..a8c055c18c81 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -94,9 +94,6 @@ ignore_missing_imports = True
 [mypy-scipy.interpolate.dfitpack]
 ignore_missing_imports = True
 
-[mypy-scipy.spatial.qhull]
-ignore_missing_imports = True
-
 [mypy-scipy.interpolate.interpnd]
 ignore_missing_imports = True
 
diff --git a/scipy/spatial/qhull.pyi b/scipy/spatial/qhull.pyi
new file mode 100644
index 000000000000..fa4f6e94b70a
--- /dev/null
+++ b/scipy/spatial/qhull.pyi
@@ -0,0 +1,98 @@
+'''
+Static type checking stub file for scipy/spatial/qhull.pyx
+'''
+
+from typing import TYPE_CHECKING, List, Tuple, Any
+
+if TYPE_CHECKING:
+    from numpy.typing import ArrayLike
+else:
+    ArrayLike = Any
+
+import numpy as np
+
+class _Qhull:
+    def __init__(self, points: ArrayLike, furthest_site: bool = ...,
+                 incremental: bool = ..., qhull_options=...): ...
+    def check_active(self) -> None: ...
+    def close(self) -> None: ...
+    def get_points(self) -> np.ndarray: ...
+    def add_points(self, points: ArrayLike,
+                   interior_point: ArrayLike = ...): ...
+    def get_paraboloid_shift_scale(self) -> Tuple[float, float]: ...
+    def volume_area(self) -> Tuple[float, float]: ...
+    def triangulate(self) -> None: ...
+    def get_simplex_facet_array(self): ...
+    def get_hull_points(self) -> np.ndarray: ...
+    def get_hull_facets(self) -> Tuple[List[List[int]], np.ndarray]: ...
+    def get_voronoi_diagram(self) -> Tuple[np.ndarray, np.ndarray,
+                                           List[List], List[List],
+                                           np.ndarray]: ...
+    def get_extremes_2d(self) -> np.ndarray: ...
+
+def _get_barycentric_transforms(points: np.ndarray, simplices: np.ndarray,
+                                eps: float) -> np.ndarray: ...
+
+class _QhullUser:
+    def close(self) -> None: ...
+    def _update(self, qhull: _Qhull) -> None: ...
+    def _add_points(self, points: ArrayLike, restart: bool = ...,
+                    interior_point: ArrayLike = ...) -> None: ...
+
+class Delaunay(_QhullUser):
+    def __init__(self, points: ArrayLike, furthest_site: bool = ...,
+                 incremental: bool = ..., qhull_options=...): ...
+    def _update(self, qhull: _Qhull) -> None: ...
+    def add_points(self, points: ArrayLike,
+                   restart: bool = ...) -> None: ...
+    @property
+    def points(self) -> np.ndarray: ...
+    @property
+    def transform(self) -> np.ndarray: ...
+    @property
+    def vertex_to_simplex(self) -> np.ndarray: ...
+    @property
+    def vertex_neighbor_vertices(self) -> Tuple[np.ndarray, np.ndarray]: ...
+    @property
+    def convex_hull(self) -> np.ndarray: ...
+    def find_simplex(self, xi: ArrayLike, bruteforce: bool = ...,
+                     tol: float = ...) -> np.ndarray: ...
+    def plane_distance(self, xi: ArrayLike) -> np.ndarray: ...
+    def lift_points(self, x: ArrayLike) -> np.ndarray: ...
+
+def tsearch(tri: Delaunay, xi: ArrayLike) -> np.ndarray: ...
+def _copy_docstr(dst: object, src: object) -> None: ...
+
+class ConvexHull(_QhullUser):
+    def __init__(self, points: ArrayLike, incremental: bool = ...,
+                 qhull_options=...): ...
+    def _update(self, qhull: _Qhull) -> None: ...
+    def add_points(self, points: ArrayLike,
+                   restart: bool = ...) -> None: ...
+    @property
+    def points(self) -> np.ndarray: ...
+    @property
+    def vertices(self) -> np.ndarray: ...
+
+class Voronoi(_QhullUser):
+    def __init__(self, points: ArrayLike, furthest_site: bool = ...,
+                 incremental: bool = ..., qhull_options=...): ...
+    def _update(self, qhull: _Qhull) -> None: ...
+    def add_points(self, points: ArrayLike,
+                   restart: bool = ...) -> None: ...
+    @property
+    def points(self) -> np.ndarray: ...
+    @property
+    def ridge_dict(self) -> dict: ...
+
+class HalfspaceIntersection(_QhullUser):
+    def __init__(self, halfspaces: ArrayLike,
+                 interior_point: ArrayLike, incremental: bool = ...,
+                 qhull_options=...): ...
+    def _update(self, qhull: _Qhull) -> None: ...
+    def add_halfspaces(self, halfspaces: ArrayLike,
+                       restart: bool = ...) -> None: ...
+    @property
+    def halfspaces(self) -> np.ndarray: ...
+    @property
+    def dual_vertices(self) -> np.ndarray: ...


Code review comments:
diff_hunk: @@ -0,0 +1,98 @@
+'''
+Static type checking stub file for scipy/spatial/qhull.pyx
+'''
+
+from typing import TYPE_CHECKING, List, Tuple, Any
+
+if TYPE_CHECKING:
+    from numpy.typing import ArrayLike
+else:
+    ArrayLike = Any
+
+import numpy as np
+
+class _Qhull:
+    def __init__(self, points: ArrayLike, furthest_site: bool = ...,
+                 incremental: bool = ..., qhull_options=...): ...
path: scipy/spatial/qhull.pyi
text: @user1:
`qhull_options` should be `str` I think.

@author:
I did try `str`, but it's converted later to `bytes` which mypy will complain about and I'm not mypy-savy enough to figure out how to get it working