Skip to content

Commit 635ea1b

Browse files
authored
🧱 static integration testing (#35)
* πŸ§‘β€πŸ’» lefthook config fine-tuning * 🎨 remove redundant `bound=object` * 🚚 restructured tests directories * πŸ”§β¬†οΈ mypy config tweaks, reorder dep groups, and bump deps * πŸ’‘ remove redundant `# noqa` * πŸ§ͺ add (failing) numpy integration test for `HasArrayNamespace` * πŸ”§ fix awkward dependency groups * πŸ‘· integration testing matrix for numpy * πŸ™ˆ ignore some irrelevant ruff codes for the static integration tests * πŸ’š fix test path * πŸ’š don't use `--frozen` when installing different a numpy version * ⬆️ might as well bump `setup-uv` * πŸ’š clean up CI debug statements * πŸ› fix `HasArrayNamespace` falsely rejecting `ndarray` instances on numpy 2 * πŸ§™ split numpy 1 and 2 integration tests with magic * βœ‚οΈ don't `cut` more than needed * 🩹 don't attempt to directly use `np.array_api.Array` * πŸ”§ move ruff ignore rules for the tests to the tests * βͺ temporarily restore the lefthook step
1 parent 080a0b3 commit 635ea1b

File tree

11 files changed

+554
-360
lines changed

11 files changed

+554
-360
lines changed

β€Ž.github/workflows/ci.yml

Lines changed: 56 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,83 +12,98 @@ concurrency:
1212
cancel-in-progress: true
1313

1414
env:
15+
UV_LOCKED: 1
1516
# Many color libraries just need this to be set to any value, but at least
1617
# one distinguishes color depth, where "3" -> "256-bit color".
1718
FORCE_COLOR: 3
1819

1920
jobs:
20-
format:
21-
name: Format
21+
lint:
22+
name: lint
2223
runs-on: ubuntu-latest
2324
steps:
24-
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
25+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
2526

2627
- name: Install uv
27-
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba
28+
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
2829

29-
- name: Install the project
30-
run: uv sync --locked --group test
30+
- name: ruff
31+
run: |
32+
uv run ruff check --output-format=github
33+
uv run ruff format --check
3134
32-
- name: Run lefthook hooks
33-
run: uv run --frozen lefthook run pre-commit
35+
# TODO: Fail if lefthook changes any files:
36+
# https://github.com/data-apis/array-api-typing/pull/35/files#r2179941334
37+
- name: lefthook
38+
run: uv run lefthook run pre-commit
3439

35-
checks:
36-
name: Check Python ${{ matrix.python-version }} on ${{ matrix.runs-on }}
40+
- name: mypy
41+
run: uv run mypy --tb --no-incremental --cache-dir=/dev/null src
42+
43+
# TODO: (based)pyright
44+
45+
test_runtime:
46+
name: runtime tests
3747
runs-on: ${{ matrix.runs-on }}
38-
needs: [format]
3948
strategy:
4049
fail-fast: false
4150
matrix:
4251
python-version: ["3.11", "3.12", "3.13"]
4352
runs-on: [ubuntu-latest, macos-latest, windows-latest]
4453

4554
steps:
46-
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
55+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
4756

4857
- name: Install uv
49-
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba
58+
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
5059
with:
5160
python-version: ${{ matrix.python-version }}
5261

53-
- name: Install the project
54-
run: uv sync --locked --group test
55-
5662
- name: Test package
5763
run: >-
58-
uv run --frozen pytest
59-
--cov --cov-report=xml --cov-report=term --durations=20
64+
uv run --group=test_runtime
65+
pytest --cov --cov-report=xml --cov-report=term --durations=20
6066
6167
- name: Upload coverage report
62-
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24
68+
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
6369
with:
6470
token: ${{ secrets.CODECOV_TOKEN }}
6571

66-
check_oldest:
67-
name: Check Oldest Dependencies
68-
runs-on: ${{ matrix.runs-on }}
69-
needs: [format]
72+
test_integration_numpy:
73+
name: integration tests (numpy)
74+
runs-on: ubuntu-latest
7075
strategy:
7176
fail-fast: false
7277
matrix:
73-
python-version: ["3.11"]
74-
runs-on: [ubuntu-latest]
78+
numpy-version: ["1.25.0", "1.26.4", "2.0.2", "2.1.3", "2.2.6", "2.3.1"]
7579

7680
steps:
77-
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
78-
79-
- name: Install uv
80-
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba
81-
with:
82-
python-version: ${{ matrix.python-version }}
83-
- name: Install the project
84-
run: uv sync --group test --resolution lowest-direct
81+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
8582

86-
- name: Test package
87-
run: >-
88-
uv run --frozen pytest
89-
--cov --cov-report=xml --cov-report=term --durations=20
90-
91-
- name: Upload coverage report
92-
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24
83+
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
9384
with:
94-
token: ${{ secrets.CODECOV_TOKEN }}
85+
python-version: "3.11"
86+
activate-environment: true
87+
88+
- name: get major numpy version
89+
id: numpy-major
90+
run: |
91+
version=$(echo ${{ matrix.numpy-version }} | cut -c 1)
92+
echo "::set-output name=version::$version"
93+
94+
- name: install deps
95+
run: |
96+
uv sync --no-editable --group=mypy
97+
uv pip install numpy==${{ matrix.numpy-version }}
98+
99+
# NOTE: `uv run --with=...` will be ignored by mypy (and `--isolated` does not help)
100+
- name: mypy
101+
run: >
102+
uv run --no-sync --active
103+
mypy --tb --no-incremental --cache-dir=/dev/null
104+
tests/integration/test_numpy${{ steps.numpy-major.outputs.version }}.pyi
105+
106+
# TODO: (based)pyright
107+
108+
# TODO: integration tests for array-api-strict
109+
# TODO: integration tests for 3rd party libs such as cupy, pytorch, tensorflow, dask, etc.

β€Žlefthook.yml

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,33 @@
11
# Refer for explanation to following link:
22
# https://lefthook.dev/configuration/
3-
#
3+
4+
templates:
5+
run: run --no-sync
46

57
pre-commit:
68
parallel: true
79
jobs:
8-
- name: ruff-fix
9-
glob: "*.py"
10-
run: uv run ruff check --fix {staged_files}
11-
- name: ruff-format
12-
glob: "*.py"
13-
run: uv run ruff format {staged_files}
10+
- name: ruff
11+
glob: "*.{py,pyi}"
12+
stage_fixed: true
13+
group:
14+
piped: true
15+
jobs:
16+
- name: check
17+
run: uv {run} ruff check --fix {staged_files}
18+
- name: format
19+
run: uv {run} ruff format {staged_files}
1420
- name: mypy
15-
glob: "*.py"
16-
run: uv run --group mypy mypy {staged_files}
21+
glob: "*.{py,pyi}"
22+
run: uv {run} mypy {staged_files}
23+
24+
post-checkout:
25+
jobs:
26+
- run: uv sync
27+
glob: uv.lock
28+
29+
post-merge:
30+
files: "git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD"
31+
jobs:
32+
- run: uv sync
33+
glob: uv.lock

β€Žpyproject.toml

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,27 @@
4141

4242
[dependency-groups]
4343
dev = [
44-
{ include-group = "test" },
45-
"lefthook>=1.11.13",
44+
{ include-group = "lint" },
45+
{ include-group = "mypy" },
46+
{ include-group = "test_runtime" },
47+
{ include-group = "test_numpy" },
48+
"lefthook==1.11.14",
49+
"orjson>=3.10.18; python_version<'3.14'", # used by mypy
4650
]
47-
test = [
48-
"numpy>=1.25",
49-
"pytest==8.3.3",
50-
"pytest-cov>=6",
51-
"pytest-github-actions-annotate-failures>=0.3.0",
52-
"sybil>=8.0.0",
51+
lint = [
52+
"ruff==0.12.1",
5353
]
5454
mypy = [
55-
"mypy>=1.16.0"
55+
"mypy==1.16.1",
56+
]
57+
test_runtime = [
58+
"pytest==8.4.1",
59+
"pytest-cov>=6.2.1",
60+
"pytest-github-actions-annotate-failures==0.3.0",
61+
"sybil==9.1.0",
62+
]
63+
test_numpy = [
64+
"numpy>=1.25",
5665
]
5766

5867

@@ -75,28 +84,14 @@ version_tuple = {version_tuple!r}
7584

7685

7786
[tool.mypy]
78-
files = ["src", "tests"]
79-
python_version = "3.10"
80-
mypy_path = "src"
87+
mypy_path = ["src"]
88+
namespace_packages = true
8189

8290
strict = true
83-
disallow_incomplete_defs = true
84-
disallow_untyped_defs = true
85-
disable_bytearray_promotion = true # Note(2024-12-05): these are private flags
86-
disable_memoryview_promotion = true # Note(2024-12-05): these are private flags
91+
allow_redefinition_new = true
92+
local_partial_types = true
8793
enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
88-
89-
warn_return_any = true
9094
warn_unreachable = true
91-
warn_unused_configs = true
92-
93-
[[tool.mypy.overrides]]
94-
module = "sybil.*"
95-
ignore_missing_imports = true
96-
97-
[[tool.mypy.overrides]]
98-
module = "tests.*"
99-
disallow_untyped_defs = false
10095

10196

10297
[tool.pytest.ini_options]
@@ -114,9 +109,8 @@ version_tuple = {version_tuple!r}
114109
"ignore:ast\\.Str is deprecated and will be removed in Python 3\\.14:DeprecationWarning",
115110
]
116111
log_cli_level = "INFO"
117-
minversion = "8.3"
118-
testpaths = ["README.md", "src/", "tests/"]
119-
norecursedirs = ["docs/_build"]
112+
minversion = "8.4"
113+
testpaths = ["README.md", "src/", "tests/runtime/"]
120114
xfail_strict = true
121115

122116

@@ -138,8 +132,15 @@ version_tuple = {version_tuple!r}
138132
"ISC001", # Conflicts with formatter
139133
]
140134

141-
[tool.ruff.lint.per-file-ignores]
142-
"tests/*.py" = ["ANN201", "D1", "S101"]
135+
[tool.ruff.lint.pylint]
136+
allow-dunder-method-names = [
137+
"__array_api_version__",
138+
"__array_namespace__",
139+
"__array_namespace_info__",
140+
"__dlpack__",
141+
"__dlpack_device__",
142+
"__dlpack_device__",
143+
]
143144

144145
[tool.ruff.lint.flake8-import-conventions]
145146
banned-from = ["array_api_typing"]
@@ -149,7 +150,7 @@ version_tuple = {version_tuple!r}
149150

150151
[tool.ruff.lint.isort]
151152
combine-as-imports = true
152-
extra-standard-library = ["typing_extensions"]
153+
extra-standard-library = ["_typeshed", "typing_extensions"]
153154
known-local-folder = ["array_api_typing"]
154155

155156
[tool.ruff.format]

β€Žsrc/array_api_typing/_namespace.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
__all__ = ("HasArrayNamespace",)
22

33
from types import ModuleType
4-
from typing import Protocol
4+
from typing import Literal, Protocol
55
from typing_extensions import TypeVar
66

7-
T_co = TypeVar("T_co", covariant=True, bound=object, default=ModuleType)
7+
T_co = TypeVar("T_co", covariant=True, default=ModuleType)
88

99

1010
class HasArrayNamespace(Protocol[T_co]):
@@ -25,4 +25,6 @@ class HasArrayNamespace(Protocol[T_co]):
2525
2626
"""
2727

28-
def __array_namespace__(self, /, *, api_version: str | None = None) -> T_co: ... # noqa: PLW3201
28+
def __array_namespace__(
29+
self, /, *, api_version: Literal["2021.12"] | None = None
30+
) -> T_co: ...

β€Žtests/.ruff.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
extend = "../pyproject.toml"
2+
3+
[lint]
4+
extend-ignore = [
5+
"ANN201", # https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/
6+
"D1", # https://docs.astral.sh/ruff/rules/#pydocstyle-d
7+
"INP001", # https://docs.astral.sh/ruff/rules/implicit-namespace-package/
8+
"S101", # https://docs.astral.sh/ruff/rules/assert/
9+
]

β€Žtests/__init__.py

Whitespace-only changes.

β€Žtests/integration/.ruff.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
extend = "../.ruff.toml"
2+
3+
[lint]
4+
extend-ignore = ["B018", "PYI015", "PYI017"]

β€Žtests/integration/test_numpy1.pyi

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from typing import Any
2+
3+
# requires numpy < 2
4+
import numpy.array_api as np
5+
6+
import array_api_typing as xpt
7+
8+
###
9+
# Ensure that `np.ndarray` instances are assignable to `xpt.HasArrayNamespace`.
10+
11+
arr = np.eye(2)
12+
arr_namespace: xpt.HasArrayNamespace[Any] = arr

β€Žtests/integration/test_numpy2.pyi

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from typing import Any
2+
3+
import numpy.typing as npt
4+
5+
import array_api_typing as xpt
6+
7+
###
8+
# Ensure that `np.ndarray` instances are assignable to `xpt.HasArrayNamespace`.
9+
10+
arr: npt.NDArray[Any]
11+
arr_namespace: xpt.HasArrayNamespace[Any] = arr

β€Žtests/test_namespace.py renamed to β€Žtests/runtime/test_namespace.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,31 @@ class CheckableHasArrayNamespace(xpt.HasArrayNamespace, Protocol):
1515
class GoodArray:
1616
"""Example class that implements the HasArrayNamespace protocol."""
1717

18-
def __array_namespace__(self) -> object: # noqa: PLW3201
18+
def __array_namespace__(self) -> object:
1919
return SimpleNamespace()
2020

2121

2222
class BadArray:
2323
"""Example class that does not implement the HasArrayNamespace protocol."""
2424

2525

26-
def test_has_namespace_class():
26+
def test_has_namespace_class() -> None:
2727
"""Test that GoodArray is a subclass of HasArrayNamespace."""
2828
assert issubclass(GoodArray, CheckableHasArrayNamespace)
2929

3030

31-
def test_has_namespace_instance():
31+
def test_has_namespace_instance() -> None:
3232
"""Test that an instance of GoodArray is recognized as HasArrayNamespace."""
3333
x = GoodArray()
3434
assert isinstance(x, CheckableHasArrayNamespace)
3535

3636

37-
def test_not_has_namespace_class():
37+
def test_not_has_namespace_class() -> None:
3838
"""Test that BadArray is not a subclass of HasArrayNamespace."""
3939
assert not issubclass(BadArray, CheckableHasArrayNamespace)
4040

4141

42-
def test_not_has_namespace_instance():
42+
def test_not_has_namespace_instance() -> None:
4343
"""Test that an instance of BadArray is not recognized as HasArrayNamespace."""
4444
y = BadArray()
4545
assert not isinstance(y, CheckableHasArrayNamespace)

0 commit comments

Comments
Β (0)