mirror of
https://github.com/wassname/jaxtyping.git
synced 2026-09-09 11:24:55 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d83ee77e6 | ||
|
|
51ac630ef0 | ||
|
|
f83170e01d | ||
|
|
07e58de0bd | ||
|
|
696cc5b90f | ||
|
|
102e499d61 | ||
|
|
f708d1742b | ||
|
|
5e2518c591 | ||
|
|
1b3173ac01 | ||
|
|
172b83b4fc | ||
|
|
17ea4b13eb | ||
|
|
9beb5f2d29 | ||
|
|
1d4d40294c | ||
|
|
28ad5275d7 | ||
|
|
d7fd59a34c | ||
|
|
8de8c0bb68 | ||
|
|
f18de2ce28 | ||
|
|
eb9a23df63 | ||
|
|
adf1a5e4e3 | ||
|
|
272be74e01 | ||
|
|
7df267efa4 | ||
|
|
d43933f942 | ||
|
|
1acc0d7153 | ||
|
|
33cf4fcdac | ||
|
|
e5cc75e4a3 | ||
|
|
125bc89ee9 |
@@ -18,12 +18,11 @@
|
|||||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/ambv/black
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: 23.9.1
|
rev: v0.1.7
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: ruff # linter
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
types_or: [ python, pyi, jupyter ]
|
||||||
rev: 'v0.0.291'
|
args: [ --fix ]
|
||||||
hooks:
|
- id: ruff-format # formatter
|
||||||
- id: ruff
|
types_or: [ python, pyi, jupyter ]
|
||||||
args: ["--fix"]
|
|
||||||
|
|||||||
@@ -39,32 +39,23 @@ The annotations provided by jaxtyping are compatible with runtime type-checking
|
|||||||
|
|
||||||
Available at [https://docs.kidger.site/jaxtyping](https://docs.kidger.site/jaxtyping).
|
Available at [https://docs.kidger.site/jaxtyping](https://docs.kidger.site/jaxtyping).
|
||||||
|
|
||||||
## Finally
|
## See also: other libraries in the JAX ecosystem
|
||||||
|
|
||||||
### See also: other libraries in the JAX ecosystem
|
**Always useful**
|
||||||
|
[Equinox](https://github.com/patrick-kidger/equinox): neural networks and everything not already in core JAX!
|
||||||
|
|
||||||
[Equinox](https://github.com/patrick-kidger/equinox): neural networks.
|
**Deep learning**
|
||||||
|
[Optax](https://github.com/deepmind/optax): first-order gradient (SGD, Adam, ...) optimisers.
|
||||||
|
[Orbax](https://github.com/google/orbax): checkpointing (async/multi-host/multi-device).
|
||||||
|
[Levanter](https://github.com/stanford-crfm/levanter): scalable+reliable training of foundation models (e.g. LLMs).
|
||||||
|
|
||||||
[Optax](https://github.com/deepmind/optax): first-order gradient (SGD, Adam, ...) optimisers.
|
**Scientific computing**
|
||||||
|
[Diffrax](https://github.com/patrick-kidger/diffrax): numerical differential equation solvers.
|
||||||
|
[Optimistix](https://github.com/patrick-kidger/optimistix): root finding, minimisation, fixed points, and least squares.
|
||||||
|
[Lineax](https://github.com/patrick-kidger/lineax): linear solvers.
|
||||||
|
[BlackJAX](https://github.com/blackjax-devs/blackjax): probabilistic+Bayesian sampling.
|
||||||
|
[sympy2jax](https://github.com/patrick-kidger/sympy2jax): SymPy<->JAX conversion; train symbolic expressions via gradient descent.
|
||||||
|
[PySR](https://github.com/milesCranmer/PySR): symbolic regression. (Non-JAX honourable mention!)
|
||||||
|
|
||||||
[Diffrax](https://github.com/patrick-kidger/diffrax): numerical differential equation solvers.
|
**Awesome JAX**
|
||||||
|
[Awesome JAX](https://github.com/n2cholas/awesome-jax): a longer list of other JAX projects.
|
||||||
[Optimistix](https://github.com/patrick-kidger/optimistix): root finding, minimisation, fixed points, and least squares.
|
|
||||||
|
|
||||||
[Lineax](https://github.com/google/lineax): linear solvers.
|
|
||||||
|
|
||||||
[BlackJAX](https://github.com/blackjax-devs/blackjax): probabilistic+Bayesian sampling.
|
|
||||||
|
|
||||||
[Orbax](https://github.com/google/orbax): checkpointing (async/multi-host/multi-device).
|
|
||||||
|
|
||||||
[sympy2jax](https://github.com/google/sympy2jax): SymPy<->JAX conversion; train symbolic expressions via gradient descent.
|
|
||||||
|
|
||||||
[Eqxvision](https://github.com/paganpasta/eqxvision): computer vision models.
|
|
||||||
|
|
||||||
[Levanter](https://github.com/stanford-crfm/levanter): scalable+reliable training of foundation models (e.g. LLMs).
|
|
||||||
|
|
||||||
[PySR](https://github.com/milesCranmer/PySR): symbolic regression. (Non-JAX honourable mention!)
|
|
||||||
|
|
||||||
### Disclaimer
|
|
||||||
|
|
||||||
This is not an official Google product.
|
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
members:
|
members:
|
||||||
false
|
false
|
||||||
|
|
||||||
|
## Printing axis bindings
|
||||||
|
|
||||||
|
::: jaxtyping.print_bindings
|
||||||
|
|
||||||
## Introspection
|
## Introspection
|
||||||
|
|
||||||
If you're writing your own type hint parser, then you may wish to detect if some Python object is a jaxtyping-provided type.
|
If you're writing your own type hint parser, then you may wish to detect if some Python object is a jaxtyping-provided type.
|
||||||
|
|||||||
+3
-3
@@ -25,7 +25,7 @@ In addition some modifiers can be applied:
|
|||||||
`def add(x: Float[Array, "#foo"], y: Float[Array, "#foo"]) -> Float[Array, "#foo"]`.
|
`def add(x: Float[Array, "#foo"], y: Float[Array, "#foo"]) -> Float[Array, "#foo"]`.
|
||||||
- Prepend `_` to an axis to disable any runtime checking of that axis (so that it can be used just as documentation). This can also be used as just `_` on its own: e.g. `"b c _ _"`.
|
- Prepend `_` to an axis to disable any runtime checking of that axis (so that it can be used just as documentation). This can also be used as just `_` on its own: e.g. `"b c _ _"`.
|
||||||
- Documentation-only names (i.e. they're ignored by jaxtyping) can be handled by prepending a name followed by `=` e.g. `Float[Array, "rows=4 cols=3"]`.
|
- Documentation-only names (i.e. they're ignored by jaxtyping) can be handled by prepending a name followed by `=` e.g. `Float[Array, "rows=4 cols=3"]`.
|
||||||
- Prepend `?` to an axis to indicate that its size can vary within a PyTree structure. (See [PyTree annotations](../pytree/).)
|
- Prepend `?` to an axis to indicate that its size can vary within a PyTree structure. (See [PyTree annotations](./pytree.md).)
|
||||||
|
|
||||||
When using multiple modifiers, their order does not matter.
|
When using multiple modifiers, their order does not matter.
|
||||||
|
|
||||||
@@ -66,9 +66,9 @@ The dtype should be any one of (all imported from `jaxtyping`):
|
|||||||
- Of particular precision: `Complex64`, `Complex128`
|
- Of particular precision: `Complex64`, `Complex128`
|
||||||
- Any integer or unsigned intger: `Integer`
|
- Any integer or unsigned intger: `Integer`
|
||||||
- Any unsigned integer: `UInt`
|
- Any unsigned integer: `UInt`
|
||||||
- Of particular precision: `UInt8`, `UInt16`, `UInt32`, `UInt64`
|
- Of particular precision: `UInt4`, `UInt8`, `UInt16`, `UInt32`, `UInt64`
|
||||||
- Any signed integer: `Int`
|
- Any signed integer: `Int`
|
||||||
- Of particular precision: `Int8`, `Int16`, `Int32`, `Int64`
|
- Of particular precision: `Int4`, `Int8`, `Int16`, `Int32`, `Int64`
|
||||||
- Any floating, integer, or unsigned integer: `Real`.
|
- Any floating, integer, or unsigned integer: `Real`.
|
||||||
|
|
||||||
Unless you really want to force a particular precision, then for most applications you should probably allow any floating-point, any integer, etc. That is, use
|
Unless you really want to force a particular precision, then for most applications you should probably allow any floating-point, any integer, etc. That is, use
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ There are two approaches: either use [`jaxtyping.jaxtyped`][] to typecheck a sin
|
|||||||
|
|
||||||
In either case, the actual business of checking types is performed with the help of a runtime type-checking library. The two most popular are [beartype](https://github.com/beartype/beartype) and [typeguard](https://github.com/agronholm/typeguard). (If using typeguard, then specifically the version `2.*` series should be used. Later versions -- `3` and `4` -- have some known issues.)
|
In either case, the actual business of checking types is performed with the help of a runtime type-checking library. The two most popular are [beartype](https://github.com/beartype/beartype) and [typeguard](https://github.com/agronholm/typeguard). (If using typeguard, then specifically the version `2.*` series should be used. Later versions -- `3` and `4` -- have some known issues.)
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
Avoid using `from __future__ import annotations`, or stringified type annotations, where possible. These are largely incompatible with runtime type checking. See also [this FAQ entry](../faq.md#dataclass-annotations-arent-being-checked-properly).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
::: jaxtyping.jaxtyped
|
::: jaxtyping.jaxtyped
|
||||||
|
|||||||
+18
@@ -20,6 +20,24 @@ Some tooling in the Python ecosystem assumes that only the latter is true, and w
|
|||||||
|
|
||||||
In the case of `flake8`, or Ruff, this can be resolved. Multi-dimensional arrays (e.g. `Float32[Array, "b c"]`) will throw a very unusual error (F722, syntax error in forward annotation), so you can safely just disable this particular error globally. Uni-dimensional arrays (e.g. `Float32[Array, "x"]`) will throw an error that's actually useful (F821, undefined name), so instead of disabling this globally, you should instead prepend a space to the start of your shape, e.g. `Float32[Array, " x"]`. `jaxtyping` will treat this in the same way, whilst `flake8` will now throw an F722 error that you can disable as before.
|
In the case of `flake8`, or Ruff, this can be resolved. Multi-dimensional arrays (e.g. `Float32[Array, "b c"]`) will throw a very unusual error (F722, syntax error in forward annotation), so you can safely just disable this particular error globally. Uni-dimensional arrays (e.g. `Float32[Array, "x"]`) will throw an error that's actually useful (F821, undefined name), so instead of disabling this globally, you should instead prepend a space to the start of your shape, e.g. `Float32[Array, " x"]`. `jaxtyping` will treat this in the same way, whilst `flake8` will now throw an F722 error that you can disable as before.
|
||||||
|
|
||||||
|
## Dataclass annotations aren't being checked properly.
|
||||||
|
|
||||||
|
Stringified dataclass annotations, e.g.
|
||||||
|
```python
|
||||||
|
@dataclass()
|
||||||
|
class Foo:
|
||||||
|
x: "int"
|
||||||
|
```
|
||||||
|
will be silently skipped without checking them. This is because these are essentially impossible to resolve at runtime. Such stringified annotations typically occur either when using them for forward references, or when using `from __future__ import annotations`. (You should essentially never use the latter, it is largely incompatible with runtime type checking and as such is [being replaced in Python 3.13](https://peps.python.org/pep-0649/).)
|
||||||
|
|
||||||
|
Partially stringified dataclass annotations, e.g.
|
||||||
|
```python
|
||||||
|
@dataclass()
|
||||||
|
class Foo:
|
||||||
|
x: tuple["int"]
|
||||||
|
```
|
||||||
|
will likely raise an error, and must not be used at all.
|
||||||
|
|
||||||
## Does jaxtyping use [PEP 646](https://www.python.org/dev/peps/pep-0646/) (variadic generics)?
|
## Does jaxtyping use [PEP 646](https://www.python.org/dev/peps/pep-0646/) (variadic generics)?
|
||||||
|
|
||||||
The intention of PEP 646 was to make it possible for static type checkers to perform shape checks of arrays. Unfortunately, this still isn't yet practical, so jaxtyping deliberately does not use this. (Yet?)
|
The intention of PEP 646 was to make it possible for static type checkers to perform shape checks of arrays. Unfortunately, this still isn't yet practical, so jaxtyping deliberately does not use this. (Yet?)
|
||||||
|
|||||||
+15
-10
@@ -43,16 +43,21 @@ Have a read of the [Array annotations](./api/array.md) documentation on the left
|
|||||||
|
|
||||||
## See also: other libraries in the JAX ecosystem
|
## See also: other libraries in the JAX ecosystem
|
||||||
|
|
||||||
[Equinox](https://github.com/patrick-kidger/equinox): neural networks.
|
**Always useful**
|
||||||
|
[Equinox](https://github.com/patrick-kidger/equinox): neural networks and everything not already in core JAX!
|
||||||
|
|
||||||
[Optax](https://github.com/deepmind/optax): first-order gradient (SGD, Adam, ...) optimisers.
|
**Deep learning**
|
||||||
|
[Optax](https://github.com/deepmind/optax): first-order gradient (SGD, Adam, ...) optimisers.
|
||||||
|
[Orbax](https://github.com/google/orbax): checkpointing (async/multi-host/multi-device).
|
||||||
|
[Levanter](https://github.com/stanford-crfm/levanter): scalable+reliable training of foundation models (e.g. LLMs).
|
||||||
|
|
||||||
[Diffrax](https://github.com/patrick-kidger/diffrax): numerical differential equation solvers.
|
**Scientific computing**
|
||||||
|
[Diffrax](https://github.com/patrick-kidger/diffrax): numerical differential equation solvers.
|
||||||
|
[Optimistix](https://github.com/patrick-kidger/optimistix): root finding, minimisation, fixed points, and least squares.
|
||||||
|
[Lineax](https://github.com/patrick-kidger/lineax): linear solvers.
|
||||||
|
[BlackJAX](https://github.com/blackjax-devs/blackjax): probabilistic+Bayesian sampling.
|
||||||
|
[sympy2jax](https://github.com/patrick-kidger/sympy2jax): SymPy<->JAX conversion; train symbolic expressions via gradient descent.
|
||||||
|
[PySR](https://github.com/milesCranmer/PySR): symbolic regression. (Non-JAX honourable mention!)
|
||||||
|
|
||||||
[Lineax](https://github.com/google/lineax): linear solvers and linear least squares.
|
**Awesome JAX**
|
||||||
|
[Awesome JAX](https://github.com/n2cholas/awesome-jax): a longer list of other JAX projects.
|
||||||
[Eqxvision](https://github.com/paganpasta/eqxvision): computer vision models.
|
|
||||||
|
|
||||||
[sympy2jax](https://github.com/google/sympy2jax): SymPy<->JAX conversion; train symbolic expressions via gradient descent.
|
|
||||||
|
|
||||||
[Levanter](https://github.com/stanford-crfm/levanter): scalable+reliable training of foundation models (e.g. LLMs).
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ mkdocs-material==7.3.6 # Theme
|
|||||||
pymdown-extensions==9.4 # Markdown extensions e.g. to handle LaTeX.
|
pymdown-extensions==9.4 # Markdown extensions e.g. to handle LaTeX.
|
||||||
mkdocstrings==0.17.0 # Autogenerate documentation from docstrings.
|
mkdocstrings==0.17.0 # Autogenerate documentation from docstrings.
|
||||||
mknotebooks==0.7.1 # Turn Jupyter Lab notebooks into webpages.
|
mknotebooks==0.7.1 # Turn Jupyter Lab notebooks into webpages.
|
||||||
pytkdocs_tweaks==0.0.5 # Tweaks mkdocstrings to improve various aspects
|
pytkdocs_tweaks==0.0.8 # Tweaks mkdocstrings to improve various aspects
|
||||||
mkdocs_include_exclude_files==0.0.1 # Tweak which files are included/excluded
|
mkdocs_include_exclude_files==0.0.1 # Tweak which files are included/excluded
|
||||||
jinja2==3.0.3 # Older version. After 3.1.0 seems to be incompatible with current versions of mkdocstrings.
|
jinja2==3.0.3 # Older version. After 3.1.0 seems to be incompatible with current versions of mkdocstrings.
|
||||||
pygments==2.14.0
|
pygments==2.14.0
|
||||||
|
|||||||
+142
-120
@@ -17,56 +17,39 @@
|
|||||||
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
import functools as ft
|
||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
|
import importlib.util
|
||||||
import typing
|
import typing
|
||||||
import warnings
|
import warnings
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
# First import some things as normal
|
|
||||||
from ._array_types import (
|
from ._array_types import (
|
||||||
AbstractArray as AbstractArray,
|
AbstractArray as AbstractArray,
|
||||||
AbstractDtype as AbstractDtype,
|
AbstractDtype as AbstractDtype,
|
||||||
get_array_name_format as get_array_name_format,
|
get_array_name_format as get_array_name_format,
|
||||||
has_jax,
|
|
||||||
set_array_name_format as set_array_name_format,
|
set_array_name_format as set_array_name_format,
|
||||||
)
|
)
|
||||||
from ._decorator import jaxtyped as jaxtyped, TypeCheckError as TypeCheckError
|
from ._config import config as config
|
||||||
|
from ._decorator import jaxtyped as jaxtyped
|
||||||
|
from ._errors import (
|
||||||
|
AnnotationError as AnnotationError,
|
||||||
|
TypeCheckError as TypeCheckError,
|
||||||
|
)
|
||||||
from ._import_hook import install_import_hook as install_import_hook
|
from ._import_hook import install_import_hook as install_import_hook
|
||||||
from ._ipython_extension import load_ipython_extension as load_ipython_extension
|
from ._ipython_extension import load_ipython_extension as load_ipython_extension
|
||||||
|
from ._storage import print_bindings as print_bindings
|
||||||
|
|
||||||
|
|
||||||
# Now import Array and ArrayLike
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
# For imports, we need to explicitly `import X as X` in order for Pyright to see
|
import typing_extensions
|
||||||
# them as public. See discussion at https://github.com/microsoft/pyright/issues/2277
|
|
||||||
from jax import Array as Array
|
from jax import Array as Array
|
||||||
from jax.typing import ArrayLike as ArrayLike
|
from jax.tree_util import PyTreeDef as PyTreeDef
|
||||||
elif has_jax:
|
from jax.typing import ArrayLike as ArrayLike, DTypeLike as DTypeLike
|
||||||
if getattr(typing, "GENERATING_DOCUMENTATION", False):
|
|
||||||
|
|
||||||
class Array:
|
|
||||||
pass
|
|
||||||
|
|
||||||
Array.__module__ = "builtins"
|
|
||||||
|
|
||||||
class ArrayLike:
|
|
||||||
pass
|
|
||||||
|
|
||||||
ArrayLike.__module__ = "builtins"
|
|
||||||
else:
|
|
||||||
from jax import Array as Array
|
|
||||||
|
|
||||||
try:
|
|
||||||
from jax.typing import ArrayLike as ArrayLike
|
|
||||||
except (ModuleNotFoundError, ImportError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# Import our dtypes
|
|
||||||
if typing.TYPE_CHECKING:
|
|
||||||
# Introduce an indirection so that we can `import X as X` to make it clear that
|
# Introduce an indirection so that we can `import X as X` to make it clear that
|
||||||
# these are public.
|
# these are public.
|
||||||
from jax.typing import DTypeLike as DTypeLike
|
|
||||||
|
|
||||||
from ._indirection import (
|
from ._indirection import (
|
||||||
BFloat16 as BFloat16,
|
BFloat16 as BFloat16,
|
||||||
Bool as Bool,
|
Bool as Bool,
|
||||||
@@ -79,6 +62,7 @@ if typing.TYPE_CHECKING:
|
|||||||
Float64 as Float64,
|
Float64 as Float64,
|
||||||
Inexact as Inexact,
|
Inexact as Inexact,
|
||||||
Int as Int,
|
Int as Int,
|
||||||
|
Int4 as Int4,
|
||||||
Int8 as Int8,
|
Int8 as Int8,
|
||||||
Int16 as Int16,
|
Int16 as Int16,
|
||||||
Int32 as Int32,
|
Int32 as Int32,
|
||||||
@@ -86,56 +70,18 @@ if typing.TYPE_CHECKING:
|
|||||||
Integer as Integer,
|
Integer as Integer,
|
||||||
Key as Key,
|
Key as Key,
|
||||||
Num as Num,
|
Num as Num,
|
||||||
|
PRNGKeyArray as PRNGKeyArray,
|
||||||
Real as Real,
|
Real as Real,
|
||||||
|
Scalar as Scalar,
|
||||||
|
ScalarLike as ScalarLike,
|
||||||
Shaped as Shaped,
|
Shaped as Shaped,
|
||||||
UInt as UInt,
|
UInt as UInt,
|
||||||
|
UInt4 as UInt4,
|
||||||
UInt8 as UInt8,
|
UInt8 as UInt8,
|
||||||
UInt16 as UInt16,
|
UInt16 as UInt16,
|
||||||
UInt32 as UInt32,
|
UInt32 as UInt32,
|
||||||
UInt64 as UInt64,
|
UInt64 as UInt64,
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
from ._array_types import (
|
|
||||||
BFloat16 as BFloat16,
|
|
||||||
Bool as Bool,
|
|
||||||
Complex as Complex,
|
|
||||||
Complex64 as Complex64,
|
|
||||||
Complex128 as Complex128,
|
|
||||||
Float as Float,
|
|
||||||
Float16 as Float16,
|
|
||||||
Float32 as Float32,
|
|
||||||
Float64 as Float64,
|
|
||||||
Inexact as Inexact,
|
|
||||||
Int as Int,
|
|
||||||
Int8 as Int8,
|
|
||||||
Int16 as Int16,
|
|
||||||
Int32 as Int32,
|
|
||||||
Int64 as Int64,
|
|
||||||
Integer as Integer,
|
|
||||||
Num as Num,
|
|
||||||
Real as Real,
|
|
||||||
Shaped as Shaped,
|
|
||||||
UInt as UInt,
|
|
||||||
UInt8 as UInt8,
|
|
||||||
UInt16 as UInt16,
|
|
||||||
UInt32 as UInt32,
|
|
||||||
UInt64 as UInt64,
|
|
||||||
)
|
|
||||||
|
|
||||||
if has_jax:
|
|
||||||
import jax.typing
|
|
||||||
|
|
||||||
from ._array_types import Key as Key
|
|
||||||
|
|
||||||
if hasattr(jax.typing, "DTypeLike"):
|
|
||||||
from jax.typing import DTypeLike as DTypeLike
|
|
||||||
|
|
||||||
|
|
||||||
# Now import PyTreeDef and PyTree
|
|
||||||
if typing.TYPE_CHECKING:
|
|
||||||
import typing_extensions
|
|
||||||
|
|
||||||
from jax.tree_util import PyTreeDef as PyTreeDef
|
|
||||||
|
|
||||||
# Set up to deliberately confuse a static type checker.
|
# Set up to deliberately confuse a static type checker.
|
||||||
PyTree: typing_extensions.TypeAlias = getattr(typing, "foo" + "bar")
|
PyTree: typing_extensions.TypeAlias = getattr(typing, "foo" + "bar")
|
||||||
@@ -155,57 +101,133 @@ if typing.TYPE_CHECKING:
|
|||||||
# If they can't figure out what a type is, then they just give up and allow
|
# If they can't figure out what a type is, then they just give up and allow
|
||||||
# anything. (I believe this is sometimes called `Unknown`.) Thus, this odd-looking
|
# anything. (I believe this is sometimes called `Unknown`.) Thus, this odd-looking
|
||||||
# annotation, which static type checkers aren't smart enough to resolve.
|
# annotation, which static type checkers aren't smart enough to resolve.
|
||||||
elif has_jax:
|
else:
|
||||||
if hasattr(typing, "GENERATING_DOCUMENTATION"):
|
from ._array_types import (
|
||||||
# Most parts of the Equinox ecosystem have
|
BFloat16 as BFloat16,
|
||||||
# `typing.GENERATING_DOCUMENTATION = True` when generating documentation, to
|
Bool as Bool,
|
||||||
# add whatever shims are necessary to get pretty docs. E.g. to have type
|
Complex as Complex,
|
||||||
# annotations appear as just `PyTree`, not `jaxtyping.PyTree`.
|
Complex64 as Complex64,
|
||||||
#
|
Complex128 as Complex128,
|
||||||
# As jaxtyping actually wants things to appear as e.g. `jaxtyping.PyTree`,
|
Float as Float,
|
||||||
# rather than just `PyTree`, then it sets
|
Float16 as Float16,
|
||||||
# `typing.GENERATING_DOCUMENTATION = False`, to disable these shims.
|
Float32 as Float32,
|
||||||
#
|
Float64 as Float64,
|
||||||
# Here we do only a `hasattr` check, as we want to get this version of
|
Inexact as Inexact,
|
||||||
# `PyTreeDef` in both the jaxtyping and the Equinox(/etc.) docs.
|
Int as Int,
|
||||||
|
Int4 as Int4,
|
||||||
class PyTreeDef:
|
Int8 as Int8,
|
||||||
"""Alias for `jax.tree_util.PyTreeDef`, which is the type of the return
|
Int16 as Int16,
|
||||||
from `jax.tree_util.tree_structure(...)`.
|
Int32 as Int32,
|
||||||
"""
|
Int64 as Int64,
|
||||||
|
Integer as Integer,
|
||||||
if typing.GENERATING_DOCUMENTATION:
|
Key as Key,
|
||||||
# Equinox etc. docs get just `PyTreeDef`.
|
Num as Num,
|
||||||
# jaxtyping docs get `jaxtyping.PyTreeDef`.
|
Real as Real,
|
||||||
PyTreeDef.__module__ = "builtins"
|
Shaped as Shaped,
|
||||||
|
UInt as UInt,
|
||||||
else:
|
UInt4 as UInt4,
|
||||||
from jax.tree_util import PyTreeDef as PyTreeDef
|
UInt8 as UInt8,
|
||||||
|
UInt16 as UInt16,
|
||||||
from ._pytree_type import PyTree as PyTree # noqa: F401
|
UInt32 as UInt32,
|
||||||
|
UInt64 as UInt64,
|
||||||
|
|
||||||
# Conveniences
|
|
||||||
if typing.TYPE_CHECKING:
|
|
||||||
from ._indirection import (
|
|
||||||
PRNGKeyArray as PRNGKeyArray,
|
|
||||||
Scalar as Scalar,
|
|
||||||
ScalarLike as ScalarLike,
|
|
||||||
)
|
)
|
||||||
elif has_jax:
|
|
||||||
from ._array_types import Scalar, ScalarLike # noqa: F401
|
|
||||||
|
|
||||||
if getattr(typing, "GENERATING_DOCUMENTATION", False):
|
# But crucially, does not actually import jax at all. We do that dynamically in
|
||||||
# That is, we're generating some downstream documentation, not the jaxtyping
|
# __getattr__ if required. See #178.
|
||||||
# documentation itself.
|
if importlib.util.find_spec("jax") is not None:
|
||||||
class PRNGKeyArray:
|
|
||||||
pass
|
|
||||||
|
|
||||||
PRNGKeyArray.__module__ = "builtins"
|
@ft.cache
|
||||||
else:
|
def __getattr__(item):
|
||||||
from ._array_types import PRNGKeyArray
|
if item == "Array":
|
||||||
|
if getattr(typing, "GENERATING_DOCUMENTATION", False):
|
||||||
|
|
||||||
del has_jax
|
class Array:
|
||||||
|
pass
|
||||||
|
|
||||||
|
Array.__module__ = "builtins"
|
||||||
|
Array.__qualname__ = "Array"
|
||||||
|
return Array
|
||||||
|
else:
|
||||||
|
import jax
|
||||||
|
|
||||||
|
return jax.Array
|
||||||
|
elif item == "ArrayLike":
|
||||||
|
if getattr(typing, "GENERATING_DOCUMENTATION", False):
|
||||||
|
|
||||||
|
class ArrayLike:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ArrayLike.__module__ = "builtins"
|
||||||
|
ArrayLike.__qualname__ = "ArrayLike"
|
||||||
|
return ArrayLike
|
||||||
|
else:
|
||||||
|
import jax.typing
|
||||||
|
|
||||||
|
return jax.typing.ArrayLike
|
||||||
|
elif item == "PRNGKeyArray":
|
||||||
|
if getattr(typing, "GENERATING_DOCUMENTATION", False):
|
||||||
|
|
||||||
|
class PRNGKeyArray:
|
||||||
|
pass
|
||||||
|
|
||||||
|
PRNGKeyArray.__module__ = "builtins"
|
||||||
|
PRNGKeyArray.__qualname__ = "PRNGKeyArray"
|
||||||
|
return PRNGKeyArray
|
||||||
|
else:
|
||||||
|
# New-style `jax.random.key` have scalar shape and dtype `key<foo>`.
|
||||||
|
# Old-style `jax.random.PRNGKey` have shape `(2,)` and dtype
|
||||||
|
# `uint32`.
|
||||||
|
import jax
|
||||||
|
|
||||||
|
return Union[Key[jax.Array, ""], UInt32[jax.Array, "2"]]
|
||||||
|
elif item == "DTypeLike":
|
||||||
|
import jax.typing
|
||||||
|
|
||||||
|
return jax.typing.DTypeLike
|
||||||
|
elif item == "Scalar":
|
||||||
|
import jax
|
||||||
|
|
||||||
|
return Shaped[jax.Array, ""]
|
||||||
|
elif item == "ScalarLike":
|
||||||
|
import jax.typing
|
||||||
|
|
||||||
|
return Shaped[jax.typing.ArrayLike, ""]
|
||||||
|
elif item == "PyTree":
|
||||||
|
from ._pytree_type import PyTree
|
||||||
|
|
||||||
|
return PyTree
|
||||||
|
elif item == "PyTreeDef":
|
||||||
|
if hasattr(typing, "GENERATING_DOCUMENTATION"):
|
||||||
|
# Most parts of the Equinox ecosystem have
|
||||||
|
# `typing.GENERATING_DOCUMENTATION = True` when generating
|
||||||
|
# documentation, to add whatever shims are necessary to get pretty
|
||||||
|
# docs. E.g. to have type annotations appear as just `PyTree`, not
|
||||||
|
# `jaxtyping.PyTree`.
|
||||||
|
#
|
||||||
|
# As jaxtyping actually wants things to appear as e.g.
|
||||||
|
# `jaxtyping.PyTree`, rather than just `PyTree`, then it sets
|
||||||
|
# `typing.GENERATING_DOCUMENTATION = False`, to disable these shims.
|
||||||
|
#
|
||||||
|
# Here we do only a `hasattr` check, as we want to get this version
|
||||||
|
# of `PyTreeDef` in both the jaxtyping and the Equinox(/etc.) docs.
|
||||||
|
|
||||||
|
class PyTreeDef:
|
||||||
|
"""Alias for `jax.tree_util.PyTreeDef`, which is the type of the
|
||||||
|
return from `jax.tree_util.tree_structure(...)`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if typing.GENERATING_DOCUMENTATION:
|
||||||
|
# Equinox etc. docs get just `PyTreeDef`.
|
||||||
|
# jaxtyping docs get `jaxtyping.PyTreeDef`.
|
||||||
|
PyTreeDef.__qualname__ = "PyTreeDef"
|
||||||
|
PyTreeDef.__module__ = "builtins"
|
||||||
|
return PyTreeDef
|
||||||
|
else:
|
||||||
|
import jax.tree_util
|
||||||
|
|
||||||
|
return jax.tree_util.PyTreeDef
|
||||||
|
else:
|
||||||
|
raise AttributeError(f"module jaxtyping has no attribute {item!r}")
|
||||||
|
|
||||||
|
|
||||||
check_equinox_version = True # easy-to-replace line with copybara
|
check_equinox_version = True # easy-to-replace line with copybara
|
||||||
|
|||||||
+131
-105
@@ -23,11 +23,12 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
import typing
|
import typing
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import Any, Literal, NoReturn, Optional, Union
|
from typing import Any, Literal, NoReturn, Optional, Union
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from ._raise import jaxtyping_raise, jaxtyping_raise_from
|
from ._errors import AnnotationError
|
||||||
from ._storage import (
|
from ._storage import (
|
||||||
get_shape_memo,
|
get_shape_memo,
|
||||||
get_treeflatten_memo,
|
get_treeflatten_memo,
|
||||||
@@ -36,18 +37,6 @@ from ._storage import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
import jax
|
|
||||||
except (ImportError, RuntimeError, AttributeError):
|
|
||||||
# We catch `RuntimeError` as JAX will throw this if it's present, but unable to run
|
|
||||||
# on the current machine. This fails with this error.
|
|
||||||
# We catch `AttributeError` as the above then leaves the module in a partially
|
|
||||||
# initialised state, which causes subsequent imports to fail with this error.
|
|
||||||
has_jax = False
|
|
||||||
else:
|
|
||||||
has_jax = True
|
|
||||||
|
|
||||||
|
|
||||||
_array_name_format = "dtype_and_shape"
|
_array_name_format = "dtype_and_shape"
|
||||||
|
|
||||||
|
|
||||||
@@ -62,7 +51,6 @@ def set_array_name_format(value):
|
|||||||
|
|
||||||
_any_dtype = object()
|
_any_dtype = object()
|
||||||
|
|
||||||
|
|
||||||
_anonymous_dim = object()
|
_anonymous_dim = object()
|
||||||
_anonymous_variadic_dim = object()
|
_anonymous_variadic_dim = object()
|
||||||
|
|
||||||
@@ -73,30 +61,30 @@ class _DimType(enum.Enum):
|
|||||||
symbolic = enum.auto()
|
symbolic = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
class _NamedDim:
|
class _NamedDim:
|
||||||
def __init__(self, name, broadcastable, treepath):
|
name: str
|
||||||
self.name = name
|
broadcastable: bool
|
||||||
self.broadcastable = broadcastable
|
treepath: Any
|
||||||
self.treepath = treepath
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
class _NamedVariadicDim:
|
class _NamedVariadicDim:
|
||||||
def __init__(self, name, broadcastable, treepath):
|
name: str
|
||||||
self.name = name
|
broadcastable: bool
|
||||||
self.broadcastable = broadcastable
|
treepath: Any
|
||||||
self.treepath = treepath
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
class _FixedDim:
|
class _FixedDim:
|
||||||
def __init__(self, size, broadcastable):
|
size: str
|
||||||
self.size = size
|
broadcastable: bool
|
||||||
self.broadcastable = broadcastable
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
class _SymbolicDim:
|
class _SymbolicDim:
|
||||||
def __init__(self, elem, broadcastable):
|
elem: Any
|
||||||
self.elem = elem
|
broadcastable: bool
|
||||||
self.broadcastable = broadcastable
|
|
||||||
|
|
||||||
|
|
||||||
_AbstractDimOrVariadicDim = Union[
|
_AbstractDimOrVariadicDim = Union[
|
||||||
@@ -115,7 +103,7 @@ def _check_dims(
|
|||||||
obj_shape: tuple[int, ...],
|
obj_shape: tuple[int, ...],
|
||||||
single_memo: dict[str, int],
|
single_memo: dict[str, int],
|
||||||
arg_memo: dict[str, Any],
|
arg_memo: dict[str, Any],
|
||||||
) -> bool:
|
) -> str:
|
||||||
assert len(cls_dims) == len(obj_shape)
|
assert len(cls_dims) == len(obj_shape)
|
||||||
for cls_dim, obj_size in zip(cls_dims, obj_shape):
|
for cls_dim, obj_size in zip(cls_dims, obj_shape):
|
||||||
if cls_dim is _anonymous_dim:
|
if cls_dim is _anonymous_dim:
|
||||||
@@ -124,7 +112,7 @@ def _check_dims(
|
|||||||
pass
|
pass
|
||||||
elif type(cls_dim) is _FixedDim:
|
elif type(cls_dim) is _FixedDim:
|
||||||
if cls_dim.size != obj_size:
|
if cls_dim.size != obj_size:
|
||||||
return False
|
return f"the dimension size {obj_size} does not equal {cls_dim.size} as expected by the type hint" # noqa: E501
|
||||||
elif type(cls_dim) is _SymbolicDim:
|
elif type(cls_dim) is _SymbolicDim:
|
||||||
try:
|
try:
|
||||||
# Support f-string syntax.
|
# Support f-string syntax.
|
||||||
@@ -133,18 +121,15 @@ def _check_dims(
|
|||||||
# Make a copy to avoid `__builtins__` getting added as a key.
|
# Make a copy to avoid `__builtins__` getting added as a key.
|
||||||
eval_size = eval(elem, single_memo.copy())
|
eval_size = eval(elem, single_memo.copy())
|
||||||
except NameError as e:
|
except NameError as e:
|
||||||
jaxtyping_raise_from(
|
raise AnnotationError(
|
||||||
NameError(
|
f"Cannot process symbolic axis '{cls_dim.elem}' as "
|
||||||
f"Cannot process symbolic axis '{cls_dim.elem}' as "
|
"some axis names have not been processed. In practice you "
|
||||||
"some axis names have not been processed. In practice you "
|
"should usually only use symbolic axes in annotations "
|
||||||
"should usually only use symbolic axes in annotations "
|
"for return types, referring only to axes annotated for "
|
||||||
"for return types, referring only to axes annotated for "
|
"arguments."
|
||||||
"arguments."
|
) from e
|
||||||
),
|
|
||||||
e,
|
|
||||||
)
|
|
||||||
if eval_size != obj_size:
|
if eval_size != obj_size:
|
||||||
return False
|
return f"the dimension size {obj_size} does not equal the existing value of {cls_dim.elem}={eval_size}" # noqa: E501
|
||||||
else:
|
else:
|
||||||
assert type(cls_dim) is _NamedDim
|
assert type(cls_dim) is _NamedDim
|
||||||
if cls_dim.treepath:
|
if cls_dim.treepath:
|
||||||
@@ -157,16 +142,26 @@ def _check_dims(
|
|||||||
single_memo[name] = obj_size
|
single_memo[name] = obj_size
|
||||||
else:
|
else:
|
||||||
if cls_size != obj_size:
|
if cls_size != obj_size:
|
||||||
return False
|
return f"the size of dimension {cls_dim.name} is {obj_size} which does not equal the existing value of {cls_size}" # noqa: E501
|
||||||
return True
|
return ""
|
||||||
|
|
||||||
|
|
||||||
class _MetaAbstractArray(type):
|
class _MetaAbstractArray(type):
|
||||||
def __instancecheck__(cls, obj):
|
_skip_instancecheck: bool = False
|
||||||
|
|
||||||
|
def make_transparent(cls):
|
||||||
|
cls._skip_instancecheck = True
|
||||||
|
|
||||||
|
def __instancecheck__(cls, obj: Any) -> bool:
|
||||||
|
return cls.__instancecheck_str__(obj) == ""
|
||||||
|
|
||||||
|
def __instancecheck_str__(cls, obj: Any) -> str:
|
||||||
|
if cls._skip_instancecheck:
|
||||||
|
return ""
|
||||||
if not isinstance(obj, cls.array_type):
|
if not isinstance(obj, cls.array_type):
|
||||||
return False
|
return f"this value is not an instance of the underlying array type {cls.array_type}" # noqa: E501
|
||||||
if get_treeflatten_memo():
|
if get_treeflatten_memo():
|
||||||
return True
|
return ""
|
||||||
|
|
||||||
if hasattr(obj.dtype, "type") and hasattr(obj.dtype.type, "__name__"):
|
if hasattr(obj.dtype, "type") and hasattr(obj.dtype.type, "__name__"):
|
||||||
# JAX, numpy
|
# JAX, numpy
|
||||||
@@ -180,8 +175,8 @@ class _MetaAbstractArray(type):
|
|||||||
if len(repr_dtype) == 2 and repr_dtype[0] == "torch":
|
if len(repr_dtype) == 2 and repr_dtype[0] == "torch":
|
||||||
dtype = repr_dtype[1]
|
dtype = repr_dtype[1]
|
||||||
else:
|
else:
|
||||||
jaxtyping_raise(
|
raise AnnotationError(
|
||||||
RuntimeError("Unrecognised array/tensor type to extract dtype from")
|
"Unrecognised array/tensor type to extract dtype from"
|
||||||
)
|
)
|
||||||
|
|
||||||
if cls.dtypes is not _any_dtype:
|
if cls.dtypes is not _any_dtype:
|
||||||
@@ -196,7 +191,10 @@ class _MetaAbstractArray(type):
|
|||||||
if in_dtypes:
|
if in_dtypes:
|
||||||
break
|
break
|
||||||
if not in_dtypes:
|
if not in_dtypes:
|
||||||
return False
|
if len(cls.dtypes) == 1:
|
||||||
|
return f"this array has dtype {dtype}, not {cls.dtypes[0]} as expected by the type hint" # noqa: E501
|
||||||
|
else:
|
||||||
|
return f"this array has dtype {dtype}, not any of {cls.dtypes} as expected by the type hint" # noqa: E501
|
||||||
|
|
||||||
single_memo, variadic_memo, pytree_memo, arg_memo = get_shape_memo()
|
single_memo, variadic_memo, pytree_memo, arg_memo = get_shape_memo()
|
||||||
single_memo_bak = single_memo.copy()
|
single_memo_bak = single_memo.copy()
|
||||||
@@ -210,13 +208,13 @@ class _MetaAbstractArray(type):
|
|||||||
single_memo_bak, variadic_memo_bak, pytree_memo_bak, arg_memo_bak
|
single_memo_bak, variadic_memo_bak, pytree_memo_bak, arg_memo_bak
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
if check:
|
if check == "":
|
||||||
return True
|
return check
|
||||||
else:
|
else:
|
||||||
set_shape_memo(
|
set_shape_memo(
|
||||||
single_memo_bak, variadic_memo_bak, pytree_memo_bak, arg_memo_bak
|
single_memo_bak, variadic_memo_bak, pytree_memo_bak, arg_memo_bak
|
||||||
)
|
)
|
||||||
return False
|
return check
|
||||||
|
|
||||||
def _check_shape(
|
def _check_shape(
|
||||||
cls,
|
cls,
|
||||||
@@ -224,27 +222,32 @@ class _MetaAbstractArray(type):
|
|||||||
single_memo: dict[str, int],
|
single_memo: dict[str, int],
|
||||||
variadic_memo: dict[str, tuple[bool, tuple[int, ...]]],
|
variadic_memo: dict[str, tuple[bool, tuple[int, ...]]],
|
||||||
arg_memo: dict[str, Any],
|
arg_memo: dict[str, Any],
|
||||||
):
|
) -> str:
|
||||||
if cls.index_variadic is None:
|
if cls.index_variadic is None:
|
||||||
if obj.ndim != len(cls.dims):
|
if len(obj.shape) != len(cls.dims):
|
||||||
return False
|
return f"this array has {len(obj.shape)} dimensions, not the {len(cls.dims)} expected by the type hint" # noqa: E501
|
||||||
return _check_dims(cls.dims, obj.shape, single_memo, arg_memo)
|
return _check_dims(cls.dims, obj.shape, single_memo, arg_memo)
|
||||||
else:
|
else:
|
||||||
if obj.ndim < len(cls.dims) - 1:
|
if len(obj.shape) < len(cls.dims) - 1:
|
||||||
return False
|
return f"this array has {len(obj.shape)} dimensions, which is fewer than {len(cls.dims) - 1} that is the minimum expected by the type hint" # noqa: E501
|
||||||
i = cls.index_variadic
|
i = cls.index_variadic
|
||||||
j = -(len(cls.dims) - i - 1)
|
j = -(len(cls.dims) - i - 1)
|
||||||
if j == 0:
|
if j == 0:
|
||||||
j = None
|
j = None
|
||||||
if not _check_dims(cls.dims[:i], obj.shape[:i], single_memo, arg_memo):
|
prefix_check = _check_dims(
|
||||||
return False
|
cls.dims[:i], obj.shape[:i], single_memo, arg_memo
|
||||||
if j is not None and not _check_dims(
|
)
|
||||||
cls.dims[j:], obj.shape[j:], single_memo, arg_memo
|
if prefix_check != "":
|
||||||
):
|
return prefix_check
|
||||||
return False
|
if j is not None:
|
||||||
|
suffix_check = _check_dims(
|
||||||
|
cls.dims[j:], obj.shape[j:], single_memo, arg_memo
|
||||||
|
)
|
||||||
|
if suffix_check != "":
|
||||||
|
return suffix_check
|
||||||
variadic_dim = cls.dims[i]
|
variadic_dim = cls.dims[i]
|
||||||
if variadic_dim is _anonymous_variadic_dim:
|
if variadic_dim is _anonymous_variadic_dim:
|
||||||
return True
|
return ""
|
||||||
else:
|
else:
|
||||||
assert type(variadic_dim) is _NamedVariadicDim
|
assert type(variadic_dim) is _NamedVariadicDim
|
||||||
if variadic_dim.treepath:
|
if variadic_dim.treepath:
|
||||||
@@ -256,16 +259,16 @@ class _MetaAbstractArray(type):
|
|||||||
prev_broadcastable, prev_shape = variadic_memo[name]
|
prev_broadcastable, prev_shape = variadic_memo[name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
variadic_memo[name] = (broadcastable, obj.shape[i:j])
|
variadic_memo[name] = (broadcastable, obj.shape[i:j])
|
||||||
return True
|
return ""
|
||||||
else:
|
else:
|
||||||
new_shape = obj.shape[i:j]
|
new_shape = obj.shape[i:j]
|
||||||
if prev_broadcastable:
|
if prev_broadcastable:
|
||||||
try:
|
try:
|
||||||
broadcast_shape = np.broadcast_shapes(new_shape, prev_shape)
|
broadcast_shape = np.broadcast_shapes(new_shape, prev_shape)
|
||||||
except ValueError: # not broadcastable e.g. (3, 4) and (5,)
|
except ValueError: # not broadcastable e.g. (3, 4) and (5,)
|
||||||
return False
|
return f"the shape of its variadic dimensions '*{variadic_dim.name}' is {new_shape}, which cannot be broadcast with the existing value of {prev_shape}" # noqa: E501
|
||||||
if not broadcastable and broadcast_shape != new_shape:
|
if not broadcastable and broadcast_shape != new_shape:
|
||||||
return False
|
return f"the shape of its variadic dimensions '*{variadic_dim.name}' is {new_shape}, which the existing value of {prev_shape} cannot be broadcast to" # noqa: E501
|
||||||
variadic_memo[name] = (broadcastable, broadcast_shape)
|
variadic_memo[name] = (broadcastable, broadcast_shape)
|
||||||
else:
|
else:
|
||||||
if broadcastable:
|
if broadcastable:
|
||||||
@@ -274,20 +277,37 @@ class _MetaAbstractArray(type):
|
|||||||
new_shape, prev_shape
|
new_shape, prev_shape
|
||||||
)
|
)
|
||||||
except ValueError: # not broadcastable e.g. (3, 4) and (5,)
|
except ValueError: # not broadcastable e.g. (3, 4) and (5,)
|
||||||
return False
|
return f"the shape of its variadic dimensions '*{variadic_dim.name}' is {new_shape}, which cannot be broadcast with the existing value of {prev_shape}" # noqa: E501
|
||||||
if broadcast_shape != prev_shape:
|
if broadcast_shape != prev_shape:
|
||||||
return False
|
return f"the shape of its variadic dimensions '*{variadic_dim.name}' is {new_shape}, which cannot be broadcast to the existing value of {prev_shape}" # noqa: E501
|
||||||
else:
|
else:
|
||||||
if new_shape != prev_shape:
|
if new_shape != prev_shape:
|
||||||
return False
|
return f"the shape of its variadic dimensions '*{variadic_dim.name}' is {new_shape}, which does not equal the existing value of {prev_shape}" # noqa: E501
|
||||||
return True
|
return ""
|
||||||
assert False
|
assert False
|
||||||
|
|
||||||
|
|
||||||
@ft.lru_cache(maxsize=None)
|
@ft.lru_cache(maxsize=None)
|
||||||
def _make_metaclass(base_metaclass):
|
def _make_metaclass(base_metaclass):
|
||||||
class MetaAbstractArray(_MetaAbstractArray, base_metaclass):
|
class MetaAbstractArray(_MetaAbstractArray, base_metaclass):
|
||||||
pass
|
def _get_props(cls):
|
||||||
|
props_tuple = (
|
||||||
|
cls.index_variadic,
|
||||||
|
cls.dims,
|
||||||
|
cls.array_type,
|
||||||
|
cls.dtypes,
|
||||||
|
cls.dim_str,
|
||||||
|
)
|
||||||
|
return props_tuple
|
||||||
|
|
||||||
|
def __eq__(cls, other):
|
||||||
|
if type(cls) is not type(other):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return cls._get_props() == other._get_props()
|
||||||
|
|
||||||
|
def __hash__(cls):
|
||||||
|
return hash(cls._get_props())
|
||||||
|
|
||||||
return MetaAbstractArray
|
return MetaAbstractArray
|
||||||
|
|
||||||
@@ -318,14 +338,13 @@ class AbstractArray(metaclass=_MetaAbstractArray):
|
|||||||
|
|
||||||
_not_made = object()
|
_not_made = object()
|
||||||
|
|
||||||
|
|
||||||
_union_types = [typing.Union]
|
_union_types = [typing.Union]
|
||||||
if sys.version_info >= (3, 10):
|
if sys.version_info >= (3, 10):
|
||||||
_union_types.append(types.UnionType)
|
_union_types.append(types.UnionType)
|
||||||
|
|
||||||
|
|
||||||
@ft.lru_cache(maxsize=None)
|
@ft.lru_cache(maxsize=None)
|
||||||
def _make_array(array_type, dim_str, dtypes, name):
|
def _make_array_cached(array_type, dim_str, dtypes, name):
|
||||||
if not isinstance(dim_str, str):
|
if not isinstance(dim_str, str):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Shape specification must be a string. Axes should be separated with "
|
"Shape specification must be a string. Axes should be separated with "
|
||||||
@@ -540,33 +559,42 @@ def _make_array(array_type, dim_str, dtypes, name):
|
|||||||
name = type_str
|
name = type_str
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"array_name_format {_array_name_format} not recognised")
|
raise ValueError(f"array_name_format {_array_name_format} not recognised")
|
||||||
metaclass = _make_metaclass(type(array_type))
|
|
||||||
out = metaclass(
|
return (array_type, name, dtypes, dims, index_variadic, dim_str)
|
||||||
name,
|
|
||||||
(array_type, AbstractArray),
|
|
||||||
dict(
|
def _make_array(*args, **kwargs):
|
||||||
array_type=array_type,
|
out = _make_array_cached(*args, **kwargs)
|
||||||
dtypes=dtypes,
|
|
||||||
dims=dims,
|
if type(out) is tuple:
|
||||||
index_variadic=index_variadic,
|
array_type, name, dtypes, dims, index_variadic, dim_str = out
|
||||||
dim_str=dim_str,
|
metaclass = _make_metaclass(type(array_type))
|
||||||
),
|
|
||||||
)
|
out = metaclass(
|
||||||
if getattr(typing, "GENERATING_DOCUMENTATION", False):
|
name,
|
||||||
out.__module__ = "builtins"
|
(array_type, AbstractArray),
|
||||||
else:
|
dict(
|
||||||
out.__module__ = "jaxtyping"
|
array_type=array_type,
|
||||||
|
dtypes=dtypes,
|
||||||
|
dims=dims,
|
||||||
|
index_variadic=index_variadic,
|
||||||
|
dim_str=dim_str,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if getattr(typing, "GENERATING_DOCUMENTATION", False):
|
||||||
|
out.__module__ = "builtins"
|
||||||
|
else:
|
||||||
|
out.__module__ = "jaxtyping"
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
class _MetaAbstractDtype(type):
|
class _MetaAbstractDtype(type):
|
||||||
def __instancecheck__(cls, obj: Any) -> NoReturn:
|
def __instancecheck__(cls, obj: Any) -> NoReturn:
|
||||||
jaxtyping_raise(
|
raise AnnotationError(
|
||||||
RuntimeError(
|
f"Do not use `isinstance(x, jaxtyping.{cls.__name__})`. If you want to "
|
||||||
f"Do not use `isinstance(x, jaxtyping.{cls.__name__})`. If you want to "
|
"check just the dtype of an array, then use "
|
||||||
"check just the dtype of an array, then use "
|
f'`jaxtyping.{cls.__name__}[jnp.ndarray, "..."]`.'
|
||||||
f'`jaxtyping.{cls.__name__}[jnp.ndarray, "..."]`.'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __getitem__(cls, item: tuple[Any, str]):
|
def __getitem__(cls, item: tuple[Any, str]):
|
||||||
@@ -646,10 +674,12 @@ class AbstractDtype(metaclass=_MetaAbstractDtype):
|
|||||||
_prng_key = "prng_key"
|
_prng_key = "prng_key"
|
||||||
_bool = "bool"
|
_bool = "bool"
|
||||||
_bool_ = "bool_"
|
_bool_ = "bool_"
|
||||||
|
_uint4 = "uint4"
|
||||||
_uint8 = "uint8"
|
_uint8 = "uint8"
|
||||||
_uint16 = "uint16"
|
_uint16 = "uint16"
|
||||||
_uint32 = "uint32"
|
_uint32 = "uint32"
|
||||||
_uint64 = "uint64"
|
_uint64 = "uint64"
|
||||||
|
_int4 = "int4"
|
||||||
_int8 = "int8"
|
_int8 = "int8"
|
||||||
_int16 = "int16"
|
_int16 = "int16"
|
||||||
_int32 = "int32"
|
_int32 = "int32"
|
||||||
@@ -675,10 +705,12 @@ def _make_dtype(_dtypes, name):
|
|||||||
return _Cls
|
return _Cls
|
||||||
|
|
||||||
|
|
||||||
|
UInt4 = _make_dtype(_uint4, "UInt4")
|
||||||
UInt8 = _make_dtype(_uint8, "UInt8")
|
UInt8 = _make_dtype(_uint8, "UInt8")
|
||||||
UInt16 = _make_dtype(_uint16, "UInt16")
|
UInt16 = _make_dtype(_uint16, "UInt16")
|
||||||
UInt32 = _make_dtype(_uint32, "UInt32")
|
UInt32 = _make_dtype(_uint32, "UInt32")
|
||||||
UInt64 = _make_dtype(_uint64, "UInt64")
|
UInt64 = _make_dtype(_uint64, "UInt64")
|
||||||
|
Int4 = _make_dtype(_int4, "Int4")
|
||||||
Int8 = _make_dtype(_int8, "Int8")
|
Int8 = _make_dtype(_int8, "Int8")
|
||||||
Int16 = _make_dtype(_int16, "Int16")
|
Int16 = _make_dtype(_int16, "Int16")
|
||||||
Int32 = _make_dtype(_int32, "Int32")
|
Int32 = _make_dtype(_int32, "Int32")
|
||||||
@@ -691,8 +723,8 @@ Complex64 = _make_dtype(_complex64, "Complex64")
|
|||||||
Complex128 = _make_dtype(_complex128, "Complex128")
|
Complex128 = _make_dtype(_complex128, "Complex128")
|
||||||
|
|
||||||
bools = [_bool, _bool_]
|
bools = [_bool, _bool_]
|
||||||
uints = [_uint8, _uint16, _uint32, _uint64]
|
uints = [_uint4, _uint8, _uint16, _uint32, _uint64]
|
||||||
ints = [_int8, _int16, _int32, _int64]
|
ints = [_int4, _int8, _int16, _int32, _int64]
|
||||||
floats = [_bfloat16, _float16, _float32, _float64]
|
floats = [_bfloat16, _float16, _float32, _float64]
|
||||||
complexes = [_complex64, _complex128]
|
complexes = [_complex64, _complex128]
|
||||||
|
|
||||||
@@ -711,10 +743,4 @@ Num = _make_dtype(uints + ints + floats + complexes, "Num")
|
|||||||
|
|
||||||
Shaped = _make_dtype(_any_dtype, "Shaped")
|
Shaped = _make_dtype(_any_dtype, "Shaped")
|
||||||
|
|
||||||
if has_jax:
|
Key = _make_dtype(_prng_key, "Key")
|
||||||
Key = _make_dtype(_prng_key, "Key")
|
|
||||||
# New-style `jax.random.key` have scalar shape and dtype `key<foo>`.
|
|
||||||
# Old-style `jax.random.PRNGKey` have shape `(2,)` and dtype `uint32`.
|
|
||||||
PRNGKeyArray = Union[Key[jax.Array, ""], UInt32[jax.Array, "2"]]
|
|
||||||
Scalar = Shaped[jax.Array, ""]
|
|
||||||
ScalarLike = Shaped[jax.typing.ArrayLike, ""]
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import os
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
|
def _maybestr2bool(value: Union[bool, str], error: str) -> bool:
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
elif isinstance(value, str):
|
||||||
|
if value.lower() in ("0", "false"):
|
||||||
|
return False
|
||||||
|
elif value.lower() in ("1", "true"):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise ValueError(error)
|
||||||
|
else:
|
||||||
|
raise ValueError(error)
|
||||||
|
|
||||||
|
|
||||||
|
class _JaxtypingConfig:
|
||||||
|
def __init__(self):
|
||||||
|
self.update("jaxtyping_disable", os.environ.get("JAXTYPING_DISABLE", "0"))
|
||||||
|
self.update(
|
||||||
|
"jaxtyping_remove_typechecker_stack",
|
||||||
|
os.environ.get("JAXTYPING_REMOVE_TYPECHECKER_STACK", "0"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def update(self, item: str, value):
|
||||||
|
if item.lower() == "jaxtyping_disable":
|
||||||
|
msg = (
|
||||||
|
"Unrecognised value for `JAXTYPING_DISABLE`. Valid values are "
|
||||||
|
"`JAXTYPING_DISABLE=0` (the default) or `JAXTYPING_DISABLE=1` (to "
|
||||||
|
"disable runtime type checking)."
|
||||||
|
)
|
||||||
|
self.jaxtyping_disable = _maybestr2bool(value, msg)
|
||||||
|
elif item.lower() == "jaxtyping_remove_typechecker_stack":
|
||||||
|
msg = (
|
||||||
|
"Unrecognised value for `JAXTYPING_REMOVE_TYPECHECKER_STACK`. Valid "
|
||||||
|
"values are `JAXTYPING_REMOVE_TYPECHECKER_STACK=0` (the default) or "
|
||||||
|
"`JAXTYPING_REMOVE_TYPECHECKER_STACK=1` (to remove the stack frames "
|
||||||
|
"from the typechecker in `jaxtyped(typechecker=...)`, when it raises a "
|
||||||
|
"runtime type-checking error)."
|
||||||
|
)
|
||||||
|
self.jaxtyping_remove_typechecker_stack = _maybestr2bool(value, msg)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unrecognised config value {item}")
|
||||||
|
|
||||||
|
|
||||||
|
config = _JaxtypingConfig()
|
||||||
+202
-133
@@ -19,46 +19,40 @@
|
|||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import functools as ft
|
import functools as ft
|
||||||
|
import importlib.util
|
||||||
import inspect
|
import inspect
|
||||||
import itertools as it
|
import itertools as it
|
||||||
import sys
|
import sys
|
||||||
import types
|
import warnings
|
||||||
import weakref
|
|
||||||
from typing import Any, get_args, get_origin, get_type_hints, overload
|
from typing import Any, get_args, get_origin, get_type_hints, overload
|
||||||
|
|
||||||
|
from jaxtyping import AbstractArray
|
||||||
|
|
||||||
try:
|
from ._config import config
|
||||||
import jax._src.traceback_util as traceback_util
|
from ._errors import AnnotationError, TypeCheckError
|
||||||
except ImportError:
|
from ._storage import pop_shape_memo, push_shape_memo, shape_str
|
||||||
pass
|
|
||||||
else:
|
|
||||||
traceback_util.register_exclusion(__file__)
|
|
||||||
|
|
||||||
|
|
||||||
from ._storage import pop_shape_memo, push_shape_memo
|
class _Sentinel:
|
||||||
|
def __repr__(self):
|
||||||
|
return "sentinel"
|
||||||
|
|
||||||
|
|
||||||
_jaxtyped_fns = weakref.WeakSet()
|
_sentinel = _Sentinel()
|
||||||
|
_tb_flag = True
|
||||||
|
|
||||||
class TypeCheckError(TypeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
TypeCheckError.__module__ = "jaxtyping" # appears in error messages
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def jaxtyped(*, typechecker=None):
|
def jaxtyped(*, typechecker=_sentinel):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def jaxtyped(fn, *, typechecker=None):
|
def jaxtyped(fn, *, typechecker=_sentinel):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
def jaxtyped(fn=None, *, typechecker=None):
|
def jaxtyped(fn=_sentinel, *, typechecker=_sentinel):
|
||||||
"""Decorate a function with this to perform runtime type-checking of its arguments
|
"""Decorate a function with this to perform runtime type-checking of its arguments
|
||||||
and return value. Decorate a dataclass to perform type-checking of its attributes.
|
and return value. Decorate a dataclass to perform type-checking of its attributes.
|
||||||
|
|
||||||
@@ -80,18 +74,32 @@ def jaxtyped(fn=None, *, typechecker=None):
|
|||||||
return x[:, :, None] * y[:, None, :]
|
return x[:, :, None] * y[:, None, :]
|
||||||
|
|
||||||
# Type-check a dataclass
|
# Type-check a dataclass
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
@jaxtyped(typechecker=typechecker)
|
@jaxtyped(typechecker=typechecker)
|
||||||
@dataclass
|
@dataclass
|
||||||
class MyDataclass:
|
class MyDataclass:
|
||||||
x: int
|
x: int
|
||||||
y: Float[Array "b c"]
|
y: Float[Array, "b c"]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Arguments:**
|
**Arguments:**
|
||||||
|
|
||||||
- `fn`: The function or dataclass to decorate.
|
- `fn`: The function or dataclass to decorate. In practice if you want to use
|
||||||
- `typechecker`: The runtime type-checker to use. This should be a function
|
dataclasses with JAX, then
|
||||||
decorator that will raise an exception if there is a type error, e.g.
|
[`equinox.Module`](https://docs.kidger.site/equinox/api/module/module/) is our
|
||||||
|
recommended approach:
|
||||||
|
```python
|
||||||
|
import equinox as eqx
|
||||||
|
|
||||||
|
@jaxtyped(typechecker=typechecker)
|
||||||
|
class MyModule(eqx.Module):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
- `typechecker`: Keyword-only argument: the runtime type-checker to use. This should
|
||||||
|
be a function decorator that will raise an exception if there is a type error,
|
||||||
|
e.g.
|
||||||
```python
|
```python
|
||||||
@typechecker
|
@typechecker
|
||||||
def f(x: int):
|
def f(x: int):
|
||||||
@@ -104,7 +112,7 @@ def jaxtyped(fn=None, *, typechecker=None):
|
|||||||
skip automatic runtime type-checking, but still support manual `isinstance`
|
skip automatic runtime type-checking, but still support manual `isinstance`
|
||||||
checks inside the function body:
|
checks inside the function body:
|
||||||
```python
|
```python
|
||||||
@jaxtyped
|
@jaxtyped(typechecker=None)
|
||||||
def f(x):
|
def f(x):
|
||||||
assert isinstance(x, Float[Array, "batch channel"])
|
assert isinstance(x, Float[Array, "batch channel"])
|
||||||
```
|
```
|
||||||
@@ -126,10 +134,10 @@ def jaxtyped(fn=None, *, typechecker=None):
|
|||||||
@typechecker
|
@typechecker
|
||||||
def f(...): ...
|
def f(...): ...
|
||||||
```
|
```
|
||||||
This is still supported, but the `jaxtyped(typechecker=typechecker)` syntax
|
This is still supported, but will now raise a warning recommending the
|
||||||
discussed above will produce easier-to-debug error messages. Under the hood, the
|
`jaxtyped(typechecker=typechecker)` syntax discussed above. (Which will produce
|
||||||
new syntax more carefully manipulates the typechecker so as to determine where
|
easier-to-debug error messages: under the hood, the new syntax more carefully
|
||||||
a type-check error arises.
|
manipulates the typechecker so as to determine where a type-check error arises.)
|
||||||
|
|
||||||
??? Info "Notes for advanced users"
|
??? Info "Notes for advanced users"
|
||||||
|
|
||||||
@@ -163,26 +171,88 @@ def jaxtyped(fn=None, *, typechecker=None):
|
|||||||
|
|
||||||
**Decoupling contexts from function calls:**
|
**Decoupling contexts from function calls:**
|
||||||
|
|
||||||
If you would like a new dynamic context *without* calling a new function, then
|
If you would like to call a new function *without* creating a new
|
||||||
`jaxtyped` may be passed the string `"context"` and used as a context manager:
|
dynamic context (and using the same set of axis and structure values), then
|
||||||
|
simply do not add a `jaxtyped` decorator to your inner function, whilst
|
||||||
|
continuing to perform type-checking in whatever way you prefer.
|
||||||
|
|
||||||
|
Conversely, if you would like a new dynamic context *without* calling a new
|
||||||
|
function, then in addition to the usage discussed above, `jaxtyped` also
|
||||||
|
supports being used as a context manager, by passing it the string `"context"`:
|
||||||
```python
|
```python
|
||||||
with jaxtyped("context"):
|
with jaxtyped("context"):
|
||||||
assert isinstance(x, Float[Array, "batch channel"])
|
assert isinstance(x, Float[Array, "batch channel"])
|
||||||
```
|
```
|
||||||
which is equivalent to placing this code inside a new function wrapped in
|
This is equivalent to placing this code inside a new function wrapped in
|
||||||
`jaxtyped(typechecker=None)`. Usage like this is very rare; it's mostly only
|
`jaxtyped(typechecker=None)`. Usage like this is very rare; it's mostly only
|
||||||
useful when working at the global scope.
|
useful when working at the global scope.
|
||||||
|
|
||||||
Conversely, if you would like to call a new function *without* creating a new
|
|
||||||
dynamic context (and using the same set of axis and structure values), then
|
|
||||||
simply do not add a `jaxtyped` decorator to your inner function, whilst
|
|
||||||
continuing to perform type-checking in whatever way you prefer.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if fn is None:
|
global _tb_flag
|
||||||
|
if (
|
||||||
|
_tb_flag
|
||||||
|
and importlib.util.find_spec("jax") is not None
|
||||||
|
and importlib.util.find_spec("jaxlib") is not None
|
||||||
|
and importlib.util.find_spec("jax._src.traceback_util") is not None
|
||||||
|
):
|
||||||
|
import jax._src.traceback_util as traceback_util
|
||||||
|
|
||||||
|
traceback_util.register_exclusion(__file__)
|
||||||
|
_tb_flag = False
|
||||||
|
|
||||||
|
# First handle the `jaxtyped("context")` usage, which is a special case.
|
||||||
|
if fn == "context":
|
||||||
|
if typechecker is not _sentinel:
|
||||||
|
raise ValueError(
|
||||||
|
"Cannot use `jaxtyped` as a context with a typechecker. That is, "
|
||||||
|
"`with jaxtyped('context', typechecker=...):`. is not allowed. In this "
|
||||||
|
"case the type checker does not actually do anything, as there is no "
|
||||||
|
"function to type-check."
|
||||||
|
)
|
||||||
|
return _JaxtypingContext()
|
||||||
|
|
||||||
|
# Now check that a typechecker has been explicitly declared. (Or explicitly declared
|
||||||
|
# as not being used, via `typechecker=None`.)
|
||||||
|
# This is needed just for backward compatibility: an undeclared typechecker
|
||||||
|
# corresponds to the old double-decorator syntax.
|
||||||
|
if typechecker is _sentinel:
|
||||||
|
# This branch will also catch the easy-to-make mistake of
|
||||||
|
# ```python
|
||||||
|
# @jaxtyped(typechecker)
|
||||||
|
# def foo(...):
|
||||||
|
# ```
|
||||||
|
# which is a bug as `typechecker` is interpreted as the function to decorate!
|
||||||
|
warnings.warn(
|
||||||
|
"As of jaxtyping version 0.2.24, jaxtyping now prefers the syntax\n"
|
||||||
|
"```\n"
|
||||||
|
"from jaxtyping import jaxtyped\n"
|
||||||
|
"# Use your favourite typechecker: usually one of the two lines below.\n"
|
||||||
|
"from typeguard import typechecked as typechecker\n"
|
||||||
|
"from beartype import beartype as typechecker\n"
|
||||||
|
"\n"
|
||||||
|
"@jaxtyped(typechecker=typechecker)\n"
|
||||||
|
"def foo(...):\n"
|
||||||
|
"```\n"
|
||||||
|
"and the old double-decorator syntax\n"
|
||||||
|
"```\n"
|
||||||
|
"@jaxtyped\n"
|
||||||
|
"@typechecker\n"
|
||||||
|
"def foo(...):\n"
|
||||||
|
"```\n"
|
||||||
|
"should no longer be used. (It will continue to work as it did before, but "
|
||||||
|
"the new approach will produce more readable error messages.)\n"
|
||||||
|
"In particular note that `typechecker` must be passed via keyword "
|
||||||
|
"argument; the following is not valid:\n"
|
||||||
|
"```\n"
|
||||||
|
"@jaxtyped(typechecker)\n"
|
||||||
|
"def foo(...):\n"
|
||||||
|
"```\n",
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
typechecker = None
|
||||||
|
|
||||||
|
if fn is _sentinel:
|
||||||
return ft.partial(jaxtyped, typechecker=typechecker)
|
return ft.partial(jaxtyped, typechecker=typechecker)
|
||||||
elif type(fn) is types.FunctionType and fn in _jaxtyped_fns:
|
|
||||||
return fn
|
|
||||||
elif inspect.isclass(fn):
|
elif inspect.isclass(fn):
|
||||||
if dataclasses.is_dataclass(fn) and typechecker is not None:
|
if dataclasses.is_dataclass(fn) and typechecker is not None:
|
||||||
# This does not check that the arguments passed to `__init__` match the
|
# This does not check that the arguments passed to `__init__` match the
|
||||||
@@ -235,15 +305,6 @@ def jaxtyped(fn=None, *, typechecker=None):
|
|||||||
else:
|
else:
|
||||||
fdel = jaxtyped(fn.fdel, typechecker=typechecker)
|
fdel = jaxtyped(fn.fdel, typechecker=typechecker)
|
||||||
return property(fget=fget, fset=fset, fdel=fdel)
|
return property(fget=fget, fset=fset, fdel=fdel)
|
||||||
elif fn == "context":
|
|
||||||
if typechecker is not None:
|
|
||||||
raise ValueError(
|
|
||||||
"Cannot use `jaxtyped` as a context with a typechecker. That is, "
|
|
||||||
"`with jaxtyped('context', typechecker=...):`. is not allowed. In this "
|
|
||||||
"case the type checker does not actually do anything, as there is no "
|
|
||||||
"function to type-check."
|
|
||||||
)
|
|
||||||
return _JaxtypingContext()
|
|
||||||
else:
|
else:
|
||||||
if typechecker is None:
|
if typechecker is None:
|
||||||
# Probably being used in the old style as
|
# Probably being used in the old style as
|
||||||
@@ -255,6 +316,27 @@ def jaxtyped(fn=None, *, typechecker=None):
|
|||||||
# in which case make a best-effort attempt to add shape information for any
|
# in which case make a best-effort attempt to add shape information for any
|
||||||
# type errors.
|
# type errors.
|
||||||
|
|
||||||
|
# we want to detect generators, and ignore return annotations on them,
|
||||||
|
# to avoid issues with O(n) typechecking trying to typecheck yielded values
|
||||||
|
wrp = fn
|
||||||
|
while hasattr(wrp, "__wrapped__"):
|
||||||
|
wrp = wrp.__wrapped__
|
||||||
|
|
||||||
|
if inspect.isgeneratorfunction(wrp) or inspect.isasyncgenfunction(wrp):
|
||||||
|
# recursively parse all the annotations, and mark all the jaxtyping
|
||||||
|
# annotations as not needing instance checks, while still being
|
||||||
|
# visible as original ones for the typechecker
|
||||||
|
def modify_annotation(ann):
|
||||||
|
if inspect.isclass(ann) and issubclass(ann, AbstractArray):
|
||||||
|
ann.make_transparent()
|
||||||
|
|
||||||
|
for sub_ann in get_args(ann):
|
||||||
|
modify_annotation(sub_ann)
|
||||||
|
|
||||||
|
# just to make sure: check that fn has valid return annotations
|
||||||
|
if hasattr(fn, "__annotations__") and "return" in fn.__annotations__:
|
||||||
|
modify_annotation(fn.__annotations__["return"])
|
||||||
|
|
||||||
signature = inspect.signature(fn)
|
signature = inspect.signature(fn)
|
||||||
|
|
||||||
@ft.wraps(fn)
|
@ft.wraps(fn)
|
||||||
@@ -264,8 +346,9 @@ def jaxtyped(fn=None, *, typechecker=None):
|
|||||||
try:
|
try:
|
||||||
return fn(*args, **kwargs)
|
return fn(*args, **kwargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# add_note api is support from python 3.11+
|
||||||
if sys.version_info >= (3, 11) and _no_jaxtyping_note(e):
|
if sys.version_info >= (3, 11) and _no_jaxtyping_note(e):
|
||||||
shape_info = _exc_shape_info(memos)
|
shape_info = shape_str(memos)
|
||||||
if shape_info != "":
|
if shape_info != "":
|
||||||
msg = (
|
msg = (
|
||||||
"The preceding error occurred within the scope of a "
|
"The preceding error occurred within the scope of a "
|
||||||
@@ -321,6 +404,9 @@ def jaxtyped(fn=None, *, typechecker=None):
|
|||||||
|
|
||||||
@ft.wraps(fn)
|
@ft.wraps(fn)
|
||||||
def wrapped_fn(*args, **kwargs):
|
def wrapped_fn(*args, **kwargs):
|
||||||
|
if config.jaxtyping_disable:
|
||||||
|
return fn(*args, **kwargs)
|
||||||
|
|
||||||
# Raise bind-time errors before we do any shape analysis. (I.e. skip
|
# Raise bind-time errors before we do any shape analysis. (I.e. skip
|
||||||
# the pointless jaxtyping information for a non-typechecking failure.)
|
# the pointless jaxtyping information for a non-typechecking failure.)
|
||||||
bound = param_signature.bind(*args, **kwargs)
|
bound = param_signature.bind(*args, **kwargs)
|
||||||
@@ -331,26 +417,34 @@ def jaxtyped(fn=None, *, typechecker=None):
|
|||||||
# called.
|
# called.
|
||||||
try:
|
try:
|
||||||
param_fn(*args, **kwargs)
|
param_fn(*args, **kwargs)
|
||||||
|
except AnnotationError:
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if hasattr(e, "_jaxtyping_malformed"):
|
argmsg = _get_problem_arg(
|
||||||
raise
|
param_signature,
|
||||||
|
args,
|
||||||
|
kwargs,
|
||||||
|
bound.arguments,
|
||||||
|
module,
|
||||||
|
typechecker,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
name = fn.__name__
|
||||||
|
except AttributeError:
|
||||||
|
name = fn.__class__.__name__
|
||||||
|
param_values = _pformat(bound.arguments, short_self=True)
|
||||||
|
param_hints = _remove_typing(param_signature)
|
||||||
|
msg = (
|
||||||
|
"Type-check error whilst checking the parameters of "
|
||||||
|
f"{name}.{argmsg}\n"
|
||||||
|
"----------------------\n"
|
||||||
|
f"Called with parameters: {param_values}\n"
|
||||||
|
f"Parameter annotations: {param_hints}.\n"
|
||||||
|
+ shape_str(memos)
|
||||||
|
)
|
||||||
|
if config.jaxtyping_remove_typechecker_stack:
|
||||||
|
raise TypeCheckError(msg) from None
|
||||||
else:
|
else:
|
||||||
argmsg = _get_problem_arg(
|
|
||||||
param_signature, args, kwargs, module, typechecker
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
name = fn.__name__
|
|
||||||
except AttributeError:
|
|
||||||
name = fn.__class__.__name__
|
|
||||||
param_values = _pformat(bound.arguments, short_self=True)
|
|
||||||
param_hints = _remove_typing(param_signature)
|
|
||||||
msg = (
|
|
||||||
"Type-check error whilst checking the parameters of "
|
|
||||||
f"{name}.{argmsg}\n"
|
|
||||||
f"Called with arguments: {param_values}\n"
|
|
||||||
f"Parameter annotations: {param_hints}.\n"
|
|
||||||
+ _exc_shape_info(memos)
|
|
||||||
)
|
|
||||||
raise TypeCheckError(msg) from e
|
raise TypeCheckError(msg) from e
|
||||||
|
|
||||||
# Actually call the function.
|
# Actually call the function.
|
||||||
@@ -374,42 +468,42 @@ def jaxtyped(fn=None, *, typechecker=None):
|
|||||||
kwargs[output_name] = out
|
kwargs[output_name] = out
|
||||||
try:
|
try:
|
||||||
full_fn(*args, **kwargs)
|
full_fn(*args, **kwargs)
|
||||||
|
except AnnotationError:
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if hasattr(e, "_jaxtyping_malformed"):
|
try:
|
||||||
raise
|
name = fn.__name__
|
||||||
|
except AttributeError:
|
||||||
|
name = fn.__class__.__name__
|
||||||
|
param_values = _pformat(bound.arguments, short_self=True)
|
||||||
|
return_value = _pformat(out, short_self=False)
|
||||||
|
param_hints = _remove_typing(param_signature)
|
||||||
|
return_hint = _remove_typing(
|
||||||
|
full_signature.return_annotation
|
||||||
|
)
|
||||||
|
if return_hint.startswith(
|
||||||
|
"<class '"
|
||||||
|
) and return_hint.endswith("'>"):
|
||||||
|
return_hint = return_hint[8:-2]
|
||||||
|
msg = (
|
||||||
|
"Type-check error whilst checking the return value "
|
||||||
|
f"of {name}.\n"
|
||||||
|
f"Actual value: {return_value}\n"
|
||||||
|
f"Expected type: {return_hint}.\n"
|
||||||
|
"----------------------\n"
|
||||||
|
f"Called with parameters: {param_values}\n"
|
||||||
|
f"Parameter annotations: {param_hints}.\n"
|
||||||
|
+ shape_str(memos)
|
||||||
|
)
|
||||||
|
if config.jaxtyping_remove_typechecker_stack:
|
||||||
|
raise TypeCheckError(msg) from None
|
||||||
else:
|
else:
|
||||||
try:
|
|
||||||
name = fn.__name__
|
|
||||||
except AttributeError:
|
|
||||||
name = fn.__class__.__name__
|
|
||||||
param_values = _pformat(
|
|
||||||
bound.arguments, short_self=True
|
|
||||||
)
|
|
||||||
return_value = _pformat(out, short_self=False)
|
|
||||||
param_hints = _remove_typing(param_signature)
|
|
||||||
return_hint = _remove_typing(
|
|
||||||
full_signature.return_annotation
|
|
||||||
)
|
|
||||||
if return_hint.startswith(
|
|
||||||
"<class '"
|
|
||||||
) and return_hint.endswith("'>"):
|
|
||||||
return_hint = return_hint[8:-2]
|
|
||||||
msg = (
|
|
||||||
"Type-check error whilst checking the return value "
|
|
||||||
f"of {name}.\n"
|
|
||||||
f"Called with arguments: {param_values}\n"
|
|
||||||
f"Return value: {return_value}\n"
|
|
||||||
f"Parameter annotations: {param_hints}.\n"
|
|
||||||
f"Return annotation: {return_hint}.\n"
|
|
||||||
+ _exc_shape_info(memos)
|
|
||||||
)
|
|
||||||
raise TypeCheckError(msg) from e
|
raise TypeCheckError(msg) from e
|
||||||
|
|
||||||
return out
|
return out
|
||||||
finally:
|
finally:
|
||||||
pop_shape_memo()
|
pop_shape_memo()
|
||||||
|
|
||||||
_jaxtyped_fns.add(wrapped_fn)
|
|
||||||
return wrapped_fn
|
return wrapped_fn
|
||||||
|
|
||||||
|
|
||||||
@@ -424,7 +518,7 @@ class _JaxtypingContext:
|
|||||||
def _check_dataclass_annotations(self, typechecker):
|
def _check_dataclass_annotations(self, typechecker):
|
||||||
"""Creates and calls a function that checks the attributes of `self`
|
"""Creates and calls a function that checks the attributes of `self`
|
||||||
|
|
||||||
`self` should be a dataclass instancae. `typechecker` should be e.g.
|
`self` should be a dataclass instance. `typechecker` should be e.g.
|
||||||
`beartype.beartype` or `typeguard.typechecked`.
|
`beartype.beartype` or `typeguard.typechecked`.
|
||||||
"""
|
"""
|
||||||
parameters = [inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD)]
|
parameters = [inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD)]
|
||||||
@@ -615,7 +709,7 @@ def _make_argpiece(p, name_to_annotation, name_to_default):
|
|||||||
|
|
||||||
|
|
||||||
def _get_problem_arg(
|
def _get_problem_arg(
|
||||||
param_signature: inspect.Signature, args, kwargs, module, typechecker
|
param_signature: inspect.Signature, args, kwargs, arguments, module, typechecker
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Determines which argument was likely to be the problematic one responsible for
|
"""Determines which argument was likely to be the problematic one responsible for
|
||||||
raising a type-check error.
|
raising a type-check error.
|
||||||
@@ -624,13 +718,17 @@ def _get_problem_arg(
|
|||||||
# anyway.
|
# anyway.
|
||||||
for keep_name in param_signature.parameters.keys():
|
for keep_name in param_signature.parameters.keys():
|
||||||
new_parameters = []
|
new_parameters = []
|
||||||
|
keep_annotation = sentinel = object()
|
||||||
for p_name, p in param_signature.parameters.items():
|
for p_name, p in param_signature.parameters.items():
|
||||||
if p_name == keep_name:
|
if p_name == keep_name:
|
||||||
new_parameters.append(
|
new_parameters.append(
|
||||||
inspect.Parameter(p.name, p.kind, annotation=p.annotation)
|
inspect.Parameter(p.name, p.kind, annotation=p.annotation)
|
||||||
)
|
)
|
||||||
|
assert keep_annotation is sentinel
|
||||||
|
keep_annotation = _remove_typing(p.annotation)
|
||||||
else:
|
else:
|
||||||
new_parameters.append(inspect.Parameter(p.name, p.kind))
|
new_parameters.append(inspect.Parameter(p.name, p.kind))
|
||||||
|
assert keep_annotation is not sentinel
|
||||||
new_signature = inspect.Signature(new_parameters)
|
new_signature = inspect.Signature(new_parameters)
|
||||||
fn = _make_fn_with_signature(
|
fn = _make_fn_with_signature(
|
||||||
"check_single_arg", new_signature, module, output=False
|
"check_single_arg", new_signature, module, output=False
|
||||||
@@ -639,7 +737,12 @@ def _get_problem_arg(
|
|||||||
try:
|
try:
|
||||||
fn(*args, **kwargs)
|
fn(*args, **kwargs)
|
||||||
except Exception:
|
except Exception:
|
||||||
return f"\nThe problem arose whilst typechecking argument '{keep_name}'."
|
keep_value = _pformat(arguments[keep_name], short_self=False)
|
||||||
|
return (
|
||||||
|
f"\nThe problem arose whilst typechecking parameter '{keep_name}'.\n"
|
||||||
|
f"Actual value: {keep_value}\n"
|
||||||
|
f"Expected type: {keep_annotation}."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Could not localise the problem to a single argument -- probably due to
|
# Could not localise the problem to a single argument -- probably due to
|
||||||
# e.g. a mismatched typevar, which each individual argument is okay with.
|
# e.g. a mismatched typevar, which each individual argument is okay with.
|
||||||
@@ -682,40 +785,6 @@ def _pformat(x, short_self: bool):
|
|||||||
return pformat(x)
|
return pformat(x)
|
||||||
|
|
||||||
|
|
||||||
def _exc_shape_info(memos) -> str:
|
|
||||||
"""Gives debug information on the current state of jaxtyping's internal memos.
|
|
||||||
Used in type-checking error messages.
|
|
||||||
"""
|
|
||||||
single_memo, variadic_memo, pytree_memo, _ = memos
|
|
||||||
single_memo = {
|
|
||||||
name: size
|
|
||||||
for name, size in single_memo.items()
|
|
||||||
if not name.startswith("~~delete~~")
|
|
||||||
}
|
|
||||||
variadic_memo = {
|
|
||||||
name: shape
|
|
||||||
for name, (_, shape) in variadic_memo.items()
|
|
||||||
if not name.startswith("~~delete~~")
|
|
||||||
}
|
|
||||||
pieces = []
|
|
||||||
if len(single_memo) > 0 or len(variadic_memo) > 0:
|
|
||||||
pieces.append(
|
|
||||||
"The current values for each jaxtyping axis annotation are as follows."
|
|
||||||
)
|
|
||||||
for name, size in single_memo.items():
|
|
||||||
pieces.append(f"{name}={size}")
|
|
||||||
for name, shape in variadic_memo.items():
|
|
||||||
pieces.append(f"{name}={shape}")
|
|
||||||
if len(pytree_memo) > 0:
|
|
||||||
pieces.append(
|
|
||||||
"The current values for each jaxtyping PyTree structure annotation are as "
|
|
||||||
"follows."
|
|
||||||
)
|
|
||||||
for name, structure in pytree_memo.items():
|
|
||||||
pieces.append(f"{name}={structure}")
|
|
||||||
return "\n".join(pieces)
|
|
||||||
|
|
||||||
|
|
||||||
class _jaxtyping_note_str(str):
|
class _jaxtyping_note_str(str):
|
||||||
"""Used with `_no_jaxtyping_note` to flag that a note came from jaxtyping."""
|
"""Used with `_no_jaxtyping_note` to flag that a note came from jaxtyping."""
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
class TypeCheckError(TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Not inheriting from TypeError as that gets caught and re-reraised as just a TypeError
|
||||||
|
# when using typeguard<3.
|
||||||
|
class AnnotationError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
TypeCheckError.__module__ = "jaxtyping"
|
||||||
|
AnnotationError.__module__ = "jaxtyping"
|
||||||
@@ -358,28 +358,6 @@ def install_import_hook(modules: Union[str, Sequence[str]], typechecker: Optiona
|
|||||||
# so will be hook'd.
|
# so will be hook'd.
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! warning
|
|
||||||
|
|
||||||
Stringified dataclass annotations, e.g.
|
|
||||||
```python
|
|
||||||
@dataclass()
|
|
||||||
class Foo:
|
|
||||||
x: "int"
|
|
||||||
```
|
|
||||||
will be silently skipped without checking them. This is because these are
|
|
||||||
essentially impossible to resolve at runtime. Such stringified annotations
|
|
||||||
typically occur either when using them for forward references, or when using
|
|
||||||
`from __future__ import annotations`. (You should never use the latter, it is
|
|
||||||
largely incompatible with runtime type checking.)
|
|
||||||
|
|
||||||
Partially stringified dataclass annotations, e.g.
|
|
||||||
```python
|
|
||||||
@dataclass()
|
|
||||||
class Foo:
|
|
||||||
x: tuple["int"]
|
|
||||||
```
|
|
||||||
will likely raise an error, and must not be used at all.
|
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
If a function already has any decorators on it, then `@jaxtyped` will get added
|
If a function already has any decorators on it, then `@jaxtyped` will get added
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from typing import (
|
|||||||
Annotated as Float64, # noqa: F401
|
Annotated as Float64, # noqa: F401
|
||||||
Annotated as Inexact, # noqa: F401
|
Annotated as Inexact, # noqa: F401
|
||||||
Annotated as Int, # noqa: F401
|
Annotated as Int, # noqa: F401
|
||||||
|
Annotated as Int4, # noqa: F401
|
||||||
Annotated as Int8, # noqa: F401
|
Annotated as Int8, # noqa: F401
|
||||||
Annotated as Int16, # noqa: F401
|
Annotated as Int16, # noqa: F401
|
||||||
Annotated as Int32, # noqa: F401
|
Annotated as Int32, # noqa: F401
|
||||||
@@ -42,12 +43,18 @@ from typing import (
|
|||||||
Annotated as Real, # noqa: F401
|
Annotated as Real, # noqa: F401
|
||||||
Annotated as Shaped, # noqa: F401
|
Annotated as Shaped, # noqa: F401
|
||||||
Annotated as UInt, # noqa: F401
|
Annotated as UInt, # noqa: F401
|
||||||
|
Annotated as UInt4, # noqa: F401
|
||||||
Annotated as UInt8, # noqa: F401
|
Annotated as UInt8, # noqa: F401
|
||||||
Annotated as UInt16, # noqa: F401
|
Annotated as UInt16, # noqa: F401
|
||||||
Annotated as UInt32, # noqa: F401
|
Annotated as UInt32, # noqa: F401
|
||||||
Annotated as UInt64, # noqa: F401
|
Annotated as UInt64, # noqa: F401
|
||||||
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if not TYPE_CHECKING:
|
||||||
|
assert False
|
||||||
|
|
||||||
from jax import (
|
from jax import (
|
||||||
Array as PRNGKeyArray, # noqa: F401
|
Array as PRNGKeyArray, # noqa: F401
|
||||||
Array as Scalar, # noqa: F401
|
Array as Scalar, # noqa: F401
|
||||||
|
|||||||
@@ -20,7 +20,9 @@
|
|||||||
from ._import_hook import JaxtypingTransformer, Typechecker
|
from ._import_hook import JaxtypingTransformer, Typechecker
|
||||||
|
|
||||||
|
|
||||||
try:
|
def choose_typechecker_magics():
|
||||||
|
# The import is local to avoid degrading import times when the magic is
|
||||||
|
# not needed.
|
||||||
from IPython.core.magic import line_magic, Magics, magics_class
|
from IPython.core.magic import line_magic, Magics, magics_class
|
||||||
|
|
||||||
@magics_class
|
@magics_class
|
||||||
@@ -40,17 +42,15 @@ try:
|
|||||||
JaxtypingTransformer(typechecker=Typechecker(typechecker))
|
JaxtypingTransformer(typechecker=Typechecker(typechecker))
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception:
|
return ChooseTypecheckerMagics
|
||||||
# Very broad exception-handling, as e.g. IPython will sometimes be
|
|
||||||
# present but fail to import for mysterious reasons.
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def load_ipython_extension(ipython):
|
def load_ipython_extension(ipython):
|
||||||
try:
|
try:
|
||||||
ipython.register_magics(ChooseTypecheckerMagics)
|
ChooseTypecheckerMagics = choose_typechecker_magics()
|
||||||
except NameError:
|
except Exception as e:
|
||||||
raise NameError(
|
# Very broad exception-handling, as e.g. IPython will sometimes be
|
||||||
"ChooseTypecheckerMagics is not defined.\n\n"
|
# present but fail to import for mysterious reasons.
|
||||||
+ "You may be trying to use IPython extension without IPython installed."
|
raise RuntimeError("Failed to define jaxtyping.typechecker magic") from e
|
||||||
)
|
|
||||||
|
ipython.register_magics(ChooseTypecheckerMagics)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from typing import Any, Generic, TypeVar
|
|||||||
import jax.tree_util as jtu
|
import jax.tree_util as jtu
|
||||||
import typeguard
|
import typeguard
|
||||||
|
|
||||||
from ._raise import jaxtyping_raise_from
|
from ._errors import AnnotationError
|
||||||
from ._storage import (
|
from ._storage import (
|
||||||
clear_treeflatten_memo,
|
clear_treeflatten_memo,
|
||||||
clear_treepath_memo,
|
clear_treepath_memo,
|
||||||
@@ -141,14 +141,11 @@ class _MetaPyTree(type):
|
|||||||
try:
|
try:
|
||||||
prev_structure = pytree_memo[identifier]
|
prev_structure = pytree_memo[identifier]
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
jaxtyping_raise_from(
|
raise AnnotationError(
|
||||||
NameError(
|
f"Cannot process composite structure '{cls.structure}' "
|
||||||
f"Cannot process composite structure '{cls.structure}' "
|
f"as the structure name {identifier} has not been seen "
|
||||||
f"as the structure name {identifier} has not been seen "
|
"before."
|
||||||
"before."
|
) from e
|
||||||
),
|
|
||||||
e,
|
|
||||||
)
|
|
||||||
# Not using `PyTreeDef.compose` due to JAX bug #18218.
|
# Not using `PyTreeDef.compose` due to JAX bug #18218.
|
||||||
prev_pytree = jtu.tree_unflatten(
|
prev_pytree = jtu.tree_unflatten(
|
||||||
prev_structure, [0] * prev_structure.num_leaves
|
prev_structure, [0] * prev_structure.num_leaves
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
from typing import NoReturn
|
|
||||||
|
|
||||||
|
|
||||||
def jaxtyping_raise(e) -> NoReturn:
|
|
||||||
"""Raises `e`, whilst adding a tag that it should not be intercepted by
|
|
||||||
`TypeCheckError`. All `raise` statements from within `__instancecheck__` should use
|
|
||||||
this.
|
|
||||||
"""
|
|
||||||
__tracebackhide__ = True
|
|
||||||
try:
|
|
||||||
raise e
|
|
||||||
except Exception as f:
|
|
||||||
f._jaxtyping_malformed = True
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def jaxtyping_raise_from(e, e_base) -> NoReturn:
|
|
||||||
__tracebackhide__ = True
|
|
||||||
try:
|
|
||||||
raise e from e_base
|
|
||||||
except Exception as f:
|
|
||||||
f._jaxtyping_malformed = True
|
|
||||||
raise
|
|
||||||
+65
-13
@@ -20,7 +20,7 @@
|
|||||||
import threading
|
import threading
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from ._raise import jaxtyping_raise
|
from ._errors import AnnotationError
|
||||||
|
|
||||||
|
|
||||||
_shape_storage = threading.local()
|
_shape_storage = threading.local()
|
||||||
@@ -71,6 +71,62 @@ def pop_shape_memo() -> None:
|
|||||||
_shape_storage.memo_stack.pop()
|
_shape_storage.memo_stack.pop()
|
||||||
|
|
||||||
|
|
||||||
|
def shape_str(memos) -> str:
|
||||||
|
"""Gives debug information on the current state of jaxtyping's internal memos.
|
||||||
|
Used in type-checking error messages.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
- `memos`: as returned by `get_shape_memo` or `push_shape_memo`.
|
||||||
|
"""
|
||||||
|
single_memo, variadic_memo, pytree_memo, _ = memos
|
||||||
|
single_memo = {
|
||||||
|
name: size
|
||||||
|
for name, size in single_memo.items()
|
||||||
|
if not name.startswith("~~delete~~")
|
||||||
|
}
|
||||||
|
variadic_memo = {
|
||||||
|
name: shape
|
||||||
|
for name, (_, shape) in variadic_memo.items()
|
||||||
|
if not name.startswith("~~delete~~")
|
||||||
|
}
|
||||||
|
pieces = []
|
||||||
|
if len(single_memo) > 0 or len(variadic_memo) > 0:
|
||||||
|
pieces.append(
|
||||||
|
"The current values for each jaxtyping axis annotation are as follows."
|
||||||
|
)
|
||||||
|
for name, size in single_memo.items():
|
||||||
|
pieces.append(f"{name}={size}")
|
||||||
|
for name, shape in variadic_memo.items():
|
||||||
|
pieces.append(f"{name}={shape}")
|
||||||
|
if len(pytree_memo) > 0:
|
||||||
|
pieces.append(
|
||||||
|
"The current values for each jaxtyping PyTree structure annotation are as "
|
||||||
|
"follows."
|
||||||
|
)
|
||||||
|
for name, structure in pytree_memo.items():
|
||||||
|
pieces.append(f"{name}={structure}")
|
||||||
|
return "\n".join(pieces)
|
||||||
|
|
||||||
|
|
||||||
|
def print_bindings():
|
||||||
|
"""Prints the values of the current jaxtyping axis bindings. Intended for debugging.
|
||||||
|
|
||||||
|
That is, whilst doing runtime type checking, so that e.g. the `foo` and `bar` of
|
||||||
|
`Float[Array, "foo bar"]` are assigned values -- this function will print out those
|
||||||
|
values.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
Nothing.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
|
||||||
|
Nothing.
|
||||||
|
"""
|
||||||
|
print(shape_str(get_shape_memo()))
|
||||||
|
|
||||||
|
|
||||||
_treepath_storage = threading.local()
|
_treepath_storage = threading.local()
|
||||||
|
|
||||||
|
|
||||||
@@ -80,12 +136,10 @@ def clear_treepath_memo() -> None:
|
|||||||
|
|
||||||
def set_treepath_memo(index: Optional[int], structure: str) -> None:
|
def set_treepath_memo(index: Optional[int], structure: str) -> None:
|
||||||
if hasattr(_treepath_storage, "value") and _treepath_storage.value is not None:
|
if hasattr(_treepath_storage, "value") and _treepath_storage.value is not None:
|
||||||
jaxtyping_raise(
|
raise AnnotationError(
|
||||||
ValueError(
|
"Cannot typecheck annotations of the form "
|
||||||
"Cannot typecheck annotations of the form "
|
"`PyTree[PyTree[Shaped[Array, '?foo'], 'T'], 'S']` as it is ambiguous "
|
||||||
"`PyTree[PyTree[Shaped[Array, '?foo'], 'T'], 'S']` as it is ambiguous "
|
"which PyTree the `?` annotation refers to."
|
||||||
"which PyTree the `?` annotation refers to."
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if index is None:
|
if index is None:
|
||||||
_treepath_storage.value = f"~~delete~~({structure}) "
|
_treepath_storage.value = f"~~delete~~({structure}) "
|
||||||
@@ -96,12 +150,10 @@ def set_treepath_memo(index: Optional[int], structure: str) -> None:
|
|||||||
|
|
||||||
def get_treepath_memo() -> str:
|
def get_treepath_memo() -> str:
|
||||||
if not hasattr(_treepath_storage, "value") or _treepath_storage.value is None:
|
if not hasattr(_treepath_storage, "value") or _treepath_storage.value is None:
|
||||||
jaxtyping_raise(
|
raise AnnotationError(
|
||||||
ValueError(
|
"Cannot use `?` annotations, e.g. `Shaped[Array, '?foo']`, except "
|
||||||
"Cannot use `?` annotations, e.g. `Shaped[Array, '?foo']`, except "
|
"when contained with structured `PyTree` annotations, e.g. "
|
||||||
"when contained with structured `PyTree` annotations, e.g. "
|
"`PyTree[Shaped[Array, '?foo'], 'T']`."
|
||||||
"`PyTree[Shaped[Array, '?foo'], 'T']`."
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return _treepath_storage.value
|
return _treepath_storage.value
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "jaxtyping"
|
name = "jaxtyping"
|
||||||
version = "0.2.24"
|
version = "0.2.28"
|
||||||
description = "Type annotations and runtime checking for shape and dtype of JAX arrays, and PyTrees."
|
description = "Type annotations and runtime checking for shape and dtype of JAX arrays, and PyTrees."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python ="~=3.9"
|
requires-python ="~=3.9"
|
||||||
@@ -23,7 +23,7 @@ classifiers = [
|
|||||||
"Topic :: Scientific/Engineering :: Mathematics",
|
"Topic :: Scientific/Engineering :: Mathematics",
|
||||||
]
|
]
|
||||||
urls = {repository = "https://github.com/google/jaxtyping" }
|
urls = {repository = "https://github.com/google/jaxtyping" }
|
||||||
dependencies = ["numpy>=1.20.0", "typeguard>=2.13.3,<3", "typing_extensions>=3.7.4.1"]
|
dependencies = ["numpy>=1.20.0", "typeguard==2.13.3"]
|
||||||
entry-points = {pytest11 = {jaxtyping = "jaxtyping._pytest_plugin"}}
|
entry-points = {pytest11 = {jaxtyping = "jaxtyping._pytest_plugin"}}
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
+2
-1
@@ -57,7 +57,8 @@ def jaxtyp(request):
|
|||||||
# def f(...)
|
# def f(...)
|
||||||
def impl(typechecker):
|
def impl(typechecker):
|
||||||
def decorator(fn):
|
def decorator(fn):
|
||||||
return jaxtyping.jaxtyped(typechecker(fn))
|
with pytest.warns(match="As of jaxtyping version 0.2.24"):
|
||||||
|
return jaxtyping.jaxtyped(typechecker(fn))
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ equinox
|
|||||||
IPython
|
IPython
|
||||||
jaxlib
|
jaxlib
|
||||||
pytest
|
pytest
|
||||||
|
pytest-asyncio
|
||||||
tensorflow
|
tensorflow
|
||||||
typeguard<3
|
typeguard<3
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# We have some pretty complicated semantics in `__init__.py`.
|
||||||
|
# Here we check that we didn't miss one of them on our runtime branch.
|
||||||
|
def test_all_importable():
|
||||||
|
# Ordered according to their appearance in the documentation.
|
||||||
|
from jaxtyping import ( # noqa: I001
|
||||||
|
Shaped, # noqa: F401
|
||||||
|
Bool, # noqa: F401
|
||||||
|
Key, # noqa: F401
|
||||||
|
Num, # noqa: F401
|
||||||
|
Inexact, # noqa: F401
|
||||||
|
Float, # noqa: F401
|
||||||
|
BFloat16, # noqa: F401
|
||||||
|
Float16, # noqa: F401
|
||||||
|
Float32, # noqa: F401
|
||||||
|
Float64, # noqa: F401
|
||||||
|
Complex, # noqa: F401
|
||||||
|
Complex64, # noqa: F401
|
||||||
|
Complex128, # noqa: F401
|
||||||
|
Integer, # noqa: F401
|
||||||
|
UInt, # noqa: F401
|
||||||
|
UInt4, # noqa: F401
|
||||||
|
UInt8, # noqa: F401
|
||||||
|
UInt16, # noqa: F401
|
||||||
|
UInt32, # noqa: F401
|
||||||
|
UInt64, # noqa: F401
|
||||||
|
Int, # noqa: F401
|
||||||
|
Int4, # noqa: F401
|
||||||
|
Int8, # noqa: F401
|
||||||
|
Int16, # noqa: F401
|
||||||
|
Int32, # noqa: F401
|
||||||
|
Int64, # noqa: F401
|
||||||
|
Real, # noqa: F401
|
||||||
|
Array, # noqa: F401
|
||||||
|
ArrayLike, # noqa: F401
|
||||||
|
Scalar, # noqa: F401
|
||||||
|
ScalarLike, # noqa: F401
|
||||||
|
PRNGKeyArray, # noqa: F401
|
||||||
|
PyTreeDef, # noqa: F401
|
||||||
|
PyTree, # noqa: F401
|
||||||
|
jaxtyped, # noqa: F401
|
||||||
|
install_import_hook, # noqa: F401
|
||||||
|
AbstractArray, # noqa: F401
|
||||||
|
AbstractDtype, # noqa: F401
|
||||||
|
print_bindings, # noqa: F401
|
||||||
|
get_array_name_format, # noqa: F401
|
||||||
|
set_array_name_format, # noqa: F401
|
||||||
|
)
|
||||||
+15
-3
@@ -25,10 +25,16 @@ import jax.numpy as jnp
|
|||||||
import jax.random as jr
|
import jax.random as jr
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
import torch
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import torch
|
||||||
|
except ImportError:
|
||||||
|
torch = None
|
||||||
|
|
||||||
from jaxtyping import (
|
from jaxtyping import (
|
||||||
AbstractDtype,
|
AbstractDtype,
|
||||||
|
AnnotationError,
|
||||||
Array,
|
Array,
|
||||||
ArrayLike,
|
ArrayLike,
|
||||||
Bool,
|
Bool,
|
||||||
@@ -66,6 +72,7 @@ def test_dtypes():
|
|||||||
Float64,
|
Float64,
|
||||||
Inexact,
|
Inexact,
|
||||||
Int,
|
Int,
|
||||||
|
Int4,
|
||||||
Int8,
|
Int8,
|
||||||
Int16,
|
Int16,
|
||||||
Int32,
|
Int32,
|
||||||
@@ -73,6 +80,7 @@ def test_dtypes():
|
|||||||
Num,
|
Num,
|
||||||
Shaped,
|
Shaped,
|
||||||
UInt,
|
UInt,
|
||||||
|
UInt4,
|
||||||
UInt8,
|
UInt8,
|
||||||
UInt16,
|
UInt16,
|
||||||
UInt32,
|
UInt32,
|
||||||
@@ -124,7 +132,9 @@ def test_any_dtype(jaxtyp, typecheck, getkey):
|
|||||||
|
|
||||||
g(jr.normal(getkey(), (3, 4)))
|
g(jr.normal(getkey(), (3, 4)))
|
||||||
g(jnp.array([[True, False]]))
|
g(jnp.array([[True, False]]))
|
||||||
|
g(jnp.array([[1, 2], [3, 4]], dtype=jnp.int4))
|
||||||
g(jnp.array([[1, 2], [3, 4]], dtype=jnp.int8))
|
g(jnp.array([[1, 2], [3, 4]], dtype=jnp.int8))
|
||||||
|
g(jnp.array([[1, 2], [3, 4]], dtype=jnp.uint4))
|
||||||
g(jnp.array([[1, 2], [3, 4]], dtype=jnp.uint16))
|
g(jnp.array([[1, 2], [3, 4]], dtype=jnp.uint16))
|
||||||
g(jr.normal(getkey(), (3, 4), dtype=jnp.complex128))
|
g(jr.normal(getkey(), (3, 4), dtype=jnp.complex128))
|
||||||
g(jr.normal(getkey(), (3, 4), dtype=jnp.bfloat16))
|
g(jr.normal(getkey(), (3, 4), dtype=jnp.bfloat16))
|
||||||
@@ -448,7 +458,7 @@ def test_incomplete_symbolic(jaxtyp, typecheck, getkey):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
x = jr.normal(getkey(), (4,))
|
x = jr.normal(getkey(), (4,))
|
||||||
with pytest.raises(NameError):
|
with pytest.raises(AnnotationError):
|
||||||
foo(x)
|
foo(x)
|
||||||
|
|
||||||
|
|
||||||
@@ -548,7 +558,9 @@ def test_arraylike(typecheck, getkey):
|
|||||||
def test_subclass():
|
def test_subclass():
|
||||||
assert issubclass(Float[Array, ""], Array)
|
assert issubclass(Float[Array, ""], Array)
|
||||||
assert issubclass(Float[np.ndarray, ""], np.ndarray)
|
assert issubclass(Float[np.ndarray, ""], np.ndarray)
|
||||||
assert issubclass(Float[torch.Tensor, ""], torch.Tensor)
|
|
||||||
|
if torch is not None:
|
||||||
|
assert issubclass(Float[torch.Tensor, ""], torch.Tensor)
|
||||||
|
|
||||||
|
|
||||||
def test_ignored_names():
|
def test_ignored_names():
|
||||||
|
|||||||
+31
-14
@@ -1,57 +1,58 @@
|
|||||||
import abc
|
import abc
|
||||||
|
|
||||||
|
import jax.numpy as jnp
|
||||||
import jax.random as jr
|
import jax.random as jr
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from jaxtyping import Array, Float, jaxtyped
|
from jaxtyping import Array, Float, jaxtyped, print_bindings
|
||||||
|
|
||||||
from .helpers import ParamError, ReturnError
|
from .helpers import ParamError, ReturnError
|
||||||
|
|
||||||
|
|
||||||
class M(metaclass=abc.ABCMeta):
|
class M(metaclass=abc.ABCMeta):
|
||||||
@jaxtyped
|
@jaxtyped(typechecker=None)
|
||||||
def f(self):
|
def f(self):
|
||||||
...
|
...
|
||||||
|
|
||||||
@jaxtyped
|
@jaxtyped(typechecker=None)
|
||||||
@classmethod
|
@classmethod
|
||||||
def g1(cls):
|
def g1(cls):
|
||||||
return 3
|
return 3
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@jaxtyped
|
@jaxtyped(typechecker=None)
|
||||||
def g2(cls):
|
def g2(cls):
|
||||||
return 4
|
return 4
|
||||||
|
|
||||||
@jaxtyped
|
@jaxtyped(typechecker=None)
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def h1():
|
def h1():
|
||||||
return 3
|
return 3
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@jaxtyped
|
@jaxtyped(typechecker=None)
|
||||||
def h2():
|
def h2():
|
||||||
return 4
|
return 4
|
||||||
|
|
||||||
@jaxtyped
|
@jaxtyped(typechecker=None)
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def i1(self):
|
def i1(self):
|
||||||
...
|
...
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@jaxtyped
|
@jaxtyped(typechecker=None)
|
||||||
def i2(self):
|
def i2(self):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
class N:
|
class N:
|
||||||
@jaxtyped
|
@jaxtyped(typechecker=None)
|
||||||
@property
|
@property
|
||||||
def j1(self):
|
def j1(self):
|
||||||
return 3
|
return 3
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@jaxtyped
|
@jaxtyped(typechecker=None)
|
||||||
def j2(self):
|
def j2(self):
|
||||||
return 4
|
return 4
|
||||||
|
|
||||||
@@ -154,13 +155,29 @@ def test_local_stringified_annotation(typecheck):
|
|||||||
|
|
||||||
f(LocalFoo())
|
f(LocalFoo())
|
||||||
|
|
||||||
@jaxtyped
|
with pytest.warns(match="As of jaxtyping version 0.2.24"):
|
||||||
@typecheck
|
|
||||||
def g(x: "LocalFoo") -> "LocalFoo":
|
@jaxtyped
|
||||||
return x
|
@typecheck
|
||||||
|
def g(x: "LocalFoo") -> "LocalFoo":
|
||||||
|
return x
|
||||||
|
|
||||||
g(LocalFoo())
|
g(LocalFoo())
|
||||||
|
|
||||||
# We don't check that errors are raised if it goes wrong, since we can't usually
|
# We don't check that errors are raised if it goes wrong, since we can't usually
|
||||||
# resolve local type annotations at runtime. Best we can hope for is not to raise
|
# resolve local type annotations at runtime. Best we can hope for is not to raise
|
||||||
# a spurious error about not being able to find the type.
|
# a spurious error about not being able to find the type.
|
||||||
|
|
||||||
|
|
||||||
|
def test_print_bindings(typecheck, capfd):
|
||||||
|
@jaxtyped(typechecker=typecheck)
|
||||||
|
def f(x: Float[Array, "foo bar"]):
|
||||||
|
print_bindings()
|
||||||
|
|
||||||
|
capfd.readouterr()
|
||||||
|
f(jnp.zeros((3, 4)))
|
||||||
|
text, _ = capfd.readouterr()
|
||||||
|
assert text == (
|
||||||
|
"The current values for each jaxtyping axis annotation are as follows."
|
||||||
|
"\nfoo=3\nbar=4\n"
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from typing import Tuple, Union
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from jaxtyping import (
|
||||||
|
Array,
|
||||||
|
Float,
|
||||||
|
Float32,
|
||||||
|
Integer,
|
||||||
|
PRNGKeyArray,
|
||||||
|
PyTree,
|
||||||
|
Shaped,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"make_fn",
|
||||||
|
[
|
||||||
|
lambda: Float[Array, "4"],
|
||||||
|
lambda: Float32[Array, ""],
|
||||||
|
lambda: Integer[Array, "1 2 3"],
|
||||||
|
lambda: Shaped[PRNGKeyArray, "2"],
|
||||||
|
lambda: Float[float, "#*shape"],
|
||||||
|
lambda: PyTree[int],
|
||||||
|
lambda: PyTree[Float[Array, ""]],
|
||||||
|
lambda: PyTree[Float32[Array, "*m b c"]],
|
||||||
|
lambda: PyTree[PyTree[Float32[Array, "1 2 b *"]]],
|
||||||
|
lambda: PyTree[Union[str, Float32[Array, "1"]]],
|
||||||
|
lambda: PyTree[
|
||||||
|
Tuple[int, float, Float[Array, ""], PyTree[Union[Float[Array, ""], float]]]
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_equals(make_fn):
|
||||||
|
assert make_fn() == make_fn()
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
from typing import AsyncIterator, Iterator
|
||||||
|
|
||||||
|
import jax.numpy as jnp
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from jaxtyping import Array, Float, Shaped
|
||||||
|
|
||||||
|
from .helpers import ParamError
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import torch
|
||||||
|
except ImportError:
|
||||||
|
torch = None
|
||||||
|
|
||||||
|
|
||||||
|
def test_generators_simple(jaxtyp, typecheck):
|
||||||
|
@jaxtyp(typecheck)
|
||||||
|
def gen(x: Float[Array, "*"]) -> Iterator[Float[Array, "*"]]:
|
||||||
|
yield x
|
||||||
|
|
||||||
|
@jaxtyp(typecheck)
|
||||||
|
def foo():
|
||||||
|
next(gen(jnp.zeros(2)))
|
||||||
|
next(gen(jnp.zeros((3, 4))))
|
||||||
|
|
||||||
|
foo()
|
||||||
|
|
||||||
|
|
||||||
|
def test_generators_return_no_annotations(jaxtyp, typecheck):
|
||||||
|
@jaxtyp(typecheck)
|
||||||
|
def gen(x: Float[Array, "*"]):
|
||||||
|
yield x
|
||||||
|
|
||||||
|
@jaxtyp(typecheck)
|
||||||
|
def foo():
|
||||||
|
next(gen(jnp.zeros(2)))
|
||||||
|
next(gen(jnp.zeros((3, 4))))
|
||||||
|
|
||||||
|
foo()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_async_generators_simple(jaxtyp, typecheck):
|
||||||
|
@jaxtyp(typecheck)
|
||||||
|
async def gen(x: Float[Array, "*"]) -> AsyncIterator[Float[Array, "*"]]:
|
||||||
|
yield x
|
||||||
|
|
||||||
|
@jaxtyp(typecheck)
|
||||||
|
async def foo():
|
||||||
|
async for _ in gen(jnp.zeros(2)):
|
||||||
|
pass
|
||||||
|
async for _ in gen(jnp.zeros((3, 4))):
|
||||||
|
pass
|
||||||
|
|
||||||
|
await foo()
|
||||||
|
|
||||||
|
|
||||||
|
def test_generators_dont_modify_same_annotations(jaxtyp, typecheck):
|
||||||
|
@jaxtyp(typecheck)
|
||||||
|
def g(x: Float[Array, "1"]) -> Iterator[Float[Array, "1"]]:
|
||||||
|
yield x
|
||||||
|
|
||||||
|
@jaxtyp(typecheck)
|
||||||
|
def m(x: Float[Array, "1"]) -> Float[Array, "1"]:
|
||||||
|
return x
|
||||||
|
|
||||||
|
with pytest.raises(ParamError):
|
||||||
|
next(g(jnp.zeros(2)))
|
||||||
|
with pytest.raises(ParamError):
|
||||||
|
m(jnp.zeros(2))
|
||||||
|
|
||||||
|
|
||||||
|
def test_generators_original_issue(jaxtyp, typecheck):
|
||||||
|
# Effectively the same as https://github.com/patrick-kidger/jaxtyping/issues/91
|
||||||
|
if torch is None:
|
||||||
|
pytest.skip("torch is not available")
|
||||||
|
|
||||||
|
@jaxtyp(typecheck)
|
||||||
|
def g(x: Shaped[torch.Tensor, "*"]) -> Iterator[Shaped[torch.Tensor, "*"]]:
|
||||||
|
yield x
|
||||||
|
|
||||||
|
@jaxtyp(typecheck)
|
||||||
|
def f():
|
||||||
|
next(g(torch.zeros(1)))
|
||||||
|
next(g(torch.zeros(2)))
|
||||||
|
|
||||||
|
f()
|
||||||
@@ -14,8 +14,8 @@ def test_arg_localisation(typecheck):
|
|||||||
|
|
||||||
matches = [
|
matches = [
|
||||||
"Type-check error whilst checking the parameters of f",
|
"Type-check error whilst checking the parameters of f",
|
||||||
"The problem arose whilst typechecking argument 'z'.",
|
"The problem arose whilst typechecking parameter 'z'.",
|
||||||
"Called with arguments: {'x': 'hi', 'y': 'bye', 'z': 'not-an-int'}",
|
"Called with parameters: {'x': 'hi', 'y': 'bye', 'z': 'not-an-int'}",
|
||||||
r"Parameter annotations: \(x: str, y: str, z: int\).",
|
r"Parameter annotations: \(x: str, y: str, z: int\).",
|
||||||
]
|
]
|
||||||
for match in matches:
|
for match in matches:
|
||||||
@@ -30,8 +30,8 @@ def test_arg_localisation(typecheck):
|
|||||||
y = jnp.zeros((4, 3))
|
y = jnp.zeros((4, 3))
|
||||||
matches = [
|
matches = [
|
||||||
"Type-check error whilst checking the parameters of g",
|
"Type-check error whilst checking the parameters of g",
|
||||||
"The problem arose whilst typechecking argument 'y'.",
|
"The problem arose whilst typechecking parameter 'y'.",
|
||||||
r"Called with arguments: {'x': f32\[2,3\], 'y': f32\[4,3\]}",
|
r"Called with parameters: {'x': f32\[2,3\], 'y': f32\[4,3\]}",
|
||||||
(
|
(
|
||||||
r"Parameter annotations: \(x: Float\[Array, 'a b'\], y: "
|
r"Parameter annotations: \(x: Float\[Array, 'a b'\], y: "
|
||||||
r"Float\[Array, 'b c'\]\)."
|
r"Float\[Array, 'b c'\]\)."
|
||||||
@@ -54,9 +54,9 @@ def test_return(typecheck):
|
|||||||
y = {"a": 1}
|
y = {"a": 1}
|
||||||
matches = [
|
matches = [
|
||||||
"Type-check error whilst checking the return value of f",
|
"Type-check error whilst checking the return value of f",
|
||||||
r"Called with arguments: {'x': \(1, 2\), 'y': {'a': 1}}",
|
r"Called with parameters: {'x': \(1, 2\), 'y': {'a': 1}}",
|
||||||
"Return value: 'foo'",
|
"Actual value: 'foo'",
|
||||||
r"Return annotation: PyTree\[Any, \"T S\"\].",
|
r"Expected type: PyTree\[Any, \"T S\"\].",
|
||||||
(
|
(
|
||||||
"The current values for each jaxtyping PyTree structure annotation are as "
|
"The current values for each jaxtyping PyTree structure annotation are as "
|
||||||
"follows."
|
"follows."
|
||||||
@@ -82,9 +82,9 @@ def test_dataclass_attribute(typecheck):
|
|||||||
|
|
||||||
matches = [
|
matches = [
|
||||||
"Type-check error whilst checking the parameters of M",
|
"Type-check error whilst checking the parameters of M",
|
||||||
"The problem arose whilst typechecking argument 'z'.",
|
"The problem arose whilst typechecking parameter 'z'.",
|
||||||
(
|
(
|
||||||
r"Called with arguments: {'self': M\(\.\.\.\), 'x': f32\[2,3\], "
|
r"Called with parameters: {'self': M\(\.\.\.\), 'x': f32\[2,3\], "
|
||||||
r"'y': \(1, \(3, 4\)\), 'z': 'not-an-int'}"
|
r"'y': \(1, \(3, 4\)\), 'z': 'not-an-int'}"
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
_py_path = sys.executable
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_jax_dependency():
|
||||||
|
result = subprocess.run(
|
||||||
|
f"{_py_path} -c "
|
||||||
|
"'import jaxtyping; import sys; sys.exit(\"jax\" in sys.modules)'",
|
||||||
|
shell=True,
|
||||||
|
)
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
# Meta-test: test that the above test will work. (i.e. that I haven't messed up using
|
||||||
|
# subprocess.)
|
||||||
|
def test_meta():
|
||||||
|
result = subprocess.run(
|
||||||
|
f"{_py_path} -c 'import jaxtyping; import jax; import sys; "
|
||||||
|
'sys.exit("jax" in sys.modules)\'',
|
||||||
|
shell=True,
|
||||||
|
)
|
||||||
|
assert result.returncode == 1
|
||||||
+3
-3
@@ -26,7 +26,7 @@ import jax.random as jr
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import jaxtyping
|
import jaxtyping
|
||||||
from jaxtyping import Array, Float, PyTree
|
from jaxtyping import AnnotationError, Array, Float, PyTree
|
||||||
|
|
||||||
from .helpers import make_mlp, ParamError
|
from .helpers import make_mlp, ParamError
|
||||||
|
|
||||||
@@ -330,7 +330,7 @@ def test_treepath_dependence_missing_structure_annotation(jaxtyp, typecheck, get
|
|||||||
|
|
||||||
x1 = jr.normal(getkey(), (2,))
|
x1 = jr.normal(getkey(), (2,))
|
||||||
y1 = jr.normal(getkey(), (2,))
|
y1 = jr.normal(getkey(), (2,))
|
||||||
with pytest.raises(ValueError, match="except when contained with structured"):
|
with pytest.raises(AnnotationError, match="except when contained with structured"):
|
||||||
f(x1, y1)
|
f(x1, y1)
|
||||||
|
|
||||||
|
|
||||||
@@ -340,5 +340,5 @@ def test_treepath_dependence_multiple_structure_annotation(jaxtyp, typecheck, ge
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
x1 = jr.normal(getkey(), (2,))
|
x1 = jr.normal(getkey(), (2,))
|
||||||
with pytest.raises(ValueError, match="ambiguous which PyTree"):
|
with pytest.raises(AnnotationError, match="ambiguous which PyTree"):
|
||||||
f(x1)
|
f(x1)
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
import cloudpickle
|
import cloudpickle
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import torch
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import torch
|
||||||
|
except ImportError:
|
||||||
|
torch = None
|
||||||
|
|
||||||
from jaxtyping import AbstractArray, Array, Shaped
|
from jaxtyping import AbstractArray, Array, Shaped
|
||||||
|
|
||||||
|
|
||||||
def test_pickle():
|
def test_pickle():
|
||||||
x = cloudpickle.dumps(Shaped[Array, ""])
|
x = cloudpickle.dumps(Shaped[Array, ""])
|
||||||
y = cloudpickle.dumps(AbstractArray)
|
|
||||||
z = cloudpickle.dumps(Shaped[np.ndarray, ""])
|
|
||||||
w = cloudpickle.dumps(Shaped[torch.Tensor, ""])
|
|
||||||
cloudpickle.loads(x)
|
cloudpickle.loads(x)
|
||||||
|
|
||||||
|
y = cloudpickle.dumps(AbstractArray)
|
||||||
cloudpickle.loads(y)
|
cloudpickle.loads(y)
|
||||||
|
|
||||||
|
z = cloudpickle.dumps(Shaped[np.ndarray, ""])
|
||||||
cloudpickle.loads(z)
|
cloudpickle.loads(z)
|
||||||
cloudpickle.loads(w)
|
|
||||||
|
if torch is not None:
|
||||||
|
w = cloudpickle.dumps(Shaped[torch.Tensor, ""])
|
||||||
|
cloudpickle.loads(w)
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ class _ErrorableThread(threading.Thread):
|
|||||||
|
|
||||||
|
|
||||||
def test_threading_jaxtyped():
|
def test_threading_jaxtyped():
|
||||||
@jaxtyped
|
@jaxtyped(typechecker=typechecked)
|
||||||
@typechecked
|
|
||||||
def add(x: Float[Array, "a b"], y: Float[Array, "a b"]) -> Float[Array, "a b"]:
|
def add(x: Float[Array, "a b"], y: Float[Array, "a b"]) -> Float[Array, "a b"]:
|
||||||
return x + y
|
return x + y
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user