Skip to content

🧱 static integration testing #35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
cf1306b
🧑‍💻 lefthook config fine-tuning
jorenham Jul 2, 2025
3b23312
🎨 remove redundant `bound=object`
jorenham Jul 2, 2025
5f5995b
🚚 restructured tests directories
jorenham Jul 2, 2025
7434579
🔧⬆️ mypy config tweaks, reorder dep groups, and bump deps
jorenham Jul 2, 2025
a32517f
💡 remove redundant `# noqa`
jorenham Jul 2, 2025
d00681e
🧪 add (failing) numpy integration test for `HasArrayNamespace`
jorenham Jul 2, 2025
a15686c
🔧 fix awkward dependency groups
jorenham Jul 2, 2025
a7fe54d
👷 integration testing matrix for numpy
jorenham Jul 2, 2025
2b30311
🙈 ignore some irrelevant ruff codes for the static integration tests
jorenham Jul 2, 2025
a6bb9b7
💚 fix test path
jorenham Jul 2, 2025
d59a1c5
💚 don't use `--frozen` when installing different a numpy version
jorenham Jul 2, 2025
9a73605
🚧 debug-print the installed numpy version
jorenham Jul 2, 2025
cc91649
🚧 `--refresh` , maybe?
jorenham Jul 2, 2025
26d85ba
🚧 `--reinstall`, maybe?
jorenham Jul 2, 2025
b9f77b5
⬆️ might as well bump `setup-uv` then...
jorenham Jul 2, 2025
d509f4f
🚧 `--isolated`, maybe?
jorenham Jul 2, 2025
55ac805
🚧 `enable-cache: false`, maybe?
jorenham Jul 2, 2025
110a869
🚧 `--exact`, maybe?
jorenham Jul 2, 2025
7d2e59b
🚧 no mypy flags, maybe?
jorenham Jul 2, 2025
de3924e
🚧 `uv pip`, maybe?
jorenham Jul 2, 2025
4db60c2
💚 clean up CI debug statements
jorenham Jul 2, 2025
57eb000
🐛 fix `HasArrayNamespace` falsely rejecting `ndarray` instances on nu…
jorenham Jul 2, 2025
9e1fc18
👷 remove numpy<2 from the integration testing matrix
jorenham Jul 2, 2025
a4a75f9
🧙 split numpy 1 and 2 integration tests with black voodoo magic
jorenham Jul 2, 2025
95ec86d
✂️ don't `cut` more than needed
jorenham Jul 2, 2025
9c89224
🩹 don't attempt to directly use `np.array_api.Array`
jorenham Jul 2, 2025
b0673fa
🔧 move ruff ignore rules for the tests to the tests
jorenham Jul 2, 2025
5644f37
⏪ temporarily restore the lefthook step
jorenham Jul 3, 2025
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
97 changes: 56 additions & 41 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,83 +12,98 @@ concurrency:
cancel-in-progress: true

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

jobs:
format:
name: Format
lint:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1

- name: Install the project
run: uv sync --locked --group test
- name: ruff
run: |
uv run ruff check --output-format=github
uv run ruff format --check

- name: Run lefthook hooks
run: uv run --frozen lefthook run pre-commit
# TODO: Fail if lefthook changes any files:
# https://github.com/data-apis/array-api-typing/pull/35/files#r2179941334
- name: lefthook
run: uv run lefthook run pre-commit

checks:
name: Check Python ${{ matrix.python-version }} on ${{ matrix.runs-on }}
- name: mypy
run: uv run mypy --tb --no-incremental --cache-dir=/dev/null src

# TODO: (based)pyright

test_runtime:
name: runtime tests
runs-on: ${{ matrix.runs-on }}
needs: [format]
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13"]
runs-on: [ubuntu-latest, macos-latest, windows-latest]

steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
with:
python-version: ${{ matrix.python-version }}

- name: Install the project
run: uv sync --locked --group test

- name: Test package
run: >-
uv run --frozen pytest
--cov --cov-report=xml --cov-report=term --durations=20
uv run --group=test_runtime
pytest --cov --cov-report=xml --cov-report=term --durations=20

- name: Upload coverage report
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
with:
token: ${{ secrets.CODECOV_TOKEN }}

check_oldest:
name: Check Oldest Dependencies
runs-on: ${{ matrix.runs-on }}
needs: [format]
test_integration_numpy:
name: integration tests (numpy)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11"]
runs-on: [ubuntu-latest]
numpy-version: ["1.25.0", "1.26.4", "2.0.2", "2.1.3", "2.2.6", "2.3.1"]

steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

- name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba
with:
python-version: ${{ matrix.python-version }}
- name: Install the project
run: uv sync --group test --resolution lowest-direct
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Test package
run: >-
uv run --frozen pytest
--cov --cov-report=xml --cov-report=term --durations=20

- name: Upload coverage report
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
python-version: "3.11"
activate-environment: true

- name: get major numpy version
id: numpy-major
run: |
version=$(echo ${{ matrix.numpy-version }} | cut -c 1)
echo "::set-output name=version::$version"

- name: install deps
run: |
uv sync --no-editable --group=mypy
uv pip install numpy==${{ matrix.numpy-version }}

# NOTE: `uv run --with=...` will be ignored by mypy (and `--isolated` does not help)
- name: mypy
run: >
uv run --no-sync --active
mypy --tb --no-incremental --cache-dir=/dev/null
tests/integration/test_numpy${{ steps.numpy-major.outputs.version }}.pyi

# TODO: (based)pyright

# TODO: integration tests for array-api-strict
# TODO: integration tests for 3rd party libs such as cupy, pytorch, tensorflow, dask, etc.
35 changes: 26 additions & 9 deletions lefthook.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
# Refer for explanation to following link:
# https://lefthook.dev/configuration/
#

templates:
run: run --no-sync

pre-commit:
parallel: true
jobs:
- name: ruff-fix
glob: "*.py"
run: uv run ruff check --fix {staged_files}
- name: ruff-format
glob: "*.py"
run: uv run ruff format {staged_files}
- name: ruff
glob: "*.{py,pyi}"
stage_fixed: true
group:
piped: true
jobs:
- name: check
run: uv {run} ruff check --fix {staged_files}
- name: format
run: uv {run} ruff format {staged_files}
- name: mypy
glob: "*.py"
run: uv run --group mypy mypy {staged_files}
glob: "*.{py,pyi}"
run: uv {run} mypy {staged_files}

post-checkout:
jobs:
- run: uv sync
glob: uv.lock

post-merge:
files: "git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD"
jobs:
- run: uv sync
glob: uv.lock
67 changes: 34 additions & 33 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,27 @@

[dependency-groups]
dev = [
{ include-group = "test" },
"lefthook>=1.11.13",
{ include-group = "lint" },
{ include-group = "mypy" },
{ include-group = "test_runtime" },
{ include-group = "test_numpy" },
"lefthook==1.11.14",
"orjson>=3.10.18; python_version<'3.14'", # used by mypy
]
test = [
"numpy>=1.25",
"pytest==8.3.3",
"pytest-cov>=6",
"pytest-github-actions-annotate-failures>=0.3.0",
"sybil>=8.0.0",
lint = [
"ruff==0.12.1",
]
mypy = [
"mypy>=1.16.0"
"mypy==1.16.1",
]
test_runtime = [
"pytest==8.4.1",
"pytest-cov>=6.2.1",
"pytest-github-actions-annotate-failures==0.3.0",
"sybil==9.1.0",
]
test_numpy = [
"numpy>=1.25",
]


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


[tool.mypy]
files = ["src", "tests"]
python_version = "3.10"
mypy_path = "src"
mypy_path = ["src"]
namespace_packages = true

strict = true
disallow_incomplete_defs = true
disallow_untyped_defs = true
disable_bytearray_promotion = true # Note(2024-12-05): these are private flags
disable_memoryview_promotion = true # Note(2024-12-05): these are private flags
allow_redefinition_new = true
local_partial_types = true
enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]

warn_return_any = true
warn_unreachable = true
warn_unused_configs = true

[[tool.mypy.overrides]]
module = "sybil.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false


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


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

[tool.ruff.lint.per-file-ignores]
"tests/*.py" = ["ANN201", "D1", "S101"]
[tool.ruff.lint.pylint]
allow-dunder-method-names = [
"__array_api_version__",
"__array_namespace__",
"__array_namespace_info__",
"__dlpack__",
"__dlpack_device__",
"__dlpack_device__",
]

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

[tool.ruff.lint.isort]
combine-as-imports = true
extra-standard-library = ["typing_extensions"]
extra-standard-library = ["_typeshed", "typing_extensions"]
known-local-folder = ["array_api_typing"]

[tool.ruff.format]
Expand Down
8 changes: 5 additions & 3 deletions src/array_api_typing/_namespace.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
__all__ = ("HasArrayNamespace",)

from types import ModuleType
from typing import Protocol
from typing import Literal, Protocol
from typing_extensions import TypeVar

T_co = TypeVar("T_co", covariant=True, bound=object, default=ModuleType)
T_co = TypeVar("T_co", covariant=True, default=ModuleType)


class HasArrayNamespace(Protocol[T_co]):
Expand All @@ -25,4 +25,6 @@ class HasArrayNamespace(Protocol[T_co]):

"""

def __array_namespace__(self, /, *, api_version: str | None = None) -> T_co: ... # noqa: PLW3201
def __array_namespace__(
self, /, *, api_version: Literal["2021.12"] | None = None
) -> T_co: ...
9 changes: 9 additions & 0 deletions tests/.ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
extend = "../pyproject.toml"

[lint]
extend-ignore = [
"ANN201", # https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/
"D1", # https://docs.astral.sh/ruff/rules/#pydocstyle-d
"INP001", # https://docs.astral.sh/ruff/rules/implicit-namespace-package/
"S101", # https://docs.astral.sh/ruff/rules/assert/
]
Empty file removed tests/__init__.py
Empty file.
4 changes: 4 additions & 0 deletions tests/integration/.ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
extend = "../.ruff.toml"

[lint]
extend-ignore = ["B018", "PYI015", "PYI017"]
12 changes: 12 additions & 0 deletions tests/integration/test_numpy1.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from typing import Any

# requires numpy < 2
import numpy.array_api as np

import array_api_typing as xpt

###
# Ensure that `np.ndarray` instances are assignable to `xpt.HasArrayNamespace`.

arr = np.eye(2)
arr_namespace: xpt.HasArrayNamespace[Any] = arr
11 changes: 11 additions & 0 deletions tests/integration/test_numpy2.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from typing import Any

import numpy.typing as npt

import array_api_typing as xpt

###
# Ensure that `np.ndarray` instances are assignable to `xpt.HasArrayNamespace`.

arr: npt.NDArray[Any]
arr_namespace: xpt.HasArrayNamespace[Any] = arr
10 changes: 5 additions & 5 deletions tests/test_namespace.py → tests/runtime/test_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,31 @@ class CheckableHasArrayNamespace(xpt.HasArrayNamespace, Protocol):
class GoodArray:
"""Example class that implements the HasArrayNamespace protocol."""

def __array_namespace__(self) -> object: # noqa: PLW3201
def __array_namespace__(self) -> object:
return SimpleNamespace()


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


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


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


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


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