diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a9cbe83 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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@11bd71901bbe5b1630ceea73d27597364c9af683 + + - name: Install uv + uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb + + - 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@11bd71901bbe5b1630ceea73d27597364c9af683 + + - name: Install uv + uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb + 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/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 + 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@11bd71901bbe5b1630ceea73d27597364c9af683 + + - name: Install uv + uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb + 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/codecov-action@v18283e04ce6e62d37312384ff67231eb8fd56d24 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/conftest.py b/conftest.py index 5bac2de..924f079 100644 --- a/conftest.py +++ b/conftest.py @@ -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() diff --git a/lefthook.yml b/lefthook.yml index 85b0257..32c2905 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -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} diff --git a/pyproject.toml b/pyproject.toml index 507f7ed..fc6f9c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,9 @@ "pytest-github-actions-annotate-failures>=0.3.0", "sybil>=8.0.0", ] + mypy = [ + "mypy>=1.16.0" + ] [tool.hatch] @@ -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 @@ -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 = [ @@ -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"] diff --git a/src/array_api_typing/__init__.py b/src/array_api_typing/__init__.py index 7c84b8b..3532743 100644 --- a/src/array_api_typing/__init__.py +++ b/src/array_api_typing/__init__.py @@ -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__ diff --git a/src/array_api_typing/_namespace.py b/src/array_api_typing/_namespace.py index 1e885d5..98099d1 100644 --- a/src/array_api_typing/_namespace.py +++ b/src/array_api_typing/_namespace.py @@ -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 diff --git a/tests/test_namespace.py b/tests/test_namespace.py new file mode 100644 index 0000000..0a35086 --- /dev/null +++ b/tests/test_namespace.py @@ -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) diff --git a/uv.lock b/uv.lock index a306191..1ffbff5 100644 --- a/uv.lock +++ b/uv.lock @@ -22,6 +22,9 @@ dev = [ { name = "pytest-github-actions-annotate-failures" }, { name = "sybil" }, ] +mypy = [ + { name = "mypy" }, +] test = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -43,6 +46,7 @@ dev = [ { name = "pytest-github-actions-annotate-failures", specifier = ">=0.3.0" }, { name = "sybil", specifier = ">=8.0.0" }, ] +mypy = [{ name = "mypy", specifier = ">=1.16.0" }] test = [ { name = "numpy", specifier = ">=1.25" }, { name = "pytest", specifier = ">=8.3.3" }, @@ -151,6 +155,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/0f/4ac809a1a005ba393ab5fa05a0731f4b5614e47d5ec2023421df97e172db/lefthook-1.11.13-py3-none-any.whl", hash = "sha256:b868faee65329949d607189a6db5c431c61f6d7bccb477e31a9544f929d1896c", size = 49719067 }, ] +[[package]] +name = "mypy" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/38/13c2f1abae94d5ea0354e146b95a1be9b2137a0d506728e0da037c4276f6/mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab", size = 3323139 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/5e/a0485f0608a3d67029d3d73cec209278b025e3493a3acfda3ef3a88540fd/mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c", size = 10967416 }, + { url = "https://files.pythonhosted.org/packages/4b/53/5837c221f74c0d53a4bfc3003296f8179c3a2a7f336d7de7bbafbe96b688/mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571", size = 10087654 }, + { url = "https://files.pythonhosted.org/packages/29/59/5fd2400352c3093bed4c09017fe671d26bc5bb7e6ef2d4bf85f2a2488104/mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491", size = 11875192 }, + { url = "https://files.pythonhosted.org/packages/ad/3e/4bfec74663a64c2012f3e278dbc29ffe82b121bc551758590d1b6449ec0c/mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777", size = 12612939 }, + { url = "https://files.pythonhosted.org/packages/88/1f/fecbe3dcba4bf2ca34c26ca016383a9676711907f8db4da8354925cbb08f/mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b", size = 12874719 }, + { url = "https://files.pythonhosted.org/packages/f3/51/c2d280601cd816c43dfa512a759270d5a5ef638d7ac9bea9134c8305a12f/mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93", size = 9487053 }, + { url = "https://files.pythonhosted.org/packages/24/c4/ff2f79db7075c274fe85b5fff8797d29c6b61b8854c39e3b7feb556aa377/mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab", size = 10884498 }, + { url = "https://files.pythonhosted.org/packages/02/07/12198e83006235f10f6a7808917376b5d6240a2fd5dce740fe5d2ebf3247/mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2", size = 10011755 }, + { url = "https://files.pythonhosted.org/packages/f1/9b/5fd5801a72b5d6fb6ec0105ea1d0e01ab2d4971893076e558d4b6d6b5f80/mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff", size = 11800138 }, + { url = "https://files.pythonhosted.org/packages/2e/81/a117441ea5dfc3746431e51d78a4aca569c677aa225bca2cc05a7c239b61/mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666", size = 12533156 }, + { url = "https://files.pythonhosted.org/packages/3f/38/88ec57c6c86014d3f06251e00f397b5a7daa6888884d0abf187e4f5f587f/mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c", size = 12742426 }, + { url = "https://files.pythonhosted.org/packages/bd/53/7e9d528433d56e6f6f77ccf24af6ce570986c2d98a5839e4c2009ef47283/mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b", size = 9478319 }, + { url = "https://files.pythonhosted.org/packages/70/cf/158e5055e60ca2be23aec54a3010f89dcffd788732634b344fc9cb1e85a0/mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13", size = 11062927 }, + { url = "https://files.pythonhosted.org/packages/94/34/cfff7a56be1609f5d10ef386342ce3494158e4d506516890142007e6472c/mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090", size = 10083082 }, + { url = "https://files.pythonhosted.org/packages/b3/7f/7242062ec6288c33d8ad89574df87c3903d394870e5e6ba1699317a65075/mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1", size = 11828306 }, + { url = "https://files.pythonhosted.org/packages/6f/5f/b392f7b4f659f5b619ce5994c5c43caab3d80df2296ae54fa888b3d17f5a/mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8", size = 12702764 }, + { url = "https://files.pythonhosted.org/packages/9b/c0/7646ef3a00fa39ac9bc0938626d9ff29d19d733011be929cfea59d82d136/mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730", size = 12896233 }, + { url = "https://files.pythonhosted.org/packages/6d/38/52f4b808b3fef7f0ef840ee8ff6ce5b5d77381e65425758d515cdd4f5bb5/mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec", size = 9565547 }, + { url = "https://files.pythonhosted.org/packages/97/9c/ca03bdbefbaa03b264b9318a98950a9c683e06472226b55472f96ebbc53d/mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b", size = 11059753 }, + { url = "https://files.pythonhosted.org/packages/36/92/79a969b8302cfe316027c88f7dc6fee70129490a370b3f6eb11d777749d0/mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0", size = 10073338 }, + { url = "https://files.pythonhosted.org/packages/14/9b/a943f09319167da0552d5cd722104096a9c99270719b1afeea60d11610aa/mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b", size = 11827764 }, + { url = "https://files.pythonhosted.org/packages/ec/64/ff75e71c65a0cb6ee737287c7913ea155845a556c64144c65b811afdb9c7/mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d", size = 12701356 }, + { url = "https://files.pythonhosted.org/packages/0a/ad/0e93c18987a1182c350f7a5fab70550852f9fabe30ecb63bfbe51b602074/mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52", size = 12900745 }, + { url = "https://files.pythonhosted.org/packages/28/5d/036c278d7a013e97e33f08c047fe5583ab4f1fc47c9a49f985f1cdd2a2d7/mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb", size = 9572200 }, + { url = "https://files.pythonhosted.org/packages/99/a3/6ed10530dec8e0fdc890d81361260c9ef1f5e5c217ad8c9b21ecb2b8366b/mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031", size = 2265773 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, +] + [[package]] name = "numpy" version = "2.2.6" @@ -286,6 +338,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + [[package]] name = "pluggy" version = "1.5.0"