mirror of
https://github.com/wassname/jaxtyping.git
synced 2026-09-09 11:24:55 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59e8fb0d18 | ||
|
|
7dba3516c2 | ||
|
|
2b1be5eb0a | ||
|
|
7b3d9a2e9a | ||
|
|
29654e7087 | ||
|
|
8fbf7bf3a5 | ||
|
|
a220df9964 | ||
|
|
784aa78f7c | ||
|
|
3f877c0dbb | ||
|
|
607f3c66b5 | ||
|
|
d3651ca70e | ||
|
|
d246e21281 | ||
|
|
165065756f | ||
|
|
dcd73e3431 | ||
|
|
f175c7f315 | ||
|
|
da8300ec6c | ||
|
|
39439c2790 | ||
|
|
6202dcc639 |
@@ -2,5 +2,6 @@
|
|||||||
force_alphabetical_sort_within_sections=true
|
force_alphabetical_sort_within_sections=true
|
||||||
lines_after_imports=2
|
lines_after_imports=2
|
||||||
profile=black
|
profile=black
|
||||||
|
combine_as_imports=True
|
||||||
treat_comments_as_code=true
|
treat_comments_as_code=true
|
||||||
extra_standard_library=typing_extensions
|
extra_standard_library=typing_extensions
|
||||||
|
|||||||
@@ -168,6 +168,8 @@ The import hook can be applied to multiple packages via
|
|||||||
install_import_hook(["foo", "bar.baz"], ...)
|
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**
|
**Example: writing an end-user script**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|||||||
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
include LICENSE
|
include jaxtyping/py.typed
|
||||||
prune tests
|
prune test
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ def accepts_pytree_of_arrays(x: PyTree[Float[Array, "batch c1 c2"]]):
|
|||||||
pip install jaxtyping
|
pip install jaxtyping
|
||||||
```
|
```
|
||||||
|
|
||||||
Requires JAX 0.3.4+.
|
|
||||||
|
|
||||||
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).
|
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).
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
@@ -46,6 +44,8 @@ Neural networks: [Equinox](https://github.com/patrick-kidger/equinox).
|
|||||||
|
|
||||||
Numerical differential equation solvers: [Diffrax](https://github.com/patrick-kidger/diffrax).
|
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).
|
SymPy<->JAX conversion; train symbolic expressions via gradient descent: [sympy2jax](https://github.com/google/sympy2jax).
|
||||||
|
|
||||||
### Acknowledgements
|
### Acknowledgements
|
||||||
|
|||||||
+81
-32
@@ -17,40 +17,89 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
from jax.numpy import ndarray as Array
|
import typing
|
||||||
|
import typing_extensions
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import jax
|
||||||
|
except ImportError:
|
||||||
|
has_jax = False
|
||||||
|
else:
|
||||||
|
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
|
||||||
|
elif has_jax:
|
||||||
|
if getattr(typing, "GENERATING_DOCUMENTATION", False):
|
||||||
|
|
||||||
|
class Array:
|
||||||
|
pass
|
||||||
|
|
||||||
|
Array.__module__ = "builtins"
|
||||||
|
else:
|
||||||
|
from jax import Array as Array
|
||||||
|
|
||||||
from .array_types import (
|
from .array_types import (
|
||||||
AbstractArray,
|
AbstractArray as AbstractArray,
|
||||||
AbstractDtype,
|
AbstractDtype as AbstractDtype,
|
||||||
BFloat16,
|
BFloat16 as BFloat16,
|
||||||
Bool,
|
Bool as Bool,
|
||||||
Complex,
|
Complex as Complex,
|
||||||
Complex64,
|
Complex64 as Complex64,
|
||||||
Complex128,
|
Complex128 as Complex128,
|
||||||
Float,
|
Float as Float,
|
||||||
Float16,
|
Float16 as Float16,
|
||||||
Float32,
|
Float32 as Float32,
|
||||||
Float64,
|
Float64 as Float64,
|
||||||
get_array_name_format,
|
get_array_name_format as get_array_name_format,
|
||||||
Inexact,
|
Inexact as Inexact,
|
||||||
Int,
|
Int as Int,
|
||||||
Int8,
|
Int8 as Int8,
|
||||||
Int16,
|
Int16 as Int16,
|
||||||
Int32,
|
Int32 as Int32,
|
||||||
Int64,
|
Int64 as Int64,
|
||||||
Integer,
|
Integer as Integer,
|
||||||
Num,
|
Num as Num,
|
||||||
set_array_name_format,
|
set_array_name_format as set_array_name_format,
|
||||||
Shaped,
|
Shaped as Shaped,
|
||||||
UInt,
|
UInt as UInt,
|
||||||
UInt8,
|
UInt8 as UInt8,
|
||||||
UInt16,
|
UInt16 as UInt16,
|
||||||
UInt32,
|
UInt32 as UInt32,
|
||||||
UInt64,
|
UInt64 as UInt64,
|
||||||
)
|
)
|
||||||
from .decorator import jaxtyped
|
from .decorator import jaxtyped as jaxtyped
|
||||||
from .import_hook import install_import_hook
|
from .import_hook import install_import_hook as install_import_hook
|
||||||
from .pytree_type import PyTree
|
|
||||||
|
|
||||||
|
|
||||||
__version__ = "0.2.2"
|
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.11"
|
||||||
|
|||||||
+51
-37
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
import enum
|
import enum
|
||||||
import functools as ft
|
import functools as ft
|
||||||
|
import typing
|
||||||
from typing import Any, Dict, List, NoReturn, Optional, Tuple, TYPE_CHECKING, Union
|
from typing import Any, Dict, List, NoReturn, Optional, Tuple, TYPE_CHECKING, Union
|
||||||
from typing_extensions import Literal
|
from typing_extensions import Literal
|
||||||
|
|
||||||
@@ -149,25 +150,25 @@ class _MetaAbstractArray(type):
|
|||||||
if cls.dtypes is not _any_dtype and dtype not in cls.dtypes:
|
if cls.dtypes is not _any_dtype and dtype not in cls.dtypes:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if len(storage.memo_stack) == 0:
|
no_temp_memo = hasattr(storage, "memo_stack") and len(storage.memo_stack) != 0
|
||||||
|
|
||||||
|
if no_temp_memo:
|
||||||
|
single_memo, variadic_memo, variadic_broadcast_memo = storage.memo_stack[-1]
|
||||||
|
# Make a copy so we don't mutate the original memo during the shape check.
|
||||||
|
single_memo = single_memo.copy()
|
||||||
|
variadic_memo = variadic_memo.copy()
|
||||||
|
variadic_broadcast_memo = variadic_broadcast_memo.copy()
|
||||||
|
else:
|
||||||
# `isinstance` happening outside any @jaxtyped decorators, e.g. at the
|
# `isinstance` happening outside any @jaxtyped decorators, e.g. at the
|
||||||
# global scope. In this case just create a temporary memo, since we're not
|
# global scope. In this case just create a temporary memo, since we're not
|
||||||
# going to be comparing against any stored values anyway.
|
# going to be comparing against any stored values anyway.
|
||||||
single_memo = {}
|
single_memo = {}
|
||||||
variadic_memo = {}
|
variadic_memo = {}
|
||||||
variadic_broadcast_memo = {}
|
variadic_broadcast_memo = {}
|
||||||
temp_memo = True
|
|
||||||
else:
|
|
||||||
single_memo, variadic_memo, variadic_broadcast_memo = storage.memo_stack[-1]
|
|
||||||
# Make a copy so we don't mutate the original memo during the shape check.
|
|
||||||
single_memo = single_memo.copy()
|
|
||||||
variadic_memo = variadic_memo.copy()
|
|
||||||
variadic_broadcast_memo = variadic_broadcast_memo.copy()
|
|
||||||
temp_memo = False
|
|
||||||
|
|
||||||
if cls._check_shape(obj, single_memo, variadic_memo, variadic_broadcast_memo):
|
if cls._check_shape(obj, single_memo, variadic_memo, variadic_broadcast_memo):
|
||||||
# We update the memo every time we successfully pass a shape check
|
# We update the memo every time we successfully pass a shape check
|
||||||
if not temp_memo:
|
if no_temp_memo:
|
||||||
storage.memo_stack[-1] = (
|
storage.memo_stack[-1] = (
|
||||||
single_memo,
|
single_memo,
|
||||||
variadic_memo,
|
variadic_memo,
|
||||||
@@ -382,10 +383,15 @@ class _MetaAbstractDtype(type):
|
|||||||
elem = compile(elem, "<string>", "eval")
|
elem = compile(elem, "<string>", "eval")
|
||||||
elem = _SymbolicDim(elem, broadcastable)
|
elem = _SymbolicDim(elem, broadcastable)
|
||||||
dims.append(elem)
|
dims.append(elem)
|
||||||
|
# In python 3.8, e.g., typing.Union lacks `__name__`.
|
||||||
|
try:
|
||||||
|
type_str = array_type.__name__
|
||||||
|
except AttributeError:
|
||||||
|
type_str = repr(array_type)
|
||||||
if _array_name_format == "dtype_and_shape":
|
if _array_name_format == "dtype_and_shape":
|
||||||
name = f"{cls.__name__}[{array_type.__name__}, '{dim_str}']"
|
name = f"{cls.__name__}[{type_str}, '{dim_str}']"
|
||||||
elif _array_name_format == "array":
|
elif _array_name_format == "array":
|
||||||
name = "Array"
|
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")
|
||||||
out = _MetaAbstractArray(
|
out = _MetaAbstractArray(
|
||||||
@@ -398,7 +404,10 @@ class _MetaAbstractDtype(type):
|
|||||||
index_variadic=index_variadic,
|
index_variadic=index_variadic,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
out.__module__ = "jaxtyping"
|
if getattr(typing, "GENERATING_DOCUMENTATION", False):
|
||||||
|
out.__module__ = "builtins"
|
||||||
|
else:
|
||||||
|
out.__module__ = "jaxtyping"
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -424,29 +433,31 @@ if TYPE_CHECKING:
|
|||||||
# Note that `from typing_extensions import Annotated; ... = Annotated`
|
# Note that `from typing_extensions import Annotated; ... = Annotated`
|
||||||
# does not work with static type checkers. `Annotated` is a typeform rather
|
# does not work with static type checkers. `Annotated` is a typeform rather
|
||||||
# than a type, meaning it cannot be assigned.
|
# than a type, meaning it cannot be assigned.
|
||||||
from typing_extensions import Annotated as BFloat16
|
from typing_extensions import (
|
||||||
from typing_extensions import Annotated as Bool
|
Annotated as BFloat16,
|
||||||
from typing_extensions import Annotated as Complex
|
Annotated as Bool,
|
||||||
from typing_extensions import Annotated as Complex64
|
Annotated as Complex,
|
||||||
from typing_extensions import Annotated as Complex128
|
Annotated as Complex64,
|
||||||
from typing_extensions import Annotated as Float
|
Annotated as Complex128,
|
||||||
from typing_extensions import Annotated as Float16
|
Annotated as Float,
|
||||||
from typing_extensions import Annotated as Float32
|
Annotated as Float16,
|
||||||
from typing_extensions import Annotated as Float64
|
Annotated as Float32,
|
||||||
from typing_extensions import Annotated as Inexact
|
Annotated as Float64,
|
||||||
from typing_extensions import Annotated as Int
|
Annotated as Inexact,
|
||||||
from typing_extensions import Annotated as Int8
|
Annotated as Int,
|
||||||
from typing_extensions import Annotated as Int16
|
Annotated as Int8,
|
||||||
from typing_extensions import Annotated as Int32
|
Annotated as Int16,
|
||||||
from typing_extensions import Annotated as Int64
|
Annotated as Int32,
|
||||||
from typing_extensions import Annotated as Integer
|
Annotated as Int64,
|
||||||
from typing_extensions import Annotated as Num
|
Annotated as Integer,
|
||||||
from typing_extensions import Annotated as Shaped
|
Annotated as Num,
|
||||||
from typing_extensions import Annotated as UInt
|
Annotated as Shaped,
|
||||||
from typing_extensions import Annotated as UInt8
|
Annotated as UInt,
|
||||||
from typing_extensions import Annotated as UInt16
|
Annotated as UInt8,
|
||||||
from typing_extensions import Annotated as UInt32
|
Annotated as UInt16,
|
||||||
from typing_extensions import Annotated as UInt64
|
Annotated as UInt32,
|
||||||
|
Annotated as UInt64,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
_bool = "bool_"
|
_bool = "bool_"
|
||||||
_uint8 = "uint8"
|
_uint8 = "uint8"
|
||||||
@@ -470,7 +481,10 @@ else:
|
|||||||
|
|
||||||
_Cls.__name__ = name
|
_Cls.__name__ = name
|
||||||
_Cls.__qualname__ = name
|
_Cls.__qualname__ = name
|
||||||
_Cls.__module__ = "jaxtyping"
|
if getattr(typing, "GENERATING_DOCUMENTATION", False):
|
||||||
|
_Cls.__module__ = "builtins"
|
||||||
|
else:
|
||||||
|
_Cls.__module__ = "jaxtyping"
|
||||||
return _Cls
|
return _Cls
|
||||||
|
|
||||||
UInt8 = _make_dtype(_uint8, "UInt8")
|
UInt8 = _make_dtype(_uint8, "UInt8")
|
||||||
|
|||||||
+30
-1
@@ -17,7 +17,9 @@
|
|||||||
# 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 dataclasses
|
||||||
import functools as ft
|
import functools as ft
|
||||||
|
import inspect
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
|
||||||
@@ -44,4 +46,31 @@ class _Jaxtyped:
|
|||||||
|
|
||||||
|
|
||||||
def jaxtyped(fn):
|
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
|
||||||
|
|||||||
+46
-22
@@ -65,7 +65,17 @@ def _call_with_frames_removed(f, *args, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
def _optimized_cache_from_source(path, debug_override=None):
|
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.
|
||||||
|
return cache_from_source(path, debug_override, optimization="jaxtyping3")
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
class _JaxtypingTransformer(ast.NodeVisitor):
|
||||||
@@ -95,31 +105,37 @@ class _JaxtypingTransformer(ast.NodeVisitor):
|
|||||||
self._parents.pop()
|
self._parents.pop()
|
||||||
return node
|
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.append(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):
|
def visit_FunctionDef(self, node: ast.FunctionDef):
|
||||||
has_annotated_args = any(arg for arg in node.args.args if arg.annotation)
|
has_annotated_args = any(arg for arg in node.args.args if arg.annotation)
|
||||||
has_annotated_return = bool(node.returns)
|
has_annotated_return = bool(node.returns)
|
||||||
if has_annotated_args or has_annotated_return:
|
if has_annotated_args or has_annotated_return:
|
||||||
# Place at the start of the decorator list, in case a typechecking
|
# Place at the end of the decorator list, as otherwise we wrap e.g.
|
||||||
# annotation has been manually applied; we need to be above that.
|
# `jax.custom_{jvp,vjp}` and lose the ability to `defjvp` etc.
|
||||||
node.decorator_list.insert(
|
#
|
||||||
0,
|
# Note that the counter-argument here is that we'd like to place this
|
||||||
ast.Attribute(
|
# at the start of the decorator list, in case a typechecking annotation
|
||||||
ast.Name(id="jaxtyping", ctx=ast.Load()), "jaxtyped", ast.Load()
|
# 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:
|
if self._typechecker is not None:
|
||||||
# Place at the end of the decorator list, as decorators
|
# Place at the end of the decorator list, as decorators
|
||||||
# frequently remove annotations from functions and we'd like to
|
# frequently remove annotations from functions and we'd like to
|
||||||
# use those annotations.
|
# use those annotations.
|
||||||
typechecker_module, typechecker_function = self._typechecker
|
node.decorator_list.append(_dot_lookup(*self._typechecker))
|
||||||
node.decorator_list.append(
|
|
||||||
ast.Attribute(
|
|
||||||
ast.Name(id=typechecker_module, ctx=ast.Load()),
|
|
||||||
typechecker_function,
|
|
||||||
ast.Load(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self._parents.append(node)
|
self._parents.append(node)
|
||||||
self.generic_visit(node)
|
self.generic_visit(node)
|
||||||
self._parents.pop()
|
self._parents.pop()
|
||||||
@@ -230,8 +246,16 @@ def install_import_hook(
|
|||||||
- `typechecker`: the module and function of the typechecker you want to use, as a
|
- `typechecker`: the module and function of the typechecker you want to use, as a
|
||||||
2-tuple of strings. For example `typechecker=("typeguard", "typechecked")` or
|
2-tuple of strings. For example `typechecker=("typeguard", "typechecked")` or
|
||||||
`typechecker=("beartype", "beartype")`. You may pass `typechecker=None` if you
|
`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
|
do not want to automatically decorate with a typechecker as well.
|
||||||
have a codebase that already has these decorators.
|
|
||||||
|
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:**
|
**Returns:**
|
||||||
|
|
||||||
@@ -243,8 +267,8 @@ def install_import_hook(
|
|||||||
```python
|
```python
|
||||||
# entry_point.py
|
# entry_point.py
|
||||||
from jaxtyped import install_import_hook
|
from jaxtyped import install_import_hook
|
||||||
install_import_hook("main", ("beartype", "beartype"))
|
with install_import_hook("main", ("beartype", "beartype"))
|
||||||
import main
|
import main
|
||||||
... # do whatever you're doing
|
... # do whatever you're doing
|
||||||
|
|
||||||
# main.py
|
# main.py
|
||||||
|
|||||||
@@ -18,9 +18,10 @@
|
|||||||
# 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 functools as ft
|
||||||
|
import typing
|
||||||
from typing import Generic, TypeVar
|
from typing import Generic, TypeVar
|
||||||
|
|
||||||
import jax
|
import jax.tree_util as jtu
|
||||||
import typeguard
|
import typeguard
|
||||||
|
|
||||||
|
|
||||||
@@ -51,10 +52,21 @@ class _MetaPyTree(type):
|
|||||||
def __getitem__(cls, item):
|
def __getitem__(cls, item):
|
||||||
name = str(_FakePyTree[item])
|
name = str(_FakePyTree[item])
|
||||||
out = _MetaSubscriptPyTree(name, (), {"leaftype": item})
|
out = _MetaSubscriptPyTree(name, (), {"leaftype": item})
|
||||||
out.__module__ = "jaxtyping"
|
if getattr(typing, "GENERATING_DOCUMENTATION", False):
|
||||||
|
out.__module__ = "builtins"
|
||||||
|
else:
|
||||||
|
out.__module__ = "jaxtyping"
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# new typeguard
|
||||||
|
_TypeCheckError = (TypeError, typeguard.TypeCheckError)
|
||||||
|
except AttributeError:
|
||||||
|
# old typeguard
|
||||||
|
_TypeCheckError = TypeError
|
||||||
|
|
||||||
|
|
||||||
class _MetaSubscriptPyTree(type):
|
class _MetaSubscriptPyTree(type):
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
raise RuntimeError("PyTree cannot be instantiated")
|
raise RuntimeError("PyTree cannot be instantiated")
|
||||||
@@ -73,17 +85,20 @@ class _MetaSubscriptPyTree(type):
|
|||||||
def is_leaftype(x):
|
def is_leaftype(x):
|
||||||
try:
|
try:
|
||||||
accepts_leaftype(x)
|
accepts_leaftype(x)
|
||||||
except TypeError:
|
except _TypeCheckError:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
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))
|
return all(map(is_leaftype, leaves))
|
||||||
|
|
||||||
|
|
||||||
PyTree = _MetaPyTree("PyTree", (), {})
|
|
||||||
PyTree.__module__ = "jaxtyping"
|
|
||||||
# Can't do `class PyTree(Generic[_T]): ...` because we need to override the
|
# Can't do `class PyTree(Generic[_T]): ...` because we need to override the
|
||||||
# instancecheck for PyTree[foo], but we subclassing
|
# instancecheck for PyTree[foo], but subclassing
|
||||||
# `type(Generic[int])`, i.e. `typing._GenericAlias` is disallowed.
|
# `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"
|
||||||
|
|||||||
@@ -67,11 +67,13 @@ python_requires = "~=3.7"
|
|||||||
|
|
||||||
# We use typeguard internally (in a fairly minimal way), but it's not required that
|
# We use typeguard internally (in a fairly minimal way), but it's not required that
|
||||||
# end users make the same choice.
|
# 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 = [
|
install_requires = [
|
||||||
"jax>=0.3.4",
|
|
||||||
"numpy>=1.20.0",
|
"numpy>=1.20.0",
|
||||||
"typeguard>=2.13.3",
|
"typeguard>=2.13.3",
|
||||||
"typing_extensions>=4.2.0",
|
"typing_extensions>=3.7.4.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
entry_points = dict(pytest11=["jaxtyping = jaxtyping.pytest_plugin"])
|
entry_points = dict(pytest11=["jaxtyping = jaxtyping.pytest_plugin"])
|
||||||
@@ -94,4 +96,5 @@ setuptools.setup(
|
|||||||
install_requires=install_requires,
|
install_requires=install_requires,
|
||||||
entry_points=entry_points,
|
entry_points=entry_points,
|
||||||
packages=[name],
|
packages=[name],
|
||||||
|
include_package_data=True,
|
||||||
)
|
)
|
||||||
|
|||||||
+19
-4
@@ -18,16 +18,31 @@
|
|||||||
# 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 equinox as eqx
|
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:
|
try:
|
||||||
import beartype
|
import beartype
|
||||||
except ImportError:
|
except ImportError:
|
||||||
ParamError = TypeError
|
pass
|
||||||
ReturnError = TypeError
|
|
||||||
else:
|
else:
|
||||||
ParamError = (TypeError, beartype.roar.BeartypeCallHintParamViolation)
|
ParamError.append(beartype.roar.BeartypeCallHintParamViolation)
|
||||||
ReturnError = (TypeError, beartype.roar.BeartypeCallHintReturnViolation)
|
ReturnError.append(beartype.roar.BeartypeCallHintReturnViolation)
|
||||||
|
|
||||||
|
ParamError = tuple(ParamError)
|
||||||
|
ReturnError = tuple(ReturnError)
|
||||||
|
|
||||||
|
|
||||||
@eqx.filter_jit
|
@eqx.filter_jit
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
# 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 equinox as eqx
|
||||||
import jax.numpy as jnp
|
import jax.numpy as jnp
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -32,3 +33,15 @@ def g(x: Float32[jnp.ndarray, " b"]):
|
|||||||
g(jnp.array([1.0]))
|
g(jnp.array([1.0]))
|
||||||
with pytest.raises(ParamError):
|
with pytest.raises(ParamError):
|
||||||
g(jnp.array(1))
|
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,7 @@
|
|||||||
# 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 equinox as eqx
|
||||||
import jax.numpy as jnp
|
import jax.numpy as jnp
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -32,3 +33,15 @@ def g(x: Float32[jnp.ndarray, " b"]):
|
|||||||
g(jnp.array([1.0]))
|
g(jnp.array([1.0]))
|
||||||
with pytest.raises(ParamError):
|
with pytest.raises(ParamError):
|
||||||
g(jnp.array(1))
|
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,7 @@
|
|||||||
# 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 equinox as eqx
|
||||||
import jax.numpy as jnp
|
import jax.numpy as jnp
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -32,3 +33,15 @@ def g(x: Float32[jnp.ndarray, " b"]):
|
|||||||
g(jnp.array([1.0]))
|
g(jnp.array([1.0]))
|
||||||
with pytest.raises(ParamError):
|
with pytest.raises(ParamError):
|
||||||
g(jnp.array(1))
|
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,7 @@
|
|||||||
# 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 equinox as eqx
|
||||||
import jax.numpy as jnp
|
import jax.numpy as jnp
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -32,3 +33,15 @@ def g(x: Float32[jnp.ndarray, " b"]):
|
|||||||
g(jnp.array([1.0]))
|
g(jnp.array([1.0]))
|
||||||
with pytest.raises(ParamError):
|
with pytest.raises(ParamError):
|
||||||
g(jnp.array(1))
|
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))
|
||||||
|
|||||||
@@ -26,9 +26,8 @@ def test_import_hook_typeguard():
|
|||||||
hook = install_import_hook(
|
hook = install_import_hook(
|
||||||
"test.import_hook_tester_typeguard", ("typeguard", "typechecked")
|
"test.import_hook_tester_typeguard", ("typeguard", "typechecked")
|
||||||
)
|
)
|
||||||
from . import import_hook_tester_typeguard # noqa: F401
|
with hook:
|
||||||
|
from . import import_hook_tester_typeguard # noqa: F401
|
||||||
hook.uninstall()
|
|
||||||
|
|
||||||
|
|
||||||
def test_import_hook_beartype():
|
def test_import_hook_beartype():
|
||||||
@@ -40,24 +39,21 @@ def test_import_hook_beartype():
|
|||||||
hook = install_import_hook(
|
hook = install_import_hook(
|
||||||
"test.import_hook_tester_beartype", ("beartype", "beartype")
|
"test.import_hook_tester_beartype", ("beartype", "beartype")
|
||||||
)
|
)
|
||||||
from . import import_hook_tester_beartype # noqa: F401
|
with hook:
|
||||||
|
from . import import_hook_tester_beartype # noqa: F401
|
||||||
hook.uninstall()
|
|
||||||
|
|
||||||
|
|
||||||
def test_import_hook_transitive():
|
def test_import_hook_transitive():
|
||||||
hook = install_import_hook(
|
hook = install_import_hook(
|
||||||
"test.import_hook_tester_transitive", ("typeguard", "typechecked")
|
"test.import_hook_tester_transitive", ("typeguard", "typechecked")
|
||||||
)
|
)
|
||||||
from . import import_hook_tester_transitive # noqa: F401
|
with hook:
|
||||||
|
from . import import_hook_tester_transitive # noqa: F401
|
||||||
hook.uninstall()
|
|
||||||
|
|
||||||
|
|
||||||
def test_import_hook_broken_checker():
|
def test_import_hook_broken_checker():
|
||||||
hook = install_import_hook(
|
hook = install_import_hook(
|
||||||
"test.import_hook_tester_broken_checker", ("jaxtyping", "does_not_exist")
|
"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
|
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
|
# 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.
|
||||||
|
|
||||||
from typing import Tuple, Union
|
from typing import NamedTuple, Tuple, Union
|
||||||
|
|
||||||
import equinox as eqx
|
import equinox as eqx
|
||||||
import jax
|
import jax
|
||||||
@@ -155,3 +155,33 @@ def test_pytree_tuple(typecheck):
|
|||||||
g([1, 1])
|
g([1, 1])
|
||||||
with pytest.raises(ParamError):
|
with pytest.raises(ParamError):
|
||||||
g([(1, 1), "hi"])
|
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)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
+27
-2
@@ -25,7 +25,22 @@ from typeguard import typechecked
|
|||||||
from jaxtyping import Array, Float, jaxtyped
|
from jaxtyping import Array, Float, jaxtyped
|
||||||
|
|
||||||
|
|
||||||
def test_threading():
|
class _ErrorableThread(threading.Thread):
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
super().run()
|
||||||
|
except Exception as e:
|
||||||
|
self.exc = e
|
||||||
|
finally:
|
||||||
|
del self._target, self._args, self._kwargs
|
||||||
|
|
||||||
|
def join(self, timeout=None):
|
||||||
|
super().join(timeout)
|
||||||
|
if hasattr(self, "exc"):
|
||||||
|
raise self.exc
|
||||||
|
|
||||||
|
|
||||||
|
def test_threading_jaxtyped():
|
||||||
@jaxtyped
|
@jaxtyped
|
||||||
@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"]:
|
||||||
@@ -36,6 +51,16 @@ def test_threading():
|
|||||||
b = jnp.array([[2.0, 3.0]])
|
b = jnp.array([[2.0, 3.0]])
|
||||||
add(a, b)
|
add(a, b)
|
||||||
|
|
||||||
thread = threading.Thread(target=run)
|
thread = _ErrorableThread(target=run)
|
||||||
|
thread.start()
|
||||||
|
thread.join()
|
||||||
|
|
||||||
|
|
||||||
|
def test_threading_nojaxtyped():
|
||||||
|
def run():
|
||||||
|
a = jnp.array([[1.0, 2.0]])
|
||||||
|
assert isinstance(a, Float[Array, "..."])
|
||||||
|
|
||||||
|
thread = _ErrorableThread(target=run)
|
||||||
thread.start()
|
thread.start()
|
||||||
thread.join()
|
thread.join()
|
||||||
|
|||||||
Reference in New Issue
Block a user