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 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
3473691
✨: move HasArrayNamespace to _array.py and update imports
nstarman Jun 22, 2025
5d56158
✨: add Array class definition
nstarman Jun 22, 2025
c869543
refactor: rename type variable in HasArrayNamespace protocol for cons…
nstarman Jun 22, 2025
c1962cd
✨: add CanArrayPos protocol
nstarman Jun 22, 2025
f21c055
✨: add CanArrayNeg protocol
nstarman Jun 22, 2025
f862bf4
✨: add CanArrayAdd protocol
nstarman Jun 22, 2025
154e658
✨: add CanArraySub Protocol
nstarman Jun 22, 2025
bcac721
✨: add HasArrayMul protocol
nstarman Jun 22, 2025
dd7bc09
✨: add CanArrayTrueDiv protocol
nstarman Jun 22, 2025
15527d1
✨: add CanArrayFloorDiv protocol
nstarman Jun 22, 2025
47da1d3
✨: add CanArrayMod protocol
nstarman Jun 22, 2025
94e4c60
✨: add CanArrayPow protocol
nstarman Jun 22, 2025
ba6c5b4
✨: add CanArrayIAdd protocol
nstarman Jun 23, 2025
83b2f18
✨: add CanArrayISub protocol
nstarman Jun 23, 2025
bba937a
✨: add CanArrayIMul protocol
nstarman Jun 23, 2025
dce36c4
✨: add CanArrayITruediv protocol
nstarman Jun 23, 2025
c26f488
✨: add CanArrayIFloorDiv protocol
nstarman Jun 23, 2025
19cd5f9
✨: add CanArrayIPow protocol
nstarman Jun 23, 2025
6e2be2c
✨: add CanArrayIMod protocol
nstarman Jun 23, 2025
fe86cd6
✨: add CanArrayRAdd protocol
nstarman Jun 23, 2025
e19b28a
✨: add CanArrayRSub protocol
nstarman Jun 23, 2025
5165e18
✨: add CanArrayRMul protocol
nstarman Jun 23, 2025
0a935a8
✨: add CanArrayRTruediv protocol
nstarman Jun 23, 2025
fa4e6b8
✨: add CanArrayRFloorDiv protocol
nstarman Jun 23, 2025
6bb01a8
✨: add CanArrayRPow protocol
nstarman Jun 23, 2025
2ba52db
✨: add CanArrayRMod protocol
nstarman Jun 23, 2025
e346681
➕ `optype`!
jorenham Jul 1, 2025
839beed
📌 pin the correct python version
jorenham Jul 1, 2025
eeb588d
🔧 `ruff` <3 `optype`
jorenham Jul 1, 2025
ba8b4f5
🚧 transition to optype
nstarman Jul 11, 2025
45cd14f
✨: add tomli dependency for Python version compatibility and load doc…
nstarman Jul 16, 2025
855ddf2
🔧 update optype version
nstarman Jul 16, 2025
56e4814
✨: refactor test files to improve clarity and organization of NDArray…
nstarman Jul 16, 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.0",
"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__
101 changes: 101 additions & 0 deletions src/array_api_typing/_array.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
__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.CanIAddSelf[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.

+= also works if you just have an __add__ and no __iadd__:

>>> class Thingy:
...     def __add__(self, rhs, /):
...         return self if isinstance(rhs, Thingy) else NotImplemented
...         
>>> a = Thingy()
>>> a + a
<__main__.Thingy object at 0x7f9896498830>
>>> a += a
>>> a
<__main__.Thingy object at 0x7f9896498830>

We already require Can{binop}Same, so can we remove CanI{binop}Self?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

How do you read https://data-apis.org/array-api/2021.12/API_specification/array_object.html#in-place-operators.
In my reading I agree that __iadd__ isn't strictly necessary since x += 2 will fall back to x = x + 2, making a new object.
So yes?

Copy link
Member

Choose a reason for hiding this comment

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

The "May be implemented" makes it sound like it optional to me

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

So I guess it depends on whether we want xpt.Array to be a flexible utility, or as a array api compliance check for static typing.

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?

op.CanISubSelf[T_contra],
op.CanRSubSelf[T_contra],
op.CanMulSame[T_contra],
op.CanIMulSelf[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 🤔

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.CanIFloordivSelf[T_contra],
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.CanIModSelf[T_contra],
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.CanIPowSelf[T_contra],
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.
"""
Loading
Loading