mirror of
https://github.com/wassname/jaxtyping.git
synced 2026-09-09 11:24:55 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c25da278a | ||
|
|
e2f004afd4 | ||
|
|
81c56052e5 | ||
|
|
d911ebb99c | ||
|
|
f30b7d1546 | ||
|
|
4b3f834e12 | ||
|
|
59e8fb0d18 | ||
|
|
7dba3516c2 | ||
|
|
2b1be5eb0a | ||
|
|
7b3d9a2e9a | ||
|
|
29654e7087 | ||
|
|
8fbf7bf3a5 | ||
|
|
a220df9964 | ||
|
|
784aa78f7c | ||
|
|
3f877c0dbb | ||
|
|
607f3c66b5 | ||
|
|
d3651ca70e | ||
|
|
d246e21281 | ||
|
|
165065756f | ||
|
|
dcd73e3431 |
@@ -26,7 +26,7 @@ jobs:
|
||||
run-tests:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [ 3.7, 3.8, 3.9 ]
|
||||
python-version: [ 3.8, 3.9 ]
|
||||
os: [ ubuntu-latest ]
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
force_alphabetical_sort_within_sections=true
|
||||
lines_after_imports=2
|
||||
profile=black
|
||||
combine_as_imports=True
|
||||
treat_comments_as_code=true
|
||||
extra_standard_library=typing_extensions
|
||||
|
||||
@@ -23,13 +23,13 @@ repos:
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/nbQA-dev/nbQA
|
||||
rev: 1.2.3
|
||||
rev: 1.6.3
|
||||
hooks:
|
||||
- id: nbqa-black
|
||||
- id: nbqa-isort
|
||||
- id: nbqa-flake8
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.10.1
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
|
||||
@@ -62,6 +62,8 @@ Float32[Array, "some_shape"]
|
||||
|
||||
The array should typically be a `jaxtyping.Array`, which is an alias for `jax.numpy.ndarray`.
|
||||
|
||||
`jaxtyping.ArrayLike` is also available, which is an alias for `jax.typing.ArrayLike`. This is a union over JAX arrays and the builtin `bool`/`int`/`float`/`complex`.
|
||||
|
||||
But you can use other types as well. `jaxtyping` has support for JAX, NumPy, TensorFlow, and PyTorch, e.g.:
|
||||
```python
|
||||
Float[np.ndarray, "..."]
|
||||
@@ -168,6 +170,8 @@ The import hook can be applied to multiple packages via
|
||||
install_import_hook(["foo", "bar.baz"], ...)
|
||||
```
|
||||
|
||||
The import hook will automatically decorate all functions, and the `__init__` method of dataclasses.
|
||||
|
||||
**Example: writing an end-user script**
|
||||
|
||||
```python
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
Type annotations **and runtime checking** for:
|
||||
|
||||
1. shape and dtype of [JAX](https://github.com/google/jax) arrays;
|
||||
1. shape and dtype of [JAX](https://github.com/google/jax) arrays; *(Now also supports PyTorch, NumPy, and TensorFlow!)*
|
||||
2. [PyTrees](https://jax.readthedocs.io/en/latest/pytrees.html).
|
||||
|
||||
|
||||
**For example:**
|
||||
```python
|
||||
from jaxtyping import Array, Float, PyTree
|
||||
@@ -28,7 +29,9 @@ def accepts_pytree_of_arrays(x: PyTree[Float[Array, "batch c1 c2"]]):
|
||||
pip install jaxtyping
|
||||
```
|
||||
|
||||
Requires JAX 0.3.4+.
|
||||
Requires Python 3.8+.
|
||||
|
||||
JAX is an optional dependency, required for `jaxtyping.{Array, ArrayLike, PyTree}`. If JAX is not installed then these types will not be available, but you may still use jaxtyping alongside PyTorch/NumPy/etc.
|
||||
|
||||
Also install your favourite runtime type-checking package. The two most popular are [typeguard](https://github.com/agronholm/typeguard) (which exhaustively checks every argument) and [beartype](https://github.com/beartype/beartype) (which checks random pieces of arguments).
|
||||
|
||||
@@ -46,14 +49,10 @@ Neural networks: [Equinox](https://github.com/patrick-kidger/equinox).
|
||||
|
||||
Numerical differential equation solvers: [Diffrax](https://github.com/patrick-kidger/diffrax).
|
||||
|
||||
Computer vision models: [Eqxvision](https://github.com/paganpasta/eqxvision).
|
||||
|
||||
SymPy<->JAX conversion; train symbolic expressions via gradient descent: [sympy2jax](https://github.com/google/sympy2jax).
|
||||
|
||||
### Acknowledgements
|
||||
|
||||
Shape annotations + runtime type checking is inspired by [TorchTyping](https://github.com/patrick-kidger/torchtyping).
|
||||
|
||||
The concise syntax is partially inspired by [etils.array_types](https://github.com/google/etils/tree/main/etils/array_types).
|
||||
|
||||
### Disclaimer
|
||||
|
||||
This is not an official Google product.
|
||||
|
||||
+87
-38
@@ -20,47 +20,96 @@
|
||||
import typing
|
||||
|
||||
|
||||
if getattr(typing, "GENERATING_DOCUMENTATION", False):
|
||||
|
||||
class Array:
|
||||
pass
|
||||
|
||||
Array.__module__ = "builtins"
|
||||
try:
|
||||
import jax
|
||||
except ImportError:
|
||||
has_jax = False
|
||||
else:
|
||||
from jax.numpy import ndarray as Array
|
||||
has_jax = True
|
||||
del jax
|
||||
|
||||
|
||||
# Type checkers don't know which branch below will be executed.
|
||||
if typing.TYPE_CHECKING:
|
||||
# For imports, we need to explicitly `import X as X` in order for Pyright to see
|
||||
# them as public. See discussion at https://github.com/microsoft/pyright/issues/2277
|
||||
from jax import Array as Array
|
||||
from jax.typing import ArrayLike as ArrayLike
|
||||
elif has_jax:
|
||||
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
|
||||
|
||||
from .array_types import (
|
||||
AbstractArray,
|
||||
AbstractDtype,
|
||||
BFloat16,
|
||||
Bool,
|
||||
Complex,
|
||||
Complex64,
|
||||
Complex128,
|
||||
Float,
|
||||
Float16,
|
||||
Float32,
|
||||
Float64,
|
||||
get_array_name_format,
|
||||
Inexact,
|
||||
Int,
|
||||
Int8,
|
||||
Int16,
|
||||
Int32,
|
||||
Int64,
|
||||
Integer,
|
||||
Num,
|
||||
set_array_name_format,
|
||||
Shaped,
|
||||
UInt,
|
||||
UInt8,
|
||||
UInt16,
|
||||
UInt32,
|
||||
UInt64,
|
||||
AbstractArray as AbstractArray,
|
||||
AbstractDtype as AbstractDtype,
|
||||
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,
|
||||
get_array_name_format as get_array_name_format,
|
||||
Inexact as Inexact,
|
||||
Int as Int,
|
||||
Int8 as Int8,
|
||||
Int16 as Int16,
|
||||
Int32 as Int32,
|
||||
Int64 as Int64,
|
||||
Integer as Integer,
|
||||
Num as Num,
|
||||
set_array_name_format as set_array_name_format,
|
||||
Shaped as Shaped,
|
||||
UInt as UInt,
|
||||
UInt8 as UInt8,
|
||||
UInt16 as UInt16,
|
||||
UInt32 as UInt32,
|
||||
UInt64 as UInt64,
|
||||
)
|
||||
from .decorator import jaxtyped
|
||||
from .import_hook import install_import_hook
|
||||
from .pytree_type import PyTree
|
||||
from .decorator import jaxtyped as jaxtyped
|
||||
from .import_hook import install_import_hook as install_import_hook
|
||||
|
||||
|
||||
__version__ = "0.2.5"
|
||||
if typing.TYPE_CHECKING:
|
||||
# Set up to deliberately confuse a static type checker.
|
||||
PyTree = getattr(typing, "foo" + "bar")
|
||||
# What's going on with this madness?
|
||||
#
|
||||
# At static-type-checking-time, we want `PyTree` to be a type for which both
|
||||
# `PyTree` and `PyTree[Foo]` are equivalent to `Any`.
|
||||
# (The intention is that `PyTree` be a runtime-only type; there's no real way to
|
||||
# do more with static type checkers.)
|
||||
#
|
||||
# Unfortunately, this isn't possible: `Any` isn't subscriptable. And there's no
|
||||
# equivalent way we can fake this using typing annotations. (In some sense the
|
||||
# closest thing would be a `Protocol[T]` with no methods, but that's actually the
|
||||
# opposite of what we want: that ends up allowing nothing at all.)
|
||||
#
|
||||
# The good news for us is that static type checkers have an internal escape hatch.
|
||||
# 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
|
||||
# annotation, which static type checkers aren't smart enough to resolve.
|
||||
elif has_jax:
|
||||
from .pytree_type import PyTree
|
||||
|
||||
del has_jax
|
||||
|
||||
__version__ = "0.2.13"
|
||||
|
||||
+174
-47
@@ -20,8 +20,17 @@
|
||||
import enum
|
||||
import functools as ft
|
||||
import typing
|
||||
from typing import Any, Dict, List, NoReturn, Optional, Tuple, TYPE_CHECKING, Union
|
||||
from typing_extensions import Literal
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
Literal,
|
||||
NoReturn,
|
||||
Optional,
|
||||
Tuple,
|
||||
TYPE_CHECKING,
|
||||
Union,
|
||||
)
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -58,24 +67,72 @@ class _NamedDim:
|
||||
self.name = name
|
||||
self.broadcastable = broadcastable
|
||||
|
||||
def __eq__(self, other):
|
||||
if type(self) is not type(other):
|
||||
return False
|
||||
if self.name != other.name:
|
||||
return False
|
||||
if self.broadcastable != other.broadcastable:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.broadcastable))
|
||||
|
||||
|
||||
class _NamedVariadicDim:
|
||||
def __init__(self, name, broadcastable):
|
||||
self.name = name
|
||||
self.broadcastable = broadcastable
|
||||
|
||||
def __eq__(self, other):
|
||||
if type(self) is not type(other):
|
||||
return False
|
||||
if self.name != other.name:
|
||||
return False
|
||||
if self.broadcastable != other.broadcastable:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.broadcastable))
|
||||
|
||||
|
||||
class _FixedDim:
|
||||
def __init__(self, size, broadcastable):
|
||||
self.size = size
|
||||
self.broadcastable = broadcastable
|
||||
|
||||
def __eq__(self, other):
|
||||
if type(self) is not type(other):
|
||||
return False
|
||||
if self.size != other.size:
|
||||
return False
|
||||
if self.broadcastable != other.broadcastable:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.size, self.broadcastable))
|
||||
|
||||
|
||||
class _SymbolicDim:
|
||||
def __init__(self, expr, broadcastable):
|
||||
self.expr = expr
|
||||
self.broadcastable = broadcastable
|
||||
|
||||
def __eq__(self, other):
|
||||
if type(self) is not type(other):
|
||||
return False
|
||||
if self.expr != other.expr:
|
||||
return False
|
||||
if self.broadcastable != other.broadcastable:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.expr, self.broadcastable))
|
||||
|
||||
|
||||
_AbstractDimOrVariadicDim = Union[
|
||||
Literal[_anonymous_dim],
|
||||
@@ -127,6 +184,22 @@ def _check_dims(
|
||||
|
||||
|
||||
class _MetaAbstractArray(type):
|
||||
def __eq__(self, other):
|
||||
if type(self) is not type(other):
|
||||
return False
|
||||
if self.array_type is not other.array_type:
|
||||
return False
|
||||
if self.dtypes != other.dtypes:
|
||||
return False
|
||||
if self.dims != other.dims:
|
||||
return False
|
||||
if self.index_variadic != other.index_variadic:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.array_type, self.dtypes, self.dims, self.index_variadic))
|
||||
|
||||
def __instancecheck__(cls, obj):
|
||||
if not isinstance(obj, cls.array_type):
|
||||
return False
|
||||
@@ -234,6 +307,12 @@ class _MetaAbstractArray(type):
|
||||
assert False
|
||||
|
||||
|
||||
def _check_scalar(dtype, dtypes, dims):
|
||||
if len(dims) != 0:
|
||||
return False
|
||||
return (_any_dtype is dtypes) or any(d.startswith(dtype) for d in dtypes)
|
||||
|
||||
|
||||
class AbstractArray(metaclass=_MetaAbstractArray):
|
||||
array_type: Any
|
||||
dtypes: List[str]
|
||||
@@ -383,26 +462,68 @@ class _MetaAbstractDtype(type):
|
||||
elem = compile(elem, "<string>", "eval")
|
||||
elem = _SymbolicDim(elem, broadcastable)
|
||||
dims.append(elem)
|
||||
if _array_name_format == "dtype_and_shape":
|
||||
name = f"{cls.__name__}[{array_type.__name__}, '{dim_str}']"
|
||||
elif _array_name_format == "array":
|
||||
name = array_type.__name__
|
||||
dims = tuple(dims)
|
||||
|
||||
_not_made = object()
|
||||
|
||||
def _make(x):
|
||||
# Allow Python built-in numeric types.
|
||||
# TODO: do something more generic than this? Should we _make all types
|
||||
# that have `shape` and `dtype` attributes or something?
|
||||
if x is bool:
|
||||
if _check_scalar("bool", cls.dtypes, dims):
|
||||
return x
|
||||
else:
|
||||
return _not_made
|
||||
elif x is int:
|
||||
if _check_scalar("int", cls.dtypes, dims):
|
||||
return x
|
||||
else:
|
||||
return _not_made
|
||||
elif x is float:
|
||||
if _check_scalar("float", cls.dtypes, dims):
|
||||
return x
|
||||
else:
|
||||
return _not_made
|
||||
elif x is complex:
|
||||
if _check_scalar("complex", cls.dtypes, dims):
|
||||
return x
|
||||
else:
|
||||
return _not_made
|
||||
try:
|
||||
type_str = x.__name__
|
||||
except AttributeError:
|
||||
type_str = repr(x)
|
||||
if _array_name_format == "dtype_and_shape":
|
||||
name = f"{cls.__name__}[{type_str}, '{dim_str}']"
|
||||
elif _array_name_format == "array":
|
||||
name = type_str
|
||||
else:
|
||||
raise ValueError(
|
||||
f"array_name_format {_array_name_format} not recognised"
|
||||
)
|
||||
out = _MetaAbstractArray(
|
||||
name,
|
||||
(AbstractArray,),
|
||||
dict(
|
||||
array_type=x,
|
||||
dtypes=cls.dtypes,
|
||||
dims=dims,
|
||||
index_variadic=index_variadic,
|
||||
),
|
||||
)
|
||||
if getattr(typing, "GENERATING_DOCUMENTATION", False):
|
||||
out.__module__ = "builtins"
|
||||
else:
|
||||
out.__module__ = "jaxtyping"
|
||||
return out
|
||||
|
||||
if typing.get_origin(array_type) is typing.Union:
|
||||
out = [_make(x) for x in typing.get_args(array_type)]
|
||||
out = tuple(x for x in out if x is not _not_made)
|
||||
out = Union[out]
|
||||
else:
|
||||
raise ValueError(f"array_name_format {_array_name_format} not recognised")
|
||||
out = _MetaAbstractArray(
|
||||
name,
|
||||
(AbstractArray,),
|
||||
dict(
|
||||
array_type=array_type,
|
||||
dtypes=cls.dtypes,
|
||||
dims=dims,
|
||||
index_variadic=index_variadic,
|
||||
),
|
||||
)
|
||||
if getattr(typing, "GENERATING_DOCUMENTATION", False):
|
||||
out.__module__ = "builtins"
|
||||
else:
|
||||
out.__module__ = "jaxtyping"
|
||||
out = _make(array_type)
|
||||
return out
|
||||
|
||||
|
||||
@@ -420,7 +541,9 @@ class AbstractDtype(metaclass=_MetaAbstractDtype):
|
||||
|
||||
dtypes: Union[Literal[_any_dtype], str, List[str]] = cls.dtypes
|
||||
if isinstance(dtypes, str):
|
||||
dtypes = [dtypes]
|
||||
dtypes = (dtypes,)
|
||||
elif dtypes is not _any_dtype:
|
||||
dtypes = tuple(dtypes)
|
||||
cls.dtypes = dtypes
|
||||
|
||||
|
||||
@@ -428,31 +551,34 @@ if TYPE_CHECKING:
|
||||
# Note that `from typing_extensions import Annotated; ... = Annotated`
|
||||
# does not work with static type checkers. `Annotated` is a typeform rather
|
||||
# than a type, meaning it cannot be assigned.
|
||||
from typing_extensions import Annotated as BFloat16
|
||||
from typing_extensions import Annotated as Bool
|
||||
from typing_extensions import Annotated as Complex
|
||||
from typing_extensions import Annotated as Complex64
|
||||
from typing_extensions import Annotated as Complex128
|
||||
from typing_extensions import Annotated as Float
|
||||
from typing_extensions import Annotated as Float16
|
||||
from typing_extensions import Annotated as Float32
|
||||
from typing_extensions import Annotated as Float64
|
||||
from typing_extensions import Annotated as Inexact
|
||||
from typing_extensions import Annotated as Int
|
||||
from typing_extensions import Annotated as Int8
|
||||
from typing_extensions import Annotated as Int16
|
||||
from typing_extensions import Annotated as Int32
|
||||
from typing_extensions import Annotated as Int64
|
||||
from typing_extensions import Annotated as Integer
|
||||
from typing_extensions import Annotated as Num
|
||||
from typing_extensions import Annotated as Shaped
|
||||
from typing_extensions import Annotated as UInt
|
||||
from typing_extensions import Annotated as UInt8
|
||||
from typing_extensions import Annotated as UInt16
|
||||
from typing_extensions import Annotated as UInt32
|
||||
from typing_extensions import Annotated as UInt64
|
||||
from typing_extensions import (
|
||||
Annotated as BFloat16,
|
||||
Annotated as Bool,
|
||||
Annotated as Complex,
|
||||
Annotated as Complex64,
|
||||
Annotated as Complex128,
|
||||
Annotated as Float,
|
||||
Annotated as Float16,
|
||||
Annotated as Float32,
|
||||
Annotated as Float64,
|
||||
Annotated as Inexact,
|
||||
Annotated as Int,
|
||||
Annotated as Int8,
|
||||
Annotated as Int16,
|
||||
Annotated as Int32,
|
||||
Annotated as Int64,
|
||||
Annotated as Integer,
|
||||
Annotated as Num,
|
||||
Annotated as Shaped,
|
||||
Annotated as UInt,
|
||||
Annotated as UInt8,
|
||||
Annotated as UInt16,
|
||||
Annotated as UInt32,
|
||||
Annotated as UInt64,
|
||||
)
|
||||
else:
|
||||
_bool = "bool_"
|
||||
_bool = "bool"
|
||||
_bool_ = "bool_"
|
||||
_uint8 = "uint8"
|
||||
_uint16 = "uint16"
|
||||
_uint32 = "uint32"
|
||||
@@ -495,6 +621,7 @@ else:
|
||||
Complex64 = _make_dtype(_complex64, "Complex64")
|
||||
Complex128 = _make_dtype(_complex128, "Complex128")
|
||||
|
||||
bools = [_bool, _bool_]
|
||||
uints = [_uint8, _uint16, _uint32, _uint64]
|
||||
ints = [_int8, _int16, _int32, _int64]
|
||||
floats = [_bfloat16, _float16, _float32, _float64]
|
||||
@@ -503,7 +630,7 @@ else:
|
||||
# We match NumPy's type hierarachy in what types to provide. See the diagram at
|
||||
# https://numpy.org/doc/stable/reference/arrays.scalars.html#scalars
|
||||
|
||||
Bool = _make_dtype(_bool, "Bool")
|
||||
Bool = _make_dtype(bools, "Bool")
|
||||
UInt = _make_dtype(uints, "UInt")
|
||||
Int = _make_dtype(ints, "Int")
|
||||
Integer = _make_dtype(uints + ints, "Integer")
|
||||
|
||||
+30
-1
@@ -17,7 +17,9 @@
|
||||
# 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.
|
||||
|
||||
import dataclasses
|
||||
import functools as ft
|
||||
import inspect
|
||||
import threading
|
||||
|
||||
|
||||
@@ -44,4 +46,31 @@ class _Jaxtyped:
|
||||
|
||||
|
||||
def jaxtyped(fn):
|
||||
return ft.wraps(fn)(_Jaxtyped(fn))
|
||||
if inspect.isclass(fn): # allow decorators on class definitions
|
||||
if dataclasses.is_dataclass(fn):
|
||||
init = jaxtyped(fn.__init__)
|
||||
fn.__init__ = init
|
||||
return fn
|
||||
else:
|
||||
raise ValueError(
|
||||
"jaxtyped may only be added as a class decorator to dataclasses"
|
||||
)
|
||||
else:
|
||||
return ft.wraps(fn)(_Jaxtyped(fn))
|
||||
|
||||
|
||||
def _jaxtyped_typechecker(typechecker):
|
||||
# typechecker is expected to probably be either `typeguard.typechecked`, or
|
||||
# `beartype.beartype`, or `None`.
|
||||
|
||||
if typechecker is None:
|
||||
typechecker = lambda x: x
|
||||
|
||||
def _wrapper(kls):
|
||||
assert inspect.isclass(kls)
|
||||
if dataclasses.is_dataclass(kls):
|
||||
init = jaxtyped(typechecker(kls.__init__))
|
||||
kls.__init__ = init
|
||||
return kls
|
||||
|
||||
return _wrapper
|
||||
|
||||
+48
-22
@@ -65,7 +65,19 @@ def _call_with_frames_removed(f, *args, **kwargs):
|
||||
|
||||
|
||||
def _optimized_cache_from_source(path, debug_override=None):
|
||||
return cache_from_source(path, debug_override, optimization="jaxtyping")
|
||||
# Version 2: change the position of the `@jaxtyped` decorator, so need a
|
||||
# different name to avoid hitting old __pycache__.
|
||||
# Version 3: now also annotating classes.
|
||||
# Version 4: I'm honestly not sure, but bumping this fixed some kind of odd error.
|
||||
# Maybe I changed something with hte classes part way through version 3?
|
||||
return cache_from_source(path, debug_override, optimization="jaxtyping4")
|
||||
|
||||
|
||||
def _dot_lookup(*elements):
|
||||
out = ast.Name(id=elements[0], ctx=ast.Load())
|
||||
for element in elements[1:]:
|
||||
out = ast.Attribute(out, element, ctx=ast.Load())
|
||||
return out
|
||||
|
||||
|
||||
class _JaxtypingTransformer(ast.NodeVisitor):
|
||||
@@ -95,31 +107,37 @@ class _JaxtypingTransformer(ast.NodeVisitor):
|
||||
self._parents.pop()
|
||||
return node
|
||||
|
||||
def visit_ClassDef(self, node: ast.ClassDef):
|
||||
func = _dot_lookup("jaxtyping", "decorator", "_jaxtyped_typechecker")
|
||||
if self._typechecker is None:
|
||||
args = [ast.Constant(None)]
|
||||
else:
|
||||
args = [_dot_lookup(*self._typechecker)]
|
||||
node.decorator_list.insert(0, ast.Call(func, args, keywords=[]))
|
||||
self._parents.append(node)
|
||||
self.generic_visit(node)
|
||||
self._parents.pop()
|
||||
return node
|
||||
|
||||
def visit_FunctionDef(self, node: ast.FunctionDef):
|
||||
has_annotated_args = any(arg for arg in node.args.args if arg.annotation)
|
||||
has_annotated_return = bool(node.returns)
|
||||
if has_annotated_args or has_annotated_return:
|
||||
# Place at the start of the decorator list, in case a typechecking
|
||||
# annotation has been manually applied; we need to be above that.
|
||||
node.decorator_list.insert(
|
||||
0,
|
||||
ast.Attribute(
|
||||
ast.Name(id="jaxtyping", ctx=ast.Load()), "jaxtyped", ast.Load()
|
||||
),
|
||||
)
|
||||
# Place at the end of the decorator list, as otherwise we wrap e.g.
|
||||
# `jax.custom_{jvp,vjp}` and lose the ability to `defjvp` etc.
|
||||
#
|
||||
# Note that the counter-argument here is that we'd like to place this
|
||||
# at the start of the decorator list, in case a typechecking annotation
|
||||
# has been manually applied, and we'd need to be above that. In this
|
||||
# case we're just going to have to need to ask the user to remove their
|
||||
# typechecking annotation (and let this decorator do it instead).
|
||||
# It's more important we be compatible with normal JAX code.
|
||||
node.decorator_list.append(_dot_lookup("jaxtyping", "jaxtyped"))
|
||||
if self._typechecker is not None:
|
||||
# Place at the end of the decorator list, as decorators
|
||||
# frequently remove annotations from functions and we'd like to
|
||||
# use those annotations.
|
||||
typechecker_module, typechecker_function = self._typechecker
|
||||
node.decorator_list.append(
|
||||
ast.Attribute(
|
||||
ast.Name(id=typechecker_module, ctx=ast.Load()),
|
||||
typechecker_function,
|
||||
ast.Load(),
|
||||
)
|
||||
)
|
||||
|
||||
node.decorator_list.append(_dot_lookup(*self._typechecker))
|
||||
self._parents.append(node)
|
||||
self.generic_visit(node)
|
||||
self._parents.pop()
|
||||
@@ -230,8 +248,16 @@ def install_import_hook(
|
||||
- `typechecker`: the module and function of the typechecker you want to use, as a
|
||||
2-tuple of strings. For example `typechecker=("typeguard", "typechecked")` or
|
||||
`typechecker=("beartype", "beartype")`. You may pass `typechecker=None` if you
|
||||
do not want to automatically decorate with a typechecker as well; e.g. if you
|
||||
have a codebase that already has these decorators.
|
||||
do not want to automatically decorate with a typechecker as well.
|
||||
|
||||
If the function already has any decorators on it, then both the `@jaxtyped` and the
|
||||
typechecker decorators will go at the bottom of the decorator list, e.g.
|
||||
```python
|
||||
@some_other_decorator
|
||||
@jaxtyped
|
||||
@beartype.beartype
|
||||
def foo(...): ...
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
|
||||
@@ -243,8 +269,8 @@ def install_import_hook(
|
||||
```python
|
||||
# entry_point.py
|
||||
from jaxtyped import install_import_hook
|
||||
install_import_hook("main", ("beartype", "beartype"))
|
||||
import main
|
||||
with install_import_hook("main", ("beartype", "beartype"))
|
||||
import main
|
||||
... # do whatever you're doing
|
||||
|
||||
# main.py
|
||||
|
||||
+17
-17
@@ -19,10 +19,9 @@
|
||||
|
||||
import functools as ft
|
||||
import typing
|
||||
from typing import Generic, TYPE_CHECKING, TypeVar
|
||||
from typing_extensions import Protocol
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
import jax
|
||||
import jax.tree_util as jtu
|
||||
import typeguard
|
||||
|
||||
|
||||
@@ -60,6 +59,14 @@ class _MetaPyTree(type):
|
||||
return out
|
||||
|
||||
|
||||
try:
|
||||
# new typeguard
|
||||
_TypeCheckError = (TypeError, typeguard.TypeCheckError)
|
||||
except AttributeError:
|
||||
# old typeguard
|
||||
_TypeCheckError = TypeError
|
||||
|
||||
|
||||
class _MetaSubscriptPyTree(type):
|
||||
def __call__(self, *args, **kwargs):
|
||||
raise RuntimeError("PyTree cannot be instantiated")
|
||||
@@ -78,27 +85,20 @@ class _MetaSubscriptPyTree(type):
|
||||
def is_leaftype(x):
|
||||
try:
|
||||
accepts_leaftype(x)
|
||||
except TypeError:
|
||||
except _TypeCheckError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
leaves = jax.tree_leaves(obj, is_leaf=is_leaftype)
|
||||
leaves = jtu.tree_leaves(obj, is_leaf=is_leaftype)
|
||||
return all(map(is_leaftype, leaves))
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Work around pytype bug #1288
|
||||
# pytype: skip-file
|
||||
class PyTree(Protocol[_T]):
|
||||
pass
|
||||
|
||||
else:
|
||||
PyTree = _MetaPyTree("PyTree", (), {})
|
||||
if getattr(typing, "GENERATING_DOCUMENTATION", False):
|
||||
PyTree.__module__ = "builtins"
|
||||
else:
|
||||
PyTree.__module__ = "jaxtyping"
|
||||
# Can't do `class PyTree(Generic[_T]): ...` because we need to override the
|
||||
# instancecheck for PyTree[foo], but subclassing
|
||||
# `type(Generic[int])`, i.e. `typing._GenericAlias` is disallowed.
|
||||
PyTree = _MetaPyTree("PyTree", (), {})
|
||||
if getattr(typing, "GENERATING_DOCUMENTATION", False):
|
||||
PyTree.__module__ = "builtins"
|
||||
else:
|
||||
PyTree.__module__ = "jaxtyping"
|
||||
|
||||
@@ -63,15 +63,17 @@ classifiers = [
|
||||
"Topic :: Scientific/Engineering :: Mathematics",
|
||||
]
|
||||
|
||||
python_requires = "~=3.7"
|
||||
python_requires = "~=3.8"
|
||||
|
||||
# We use typeguard internally (in a fairly minimal way), but it's not required that
|
||||
# end users make the same choice.
|
||||
# For typing_extensions, we choose versions that match
|
||||
# https://github.com/explosion/confection/blob/main/setup.cfg#L33 used in colab
|
||||
|
||||
install_requires = [
|
||||
"jax>=0.3.4",
|
||||
"numpy>=1.20.0",
|
||||
"typeguard>=2.13.3",
|
||||
"typing_extensions>=4.2.0",
|
||||
"typing_extensions>=3.7.4.1",
|
||||
]
|
||||
|
||||
entry_points = dict(pytest11=["jaxtyping = jaxtyping.pytest_plugin"])
|
||||
|
||||
+19
-4
@@ -18,16 +18,31 @@
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import equinox as eqx
|
||||
import typeguard
|
||||
|
||||
|
||||
ParamError = []
|
||||
ReturnError = []
|
||||
ParamError.append(TypeError) # old typeguard
|
||||
ReturnError.append(TypeError) # old typeguard
|
||||
|
||||
try:
|
||||
# new typeguard
|
||||
ParamError.append(typeguard.TypeCheckError)
|
||||
ReturnError.append(typeguard.TypeCheckError)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import beartype
|
||||
except ImportError:
|
||||
ParamError = TypeError
|
||||
ReturnError = TypeError
|
||||
pass
|
||||
else:
|
||||
ParamError = (TypeError, beartype.roar.BeartypeCallHintParamViolation)
|
||||
ReturnError = (TypeError, beartype.roar.BeartypeCallHintReturnViolation)
|
||||
ParamError.append(beartype.roar.BeartypeCallHintParamViolation)
|
||||
ReturnError.append(beartype.roar.BeartypeCallHintReturnViolation)
|
||||
|
||||
ParamError = tuple(ParamError)
|
||||
ReturnError = tuple(ReturnError)
|
||||
|
||||
|
||||
@eqx.filter_jit
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
# 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.
|
||||
|
||||
import dataclasses
|
||||
|
||||
import equinox as eqx
|
||||
import jax.numpy as jnp
|
||||
import pytest
|
||||
|
||||
@@ -32,3 +35,28 @@ def g(x: Float32[jnp.ndarray, " b"]):
|
||||
g(jnp.array([1.0]))
|
||||
with pytest.raises(ParamError):
|
||||
g(jnp.array(1))
|
||||
|
||||
|
||||
class M(eqx.Module):
|
||||
foo: int
|
||||
bar: Float32[jnp.ndarray, " a"]
|
||||
|
||||
|
||||
M(1, jnp.array([1.0]))
|
||||
with pytest.raises(ParamError):
|
||||
M(1.0, jnp.array([1.0]))
|
||||
with pytest.raises(ParamError):
|
||||
M(1, jnp.array(1.0))
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class D:
|
||||
foo: int
|
||||
bar: Float32[jnp.ndarray, " a"]
|
||||
|
||||
|
||||
D(1, jnp.array([1.0]))
|
||||
with pytest.raises(ParamError):
|
||||
D(1.0, jnp.array([1.0]))
|
||||
with pytest.raises(ParamError):
|
||||
D(1, jnp.array(1.0))
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
# 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.
|
||||
|
||||
import dataclasses
|
||||
|
||||
import equinox as eqx
|
||||
import jax.numpy as jnp
|
||||
import pytest
|
||||
|
||||
@@ -32,3 +35,28 @@ def g(x: Float32[jnp.ndarray, " b"]):
|
||||
g(jnp.array([1.0]))
|
||||
with pytest.raises(ParamError):
|
||||
g(jnp.array(1))
|
||||
|
||||
|
||||
class M(eqx.Module):
|
||||
foo: int
|
||||
bar: Float32[jnp.ndarray, " a"]
|
||||
|
||||
|
||||
M(1, jnp.array([1.0]))
|
||||
with pytest.raises(ParamError):
|
||||
M(1.0, jnp.array([1.0]))
|
||||
with pytest.raises(ParamError):
|
||||
M(1, jnp.array(1.0))
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class D:
|
||||
foo: int
|
||||
bar: Float32[jnp.ndarray, " a"]
|
||||
|
||||
|
||||
D(1, jnp.array([1.0]))
|
||||
with pytest.raises(ParamError):
|
||||
D(1.0, jnp.array([1.0]))
|
||||
with pytest.raises(ParamError):
|
||||
D(1, jnp.array(1.0))
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# 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.
|
||||
|
||||
import equinox as eqx
|
||||
import jax.numpy as jnp
|
||||
import pytest
|
||||
|
||||
@@ -32,3 +33,15 @@ def g(x: Float32[jnp.ndarray, " b"]):
|
||||
g(jnp.array([1.0]))
|
||||
with pytest.raises(ParamError):
|
||||
g(jnp.array(1))
|
||||
|
||||
|
||||
class M(eqx.Module):
|
||||
foo: int
|
||||
bar: Float32[jnp.ndarray, " a"]
|
||||
|
||||
|
||||
M(1, jnp.array([1.0]))
|
||||
with pytest.raises(ParamError):
|
||||
M(1.0, jnp.array([1.0]))
|
||||
with pytest.raises(ParamError):
|
||||
M(1, jnp.array(1.0))
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
# 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.
|
||||
|
||||
import dataclasses
|
||||
|
||||
import equinox as eqx
|
||||
import jax.numpy as jnp
|
||||
import pytest
|
||||
|
||||
@@ -32,3 +35,28 @@ def g(x: Float32[jnp.ndarray, " b"]):
|
||||
g(jnp.array([1.0]))
|
||||
with pytest.raises(ParamError):
|
||||
g(jnp.array(1))
|
||||
|
||||
|
||||
class M(eqx.Module):
|
||||
foo: int
|
||||
bar: Float32[jnp.ndarray, " a"]
|
||||
|
||||
|
||||
M(1, jnp.array([1.0]))
|
||||
with pytest.raises(ParamError):
|
||||
M(1.0, jnp.array([1.0]))
|
||||
with pytest.raises(ParamError):
|
||||
M(1, jnp.array(1.0))
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class D:
|
||||
foo: int
|
||||
bar: Float32[jnp.ndarray, " a"]
|
||||
|
||||
|
||||
D(1, jnp.array([1.0]))
|
||||
with pytest.raises(ParamError):
|
||||
D(1.0, jnp.array([1.0]))
|
||||
with pytest.raises(ParamError):
|
||||
D(1, jnp.array(1.0))
|
||||
|
||||
+55
-1
@@ -17,11 +17,14 @@
|
||||
# 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.
|
||||
|
||||
from typing import get_args, get_origin, Union
|
||||
|
||||
import jax.numpy as jnp
|
||||
import jax.random as jr
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from jaxtyping import AbstractDtype, Array, Float, Float32, jaxtyped, Shaped
|
||||
from jaxtyping import AbstractDtype, Array, ArrayLike, Float, Float32, jaxtyped, Shaped
|
||||
|
||||
from .helpers import ParamError, ReturnError
|
||||
|
||||
@@ -409,3 +412,54 @@ def test_incomplete_symbolic(typecheck, getkey):
|
||||
x = jr.normal(getkey(), (4,))
|
||||
with pytest.raises(NameError):
|
||||
foo(x)
|
||||
|
||||
|
||||
def test_arraylike(typecheck, getkey):
|
||||
floatlike1 = Float32[ArrayLike, ""]
|
||||
floatlike2 = Float[ArrayLike, ""]
|
||||
floatlike3 = Float32[ArrayLike, "4"]
|
||||
|
||||
assert get_origin(floatlike1) is Union
|
||||
assert get_origin(floatlike2) is Union
|
||||
assert get_origin(floatlike3) is Union
|
||||
assert set(get_args(floatlike1)) == {
|
||||
Float32[Array, ""],
|
||||
Float32[np.ndarray, ""],
|
||||
Float32[np.bool_, ""],
|
||||
Float32[np.number, ""],
|
||||
float,
|
||||
}
|
||||
assert set(get_args(floatlike2)) == {
|
||||
Float[Array, ""],
|
||||
Float[np.ndarray, ""],
|
||||
Float[np.bool_, ""],
|
||||
Float[np.number, ""],
|
||||
float,
|
||||
}
|
||||
assert set(get_args(floatlike3)) == {
|
||||
Float32[Array, "4"],
|
||||
Float32[np.ndarray, "4"],
|
||||
Float32[np.bool_, "4"],
|
||||
Float32[np.number, "4"],
|
||||
}
|
||||
|
||||
shaped1 = Shaped[ArrayLike, ""]
|
||||
shaped2 = Shaped[ArrayLike, "4"]
|
||||
assert get_origin(shaped1) is Union
|
||||
assert get_origin(shaped2) is Union
|
||||
assert set(get_args(shaped1)) == {
|
||||
Shaped[Array, ""],
|
||||
Shaped[np.ndarray, ""],
|
||||
Shaped[np.bool_, ""],
|
||||
Shaped[np.number, ""],
|
||||
bool,
|
||||
int,
|
||||
float,
|
||||
complex,
|
||||
}
|
||||
assert set(get_args(shaped2)) == {
|
||||
Shaped[Array, "4"],
|
||||
Shaped[np.ndarray, "4"],
|
||||
Shaped[np.bool_, "4"],
|
||||
Shaped[np.number, "4"],
|
||||
}
|
||||
|
||||
+15
-2
@@ -1,13 +1,26 @@
|
||||
import abc
|
||||
|
||||
from jaxtyping import jaxtyped
|
||||
|
||||
|
||||
class M:
|
||||
class M(metaclass=abc.ABCMeta):
|
||||
@jaxtyped
|
||||
@classmethod
|
||||
def f(cls):
|
||||
return 3
|
||||
|
||||
@jaxtyped
|
||||
@abc.abstractmethod
|
||||
def g(self):
|
||||
...
|
||||
|
||||
|
||||
# Check that the @jaxtyped decorator doesn't blat the __get__ of @classmethod
|
||||
def test_decorator():
|
||||
def test_classmethod():
|
||||
assert M.f() == 3
|
||||
|
||||
|
||||
# Check that the @jaxtyped decorator doesn't blat the __isabstractmethod__ of
|
||||
# @abstractmethod
|
||||
def test_abstractmethod():
|
||||
assert M.g.__isabstractmethod__
|
||||
|
||||
@@ -26,9 +26,8 @@ def test_import_hook_typeguard():
|
||||
hook = install_import_hook(
|
||||
"test.import_hook_tester_typeguard", ("typeguard", "typechecked")
|
||||
)
|
||||
from . import import_hook_tester_typeguard # noqa: F401
|
||||
|
||||
hook.uninstall()
|
||||
with hook:
|
||||
from . import import_hook_tester_typeguard # noqa: F401
|
||||
|
||||
|
||||
def test_import_hook_beartype():
|
||||
@@ -40,24 +39,21 @@ def test_import_hook_beartype():
|
||||
hook = install_import_hook(
|
||||
"test.import_hook_tester_beartype", ("beartype", "beartype")
|
||||
)
|
||||
from . import import_hook_tester_beartype # noqa: F401
|
||||
|
||||
hook.uninstall()
|
||||
with hook:
|
||||
from . import import_hook_tester_beartype # noqa: F401
|
||||
|
||||
|
||||
def test_import_hook_transitive():
|
||||
hook = install_import_hook(
|
||||
"test.import_hook_tester_transitive", ("typeguard", "typechecked")
|
||||
)
|
||||
from . import import_hook_tester_transitive # noqa: F401
|
||||
|
||||
hook.uninstall()
|
||||
with hook:
|
||||
from . import import_hook_tester_transitive # noqa: F401
|
||||
|
||||
|
||||
def test_import_hook_broken_checker():
|
||||
hook = install_import_hook(
|
||||
"test.import_hook_tester_broken_checker", ("jaxtyping", "does_not_exist")
|
||||
)
|
||||
with pytest.raises(AttributeError):
|
||||
with hook, pytest.raises(AttributeError):
|
||||
from . import import_hook_tester_broken_checker # noqa: F401
|
||||
hook.uninstall()
|
||||
|
||||
+31
-1
@@ -17,7 +17,7 @@
|
||||
# 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.
|
||||
|
||||
from typing import Tuple, Union
|
||||
from typing import NamedTuple, Tuple, Union
|
||||
|
||||
import equinox as eqx
|
||||
import jax
|
||||
@@ -155,3 +155,33 @@ def test_pytree_tuple(typecheck):
|
||||
g([1, 1])
|
||||
with pytest.raises(ParamError):
|
||||
g([(1, 1), "hi"])
|
||||
|
||||
|
||||
def test_pytree_namedtuple(typecheck):
|
||||
class CustomNamedTuple(NamedTuple):
|
||||
x: Float[jnp.ndarray, "a b"]
|
||||
y: Float[jnp.ndarray, "b c"]
|
||||
|
||||
class OtherCustomNamedTuple(NamedTuple):
|
||||
x: Float[jnp.ndarray, "a b"]
|
||||
y: Float[jnp.ndarray, "b c"]
|
||||
|
||||
@typecheck
|
||||
def g(x: PyTree[CustomNamedTuple]):
|
||||
...
|
||||
|
||||
g(
|
||||
CustomNamedTuple(
|
||||
x=jax.random.normal(jax.random.PRNGKey(42), (3, 2)),
|
||||
y=jax.random.normal(jax.random.PRNGKey(420), (2, 5)),
|
||||
)
|
||||
)
|
||||
with pytest.raises(ParamError):
|
||||
g(object())
|
||||
with pytest.raises(ParamError):
|
||||
g(
|
||||
OtherCustomNamedTuple(
|
||||
x=jax.random.normal(jax.random.PRNGKey(42), (3, 2)),
|
||||
y=jax.random.normal(jax.random.PRNGKey(420), (2, 5)),
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user