Compare commits

..
42 Commits
Author SHA1 Message Date
Patrick Kidger 849b15db3b Added better docs; added PRNGKeys. Packaging with pyproject.toml.
Also added support for matching dtypes using regexes, and for nesting e.g. Shaped[Float[Array, "dim1 dim2"], "dim3"].
2023-05-10 15:27:25 -07:00
Patrick Kidger 46c7896c99 Transitive test now uses typeguard instead of beartype 2023-04-19 11:48:08 -07:00
Patrick Kidger c92b0d0ab1 Some improvements (#77)
* Various improvements.

- Added support for functions in symbolic dimensions, e.g. "min(foo,bar)", which were previously disallowed due to the presence of a comma. (#51)
- Added support for adding ignored names to dimensions, e.g. "cols=4". (#76)

* Now works with Python 3.10 A | B union types.
2023-04-13 19:18:55 +01:00
Patrick Kidger 158b8b8f0c Now works with torch.compile? (#72) 2023-04-13 18:53:14 +01:00
Patrick Kidger 9b6df18b83 Update FAQ to mention ruff 2023-03-19 22:21:21 +00:00
Patrick Kidger f0b240df5f Switched to ruff 2023-03-15 22:36:24 -07:00
Patrick Kidger a4d27c7cc1 Fixed _Jaxtyped.__get__, e.g. swallowing abstractmethod decorations 2023-03-15 22:27:36 -07:00
Patrick Kidger ee46c57e53 Version bump 2023-03-05 20:26:01 -08:00
Patrick Kidger 38be24f9c8 beartype+inheritance fix. Bool[int, '...'] now correctly raises an error. 2023-03-05 20:09:49 -08:00
Patrick Kidger e718f00cc5 Fixed import hook hitting __pycache__ even when you change the choice of runtime type checker 2023-03-05 16:19:44 -08:00
Patrick Kidger c232eeaa89 Fixed pytest plugin with new import hook typechecker syntax 2023-03-05 12:19:14 -08:00
Patrick Kidger e03c1c329e We now have Float[np.ndarray, ...] <: np.ndarray. Added basic torch tests. (#68)
This required quite a lot of refactoring! JAX supports virtual subclass registration (its metaclass is ABCMeta) but NumPy does not, so we have to actually subclass `np.ndarray`.
Simple stuff like __base__ hacking fails due to deallocator conflicts.
2023-03-04 17:29:04 +00:00
Patrick Kidger fef81cf0a0 The import hook now supports BeartypeConf/BeartypeStrategy 2023-03-03 10:34:03 -08:00
Patrick Kidger bf241b4e27 We now have e.g. Float[Array, ""] <: Array. 2023-03-03 10:32:26 -08:00
Patrick Kidger 5600a1aac8 Fixed cloudpickle breaking, mark 2 2023-03-02 17:37:53 -08:00
Patrick Kidger 2b339715f9 Fixed cloudpickle breaking 2023-03-02 12:35:38 -08:00
Zac Cranko 8c86958b77 Add TypeAlias decoration to PyTree (#66)
Doing this silences a *whole heap* of Pyright warnings that all say "Illegal type annotation: variable not allowed unless it is a type alias"
2023-02-28 01:23:47 +00:00
Patrick Kidger ffc56bf782 Edge case fix 2023-02-25 17:38:45 -08:00
Patrick Kidger 5c25da278a Bump version 2023-02-25 17:03:25 -08:00
Patrick Kidger e2f004afd4 Added support for jax.typing.ArrayLike; now works with PyTorch's bool 2023-02-25 17:01:27 -08:00
Patrick Kidger 81c56052e5 Fixes for some new failures. (Where did they come from?) (#65)
* Fixes for some new failures. (Where did they come from?)

* Fixed isort?
2023-02-16 10:08:57 -08:00
Patrick Kidger d911ebb99c Fix abstractmethods being ignored after @jaxtyped 2023-01-22 11:43:36 -08:00
Patrick Kidger f30b7d1546 Update README.md 2023-01-20 07:44:45 -08:00
Brent Yi 4b3f834e12 Fix vanilla dataclasses (#56) 2023-01-15 10:52:18 +01:00
Patrick Kidger 59e8fb0d18 Hopefully fixed PyTree raising spurious errors. Bit mysterious that this worked before, really. I've tested this fix as best I can against the various static type checkers, but these are weird and varied enough that this might not be a perfect fix. If you see this and have issues, let me know. (#54) 2022-12-30 19:00:26 +00:00
Patrick Kidger 7dba3516c2 Fixed working with the new (unreleased) version of typeguard (#53) 2022-12-29 17:46:25 +00:00
Patrick Kidger 2b1be5eb0a Update README.md 2022-12-07 17:22:57 -08:00
Brent Yi 7b3d9a2e9a Explicitly export names in jaxtyping.* (#49)
* Explicitly export names to make pyright happy

* Bump jax and jaxtyping versions

* Add note on `jaxtyping` names

* Remove __all__ from `array_types.py`

* Appease flake8

* Reduce import redundancy

* Fix capitalization
2022-12-07 17:21:33 -08:00
Patrick Kidger 29654e7087 JAX is no longer a hard dependency (to support e.g. PyTorch) (#50) 2022-12-07 10:48:47 -08:00
Patrick Kidger 8fbf7bf3a5 added link to eqxvision 2022-12-05 11:37:32 -08:00
Patrick Kidger a220df9964 The import hook now decorates dataclass __init__ methods (#48) 2022-11-16 13:38:04 -08:00
Kevin P Murphy 784aa78f7c update jaxtyped decorator (#44)
* update jaxtyped decorator

* add newline character to pacify flake

* add precomit hooks

* add missing return statement

* replace typing_extensions>=4.2.0 with typing_extensions

* pin version range for typing_extensions

* set min version of typing-extensions but not max

* bump version number to 0.2.8
2022-11-13 22:25:28 -08:00
Patrick Kidger 3f877c0dbb Update array_types.py (#41) 2022-11-08 22:52:17 -08:00
Patrick Kidger 607f3c66b5 Silenced warning (#40) 2022-10-28 15:19:52 -07:00
Peter Roelants d3651ca70e NamedTuple example (#36) 2022-10-03 07:40:51 -07:00
Patrick Kidger d246e21281 Better import hook (#35) 2022-09-25 23:28:40 -07:00
Patrick Kidger 165065756f Static type-checking fix (#34) 2022-09-24 18:27:22 -07:00
ebrevdo dcd73e3431 Add support for e.g. jaxtyping.Float[Union[...], ...] in py3.8 (#31)
* Add support for e.g. jaxtyping.Float[Union[...], ...] in py3.8

Turns out that python3.8, Union lacks the __name__ attribute.  Use repr()
in these cases.

* Fix linter.

* Remove implicit cast to bool in favor of try/except.
2022-09-22 13:25:29 -07:00
Patrick Kidger f175c7f315 Fixed py.type not being packaged (#30) 2022-09-22 12:16:25 -07:00
Patrick Kidger da8300ec6c tidyness tweak (#28) 2022-09-20 15:16:52 -07:00
Patrick Kidger 39439c2790 More fixes (#27)
* Edge-case doc fixes for parameterising types with PyTrees or AbstractDtypes

* Fixes for threading

* version bump
2022-09-20 15:12:21 -07:00
Patrick Kidger 6202dcc639 doc fix (#26) 2022-09-19 23:30:29 -07:00
50 changed files with 2146 additions and 749 deletions
-4
View File
@@ -1,4 +0,0 @@
[flake8]
max-line-length = 88
ignore = W291,W293,W503,W504,E123,E126,E203,E402,E701,E731,F722
per-file-ignores = __init__.py: F401
+39
View File
@@ -0,0 +1,39 @@
name: Build docs
on:
push:
branches:
- main
jobs:
build:
strategy:
matrix:
python-version: [ 3.11 ]
os: [ ubuntu-latest ]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install .
python -m pip install -r docs/requirements.txt
- name: Build docs
run: |
mkdocs build
mkdocs build # twice, see https://github.com/patrick-kidger/pytkdocs_tweaks
- name: Upload docs
uses: actions/upload-artifact@v2
with:
name: docs
path: site # where `mkdocs build` puts the built site
+4 -3
View File
@@ -29,11 +29,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Release
uses: patrick-kidger/action_update_python_project@v1
uses: patrick-kidger/action_update_python_project@v2
with:
python-version: "3.8"
python-version: "3.11"
test-script: |
python -m pip install pytest beartype equinox jaxlib
python -m pip install pytest beartype equinox jaxlib cloudpickle
python -m pip install torch --extra-index-url https://download.pytorch.org/whl/cpu
cp -r ${{ github.workspace }}/test ./test
pytest
pypi-token: ${{ secrets.pypi_token }}
+3 -2
View File
@@ -26,7 +26,7 @@ jobs:
run-tests:
strategy:
matrix:
python-version: [ 3.7, 3.8, 3.9 ]
python-version: [ 3.11 ]
os: [ ubuntu-latest ]
fail-fast: false
runs-on: ${{ matrix.os }}
@@ -42,7 +42,8 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pytest wheel beartype equinox jaxlib
python -m pip install pytest wheel beartype equinox jaxlib cloudpickle
python -m pip install torch --extra-index-url https://download.pytorch.org/whl/cpu
- name: Checks with pre-commit
uses: pre-commit/action@v2.0.3
+2 -1
View File
@@ -2,4 +2,5 @@
*.egg-info
build/
dist/
site/
.all_objects.cache
-6
View File
@@ -1,6 +0,0 @@
[settings]
force_alphabetical_sort_within_sections=true
lines_after_imports=2
profile=black
treat_comments_as_code=true
extra_standard_library=typing_extensions
+3 -13
View File
@@ -22,17 +22,7 @@ repos:
rev: 22.3.0
hooks:
- id: black
- repo: https://github.com/nbQA-dev/nbQA
rev: 1.2.3
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 'v0.0.255'
hooks:
- id: nbqa-black
- id: nbqa-isort
- id: nbqa-flake8
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
hooks:
- id: isort
- repo: https://github.com/pycqa/flake8
rev: 4.0.1
hooks:
- id: flake8
- id: ruff
-229
View File
@@ -1,229 +0,0 @@
# Full API
## Annotating array types
Each array is denoted by a type `dtype[array, shape]`, such as `Float[Array, "batch channels"]`.
### Shape
The shape should be a string of space-separated symbols, such as `"a b c d"`. Each symbol can be either an:
- `int`: fixed-size axis, e.g. `"28 28"`.
- `str`: variable-size axis, e.g. `"channels"`.
- A symbolic expression (without spaces!) in terms of other variable-size axes, e.g. `def remove_last(x: Float[Array, "dim"]) -> Float[Array, "dim-1"]`.
When calling a function, variable-size axes and symbolic axes will be matched up across all arguments and checked for consistency. (See [runtime type checking](#runtime-type-checking) below.)
In addition some modifiers can be applied:
- Prepend `*` to a dimension to indicate that it can match multiple axes, e.g. `"*batch c h w"` will match zero or more batch axes.
- Prepend `#` to a dimension to indicate that it can be that size *or* equal to one -- i.e. broadcasting is acceptable, e.g. `def add(x: Float[Array, "#foo"], y: Float[Array, "#foo"]) -> Float[Array, "#foo"]`.
- Prepend `_` to a dimension to disable any runtime checking of that dimension (so that it can be used just as documentation). This can also be used as just `_` on its own: e.g. `"b c _ _"`.
When using multiple modifiers, their order does not matter.
As a special case:
- `...`: anonymous zero or more axes (equivalent to `*_`) e.g. `"... c h w"`
Some notes:
- To denote a scalar shape use `""`, e.g. `Float[Array, ""]`.
- To denote an arbitrary shape (and only check dtype) use `"..."`, e.g. `Float[Array, "..."]`.
- You cannot have more than one use of multiple-axes, i.e. you can only use `...` or `*name` at most once in each array.
- An example of broadcasting multiple dimensions: `def add(x: Float[Array, "*#foo"], y: Float[Array, "*#foo"]) -> Float[Array, "*#foo"]`.
- A symbolic expression cannot be evaluated unless all of the axes sizes it refers to have already been processed. In practice this usually means that they should only be used in annotations for the return type, and only use axes declared in the arguments.
### Dtype
The dtype should be any one of (imported from `jaxtyping`):
- Any dtype at all: `Shaped`
- Boolean: `Bool`
- Any integer, unsigned integer, floating, or complex: `Num`
- Any floating or complex: `Inexact`
- Any floating point: `Float`
- Of particular precision: `BFloat16`, `Float16`, `Float32`, `Float64`
- Any complex: `Complex`
- Of particular precision: `Complex64`, `Complex128`
- Any integer or unsigned intger: `Integer`
- Any unsigned integer: `UInt`
- Of particular precision: `UInt8`, `UInt16`, `UInt32`, `UInt64`
- Any signed integer: `Int`
- Of particular precision: `Int8`, `Int16`, `Int32`, `Int64`
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
```python
from jaxtyping import Array, Float
Float[Array, "some_shape"]
```
rather than
```python
from jaxtyping import Array, Float32
Float32[Array, "some_shape"]
```
### Array
The array should typically be a `jaxtyping.Array`, which is an alias for `jax.numpy.ndarray`.
But you can use other types as well. `jaxtyping` has support for JAX, NumPy, TensorFlow, and PyTorch, e.g.:
```python
Float[np.ndarray, "..."]
Float[tf.Tensor, "..."]
Float[torch.Tensor, "..."]
```
## PyTrees
### `jaxtyping.PyTree`
Each PyTree is denoted by a type `PyTree[LeafType]`, such as `PyTree[int]` or `PyTree[Union[str, Float32[Array, "b c"]]]`.
You can leave off the `[...]`, in which case `PyTree` is simply a suggestively-named alternative to `Any`. ([By definition all types are PyTrees.](https://jax.readthedocs.io/en/latest/pytrees.html))
## Runtime type checking
Single-argument type checking will work with any runtime type checker out-of-the-box.
To enable multi-argument consistency checks (i.e. that shapes match up between arrays), then you have two options, as discussed below. (And if either are too much magic for you, you can safely use neither and stick to just single-argument type checking.)
Regardless of your choice, **this approach synergises beautifully with `jax.jit`!** All shape checks will be performed at trace-time only, and will not impact runtime performance.
### Option 1: `jaxtyping.jaxtyped`
Decorate a function with this to have shapes checked for consistency across multiple arguments.
Example:
```python
# Import both the annotation and the `jaxtyped` decorator from `jaxtyping`
from jaxtyping import Array, Float32, jaxtyped
# Use your favourite typechecker: usually one of the two lines below.
from typeguard import typechecked as typechecker
from beartype import beartype as typechecker
# Write your function. @jaxtyped must be applied above @typechecker!
@jaxtyped
@typechecker
def batch_outer_product(x: Float32[Array, "b c1"],
y: Float32[Array, "b c2"]
) -> Float32[Array, "b c1 c2"]:
return x[:, :, None] * y[:, None, :]
```
Note that `@jaxtyped` is applied above the type checker.
#### `jaxtyping.jaxtyped` for advanced users
Put precisely, all `isinstance` shape checks are scoped to the thread-local dynamic context
of a `jaxtyped` call. A new dynamic context will allow different dimensions
sizes to be bound to the same name. After this new dynamic context is finished
then the old one is returned to.
For example, this means you could leave off the `@jaxtyped` decorator to enforce that
this function use the same axes sizes as the function it was called from.
Likewise, this means you can use `isinstance` checks inside a function body
and have them contribute to the same collection of consistency checks performed
by a typechecker against its arguments. (Or even forgo a typechecker that analyses arguments,
and instead just do your own manual `isinstance` checks.)
Only `isinstance` checks that pass will contribute to the store of axis name-size pairs; those
that fail will not. As such it is safe to write e.g. `assert not isinstance(x,
Float32[Array, "foo"])`.
### Option 2: `jaxtyping.install_import_hook`
It can be a lot of effort to add `@jaxtyped` decorators all over your codebase.
(Not to mention that double-decorators everywhere are a bit ugly.)
The easier option is usually to use the import hook.
This can be used via a `with` block; for example:
```python
from jaxtyping import install_import_hook
# Plus any one of the following:
# decorate @jaxtyped and @typeguard.typechecked
with install_import_hook("foo", ("typeguard", "typechecked")):
import foo # Any module imported inside this `with` block, whose name begins
import foo.bar # with the specified string, will automatically have both `@jaxtyped`
import foo.bar.qux # and the specified typechecker applied to all of their functions.
# decorate @jaxtyped and @beartype.beartype
with install_import_hook("foo", ("beartype", "beartype")):
...
# decorate only @jaxtyped (if you want that for some reason)
with install_import_hook("foo", None):
...
```
If you don't like using the `with` block, the hook can be used without that:
```python
hook = install_import_hook(...):
import ...
hook.uninstall()
```
The import hook can be applied to multiple packages via
```python
install_import_hook(["foo", "bar.baz"], ...)
```
**Example: writing an end-user script**
```python
### entry_point.py
from jaxtyping import install_import_hook
with install_import_hook("do_stuff", ("typeguard", "typechecked")):
import do_stuff
### do_stuff.py
from jaxtyping import Array, Float32
def g(x: Float32[Array, "..."]):
...
```
**Example: writing a library**
```python
### __init__.py
from jaxtyping import install_import_hook
with install_import_hook("my_library_name", ("beartype", "beartype")):
from .subpackage import foo # full name is my_library_name.subpackage so will be hook'd
from .another_subpackage import bar # full name is my_library_name.another_subpackage so will be hook'd.
```
#### pytest hook
The import hook can be installed at test-time only, as a pytest hook. The syntax is
```
pytest --jaxtyping-packages=foo,bar.baz,beartype.beartype
```
which will apply the import hook to all modules whose names start with either `foo` or `bar.baz`. The typechecker used in this example is `beartype.beartype`.
## Static type checking
jaxtyping should be compatible with static type checkers (the big three are `mypy`, `pyright`, `pytype`) out of the box.
Due to limitations of static type checkers, only the array type (JAX array vs NumPy array vs PyTorch tensor vs TensorFlow tensor) is checked. Shape and dtype are not checked. [See the FAQ](./FAQ.md) for more details.
## Abstract base classes
### `jaxtyping.AbstractDtype`
The base class of all dtypes. This can be used to create your own custom collection of dtypes (analogous to `Float`, `Inexact` etc.) For example:
```python
class UInt8or16(AbstractDtype):
dtypes = ["uint8", "uint16"]
UInt8or16[Array, "shape"]
```
which is functionally equivalent to
```python
Union[UInt8[Array, "shape"], UInt16[Array, "shape"]]
```
### `jaxtyping.AbstractArray`
The base class of all shape-and-dtype-specified arrays, e.g. it's a base class
for `Float32[Array, "foo"]`.
+2 -1
View File
@@ -28,7 +28,8 @@ Now make your changes. Make sure to include additional tests if necessary.
Next verify the tests all pass:
```bash
pip install pytest
pip install pytest cloudpickle
pip install torch --extra-index-url https://download.pytorch.org/whl/cpu
pytest
```
-2
View File
@@ -1,2 +0,0 @@
include LICENSE
prune tests
+10 -13
View File
@@ -1,10 +1,11 @@
<h1 align="center">jaxtyping</h1>
Type annotations **and runtime checking** for:
Type annotations **and runtime type-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,15 +29,15 @@ def accepts_pytree_of_arrays(x: PyTree[Float[Array, "batch c1 c2"]]):
pip install jaxtyping
```
Requires JAX 0.3.4+.
Requires Python 3.8+.
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).
JAX is an optional dependency, required for a few JAX-specific types. If JAX is not installed then these will not be available, but you may still use jaxtyping to provide shape/dtype annotations for PyTorch/NumPy/TensorFlow/etc.
The annotations provided by jaxtyping are compatible with runtime type-checking packages, so it is common to also install one of these. 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
[Full API reference](./API.md)
[FAQ (static type checking, flake8, etc.)](./FAQ.md)
Available at [https://docs.kidger.site/jaxtyping](https://docs.kidger.site/jaxtyping).
## Finally
@@ -46,14 +47,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.
+12
View File
@@ -0,0 +1,12 @@
If you found this library to be useful in academic work, then please cite: ([arXiv link](https://arxiv.org/abs/2111.00254))
```bibtex
@article{kidger2021equinox,
author={Patrick Kidger and Cristian Garcia},
title={{E}quinox: neural networks in {JAX} via callable {P}y{T}rees and filtered transformations},
year={2021},
journal={Differentiable Programming workshop at Neural Information Processing Systems 2021}
}
```
(Also consider starring the project [on GitHub](https://github.com/patrick-kidger/equinox).)
+1
View File
@@ -0,0 +1 @@
ErrorDocument 404 /jaxtyping/404.html
+20
View File
@@ -0,0 +1,20 @@
{% import "partials/language.html" as lang with context %}
<a href="{{ config.repo_url }}" title="{{ lang.t('source.link.title') }}" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
{% set icon = config.theme.icon.repo or "fontawesome/brands/git-alt" %}
{% include ".icons/" ~ icon ~ ".svg" %}
</div>
<div class="md-source__repository">
{{ config.repo_name }}
</div>
</a>
{% if config.theme.twitter_url %}
<a href="{{ config.theme.twitter_url }}" title="Go to Twitter" class="md-source">
<div class="md-source__icon md-icon">
{% include ".icons/fontawesome/brands/twitter.svg" %}
</div>
<div class="md-source__repository">
{{ config.theme.twitter_name }}
</div>
</a>
{% endif %}
+2
View File
@@ -0,0 +1,2 @@
The favicon is `math-integral` from https://materialdesignicons.com, found by way of https://pictogrammers.com.
(The logo is `math-integral-box`.)
+167
View File
@@ -0,0 +1,167 @@
/* Fix /page#foo going to the top of the viewport and being hidden by the navbar */
html {
scroll-padding-top: 50px;
}
/* Fit the Twitter handle alongside the GitHub one in the top right. */
div.md-header__source {
width: revert;
max-width: revert;
}
a.md-source {
display: inline-block;
}
.md-source__repository {
max-width: 100%;
}
/* Emphasise sections of nav on left hand side */
nav.md-nav {
padding-left: 5px;
}
nav.md-nav--secondary {
border-left: revert !important;
}
.md-nav__title {
font-size: 0.9rem;
}
.md-nav__item--section > .md-nav__link {
font-size: 0.9rem;
}
/* Indent autogenerated documentation */
div.doc-contents {
padding-left: 25px;
border-left: 4px solid rgba(230, 230, 230);
}
/* Increase visibility of splitters "---" */
[data-md-color-scheme="default"] .md-typeset hr {
border-bottom-color: rgb(0, 0, 0);
border-bottom-width: 1pt;
}
[data-md-color-scheme="slate"] .md-typeset hr {
border-bottom-color: rgb(230, 230, 230);
}
/* More space at the bottom of the page */
.md-main__inner {
margin-bottom: 1.5rem;
}
/* Remove prev/next footer buttons */
.md-footer__inner {
display: none;
}
/* Change font sizes */
html {
/* Decrease font size for overall webpage
Down from 137.5% which is the Material default */
font-size: 110%;
}
.md-typeset .admonition {
/* Increase font size in admonitions */
font-size: 100% !important;
}
.md-typeset details {
/* Increase font size in details */
font-size: 100% !important;
}
.md-typeset h1 {
font-size: 1.6rem;
}
.md-typeset h2 {
font-size: 1.5rem;
}
.md-typeset h3 {
font-size: 1.3rem;
}
.md-typeset h4 {
font-size: 1.1rem;
}
.md-typeset h5 {
font-size: 0.9rem;
}
.md-typeset h6 {
font-size: 0.8rem;
}
/* Bugfix: remove the superfluous parts generated when doing:
??? Blah
::: library.something
*/
.md-typeset details .mkdocstrings > h4 {
display: none;
}
.md-typeset details .mkdocstrings > h5 {
display: none;
}
/* Change default colours for <a> tags */
[data-md-color-scheme="default"] {
--md-typeset-a-color: rgb(0, 189, 164) !important;
}
[data-md-color-scheme="slate"] {
--md-typeset-a-color: rgb(0, 189, 164) !important;
}
/* Highlight functions, classes etc. type signatures. Really helps to make clear where
one item ends and another begins. */
[data-md-color-scheme="default"] {
--doc-heading-color: #DDD;
--doc-heading-border-color: #CCC;
--doc-heading-color-alt: #F0F0F0;
}
[data-md-color-scheme="slate"] {
--doc-heading-color: rgb(25,25,33);
--doc-heading-border-color: rgb(25,25,33);
--doc-heading-color-alt: rgb(33,33,44);
--md-code-bg-color: rgb(38,38,50);
}
h4.doc-heading {
/* NOT var(--md-code-bg-color) as that's not visually distinct from other code blocks.*/
background-color: var(--doc-heading-color);
border: solid var(--doc-heading-border-color);
border-width: 1.5pt;
border-radius: 2pt;
padding: 0pt 5pt 2pt 5pt;
}
h5.doc-heading, h6.heading {
background-color: var(--doc-heading-color-alt);
border-radius: 2pt;
padding: 0pt 5pt 2pt 5pt;
}
/* Make errors in notebooks have scrolling */
.output_error > pre {
overflow: auto;
}
Vendored Executable
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

+16
View File
@@ -0,0 +1,16 @@
window.MathJax = {
tex: {
inlineMath: [["\\(", "\\)"]],
displayMath: [["\\[", "\\]"]],
processEscapes: true,
processEnvironments: true
},
options: {
ignoreHtmlClass: ".*|",
processHtmlClass: "arithmatex"
}
};
document$.subscribe(() => {
MathJax.typesetPromise()
})
+13
View File
@@ -0,0 +1,13 @@
# Advanced features
## Abstract base classes
::: jaxtyping.AbstractDtype
selection:
members:
false
::: jaxtyping.AbstractArray
selection:
members:
false
+112
View File
@@ -0,0 +1,112 @@
# Array annotations
The shape and dtypes of arrays can be annotated in the form `dtype[array, shape]`, such as `Float[Array, "batch channels"]`.
## Shape
**Symbols**
The shape should be a string of space-separated symbols, such as `"a b c d"`. Each symbol can be either an:
- `int`: fixed-size axis, e.g. `"28 28"`.
- `str`: variable-size axis, e.g. `"channels"`.
- A symbolic expression (without spaces!) in terms of other variable-size axes, e.g.
`def remove_last(x: Float[Array, "dim"]) -> Float[Array, "dim-1"]`.
When calling a function, variable-size axes and symbolic axes will be matched up across all arguments and checked for consistency. (See [Runtime type checking](./runtime-type-checking.md).)
**Modifiers**
In addition some modifiers can be applied:
- Prepend `*` to a dimension to indicate that it can match multiple axes, e.g. `"*batch c h w"` will match zero or more batch axes.
- Prepend `#` to a dimension to indicate that it can be that size *or* equal to one -- i.e. broadcasting is acceptable, e.g.
`def add(x: Float[Array, "#foo"], y: Float[Array, "#foo"]) -> Float[Array, "#foo"]`.
- Prepend `_` to a dimension to disable any runtime checking of that dimension (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"]`.
When using multiple modifiers, their order does not matter.
As a special case:
- `...`: anonymous zero or more axes (equivalent to `*_`) e.g. `"... c h w"`
**Notes**
- To denote a scalar shape use `""`, e.g. `Float[Array, ""]`.
- To denote an arbitrary shape (and only check dtype) use `"..."`, e.g. `Float[Array, "..."]`.
- You cannot have more than one use of multiple-axes, i.e. you can only use `...` or `*name` at most once in each array.
- An example of broadcasting multiple dimensions:
`def add(x: Float[Array, "*#foo"], y: Float[Array, "*#foo"]) -> Float[Array, "*#foo"]`.
- A symbolic expression cannot be evaluated unless all of the axes sizes it refers to have already been processed. In practice this usually means that they should only be used in annotations for the return type, and only use axes declared in the arguments.
## Dtype
The dtype should be any one of (all imported from `jaxtyping`):
- Any dtype at all: `Shaped`
- Boolean: `Bool`
- PRNG key: `Key`
- Any integer, unsigned integer, floating, or complex: `Num`
- Any floating or complex: `Inexact`
- Any floating point: `Float`
- Of particular precision: `BFloat16`, `Float16`, `Float32`, `Float64`
- Any complex: `Complex`
- Of particular precision: `Complex64`, `Complex128`
- Any integer or unsigned intger: `Integer`
- Any unsigned integer: `UInt`
- Of particular precision: `UInt8`, `UInt16`, `UInt32`, `UInt64`
- Any signed integer: `Int`
- Of particular precision: `Int8`, `Int16`, `Int32`, `Int64`
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
```python
from jaxtyping import Array, Float
Float[Array, "some_shape"]
```
rather than
```python
from jaxtyping import Array, Float32
Float32[Array, "some_shape"]
```
## Array
The array should usually be a `jaxtyping.Array`, which is an alias for `jax.numpy.ndarray` (which is itself an alias for `jax.Array`).
`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`.
You can use non-JAX types as well. jaxtyping also supports NumPy, TensorFlow, and PyTorch, e.g.:
```python
Float[np.ndarray, "..."]
Float[tf.Tensor, "..."]
Float[torch.Tensor, "..."]
```
Shape-and-dtype specified jaxtyping arrays can also be used, e.g.
```python
Image = Float[Array, "channels height width"]
BatchImage = Float[Image, "batch"]
```
in which case the additional shape is prepended, and the acceptable dtypes are the intersection of the two dtype specifiers used. (So that e.g. `BatchImage = Shaped[Image, "batch"]` would work just as well. But `Bool[Image, "batch"]` would throw an error, as there are no dtypes that are both bools and floats.) Thus the above is equivalent to
```python
BatchImage = Float[Array, "batch channels height width"]
```
Note that `jaxtyping.{Array, ArrayLike}` are only available if JAX has been installed.
## Scalars, PRNGKeys
For convenience, jaxtyping also includes `jaxtyping.Scalar`, `jaxtyping.ScalarLike`, and `jaxtyping.PRNGKeyArray`, defined as:
```python
Scalar = Shaped[Array, ""]
ScalarLike = Shaped[ArrayLike, ""]
# Depending on the value of `JAX_ENABLE_CUSTOM_PRNG`:
PRNGKeyArray = Key[Array, ""]
PRNGKeyArray = UInt32[Array, "2"]
```
Recalling that shape-and-dtype specified jaxtyping arrays can be nested, this means that e.g. you can annotate the output of `jax.random.split` with `Shaped[PRNGKeyArray, "2"]`, or e.g. an integer scalar with `Int[Scalar, ""]`.
Note that `jaxtyping.{Scalar, ScalarLike, PRNGKeyArray}` are only available if JAX has been installed.
+8
View File
@@ -0,0 +1,8 @@
# PyTree annotations
:::jaxtyping.PyTree
selection:
members:
false
Note that `jaxtyping.PyTree` is only available if JAX has been installed.
+22
View File
@@ -0,0 +1,22 @@
# Runtime type checking
(See the [FAQ](../faq.md) for details on static type checking.)
Runtime type checking **synergises beautifully with `jax.jit`!** All shape checks will be performed at trace-time only, and will not impact runtime performance.
Runtime type-checking should be performed using a library like [typeguard](https://github.com/agronholm/typeguard) or [beartype](https://github.com/beartype/beartype).
The types provided by `jaxtyping`, e.g. `Float[Array, "batch channels"]`, are all compatible with `isinstance` checks, e.g. `isinstance(x, Float[Array, "batch channels"])`. This means that jaxtyping should be compatible with all runtime type checkers out-of-the-box.
Some additional context is needed to ensure consistency between multiple argments (i.e. that shapes match up between arrays). For this, you can use either `jaxtyping.jaxtyped` to add this capability to a single function, or `jaxtyping.install_import_hook` to add this capability to a whole codebase. If either are too much magic for you, you can safely use neither and have just single-argument type checking.
::: jaxtyping.jaxtyped
---
It can be a lot of effort to add `@jaxtyped` decorators all over your codebase.
(Not to mention that double-decorators everywhere are a bit ugly.)
The easier option is usually to use the import hook.
::: jaxtyping.install_import_hook
+3 -3
View File
@@ -1,6 +1,6 @@
# FAQ
## Does jaxtyping work with static type checkers like `mypy`/`pyright`/`pytype`?
## Is jaxtyping compatible with static type checkers like `mypy`/`pyright`/`pytype`?
There is partial support for these. An annotation of the form `dtype[array, shape]` should be treated as just `array` by a static type checker. Unfortunately full dtype/shape checking is beyond the scope of what static type checking is currently capable of.
@@ -12,13 +12,13 @@ jaxtyping and `jax.jit` synergise beautifully.
When calling JAX operations wrapped in a `jax.jit`, then the dtype/shape-checking will happen at trace time. (When JAX traces your function prior to compiling it.) The actual compiled code does not have any dtype/shape-checking, and will therefore still be just as fast as before!
## `flake8` is throwing an error.
## `flake8` or Ruff are throwing an error.
In type annotations, strings are used for two different things. Sometimes they're strings. Sometimes they're "forward references", used to refer to a type that will be defined later.
Some tooling in the Python ecosystem assumes that only the latter is true, and will throw spurious errors if you try to use a string just as a string (like we do).
In the case of `flake8`, at least, this is easily 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.
## Does jaxtyping use [PEP 646](https://www.python.org/dev/peps/pep-0646/) (variadic generics)?
+42
View File
@@ -0,0 +1,42 @@
# Getting started
jaxtyping is a library providing type annotations **and runtime type-checking** for:
1. shape and dtype of [JAX](https://github.com/google/jax) arrays;
2. [PyTrees](https://jax.readthedocs.io/en/latest/pytrees.html).
*(Now also supports PyTorch, NumPy, and TensorFlow!)*
## Installation
```bash
pip install jaxtyping
```
Requires Python 3.8+.
JAX is an optional dependency, required for a few JAX-specific types. If JAX is not installed then these will not be available, but you may still use jaxtyping to provide shape/dtype annotations for PyTorch/NumPy/TensorFlow/etc.
The annotations provided by jaxtyping are compatible with runtime type-checking packages, so it is common to also install one of these. 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).
## Example
```python
from jaxtyping import Array, Float, PyTree
# Accepts floating-point 2D arrays with matching dimensions
def matrix_multiply(x: Float[Array, "dim1 dim2"],
y: Float[Array, "dim2 dim3"]
) -> Float[Array, "dim1 dim3"]:
...
def accepts_pytree_of_ints(x: PyTree[int]):
...
def accepts_pytree_of_arrays(x: PyTree[Float[Array, "batch c1 c2"]]):
...
```
## Next steps
Have a read of the [Array annotations](./api/array.md) documentation on the left-hand bar!
+13
View File
@@ -0,0 +1,13 @@
mkdocs==1.3.0 # Main documentation generator.
mkdocs-material==7.3.6 # Theme
pymdown-extensions==9.4 # Markdown extensions e.g. to handle LaTeX.
mkdocstrings==0.17.0 # Autogenerate documentation from docstrings.
mknotebooks==0.7.1 # Turn Jupyter Lab notebooks into webpages.
pytkdocs_tweaks==0.0.5 # Tweaks mkdocstrings to improve various aspects
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.
pygments==2.14.0
# Dependencies of jaxtyping itself.
# Always use most up-to-date versions.
jax[cpu]
+144 -32
View File
@@ -17,40 +17,152 @@
# 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 jax.numpy import ndarray as Array
import importlib.metadata
import typing
try:
import jax
except ImportError:
has_jax = False
else:
has_jax = True
del jax
# First import some things as normal
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,
get_array_name_format as get_array_name_format,
set_array_name_format as set_array_name_format,
)
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.2"
# Now import Array and ArrayLike
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
# Import our dtypes
if typing.TYPE_CHECKING:
# Note that `from typing_extensions import Annotated; Bool = 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,
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 Key,
Annotated as Num,
Annotated as Shaped,
Annotated as UInt,
Annotated as UInt8,
Annotated as UInt16,
Annotated as UInt32,
Annotated as UInt64,
)
else:
# noqas to work around ruff bug
from .array_types import (
BFloat16 as BFloat16, # noqa: F401
Bool as Bool, # noqa: F401
Complex as Complex, # noqa: F401
Complex64 as Complex64, # noqa: F401
Complex128 as Complex128, # noqa: F401
Float as Float, # noqa: F401
Float16 as Float16, # noqa: F401
Float32 as Float32, # noqa: F401
Float64 as Float64, # noqa: F401
Inexact as Inexact, # noqa: F401
Int as Int, # noqa: F401
Int8 as Int8, # noqa: F401
Int16 as Int16, # noqa: F401
Int32 as Int32, # noqa: F401
Int64 as Int64, # noqa: F401
Integer as Integer, # noqa: F401
Key as Key, # noqa: F401
Num as Num, # noqa: F401
Shaped as Shaped, # noqa: F401
UInt as UInt, # noqa: F401
UInt8 as UInt8, # noqa: F401
UInt16 as UInt16, # noqa: F401
UInt32 as UInt32, # noqa: F401
UInt64 as UInt64, # noqa: F401
)
# Now import PyTree
if typing.TYPE_CHECKING:
# Set up to deliberately confuse a static type checker.
import typing_extensions
PyTree: typing_extensions.TypeAlias = 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 as PyTree # noqa: F401
# Conveniences
if typing.TYPE_CHECKING:
from jax import Array as Scalar
from jax.random import PRNGKeyArray as PRNGKeyArray
from jax.typing import ArrayLike as ScalarLike
elif has_jax:
from .array_types import PRNGKeyArray, Scalar, ScalarLike # noqa: F401
del has_jax
__version__ = importlib.metadata.version("jaxtyping")
+399 -240
View File
@@ -19,14 +19,34 @@
import enum
import functools as ft
from typing import Any, Dict, List, NoReturn, Optional, Tuple, TYPE_CHECKING, Union
from typing_extensions import Literal
import re
import sys
import types
import typing
from typing import (
Any,
Dict,
List,
Literal,
NoReturn,
Optional,
Tuple,
Union,
)
import numpy as np
from .decorator import storage
try:
import jax
except ImportError:
has_jax = False
else:
has_jax = True
_array_name_format = "dtype_and_shape"
@@ -130,7 +150,9 @@ class _MetaAbstractArray(type):
if not isinstance(obj, cls.array_type):
return False
if hasattr(obj.dtype, "type") and hasattr(obj.dtype.type, "__name__"):
if has_jax and jax.core.is_opaque_dtype(obj.dtype):
dtype = str(obj.dtype)
elif hasattr(obj.dtype, "type") and hasattr(obj.dtype.type, "__name__"):
# JAX, numpy
dtype = obj.dtype.type.__name__
elif hasattr(obj.dtype, "as_numpy_dtype"):
@@ -146,28 +168,39 @@ class _MetaAbstractArray(type):
"Unrecognised array/tensor type to extract dtype from"
)
if cls.dtypes is not _any_dtype and dtype not in cls.dtypes:
return False
if cls.dtypes is not _any_dtype:
in_dtypes = False
for cls_dtype in cls.dtypes:
if type(cls_dtype) is str:
in_dtypes = dtype == cls_dtype
elif type(cls_dtype) is re.Pattern:
in_dtypes = bool(cls_dtype.match(dtype))
else:
assert False
if in_dtypes:
break
if not in_dtypes:
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
# global scope. In this case just create a temporary memo, since we're not
# going to be comparing against any stored values anyway.
single_memo = {}
variadic_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):
# We update the memo every time we successfully pass a shape check
if not temp_memo:
if no_temp_memo:
storage.memo_stack[-1] = (
single_memo,
variadic_memo,
@@ -233,11 +266,247 @@ class _MetaAbstractArray(type):
assert False
@ft.lru_cache(maxsize=None)
def _make_metaclass(base_metaclass):
class MetaAbstractArray(_MetaAbstractArray, base_metaclass):
pass
return MetaAbstractArray
def _check_scalar(dtype, dtypes, dims):
if len(dims) != 0:
return dims == (_anonymous_variadic_dim,)
return (_any_dtype is dtypes) or any(d.startswith(dtype) for d in dtypes)
class AbstractArray(metaclass=_MetaAbstractArray):
"""This is the base class of all shape-and-dtype-specified arrays, e.g. it's a base
class for `Float32[Array, "foo"]`.
This might be useful if you're trying to inspect type annotations yourself, e.g.
you can check `issubclass(annotation, jaxtyping.AbstractArray)`.
"""
array_type: Any
dtypes: List[str]
dims: List[_AbstractDimOrVariadicDim]
dims: Tuple[_AbstractDimOrVariadicDim, ...]
index_variadic: Optional[int]
dim_str: str
_not_made = object()
_union_types = [typing.Union]
if sys.version_info >= (3, 10):
_union_types.append(types.UnionType)
@ft.lru_cache(maxsize=None)
def _make_array(array_type, dim_str, dtypes, name):
if not isinstance(dim_str, str):
raise ValueError(
"Shape specification must be a string. Axes should be separated with "
"spaces."
)
dims = []
index_variadic = None
for index, elem in enumerate(dim_str.split()):
if "," in elem and "(" not in elem:
# Common mistake.
# Disable in the case that there's brackets to allow for function calls,
# e.g. `min(foo,bar)`, in symbolic dimensions.
raise ValueError("Dimensions should be separated with spaces, not commas")
if elem.endswith("#"):
raise ValueError(
"As of jaxtyping v0.1.0, broadcastable dimensions are now denoted "
"with a # at the start, rather than at the end"
)
if "..." in elem:
if elem != "...":
raise ValueError(
"Anonymous multiple dimension '...' must be used on its own; "
f"got {elem}"
)
broadcastable = False
variadic = True
anonymous = True
dim_type = _DimType.named
else:
broadcastable = False
variadic = False
anonymous = False
while True:
if len(elem) == 0:
# This branch needed as just `_` is valid
break
first_char = elem[0]
if first_char == "#":
if broadcastable:
raise ValueError(
"Do not use # twice to denote broadcastability, e.g. "
"`##foo` is not allowed"
)
broadcastable = True
elem = elem[1:]
elif first_char == "*":
if variadic:
raise ValueError(
"Do not use * twice to denote accepting multiple "
"dimensions, e.g. `**foo` is not allowed"
)
variadic = True
elem = elem[1:]
elif first_char == "_":
if anonymous:
raise ValueError(
"Do not use _ twice to denote anonymity, e.g. `__foo` "
"is not allowed"
)
anonymous = True
elem = elem[1:]
# Allow e.g. `foo=4` as an alternate syntax for just `4`, so that one
# can write e.g. `Float[Array, "rows=3 cols=4"]`
elif elem.count("=") == 1:
_, elem = elem.split("=")
else:
break
if len(elem) == 0 or elem.isidentifier():
dim_type = _DimType.named
else:
try:
elem = int(elem)
except ValueError:
dim_type = _DimType.symbolic
else:
dim_type = _DimType.fixed
if variadic:
if index_variadic is not None:
raise ValueError(
"Cannot use multiple-dimension specifiers (`*name` or `...`) "
"more than once"
)
index_variadic = index
if dim_type is _DimType.fixed:
if variadic:
raise ValueError(
"Cannot have a fixed axis bind to multiple dimensions, e.g. "
"`*4` is not allowed"
)
if anonymous:
raise ValueError(
"Cannot have a fixed axis be anonymous, e.g. `_4` is not " "allowed"
)
elem = _FixedDim(elem, broadcastable)
elif dim_type is _DimType.named:
if anonymous:
if broadcastable:
raise ValueError(
"Cannot have a dimension be both anonymous and "
"broadcastable, e.g. `#_` is not allowed"
)
if variadic:
elem = _anonymous_variadic_dim
else:
elem = _anonymous_dim
else:
if variadic:
elem = _NamedVariadicDim(elem, broadcastable)
else:
elem = _NamedDim(elem, broadcastable)
else:
assert dim_type is _DimType.symbolic
if anonymous:
raise ValueError(
"Cannot have a symbolic dimension be anonymous, e.g. "
"`_foo+bar` is not allowed"
)
if variadic:
raise ValueError(
"Cannot have symbolic multiple-dimensions, e.g. "
"`*foo+bar` is not allowed"
)
elem = compile(elem, "<string>", "eval")
elem = _SymbolicDim(elem, broadcastable)
dims.append(elem)
dims = tuple(dims)
# 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 array_type is bool:
if _check_scalar("bool", dtypes, dims):
return array_type
else:
return _not_made
elif array_type is int:
if _check_scalar("int", dtypes, dims):
return array_type
else:
return _not_made
elif array_type is float:
if _check_scalar("float", dtypes, dims):
return array_type
else:
return _not_made
elif array_type is complex:
if _check_scalar("complex", dtypes, dims):
return array_type
else:
return _not_made
if issubclass(array_type, AbstractArray):
if dtypes is _any_dtype:
dtypes = array_type.dtypes
elif array_type.dtypes is not _any_dtype:
dtypes = tuple(x for x in dtypes if x in array_type.dtypes)
if len(dtypes) == 0:
raise ValueError(
"A jaxtyping annotation cannot be extended with no overlapping "
"dtypes. For example, `Bool[Float[Array, 'dim1'], 'dim2']` is an "
"error. You probably want to make the outer wrapper be `Shaped`."
)
if array_type.index_variadic is not None:
if index_variadic is None:
index_variadic = array_type.index_variadic + len(dims)
else:
raise ValueError(
"Cannot use multiple-dimension specifiers (`*name` or `...`) "
"in both the original array and the extended array"
)
dims = dims + array_type.dims
dim_str = dim_str + " " + array_type.dim_str
array_type = array_type.array_type
try:
type_str = array_type.__name__
except AttributeError:
type_str = repr(array_type)
if _array_name_format == "dtype_and_shape":
name = f"{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")
metaclass = _make_metaclass(type(array_type))
out = metaclass(
name,
(array_type, AbstractArray),
dict(
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
class _MetaAbstractDtype(type):
@@ -248,162 +517,60 @@ class _MetaAbstractDtype(type):
f'`jaxtyping.{cls.__name__}[jnp.ndarray, "..."]`.'
)
@ft.lru_cache(maxsize=None)
def __getitem__(cls, item: Tuple[Any, str]) -> _MetaAbstractArray:
def __getitem__(cls, item: Tuple[Any, str]):
if not isinstance(item, tuple) or len(item) != 2:
raise ValueError(
"As of jaxtyping v0.2.0, type annotations must now include an explicit "
"array type. For example `jaxtyping.Float32[jnp.ndarray, 'foo bar']`."
"array type. For example `jaxtyping.Float32[jax.Array, 'foo bar']`."
)
array_type, dim_str = item
del item
if not isinstance(dim_str, str):
raise ValueError(
"Shape specification must be a string. Axes should be separated with "
"spaces."
)
dims = []
index_variadic = None
for index, elem in enumerate(dim_str.split()):
if "," in elem:
# Common mistake
raise ValueError(
"Dimensions should be separated with spaces, not commas"
)
if elem.endswith("#"):
raise ValueError(
"As of jaxtyping v0.1.0, broadcastable dimensions are now denoted "
"with a # at the start, rather than at the end"
)
if "..." in elem:
if elem != "...":
raise ValueError(
"Anonymous multiple dimension '...' must be used on its own; "
f"got {elem}"
)
broadcastable = False
variadic = True
anonymous = True
dim_type = _DimType.named
if typing.get_origin(array_type) in _union_types:
out = [
_make_array(x, dim_str, cls.dtypes, cls.__name__)
for x in typing.get_args(array_type)
]
out = tuple(x for x in out if x is not _not_made)
if len(out) == 0:
raise ValueError("Invalid jaxtyping type annotation.")
elif len(out) == 1:
(out,) = out
else:
broadcastable = False
variadic = False
anonymous = False
while True:
if len(elem) == 0:
# This branch needed as just `_` is valid
break
first_char = elem[0]
if first_char == "#":
if broadcastable:
raise ValueError(
"Do not use # twice to denote broadcastability, e.g. "
"`##foo` is not allowed"
)
broadcastable = True
elem = elem[1:]
elif first_char == "*":
if variadic:
raise ValueError(
"Do not use * twice to denote accepting multiple "
"dimensions, e.g. `**foo` is not allowed"
)
variadic = True
elem = elem[1:]
elif first_char == "_":
if anonymous:
raise ValueError(
"Do not use _ twice to denote anonymity, e.g. `__foo` "
"is not allowed"
)
anonymous = True
elem = elem[1:]
else:
break
try:
elem = int(elem)
except ValueError:
if len(elem) == 0 or elem.isidentifier():
dim_type = _DimType.named
else:
dim_type = _DimType.symbolic
else:
dim_type = _DimType.fixed
if variadic:
if index_variadic is not None:
raise ValueError(
"Cannot use multiple-dimension specifiers (`*name` or `...`) "
"more than once"
)
index_variadic = index
if dim_type is _DimType.fixed:
if variadic:
raise ValueError(
"Cannot have a fixed axis bind to multiple dimensions, e.g. "
"`*4` is not allowed"
)
if anonymous:
raise ValueError(
"Cannot have a fixed axis be anonymous, e.g. `_4` is not "
"allowed"
)
elem = _FixedDim(elem, broadcastable)
elif dim_type is _DimType.named:
if anonymous:
if broadcastable:
raise ValueError(
"Cannot have a dimension be both anonymous and "
"broadcastable, e.g. `#_` is not allowed"
)
if variadic:
elem = _anonymous_variadic_dim
else:
elem = _anonymous_dim
else:
if variadic:
elem = _NamedVariadicDim(elem, broadcastable)
else:
elem = _NamedDim(elem, broadcastable)
else:
assert dim_type is _DimType.symbolic
if anonymous:
raise ValueError(
"Cannot have a symbolic dimension be anonymous, e.g. "
"`_foo+bar` is not allowed"
)
if variadic:
raise ValueError(
"Cannot have symbolic multiple-dimensions, e.g. "
"`*foo+bar` is not allowed"
)
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"
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,
),
)
out.__module__ = "jaxtyping"
out = _make_array(array_type, dim_str, cls.dtypes, cls.__name__)
if out is _not_made:
raise ValueError("Invalid jaxtyping type annotation.")
return out
class AbstractDtype(metaclass=_MetaAbstractDtype):
dtypes: Union[Literal[_any_dtype], List[str]]
"""This is the base class of all dtypes. This can be used to create your own custom
collection of dtypes (analogous to `Float`, `Inexact` etc.)
You must specify the class attribute `dtypes`. This can either be a string, a
regex (as returned by `re.compile(...)`), or a tuple/list of strings/regexes.
At runtime, the array or tensor's dtype is converted to a string and compared
against the string (an exact match is required) or regex. (String matching is
performed, rather than just e.g. `array.dtype == dtype`, to provide cross-library
compatibility between JAX/PyTorch/etc.)
!!! Example
```python
class UInt8or16(AbstractDtype):
dtypes = ["uint8", "uint16"]
UInt8or16[Array, "shape"]
```
which is essentially equivalent to
```python
Union[UInt8[Array, "shape"], UInt16[Array, "shape"]]
```
"""
dtypes: Union[Literal[_any_dtype], List[Union[str, re.Pattern]]]
def __init__(self, *args, **kwargs):
raise RuntimeError(
@@ -415,94 +582,86 @@ class AbstractDtype(metaclass=_MetaAbstractDtype):
super().__init_subclass__(**kwargs)
dtypes: Union[Literal[_any_dtype], str, List[str]] = cls.dtypes
if isinstance(dtypes, str):
dtypes = [dtypes]
if isinstance(dtypes, (str, re.Pattern)):
dtypes = (dtypes,)
elif dtypes is not _any_dtype:
dtypes = tuple(dtypes)
cls.dtypes = dtypes
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
else:
_bool = "bool_"
_uint8 = "uint8"
_uint16 = "uint16"
_uint32 = "uint32"
_uint64 = "uint64"
_int8 = "int8"
_int16 = "int16"
_int32 = "int32"
_int64 = "int64"
_bfloat16 = "bfloat16"
_float16 = "float16"
_float32 = "float32"
_float64 = "float64"
_complex64 = "complex64"
_complex128 = "complex128"
_bool = "bool"
_bool_ = "bool_"
_uint8 = "uint8"
_uint16 = "uint16"
_uint32 = "uint32"
_uint64 = "uint64"
_int8 = "int8"
_int16 = "int16"
_int32 = "int32"
_int64 = "int64"
_bfloat16 = "bfloat16"
_float16 = "float16"
_float32 = "float32"
_float64 = "float64"
_complex64 = "complex64"
_complex128 = "complex128"
def _make_dtype(_dtypes, name):
class _Cls(AbstractDtype):
dtypes = _dtypes
_Cls.__name__ = name
_Cls.__qualname__ = name
def _make_dtype(_dtypes, name):
class _Cls(AbstractDtype):
dtypes = _dtypes
_Cls.__name__ = name
_Cls.__qualname__ = name
if getattr(typing, "GENERATING_DOCUMENTATION", False):
_Cls.__module__ = "builtins"
else:
_Cls.__module__ = "jaxtyping"
return _Cls
return _Cls
UInt8 = _make_dtype(_uint8, "UInt8")
UInt16 = _make_dtype(_uint16, "UInt16")
UInt32 = _make_dtype(_uint32, "UInt32")
UInt64 = _make_dtype(_uint64, "UInt64")
Int8 = _make_dtype(_int8, "Int8")
Int16 = _make_dtype(_int16, "Int16")
Int32 = _make_dtype(_int32, "Int32")
Int64 = _make_dtype(_int64, "Int64")
BFloat16 = _make_dtype(_bfloat16, "BFloat16")
Float16 = _make_dtype(_float16, "Float16")
Float32 = _make_dtype(_float32, "Float32")
Float64 = _make_dtype(_float64, "Float64")
Complex64 = _make_dtype(_complex64, "Complex64")
Complex128 = _make_dtype(_complex128, "Complex128")
uints = [_uint8, _uint16, _uint32, _uint64]
ints = [_int8, _int16, _int32, _int64]
floats = [_bfloat16, _float16, _float32, _float64]
complexes = [_complex64, _complex128]
UInt8 = _make_dtype(_uint8, "UInt8")
UInt16 = _make_dtype(_uint16, "UInt16")
UInt32 = _make_dtype(_uint32, "UInt32")
UInt64 = _make_dtype(_uint64, "UInt64")
Int8 = _make_dtype(_int8, "Int8")
Int16 = _make_dtype(_int16, "Int16")
Int32 = _make_dtype(_int32, "Int32")
Int64 = _make_dtype(_int64, "Int64")
BFloat16 = _make_dtype(_bfloat16, "BFloat16")
Float16 = _make_dtype(_float16, "Float16")
Float32 = _make_dtype(_float32, "Float32")
Float64 = _make_dtype(_float64, "Float64")
Complex64 = _make_dtype(_complex64, "Complex64")
Complex128 = _make_dtype(_complex128, "Complex128")
# 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
bools = [_bool, _bool_]
uints = [_uint8, _uint16, _uint32, _uint64]
ints = [_int8, _int16, _int32, _int64]
floats = [_bfloat16, _float16, _float32, _float64]
complexes = [_complex64, _complex128]
Bool = _make_dtype(_bool, "Bool")
UInt = _make_dtype(uints, "UInt")
Int = _make_dtype(ints, "Int")
Integer = _make_dtype(uints + ints, "Integer")
Float = _make_dtype(floats, "Float")
Complex = _make_dtype(complexes, "Complex")
Inexact = _make_dtype(floats + complexes, "Inexact")
Num = _make_dtype(uints + ints + floats + complexes, "Num")
# 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
Shaped = _make_dtype(_any_dtype, "Shaped")
Bool = _make_dtype(bools, "Bool")
UInt = _make_dtype(uints, "UInt")
Int = _make_dtype(ints, "Int")
Integer = _make_dtype(uints + ints, "Integer")
Float = _make_dtype(floats, "Float")
Complex = _make_dtype(complexes, "Complex")
Inexact = _make_dtype(floats + complexes, "Inexact")
Num = _make_dtype(uints + ints + floats + complexes, "Num")
Shaped = _make_dtype(_any_dtype, "Shaped")
if has_jax:
if jax.config.jax_enable_custom_prng:
_key_regex = re.compile(r"^key<\w+>$")
Key = _make_dtype(_key_regex, "Key")
PRNGKeyArray = Key[jax.Array, ""]
else:
Key = UInt32
PRNGKeyArray = Key[jax.Array, "2"]
Scalar = Shaped[jax.Array, ""]
ScalarLike = Shaped[jax.typing.ArrayLike, ""]
+114 -18
View File
@@ -17,31 +17,127 @@
# 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
import types
import weakref
storage = threading.local()
class _Jaxtyped:
def __init__(self, fn):
self.fn = fn
def __get__(self, instance, owner):
return ft.wraps(self.fn)(_Jaxtyped(self.fn.__get__(instance, owner)))
def __call__(self, *args, **kwargs):
try:
memo_stack = storage.memo_stack
except AttributeError:
memo_stack = storage.memo_stack = []
memo_stack.append(({}, {}, {}))
try:
return self.fn(*args, **kwargs)
finally:
memo_stack.pop()
_jaxtyped_fns = weakref.WeakSet()
def jaxtyped(fn):
return ft.wraps(fn)(_Jaxtyped(fn))
"""Used in conjunction with a runtime type checker. Decorate a function with this to
have shapes checked for consistency across multiple arguments.
Note that `@jaxtyped` is applied above the type checker.
!!! Example
```python
# Import both the annotation and the `jaxtyped` decorator from `jaxtyping`
from jaxtyping import Array, Float32, jaxtyped
# Use your favourite typechecker: usually one of the two lines below.
from typeguard import typechecked as typechecker
from beartype import beartype as typechecker
# Write your function. @jaxtyped must be applied above @typechecker!
@jaxtyped
@typechecker
def batch_outer_product(x: Float32[Array, "b c1"],
y: Float32[Array, "b c2"]
) -> Float32[Array, "b c1 c2"]:
return x[:, :, None] * y[:, None, :]
```
**Notes for advanced users**
Put precisely, all `isinstance` shape checks are scoped to the thread-local dynamic
context of a `jaxtyped` call. A new dynamic context will allow different dimensions
sizes to be bound to the same name. After this new dynamic context is finished
then the old one is returned to.
For example, this means you could leave off the `@jaxtyped` decorator to enforce
that this function use the same axes sizes as the function it was called from.
Likewise, this means you can use `isinstance` checks inside a function body
and have them contribute to the same collection of consistency checks performed
by a typechecker against its arguments. (Or even forgo a typechecker that analyses
arguments, and instead just do your own manual `isinstance` checks.)
Only `isinstance` checks that pass will contribute to the store of axis name-size
pairs; those that fail will not. As such it is safe to write e.g.
`assert not isinstance(x, Float32[Array, "foo"])`.
"""
if type(fn) is types.FunctionType and fn in _jaxtyped_fns:
return fn
elif 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"
)
# It'd be lovely if we could handle arbitrary descriptors, and not just the builtin
# ones. Unfortunately that means returning a class instance with a __get__ method,
# and that turns out to break loads of other things. See beartype issue #211 and
# jaxtyping issue #71.
elif isinstance(fn, classmethod):
return classmethod(jaxtyped(fn.__func__))
elif isinstance(fn, staticmethod):
return staticmethod(jaxtyped(fn.__func__))
elif isinstance(fn, property):
if fn.fget is None:
fget = None
else:
fget = jaxtyped(fn.fget)
if fn.fset is None:
fset = None
else:
fset = jaxtyped(fn.fset)
if fn.fdel is None:
fdel = None
else:
fdel = jaxtyped(fn.fdel)
return property(fget=fget, fset=fset, fdel=fdel)
else:
@ft.wraps(fn)
def wrapped_fn(*args, **kwargs):
try:
memo_stack = storage.memo_stack
except AttributeError:
memo_stack = storage.memo_stack = []
memo_stack.append(({}, {}, {}))
try:
return fn(*args, **kwargs)
finally:
memo_stack.pop()
_jaxtyped_fns.add(wrapped_fn)
return wrapped_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
+161 -50
View File
@@ -50,12 +50,13 @@
import ast
import functools as ft
import sys
from importlib.abc import MetaPathFinder
from importlib.machinery import SourceFileLoader
from importlib.util import cache_from_source, decode_source
from inspect import isclass
from typing import Iterable, List, Optional, Tuple
from typing import List, Optional, Sequence, Union
from unittest.mock import patch
@@ -64,8 +65,31 @@ def _call_with_frames_removed(f, *args, **kwargs):
return f(*args, **kwargs)
def _optimized_cache_from_source(path, debug_override=None):
return cache_from_source(path, debug_override, optimization="jaxtyping")
def _optimized_cache_from_source(typechecker_hash, /, path, debug_override=None):
# 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?
# Version 5: Added support for string-based `typechecker` argument.
# Version 6: optimization tag now depends on `typechecker` argument, so that
# changing the typechecker will hit a different cache.
return cache_from_source(
path, debug_override, optimization=f"jaxtyping6{typechecker_hash}"
)
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
def _str_lookup(string):
module = ast.parse(string)
(expr,) = module.body
return expr.value
class _JaxtypingTransformer(ast.NodeVisitor):
@@ -84,7 +108,7 @@ class _JaxtypingTransformer(ast.NodeVisitor):
else:
node.body.insert(i, ast.Import(names=[ast.alias("jaxtyping", None)]))
if self._typechecker is not None:
typechecker_module, _ = self._typechecker
typechecker_module, _ = self._typechecker.split(".", 1)
node.body.insert(
i, ast.Import(names=[ast.alias(typechecker_module, None)])
)
@@ -95,31 +119,41 @@ 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 = [_str_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.
#
#
# FWIW, typeguard also wants to be at the end of the decorator list, as it
# works by recompiling the wrapped function.
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(_str_lookup(self._typechecker))
self._parents.append(node)
self.generic_visit(node)
self._parents.pop()
@@ -130,6 +164,7 @@ class _JaxtypingLoader(SourceFileLoader):
def __init__(self, *args, typechecker, **kwargs):
super().__init__(*args, **kwargs)
self._typechecker = typechecker
self._typechecker_hash = str(abs(hash(self._typechecker)))
def source_to_code(self, data, path, *, _optimize=-1):
source = decode_source(data)
@@ -153,7 +188,7 @@ class _JaxtypingLoader(SourceFileLoader):
# patch safe
with patch(
"importlib._bootstrap_external.cache_from_source",
_optimized_cache_from_source,
ft.partial(_optimized_cache_from_source, self._typechecker_hash),
):
return super().exec_module(module)
@@ -215,51 +250,127 @@ class ImportHookManager:
# Deliberately no default for `typechecker` so that folks must opt-in to not having
# a typechecker.
def install_import_hook(
modules: Iterable[str], typechecker: Optional[Tuple[str, str]]
) -> ImportHookManager:
"""Automatically apply `@jaxtyped`, and optionally a type checker, to all classes
and functions.
def install_import_hook(modules: Union[str, Sequence[str]], typechecker: Optional[str]):
"""Automatically apply `@jaxtyped`, and optionally a type checker, as decorators.
It will only be applied to modules loaded **after** this hook has been installed.
!!! Tip "Usage"
```python
from jaxtyping import install_import_hook
# Plus any one of the following:
# decorate @jaxtyped and @typeguard.typechecked
with install_import_hook("foo", "typeguard.typechecked"):
import foo # Any module imported inside this `with` block, whose
import foo.bar # name begins with the specified string, will
import foo.bar.qux # automatically have both `@jaxtyped` and the specified
# typechecker applied to all of their functions.
# decorate @jaxtyped and @beartype.beartype
with install_import_hook("foo", "beartype.beartype"):
...
# decorate only @jaxtyped (if you want that for some reason)
with install_import_hook("foo", None):
...
```
If you don't like using the `with` block, the hook can be used without that:
```python
hook = install_import_hook(...):
import ...
hook.uninstall()
```
The import hook can be applied to multiple packages via
```python
install_import_hook(["foo", "bar.baz"], ...)
```
The import hook will automatically decorate all functions, and the `__init__` method
of dataclasses.
If the function already has any decorators on it, then both the `@jaxtyped` and the
typechecker decorators will get added at the bottom of the decorator list, e.g.
```python
@some_other_decorator
@jaxtyped
@beartype.beartype
def foo(...): ...
```
**Arguments:**:
- `packages`: the names of the modules in which to automatically apply `@jaxtyped`
- `modules`: the names of the modules in which to automatically apply `@jaxtyped`
and `@typechecked`.
- `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.
string. 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.
**Returns:**
A context manager that uninstalls the hook on exit, or when you call `.uninstall()`.
**Example:**
??? Example "Example: end-user script"
Typically you should apply this import hook at the entry point for your own scripts:
```python
# entry_point.py
from jaxtyped import install_import_hook
install_import_hook("main", ("beartype", "beartype"))
import main
... # do whatever you're doing
```python
### entry_point.py
from jaxtyping import install_import_hook
with install_import_hook("main", "typeguard.typechecked"):
import main
# main.py
from jaxtyped import f32
### main.py
from jaxtyping import Array, Float32
def f(x: Float32[Array, "batch channels"]):
...
```
??? Example "Example: writing a library"
```python
### __init__.py
from jaxtyping import install_import_hook
with install_import_hook("my_library_name", "beartype.beartype"):
from .subpackage import foo # full name is my_library_name.subpackage so
# will be hook'd
from .another_subpackage import bar # full name is my_library_name.another_subpackage
# so will be hook'd.
```
??? info "Pytest hook"
The import hook can be installed at test-time only, as a pytest hook. From the
command line the syntax is:
```
pytest --jaxtyping-packages=foo,bar.baz,beartype.beartype
```
or in `pyproject.toml`:
```toml
[tool.pytest.ini_options]
addopts = "--jaxtyping-packages=foo,bar.baz,beartype.beartype"
```
or in `pytest.ini`:
```ini
[pytest]
addopts = --jaxtyping-packages=foo,bar.baz,beartype.beartype
```
This example will apply the import hook to all modules whose names start with
either `foo` or `bar.baz`. The typechecker used in this example is
`beartype.beartype`.
(This is the author's preferred approach to performing runtime type-checking
with jaxtyping!)
""" # noqa: E501
def f(x: f32["b c"]):
pass
```
Which as you can see means you never to import `@jaxtyped`, nor do you need to
import the typechecker directly (e.g. `beartype.beartype` or
`typeguard.typechecked`).
"""
if isinstance(modules, str):
modules = [modules]
# Support old less-flexible API.
if isinstance(typechecker, tuple):
typechecker = ".".join(typechecker)
for i, finder in enumerate(sys.meta_path):
if (
isclass(finder)
+1 -1
View File
@@ -52,4 +52,4 @@ def pytest_configure(config):
)
raise RuntimeError(message.format(", ".join(already_imported_packages)))
install_import_hook(packages, typechecker.rsplit(".", 1))
install_import_hook(packages, typechecker)
+31 -7
View File
@@ -18,9 +18,10 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import functools as ft
import typing
from typing import Generic, TypeVar
import jax
import jax.tree_util as jtu
import typeguard
@@ -51,10 +52,21 @@ class _MetaPyTree(type):
def __getitem__(cls, item):
name = str(_FakePyTree[item])
out = _MetaSubscriptPyTree(name, (), {"leaftype": item})
out.__module__ = "jaxtyping"
if getattr(typing, "GENERATING_DOCUMENTATION", False):
out.__module__ = "builtins"
else:
out.__module__ = "jaxtyping"
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")
@@ -73,17 +85,29 @@ 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))
PyTree = _MetaPyTree("PyTree", (), {})
PyTree.__module__ = "jaxtyping"
# 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.
PyTree = _MetaPyTree("PyTree", (), {})
if getattr(typing, "GENERATING_DOCUMENTATION", False):
PyTree.__module__ = "builtins"
else:
PyTree.__module__ = "jaxtyping"
PyTree.__doc__ = """Represents a PyTree.
Each PyTree is denoted by a type `PyTree[LeafType]`, such as `PyTree[int]` or
`PyTree[Union[str, Float32[Array, "b c"]]]`.
You can leave off the `[...]`, in which case `PyTree` is simply a suggestively-named
alternative to `Any`.
([By definition all types are PyTrees.](https://jax.readthedocs.io/en/latest/pytrees.html))
""" # noqa: E501
+100
View File
@@ -0,0 +1,100 @@
theme:
name: material
features:
- navigation.sections # Sections are included in the navigation on the left.
- toc.integrate # Table of contents is integrated on the left; does not appear separately on the right.
- header.autohide # header disappears as you scroll
palette:
# Light mode / dark mode
# We deliberately don't automatically use `media` to check a user's preferences. We default to light mode as
# (a) it looks more professional, and (b) is more obvious about the fact that it offers a (dark mode) toggle.
- scheme: default
primary: white
accent: amber
toggle:
icon: material/weather-night
name: Switch to dark mode
- scheme: slate
primary: black
accent: amber
toggle:
icon: material/weather-sunny
name: Switch to light mode
icon:
repo: fontawesome/brands/github # GitHub logo in top right
logo: "material/check-network-outline" # jaxtyping logo in top left
favicon: "_static/favicon.png"
custom_dir: "docs/_overrides" # Overriding part of the HTML
# These additions are my own custom ones, having overridden a partial.
twitter_name: "@PatrickKidger"
twitter_url: "https://twitter.com/PatrickKidger"
site_name: jaxtyping
site_description: The documentation for the jaxtyping software library.
site_author: Patrick Kidger
site_url: https://docs.kidger.site/jaxtyping
repo_url: https://github.com/google/jaxtyping
repo_name: google/jaxtyping
edit_uri: "" # No edit button, as some of our pages are in /docs and some in /examples via symlink, so it's impossible for them all to be accurate
strict: true # Don't allow warnings during the build process
extra_javascript:
# The below three make MathJax work, see https://squidfunk.github.io/mkdocs-material/reference/mathjax/
- _static/mathjax.js
- https://polyfill.io/v3/polyfill.min.js?features=es6
- https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
extra_css:
- _static/custom_css.css
markdown_extensions:
- pymdownx.arithmatex: # Render LaTeX via MathJax
generic: true
- pymdownx.superfences # Seems to enable syntax highlighting when used with the Material theme.
- pymdownx.details # Allowing hidden expandable regions denoted by ???
- pymdownx.snippets: # Include one Markdown file into another
base_path: docs
- admonition
- toc:
permalink: "¤" # Adds a clickable permalink to each section heading
toc_depth: 4
plugins:
- search # default search plugin; needs manually re-enabling when using any other plugins
- autorefs # Cross-links to headings
- include_exclude_files:
include:
- ".htaccess"
exclude:
- "_overrides"
- mknotebooks # Jupyter notebooks
- mkdocstrings:
handlers:
python:
setup_commands:
- import pytkdocs_tweaks, typing
- pytkdocs_tweaks.main()
- typing.GENERATING_DOCUMENTATION = False
selection:
inherited_members: true # Allow looking up inherited methods
rendering:
show_root_heading: true # actually display anything at all...
show_root_full_path: true # display "diffrax.asdf" not just "asdf"
show_if_no_docstring: true
show_signature_annotations: true
show_source: false # don't include source code
members_order: source # order methods according to their order of definition in the source code, not alphabetical order
heading_level: 4
nav:
- 'index.md'
- API:
- 'api/array.md'
- 'api/pytree.md'
- 'api/runtime-type-checking.md'
- 'api/advanced-features.md'
- 'faq.md'
+45
View File
@@ -0,0 +1,45 @@
[project]
name = "jaxtyping"
version = "0.2.16"
description = "Type annotations and runtime checking for shape and dtype of JAX arrays, and PyTrees."
readme = "README.md"
requires-python ="~=3.8"
license = {file = "LICENSE"}
authors = [
{name = "Patrick Kidger", email = "contact@kidger.site"},
]
keywords = ["jax", "neural-networks", "deep-learning", "equinox", "typing"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Intended Audience :: Financial and Insurance Industry",
"Intended Audience :: Information Technology",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Programming Language :: Python :: 3",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Topic :: Scientific/Engineering :: Information Analysis",
"Topic :: Scientific/Engineering :: Mathematics",
]
urls = {repository = "https://github.com/google/jaxtyping" }
dependencies = ["numpy>=1.20.0", "typeguard>=2.13.3", "typing_extensions>=3.7.4.1"]
entry_points = {pytest11 = ["jaxtyping = jaxtyping.pytest_plugin"]}
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build]
include = ["jaxtyping/*"]
[tool.ruff]
select = ["E", "F", "I001"]
ignore = ["E721", "E731", "F722"]
ignore-init-module-imports = true
[tool.ruff.isort]
combine-as-imports = true
lines-after-imports = 2
extra-standard-library = ["typing_extensions"]
order-by-type = false
-97
View File
@@ -1,97 +0,0 @@
# Copyright (c) 2022 Google LLC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# 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 pathlib
import re
import setuptools
_here = pathlib.Path(__file__).resolve().parent
name = "jaxtyping"
# for simplicity we actually store the version in the __version__ attribute in the
# source
with open(_here / name / "__init__.py") as f:
meta_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", f.read(), re.M)
if meta_match:
version = meta_match.group(1)
else:
raise RuntimeError("Unable to find __version__ string.")
author = "Patrick Kidger"
author_email = "contact@kidger.site"
description = (
"Type annotations and runtime checking for shape and dtype of JAX "
"arrays, and PyTrees."
)
with open(_here / "README.md", "r") as f:
readme = f.read()
url = "https://github.com/google/jaxtyping"
license = "MIT"
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Programming Language :: Python :: 3",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Topic :: Scientific/Engineering :: Mathematics",
]
python_requires = "~=3.7"
# We use typeguard internally (in a fairly minimal way), but it's not required that
# end users make the same choice.
install_requires = [
"jax>=0.3.4",
"numpy>=1.20.0",
"typeguard>=2.13.3",
"typing_extensions>=4.2.0",
]
entry_points = dict(pytest11=["jaxtyping = jaxtyping.pytest_plugin"])
setuptools.setup(
name=name,
version=version,
author=author,
author_email=author_email,
maintainer=author,
maintainer_email=author_email,
description=description,
long_description=readme,
long_description_content_type="text/markdown",
url=url,
license=license,
classifiers=classifiers,
zip_safe=False,
python_requires=python_requires,
install_requires=install_requires,
entry_points=entry_points,
packages=[name],
)
+19 -4
View File
@@ -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
+28
View File
@@ -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))
+62
View File
@@ -0,0 +1,62 @@
# Copyright (c) 2022 Google LLC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# 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
from jaxtyping import Float32
from .helpers import ParamError
def g(x: Float32[jnp.ndarray, " b"]):
pass
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))
+62
View File
@@ -0,0 +1,62 @@
# Copyright (c) 2022 Google LLC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# 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
from jaxtyping import Float32
from .helpers import ParamError
def g(x: Float32[jnp.ndarray, " b"]):
pass
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))
+28
View File
@@ -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,4 +17,4 @@
# 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 . import another_file
from . import another_file # noqa: F401
@@ -32,3 +32,17 @@ def g(x: Float32[jnp.ndarray, " b"]):
g(jnp.array([1.0]))
with pytest.raises(ParamError):
g(jnp.array(1))
# Typeguard 3.0 no longer supports this
#
# 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))
+29
View File
@@ -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 jax.numpy as jnp
import pytest
@@ -32,3 +33,31 @@ def g(x: Float32[jnp.ndarray, " b"]):
g(jnp.array([1.0]))
with pytest.raises(ParamError):
g(jnp.array(1))
# Typeguard 3.0 no longer supports this.
#
# 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))
+63
View File
@@ -0,0 +1,63 @@
# Copyright (c) 2022 Google LLC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# 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 jax.numpy as jnp
import pytest
from jaxtyping import Float32
from .helpers import ParamError
def g(x: Float32[jnp.ndarray, " b"]):
pass
g(jnp.array([1.0]))
with pytest.raises(ParamError):
g(jnp.array(1))
# Typeguard 3.0 no longer supports this.
#
# 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))
+1
View File
@@ -2,3 +2,4 @@ equinox>=0.5.3
pytest>=7.0.1
beartype>=0.10.4
typeguard>=2.13.3
cloudpickle>=2.2.1
+167 -1
View File
@@ -17,11 +17,25 @@
# 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 sys
from typing import get_args, get_origin, Union
import jax.numpy as jnp
import jax.random as jr
import numpy as np
import pytest
import torch
from jaxtyping import AbstractDtype, Array, Float, Float32, jaxtyped, Shaped
from jaxtyping import (
AbstractDtype,
Array,
ArrayLike,
Float,
Float32,
jaxtyped,
PRNGKeyArray,
Shaped,
)
from .helpers import ParamError, ReturnError
@@ -409,3 +423,155 @@ 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"],
}
def test_subclass():
assert issubclass(Float[Array, ""], Array)
assert issubclass(Float[np.ndarray, ""], np.ndarray)
assert issubclass(Float[torch.Tensor, ""], torch.Tensor)
def test_ignored_names():
x = Float[np.ndarray, "foo=4"]
assert isinstance(np.zeros(4), x)
assert not isinstance(np.zeros(5), x)
assert not isinstance(np.zeros((4, 5)), x)
y = Float[np.ndarray, "bar qux foo=bar+qux"]
assert isinstance(np.zeros((2, 3, 5)), y)
assert not isinstance(np.zeros((2, 3, 6)), y)
z = Float[np.ndarray, "bar #foo=bar"]
assert isinstance(np.zeros((3, 3)), z)
assert isinstance(np.zeros((3, 1)), z)
assert not isinstance(np.zeros((3, 4)), z)
# Weird but legal
w = Float[np.ndarray, "bar foo=#bar"]
assert isinstance(np.zeros((3, 3)), w)
assert isinstance(np.zeros((3, 1)), w)
assert not isinstance(np.zeros((3, 4)), w)
def test_symbolic_functions():
x = Float[np.ndarray, "foo bar min(foo,bar)"]
assert isinstance(np.zeros((2, 3, 2)), x)
assert isinstance(np.zeros((3, 2, 2)), x)
assert not isinstance(np.zeros((3, 2, 4)), x)
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires Python 3.10")
def test_py310_unions():
x = np.zeros(3)
y = Shaped[Array | np.ndarray, "_"]
assert isinstance(x, get_args(y))
def test_key(typecheck):
@jaxtyped
@typecheck
def f(x: PRNGKeyArray):
pass
x = jr.PRNGKey(0)
f(x)
with pytest.raises(ParamError):
f(object())
with pytest.raises(ParamError):
f(1)
with pytest.raises(ParamError):
f(jnp.array(3))
with pytest.raises(ParamError):
f(jnp.array(3.0))
def test_extension(typecheck, getkey):
X = Shaped[Array, "a b"]
Y = Shaped[X, "c d"]
Z = Shaped[Array, "c d a b"]
assert str(Z) == str(Y)
X = Float[Array, "a"]
Y = Float[X, "b"]
@jaxtyped
@typecheck
def f(a: X, b: Y):
...
a = jr.normal(getkey(), (3, 4))
b = jr.normal(getkey(), (4,))
c = jr.normal(getkey(), (3,))
f(b, a)
with pytest.raises(ParamError):
f(c, a)
with pytest.raises(ParamError):
f(a, a)
@typecheck
def g(a: Shaped[PRNGKeyArray, "2"]):
...
with pytest.raises(ParamError):
g(jr.PRNGKey(0))
g(jr.split(jr.PRNGKey(0)))
with pytest.raises(ParamError):
g(jr.split(jr.PRNGKey(0), 3))
+69 -5
View File
@@ -1,13 +1,77 @@
import abc
from jaxtyping import jaxtyped
class M:
class M(metaclass=abc.ABCMeta):
@jaxtyped
def f(self):
...
@jaxtyped
@classmethod
def f(cls):
def g1(cls):
return 3
@classmethod
@jaxtyped
def g2(cls):
return 4
# Check that the @jaxtyped decorator doesn't blat the __get__ of @classmethod
def test_decorator():
assert M.f() == 3
@jaxtyped
@staticmethod
def h1():
return 3
@staticmethod
@jaxtyped
def h2():
return 4
@jaxtyped
@abc.abstractmethod
def i1(self):
...
@abc.abstractmethod
@jaxtyped
def i2(self):
...
class N:
@jaxtyped
@property
def j1(self):
return 3
@property
@jaxtyped
def j2(self):
return 4
def test_identity():
assert M.f is M.f
def test_classmethod():
assert M.g1() == 3
assert M.g2() == 4
def test_staticmethod():
assert M.h1() == 3
assert M.h2() == 4
# Check that the @jaxtyped decorator doesn't blat the __isabstractmethod__ of
# @abstractmethod
def test_abstractmethod():
assert M.i1.__isabstractmethod__
assert M.i2.__isabstractmethod__
def test_property():
assert N().j1 == 3
assert N().j2 == 4
+42 -13
View File
@@ -22,13 +22,33 @@ import pytest
from jaxtyping import install_import_hook
def test_import_hook_typeguard_old():
hook = install_import_hook(
"test.import_hook_tester_typeguard_old", ("typeguard", "typechecked")
)
with hook:
from . import import_hook_tester_typeguard_old # noqa: F401
def test_import_hook_typeguard():
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_old():
try:
import beartype # noqa: F401
except ImportError:
pytest.skip("Beartype not installed")
else:
hook = install_import_hook(
"test.import_hook_tester_beartype_old", ("beartype", "beartype")
)
with hook:
from . import import_hook_tester_beartype_old # noqa: F401
def test_import_hook_beartype():
@@ -38,26 +58,35 @@ def test_import_hook_beartype():
pytest.skip("Beartype not installed")
else:
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_beartype_full():
try:
import beartype # noqa: F401
except ImportError:
pytest.skip("Beartype not installed")
else:
bearchecker = "beartype.beartype(conf=beartype.BeartypeConf(strategy=beartype.BeartypeStrategy.On))" # noqa: E501
hook = install_import_hook("test.import_hook_tester_beartype_full", bearchecker)
with hook:
from . import import_hook_tester_beartype_full # noqa: F401
def test_import_hook_transitive():
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
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")
"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
View File
@@ -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)),
)
)
+16
View File
@@ -0,0 +1,16 @@
import cloudpickle
import numpy as np
import torch
from jaxtyping import AbstractArray, Array, Shaped
def test_pickle():
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(y)
cloudpickle.loads(z)
cloudpickle.loads(w)
+25 -2
View File
@@ -25,7 +25,20 @@ from typeguard import typechecked
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
def join(self, timeout=None):
super().join(timeout)
if hasattr(self, "exc"):
raise self.exc
def test_threading_jaxtyped():
@jaxtyped
@typechecked
def add(x: Float[Array, "a b"], y: Float[Array, "a b"]) -> Float[Array, "a b"]:
@@ -36,6 +49,16 @@ def test_threading():
b = jnp.array([[2.0, 3.0]])
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.join()