Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ jobs:
- name: Run mypy tests
run: python -m mypy . --python-version=${{ matrix.python-version }}

- name: Run runtime regression tests
run: python -m pytest -q tests

- name: Run stubtest
run: python tests/run_stubtest.py

Expand Down
28 changes: 28 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,31 @@ instructions provided by the bot. You will only need to do this once across all
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

## Checking stub changes

Use an isolated Python environment and install the test and hygiene dependencies:

```shell
python -m pip install --group tests --group hygiene
python -m ruff check
python -m ruff format --check
python -m mypy . --python-version 3.13
python -m pyright --pythonversion 3.13
python -m pytest -q tests
python tests/run_stubtest.py
```

Select the same environment in Pyright, using `--pythonpath` if necessary. Include the
local stubs when running mypy on regression tests; checking only a test directory can
resolve installed library code instead of the stubs being changed.

CI checks Python 3.10 and 3.13 on Linux, Windows, and macOS. Run against both Python
versions when updating runtime APIs: the latest compatible library release can differ
between them. For a focused runtime check, pass package directories to the runner,
for example `python tests/run_stubtest.py vispy sklearn`.

Fix incorrect declarations before adding allowlist entries. Keep exceptions narrow
and documented, and remove entries that no longer suppress an error. Modules shipped
only in some wheels or on some platforms need optional entries rather than disabling
unused-allowlist checks globally.
18 changes: 18 additions & 0 deletions stubs/skimage/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
These stubs were created with https://github.com/gramster/docs2stubs.

The stubs target the latest scikit-image supported by each Python version:
0.25 on Python 3.10 and 0.26 on Python 3.11 and newer. Version-dependent APIs
use `sys.version_info` branches because scikit-image 0.26 requires Python 3.11.
For Python 3.11 and newer, use 0.26 to obtain the newly annotated model and
transform estimation APIs.

The deprecated no-argument model constructors and `estimate` methods remain
available. New `from_estimate` methods return either the requested model or
`skimage._shared.utils.FailedEstimation`, not a boolean or `None`.

`RansacModelProtocol` accepts a variadic data signature: annotate one-array
models as `RansacModelProtocol[NDArray]` and two-array transforms as
`RansacModelProtocol[NDArray, NDArray]`. These parameters are typing-only;
use postponed annotations (`from __future__ import annotations`) because the
runtime protocol does not support subscripting.

The allowlist documents five specific 0.26 signature-introspection bugs rather
than changing the annotations to match incorrect wrapper metadata. Optional
test-module entries are exact names and do not suppress public API failures.
9 changes: 8 additions & 1 deletion stubs/skimage/_shared/testing.pyi
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import sys

from numpy.typing import ArrayLike

SKIP_RE = ...
Expand All @@ -23,5 +25,10 @@ def color_check(plugin, fmt="png"): ...
def mono_check(plugin, fmt="png"): ...
def setup_test(): ...
def teardown_test(): ...
def fetch(data_filename): ...

if sys.version_info >= (3, 11):
def fetch(data_filename: str, prefix: str | None = None) -> str: ...
else:
def fetch(data_filename: str) -> str: ...

def test_parallel(num_threads: int = 2, warnings_matching: ArrayLike | None = None): ...
29 changes: 27 additions & 2 deletions stubs/skimage/_shared/utils.pyi
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import sys
from collections.abc import Iterable, Mapping
from typing import Any, Literal
from typing import Any, ClassVar, Literal
from typing_extensions import Never

from numpy.typing import ArrayLike, NDArray

Expand Down Expand Up @@ -30,7 +32,18 @@ class _DecoratorBaseClass:
def get_stack_length(self, func): ...

class change_default_value(_DecoratorBaseClass):
def __init__(self, arg_name: str, *, new_value: Any, changed_version: str, warning_msg: str | None = None): ...
if sys.version_info >= (3, 11):
def __init__(
self,
arg_name: str,
*,
new_value: Any,
changed_version: str,
warning_msg: str | None = None,
stacklevel: int | None = None,
): ...
else:
def __init__(self, arg_name: str, *, new_value: Any, changed_version: str, warning_msg: str | None = None): ...
def __call__(self, func): ...

class remove_arg(_DecoratorBaseClass):
Expand Down Expand Up @@ -87,3 +100,15 @@ new_float_type: dict = ...

def _supported_float_type(input_dtype, allow_complex=False): ...
def identity(image, *args, **kwargs): ...

if sys.version_info >= (3, 11):
class FailedEstimationAccessError(AttributeError): ...

class FailedEstimation:
error_cls: ClassVar[type[FailedEstimationAccessError]]
hint: ClassVar[str]
message: str
def __init__(self, message: str) -> None: ...
def __bool__(self) -> Literal[False]: ...
def __call__(self, *args: object, **kwargs: object) -> Never: ...
def __getattr__(self, name: str) -> Never: ...
8 changes: 7 additions & 1 deletion stubs/skimage/data/_fetchers.pyi
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
from os import PathLike

def file_hash(fname, alg="sha256") -> str: ...
Expand All @@ -12,7 +13,12 @@ image_fetcher = ...
data_dir = ...

def _skip_pytest_case_requiring_pooch(data_filename): ...
def _fetch(data_filename): ...

if sys.version_info >= (3, 11):
def _fetch(data_filename: str, prefix: str | None = None) -> str: ...
else:
def _fetch(data_filename: str) -> str: ...

def _init_pooch(): ...
def download_all(directory: PathLike | None = None): ...
def lbp_frontal_face_cascade_filename(): ...
Expand Down
13 changes: 5 additions & 8 deletions stubs/skimage/data/_registry.pyi
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
# This minimal dataset was available as part of
# scikit-image 0.15 and will be retained until
# further notice.
# Testing data and additional datasets should only
# be made available by pooch
legacy_datasets: list = ...
import sys

if sys.version_info < (3, 11):
legacy_datasets: list[str]
legacy_registry: dict[str, str]

# Registry of datafiles that can be downloaded along with their SHA256 hashes
# To generate the SHA256 hash, use the command
# openssl sha256 filename
registry: dict = ...

registry_urls: dict = ...

legacy_registry: dict = ...
4 changes: 4 additions & 0 deletions stubs/skimage/draw/draw.pyi
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
from typing import Literal

import numpy as np
Expand All @@ -6,6 +7,9 @@ from numpy.typing import NDArray
from .._shared._geometry import polygon_clip
from .._shared.version_requirements import require

if sys.version_info >= (3, 11):
__doctest_requires__: dict[tuple[str, ...], list[str]]

def _ellipse_in_shape(shape, center, radii, rotation=0.0): ...
def ellipse(r, c, r_radius, c_radius, shape: tuple | None = None, rotation=0.0) -> NDArray: ...
def disk(center: tuple, radius, *, shape: tuple | None = None) -> NDArray: ...
Expand Down
37 changes: 27 additions & 10 deletions stubs/skimage/feature/_basic_features.pyi
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
import sys
from collections.abc import Iterator

from numpy.typing import NDArray

from .._shared import utils

def _texture_filter(gaussian_filtered): ...
def _singlescale_basic_features_singlechannel(img, sigma, intensity=True, edges=True, texture=True): ...
def _mutiscale_basic_features_singlechannel(
img,
intensity=True,
edges=True,
texture=True,
sigma_min=0.5,
sigma_max=16,
num_sigma=None,
num_workers=None,
): ...

if sys.version_info >= (3, 11):
def _mutiscale_basic_features_singlechannel(
img: NDArray,
intensity: bool = True,
edges: bool = True,
texture: bool = True,
sigma_min: float = 0.5,
sigma_max: float = 16,
num_sigma: int | None = None,
workers: int | None = None,
) -> Iterator[NDArray]: ...
else:
def _mutiscale_basic_features_singlechannel(
img: NDArray,
intensity: bool = True,
edges: bool = True,
texture: bool = True,
sigma_min: float = 0.5,
sigma_max: float = 16,
num_sigma: int | None = None,
num_workers: int | None = None,
) -> Iterator[NDArray]: ...

@utils.deprecate_multichannel_kwarg(multichannel_position=1)
def multiscale_basic_features(
image: NDArray,
Expand Down
2 changes: 2 additions & 0 deletions stubs/skimage/feature/_cascade.pyi
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from typing import Any, ClassVar
from typing_extensions import disjoint_base

__reduce_cython__: Any
__setstate_cython__: Any
__test__: dict

@disjoint_base
class Cascade:
__pyx_vtable__: ClassVar[Any] = ...
eps: Any
Expand Down
8 changes: 8 additions & 0 deletions stubs/skimage/measure/__init__.pyi
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import sys

from ._blur_effect import blur_effect as blur_effect
from ._find_contours import find_contours as find_contours
from ._label import label as label
Expand Down Expand Up @@ -28,6 +30,9 @@ from .fit import CircleModel as CircleModel, EllipseModel as EllipseModel, LineM
from .pnpoly import grid_points_in_poly as grid_points_in_poly, points_in_poly as points_in_poly
from .profile import profile_line as profile_line

if sys.version_info >= (3, 11):
from .fit import RansacModelProtocol as RansacModelProtocol

__all__ = [
"find_contours",
"regionprops",
Expand Down Expand Up @@ -59,3 +64,6 @@ __all__ = [
"shannon_entropy",
"blur_effect",
]

if sys.version_info >= (3, 11):
__all__ += ["RansacModelProtocol"]
6 changes: 5 additions & 1 deletion stubs/skimage/measure/_regionprops.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import sys
from collections.abc import Mapping

from numpy.typing import ArrayLike
from numpy.typing import ArrayLike, NDArray

from ._regionprops_utils import euler_number, perimeter, perimeter_crofton

Expand Down Expand Up @@ -80,6 +81,9 @@ class RegionProperties:
def intensity_max(self): ...
@property
def intensity_mean(self): ...
if sys.version_info >= (3, 11):
@property
def intensity_median(self) -> float | NDArray: ...
@property
def intensity_min(self): ...
@property
Expand Down
94 changes: 74 additions & 20 deletions stubs/skimage/measure/fit.pyi
Original file line number Diff line number Diff line change
@@ -1,34 +1,88 @@
import math
from typing import Any
import sys
from typing import Any, Protocol, TypeVar, overload, runtime_checkable
from typing_extensions import Self, TypeVarTuple, Unpack
from warnings import warn

import numpy as np
from numpy.linalg import inv
from numpy.typing import ArrayLike
from numpy.typing import ArrayLike, NDArray
from scipy import optimize, spatial

if sys.version_info >= (3, 11):
from .._shared.utils import FailedEstimation

_Data = TypeVarTuple("_Data")

@runtime_checkable
class RansacModelProtocol(Protocol[Unpack[_Data]]):
@classmethod
def from_estimate(cls, *data: Unpack[_Data]) -> Self | FailedEstimation: ...
def residuals(self, *data: Unpack[_Data]) -> NDArray[np.floating]: ...

_ModelT = TypeVar("_ModelT")

def add_from_estimate(cls: type[_ModelT]) -> type[_ModelT]: ...

def _check_data_dim(data, dim): ...
def _check_data_atleast_2D(data): ...

class BaseModel:
def __init__(self): ...

class LineModelND(BaseModel):
def estimate(self, data) -> bool: ...
def residuals(self, data, params=None): ...
def predict(self, x, axis: int = 0, params=None): ...
def predict_x(self, y: ArrayLike, params=None) -> ArrayLike: ...
def predict_y(self, x: ArrayLike, params=None) -> ArrayLike: ...

class CircleModel(BaseModel):
def estimate(self, data) -> bool: ...
def residuals(self, data): ...
def predict_xy(self, t: ArrayLike, params=None): ...

class EllipseModel(BaseModel):
def estimate(self, data) -> bool: ...
def residuals(self, data): ...
def predict_xy(self, t: ArrayLike, params=None): ...
def __init__(self) -> None: ...

if sys.version_info >= (3, 11):
class _BaseModel:
@classmethod
def from_estimate(cls, data: NDArray) -> Self | FailedEstimation: ...

_ModelBase = _BaseModel
else:
_ModelBase = BaseModel

class LineModelND(_ModelBase):
if sys.version_info >= (3, 11):
origin: NDArray | None
direction: NDArray | None
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, origin: ArrayLike, direction: ArrayLike) -> None: ...
@property
def params(self) -> tuple[NDArray | None, NDArray | None]: ...
def estimate(self, data: NDArray) -> bool: ...
def residuals(self, data: NDArray, params: tuple[ArrayLike, ArrayLike] | None = ...) -> NDArray[np.floating]: ...
def predict(self, x: ArrayLike, axis: int = 0, params: tuple[ArrayLike, ArrayLike] | None = ...) -> NDArray: ...
def predict_x(self, y: ArrayLike, params: tuple[ArrayLike, ArrayLike] | None = ...) -> NDArray: ...
def predict_y(self, x: ArrayLike, params: tuple[ArrayLike, ArrayLike] | None = ...) -> NDArray: ...

class CircleModel(_ModelBase):
if sys.version_info >= (3, 11):
center: NDArray | None
radius: float | None
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, center: ArrayLike, radius: float) -> None: ...
@property
def params(self) -> NDArray: ...
def estimate(self, data: NDArray) -> bool: ...
def residuals(self, data: NDArray) -> NDArray[np.floating]: ...
def predict_xy(self, t: ArrayLike, params: ArrayLike | None = ...) -> NDArray: ...

class EllipseModel(_ModelBase):
if sys.version_info >= (3, 11):
center: NDArray | None
axis_lengths: NDArray | None
theta: float | None
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, center: ArrayLike, axis_lengths: ArrayLike, theta: float) -> None: ...
@property
def params(self) -> NDArray: ...
def estimate(self, data: NDArray) -> bool: ...
def residuals(self, data: NDArray) -> NDArray[np.float64]: ...
def predict_xy(self, t: ArrayLike, params: ArrayLike | None = ...) -> NDArray: ...

def _dynamic_max_trials(n_inliers, n_samples, min_samples, probability): ...
def ransac(
Expand Down
Loading
Loading