Skip to content

✨: add CanArrayX protocols #32

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 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
135f8ad
✨: move HasArrayNamespace to _array.py and update imports
nstarman Jun 22, 2025
003b4eb
✨: add Array class definition
nstarman Jun 22, 2025
382373c
refactor: rename type variable in HasArrayNamespace protocol for cons…
nstarman Jun 22, 2025
f78d155
✨: add CanArrayPos protocol
nstarman Jun 22, 2025
3ee1591
✨: add CanArrayNeg protocol
nstarman Jun 22, 2025
ebd7bf2
✨: add CanArrayAdd protocol
nstarman Jun 22, 2025
e9d6d23
✨: add CanArraySub Protocol
nstarman Jun 22, 2025
953458a
✨: add HasArrayMul protocol
nstarman Jun 22, 2025
ea598bc
✨: add CanArrayTrueDiv protocol
nstarman Jun 22, 2025
5b873c7
✨: add CanArrayFloorDiv protocol
nstarman Jun 22, 2025
66c50b8
✨: add CanArrayMod protocol
nstarman Jun 22, 2025
5ce8592
✨: add CanArrayPow protocol
nstarman Jun 22, 2025
25d0170
✨: add CanArrayIAdd protocol
nstarman Jun 23, 2025
bee031f
✨: add CanArrayISub protocol
nstarman Jun 23, 2025
2696f0a
✨: add CanArrayIMul protocol
nstarman Jun 23, 2025
3e39bf4
✨: add CanArrayITruediv protocol
nstarman Jun 23, 2025
f112f54
✨: add CanArrayIFloorDiv protocol
nstarman Jun 23, 2025
d1378b3
✨: add CanArrayIPow protocol
nstarman Jun 23, 2025
5adb634
✨: add CanArrayIMod protocol
nstarman Jun 23, 2025
baab474
✨: add CanArrayRAdd protocol
nstarman Jun 23, 2025
2cf632e
✨: add CanArrayRSub protocol
nstarman Jun 23, 2025
3652bab
✨: add CanArrayRMul protocol
nstarman Jun 23, 2025
cd5ba89
✨: add CanArrayRTruediv protocol
nstarman Jun 23, 2025
5ca860c
✨: add CanArrayRFloorDiv protocol
nstarman Jun 23, 2025
11775ff
✨: add CanArrayRPow protocol
nstarman Jun 23, 2025
b8b9570
✨: add CanArrayRMod protocol
nstarman Jun 23, 2025
5d50e54
➕ `optype`!
jorenham Jul 1, 2025
6425dec
📌 pin the correct python version
jorenham Jul 1, 2025
dac9bb4
🔧 `ruff` <3 `optype`
jorenham Jul 1, 2025
14dc47a
🚧 transition to optype
nstarman Jul 11, 2025
d2a0e69
✨: add tomli dependency for Python version compatibility and load doc…
nstarman Jul 16, 2025
e4fd64a
🔧 update optype version
nstarman Jul 16, 2025
876b4a5
✨: refactor test files to improve clarity and organization of NDArray…
nstarman Jul 16, 2025
083ccf1
✨: remove redundant in-place operations
nstarman Jul 18, 2025
ba3ee4e
WIP
nstarman Jul 18, 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
11 changes: 10 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
]
dependencies = [
"typing-extensions>=4.14.1",
"optype>=0.9.3; python_version < '3.11'",
"optype>=0.12.0; python_version >= '3.11'",
"tomli>=1.2.0 ; python_full_version < '3.11'",
]

[project.urls]
Expand Down Expand Up @@ -127,9 +130,12 @@ version_tuple = {version_tuple!r}
"D107", # Missing docstring in __init__
"D203", # 1 blank line required before class docstring
"D213", # Multi-line docstring summary should start at the second line
"D401", # First line of docstring should be in imperative mood
"FBT", # flake8-boolean-trap
"FIX", # flake8-fixme
"ISC001", # Conflicts with formatter
"PLW1641", # Object does not implement `__hash__` method
"PYI041", # Use `float` instead of `int | float`
]

[tool.ruff.lint.pylint]
Expand All @@ -143,10 +149,13 @@ version_tuple = {version_tuple!r}
]

[tool.ruff.lint.flake8-import-conventions]
banned-from = ["array_api_typing"]
banned-from = ["array_api_typing", "optype", "optype.numpy", "optype.numpy.compat"]

[tool.ruff.lint.flake8-import-conventions.extend-aliases]
array_api_typing = "xpt"
optype = "op"
"optype.numpy" = "onp"
"optype.numpy.compat" = "npc"

[tool.ruff.lint.isort]
combine-as-imports = true
Expand Down
3 changes: 2 additions & 1 deletion src/array_api_typing/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Static typing support for the array API standard."""

__all__ = (
"Array",
"HasArrayNamespace",
"__version__",
"__version_tuple__",
)

from ._namespace import HasArrayNamespace
from ._array import Array, HasArrayNamespace
from ._version import version as __version__, version_tuple as __version_tuple__
95 changes: 95 additions & 0 deletions src/array_api_typing/_array.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
__all__ = (
"Array",
"BoolArray",
"HasArrayNamespace",
"NumericArray",
)

from pathlib import Path
from types import ModuleType
from typing import Literal, Protocol, TypeAlias
from typing_extensions import TypeVar

import optype as op

from ._utils import docstring_setter

# Load docstrings from TOML file
try:
import tomllib
except ImportError:
import tomli as tomllib # type: ignore[import-not-found, no-redef]

_docstrings_path = Path(__file__).parent / "_array_docstrings.toml"
with _docstrings_path.open("rb") as f:
_array_docstrings = tomllib.load(f)["docstrings"]

NS_co = TypeVar("NS_co", covariant=True, default=ModuleType)
T_contra = TypeVar("T_contra", contravariant=True)


class HasArrayNamespace(Protocol[NS_co]):
"""Protocol for classes that have an `__array_namespace__` method.

Example:
>>> import array_api_typing as xpt
>>>
>>> class MyArray:
... def __array_namespace__(self):
... return object()
>>>
>>> x = MyArray()
>>> def has_array_namespace(x: xpt.HasArrayNamespace) -> bool:
... return hasattr(x, "__array_namespace__")
>>> has_array_namespace(x)
True

"""

def __array_namespace__(
self, /, *, api_version: Literal["2021.12"] | None = None
) -> NS_co: ...


@docstring_setter(**_array_docstrings)
class Array(
HasArrayNamespace[NS_co],
op.CanPosSelf,
op.CanNegSelf,
op.CanAddSame[T_contra],
op.CanRAddSelf[T_contra],
op.CanSubSame[T_contra],
Copy link
Member

Choose a reason for hiding this comment

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

This won't accept boolean numpy arrays:

>>> import numpy as np
>>> np.array(True) - np.array(False)
Traceback (most recent call last):
  File "<python-input-2>", line 1, in <module>
    np.array(True) - np.array(False)
    ~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~
TypeError: numpy boolean subtract, the `-` operator, is not supported, use the bitwise_xor, the `^` operator, or the logical_xor function instead.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hm. Suggestions?

Copy link
Member

Choose a reason for hiding this comment

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

isomorphic to #32 (comment)

op.CanRSubSelf[T_contra],
op.CanMulSame[T_contra],
op.CanRMulSelf[T_contra],
op.CanTruedivSame[T_contra],
Copy link
Member

Choose a reason for hiding this comment

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

CanTruedivSame requires __truediv__: (Self, Self) -> Self. In NumPy, that only holds for np.inexact dtypes (floating and complex). So this would reject integer and boolean arrays:

>>> import numpy as np
>>> np.array([1]) / np.array([1])
array([1.])
>>> np.array([True]) / np.array([True])
array([1.])

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So we need to write a more flexible Protocol for Truediv?

Copy link
Member

Choose a reason for hiding this comment

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

It's just that we can't have it return Self, but something like xpt.Array would work I suppose. Something op.CanTruediv[int, xpt.CanArray] could work, but there's currently no optype protocol for __truediv__: (Self, Self) -> T.

If you think we'll need that, I wouldn't mind adding such protocols to optype. I'm not sure what to call them though 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's a real shame that Self and TypeVar don't play so nicely together.

Copy link
Member

Choose a reason for hiding this comment

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

Yea, and there's no need for that restriction either: https://discuss.python.org/t/self-as-typevar-default/909

Copy link
Collaborator Author

@nstarman nstarman Jul 18, 2025

Choose a reason for hiding this comment

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

I've investigated further an it seems like we need Self as the default

CanTruedivSame[_T_contra, _TT_co = Self]:
    def __add__(self, rhs: Self | _T_contra, /) -> _TT_co: ...

Which isn't allowed because of restrictions in https://discuss.python.org/t/self-as-typevar-default/909, right?

So what about expanding CanTruedivSame to be?

CanTruedivSame[_T_contra, _TT_co = Never]:
    def __add__(self, rhs: Self | _T_contra, /) -> Self | _TT_co: ...

Then this reduces to (Self, Self) -> Self for CanTruedivSame[Never, Never]
Then we could correctly annotate for integer inputs that don't necessarily result in integer outputs.

If this works, it would be a good change for all the binops-Same.

Copy link
Member

Choose a reason for hiding this comment

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

I like it! Then CanTruedivSame[bool, xpt.Array] would work for booleans, since Self | xpt.Array is in this context equivalent to xpt.Array. I'll put it in optype 0.12.1 👌🏻

op.CanITruedivSelf[T_contra],
op.CanRTruedivSelf[T_contra],
op.CanFloordivSame[T_contra],
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't hold for boolean numpy arrays:

>>> import numpy as np
>>> np.array([True]) // np.array([True])
array([1], dtype=int8)

op.CanRFloordivSelf[T_contra],
op.CanModSame[T_contra],
Copy link
Member

Choose a reason for hiding this comment

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

mod and floordiv have identical signatures in numpy, so this won't work for boolean arrays:

>>> import numpy as np
>>> np.array([True]) % np.array([True])
array([0], dtype=int8)

op.CanRModSelf[T_contra],
op.CanPowSame[T_contra],
Copy link
Member

Choose a reason for hiding this comment

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

poor boolean arrays:

>>> np.array([True]) ** np.array([True])
array([1], dtype=int8)

op.CanRPowSelf[T_contra],
Protocol[T_contra, NS_co],
):
"""Array API specification for array object attributes and methods."""


BoolArray: TypeAlias = Array[bool, NS_co]
"""Array API specification for boolean array object attributes and methods.

Specifically, this type alias fills the `T_contra` type variable with `bool`,
allowing for `bool` objects to be added, subtracted, multiplied, etc. to the
array object.

"""

NumericArray: TypeAlias = Array[float | int, NS_co]
"""Array API specification for numeric array object attributes and methods.

Specifically, this type alias fills the `T_contra` type variable with `float |
int`, allowing for `float | int` objects to be added, subtracted, multiplied,
etc. to the array object.

"""
Empty file.
Loading
Loading