Skip to content

✅ test: add tests & CI #18

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 2 commits into
base: main
Choose a base branch
from
Open
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
96 changes: 96 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: CI
permissions: read-all

on:
workflow_dispatch:
pull_request:
push:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
# 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
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v6

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

- name: Run lefthook hooks
run: uv run --frozen lefthook run pre-commit

checks:
name: Check Python ${{ matrix.python-version }} on ${{ matrix.runs-on }}
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@v4

- name: Install uv
uses: astral-sh/setup-uv@v6
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
src docs tests

- name: Upload coverage report
uses: codecov/[email protected]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we should we go major-only (@v5 in this case), or use this patch-level version spec everywhere? Personally I'm +0 on this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defer to Dependabot, with grouped updates and filtering on inclusions in releases?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might become mildy annoying to have to merge many dependabot PR though. But that's just speculation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could enable automerge for dependabot

Also, we should be using full commit hashes (check out https://scientific-python.org/specs/spec-0008/).

FWIW, in array-api-extra I have set up renovate to send a monthly PR to update actions, it works well: data-apis/array-api-extra#293

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM. @lucascolley do you want to commit directly this PR?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I have commit access

with:
token: ${{ secrets.CODECOV_TOKEN }}

check_oldest:
name: Check Oldest Dependencies
runs-on: ${{ matrix.runs-on }}
needs: [format]
strategy:
fail-fast: false
matrix:
python-version: ["3.11"]
runs-on: [ubuntu-latest]

steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install the project
run: uv sync --locked --group test --resolution lowest-direct

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

- name: Upload coverage report
uses: codecov/[email protected]
with:
token: ${{ secrets.CODECOV_TOKEN }}
8 changes: 5 additions & 3 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
"""Pytest configuration file."""

from typing import Final

from sybil import Sybil
from sybil.parsers.doctest import DocTestParser

readme_tester = Sybil(
readme_tester: Final = Sybil(
parsers=[DocTestParser()],
pattern="README.md",
)

python_file_tester = Sybil(
python_file_tester: Final = Sybil(
parsers=[DocTestParser()],
pattern="src/**/*.py",
)

pytest_collect_file = (readme_tester + python_file_tester).pytest()
pytest_collect_file: Final = (readme_tester + python_file_tester).pytest()
2 changes: 1 addition & 1 deletion lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ pre-commit:
run: uv run ruff format {staged_files}
- name: mypy
glob: "*.py"
run: uv run mypy {staged_files}
run: uv run --group mypy mypy {staged_files}
11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
"pytest-github-actions-annotate-failures>=0.3.0",
"sybil>=8.0.0",
]
mypy = [
"mypy>=1.13.0"
Copy link
Collaborator

@jorenham jorenham Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can matter quite a bit

Suggested change
"mypy>=1.13.0"
"mypy>=1.16.0"

]


[tool.hatch]
Expand All @@ -74,6 +77,7 @@ version_tuple = {version_tuple!r}
[tool.mypy]
files = ["src", "tests"]
python_version = "3.10"
mypy_path = "src"

strict = true
disallow_incomplete_defs = true
Expand All @@ -90,6 +94,10 @@ version_tuple = {version_tuple!r}
module = "sybil.*"
ignore_missing_imports = true

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


[tool.pytest.ini_options]
addopts = [
Expand Down Expand Up @@ -130,6 +138,9 @@ version_tuple = {version_tuple!r}
"ISC001", # Conflicts with formatter
]

[tool.ruff.lint.per-file-ignores]
"tests/*.py" = ["ANN201", "D1", "S101"]

[tool.ruff.lint.flake8-import-conventions]
banned-from = ["array_api_typing"]

Expand Down
6 changes: 5 additions & 1 deletion src/array_api_typing/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
"""Static typing support for the array API standard."""

__all__ = ["HasArrayNamespace", "__version__", "__version_tuple__"]
__all__ = (
"HasArrayNamespace",
"__version__",
"__version_tuple__",
)

from ._namespace import HasArrayNamespace
from ._version import version as __version__, version_tuple as __version_tuple__
4 changes: 1 addition & 3 deletions src/array_api_typing/_namespace.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
"""Static typing support for the array API standard."""

__all__ = ["HasArrayNamespace"]
__all__ = ("HasArrayNamespace",)

from types import ModuleType
from typing import Protocol, final
Expand Down
42 changes: 42 additions & 0 deletions tests/test_namespace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from types import SimpleNamespace
from typing import Protocol, runtime_checkable

import array_api_typing as xpt


@runtime_checkable
class CheckableHasArrayNamespace(xpt.HasArrayNamespace, Protocol): # type: ignore[misc]
"""Runtime checkable version of HasArrayNamespace."""


class GoodArray:
"""Example class that implements the HasArrayNamespace protocol."""

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


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


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


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


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


def test_not_has_namespace_instance():
"""Test that an instance of BadArray is not recognized as HasArrayNamespace."""
y = BadArray()
assert not isinstance(y, CheckableHasArrayNamespace)
61 changes: 61 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.