Compare commits

...
129 Commits
Author SHA1 Message Date
Michael J Clark 1456302503 link to why use type annotations 2024-05-19 09:16:10 +08:00
Michael J Clark 1c0186a1c9 tf, np, torch examples 2024-05-19 09:12:49 +08:00
Patrick Kidger 0d83ee77e6 Updated ecosystem again 2024-04-20 11:25:42 +02:00
Patrick Kidger 51ac630ef0 Updated ecosystem 2024-04-20 11:18:48 +02:00
Sergei Lebedev f83170e01d Define the IPython magic lazily
IPython is quite a chunky package, and importing it unconditionally makes
jaxtyping itself relatively slow to import.
2024-04-17 09:45:56 +02:00
Piotr Kaminski 07e58de0bd Replace ndim with len(shape) 2024-03-11 20:35:29 +01:00
Andy Rock 696cc5b90f also require jaxlib 2024-03-08 18:17:50 +01:00
Patrick Kidger 102e499d61 Fixes #188. 2024-03-07 18:28:08 +01:00
Patrick Kidger f708d1742b Quick fix for docs not generating correctly since the last jaxtyping release 2024-03-06 20:34:48 +01:00
Alex Ford 5e2518c591 Fix _check_shape str formatting for variadics 2024-03-06 20:21:03 +01:00
Patrick Kidger 1b3173ac01 Bump version 2024-02-25 12:07:01 +00:00
Roman Knyazhitskiy 172b83b4fc Adding a test for generator support (#171)
* Add a test for generators

* Remove output annotations from decorators

Also guarded torch imports for better compatibility with
requirements.txt

* Add flag to the main meta class to skip the typecheck

* Return to the old solution

* Make async tests work

* Minor adjustments/fixing typos

* Correct Python path for new tests

* Remove some jax-dependent code

* Implement equality for MetaArrays

* Make all Dim variations frozen dataclasses

* Shorten AbstractArray methods

* Final touches

* Removing get_origin use

* Update tests with @jaxtyp
2024-02-25 12:07:01 +00:00
Patrick Kidger 17ea4b13eb No longer imports JAX at all! This is done dynamically when required. See #178 2024-02-25 12:07:01 +00:00
jianlijianli 9beb5f2d29 Add int4/uint4 support in jaxtyping. (#174)
* Add int4/uint4 support in jaxtyping.

* Fix a typo and update api docs.
2024-02-25 12:07:01 +00:00
Patrick Kidger 1d4d40294c Added support for beartype 0.17.0's __instancecheck_str__.
Recall that jaxtyping will currently generate rich error messages in precisely one scenario: about the arguments and return types when doing:
```python
@jaxtyped(typechecker=beartype)
def foo(...): ...
```

With this commit we add support for beartype 0.17.0's pseudo-standard `__instancecheck_str__`, which means the following:

1. For those using beartype decorators, the following will *also* generate an informative error message, and moreover it will state exactly why (shape mismatch, dtype mismatch etc):
    ```python
    @jaxtyped(typechecker=None)
    @beartype
    def foo(...): ...
    ```
    (In practice we probably won't recommend the above combination in the docs just to keep things simple.)

2. For those using the beartype import hook together with the jaxtyping import hook, we can probably also check `assert isinstance(x, Float[Array, "foo"])` statements with rich error messages. (#153) We'll need to test + document that though. (@jeezrick interested?)

3. For those using plain `assert isinstance(...)` statements without beartype (#167, tagging @reinerp), then they can *also* get rich error messages by doing
    ```python
    tt = Float[Array, "foo"]
    assert isinstance(x, tt), tt.__instancecheck_str__(x) + "\n" + print_bindings()
    ```
    which is still a bit long-winded right now but is a step in the right direction.

(CC @leycec for interest.)
2024-02-25 12:07:01 +00:00
Patrick Kidger 28ad5275d7 Moved print_bindings into storage.py 2024-02-25 12:07:01 +00:00
Patrick Kidger d7fd59a34c Added print_bindings. 2024-02-25 12:07:01 +00:00
Afroz Mohiuddin 8de8c0bb68 Correct pytree path in array.md
Correct pytree path in array.md
2024-02-12 15:27:03 +00:00
Patrick Kidger f18de2ce28 Added better docs on stringified type annotations 2024-01-08 05:45:32 -08:00
Patrick Kidger eb9a23df63 Update dataclass docs (#155)
* Update dataclass docs
2024-01-05 13:17:45 +00:00
Jérome Eertmans adf1a5e4e3 chore(docs): fix typos in docstrings
Hello!

This is a small PR to fix typos in docstrings.

Maybe, I would suggest adding an import for `dataclass` in the example (otherwise it will not run), and maybe indicate that it works with other dataclasses decorators, like the `dataclass` decorator from chex.
2024-01-05 04:36:08 -08:00
Patrick Kidger 272be74e01 Bump version 2023-12-15 10:34:45 -08:00
Patrick Kidger 7df267efa4 Upgrade to ruff-format 2023-12-10 15:27:42 -08:00
Patrick Kidger d43933f942 Updated to latest pyktdocs_tweaks 2023-12-09 14:55:30 -08:00
Patrick Kidger 1acc0d7153 Improved error messages a little bit, in particular to highlight individual problematic arguments. 2023-12-08 10:17:57 -08:00
Patrick Kidger 33cf4fcdac Simplified internals by removing jaxtyping_raise; jaxtyping_malformed. 2023-12-05 19:06:00 -08:00
Patrick Kidger e5cc75e4a3 Removed internal jaxtyped_fns registry that is no longer needed. 2023-12-05 19:06:00 -08:00
Patrick Kidger 125bc89ee9 Added environment config flags.
These flags are `JAXTYPING_DISABLE` and `JAXTYPING_REMOVE_TYPECHECKER_STACK`.

In addition, have now added warnings when using old-style double-decorator syntax, which also serves to guard against the easy mistake of
```python
@jaxtyped(typechecker)
def foo(...)
```
which actually decorates the `typechecker`, not `foo`.
2023-12-05 19:06:00 -08:00
Patrick Kidger 8e47c9081c version bump 2023-11-27 09:50:02 -08:00
Patrick Kidger 205978958f Fix install_import_hook(..., None) 2023-11-27 09:50:02 -08:00
Patrick Kidger 850f4e72cd Handle beartype doing isinstance(None, hint) 2023-11-27 09:50:02 -08:00
Patrick Kidger 0a76c9c70c Error message improvements 2023-11-27 09:50:02 -08:00
Patrick Kidger 5fbd6718ab Added support for 'self' in dataclass attribute annotations; switched from args and kwargs to just arguments. 2023-11-27 09:50:02 -08:00
Patrick Kidger 80a99568f7 Removed unused elements from unions, e.g. Float[ArrayLike, ...] will no longer include Float[np.bool, ...]. 2023-11-27 09:50:02 -08:00
Patrick Kidger baffbef5ca Doc fix 2023-11-27 09:50:02 -08:00
Patrick Kidger 127eae56b7 array shapes+dtypes no longer checked as part of pytree flattening. This avoids edge-case crash when using pytree-path dependent sizes 2023-11-27 09:50:02 -08:00
Patrick Kidger 58600d3fe0 Fixed new-style PRNG keys. 2023-11-27 09:50:02 -08:00
Patrick Kidger 7925e278f4 Symbolic expressions now support delayed binding to arguments. Fixes #93. 2023-11-27 09:50:02 -08:00
Patrick Kidger ba3b2027cc Standardised terminology: now using just "axis"/"axes", not "dimension" 2023-11-27 09:50:02 -08:00
Patrick Kidger 12d540794f Pretty error messages: fixes #6.
Phew, this ended up being a pretty complicated change!
The basic summary is that we now support the syntax
```
@jaxtyped(typechecker=typechecker)
def f(...): ...
```
and when using this, we now get pretty error messages about what went
wrong.

(
The old syntax, i.e.
```
@jaxtyped
@typechecker
def f(...): ...
```
is still supported, but doesn't give much information.
)

The internals of this do quite a lot of magic! In particular we
dynamically create quite a lot of functions and test the provided
arguments against their signatures. The overhead should still be
minimal under `jax.jit`, though.
(TODO: what's the overhead like in non-jit situations, e.g. PyTorch?
I've tried to minimise the overhead throughout just to be sure, but
perhaps PyTorch users should stick to the old syntax?)
2023-11-27 09:50:02 -08:00
Patrick Kidger 63e0fdff74 Typecheck errors now state the size of the stored axis and structure values. 2023-11-27 09:50:02 -08:00
Patrick Kidger d12291de7e Added support for treepath-dependent sizes. 2023-11-27 09:50:02 -08:00
Patrick Kidger 9e1ba8a77d Added support for declaring PyTree structures. 2023-11-27 09:50:02 -08:00
Patrick Kidger e4a93ee218 Added with jaxtyped("context"):, for now undocumented. 2023-11-07 11:34:40 -08:00
Patrick Kidger 260fb36876 Fixed mixing variadic+broadcast with variadic+nonbroadcast dimensions.
Previously, something like this would not raise an error, as
variadic+broadcast dimensions were stored in a separate namespace to
variadic+nonbroadcast dimensions:
```python
def f(x: Float[Array, "*foo"], y: Float[Array, "#*foo"]):
    pass

a, b = ...
assert a.shape == (3, 4)
assert b.shape == (5,)
f(a, b)
```
2023-11-07 11:34:40 -08:00
Patrick Kidger 9646eff7e1 Fixes for g3 2023-10-23 22:00:51 -07:00
Patrick Kidger 1a048b1f2f Fixed Float[ArrayLike, "#*foo"] leaving out bool/int/float/complex. 2023-10-21 12:01:21 -07:00
Patrick Kidger 338ca631c6 beartype error messages for dataclass attributes now use the correct name 2023-10-21 11:38:44 -07:00
Patrick Kidger 2ab8286c81 Error message for bad symbolic shapes is now useful. 2023-10-21 11:38:04 -07:00
Patrick Kidger 7f85a12a85 Added DTypeLike to match jax.typing 2023-10-17 09:43:31 -07:00
Patrick Kidger e55348a4b4 Added jaxtyping.Real 2023-10-17 09:33:03 -07:00
Patrick Kidger 7a84b27da9 Bump version 2023-10-11 11:25:06 -07:00
Roma Knyaz 77c263c3de Allow only typeguard lower than 3.x.x version 2023-10-10 10:18:49 -07:00
Patrick Kidger 91a36aaee4 dataclasses now have fields checked, not __init__.
Previously, using the import hook with dataclasses resulted in the `__init__` method of the dataclass being checked.
This was undesirable when using `eqx.field(converter=...)`, as the annotation didn't necessarily reflect the argument type.
A typical example was
```python
class Foo(eqx.Module):
    x: jax.Array = eqx.field(converter=jnp.ndarray)

Foo(1)  # 1 is not an array! But this code is valid.
```

After this change, we instead monkey-patch our checks to happen at the end of the `__init__` of the dataclass -- after conversion has run.

Note that this requires https://github.com/patrick-kidger/equinox/pull/524. Otherwise, Equinox does conversion too late (in `_ModuleMeta.__call__`, after `__init__` has been run).
2023-10-09 21:58:14 -07:00
Roma Knyaz 513a54b048 Better handling of user-defined typechecker 2023-10-09 10:21:07 -07:00
Patrick Kidger 9c9635d4f3 Add Orbax to ecosystem list. 2023-10-06 15:51:22 +01:00
Patrick Kidger c3e7fd35a2 Update ecosystem links 2023-10-06 15:43:29 +01:00
Roma Knyaz ef102f40b4 Bump up pre-commit ruff and black versions 2023-10-02 13:43:36 -07:00
Roma Knyaz 4917c2e30f Fix flakiness of transitive import hook test 2023-10-02 09:03:06 -07:00
Patrick Kidger 17092ad8d8 Simplified the import hook tests 2023-09-27 15:37:56 -07:00
Patrick Kidger 75392d6330 Added missing license headers 2023-09-27 15:37:56 -07:00
Patrick Kidger 18b8e76d67 Be tolerant of faulty IPython installs. 2023-09-25 18:15:02 -07:00
Patrick Kidger 1e5229c20e Should be more robust to jax/numpy/tensorflow version changes 2023-09-25 11:07:36 -07:00
Patrick Kidger e05985df2b Document IPython extension and version nump 2023-09-20 11:33:19 -07:00
Roma Knyaz f454cb797c Make jaxtyping an IPython extension 2023-09-20 10:34:32 -07:00
Patrick Kidger d2785baced Better suppotr for incomplete JAX installations. Supersedes #105. 2023-09-14 20:13:06 -07:00
Patrick Kidger c80c1264d3 Error message now mentions both array type and shape requirements. Supersedes #107. 2023-09-14 20:09:48 -07:00
Patrick Kidger e308695293 Updated to support both new and old style JAX PRNG keys, as they are going to co-exist simultaneously. See https://github.com/google/jax/pull/17297 2023-09-14 20:00:46 -07:00
Patrick Kidger e347c480d5 Hash is now stable across python runtimes 2023-08-17 16:23:33 +01:00
Patrick Kidger 13e6870fb8 Compatibility with JAX changes to opaque dtypes 2023-07-26 09:33:32 -07:00
Patrick Kidger 4c90808401 jaxtyping no longer appears in tracebacks 2023-07-26 09:33:32 -07:00
Patrick Kidger 5a57456e15 document Levanter 2023-07-12 19:36:09 +01:00
Patrick Kidger a6ab6c0d28 Have PRNGKeyArray appear correctly in downstream documentation 2023-06-29 18:39:22 -07:00
Patrick Kidger 83be9e9d16 Fixed jaxtyping doc generation 2023-06-25 12:00:16 -07:00
Patrick Kidger d2aa9c1e8d Merge branch 'main' of https://github.com/google/jaxtyping 2023-06-14 10:42:32 -07:00
Patrick Kidger 926dc53856 Have PyTreeDef appear correctly in docs 2023-06-14 10:42:14 -07:00
Patrick Kidger edc34f14f8 Update ecosystem links. 2023-06-07 15:35:45 +01:00
Patrick Kidger 8fa15050bc Update ecosystem links. 2023-06-07 15:35:14 +01:00
Patrick Kidger 356f5b7f7b Build fixes 2023-06-01 11:06:02 -07:00
Patrick Kidger 1b9c9fab52 Bump to Py3.9 2023-06-01 10:56:00 -07:00
Patrick Kidger 066a5b058f Made modules private. 2023-06-01 10:56:00 -07:00
Patrick Kidger 319d54abcf Avoid __builtins__ getting added as a key 2023-06-01 10:56:00 -07:00
Patrick Kidger 6a64ef114e Now provides PyTreeDef, and can detect PyTrees via issubclass(x, PyTree) 2023-06-01 10:56:00 -07:00
Patrick Kidger 10e1852b37 Fix favicon 2023-05-12 11:57:06 -07:00
Patrick Kidger a19149d23d Fixed pytest hook 2023-05-11 09:08:09 -07:00
Patrick Kidger 0c596ff373 Fix for non-JAX installations. 2023-05-10 17:16:42 -07:00
Patrick Kidger 7934d2afed Static typing fixes 2023-05-10 16:35:45 -07:00
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
65 changed files with 5282 additions and 1623 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 runs-on: ubuntu-latest
steps: steps:
- name: Release - name: Release
uses: patrick-kidger/action_update_python_project@v1 uses: patrick-kidger/action_update_python_project@v2
with: with:
python-version: "3.8" python-version: "3.11"
test-script: | test-script: |
python -m pip install pytest beartype equinox jaxlib python -m pip install -r ${{ github.workspace }}/test/requirements.txt
python -m pip install torch --extra-index-url https://download.pytorch.org/whl/cpu
cp -r ${{ github.workspace }}/test ./test cp -r ${{ github.workspace }}/test ./test
pytest pytest
pypi-token: ${{ secrets.pypi_token }} pypi-token: ${{ secrets.pypi_token }}
+3 -2
View File
@@ -26,7 +26,7 @@ jobs:
run-tests: run-tests:
strategy: strategy:
matrix: matrix:
python-version: [ 3.7, 3.8, 3.9 ] python-version: [ 3.11 ]
os: [ ubuntu-latest ] os: [ ubuntu-latest ]
fail-fast: false fail-fast: false
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@@ -42,7 +42,8 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install pytest wheel beartype equinox jaxlib python -m pip install -r test/requirements.txt
python -m pip install torch --extra-index-url https://download.pytorch.org/whl/cpu
- name: Checks with pre-commit - name: Checks with pre-commit
uses: pre-commit/action@v2.0.3 uses: pre-commit/action@v2.0.3
+2 -1
View File
@@ -2,4 +2,5 @@
*.egg-info *.egg-info
build/ build/
dist/ 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
+7 -17
View File
@@ -18,21 +18,11 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
repos: repos:
- repo: https://github.com/ambv/black - repo: https://github.com/astral-sh/ruff-pre-commit
rev: 22.3.0 rev: v0.1.7
hooks: hooks:
- id: black - id: ruff # linter
- repo: https://github.com/nbQA-dev/nbQA types_or: [ python, pyi, jupyter ]
rev: 1.2.3 args: [ --fix ]
hooks: - id: ruff-format # formatter
- id: nbqa-black types_or: [ python, pyi, jupyter ]
- 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
-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: Next verify the tests all pass:
```bash ```bash
pip install pytest pip install pytest cloudpickle
pip install torch --extra-index-url https://download.pytorch.org/whl/cpu
pytest pytest
``` ```
-2
View File
@@ -1,2 +0,0 @@
include LICENSE
prune tests
+39 -24
View File
@@ -1,15 +1,20 @@
<h1 align="center">jaxtyping</h1> <h1 align="center">jaxtyping</h1>
Type annotations **and runtime checking** for: [Use type annotations **and runtime type-checking**](https://jax.readthedocs.io/en/latest/jep/12049-type-annotations.html) 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). 2. [PyTrees](https://jax.readthedocs.io/en/latest/pytrees.html).
**For example:** **For example:**
```python ```python
from jaxtyping import Array, Float, PyTree from jaxtyping import Array, Float, PyTree, , UInt, Int, Bool
import torch
impport numpy as np
import tensorflow as tf
# Accepts floating-point 2D arrays with matching dimensions
# Accepts floating-point 2D arrays with matching axes
def matrix_multiply(x: Float[Array, "dim1 dim2"], def matrix_multiply(x: Float[Array, "dim1 dim2"],
y: Float[Array, "dim2 dim3"] y: Float[Array, "dim2 dim3"]
) -> Float[Array, "dim1 dim3"]: ) -> Float[Array, "dim1 dim3"]:
@@ -20,6 +25,15 @@ def accepts_pytree_of_ints(x: PyTree[int]):
def accepts_pytree_of_arrays(x: PyTree[Float[Array, "batch c1 c2"]]): def accepts_pytree_of_arrays(x: PyTree[Float[Array, "batch c1 c2"]]):
... ...
def accepts_torch.Long(x: Int[torch.Tensor, "batch channel height width"]):
....
def accepts_numpy_float(x :Float[np.ndarray, "batch sequence features"]):
...
def accepts_tensorflow_uint(x: hint = UInt[tf.Tensor, "b c h w"]):
...
``` ```
## Installation ## Installation
@@ -28,32 +42,33 @@ def accepts_pytree_of_arrays(x: PyTree[Float[Array, "batch c1 c2"]]):
pip install jaxtyping pip install jaxtyping
``` ```
Requires JAX 0.3.4+. Requires Python 3.9+.
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 ## Documentation
[Full API reference](./API.md) Available at [https://docs.kidger.site/jaxtyping](https://docs.kidger.site/jaxtyping).
[FAQ (static type checking, flake8, etc.)](./FAQ.md) ## See also: other libraries in the JAX ecosystem
## Finally **Always useful**
[Equinox](https://github.com/patrick-kidger/equinox): neural networks and everything not already in core JAX!
### See also: other tools in the JAX ecosystem **Deep learning**
[Optax](https://github.com/deepmind/optax): first-order gradient (SGD, Adam, ...) optimisers.
[Orbax](https://github.com/google/orbax): checkpointing (async/multi-host/multi-device).
[Levanter](https://github.com/stanford-crfm/levanter): scalable+reliable training of foundation models (e.g. LLMs).
Neural networks: [Equinox](https://github.com/patrick-kidger/equinox). **Scientific computing**
[Diffrax](https://github.com/patrick-kidger/diffrax): numerical differential equation solvers.
[Optimistix](https://github.com/patrick-kidger/optimistix): root finding, minimisation, fixed points, and least squares.
[Lineax](https://github.com/patrick-kidger/lineax): linear solvers.
[BlackJAX](https://github.com/blackjax-devs/blackjax): probabilistic+Bayesian sampling.
[sympy2jax](https://github.com/patrick-kidger/sympy2jax): SymPy<->JAX conversion; train symbolic expressions via gradient descent.
[PySR](https://github.com/milesCranmer/PySR): symbolic regression. (Non-JAX honourable mention!)
Numerical differential equation solvers: [Diffrax](https://github.com/patrick-kidger/diffrax). **Awesome JAX**
[Awesome JAX](https://github.com/n2cholas/awesome-jax): a longer list of other JAX projects.
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;
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 541 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()
})
+22
View File
@@ -0,0 +1,22 @@
# Advanced features
## Creating your own dtypes
::: jaxtyping.AbstractDtype
selection:
members:
false
## Printing axis bindings
::: jaxtyping.print_bindings
## Introspection
If you're writing your own type hint parser, then you may wish to detect if some Python object is a jaxtyping-provided type.
You can check for dtypes by doing `issubclass(x, AbstractDtype)`. For example, `issubclass(Float32, AbstractDtype)` will pass.
You can check for arrays by doing `issubclass(x, AbstractArray)`. Here, `AbstractArray` is the base class for all shape-and-dtype specified arrays, e.g. it's a base class for `Float32[Array, "foo"]`.
You can check for pytrees by doing `issubclass(x, PyTree)`. For example, `issubclass(PyTree[int], PyTree)` will pass.
+123
View File
@@ -0,0 +1,123 @@
# 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 in terms of other variable-size axes, e.g.
`def remove_last(x: Float[Array, "dim"]) -> Float[Array, "dim-1"]`.
Symbolic expressions must not use any spaces, otherwise each piece is treated as as a separate axis.
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 an axis to indicate that it can match multiple axes, e.g. `"*batch"` will match zero or more batch axes.
- Prepend `#` to an axis 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 an axis to disable any runtime checking of that axis (so that it can be used just as documentation). This can also be used as just `_` on its own: e.g. `"b c _ _"`.
- Documentation-only names (i.e. they're ignored by jaxtyping) can be handled by prepending a name followed by `=` e.g. `Float[Array, "rows=4 cols=3"]`.
- Prepend `?` to an axis to indicate that its size can vary within a PyTree structure. (See [PyTree annotations](./pytree.md).)
When using multiple modifiers, their order does not matter.
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.
- 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.
- Symbolic expressions are evaluated in two stages: they are first evaluated as f-strings using the arguments of the function, and second are evaluated using the processed axis sizes. The f-string evaluation means that they can use local variables by enclosing them with curly braces, e.g. `{variable}`, e.g.
```python
def full(size: int, fill: float) -> Float[Array, "{size}"]:
return jax.numpy.full((size,), fill)
class SomeClass:
some_value = 5
def full(self, fill: float) -> Float[Array, "{self.some_value}+3"]:
return jax.numpy.full((self.some_value + 3,), fill)
```
## 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: `UInt4`, `UInt8`, `UInt16`, `UInt32`, `UInt64`
- Any signed integer: `Int`
- Of particular precision: `Int4`, `Int8`, `Int16`, `Int32`, `Int64`
- Any floating, integer, or unsigned integer: `Real`.
Unless you really want to force a particular precision, then for most applications you should probably allow any floating-point, any integer, etc. That is, use
```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, PRNG keys
For convenience, jaxtyping also includes `jaxtyping.Scalar`, `jaxtyping.ScalarLike`, and `jaxtyping.PRNGKeyArray`, defined as:
```python
Scalar = Shaped[Array, ""]
ScalarLike = Shaped[ArrayLike, ""]
# Left: new-style typed keys; right: old-style keys. See JEP 9263.
PRNGKeyArray = Union[Key[Array, ""], 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.
+46
View File
@@ -0,0 +1,46 @@
# PyTree annotations
:::jaxtyping.PyTree
selection:
members:
false
---
:::jaxtyping.PyTreeDef
---
## Path-dependent shapes
The prefix `?` may be used to indicate that the axis size can depend on which leaf of a PyTree the array is at. For example:
```python
def f(
x: PyTree[Shaped[Array, "?foo"], "T"],
y: PyTree[Shaped[Array, "?foo"], "T"],
):
pass
```
The above demands that `x` and `y` have matching PyTree structures (due to the `T` annotation), and that their leaves must all be one-dimensional arrays, *and that the corresponding pairs of leaves in `x` and `y` must have the same size as each other*.
Thus the following is allowed:
```python
x0 = jnp.arange(3)
x1 = jnp.arange(5)
y0 = jnp.arange(3) + 1
y1 = jnp.arange(5) + 1
f((x0, x1), (y0, y1)) # x0 matches y0, and x1 matches y1. All good!
```
But this is not:
```python
f((x1, x1), (y0, y1)) # x1 does not have a size matching y0!
```
Internally, all that is happening is that `foo` is replaced with `0foo` for the first leaf, `1foo` for the next leaf, etc., so that each leaf gets a unique version of the name.
---
Note that `jaxtyping.{PyTree, PyTreeDef}` are only available if JAX has been installed.
+55
View File
@@ -0,0 +1,55 @@
# 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 only whilst tracing, and will not impact runtime performance.
There are two approaches: either use [`jaxtyping.jaxtyped`][] to typecheck a single function, or [`jaxtyping.install_import_hook`][] to typecheck a whole codebase.
In either case, the actual business of checking types is performed with the help of a runtime type-checking library. The two most popular are [beartype](https://github.com/beartype/beartype) and [typeguard](https://github.com/agronholm/typeguard). (If using typeguard, then specifically the version `2.*` series should be used. Later versions -- `3` and `4` -- have some known issues.)
!!! warning
Avoid using `from __future__ import annotations`, or stringified type annotations, where possible. These are largely incompatible with runtime type checking. See also [this FAQ entry](../faq.md#dataclass-annotations-arent-being-checked-properly).
---
::: jaxtyping.jaxtyped
---
::: jaxtyping.install_import_hook
---
#### 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`.
#### IPython extension
If you are running in an IPython environment (for example a Jupyter or Colab notebook), then the jaxtyping hook can be automatically ran via a custom magic:
```python
import jaxtyping
%load_ext jaxtyping
%jaxtyping.typechecker beartype.beartype # or any other runtime type checker
```
Place this at the start of your notebook -- everything that is directly defined in the notebook, after this magic is run, will be hook'd.
#### Other runtime type-checking libraries
Beartype and typeguard happen to be the two most popular runtime type-checking libraries (at least at time of writing), but jaxtyping should be compatible with all runtime type checkers out-of-the-box. The runtime type-checking library just needs to provide a type-checking decorator (analgous to `beartype.beartype` or `typeguard.typechecked`), and perform `isinstance` checks against jaxtyping's types.
+21 -3
View File
@@ -1,6 +1,6 @@
# FAQ # 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. 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,31 @@ 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! 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. 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). 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.
## Dataclass annotations aren't being checked properly.
Stringified dataclass annotations, e.g.
```python
@dataclass()
class Foo:
x: "int"
```
will be silently skipped without checking them. This is because these are essentially impossible to resolve at runtime. Such stringified annotations typically occur either when using them for forward references, or when using `from __future__ import annotations`. (You should essentially never use the latter, it is largely incompatible with runtime type checking and as such is [being replaced in Python 3.13](https://peps.python.org/pep-0649/).)
Partially stringified dataclass annotations, e.g.
```python
@dataclass()
class Foo:
x: tuple["int"]
```
will likely raise an error, and must not be used at all.
## Does jaxtyping use [PEP 646](https://www.python.org/dev/peps/pep-0646/) (variadic generics)? ## Does jaxtyping use [PEP 646](https://www.python.org/dev/peps/pep-0646/) (variadic generics)?
+63
View File
@@ -0,0 +1,63 @@
# 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.9+.
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 axes
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!
## See also: other libraries in the JAX ecosystem
**Always useful**
[Equinox](https://github.com/patrick-kidger/equinox): neural networks and everything not already in core JAX!
**Deep learning**
[Optax](https://github.com/deepmind/optax): first-order gradient (SGD, Adam, ...) optimisers.
[Orbax](https://github.com/google/orbax): checkpointing (async/multi-host/multi-device).
[Levanter](https://github.com/stanford-crfm/levanter): scalable+reliable training of foundation models (e.g. LLMs).
**Scientific computing**
[Diffrax](https://github.com/patrick-kidger/diffrax): numerical differential equation solvers.
[Optimistix](https://github.com/patrick-kidger/optimistix): root finding, minimisation, fixed points, and least squares.
[Lineax](https://github.com/patrick-kidger/lineax): linear solvers.
[BlackJAX](https://github.com/blackjax-devs/blackjax): probabilistic+Bayesian sampling.
[sympy2jax](https://github.com/patrick-kidger/sympy2jax): SymPy<->JAX conversion; train symbolic expressions via gradient descent.
[PySR](https://github.com/milesCranmer/PySR): symbolic regression. (Non-JAX honourable mention!)
**Awesome JAX**
[Awesome JAX](https://github.com/n2cholas/awesome-jax): a longer list of other JAX projects.
+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.8 # 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]
+226 -33
View File
@@ -17,40 +17,233 @@
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from jax.numpy import ndarray as Array import functools as ft
import importlib.metadata
import importlib.util
import typing
import warnings
from typing import Union
from .array_types import ( from ._array_types import (
AbstractArray, AbstractArray as AbstractArray,
AbstractDtype, AbstractDtype as AbstractDtype,
BFloat16, get_array_name_format as get_array_name_format,
Bool, set_array_name_format as set_array_name_format,
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,
) )
from .decorator import jaxtyped from ._config import config as config
from .import_hook import install_import_hook from ._decorator import jaxtyped as jaxtyped
from .pytree_type import PyTree from ._errors import (
AnnotationError as AnnotationError,
TypeCheckError as TypeCheckError,
)
from ._import_hook import install_import_hook as install_import_hook
from ._ipython_extension import load_ipython_extension as load_ipython_extension
from ._storage import print_bindings as print_bindings
__version__ = "0.2.2" if typing.TYPE_CHECKING:
import typing_extensions
from jax import Array as Array
from jax.tree_util import PyTreeDef as PyTreeDef
from jax.typing import ArrayLike as ArrayLike, DTypeLike as DTypeLike
# Introduce an indirection so that we can `import X as X` to make it clear that
# these are public.
from ._indirection import (
BFloat16 as BFloat16,
Bool as Bool,
Complex as Complex,
Complex64 as Complex64,
Complex128 as Complex128,
Float as Float,
Float16 as Float16,
Float32 as Float32,
Float64 as Float64,
Inexact as Inexact,
Int as Int,
Int4 as Int4,
Int8 as Int8,
Int16 as Int16,
Int32 as Int32,
Int64 as Int64,
Integer as Integer,
Key as Key,
Num as Num,
PRNGKeyArray as PRNGKeyArray,
Real as Real,
Scalar as Scalar,
ScalarLike as ScalarLike,
Shaped as Shaped,
UInt as UInt,
UInt4 as UInt4,
UInt8 as UInt8,
UInt16 as UInt16,
UInt32 as UInt32,
UInt64 as UInt64,
)
# Set up to deliberately confuse a static type checker.
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.
else:
from ._array_types import (
BFloat16 as BFloat16,
Bool as Bool,
Complex as Complex,
Complex64 as Complex64,
Complex128 as Complex128,
Float as Float,
Float16 as Float16,
Float32 as Float32,
Float64 as Float64,
Inexact as Inexact,
Int as Int,
Int4 as Int4,
Int8 as Int8,
Int16 as Int16,
Int32 as Int32,
Int64 as Int64,
Integer as Integer,
Key as Key,
Num as Num,
Real as Real,
Shaped as Shaped,
UInt as UInt,
UInt4 as UInt4,
UInt8 as UInt8,
UInt16 as UInt16,
UInt32 as UInt32,
UInt64 as UInt64,
)
# But crucially, does not actually import jax at all. We do that dynamically in
# __getattr__ if required. See #178.
if importlib.util.find_spec("jax") is not None:
@ft.cache
def __getattr__(item):
if item == "Array":
if getattr(typing, "GENERATING_DOCUMENTATION", False):
class Array:
pass
Array.__module__ = "builtins"
Array.__qualname__ = "Array"
return Array
else:
import jax
return jax.Array
elif item == "ArrayLike":
if getattr(typing, "GENERATING_DOCUMENTATION", False):
class ArrayLike:
pass
ArrayLike.__module__ = "builtins"
ArrayLike.__qualname__ = "ArrayLike"
return ArrayLike
else:
import jax.typing
return jax.typing.ArrayLike
elif item == "PRNGKeyArray":
if getattr(typing, "GENERATING_DOCUMENTATION", False):
class PRNGKeyArray:
pass
PRNGKeyArray.__module__ = "builtins"
PRNGKeyArray.__qualname__ = "PRNGKeyArray"
return PRNGKeyArray
else:
# New-style `jax.random.key` have scalar shape and dtype `key<foo>`.
# Old-style `jax.random.PRNGKey` have shape `(2,)` and dtype
# `uint32`.
import jax
return Union[Key[jax.Array, ""], UInt32[jax.Array, "2"]]
elif item == "DTypeLike":
import jax.typing
return jax.typing.DTypeLike
elif item == "Scalar":
import jax
return Shaped[jax.Array, ""]
elif item == "ScalarLike":
import jax.typing
return Shaped[jax.typing.ArrayLike, ""]
elif item == "PyTree":
from ._pytree_type import PyTree
return PyTree
elif item == "PyTreeDef":
if hasattr(typing, "GENERATING_DOCUMENTATION"):
# Most parts of the Equinox ecosystem have
# `typing.GENERATING_DOCUMENTATION = True` when generating
# documentation, to add whatever shims are necessary to get pretty
# docs. E.g. to have type annotations appear as just `PyTree`, not
# `jaxtyping.PyTree`.
#
# As jaxtyping actually wants things to appear as e.g.
# `jaxtyping.PyTree`, rather than just `PyTree`, then it sets
# `typing.GENERATING_DOCUMENTATION = False`, to disable these shims.
#
# Here we do only a `hasattr` check, as we want to get this version
# of `PyTreeDef` in both the jaxtyping and the Equinox(/etc.) docs.
class PyTreeDef:
"""Alias for `jax.tree_util.PyTreeDef`, which is the type of the
return from `jax.tree_util.tree_structure(...)`.
"""
if typing.GENERATING_DOCUMENTATION:
# Equinox etc. docs get just `PyTreeDef`.
# jaxtyping docs get `jaxtyping.PyTreeDef`.
PyTreeDef.__qualname__ = "PyTreeDef"
PyTreeDef.__module__ = "builtins"
return PyTreeDef
else:
import jax.tree_util
return jax.tree_util.PyTreeDef
else:
raise AttributeError(f"module jaxtyping has no attribute {item!r}")
check_equinox_version = True # easy-to-replace line with copybara
if check_equinox_version:
try:
eqx_version = importlib.metadata.version("equinox")
except importlib.metadata.PackageNotFoundError:
pass
else:
major, minor, patch = eqx_version.split(".")
equinox_version = (int(major), int(minor), int(patch))
if equinox_version < (0, 11, 0):
warnings.warn(
"jaxtyping version >=0.2.23 should be used with Equinox version "
">=0.11.1"
)
__version__ = importlib.metadata.version("jaxtyping")
+746
View File
@@ -0,0 +1,746 @@
# 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 enum
import functools as ft
import re
import sys
import types
import typing
from dataclasses import dataclass
from typing import Any, Literal, NoReturn, Optional, Union
import numpy as np
from ._errors import AnnotationError
from ._storage import (
get_shape_memo,
get_treeflatten_memo,
get_treepath_memo,
set_shape_memo,
)
_array_name_format = "dtype_and_shape"
def get_array_name_format():
return _array_name_format
def set_array_name_format(value):
global _array_name_format
_array_name_format = value
_any_dtype = object()
_anonymous_dim = object()
_anonymous_variadic_dim = object()
class _DimType(enum.Enum):
named = enum.auto()
fixed = enum.auto()
symbolic = enum.auto()
@dataclass(frozen=True)
class _NamedDim:
name: str
broadcastable: bool
treepath: Any
@dataclass(frozen=True)
class _NamedVariadicDim:
name: str
broadcastable: bool
treepath: Any
@dataclass(frozen=True)
class _FixedDim:
size: str
broadcastable: bool
@dataclass(frozen=True)
class _SymbolicDim:
elem: Any
broadcastable: bool
_AbstractDimOrVariadicDim = Union[
Literal[_anonymous_dim],
Literal[_anonymous_variadic_dim],
_NamedDim,
_NamedVariadicDim,
_FixedDim,
_SymbolicDim,
]
_AbstractDim = Union[Literal[_anonymous_dim], _NamedDim, _FixedDim, _SymbolicDim]
def _check_dims(
cls_dims: list[_AbstractDim],
obj_shape: tuple[int, ...],
single_memo: dict[str, int],
arg_memo: dict[str, Any],
) -> str:
assert len(cls_dims) == len(obj_shape)
for cls_dim, obj_size in zip(cls_dims, obj_shape):
if cls_dim is _anonymous_dim:
pass
elif cls_dim.broadcastable and obj_size == 1:
pass
elif type(cls_dim) is _FixedDim:
if cls_dim.size != obj_size:
return f"the dimension size {obj_size} does not equal {cls_dim.size} as expected by the type hint" # noqa: E501
elif type(cls_dim) is _SymbolicDim:
try:
# Support f-string syntax.
# https://stackoverflow.com/a/53671539/22545467
elem = eval(f"f'{cls_dim.elem}'", arg_memo.copy())
# Make a copy to avoid `__builtins__` getting added as a key.
eval_size = eval(elem, single_memo.copy())
except NameError as e:
raise AnnotationError(
f"Cannot process symbolic axis '{cls_dim.elem}' as "
"some axis names have not been processed. In practice you "
"should usually only use symbolic axes in annotations "
"for return types, referring only to axes annotated for "
"arguments."
) from e
if eval_size != obj_size:
return f"the dimension size {obj_size} does not equal the existing value of {cls_dim.elem}={eval_size}" # noqa: E501
else:
assert type(cls_dim) is _NamedDim
if cls_dim.treepath:
name = get_treepath_memo() + cls_dim.name
else:
name = cls_dim.name
try:
cls_size = single_memo[name]
except KeyError:
single_memo[name] = obj_size
else:
if cls_size != obj_size:
return f"the size of dimension {cls_dim.name} is {obj_size} which does not equal the existing value of {cls_size}" # noqa: E501
return ""
class _MetaAbstractArray(type):
_skip_instancecheck: bool = False
def make_transparent(cls):
cls._skip_instancecheck = True
def __instancecheck__(cls, obj: Any) -> bool:
return cls.__instancecheck_str__(obj) == ""
def __instancecheck_str__(cls, obj: Any) -> str:
if cls._skip_instancecheck:
return ""
if not isinstance(obj, cls.array_type):
return f"this value is not an instance of the underlying array type {cls.array_type}" # noqa: E501
if get_treeflatten_memo():
return ""
if hasattr(obj.dtype, "type") and hasattr(obj.dtype.type, "__name__"):
# JAX, numpy
dtype = obj.dtype.type.__name__
elif hasattr(obj.dtype, "as_numpy_dtype"):
# TensorFlow
dtype = obj.dtype.as_numpy_dtype.__name__
else:
# PyTorch
repr_dtype = repr(obj.dtype).split(".")
if len(repr_dtype) == 2 and repr_dtype[0] == "torch":
dtype = repr_dtype[1]
else:
raise AnnotationError(
"Unrecognised array/tensor type to extract dtype from"
)
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:
if len(cls.dtypes) == 1:
return f"this array has dtype {dtype}, not {cls.dtypes[0]} as expected by the type hint" # noqa: E501
else:
return f"this array has dtype {dtype}, not any of {cls.dtypes} as expected by the type hint" # noqa: E501
single_memo, variadic_memo, pytree_memo, arg_memo = get_shape_memo()
single_memo_bak = single_memo.copy()
variadic_memo_bak = variadic_memo.copy()
pytree_memo_bak = pytree_memo.copy()
arg_memo_bak = arg_memo.copy()
try:
check = cls._check_shape(obj, single_memo, variadic_memo, arg_memo)
except Exception:
set_shape_memo(
single_memo_bak, variadic_memo_bak, pytree_memo_bak, arg_memo_bak
)
raise
if check == "":
return check
else:
set_shape_memo(
single_memo_bak, variadic_memo_bak, pytree_memo_bak, arg_memo_bak
)
return check
def _check_shape(
cls,
obj,
single_memo: dict[str, int],
variadic_memo: dict[str, tuple[bool, tuple[int, ...]]],
arg_memo: dict[str, Any],
) -> str:
if cls.index_variadic is None:
if len(obj.shape) != len(cls.dims):
return f"this array has {len(obj.shape)} dimensions, not the {len(cls.dims)} expected by the type hint" # noqa: E501
return _check_dims(cls.dims, obj.shape, single_memo, arg_memo)
else:
if len(obj.shape) < len(cls.dims) - 1:
return f"this array has {len(obj.shape)} dimensions, which is fewer than {len(cls.dims) - 1} that is the minimum expected by the type hint" # noqa: E501
i = cls.index_variadic
j = -(len(cls.dims) - i - 1)
if j == 0:
j = None
prefix_check = _check_dims(
cls.dims[:i], obj.shape[:i], single_memo, arg_memo
)
if prefix_check != "":
return prefix_check
if j is not None:
suffix_check = _check_dims(
cls.dims[j:], obj.shape[j:], single_memo, arg_memo
)
if suffix_check != "":
return suffix_check
variadic_dim = cls.dims[i]
if variadic_dim is _anonymous_variadic_dim:
return ""
else:
assert type(variadic_dim) is _NamedVariadicDim
if variadic_dim.treepath:
name = get_treepath_memo() + variadic_dim.name
else:
name = variadic_dim.name
broadcastable = variadic_dim.broadcastable
try:
prev_broadcastable, prev_shape = variadic_memo[name]
except KeyError:
variadic_memo[name] = (broadcastable, obj.shape[i:j])
return ""
else:
new_shape = obj.shape[i:j]
if prev_broadcastable:
try:
broadcast_shape = np.broadcast_shapes(new_shape, prev_shape)
except ValueError: # not broadcastable e.g. (3, 4) and (5,)
return f"the shape of its variadic dimensions '*{variadic_dim.name}' is {new_shape}, which cannot be broadcast with the existing value of {prev_shape}" # noqa: E501
if not broadcastable and broadcast_shape != new_shape:
return f"the shape of its variadic dimensions '*{variadic_dim.name}' is {new_shape}, which the existing value of {prev_shape} cannot be broadcast to" # noqa: E501
variadic_memo[name] = (broadcastable, broadcast_shape)
else:
if broadcastable:
try:
broadcast_shape = np.broadcast_shapes(
new_shape, prev_shape
)
except ValueError: # not broadcastable e.g. (3, 4) and (5,)
return f"the shape of its variadic dimensions '*{variadic_dim.name}' is {new_shape}, which cannot be broadcast with the existing value of {prev_shape}" # noqa: E501
if broadcast_shape != prev_shape:
return f"the shape of its variadic dimensions '*{variadic_dim.name}' is {new_shape}, which cannot be broadcast to the existing value of {prev_shape}" # noqa: E501
else:
if new_shape != prev_shape:
return f"the shape of its variadic dimensions '*{variadic_dim.name}' is {new_shape}, which does not equal the existing value of {prev_shape}" # noqa: E501
return ""
assert False
@ft.lru_cache(maxsize=None)
def _make_metaclass(base_metaclass):
class MetaAbstractArray(_MetaAbstractArray, base_metaclass):
def _get_props(cls):
props_tuple = (
cls.index_variadic,
cls.dims,
cls.array_type,
cls.dtypes,
cls.dim_str,
)
return props_tuple
def __eq__(cls, other):
if type(cls) is not type(other):
return False
return cls._get_props() == other._get_props()
def __hash__(cls):
return hash(cls._get_props())
return MetaAbstractArray
def _check_scalar(dtype, dtypes, dims):
for dim in dims:
if dim is not _anonymous_variadic_dim and not isinstance(
dim, _NamedVariadicDim
):
return False
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: 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_cached(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 axes.
raise ValueError("Axes should be separated with spaces, not commas")
if elem.endswith("#"):
raise ValueError(
"As of jaxtyping v0.1.0, broadcastable axes are now denoted "
"with a # at the start, rather than at the end"
)
if "..." in elem:
if elem != "...":
raise ValueError(
"Anonymous multiple axes '...' must be used on its own; "
f"got {elem}"
)
broadcastable = False
variadic = True
anonymous = True
treepath = False
dim_type = _DimType.named
else:
broadcastable = False
variadic = False
anonymous = False
treepath = 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 "
"axes, 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:]
elif first_char == "?":
if treepath:
raise ValueError(
"Do not use ? twice to denote dependence on location "
"within a PyTree, e.g. `??foo` is not allowed"
)
treepath = 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 variadic 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 axes, e.g. "
"`*4` is not allowed."
)
if anonymous:
raise ValueError(
"Cannot have a fixed axis be anonymous, e.g. `_4` is not allowed."
)
if treepath:
raise ValueError(
"Cannot have a fixed axis have tree-path dependence, e.g. `?4` is "
"not allowed."
)
elem = _FixedDim(elem, broadcastable)
elif dim_type is _DimType.named:
if anonymous:
if broadcastable:
raise ValueError(
"Cannot have an axis 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, treepath)
else:
elem = _NamedDim(elem, broadcastable, treepath)
else:
assert dim_type is _DimType.symbolic
if anonymous:
raise ValueError(
"Cannot have a symbolic axis be anonymous, e.g. "
"`_foo+bar` is not allowed"
)
if variadic:
raise ValueError(
"Cannot have symbolic multiple-axes, e.g. "
"`*foo+bar` is not allowed"
)
if treepath:
raise ValueError(
"Cannot have a symbolic axis with tree-path dependence, e.g. "
"`?foo+bar` is not allowed"
)
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
elif array_type is np.bool_:
if _check_scalar("bool", dtypes, dims):
return array_type
else:
return _not_made
elif array_type is np.generic or array_type is np.number:
if _check_scalar("", 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 variadic 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")
return (array_type, name, dtypes, dims, index_variadic, dim_str)
def _make_array(*args, **kwargs):
out = _make_array_cached(*args, **kwargs)
if type(out) is tuple:
array_type, name, dtypes, dims, index_variadic, dim_str = out
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):
def __instancecheck__(cls, obj: Any) -> NoReturn:
raise AnnotationError(
f"Do not use `isinstance(x, jaxtyping.{cls.__name__})`. If you want to "
"check just the dtype of an array, then use "
f'`jaxtyping.{cls.__name__}[jnp.ndarray, "..."]`.'
)
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 both an "
"array type and a shape. For example `Float[Array, 'foo bar']`.\n"
"Ellipsis can be used to accept any shape: `Float[Array, '...']`."
)
array_type, dim_str = item
dim_str = dim_str.strip()
del item
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:
out = Union[out]
else:
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):
"""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(
"AbstractDtype cannot be instantiated. Perhaps you wrote e.g. "
'`Float32("shape")` when you mean `Float32[jnp.ndarray, "shape"]`?'
)
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
dtypes: Union[Literal[_any_dtype], str, list[str]] = cls.dtypes
if isinstance(dtypes, (str, re.Pattern)):
dtypes = (dtypes,)
elif dtypes is not _any_dtype:
dtypes = tuple(dtypes)
cls.dtypes = dtypes
_prng_key = "prng_key"
_bool = "bool"
_bool_ = "bool_"
_uint4 = "uint4"
_uint8 = "uint8"
_uint16 = "uint16"
_uint32 = "uint32"
_uint64 = "uint64"
_int4 = "int4"
_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
if getattr(typing, "GENERATING_DOCUMENTATION", False):
_Cls.__module__ = "builtins"
else:
_Cls.__module__ = "jaxtyping"
return _Cls
UInt4 = _make_dtype(_uint4, "UInt4")
UInt8 = _make_dtype(_uint8, "UInt8")
UInt16 = _make_dtype(_uint16, "UInt16")
UInt32 = _make_dtype(_uint32, "UInt32")
UInt64 = _make_dtype(_uint64, "UInt64")
Int4 = _make_dtype(_int4, "Int4")
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")
bools = [_bool, _bool_]
uints = [_uint4, _uint8, _uint16, _uint32, _uint64]
ints = [_int4, _int8, _int16, _int32, _int64]
floats = [_bfloat16, _float16, _float32, _float64]
complexes = [_complex64, _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
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")
Real = _make_dtype(floats + uints + ints, "Real")
Num = _make_dtype(uints + ints + floats + complexes, "Num")
Shaped = _make_dtype(_any_dtype, "Shaped")
Key = _make_dtype(_prng_key, "Key")
+48
View File
@@ -0,0 +1,48 @@
import os
from typing import Union
def _maybestr2bool(value: Union[bool, str], error: str) -> bool:
if isinstance(value, bool):
return value
elif isinstance(value, str):
if value.lower() in ("0", "false"):
return False
elif value.lower() in ("1", "true"):
return True
else:
raise ValueError(error)
else:
raise ValueError(error)
class _JaxtypingConfig:
def __init__(self):
self.update("jaxtyping_disable", os.environ.get("JAXTYPING_DISABLE", "0"))
self.update(
"jaxtyping_remove_typechecker_stack",
os.environ.get("JAXTYPING_REMOVE_TYPECHECKER_STACK", "0"),
)
def update(self, item: str, value):
if item.lower() == "jaxtyping_disable":
msg = (
"Unrecognised value for `JAXTYPING_DISABLE`. Valid values are "
"`JAXTYPING_DISABLE=0` (the default) or `JAXTYPING_DISABLE=1` (to "
"disable runtime type checking)."
)
self.jaxtyping_disable = _maybestr2bool(value, msg)
elif item.lower() == "jaxtyping_remove_typechecker_stack":
msg = (
"Unrecognised value for `JAXTYPING_REMOVE_TYPECHECKER_STACK`. Valid "
"values are `JAXTYPING_REMOVE_TYPECHECKER_STACK=0` (the default) or "
"`JAXTYPING_REMOVE_TYPECHECKER_STACK=1` (to remove the stack frames "
"from the typechecker in `jaxtyped(typechecker=...)`, when it raises a "
"runtime type-checking error)."
)
self.jaxtyping_remove_typechecker_stack = _maybestr2bool(value, msg)
else:
raise ValueError(f"Unrecognised config value {item}")
config = _JaxtypingConfig()
+805
View File
@@ -0,0 +1,805 @@
# 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 functools as ft
import importlib.util
import inspect
import itertools as it
import sys
import warnings
from typing import Any, get_args, get_origin, get_type_hints, overload
from jaxtyping import AbstractArray
from ._config import config
from ._errors import AnnotationError, TypeCheckError
from ._storage import pop_shape_memo, push_shape_memo, shape_str
class _Sentinel:
def __repr__(self):
return "sentinel"
_sentinel = _Sentinel()
_tb_flag = True
@overload
def jaxtyped(*, typechecker=_sentinel):
...
@overload
def jaxtyped(fn, *, typechecker=_sentinel):
...
def jaxtyped(fn=_sentinel, *, typechecker=_sentinel):
"""Decorate a function with this to perform runtime type-checking of its arguments
and return value. Decorate a dataclass to perform type-checking of its attributes.
!!! Example
```python
# Import both the annotation and the `jaxtyped` decorator from `jaxtyping`
from jaxtyping import Array, Float, jaxtyped
# Use your favourite typechecker: usually one of the two lines below.
from typeguard import typechecked as typechecker
from beartype import beartype as typechecker
# Type-check a function
@jaxtyped(typechecker=typechecker)
def batch_outer_product(x: Float[Array, "b c1"],
y: Float[Array, "b c2"]
) -> Float[Array, "b c1 c2"]:
return x[:, :, None] * y[:, None, :]
# Type-check a dataclass
from dataclasses import dataclass
@jaxtyped(typechecker=typechecker)
@dataclass
class MyDataclass:
x: int
y: Float[Array, "b c"]
```
**Arguments:**
- `fn`: The function or dataclass to decorate. In practice if you want to use
dataclasses with JAX, then
[`equinox.Module`](https://docs.kidger.site/equinox/api/module/module/) is our
recommended approach:
```python
import equinox as eqx
@jaxtyped(typechecker=typechecker)
class MyModule(eqx.Module):
...
```
- `typechecker`: Keyword-only argument: the runtime type-checker to use. This should
be a function decorator that will raise an exception if there is a type error,
e.g.
```python
@typechecker
def f(x: int):
pass
f("a string is not an integer") # this line should raise an exception
```
Common choices are `typechecker=beartype.beartype` or
`typechecker=typeguard.typechecked`. Can also be set as `typechecker=None` to
skip automatic runtime type-checking, but still support manual `isinstance`
checks inside the function body:
```python
@jaxtyped(typechecker=None)
def f(x):
assert isinstance(x, Float[Array, "batch channel"])
```
**Returns:**
If `fn` is a function (including a `staticmethod`, `classmethod`, or `property`),
then a wrapped function is returned.
If `fn` is a dataclass, then `fn` is returned directly, and additionally its
`__init__` method is wrapped and modified in-place.
!!! Info "Old syntax"
jaxtyping previously (before v0.2.24) recommended using this double-decorator
syntax:
```python
@jaxtyped
@typechecker
def f(...): ...
```
This is still supported, but will now raise a warning recommending the
`jaxtyped(typechecker=typechecker)` syntax discussed above. (Which will produce
easier-to-debug error messages: under the hood, the new syntax more carefully
manipulates the typechecker so as to determine where a type-check error arises.)
??? Info "Notes for advanced users"
**Dynamic contexts:**
Put precisely, the axis names in e.g. `Float[Array, "batch channels"]` and the
structure names in e.g. `PyTree[int, "T"]` are all scoped to the thread-local
dynamic context of a `jaxtyped`-wrapped function. If from within that function
we then call another `jaxtyped`-wrapped function, then a new context is pushed
to the stack. The axis sizes and PyTree structures of this inner function will
then not be compared against the axis sizes and PyTree structures of the outer
function. After the inner function returns then this inner context is popped
from the stack, and the previous context is returned to.
**isinstance:**
Binding of a value against a name is done with an `isinstance` check, for
example `isinstance(jnp.zeros((3, 4)), Float[Array, "dim1 dim2"])` will bind
`dim1=3` and `dim2=4`. In practice these `isinstance` checks are usually done by
the run-time typechecker `typechecker` that is supplied as an argument.
This can also be done manually: add `isinstance` checks inside a function body
and they will contribute to the same collection of consistency checks as are
performed by the typechecker on the arguments and return values. (Or you can
forgo such a typechecker altogether -- i.e. `typechecker=None` -- and only do
your own manual `isinstance` checks.)
Only `isinstance` checks that pass will contribute to the store of values; those
that fail will not. As such it is safe to write e.g.
`assert not isinstance(x, Float32[Array, "foo"])`.
**Decoupling contexts from function calls:**
If you would like to call a new function *without* creating a new
dynamic context (and using the same set of axis and structure values), then
simply do not add a `jaxtyped` decorator to your inner function, whilst
continuing to perform type-checking in whatever way you prefer.
Conversely, if you would like a new dynamic context *without* calling a new
function, then in addition to the usage discussed above, `jaxtyped` also
supports being used as a context manager, by passing it the string `"context"`:
```python
with jaxtyped("context"):
assert isinstance(x, Float[Array, "batch channel"])
```
This is equivalent to placing this code inside a new function wrapped in
`jaxtyped(typechecker=None)`. Usage like this is very rare; it's mostly only
useful when working at the global scope.
"""
global _tb_flag
if (
_tb_flag
and importlib.util.find_spec("jax") is not None
and importlib.util.find_spec("jaxlib") is not None
and importlib.util.find_spec("jax._src.traceback_util") is not None
):
import jax._src.traceback_util as traceback_util
traceback_util.register_exclusion(__file__)
_tb_flag = False
# First handle the `jaxtyped("context")` usage, which is a special case.
if fn == "context":
if typechecker is not _sentinel:
raise ValueError(
"Cannot use `jaxtyped` as a context with a typechecker. That is, "
"`with jaxtyped('context', typechecker=...):`. is not allowed. In this "
"case the type checker does not actually do anything, as there is no "
"function to type-check."
)
return _JaxtypingContext()
# Now check that a typechecker has been explicitly declared. (Or explicitly declared
# as not being used, via `typechecker=None`.)
# This is needed just for backward compatibility: an undeclared typechecker
# corresponds to the old double-decorator syntax.
if typechecker is _sentinel:
# This branch will also catch the easy-to-make mistake of
# ```python
# @jaxtyped(typechecker)
# def foo(...):
# ```
# which is a bug as `typechecker` is interpreted as the function to decorate!
warnings.warn(
"As of jaxtyping version 0.2.24, jaxtyping now prefers the syntax\n"
"```\n"
"from jaxtyping import jaxtyped\n"
"# Use your favourite typechecker: usually one of the two lines below.\n"
"from typeguard import typechecked as typechecker\n"
"from beartype import beartype as typechecker\n"
"\n"
"@jaxtyped(typechecker=typechecker)\n"
"def foo(...):\n"
"```\n"
"and the old double-decorator syntax\n"
"```\n"
"@jaxtyped\n"
"@typechecker\n"
"def foo(...):\n"
"```\n"
"should no longer be used. (It will continue to work as it did before, but "
"the new approach will produce more readable error messages.)\n"
"In particular note that `typechecker` must be passed via keyword "
"argument; the following is not valid:\n"
"```\n"
"@jaxtyped(typechecker)\n"
"def foo(...):\n"
"```\n",
stacklevel=2,
)
typechecker = None
if fn is _sentinel:
return ft.partial(jaxtyped, typechecker=typechecker)
elif inspect.isclass(fn):
if dataclasses.is_dataclass(fn) and typechecker is not None:
# This does not check that the arguments passed to `__init__` match the
# type annotations. There may be a custom user `__init__`, or a
# dataclass-generated `__init__` used alongside
# `equinox.field(converter=...)`
init = fn.__init__
@ft.wraps(init)
def __init__(self, *args, **kwargs):
init(self, *args, **kwargs)
# `fn.__init__` is late-binding to the `__init__` function that
# we're in now. (Or to someone else's monkey-patch.) Either way,
# this checks that we're in the "top-level" `__init__`, and not one
# that is being called via `super()`. We don't want to trigger too
# early, before all fields have been assigned.
#
# We're not checking `if self.__class__ is fn` because Equinox
# replaces the with a defrozen version of itself during `__init__`,
# so the check wouldn't trigger.
#
# We're not doing this check by adding it to the end of the
# metaclass `__call__`, because Python doesn't allow you
# monkey-patch metaclasses.
if self.__class__.__init__ is fn.__init__:
_check_dataclass_annotations(self, typechecker)
fn.__init__ = __init__
return fn
# 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__, typechecker=typechecker))
elif isinstance(fn, staticmethod):
return staticmethod(jaxtyped(fn.__func__, typechecker=typechecker))
elif isinstance(fn, property):
if fn.fget is None:
fget = None
else:
fget = jaxtyped(fn.fget, typechecker=typechecker)
if fn.fset is None:
fset = None
else:
fset = jaxtyped(fn.fset, typechecker=typechecker)
if fn.fdel is None:
fdel = None
else:
fdel = jaxtyped(fn.fdel, typechecker=typechecker)
return property(fget=fget, fset=fset, fdel=fdel)
else:
if typechecker is None:
# Probably being used in the old style as
# ```
# @jaxtyped
# @typechecker
# def foo(x: int): ...
# ```
# in which case make a best-effort attempt to add shape information for any
# type errors.
# we want to detect generators, and ignore return annotations on them,
# to avoid issues with O(n) typechecking trying to typecheck yielded values
wrp = fn
while hasattr(wrp, "__wrapped__"):
wrp = wrp.__wrapped__
if inspect.isgeneratorfunction(wrp) or inspect.isasyncgenfunction(wrp):
# recursively parse all the annotations, and mark all the jaxtyping
# annotations as not needing instance checks, while still being
# visible as original ones for the typechecker
def modify_annotation(ann):
if inspect.isclass(ann) and issubclass(ann, AbstractArray):
ann.make_transparent()
for sub_ann in get_args(ann):
modify_annotation(sub_ann)
# just to make sure: check that fn has valid return annotations
if hasattr(fn, "__annotations__") and "return" in fn.__annotations__:
modify_annotation(fn.__annotations__["return"])
signature = inspect.signature(fn)
@ft.wraps(fn)
def wrapped_fn(*args, **kwargs): # pyright: ignore
bound = signature.bind(*args, **kwargs)
memos = push_shape_memo(bound.arguments)
try:
return fn(*args, **kwargs)
except Exception as e:
# add_note api is support from python 3.11+
if sys.version_info >= (3, 11) and _no_jaxtyping_note(e):
shape_info = shape_str(memos)
if shape_info != "":
msg = (
"The preceding error occurred within the scope of a "
"`jaxtyping.jaxtyped` function, and may be due to a "
"typecheck error. "
)
e.add_note(_jaxtyping_note_str(_spacer + msg + shape_info))
raise
finally:
pop_shape_memo()
else:
# New-style
# ```
# @jaxtyped(typechecker=typechecker)
# def foo(x: int): ...
# ```
# in which case we can do a better job reporting errors.
full_signature = inspect.signature(fn)
try:
destring_annotations = get_type_hints(fn, include_extras=True)
except NameError:
# Best-effort attempt to destringify annotations.
pass
else:
new_params = []
for p_name, p_value in full_signature.parameters.items():
p_annotation = destring_annotations.get(p_name, p_value.annotation)
p_value = p_value.replace(annotation=p_annotation)
new_params.append(p_value)
return_annotation = destring_annotations.get(
"return", full_signature.return_annotation
)
full_signature = full_signature.replace(
parameters=new_params, return_annotation=return_annotation
)
param_signature = full_signature.replace(
return_annotation=inspect.Signature.empty
)
module = getattr(fn, "__module__", "generated")
full_fn, output_name = _make_fn_with_signature(
"check_return", full_signature, module, output=True
)
full_fn = typechecker(full_fn)
param_fn = _make_fn_with_signature(
"check_params", param_signature, module, output=False
)
param_fn = typechecker(param_fn)
@ft.wraps(fn)
def wrapped_fn(*args, **kwargs):
if config.jaxtyping_disable:
return fn(*args, **kwargs)
# Raise bind-time errors before we do any shape analysis. (I.e. skip
# the pointless jaxtyping information for a non-typechecking failure.)
bound = param_signature.bind(*args, **kwargs)
memos = push_shape_memo(bound.arguments)
try:
# First type-check just the parameters before the function is
# called.
try:
param_fn(*args, **kwargs)
except AnnotationError:
raise
except Exception as e:
argmsg = _get_problem_arg(
param_signature,
args,
kwargs,
bound.arguments,
module,
typechecker,
)
try:
name = fn.__name__
except AttributeError:
name = fn.__class__.__name__
param_values = _pformat(bound.arguments, short_self=True)
param_hints = _remove_typing(param_signature)
msg = (
"Type-check error whilst checking the parameters of "
f"{name}.{argmsg}\n"
"----------------------\n"
f"Called with parameters: {param_values}\n"
f"Parameter annotations: {param_hints}.\n"
+ shape_str(memos)
)
if config.jaxtyping_remove_typechecker_stack:
raise TypeCheckError(msg) from None
else:
raise TypeCheckError(msg) from e
# Actually call the function.
out = fn(*args, **kwargs)
if full_signature.return_annotation is not inspect.Signature.empty:
# Now type-check the return value. We need to include the
# parameters in the type-checking here in case there are any
# type variables shared across the parameters and return.
#
# Incidentally this does mean that if `fn` mutates its arguments
# so that they no longer satisfy their type annotations, this
# will throw an error here. But that's like, super weird, so
# don't do that. An error in that scenario is probably still
# desirable.
#
# There is a small performance concern here when used in
# non-jit'd contexts, like PyTorch, due to the duplicate
# checking of the parameters. Unfortunately there doesn't seem
# to be a way around that, so c'est la vie.
kwargs[output_name] = out
try:
full_fn(*args, **kwargs)
except AnnotationError:
raise
except Exception as e:
try:
name = fn.__name__
except AttributeError:
name = fn.__class__.__name__
param_values = _pformat(bound.arguments, short_self=True)
return_value = _pformat(out, short_self=False)
param_hints = _remove_typing(param_signature)
return_hint = _remove_typing(
full_signature.return_annotation
)
if return_hint.startswith(
"<class '"
) and return_hint.endswith("'>"):
return_hint = return_hint[8:-2]
msg = (
"Type-check error whilst checking the return value "
f"of {name}.\n"
f"Actual value: {return_value}\n"
f"Expected type: {return_hint}.\n"
"----------------------\n"
f"Called with parameters: {param_values}\n"
f"Parameter annotations: {param_hints}.\n"
+ shape_str(memos)
)
if config.jaxtyping_remove_typechecker_stack:
raise TypeCheckError(msg) from None
else:
raise TypeCheckError(msg) from e
return out
finally:
pop_shape_memo()
return wrapped_fn
class _JaxtypingContext:
def __enter__(self):
push_shape_memo({})
def __exit__(self, exc_type, exc_value, exc_tb):
pop_shape_memo()
def _check_dataclass_annotations(self, typechecker):
"""Creates and calls a function that checks the attributes of `self`
`self` should be a dataclass instance. `typechecker` should be e.g.
`beartype.beartype` or `typeguard.typechecked`.
"""
parameters = [inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD)]
values = {}
for field in dataclasses.fields(self):
annotation = field.type
if isinstance(annotation, str):
# Don't check stringified annotations. These are basically impossible to
# resolve correctly, so just skip them.
continue
if get_origin(annotation) is type:
args = get_args(annotation)
if len(args) == 1 and isinstance(args[0], str):
# We also special-case this one kind of partially-stringified type
# annotation, so as to support Equinox <v0.11.1.
# This was fixed in Equinox in
# https://github.com/patrick-kidger/equinox/pull/543
continue
try:
value = getattr(self, field.name) # noqa: F841
except AttributeError:
continue # allow uninitialised fields, which are allowed on dataclasses
parameters.append(
inspect.Parameter(
field.name,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
annotation=field.type,
)
)
values[field.name] = value
signature = inspect.Signature(parameters)
module = self.__class__.__module__
f = _make_fn_with_signature(
self.__class__.__name__, signature, module, output=False
)
f = jaxtyped(f, typechecker=typechecker)
f(self, **values)
def _make_fn_with_signature(
name: str, signature: inspect.Signature, module: str, output: bool
):
"""Dynamically creates a function `fn` with name `name` and signature `signature`.
If `output=True` then `fn` will consume an additional keyword-only argument (in
addition to the provided signature), and will directly return this argument. In this
case the returned value from `_make_fn_with_signature` is a 2-tuple of `(fn, name)`,
where `fn` is the generated function, and `name` is the name of this extra argument.
If `output=False` then `fn` will just have a single `pass` statement, and the
returned value from `_make_fn_with_signature` will just be `fn`.
---
Note that this function operates by dynamically creating and eval'ing a string, not
simply by assigning `__signature__` and `__annotations__`. The latter is enough for
typeguard (at least v2), but does not work with beartype (at least v16).
"""
pos = []
pos_or_key = []
varpos = []
key = []
varkey = []
for p in signature.parameters.values():
if p.kind == inspect.Parameter.POSITIONAL_ONLY:
pos.append(p)
elif p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
pos_or_key.append(p)
elif p.kind == inspect.Parameter.VAR_POSITIONAL:
varpos.append(p)
elif p.kind == inspect.Parameter.KEYWORD_ONLY:
key.append(p)
elif p.kind == inspect.Parameter.VAR_KEYWORD:
varkey.append(p)
else:
assert False
param_names = frozenset(signature.parameters.keys())
if output:
output_name = _gensym(param_names, prefix="ret")
outstr = "return " + output_name
param_names = param_names | frozenset({output_name})
key.append(inspect.Parameter(output_name, kind=inspect.Parameter.KEYWORD_ONLY))
else:
outstr = "pass"
scope = {name: None}
name_to_annotation = {}
name_to_default = {}
param_triples = (
(p.name, p.annotation, p.default) for p in signature.parameters.values()
)
if output:
triples = it.chain(
param_triples,
[
("return", signature.return_annotation, inspect.Signature.empty),
(output_name, Any, inspect.Signature.empty),
],
)
else:
triples = it.chain(
param_triples,
[("return", signature.return_annotation, inspect.Signature.empty)],
)
for p_name, p_annotation, p_default in triples:
annotation_name = _gensym(frozenset(scope.keys()) | param_names, prefix="T")
name_to_annotation[p_name] = annotation_name
if p_annotation is inspect.Signature.empty or isinstance(p_annotation, str):
# If we have a stringified annotation here it's because the get_type_hints
# lookup above failed. Typically this occurs when using a local variable as
# the annotation. In this case we really have no idea what the annotation
# refers to, so just set it to Any.
# This does mean that we don't handle partially-stringified local
# annotations, e.g. `type["Foo"]` for some local type `Foo`. Those will
# probably just error out. Nothing better we can do about that
# unfortunately.
scope[annotation_name] = Any
else:
scope[annotation_name] = p_annotation
default_name = _gensym(frozenset(scope.keys()) | param_names, prefix="default")
name_to_default[p_name] = default_name
scope[default_name] = p_default
argstr_pieces = []
if len(pos) > 0:
for p in pos:
argstr_pieces.append(_make_argpiece(p, name_to_annotation, name_to_default))
argstr_pieces.append("/")
if len(pos_or_key) > 0:
for p in pos_or_key:
argstr_pieces.append(_make_argpiece(p, name_to_annotation, name_to_default))
if len(varpos) == 1:
[p] = varpos
argstr_pieces.append(
"*" + _make_argpiece(p, name_to_annotation, name_to_default)
)
else:
assert len(varpos) == 0
if len(key) > 0:
argstr_pieces.append("*")
if len(key) > 0:
for p in key:
argstr_pieces.append(_make_argpiece(p, name_to_annotation, name_to_default))
if len(varkey) == 1:
[p] = varkey
argstr_pieces.append(
"**" + _make_argpiece(p, name_to_annotation, name_to_default)
)
else:
assert len(varkey) == 0
argstr = ", ".join(argstr_pieces)
if signature.return_annotation is inspect.Signature.empty:
retstr = ""
else:
retstr = f"-> {name_to_annotation['return']}"
fnstr = f"def {name}({argstr}){retstr}:\n {outstr}"
exec(fnstr, scope)
fn = scope[name]
fn.__module__ = module
assert fn is not None
if output:
return fn, output_name
else:
return fn
def _gensym(names: frozenset[str], prefix: str) -> str:
assert prefix.isidentifier()
output_index = 0
output_name = prefix + str(output_index)
while output_name in names:
output_index += 1
output_name = prefix + str(output_index)
assert output_name.isidentifier()
return output_name
def _make_argpiece(p, name_to_annotation, name_to_default):
if p.default is inspect.Signature.empty:
return f"{p.name}: {name_to_annotation[p.name]}"
else:
return f"{p.name}: {name_to_annotation[p.name]} = {name_to_default[p.name]}"
def _get_problem_arg(
param_signature: inspect.Signature, args, kwargs, arguments, module, typechecker
) -> str:
"""Determines which argument was likely to be the problematic one responsible for
raising a type-check error.
"""
# No performance concerns, as this is only used when we're about to raise an error
# anyway.
for keep_name in param_signature.parameters.keys():
new_parameters = []
keep_annotation = sentinel = object()
for p_name, p in param_signature.parameters.items():
if p_name == keep_name:
new_parameters.append(
inspect.Parameter(p.name, p.kind, annotation=p.annotation)
)
assert keep_annotation is sentinel
keep_annotation = _remove_typing(p.annotation)
else:
new_parameters.append(inspect.Parameter(p.name, p.kind))
assert keep_annotation is not sentinel
new_signature = inspect.Signature(new_parameters)
fn = _make_fn_with_signature(
"check_single_arg", new_signature, module, output=False
)
fn = typechecker(fn) # but no `jaxtyped`; keep the same environment.
try:
fn(*args, **kwargs)
except Exception:
keep_value = _pformat(arguments[keep_name], short_self=False)
return (
f"\nThe problem arose whilst typechecking parameter '{keep_name}'.\n"
f"Actual value: {keep_value}\n"
f"Expected type: {keep_annotation}."
)
else:
# Could not localise the problem to a single argument -- probably due to
# e.g. a mismatched typevar, which each individual argument is okay with.
return ""
def _remove_typing(x):
x = str(x)
x = x.replace(" jaxtyping.", " ")
x = x.replace("[jaxtyping.", "[")
x = x.replace("'jaxtyping.", "'")
x = x.replace(" typing.", " ")
x = x.replace("[typing.", "[")
x = x.replace("'typing.", "'")
return x
def _pformat(x, short_self: bool):
# No performance concerns from delayed imports -- this is only used when we're about
# to raise an error anyway.
try:
# TODO(kidger): this is pretty ugly. We have a circular dependency
# equinox->jaxtyping->equinox. We could consider moving all the pretty-printing
# code from equinox into jaxtyping maybe? Or into some shared dependency?
import equinox as eqx
pformat = eqx.tree_pformat
if short_self:
try:
self = x["self"]
except KeyError:
pass
else:
is_self = lambda y: y is self
pformat = ft.partial(pformat, truncate_leaf=is_self)
except Exception:
import pprint
pformat = ft.partial(pprint.pformat, indent=2, compact=True)
return pformat(x)
class _jaxtyping_note_str(str):
"""Used with `_no_jaxtyping_note` to flag that a note came from jaxtyping."""
def _no_jaxtyping_note(e: Exception) -> bool:
"""Checks if any of the exception's notes are from jaxtyping."""
try:
notes = e.__notes__
except AttributeError:
return True
else:
for note in notes:
if isinstance(note, _jaxtyping_note_str):
return False
return True
_spacer = "--------------------\n"
+12
View File
@@ -0,0 +1,12 @@
class TypeCheckError(TypeError):
pass
# Not inheriting from TypeError as that gets caught and re-reraised as just a TypeError
# when using typeguard<3.
class AnnotationError(Exception):
pass
TypeCheckError.__module__ = "jaxtyping"
AnnotationError.__module__ = "jaxtyping"
+405
View File
@@ -0,0 +1,405 @@
# 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.
# This source code is adapted from typeguard:
# https://github.com/agronholm/typeguard/blob/0dd7f7510b7c694e66a0d17d1d58d185125bad5d/src/typeguard/importhook.py
#
# Copied and adapted in compliance with the terms of typeguard's MIT license.
# The original license is reproduced here.
#
# ---------
#
# This is the MIT license: http://www.opensource.org/licenses/mit-license.php
#
# Copyright (c) Alex Grönholm
#
# 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 ast
import functools as ft
import hashlib
import sys
from collections.abc import Sequence
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 Optional, Union
from unittest.mock import patch
# The name of this function is magical
def _call_with_frames_removed(f, *args, **kwargs):
return f(*args, **kwargs)
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.
# Version 7: Using the same md5 hash of the `typechecker` argument
# for importlib and decorator lookup.
# Version 8: Now using new-style `jaxtyped(typechecker=...)` rather than old-style
# double-decorators.
return cache_from_source(
path, debug_override, optimization=f"jaxtyping8{typechecker_hash}"
)
class Typechecker:
lookup = {}
def __init__(self, typechecker):
self.ast = None
if isinstance(typechecker, str):
# If the typechecker is a string, then we parse it
string_to_eval = (
"def f(x, *args, **kwargs):\n"
+ f" import {typechecker.split('.', 1)[0]}\n"
+ f" return {typechecker}(x, *args, **kwargs)"
)
# md5 hashing instead of __hash__
# because __hash__ is different for each Python session
self.hash = hashlib.md5(typechecker.encode("utf-8")).hexdigest()
vars = {}
exec(string_to_eval, {}, vars)
Typechecker.lookup[self.hash] = vars["f"]
elif typechecker is None:
# If it is None, ignore it silently (use dummy decorator)
self.hash = "0"
Typechecker.lookup[self.hash] = lambda x, *_, **__: x
else:
# Passed typechecker is invalid
raise TypeError(
"Jaxtyping typechecker has to be either a string or a None."
)
def get_hash(self):
return self.hash
def get_ast(self):
# we compile AST only if we missed importlib cache
if self.ast is None:
self.ast = (
ast.parse(
f"@jaxtyping.jaxtyped(typechecker=jaxtyping._import_hook.Typechecker.lookup['{self.hash}'])\n"
"def _():\n ..."
)
.body[0]
.decorator_list[0]
)
return self.ast
class JaxtypingTransformer(ast.NodeVisitor):
def __init__(self, *, typechecker: Typechecker) -> None:
self._parents: list[ast.AST] = []
self._typechecker = typechecker
def visit_Module(self, node: ast.Module):
# Insert "import jaxtyping" after any "from __future__ ..." imports
for i, child in enumerate(node.body):
if isinstance(child, ast.ImportFrom) and child.module == "__future__":
continue
elif isinstance(child, ast.Expr) and isinstance(child.value, ast.Str):
continue # module docstring
else:
node.body.insert(i, ast.Import(names=[ast.alias("jaxtyping", None)]))
break
self._parents.append(node)
self.generic_visit(node)
self._parents.pop()
return node
def visit_ClassDef(self, node: ast.ClassDef):
# Place at the start of the decorator list, so that `@dataclass` decorators get
# called first.
node.decorator_list.insert(0, self._typechecker.get_ast())
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 end of the decorator list, because:
# - as otherwise we wrap e.g. `jax.custom_{jvp,vjp}` and lose the ability
# to `defjvp` etc.
# - decorators frequently remove annotations from functions, and we'd like
# to use those annotations.
# - typeguard in particular wants to be at the end of the decorator list, as
# it works by recompling the wrapped function.
#
# Note that the counter-argument here is that we'd like to place this
# at the start of the decorator list, in case a typechecking annotation
# has been manually applied, and we'd need to be above that. In this
# case we're just going to have to need to ask the user to remove their
# typechecking annotation (and let this decorator do it instead).
# It's more important we be compatible with normal JAX code.
node.decorator_list.append(self._typechecker.get_ast())
self._parents.append(node)
self.generic_visit(node)
self._parents.pop()
return node
class _JaxtypingLoader(SourceFileLoader):
def __init__(self, *args, typechecker: Typechecker, **kwargs):
super().__init__(*args, **kwargs)
self._typechecker = typechecker
def source_to_code(self, data, path, *, _optimize=-1):
source = decode_source(data)
tree = _call_with_frames_removed(
compile,
source,
path,
"exec",
ast.PyCF_ONLY_AST,
dont_inherit=True,
optimize=_optimize,
)
tree = JaxtypingTransformer(typechecker=self._typechecker).visit(tree)
ast.fix_missing_locations(tree)
return _call_with_frames_removed(
compile, tree, path, "exec", dont_inherit=True, optimize=_optimize
)
def exec_module(self, module):
# Use a custom optimization marker - the import lock should make this monkey
# patch safe
with patch(
"importlib._bootstrap_external.cache_from_source",
ft.partial(_optimized_cache_from_source, self._typechecker.get_hash()),
):
return super().exec_module(module)
class _JaxtypingFinder(MetaPathFinder):
"""Wraps another path finder and instruments the module with `@jaxtyped` and
`@typechecked` if `should_instrument()` returns `True`.
Should not be used directly, but rather via `install_import_hook`.
"""
def __init__(self, modules, original_pathfinder, typechecker: Typechecker):
self.modules = modules
self._original_pathfinder = original_pathfinder
self._typechecker = typechecker
def find_spec(self, fullname, path=None, target=None):
if self.should_instrument(fullname):
spec = self._original_pathfinder.find_spec(fullname, path, target)
if spec is not None and isinstance(spec.loader, SourceFileLoader):
spec.loader = _JaxtypingLoader(
spec.loader.name, spec.loader.path, typechecker=self._typechecker
)
return spec
return None
def should_instrument(self, module_name: str) -> bool:
"""Determine whether the module with the given name should be instrumented.
**Arguments:**
- `module_name`: the full name of the module that is about to be imported
(e.g. ``xyz.abc``)
"""
for module in self.modules:
if module_name == module or module_name.startswith(module + "."):
return True
return False
class ImportHookManager:
def __init__(self, hook: MetaPathFinder):
self.hook = hook
def __enter__(self):
pass
def __exit__(self, exc_type, exc_val, exc_tb):
self.uninstall()
def uninstall(self):
try:
sys.meta_path.remove(self.hook)
except ValueError:
pass # already removed
# Deliberately no default for `typechecker` so that folks must opt-in to not having
# a typechecker.
def install_import_hook(modules: Union[str, Sequence[str]], typechecker: Optional[str]):
"""Automatically apply the `@jaxtyped(typechecker=typechecker)` decorator to every
function and dataclass over a whole codebase.
!!! Tip "Usage"
```python
from jaxtyping import install_import_hook
# Plus any one of the following:
# decorate `@jaxtyped(typechecker=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 and
# dataclasses.
# decorate `@jaxtyped(typechecker=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"], ...)
```
**Arguments:**:
- `modules`: the names of the modules in which to automatically apply `@jaxtyped`.
- `typechecker`: the module and function of the typechecker you want to use, as a
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: end-user script"
```python
### entry_point.py
from jaxtyping import install_import_hook
with install_import_hook("main", "typeguard.typechecked"):
import main
### 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.
```
!!! warning
If a function already has any decorators on it, then `@jaxtyped` will get added
at the bottom of the decorator list, e.g.
```python
@some_other_decorator
@jaxtyped(typechecker=beartype.beartype)
def foo(...): ...
```
This is to support the common case in which
`some_other_decorator = jax.custom_jvp` etc.
If a class already has any decorators in it, then `@jaxtyped` will get added to
the top of the decorator list, e.g.
```python
@jaxtyped(typechecker=beartype.beartype)
@some_other_decorator
class A:
...
```
This is to support the common case in which
`some_other_decorator = dataclasses.dataclass`.
""" # noqa: E501
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)
and finder.__name__ == "PathFinder"
and hasattr(finder, "find_spec")
):
break
else:
raise RuntimeError("Cannot find a PathFinder in sys.meta_path")
wrapped_typechecker = Typechecker(typechecker)
hook = _JaxtypingFinder(modules, finder, wrapped_typechecker)
sys.meta_path.insert(0, hook)
return ImportHookManager(hook)
+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.
# Note that `from typing 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 import (
Annotated as BFloat16, # noqa: F401
Annotated as Bool, # noqa: F401
Annotated as Complex, # noqa: F401
Annotated as Complex64, # noqa: F401
Annotated as Complex128, # noqa: F401
Annotated as Float, # noqa: F401
Annotated as Float16, # noqa: F401
Annotated as Float32, # noqa: F401
Annotated as Float64, # noqa: F401
Annotated as Inexact, # noqa: F401
Annotated as Int, # noqa: F401
Annotated as Int4, # noqa: F401
Annotated as Int8, # noqa: F401
Annotated as Int16, # noqa: F401
Annotated as Int32, # noqa: F401
Annotated as Int64, # noqa: F401
Annotated as Integer, # noqa: F401
Annotated as Key, # noqa: F401
Annotated as Num, # noqa: F401
Annotated as Real, # noqa: F401
Annotated as Shaped, # noqa: F401
Annotated as UInt, # noqa: F401
Annotated as UInt4, # noqa: F401
Annotated as UInt8, # noqa: F401
Annotated as UInt16, # noqa: F401
Annotated as UInt32, # noqa: F401
Annotated as UInt64, # noqa: F401
TYPE_CHECKING,
)
if not TYPE_CHECKING:
assert False
from jax import (
Array as PRNGKeyArray, # noqa: F401
Array as Scalar, # noqa: F401
)
from jax.typing import ArrayLike as ScalarLike # noqa: F401
+56
View File
@@ -0,0 +1,56 @@
# 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.
from ._import_hook import JaxtypingTransformer, Typechecker
def choose_typechecker_magics():
# The import is local to avoid degrading import times when the magic is
# not needed.
from IPython.core.magic import line_magic, Magics, magics_class
@magics_class
class ChooseTypecheckerMagics(Magics):
@line_magic("jaxtyping.typechecker")
def typechecker(self, typechecker):
# remove old JaxtypingTransformer, if present
self.shell.ast_transformers = list(
filter(
lambda x: not isinstance(x, JaxtypingTransformer),
self.shell.ast_transformers,
)
)
# add new one
self.shell.ast_transformers.append(
JaxtypingTransformer(typechecker=Typechecker(typechecker))
)
return ChooseTypecheckerMagics
def load_ipython_extension(ipython):
try:
ChooseTypecheckerMagics = choose_typechecker_magics()
except Exception as e:
# Very broad exception-handling, as e.g. IPython will sometimes be
# present but fail to import for mysterious reasons.
raise RuntimeError("Failed to define jaxtyping.typechecker magic") from e
ipython.register_magics(ChooseTypecheckerMagics)
@@ -19,7 +19,7 @@
import sys import sys
from .import_hook import install_import_hook from ._import_hook import install_import_hook
def pytest_addoption(parser): def pytest_addoption(parser):
@@ -52,4 +52,4 @@ def pytest_configure(config):
) )
raise RuntimeError(message.format(", ".join(already_imported_packages))) raise RuntimeError(message.format(", ".join(already_imported_packages)))
install_import_hook(packages, typechecker.rsplit(".", 1)) install_import_hook(packages, typechecker)
+322
View File
@@ -0,0 +1,322 @@
# 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 functools as ft
import typing
from typing import Any, Generic, TypeVar
import jax.tree_util as jtu
import typeguard
from ._errors import AnnotationError
from ._storage import (
clear_treeflatten_memo,
clear_treepath_memo,
get_shape_memo,
set_shape_memo,
set_treeflatten_memo,
set_treepath_memo,
)
_T = TypeVar("_T")
class _FakePyTree(Generic[_T]):
pass
_FakePyTree.__name__ = "PyTree"
_FakePyTree.__qualname__ = "PyTree"
_FakePyTree.__module__ = "builtins"
class _MetaPyTree(type):
def __call__(self, *args, **kwargs):
raise RuntimeError("PyTree cannot be instantiated")
def __instancecheck__(cls, obj):
if not hasattr(cls, "leaftype"):
return True # Just `isinstance(x, PyTree)`
# Handle beartype doing `isinstance(None, hint)` to check if
# is `instance`able.
if obj is None:
return True
single_memo, variadic_memo, pytree_memo, arg_memo = get_shape_memo()
single_memo_bak = single_memo.copy()
variadic_memo_bak = variadic_memo.copy()
pytree_memo_bak = pytree_memo.copy()
arg_memo_bak = arg_memo.copy()
try:
out = cls._check(obj, pytree_memo)
except Exception:
set_shape_memo(
single_memo_bak, variadic_memo_bak, pytree_memo_bak, arg_memo_bak
)
raise
if out:
return True
else:
set_shape_memo(
single_memo_bak, variadic_memo_bak, pytree_memo_bak, arg_memo_bak
)
return False
def _check(cls, obj, pytree_memo):
if cls.leaftype is Any:
def is_flatten_leaftype(x):
return False
def is_check_leaftype(x):
return True
else:
# We could use `isinstance` here but that would fail for more complicated
# types, e.g. PyTree[tuple[int]]. So at least internally we make a
# particular choice of typechecker.
#
# Deliberately not using @jaxtyped so that we share the same `memo` as
# whatever dynamic context we're currently in.
@typeguard.typechecked
def accepts_leaftype(x: cls.leaftype):
pass
def is_leaftype(x):
try:
accepts_leaftype(x)
except _TypeCheckError:
return False
else:
return True
is_flatten_leaftype = is_check_leaftype = is_leaftype
set_treeflatten_memo()
try:
leaves, structure = jtu.tree_flatten(obj, is_leaf=is_flatten_leaftype)
finally:
clear_treeflatten_memo()
if cls.structure is not None:
if cls.structure.isidentifier():
try:
prev_structure = pytree_memo[cls.structure]
except KeyError:
pytree_memo[cls.structure] = structure
else:
if prev_structure != structure:
return False
else:
named_pytree = 0
pieces = cls.structure.split()
if pieces[0] == "...":
pieces = pieces[1:]
prefix = False
suffix = True
elif pieces[-1] == "...":
pieces = pieces[:-1]
prefix = True
suffix = False
else:
prefix = False
suffix = False
for identifier in pieces:
try:
prev_structure = pytree_memo[identifier]
except KeyError as e:
raise AnnotationError(
f"Cannot process composite structure '{cls.structure}' "
f"as the structure name {identifier} has not been seen "
"before."
) from e
# Not using `PyTreeDef.compose` due to JAX bug #18218.
prev_pytree = jtu.tree_unflatten(
prev_structure, [0] * prev_structure.num_leaves
)
named_pytree = jtu.tree_map(lambda _: prev_pytree, named_pytree)
named_structure = jtu.tree_structure(named_pytree)
if prefix:
dummy_pytree = jtu.tree_unflatten(structure, [0] * len(leaves))
dummy_named = jtu.tree_unflatten(
named_structure, [0] * named_structure.num_leaves
)
try:
jtu.tree_map(lambda _, __: 0, dummy_named, dummy_pytree)
except ValueError:
return False
elif suffix:
has_structure = lambda x: jtu.tree_structure(x) == named_structure
dummy_pytree = jtu.tree_unflatten(structure, [0] * len(leaves))
dummy_leaves = jtu.tree_leaves(dummy_pytree, is_leaf=has_structure)
if any(not has_structure(x) for x in dummy_leaves):
return False
else:
if structure != named_structure:
return False
try:
for leaf_index, leaf in enumerate(leaves):
if cls.structure is not None:
set_treepath_memo(leaf_index, cls.structure)
if not is_check_leaftype(leaf):
return False
clear_treepath_memo()
finally:
clear_treepath_memo()
return True
# Can't return a generic (e.g. _FakePyTree[item]) because generic aliases don't do
# the custom __instancecheck__ that we want.
# We can't add that __instancecheck__ via subclassing, e.g.
# type("PyTree", (Generic[_T],), {}), because dynamic subclassing of typeforms
# isn't allowed.
# Likewise we can't do types.new_class("PyTree", (Generic[_T],), {}) because that
# has __module__ "types", e.g. we get types.PyTree[int].
@ft.lru_cache(maxsize=None)
def __getitem__(cls, item):
if isinstance(item, tuple):
if len(item) == 2:
class X(PyTree):
leaftype = item[0]
structure = item[1].strip()
if not isinstance(X.structure, str):
raise ValueError(
"The structure annotation `struct` in "
"`jaxtyping.PyTree[leaftype, struct]` must be be a string, "
f"e.g. `jaxtyping.PyTree[leaftype, 'T']`. Got '{X.structure}'."
)
pieces = X.structure.split()
if len(pieces) == 0:
raise ValueError(
"The string `struct` in `jaxtyping.PyTree[leaftype, struct]` "
"cannot be the empty string."
)
for piece_index, piece in enumerate(pieces):
if (piece_index == 0) or (piece_index == len(pieces) - 1):
if piece == "...":
continue
if not piece.isidentifier():
raise ValueError(
"The string `struct` in "
"`jaxtyping.PyTree[leaftype, struct]` must be be a "
"whitespace-separated sequence of identifiers, e.g. "
"`jaxtyping.PyTree[leaftype, 'T']` or "
"`jaxtyping.PyTree[leaftype, 'foo bar']`.\n"
"(Here, 'identifier' is used in the same sense as in "
"regular Python, i.e. a valid variable name.)\n"
f"Got piece '{piece}' in overall structure '{X.structure}'."
)
name = str(_FakePyTree[item[0]])[:-1] + ', "' + item[1].strip() + '"]'
else:
raise ValueError(
"The subscript `foo` in `jaxtyping.PyTree[foo]` must either be a "
"leaf type, e.g. `PyTree[int]`, or a 2-tuple of leaf and "
"structure, e.g. `PyTree[int, 'T']`. Received a tuple of length "
f"{len(item)}."
)
else:
name = str(_FakePyTree[item])
class X(PyTree):
leaftype = item
structure = None
X.__name__ = name
X.__qualname__ = name
if getattr(typing, "GENERATING_DOCUMENTATION", False):
X.__module__ = "builtins"
else:
X.__module__ = "jaxtyping"
return X
try:
# new typeguard
_TypeCheckError = (TypeError, typeguard.TypeCheckError)
except AttributeError:
# old typeguard
_TypeCheckError = TypeError
# Can't do `class PyTree(Generic[_T]): ...` because we need to override the
# instancecheck for PyTree[foo], but subclassing
# `type(Generic[int])`, i.e. `typing._GenericAlias` is disallowed.
PyTree = _MetaPyTree("PyTree", (), {})
if getattr(typing, "GENERATING_DOCUMENTATION", False):
PyTree.__module__ = "builtins"
else:
PyTree.__module__ = "jaxtyping"
PyTree.__doc__ = """Represents a PyTree.
Annotations of the following sorts are supported:
```python
a: PyTree
b: PyTree[LeafType]
c: PyTree[LeafType, "T"]
d: PyTree[LeafType, "S T"]
e: PyTree[LeafType, "... T"]
f: PyTree[LeafType, "T ..."]
```
These correspond to:
a. A plain `PyTree` can be used an annotation, 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))
b. `PyTree[LeafType]` denotes a PyTree all of whose leaves match `LeafType`. For
example, `PyTree[int]` or `PyTree[Union[str, Float32[Array, "b c"]]]`.
c. A structure name can also be passed. In this case
`jax.tree_util.tree_structure(...)` will be called, and bound to the structure name.
This can be used to mark that multiple PyTrees all have the same structure:
```python
def f(x: PyTree[int, "T"], y: PyTree[int, "T"]):
...
```
Structures are bound to names in the same way as array shape annotations, i.e.
within the thread-local dynamic context of a [`jaxtyping.jaxtyped`][] decorator.
d. A composite structure can be declared. In this case the variable must have a PyTree
structure each to the composition of multiple previously-bound PyTree structures.
For example:
```python
def f(x: PyTree[int, "T"], y: PyTree[int, "S"], z: PyTree[int, "S T"]):
...
x = (1, 2)
y = {"key": 3}
z = {"key": (4, 5)} # structure is the composition of the structures of `y` and `z`
f(x, y, z)
```
When performing runtime type-checking, all the individual pieces must have already
been bound to structures, otherwise the composite structure check will throw an error.
e. A structure can begin with a `...`, to denote that the lower levels of the PyTree
must match the declared structure, but the upper levels can be arbitrary. As in the
previous case, all named pieces must already have been seen and their structures
bound.
f. A structure can end with a `...`, to denote that the PyTree must be a prefix of the
declared structure, but the lower levels can be arbitrary. As in the previous two
cases, all named pieces must already have been seen and their structures bound.
""" # noqa: E501
+176
View File
@@ -0,0 +1,176 @@
# 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 threading
from typing import Any, Optional
from ._errors import AnnotationError
_shape_storage = threading.local()
def _has_shape_memo():
return hasattr(_shape_storage, "memo_stack") and len(_shape_storage.memo_stack) != 0
def get_shape_memo():
if _has_shape_memo():
single_memo, variadic_memo, pytree_memo, arguments = _shape_storage.memo_stack[
-1
]
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 = {}
pytree_memo = {}
arguments = {}
return single_memo, variadic_memo, pytree_memo, arguments
def set_shape_memo(single_memo, variadic_memo, pytree_memo, arg_memo) -> None:
if _has_shape_memo():
_shape_storage.memo_stack[-1] = (
single_memo,
variadic_memo,
pytree_memo,
arg_memo,
)
def push_shape_memo(arguments: dict[str, Any]):
try:
memo_stack = _shape_storage.memo_stack
except AttributeError:
# Can't be done when `_stack_storage` is created for reasons I forget.
memo_stack = _shape_storage.memo_stack = []
memos = ({}, {}, {}, arguments.copy())
memo_stack.append(memos)
return memos
def pop_shape_memo() -> None:
_shape_storage.memo_stack.pop()
def shape_str(memos) -> str:
"""Gives debug information on the current state of jaxtyping's internal memos.
Used in type-checking error messages.
**Arguments:**
- `memos`: as returned by `get_shape_memo` or `push_shape_memo`.
"""
single_memo, variadic_memo, pytree_memo, _ = memos
single_memo = {
name: size
for name, size in single_memo.items()
if not name.startswith("~~delete~~")
}
variadic_memo = {
name: shape
for name, (_, shape) in variadic_memo.items()
if not name.startswith("~~delete~~")
}
pieces = []
if len(single_memo) > 0 or len(variadic_memo) > 0:
pieces.append(
"The current values for each jaxtyping axis annotation are as follows."
)
for name, size in single_memo.items():
pieces.append(f"{name}={size}")
for name, shape in variadic_memo.items():
pieces.append(f"{name}={shape}")
if len(pytree_memo) > 0:
pieces.append(
"The current values for each jaxtyping PyTree structure annotation are as "
"follows."
)
for name, structure in pytree_memo.items():
pieces.append(f"{name}={structure}")
return "\n".join(pieces)
def print_bindings():
"""Prints the values of the current jaxtyping axis bindings. Intended for debugging.
That is, whilst doing runtime type checking, so that e.g. the `foo` and `bar` of
`Float[Array, "foo bar"]` are assigned values -- this function will print out those
values.
**Arguments:**
Nothing.
**Returns:**
Nothing.
"""
print(shape_str(get_shape_memo()))
_treepath_storage = threading.local()
def clear_treepath_memo() -> None:
_treepath_storage.value = None
def set_treepath_memo(index: Optional[int], structure: str) -> None:
if hasattr(_treepath_storage, "value") and _treepath_storage.value is not None:
raise AnnotationError(
"Cannot typecheck annotations of the form "
"`PyTree[PyTree[Shaped[Array, '?foo'], 'T'], 'S']` as it is ambiguous "
"which PyTree the `?` annotation refers to."
)
if index is None:
_treepath_storage.value = f"~~delete~~({structure}) "
else:
# Appears in error messages, so human-readable
_treepath_storage.value = f"(Leaf {index} in structure {structure}) "
def get_treepath_memo() -> str:
if not hasattr(_treepath_storage, "value") or _treepath_storage.value is None:
raise AnnotationError(
"Cannot use `?` annotations, e.g. `Shaped[Array, '?foo']`, except "
"when contained with structured `PyTree` annotations, e.g. "
"`PyTree[Shaped[Array, '?foo'], 'T']`."
)
return _treepath_storage.value
_treeflatten_storage = threading.local()
def clear_treeflatten_memo() -> None:
_treeflatten_storage.value = False
def set_treeflatten_memo():
_treeflatten_storage.value = True
def get_treeflatten_memo():
try:
return _treeflatten_storage.value
except AttributeError:
return False
-508
View File
@@ -1,508 +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 enum
import functools as ft
from typing import Any, Dict, List, NoReturn, Optional, Tuple, TYPE_CHECKING, Union
from typing_extensions import Literal
import numpy as np
from .decorator import storage
_array_name_format = "dtype_and_shape"
def get_array_name_format():
return _array_name_format
def set_array_name_format(value):
global _array_name_format
_array_name_format = value
_any_dtype = object()
_anonymous_dim = object()
_anonymous_variadic_dim = object()
class _DimType(enum.Enum):
named = enum.auto()
fixed = enum.auto()
symbolic = enum.auto()
class _NamedDim:
def __init__(self, name, broadcastable):
self.name = name
self.broadcastable = broadcastable
class _NamedVariadicDim:
def __init__(self, name, broadcastable):
self.name = name
self.broadcastable = broadcastable
class _FixedDim:
def __init__(self, size, broadcastable):
self.size = size
self.broadcastable = broadcastable
class _SymbolicDim:
def __init__(self, expr, broadcastable):
self.expr = expr
self.broadcastable = broadcastable
_AbstractDimOrVariadicDim = Union[
Literal[_anonymous_dim],
Literal[_anonymous_variadic_dim],
_NamedDim,
_NamedVariadicDim,
_FixedDim,
_SymbolicDim,
]
_AbstractDim = Union[Literal[_anonymous_dim], _NamedDim, _FixedDim, _SymbolicDim]
def _check_dims(
cls_dims: List[_AbstractDim],
obj_shape: Tuple[int],
single_memo: Dict[str, int],
) -> bool:
assert len(cls_dims) == len(obj_shape)
for cls_dim, obj_size in zip(cls_dims, obj_shape):
if cls_dim is _anonymous_dim:
pass
elif cls_dim.broadcastable and obj_size == 1:
pass
elif type(cls_dim) is _FixedDim:
if cls_dim.size != obj_size:
return False
elif type(cls_dim) is _SymbolicDim:
try:
eval_size = eval(cls_dim.expr, single_memo)
except NameError as e:
raise NameError(
f"Cannot process symbolic dimension '{cls_dim.expr}' as some "
"dimension names have not been processed. In practice you should "
"usually only use symbolic dimensions in annotations for return "
"types, referring only to dimensions annotated for arguments."
) from e
if eval_size != obj_size:
return False
else:
assert type(cls_dim) is _NamedDim
try:
cls_size = single_memo[cls_dim.name]
except KeyError:
single_memo[cls_dim.name] = obj_size
else:
if cls_size != obj_size:
return False
return True
class _MetaAbstractArray(type):
def __instancecheck__(cls, obj):
if not isinstance(obj, cls.array_type):
return False
if hasattr(obj.dtype, "type") and hasattr(obj.dtype.type, "__name__"):
# JAX, numpy
dtype = obj.dtype.type.__name__
elif hasattr(obj.dtype, "as_numpy_dtype"):
# TensorFlow
dtype = obj.dtype.as_numpy_dtype.__name__
else:
# PyTorch
repr_dtype = repr(obj.dtype).split(".")
if len(repr_dtype) == 2 and repr_dtype[0] == "torch":
dtype = repr_dtype[1]
else:
raise RuntimeError(
"Unrecognised array/tensor type to extract dtype from"
)
if cls.dtypes is not _any_dtype and dtype not in cls.dtypes:
return False
if len(storage.memo_stack) == 0:
# `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:
storage.memo_stack[-1] = (
single_memo,
variadic_memo,
variadic_broadcast_memo,
)
return True
else:
return False
def _check_shape(
cls,
obj,
single_memo: Dict[str, int],
variadic_memo: Dict[str, Tuple[int, ...]],
variadic_broadcast_memo: Dict[str, List[Tuple[int, ...]]],
):
if cls.index_variadic is None:
if obj.ndim != len(cls.dims):
return False
return _check_dims(cls.dims, obj.shape, single_memo)
else:
if obj.ndim < len(cls.dims) - 1:
return False
i = cls.index_variadic
j = -(len(cls.dims) - i - 1)
if j == 0:
j = None
if not _check_dims(cls.dims[:i], obj.shape[:i], single_memo):
return False
if j is not None and not _check_dims(
cls.dims[j:], obj.shape[j:], single_memo
):
return False
variadic_dim = cls.dims[i]
if variadic_dim is _anonymous_variadic_dim:
return True
else:
assert type(variadic_dim) is _NamedVariadicDim
variadic_name = variadic_dim.name
try:
if variadic_dim.broadcastable:
variadic_shapes = variadic_broadcast_memo[variadic_name]
else:
variadic_shape = variadic_memo[variadic_name]
except KeyError:
if variadic_dim.broadcastable:
variadic_broadcast_memo[variadic_name] = [obj.shape[i:j]]
else:
variadic_memo[variadic_name] = obj.shape[i:j]
return True
else:
if variadic_dim.broadcastable:
new_shape = obj.shape[i:j]
for existing_shape in variadic_shapes:
try:
np.broadcast_shapes(new_shape, existing_shape)
except ValueError:
return False
variadic_shapes.append(new_shape)
return True
else:
return variadic_shape == obj.shape[i:j]
assert False
class AbstractArray(metaclass=_MetaAbstractArray):
array_type: Any
dtypes: List[str]
dims: List[_AbstractDimOrVariadicDim]
index_variadic: Optional[int]
class _MetaAbstractDtype(type):
def __instancecheck__(cls, obj: Any) -> NoReturn:
raise RuntimeError(
f"Do not use `isinstance(x, jaxtyping.{cls.__name__})`. If you want to "
"check just the dtype of an array, then use "
f'`jaxtyping.{cls.__name__}[jnp.ndarray, "..."]`.'
)
@ft.lru_cache(maxsize=None)
def __getitem__(cls, item: Tuple[Any, str]) -> _MetaAbstractArray:
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, 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
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"
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"
return out
class AbstractDtype(metaclass=_MetaAbstractDtype):
dtypes: Union[Literal[_any_dtype], List[str]]
def __init__(self, *args, **kwargs):
raise RuntimeError(
"AbstractDtype cannot be instantiated. Perhaps you wrote e.g. "
'`Float32("shape")` when you mean `Float32[jnp.ndarray, "shape"]`?'
)
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
dtypes: Union[Literal[_any_dtype], str, List[str]] = cls.dtypes
if isinstance(dtypes, str):
dtypes = [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"
def _make_dtype(_dtypes, name):
class _Cls(AbstractDtype):
dtypes = _dtypes
_Cls.__name__ = name
_Cls.__qualname__ = name
_Cls.__module__ = "jaxtyping"
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]
# We match NumPy's type hierarachy in what types to provide. See the diagram at
# https://numpy.org/doc/stable/reference/arrays.scalars.html#scalars
Bool = _make_dtype(_bool, "Bool")
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")
-47
View File
@@ -1,47 +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 functools as ft
import threading
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()
def jaxtyped(fn):
return ft.wraps(fn)(_Jaxtyped(fn))
-275
View File
@@ -1,275 +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.
# This source code is adapted from typeguard:
# https://github.com/agronholm/typeguard/blob/0dd7f7510b7c694e66a0d17d1d58d185125bad5d/src/typeguard/importhook.py
#
# Copied and adapted in compliance with the terms of typeguard's MIT license.
# The original license is reproduced here.
#
# ---------
#
# This is the MIT license: http://www.opensource.org/licenses/mit-license.php
#
# Copyright (c) Alex Grönholm
#
# 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 ast
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 unittest.mock import patch
# The name of this function is magical
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")
class _JaxtypingTransformer(ast.NodeVisitor):
def __init__(self, *, typechecker) -> None:
self._parents: List[ast.AST] = []
self._typechecker = typechecker
def visit_Module(self, node: ast.Module):
# Insert "import typeguard; import jaxtping" after any "from __future__ ..."
# imports
for i, child in enumerate(node.body):
if isinstance(child, ast.ImportFrom) and child.module == "__future__":
continue
elif isinstance(child, ast.Expr) and isinstance(child.value, ast.Str):
continue # module docstring
else:
node.body.insert(i, ast.Import(names=[ast.alias("jaxtyping", None)]))
if self._typechecker is not None:
typechecker_module, _ = self._typechecker
node.body.insert(
i, ast.Import(names=[ast.alias(typechecker_module, None)])
)
break
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()
),
)
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(),
)
)
self._parents.append(node)
self.generic_visit(node)
self._parents.pop()
return node
class _JaxtypingLoader(SourceFileLoader):
def __init__(self, *args, typechecker, **kwargs):
super().__init__(*args, **kwargs)
self._typechecker = typechecker
def source_to_code(self, data, path, *, _optimize=-1):
source = decode_source(data)
tree = _call_with_frames_removed(
compile,
source,
path,
"exec",
ast.PyCF_ONLY_AST,
dont_inherit=True,
optimize=_optimize,
)
tree = _JaxtypingTransformer(typechecker=self._typechecker).visit(tree)
ast.fix_missing_locations(tree)
return _call_with_frames_removed(
compile, tree, path, "exec", dont_inherit=True, optimize=_optimize
)
def exec_module(self, module):
# Use a custom optimization marker - the import lock should make this monkey
# patch safe
with patch(
"importlib._bootstrap_external.cache_from_source",
_optimized_cache_from_source,
):
return super().exec_module(module)
class _JaxtypingFinder(MetaPathFinder):
"""Wraps another path finder and instruments the module with `@jaxtyped` and
`@typechecked` if `should_instrument()` returns `True`.
Should not be used directly, but rather via `install_import_hook`.
"""
def __init__(self, modules, original_pathfinder, typechecker):
self.modules = modules
self._original_pathfinder = original_pathfinder
self._typechecker = typechecker
def find_spec(self, fullname, path=None, target=None):
if self.should_instrument(fullname):
spec = self._original_pathfinder.find_spec(fullname, path, target)
if spec is not None and isinstance(spec.loader, SourceFileLoader):
spec.loader = _JaxtypingLoader(
spec.loader.name, spec.loader.path, typechecker=self._typechecker
)
return spec
return None
def should_instrument(self, module_name: str) -> bool:
"""Determine whether the module with the given name should be instrumented.
**Arguments:**
- `module_name`: the full name of the module that is about to be imported
(e.g. ``xyz.abc``)
"""
for module in self.modules:
if module_name == module or module_name.startswith(module + "."):
return True
return False
class ImportHookManager:
def __init__(self, hook: MetaPathFinder):
self.hook = hook
def __enter__(self):
pass
def __exit__(self, exc_type, exc_val, exc_tb):
self.uninstall()
def uninstall(self):
try:
sys.meta_path.remove(self.hook)
except ValueError:
pass # already removed
# 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.
It will only be applied to modules loaded **after** this hook has been installed.
**Arguments:**:
- `packages`: 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.
**Returns:**
A context manager that uninstalls the hook on exit, or when you call `.uninstall()`.
**Example:**
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
# main.py
from jaxtyped import f32
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]
for i, finder in enumerate(sys.meta_path):
if (
isclass(finder)
and finder.__name__ == "PathFinder"
and hasattr(finder, "find_spec")
):
break
else:
raise RuntimeError("Cannot find a PathFinder in sys.meta_path")
hook = _JaxtypingFinder(modules, finder, typechecker)
sys.meta_path.insert(0, hook)
return ImportHookManager(hook)
-89
View File
@@ -1,89 +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 functools as ft
from typing import Generic, TypeVar
import jax
import typeguard
_T = TypeVar("_T")
class _FakePyTree(Generic[_T]):
pass
_FakePyTree.__name__ = "PyTree"
_FakePyTree.__qualname__ = "PyTree"
_FakePyTree.__module__ = "builtins"
# Can't do type("PyTree", (Generic[_T],), {}) because dynamic subclassing of typeforms
# isn't allowed.
# Can't do types.new_class("PyTree", (Generic[_T],), {}) because that has __module__
# "types", e.g. we get types.PyTree[int].
class _MetaPyTree(type):
def __call__(self, *args, **kwargs):
raise RuntimeError("PyTree cannot be instantiated")
def __instancecheck__(cls, obj):
return True
@ft.lru_cache(maxsize=None)
def __getitem__(cls, item):
name = str(_FakePyTree[item])
out = _MetaSubscriptPyTree(name, (), {"leaftype": item})
out.__module__ = "jaxtyping"
return out
class _MetaSubscriptPyTree(type):
def __call__(self, *args, **kwargs):
raise RuntimeError("PyTree cannot be instantiated")
def __instancecheck__(cls, obj):
# We could use `isinstance` here but that would fail for more complicated
# types, e.g. PyTree[Tuple[int]]. So at least internally we make a particular
# choice of typechecker.
#
# Deliberately not using @jaxtyped so that we share the same `memo` as whatever
# dynamic context we're currently in.
@typeguard.typechecked
def accepts_leaftype(x: cls.leaftype):
pass
def is_leaftype(x):
try:
accepts_leaftype(x)
except TypeError:
return False
else:
return True
leaves = jax.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
# `type(Generic[int])`, i.e. `typing._GenericAlias` is disallowed.
+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.28"
description = "Type annotations and runtime checking for shape and dtype of JAX arrays, and PyTrees."
readme = "README.md"
requires-python ="~=3.9"
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"]
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],
)
+34
View File
@@ -41,6 +41,30 @@ def typecheck(request):
return request.param return request.param
@pytest.fixture(params=(False, True))
def jaxtyp(request):
import jaxtyping
if request.param:
# New-style
# @jaxtyping.jaxtyped(typechecker=typechecker)
# def f(...)
return lambda typechecker: jaxtyping.jaxtyped(typechecker=typechecker)
else:
# Old-style
# @jaxtyping.jaxtyped
# @typechecker
# def f(...)
def impl(typechecker):
def decorator(fn):
with pytest.warns(match="As of jaxtyping version 0.2.24"):
return jaxtyping.jaxtyped(typechecker(fn))
return decorator
return impl
@pytest.fixture() @pytest.fixture()
def getkey(): def getkey():
def _getkey(): def _getkey():
@@ -48,3 +72,13 @@ def getkey():
return jr.PRNGKey(random.randint(0, 2**31 - 1)) return jr.PRNGKey(random.randint(0, 2**31 - 1))
return _getkey return _getkey
@pytest.fixture(scope="module")
def beartype_or_skip():
yield pytest.importorskip("beartype")
@pytest.fixture(scope="module")
def typeguard_or_skip():
yield pytest.importorskip("typeguard")
+19 -4
View File
@@ -18,16 +18,31 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import equinox as eqx import equinox as eqx
import typeguard
ParamError = []
ReturnError = []
ParamError.append(TypeError) # old typeguard
ReturnError.append(TypeError) # old typeguard
try:
# new typeguard
ParamError.append(typeguard.TypeCheckError)
ReturnError.append(typeguard.TypeCheckError)
except AttributeError:
pass
try: try:
import beartype import beartype
except ImportError: except ImportError:
ParamError = TypeError pass
ReturnError = TypeError
else: else:
ParamError = (TypeError, beartype.roar.BeartypeCallHintParamViolation) ParamError.append(beartype.roar.BeartypeCallHintParamViolation)
ReturnError = (TypeError, beartype.roar.BeartypeCallHintReturnViolation) ReturnError.append(beartype.roar.BeartypeCallHintReturnViolation)
ParamError = tuple(ParamError)
ReturnError = tuple(ReturnError)
@eqx.filter_jit @eqx.filter_jit
+234
View File
@@ -0,0 +1,234 @@
# 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 helpers import ParamError, ReturnError
import jaxtyping
from jaxtyping import Float32, Int
#
# Test that functions get checked
#
def g(x: Float32[jnp.ndarray, " b"]):
pass
g(jnp.array([1.0]))
with pytest.raises(ParamError):
g(jnp.array(1))
#
# Test that Equinox modules get checked
#
# Dataclass `__init__`, no converter
class Mod1(eqx.Module):
foo: int
bar: Float32[jnp.ndarray, " a"]
Mod1(1, jnp.array([1.0]))
with pytest.raises(ParamError):
Mod1(1.0, jnp.array([1.0]))
with pytest.raises(ParamError):
Mod1(1, jnp.array(1.0))
# Dataclass `__init__`, converter
class Mod2(eqx.Module):
a: jnp.ndarray = eqx.field(converter=jnp.asarray)
Mod2(1) # This will fail unless we run typechecking after conversion
class BadMod2(eqx.Module):
a: jnp.ndarray = eqx.field(converter=lambda x: x)
with pytest.raises(ParamError):
BadMod2(1)
with pytest.raises(ParamError):
BadMod2("asdf")
# Custom `__init__`, no converter
class Mod3(eqx.Module):
foo: int
bar: Float32[jnp.ndarray, " a"]
def __init__(self, foo: str, bar: Float32[jnp.ndarray, " a"]):
self.foo = int(foo)
self.bar = bar
Mod3("1", jnp.array([1.0]))
with pytest.raises(ParamError):
Mod3(1, jnp.array([1.0]))
with pytest.raises(ParamError):
Mod3("1", jnp.array(1.0))
# Custom `__init__`, converter
class Mod4(eqx.Module):
a: Int[jnp.ndarray, ""] = eqx.field(converter=jnp.asarray)
def __init__(self, a: str):
self.a = int(a)
Mod4("1") # This will fail unless we run typechecking after conversion
# Custom `__post_init__`, no converter
class Mod5(eqx.Module):
foo: int
bar: Float32[jnp.ndarray, " a"]
def __post_init__(self):
pass
Mod5(1, jnp.array([1.0]))
with pytest.raises(ParamError):
Mod5(1.0, jnp.array([1.0]))
with pytest.raises(ParamError):
Mod5(1, jnp.array(1.0))
# Dataclass `__init__`, converter
class Mod6(eqx.Module):
a: jnp.ndarray = eqx.field(converter=jnp.asarray)
def __post_init__(self):
pass
Mod6(1) # This will fail unless we run typechecking after conversion
#
# Test that dataclasses get checked
#
@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))
#
# Test that methods get checked
#
class N(eqx.Module):
a: jnp.ndarray
def __init__(self, foo: str):
self.a = jnp.array(1)
def foo(self, x: jnp.ndarray):
pass
def bar(self) -> jnp.ndarray:
return self.a
n = N("hi")
with pytest.raises(ParamError):
N(123)
with pytest.raises(ParamError):
n.foo("not_an_array_either")
bad_n = eqx.tree_at(lambda x: x.a, n, "not_an_array")
with pytest.raises(ReturnError):
bad_n.bar()
#
# Test that we don't get called in `super()`.
#
called = False
class Base(eqx.Module):
x: int
def __init__(self):
self.x = "not an int"
global called
assert not called
called = True
class Derived(Base):
def __init__(self):
assert not called
super().__init__()
assert called
self.x = 2
Derived()
#
# Test that stringified type annotations work
class Foo:
pass
class Bar(eqx.Module):
x: type[Foo]
y: "type[Foo]"
# Note that this is the *only* kind of partially-stringified type annotation that
# is supported. This is for compatibility with older Equinox versions.
z: type["Foo"]
Bar(Foo, Foo, Foo)
with pytest.raises(ParamError):
Bar(1, Foo, Foo)
# Record that we've finished our checks successfully
jaxtyping._test_import_hook_counter += 1
-34
View File
@@ -1,34 +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 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))
-34
View File
@@ -1,34 +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 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))
@@ -1,20 +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.
from . import another_file
@@ -1,34 +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 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))
-34
View File
@@ -1,34 +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 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))
+9 -4
View File
@@ -1,4 +1,9 @@
equinox>=0.5.3 beartype
pytest>=7.0.1 cloudpickle
beartype>=0.10.4 equinox
typeguard>=2.13.3 IPython
jaxlib
pytest
pytest-asyncio
tensorflow
typeguard<3
+47
View File
@@ -0,0 +1,47 @@
# We have some pretty complicated semantics in `__init__.py`.
# Here we check that we didn't miss one of them on our runtime branch.
def test_all_importable():
# Ordered according to their appearance in the documentation.
from jaxtyping import ( # noqa: I001
Shaped, # noqa: F401
Bool, # noqa: F401
Key, # noqa: F401
Num, # noqa: F401
Inexact, # noqa: F401
Float, # noqa: F401
BFloat16, # noqa: F401
Float16, # noqa: F401
Float32, # noqa: F401
Float64, # noqa: F401
Complex, # noqa: F401
Complex64, # noqa: F401
Complex128, # noqa: F401
Integer, # noqa: F401
UInt, # noqa: F401
UInt4, # noqa: F401
UInt8, # noqa: F401
UInt16, # noqa: F401
UInt32, # noqa: F401
UInt64, # noqa: F401
Int, # noqa: F401
Int4, # noqa: F401
Int8, # noqa: F401
Int16, # noqa: F401
Int32, # noqa: F401
Int64, # noqa: F401
Real, # noqa: F401
Array, # noqa: F401
ArrayLike, # noqa: F401
Scalar, # noqa: F401
ScalarLike, # noqa: F401
PRNGKeyArray, # noqa: F401
PyTreeDef, # noqa: F401
PyTree, # noqa: F401
jaxtyped, # noqa: F401
install_import_hook, # noqa: F401
AbstractArray, # noqa: F401
AbstractDtype, # noqa: F401
print_bindings, # noqa: F401
get_array_name_format, # noqa: F401
set_array_name_format, # noqa: F401
)
+347 -64
View File
@@ -17,18 +17,41 @@
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import dataclasses as dc
import sys
from typing import get_args, get_origin, Union
import jax.numpy as jnp import jax.numpy as jnp
import jax.random as jr import jax.random as jr
import numpy as np
import pytest import pytest
from jaxtyping import AbstractDtype, Array, Float, Float32, jaxtyped, Shaped
try:
import torch
except ImportError:
torch = None
from jaxtyping import (
AbstractDtype,
AnnotationError,
Array,
ArrayLike,
Bool,
Float,
Float32,
jaxtyped,
Key,
PRNGKeyArray,
Scalar,
Shaped,
)
from .helpers import ParamError, ReturnError from .helpers import ParamError, ReturnError
def test_basic(typecheck): def test_basic(jaxtyp, typecheck):
@jaxtyped @jaxtyp(typecheck)
@typecheck
def g(x: Shaped[Array, "..."]): def g(x: Shaped[Array, "..."]):
pass pass
@@ -49,6 +72,7 @@ def test_dtypes():
Float64, Float64,
Inexact, Inexact,
Int, Int,
Int4,
Int8, Int8,
Int16, Int16,
Int32, Int32,
@@ -56,6 +80,7 @@ def test_dtypes():
Num, Num,
Shaped, Shaped,
UInt, UInt,
UInt4,
UInt8, UInt8,
UInt16, UInt16,
UInt32, UInt32,
@@ -67,16 +92,14 @@ def test_dtypes():
assert key == val.__name__ assert key == val.__name__
def test_return(typecheck, getkey): def test_return(jaxtyp, typecheck, getkey):
@jaxtyped @jaxtyp(typecheck)
@typecheck
def g(x: Float[Array, "b c"]) -> Float[Array, "c b"]: def g(x: Float[Array, "b c"]) -> Float[Array, "c b"]:
return jnp.transpose(x) return jnp.transpose(x)
g(jr.normal(getkey(), (3, 4))) g(jr.normal(getkey(), (3, 4)))
@jaxtyped @jaxtyp(typecheck)
@typecheck
def h(x: Float[Array, "b c"]) -> Float[Array, "b c"]: def h(x: Float[Array, "b c"]) -> Float[Array, "b c"]:
return jnp.transpose(x) return jnp.transpose(x)
@@ -84,9 +107,8 @@ def test_return(typecheck, getkey):
h(jr.normal(getkey(), (3, 4))) h(jr.normal(getkey(), (3, 4)))
def test_two_args(typecheck, getkey): def test_two_args(jaxtyp, typecheck, getkey):
@jaxtyped @jaxtyp(typecheck)
@typecheck
def g(x: Shaped[Array, "b c"], y: Shaped[Array, "c d"]): def g(x: Shaped[Array, "b c"], y: Shaped[Array, "c d"]):
return x @ y return x @ y
@@ -94,8 +116,7 @@ def test_two_args(typecheck, getkey):
with pytest.raises(ParamError): with pytest.raises(ParamError):
g(jr.normal(getkey(), (3, 4)), jr.normal(getkey(), (5, 4))) g(jr.normal(getkey(), (3, 4)), jr.normal(getkey(), (5, 4)))
@jaxtyped @jaxtyp(typecheck)
@typecheck
def h(x: Shaped[Array, "b c"], y: Shaped[Array, "c d"]) -> Shaped[Array, "b d"]: def h(x: Shaped[Array, "b c"], y: Shaped[Array, "c d"]) -> Shaped[Array, "b d"]:
return x @ y return x @ y
@@ -104,15 +125,16 @@ def test_two_args(typecheck, getkey):
h(jr.normal(getkey(), (3, 4)), jr.normal(getkey(), (5, 4))) h(jr.normal(getkey(), (3, 4)), jr.normal(getkey(), (5, 4)))
def test_any_dtype(typecheck, getkey): def test_any_dtype(jaxtyp, typecheck, getkey):
@jaxtyped @jaxtyp(typecheck)
@typecheck
def g(x: Shaped[Array, "a b"]) -> Shaped[Array, "a b"]: def g(x: Shaped[Array, "a b"]) -> Shaped[Array, "a b"]:
return x return x
g(jr.normal(getkey(), (3, 4))) g(jr.normal(getkey(), (3, 4)))
g(jnp.array([[True, False]])) g(jnp.array([[True, False]]))
g(jnp.array([[1, 2], [3, 4]], dtype=jnp.int4))
g(jnp.array([[1, 2], [3, 4]], dtype=jnp.int8)) g(jnp.array([[1, 2], [3, 4]], dtype=jnp.int8))
g(jnp.array([[1, 2], [3, 4]], dtype=jnp.uint4))
g(jnp.array([[1, 2], [3, 4]], dtype=jnp.uint16)) g(jnp.array([[1, 2], [3, 4]], dtype=jnp.uint16))
g(jr.normal(getkey(), (3, 4), dtype=jnp.complex128)) g(jr.normal(getkey(), (3, 4), dtype=jnp.complex128))
g(jr.normal(getkey(), (3, 4), dtype=jnp.bfloat16)) g(jr.normal(getkey(), (3, 4), dtype=jnp.bfloat16))
@@ -121,14 +143,12 @@ def test_any_dtype(typecheck, getkey):
g(jr.normal(getkey(), (1,))) g(jr.normal(getkey(), (1,)))
def test_nested_jaxtyped(typecheck, getkey): def test_nested_jaxtyped(jaxtyp, typecheck, getkey):
@jaxtyped @jaxtyp(typecheck)
@typecheck
def g(x: Float32[Array, "b c"], transpose: bool) -> Float32[Array, "c b"]: def g(x: Float32[Array, "b c"], transpose: bool) -> Float32[Array, "c b"]:
return h(x, transpose) return h(x, transpose)
@jaxtyped @jaxtyp(typecheck)
@typecheck
def h(x: Float32[Array, "c b"], transpose: bool) -> Float32[Array, "b c"]: def h(x: Float32[Array, "c b"], transpose: bool) -> Float32[Array, "b c"]:
if transpose: if transpose:
return jnp.transpose(x) return jnp.transpose(x)
@@ -142,9 +162,8 @@ def test_nested_jaxtyped(typecheck, getkey):
g(jr.normal(getkey(), (2, 3)), False) g(jr.normal(getkey(), (2, 3)), False)
def test_nested_nojaxtyped(typecheck, getkey): def test_nested_nojaxtyped(jaxtyp, typecheck, getkey):
@jaxtyped @jaxtyp(typecheck)
@typecheck
def g(x: Float32[Array, "b c"]): def g(x: Float32[Array, "b c"]):
return h(x) return h(x)
@@ -156,9 +175,8 @@ def test_nested_nojaxtyped(typecheck, getkey):
g(jr.normal(getkey(), (2, 3))) g(jr.normal(getkey(), (2, 3)))
def test_isinstance(typecheck, getkey): def test_isinstance(jaxtyp, typecheck, getkey):
@jaxtyped @jaxtyp(typecheck)
@typecheck
def g(x: Float32[Array, "b c"]) -> Float32[Array, " z"]: def g(x: Float32[Array, "b c"]) -> Float32[Array, " z"]:
y = jnp.transpose(x) y = jnp.transpose(x)
assert isinstance(y, Float32[Array, "c b"]) assert isinstance(y, Float32[Array, "c b"])
@@ -172,9 +190,8 @@ def test_isinstance(typecheck, getkey):
g(jr.normal(getkey(), (2, 3))) g(jr.normal(getkey(), (2, 3)))
def test_fixed(typecheck, getkey): def test_fixed(jaxtyp, typecheck, getkey):
@jaxtyped @jaxtyp(typecheck)
@typecheck
def g( def g(
x: Float32[Array, "4 5 foo"], y: Float32[Array, " foo"] x: Float32[Array, "4 5 foo"], y: Float32[Array, " foo"]
) -> Float32[Array, "4 5"]: ) -> Float32[Array, "4 5"]:
@@ -189,9 +206,8 @@ def test_fixed(typecheck, getkey):
g(c, b) g(c, b)
def test_anonymous(typecheck, getkey): def test_anonymous(jaxtyp, typecheck, getkey):
@jaxtyped @jaxtyp(typecheck)
@typecheck
def g(x: Float32[Array, "foo _"], y: Float32[Array, " _"]): def g(x: Float32[Array, "foo _"], y: Float32[Array, " _"]):
pass pass
@@ -200,9 +216,8 @@ def test_anonymous(typecheck, getkey):
g(a, b) g(a, b)
def test_named_variadic(typecheck, getkey): def test_named_variadic(jaxtyp, typecheck, getkey):
@jaxtyped @jaxtyp(typecheck)
@typecheck
def g( def g(
x: Float32[Array, "*batch foo"], x: Float32[Array, "*batch foo"],
y: Float32[Array, " *batch"], y: Float32[Array, " *batch"],
@@ -225,8 +240,7 @@ def test_named_variadic(typecheck, getkey):
with pytest.raises(ParamError): with pytest.raises(ParamError):
g(a2, b1, c) g(a2, b1, c)
@jaxtyped @jaxtyp(typecheck)
@typecheck
def h(x: Float32[Array, " foo *batch"], y: Float32[Array, " foo *batch bar"]): def h(x: Float32[Array, " foo *batch"], y: Float32[Array, " foo *batch bar"]):
pass pass
@@ -240,9 +254,8 @@ def test_named_variadic(typecheck, getkey):
h(b, c) h(b, c)
def test_anonymous_variadic(typecheck, getkey): def test_anonymous_variadic(jaxtyp, typecheck, getkey):
@jaxtyped @jaxtyp(typecheck)
@typecheck
def g(x: Float32[Array, "... foo"], y: Float32[Array, " foo"]): def g(x: Float32[Array, "... foo"], y: Float32[Array, " foo"]):
pass pass
@@ -262,9 +275,8 @@ def test_anonymous_variadic(typecheck, getkey):
g(a3, c) g(a3, c)
def test_broadcast_fixed(typecheck, getkey): def test_broadcast_fixed(jaxtyp, typecheck, getkey):
@jaxtyped @jaxtyp(typecheck)
@typecheck
def g(x: Float32[Array, "#4"]): def g(x: Float32[Array, "#4"]):
pass pass
@@ -275,9 +287,8 @@ def test_broadcast_fixed(typecheck, getkey):
g(jr.normal(getkey(), (3,))) g(jr.normal(getkey(), (3,)))
def test_broadcast_named(typecheck, getkey): def test_broadcast_named(jaxtyp, typecheck, getkey):
@jaxtyped @jaxtyp(typecheck)
@typecheck
def g(x: Float32[Array, " #foo"], y: Float32[Array, " #foo"]): def g(x: Float32[Array, " #foo"], y: Float32[Array, " #foo"]):
pass pass
@@ -299,9 +310,8 @@ def test_broadcast_named(typecheck, getkey):
g(b, a) g(b, a)
def test_broadcast_variadic_named(typecheck, getkey): def test_broadcast_variadic_named(jaxtyp, typecheck, getkey):
@jaxtyped @jaxtyp(typecheck)
@typecheck
def g(x: Float32[Array, " *#foo"], y: Float32[Array, " *#foo"]): def g(x: Float32[Array, " *#foo"], y: Float32[Array, " *#foo"]):
pass pass
@@ -358,29 +368,71 @@ def test_broadcast_variadic_named(typecheck, getkey):
g(o, a) g(o, a)
def test_variadic_mixed_broadcast(jaxtyp, typecheck, getkey):
@jaxtyp(typecheck)
def f(x: Float[Array, " *foo"], y: Float[Array, " #*foo"]):
pass
a = jr.normal(getkey(), (3, 4))
b = jr.normal(getkey(), (5,))
with pytest.raises(ParamError):
f(a, b)
c = jr.normal(getkey(), (7, 3, 2))
d = jr.normal(getkey(), (1, 2))
f(c, d)
def test_variadic_mixed_broadcast2(jaxtyp, typecheck, getkey):
@jaxtyp(typecheck)
def f(x: Float[Array, " *#foo"], y: Float[Array, " *foo"]):
pass
a = jr.normal(getkey(), (3, 4))
b = jr.normal(getkey(), (5,))
with pytest.raises(ParamError):
f(a, b)
c = jr.normal(getkey(), (1, 2))
d = jr.normal(getkey(), (7, 3, 2))
f(c, d)
def test_variadic_mixed_broadcast3(jaxtyp, typecheck, getkey):
@jaxtyp(typecheck)
def f(
x: Float[Array, "*B L D"],
*,
y: Float[Array, "*#B J d"],
z: Bool[Array, "*B L J"],
) -> Float[Array, "*B L D"]:
return x
x = jr.normal(getkey(), (2, 7, 3, 2, 2))
y = jr.bernoulli(getkey(), shape=(2, 7, 3, 2, 2))
z = jr.normal(getkey(), (2, 7, 1, 2, 2))
f(x, y=z, z=y)
def test_no_commas(): def test_no_commas():
with pytest.raises(ValueError): with pytest.raises(ValueError):
Float32[Array, "foo, bar"] Float32[Array, "foo, bar"]
def test_symbolic(typecheck, getkey): def test_symbolic(jaxtyp, typecheck, getkey):
@jaxtyped @jaxtyp(typecheck)
@typecheck
def make_slice(x: Float32[Array, " dim"]) -> Float32[Array, " dim-1"]: def make_slice(x: Float32[Array, " dim"]) -> Float32[Array, " dim-1"]:
return x[1:] return x[1:]
@jaxtyped @jaxtyp(typecheck)
@typecheck
def cat(x: Float32[Array, " dim"]) -> Float32[Array, " 2*dim"]: def cat(x: Float32[Array, " dim"]) -> Float32[Array, " 2*dim"]:
return jnp.concatenate([x, x]) return jnp.concatenate([x, x])
@jaxtyped @jaxtyp(typecheck)
@typecheck
def bad_make_slice(x: Float32[Array, " dim"]) -> Float32[Array, " dim-1"]: def bad_make_slice(x: Float32[Array, " dim"]) -> Float32[Array, " dim-1"]:
return x return x
@jaxtyped @jaxtyp(typecheck)
@typecheck
def bad_cat(x: Float32[Array, " dim"]) -> Float32[Array, " 2*dim"]: def bad_cat(x: Float32[Array, " dim"]) -> Float32[Array, " 2*dim"]:
return jnp.concatenate([x, x, x]) return jnp.concatenate([x, x, x])
@@ -400,12 +452,243 @@ def test_symbolic(typecheck, getkey):
bad_cat(x) bad_cat(x)
def test_incomplete_symbolic(typecheck, getkey): def test_incomplete_symbolic(jaxtyp, typecheck, getkey):
@jaxtyped @jaxtyp(typecheck)
@typecheck
def foo(x: Float32[Array, " 2*dim"]): def foo(x: Float32[Array, " 2*dim"]):
pass pass
x = jr.normal(getkey(), (4,)) x = jr.normal(getkey(), (4,))
with pytest.raises(NameError): with pytest.raises(AnnotationError):
foo(x) foo(x)
def test_deferred_symbolic_good(jaxtyp, typecheck):
@jaxtyp(typecheck)
def foo(dim: int, fill: Float[Array, ""]) -> Float[Array, " {dim}"]:
return jnp.full((dim,), fill)
class A:
size = 5
@jaxtyp(typecheck)
def bar(self, fill: Float[Array, ""]) -> Float[Array, " {self.size}"]:
return jnp.full((self.size,), fill)
foo(3, jnp.array(0.0))
A().bar(jnp.array(0.0))
def test_deferred_symbolic_bad(jaxtyp, typecheck):
@jaxtyp(typecheck)
def foo(dim: int, fill: Float[Array, ""]) -> Float[Array, " {dim-1}"]:
return jnp.full((dim,), fill)
class A:
size = 5
@jaxtyp(typecheck)
def bar(self, fill: Float[Array, ""]) -> Float[Array, " {self.size}-1"]:
return jnp.full((self.size,), fill)
with pytest.raises(ReturnError):
foo(3, jnp.array(0.0))
with pytest.raises(ReturnError):
A().bar(jnp.array(0.0))
def test_deferred_symbolic_dataclass(typecheck):
@jaxtyped(typechecker=typecheck)
@dc.dataclass
class A:
value: int
array: Float[Array, " {self.value}"]
A(3, jnp.zeros(3))
with pytest.raises(ParamError):
A(3, jnp.zeros(4))
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.number, ""],
float,
}
assert set(get_args(floatlike2)) == {
Float[Array, ""],
Float[np.ndarray, ""],
Float[np.number, ""],
float,
}
assert set(get_args(floatlike3)) == {
Float32[Array, "4"],
Float32[np.ndarray, "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"],
}
def test_subclass():
assert issubclass(Float[Array, ""], Array)
assert issubclass(Float[np.ndarray, ""], np.ndarray)
if torch is not None:
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(jaxtyp, typecheck):
@jaxtyp(typecheck)
def f(x: PRNGKeyArray):
pass
f(jr.key(0))
f(jr.PRNGKey(0))
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_key_dtype(jaxtyp, typecheck):
@jaxtyp(typecheck)
def f1(x: Key[Array, ""]):
pass
@jaxtyp(typecheck)
def f2(x: Key[Scalar, ""]):
pass
for f in (f1, f2):
f(jr.key(0))
with pytest.raises(ParamError):
f(jr.PRNGKey(0))
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(jaxtyp, 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"]
@jaxtyp(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))
def test_scalar_variadic_dim():
assert Float[float, "..."] is float
assert Float[float, "#*shape"] is float
# This one is a bit weird -- it should really also assert that shape==(), but we
# don't implement that.
assert Float[float, "*shape"] is float
def test_scalar_dtype_mismatch():
with pytest.raises(ValueError):
Float[bool, "..."]
+177 -7
View File
@@ -1,13 +1,183 @@
from jaxtyping import jaxtyped import abc
import jax.numpy as jnp
import jax.random as jr
import pytest
from jaxtyping import Array, Float, jaxtyped, print_bindings
from .helpers import ParamError, ReturnError
class M: class M(metaclass=abc.ABCMeta):
@jaxtyped @jaxtyped(typechecker=None)
def f(self):
...
@jaxtyped(typechecker=None)
@classmethod @classmethod
def f(cls): def g1(cls):
return 3 return 3
@classmethod
@jaxtyped(typechecker=None)
def g2(cls):
return 4
# Check that the @jaxtyped decorator doesn't blat the __get__ of @classmethod @jaxtyped(typechecker=None)
def test_decorator(): @staticmethod
assert M.f() == 3 def h1():
return 3
@staticmethod
@jaxtyped(typechecker=None)
def h2():
return 4
@jaxtyped(typechecker=None)
@abc.abstractmethod
def i1(self):
...
@abc.abstractmethod
@jaxtyped(typechecker=None)
def i2(self):
...
class N:
@jaxtyped(typechecker=None)
@property
def j1(self):
return 3
@property
@jaxtyped(typechecker=None)
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
def test_context(getkey):
a = jr.normal(getkey(), (3, 4))
b = jr.normal(getkey(), (5,))
with jaxtyped("context"):
assert isinstance(a, Float[Array, "foo bar"])
assert not isinstance(b, Float[Array, "foo"])
assert isinstance(a, Float[Array, "foo bar"])
assert isinstance(b, Float[Array, "foo"])
def test_varargs(jaxtyp, typecheck):
@jaxtyp(typecheck)
def f(*args):
pass
f(1, 2)
def test_varkwargs(jaxtyp, typecheck):
@jaxtyp(typecheck)
def f(**kwargs):
pass
f(a=1, b=2)
def test_defaults(jaxtyp, typecheck):
@jaxtyp(typecheck)
def f(x: int, y=1):
pass
f(1)
class _GlobalFoo:
pass
def test_global_stringified_annotation(jaxtyp, typecheck):
@jaxtyp(typecheck)
def f(x: "_GlobalFoo") -> "_GlobalFoo":
return x
f(_GlobalFoo())
@jaxtyp(typecheck)
def g(x: int) -> "_GlobalFoo":
return x
@jaxtyp(typecheck)
def h(x: "_GlobalFoo") -> int:
return x
with pytest.raises(ReturnError):
g(1)
with pytest.raises(ParamError):
h(1)
# This test does not use `jaxtyp(typecheck)` because typeguard does some evil stack
# frame introspection to try and grab local variables.
def test_local_stringified_annotation(typecheck):
class LocalFoo:
pass
@jaxtyped(typechecker=typecheck)
def f(x: "LocalFoo") -> "LocalFoo":
return x
f(LocalFoo())
with pytest.warns(match="As of jaxtyping version 0.2.24"):
@jaxtyped
@typecheck
def g(x: "LocalFoo") -> "LocalFoo":
return x
g(LocalFoo())
# We don't check that errors are raised if it goes wrong, since we can't usually
# resolve local type annotations at runtime. Best we can hope for is not to raise
# a spurious error about not being able to find the type.
def test_print_bindings(typecheck, capfd):
@jaxtyped(typechecker=typecheck)
def f(x: Float[Array, "foo bar"]):
print_bindings()
capfd.readouterr()
f(jnp.zeros((3, 4)))
text, _ = capfd.readouterr()
assert text == (
"The current values for each jaxtyping axis annotation are as follows."
"\nfoo=3\nbar=4\n"
)
+35
View File
@@ -0,0 +1,35 @@
from typing import Tuple, Union
import pytest
from jaxtyping import (
Array,
Float,
Float32,
Integer,
PRNGKeyArray,
PyTree,
Shaped,
)
@pytest.mark.parametrize(
"make_fn",
[
lambda: Float[Array, "4"],
lambda: Float32[Array, ""],
lambda: Integer[Array, "1 2 3"],
lambda: Shaped[PRNGKeyArray, "2"],
lambda: Float[float, "#*shape"],
lambda: PyTree[int],
lambda: PyTree[Float[Array, ""]],
lambda: PyTree[Float32[Array, "*m b c"]],
lambda: PyTree[PyTree[Float32[Array, "1 2 b *"]]],
lambda: PyTree[Union[str, Float32[Array, "1"]]],
lambda: PyTree[
Tuple[int, float, Float[Array, ""], PyTree[Union[Float[Array, ""], float]]]
],
],
)
def test_equals(make_fn):
assert make_fn() == make_fn()
+88
View File
@@ -0,0 +1,88 @@
from typing import AsyncIterator, Iterator
import jax.numpy as jnp
import pytest
from jaxtyping import Array, Float, Shaped
from .helpers import ParamError
try:
import torch
except ImportError:
torch = None
def test_generators_simple(jaxtyp, typecheck):
@jaxtyp(typecheck)
def gen(x: Float[Array, "*"]) -> Iterator[Float[Array, "*"]]:
yield x
@jaxtyp(typecheck)
def foo():
next(gen(jnp.zeros(2)))
next(gen(jnp.zeros((3, 4))))
foo()
def test_generators_return_no_annotations(jaxtyp, typecheck):
@jaxtyp(typecheck)
def gen(x: Float[Array, "*"]):
yield x
@jaxtyp(typecheck)
def foo():
next(gen(jnp.zeros(2)))
next(gen(jnp.zeros((3, 4))))
foo()
@pytest.mark.asyncio
async def test_async_generators_simple(jaxtyp, typecheck):
@jaxtyp(typecheck)
async def gen(x: Float[Array, "*"]) -> AsyncIterator[Float[Array, "*"]]:
yield x
@jaxtyp(typecheck)
async def foo():
async for _ in gen(jnp.zeros(2)):
pass
async for _ in gen(jnp.zeros((3, 4))):
pass
await foo()
def test_generators_dont_modify_same_annotations(jaxtyp, typecheck):
@jaxtyp(typecheck)
def g(x: Float[Array, "1"]) -> Iterator[Float[Array, "1"]]:
yield x
@jaxtyp(typecheck)
def m(x: Float[Array, "1"]) -> Float[Array, "1"]:
return x
with pytest.raises(ParamError):
next(g(jnp.zeros(2)))
with pytest.raises(ParamError):
m(jnp.zeros(2))
def test_generators_original_issue(jaxtyp, typecheck):
# Effectively the same as https://github.com/patrick-kidger/jaxtyping/issues/91
if torch is None:
pytest.skip("torch is not available")
@jaxtyp(typecheck)
def g(x: Shaped[torch.Tensor, "*"]) -> Iterator[Shaped[torch.Tensor, "*"]]:
yield x
@jaxtyp(typecheck)
def f():
next(g(torch.zeros(1)))
next(g(torch.zeros(2)))
f()
+89 -31
View File
@@ -17,47 +17,105 @@
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import importlib
import importlib.metadata
import pathlib
import shutil
import sys
import tempfile
import pytest import pytest
from jaxtyping import install_import_hook import jaxtyping
def test_import_hook_typeguard(): _here = pathlib.Path(__file__).parent
hook = install_import_hook(
"test.import_hook_tester_typeguard", ("typeguard", "typechecked")
)
from . import import_hook_tester_typeguard # noqa: F401
hook.uninstall()
def test_import_hook_beartype(): try:
typeguard_version = importlib.metadata.version("typeguard")
except Exception as e:
raise ImportError("Could not find typeguard version") from e
else:
try: try:
import beartype # noqa: F401 major, _, _ = typeguard_version.split(".")
except ImportError: major = int(major)
pytest.skip("Beartype not installed") except Exception as e:
else: raise ImportError(
hook = install_import_hook( f"Unexpected typeguard version {typeguard_version}; not formatted as "
"test.import_hook_tester_beartype", ("beartype", "beartype") "`major.minor.patch`"
) from e
if major != 2:
raise ImportError(
"jaxtyping's tests required typeguard version 2. (Versions 3 and 4 are both "
"known to have bugs.)"
) )
from . import import_hook_tester_beartype # noqa: F401
hook.uninstall()
def test_import_hook_transitive(): assert not hasattr(jaxtyping, "_test_import_hook_counter")
hook = install_import_hook( jaxtyping._test_import_hook_counter = 0
"test.import_hook_tester_transitive", ("typeguard", "typechecked")
)
from . import import_hook_tester_transitive # noqa: F401
hook.uninstall()
def test_import_hook_broken_checker(): @pytest.fixture(scope="module")
hook = install_import_hook( def importhook_tempdir():
"test.import_hook_tester_broken_checker", ("jaxtyping", "does_not_exist") with tempfile.TemporaryDirectory() as dir:
) sys.path.append(dir)
dir = pathlib.Path(dir)
shutil.copyfile(_here / "helpers.py", dir / "helpers.py")
yield dir
def _test_import_hook(importhook_tempdir, typechecker):
counter = jaxtyping._test_import_hook_counter
stem = f"import_hook_tester{counter}"
shutil.copyfile(_here / "import_hook_tester.py", importhook_tempdir / f"{stem}.py")
importlib.invalidate_caches()
with jaxtyping.install_import_hook(stem, typechecker):
importlib.import_module(stem)
assert counter + 1 == jaxtyping._test_import_hook_counter
# Tests start below...
def test_import_hook_typeguard(importhook_tempdir, typeguard_or_skip):
_test_import_hook(importhook_tempdir, "typeguard.typechecked")
def test_import_hook_beartype(importhook_tempdir, beartype_or_skip):
_test_import_hook(importhook_tempdir, "beartype.beartype")
def test_import_hook_beartype_full(importhook_tempdir, beartype_or_skip):
bearchecker = "beartype.beartype(conf=beartype.BeartypeConf(strategy=beartype.BeartypeStrategy.On))" # noqa: E501
_test_import_hook(importhook_tempdir, bearchecker)
def test_import_hook_typeguard_old(importhook_tempdir, typeguard_or_skip):
_test_import_hook(importhook_tempdir, ("typeguard", "typechecked"))
def test_import_hook_beartype_old(importhook_tempdir, beartype_or_skip):
_test_import_hook(importhook_tempdir, ("beartype", "beartype"))
def test_import_hook_broken_checker(importhook_tempdir):
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
from . import import_hook_tester_broken_checker # noqa: F401 _test_import_hook(importhook_tempdir, "jaxtyping.does_not_exist")
hook.uninstall()
def test_import_hook_transitive(importhook_tempdir, typeguard_or_skip):
counter = jaxtyping._test_import_hook_counter
transitive_name = "jaxtyping_transitive_test"
transitive_dir = importhook_tempdir / transitive_name
transitive_dir.mkdir()
shutil.copyfile(_here / "import_hook_tester.py", transitive_dir / "tester.py")
with open(transitive_dir / "__init__.py", "w") as f:
f.write("from . import tester")
f.flush()
importlib.invalidate_caches()
with jaxtyping.install_import_hook(transitive_name, "typeguard.typechecked"):
importlib.import_module(transitive_name)
assert counter + 1 == jaxtyping._test_import_hook_counter
+151
View File
@@ -0,0 +1,151 @@
import pytest
from IPython.testing.globalipapp import start_ipython
from .helpers import ParamError
@pytest.fixture(scope="session")
def session_ip():
yield start_ipython()
@pytest.fixture(scope="function")
def ip(session_ip):
session_ip.run_cell(raw_cell="import jaxtyping")
session_ip.run_line_magic(magic_name="load_ext", line="jaxtyping")
session_ip.run_line_magic(
magic_name="jaxtyping.typechecker", line="typeguard.typechecked"
)
yield session_ip
def test_that_ipython_works(ip):
ip.run_cell(raw_cell="x = 1").raise_error()
assert ip.user_global_ns["x"] == 1
def test_function_beartype(ip):
ip.run_cell(
raw_cell="""
def f(x: int):
pass
"""
).raise_error()
ip.run_cell(raw_cell="f(1)").raise_error()
with pytest.raises(ParamError):
ip.run_cell(raw_cell='f("x")').raise_error()
def test_function_none(ip):
ip.run_cell(
raw_cell="""
def f(a,b,c):
pass
"""
).raise_error()
ip.run_cell(raw_cell='f(1,2,"k")').raise_error()
def test_function_jaxtyped(ip):
ip.run_cell(
raw_cell="""
from jaxtyping import Float, Array, Int
import jax
def g(x: Float[Array, "1"]):
return x + 1
"""
).raise_error()
ip.run_cell(raw_cell="g(jax.numpy.array([1.0]))").raise_error()
with pytest.raises(ParamError):
ip.run_cell(raw_cell="g(jax.numpy.array(1.0))").raise_error()
with pytest.raises(ParamError):
ip.run_cell(raw_cell="g(jax.numpy.array([1]))").raise_error()
with pytest.raises(ParamError):
ip.run_cell(raw_cell="g(jax.numpy.array([2, 3]))").raise_error()
with pytest.raises(ParamError):
ip.run_cell(raw_cell='g("string")').raise_error()
def test_function_jaxtyped_and_jitted(ip):
ip.run_cell(
raw_cell="""
from jaxtyping import Float, Array, Int
import jax
@jax.jit
def g(x: Float[Array, "1"]):
return x + 1
"""
).raise_error()
ip.run_cell(raw_cell="g(jax.numpy.array([1.0]))").raise_error()
with pytest.raises(ParamError):
ip.run_cell(raw_cell="g(jax.numpy.array(1.0))").raise_error()
with pytest.raises(ParamError):
ip.run_cell(raw_cell="g(jax.numpy.array([1]))").raise_error()
with pytest.raises(ParamError):
ip.run_cell(raw_cell="g(jax.numpy.array([2, 3]))").raise_error()
with pytest.raises(ParamError):
ip.run_cell(raw_cell='g("string")').raise_error()
def test_class_jaxtyped(ip):
ip.run_cell(
raw_cell="""
from jaxtyping import Float, Array, Int
import equinox as eqx
import jax
class A(eqx.Module):
x: Float[Array, "2"]
def do_something(self, y: Int[Array, ""]):
return self.x + y
"""
).raise_error()
ip.run_cell(raw_cell="a = A(jax.numpy.array([1.0, 2.0]))").raise_error()
ip.run_cell(raw_cell="a.do_something(jax.numpy.array(2))").raise_error()
with pytest.raises(ParamError):
ip.run_cell(raw_cell="A(jax.numpy.array([1.0]))").raise_error()
with pytest.raises(ParamError):
ip.run_cell(
raw_cell="a.do_something(jax.numpy.array([2.0, 3.0]))"
).raise_error()
def test_class_not_dataclass(ip):
ip.run_cell(
raw_cell="""
from jaxtyping import Float, Array, Int
import equinox as eqx
import jax
class A:
def __init__(self, x):
self.x = x
def do_something(self, y):
return x + y
"""
).raise_error()
ip.run_cell(raw_cell="a = A(jax.numpy.array([1.0, 2.0]))").raise_error()
ip.run_cell(raw_cell="a.do_something(jax.numpy.array(2))").raise_error()
ip.run_cell(raw_cell="A(jax.numpy.array([1.0]))").raise_error()
ip.run_cell(raw_cell="a.do_something(jax.numpy.array([2.0, 3.0]))").raise_error()
+104
View File
@@ -0,0 +1,104 @@
from typing import Any
import equinox as eqx
import jax.numpy as jnp
import pytest
from jaxtyping import Array, Float, jaxtyped, PyTree, TypeCheckError
def test_arg_localisation(typecheck):
@jaxtyped(typechecker=typecheck)
def f(x: str, y: str, z: int):
pass
matches = [
"Type-check error whilst checking the parameters of f",
"The problem arose whilst typechecking parameter 'z'.",
"Called with parameters: {'x': 'hi', 'y': 'bye', 'z': 'not-an-int'}",
r"Parameter annotations: \(x: str, y: str, z: int\).",
]
for match in matches:
with pytest.raises(TypeCheckError, match=match):
f("hi", "bye", "not-an-int")
@jaxtyped(typechecker=typecheck)
def g(x: Float[Array, "a b"], y: Float[Array, "b c"]):
pass
x = jnp.zeros((2, 3))
y = jnp.zeros((4, 3))
matches = [
"Type-check error whilst checking the parameters of g",
"The problem arose whilst typechecking parameter 'y'.",
r"Called with parameters: {'x': f32\[2,3\], 'y': f32\[4,3\]}",
(
r"Parameter annotations: \(x: Float\[Array, 'a b'\], y: "
r"Float\[Array, 'b c'\]\)."
),
"The current values for each jaxtyping axis annotation are as follows.",
"a=2",
"b=3",
]
for match in matches:
with pytest.raises(TypeCheckError, match=match):
g(x, y=y)
def test_return(typecheck):
@jaxtyped(typechecker=typecheck)
def f(x: PyTree[Any, " T"], y: PyTree[Any, " S"]) -> PyTree[Any, "T S"]:
return "foo"
x = (1, 2)
y = {"a": 1}
matches = [
"Type-check error whilst checking the return value of f",
r"Called with parameters: {'x': \(1, 2\), 'y': {'a': 1}}",
"Actual value: 'foo'",
r"Expected type: PyTree\[Any, \"T S\"\].",
(
"The current values for each jaxtyping PyTree structure annotation are as "
"follows."
),
r"T=PyTreeDef\(\(\*, \*\)\)",
r"S=PyTreeDef\({'a': \*}\)",
]
for match in matches:
with pytest.raises(TypeCheckError, match=match):
f(x, y=y)
def test_dataclass_attribute(typecheck):
@jaxtyped(typechecker=typecheck)
class M(eqx.Module):
x: Float[Array, " *foo"]
y: PyTree[Any, " T"]
z: int
x = jnp.zeros((2, 3))
y = (1, (3, 4))
z = "not-an-int"
matches = [
"Type-check error whilst checking the parameters of M",
"The problem arose whilst typechecking parameter 'z'.",
(
r"Called with parameters: {'self': M\(\.\.\.\), 'x': f32\[2,3\], "
r"'y': \(1, \(3, 4\)\), 'z': 'not-an-int'}"
),
(
r"Parameter annotations: \(self: Any, x: Float\[Array, '\*foo'\], "
r"y: PyTree\[Any, \"T\"\], z: int\)."
),
"The current values for each jaxtyping axis annotation are as follows.",
r"foo=\(2, 3\)",
(
"The current values for each jaxtyping PyTree structure annotation are as "
"follows."
),
r"T=PyTreeDef\(\(\*, \(\*, \*\)\)\)",
]
for match in matches:
with pytest.raises(TypeCheckError, match=match):
M(x, y, z)
+25
View File
@@ -0,0 +1,25 @@
import subprocess
import sys
_py_path = sys.executable
def test_no_jax_dependency():
result = subprocess.run(
f"{_py_path} -c "
"'import jaxtyping; import sys; sys.exit(\"jax\" in sys.modules)'",
shell=True,
)
assert result.returncode == 0
# Meta-test: test that the above test will work. (i.e. that I haven't messed up using
# subprocess.)
def test_meta():
result = subprocess.run(
f"{_py_path} -c 'import jaxtyping; import jax; import sys; "
'sys.exit("jax" in sys.modules)\'',
shell=True,
)
assert result.returncode == 1
+195 -8
View File
@@ -17,7 +17,7 @@
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from typing import Tuple, Union from typing import NamedTuple, Tuple, Union
import equinox as eqx import equinox as eqx
import jax import jax
@@ -25,7 +25,8 @@ import jax.numpy as jnp
import jax.random as jr import jax.random as jr
import pytest import pytest
from jaxtyping import Float, jaxtyped, PyTree import jaxtyping
from jaxtyping import AnnotationError, Array, Float, PyTree
from .helpers import make_mlp, ParamError from .helpers import make_mlp, ParamError
@@ -92,9 +93,8 @@ def test_nested_pytrees(getkey, typecheck):
g([1, 2, make_mlp()]) g([1, 2, make_mlp()])
def test_pytree_array(typecheck): def test_pytree_array(jaxtyp, typecheck):
@jaxtyped @jaxtyp(typecheck)
@typecheck
def g(x: PyTree[Float[jnp.ndarray, "..."]]): def g(x: PyTree[Float[jnp.ndarray, "..."]]):
pass pass
@@ -106,9 +106,8 @@ def test_pytree_array(typecheck):
g(1.0) g(1.0)
def test_pytree_shaped_array(typecheck, getkey): def test_pytree_shaped_array(jaxtyp, typecheck, getkey):
@jaxtyped @jaxtyp(typecheck)
@typecheck
def g(x: PyTree[Float[jnp.ndarray, "b c"]]): def g(x: PyTree[Float[jnp.ndarray, "b c"]]):
pass pass
@@ -155,3 +154,191 @@ def test_pytree_tuple(typecheck):
g([1, 1]) g([1, 1])
with pytest.raises(ParamError): with pytest.raises(ParamError):
g([(1, 1), "hi"]) g([(1, 1), "hi"])
def test_pytree_namedtuple(typecheck):
class CustomNamedTuple(NamedTuple):
x: Float[jnp.ndarray, "a b"]
y: Float[jnp.ndarray, "b c"]
class OtherCustomNamedTuple(NamedTuple):
x: Float[jnp.ndarray, "a b"]
y: Float[jnp.ndarray, "b c"]
@typecheck
def g(x: PyTree[CustomNamedTuple]):
...
g(
CustomNamedTuple(
x=jax.random.normal(jax.random.PRNGKey(42), (3, 2)),
y=jax.random.normal(jax.random.PRNGKey(420), (2, 5)),
)
)
with pytest.raises(ParamError):
g(object())
with pytest.raises(ParamError):
g(
OtherCustomNamedTuple(
x=jax.random.normal(jax.random.PRNGKey(42), (3, 2)),
y=jax.random.normal(jax.random.PRNGKey(420), (2, 5)),
)
)
def test_subclass_pytree():
x = PyTree
y = PyTree[int]
assert issubclass(x, PyTree)
assert issubclass(y, PyTree)
assert not issubclass(int, PyTree)
def test_structure_match(jaxtyp, typecheck):
@jaxtyp(typecheck)
def f(x: PyTree[int, " T"], y: PyTree[str, " T"]):
pass
f(1, "hi")
f((3, 4, {"a": 5}), ("a", "b", {"a": "c"}))
with pytest.raises(ParamError):
f(1, ("hi",))
def test_structure_prefix(jaxtyp, typecheck):
@jaxtyp(typecheck)
def f(x: PyTree[int, " T"], y: PyTree[str, "T ..."]):
pass
f(1, "hi")
f((3, 4, {"a": 5}), ("a", "b", {"a": "c"}))
f(1, ("hi",))
f((1, 2), ({"a": "hi"}, {"a": "bye"}))
f((1, 2), ({"a": "hi"}, {"not-a": "bye"}))
with pytest.raises(ParamError):
f((1, 2), ({"a": "hi"}, {"a": "bye"}, {"a": "oh-no"}))
with pytest.raises(ParamError):
f((3, 4, 5), {"a": ("hi", "bye")})
def test_structure_suffix(jaxtyp, typecheck):
@jaxtyp(typecheck)
def f(x: PyTree[int, " T"], y: PyTree[str, "... T"]):
pass
f(1, "hi")
f((3, 4, {"a": 5}), ("a", "b", {"a": "c"}))
f(1, ("hi",))
with pytest.raises(ParamError):
f((3, 4), {"a": (1, 2)})
with pytest.raises(ParamError):
f((3, 4, 5), {"a": ("hi", "bye")})
def test_structure_compose(jaxtyp, typecheck):
@jaxtyp(typecheck)
def f(x: PyTree[int, " T"], y: PyTree[int, " S"], z: PyTree[str, "S T"]):
pass
f(1, 2, "hi")
f((1, 2), 2, ("a", "b"))
with pytest.raises(ParamError):
f((1, 2), 2, (1, 2))
f((1, 2), {"a": 3}, {"a": ("hi", "bye")})
with pytest.raises(ParamError):
f((1, 2), {"a": 3}, ({"a": "hi"}, {"a": "bye"}))
@jaxtyp(typecheck)
def g(x: PyTree[int, " T"], y: PyTree[int, " S"], z: PyTree[str, "T S"]):
pass
with pytest.raises(ParamError):
g((1, 2), {"a": 3}, {"a": ("hi", "bye")})
g((1, 2), {"a": 3}, ({"a": "hi"}, {"a": "bye"}))
@pytest.mark.parametrize("variadic", (False, True))
def test_treepath_dependence_function(variadic, jaxtyp, typecheck, getkey):
if variadic:
jtshape = "*?foo"
shape = (2, 3)
else:
jtshape = "?foo"
shape = (4,)
@jaxtyp(typecheck)
def f(
x: PyTree[Float[Array, jtshape], " T"], y: PyTree[Float[Array, jtshape], " T"]
):
pass
x1 = jr.normal(getkey(), shape)
y1 = jr.normal(getkey(), shape)
x2 = jr.normal(getkey(), (5,))
y2 = jr.normal(getkey(), (5,))
f(x1, y1)
f((x1, x2), (y1, y2))
with pytest.raises(ParamError):
f(x1, y2)
with pytest.raises(ParamError):
f((x1, x2), (y2, y1))
@pytest.mark.parametrize("variadic", (False, True))
def test_treepath_dependence_dataclass(variadic, typecheck, getkey):
if variadic:
jtshape = "*?foo"
shape = (2, 3)
else:
jtshape = "?foo"
shape = (4,)
@jaxtyping.jaxtyped(typechecker=typecheck)
class A(eqx.Module):
x: PyTree[Float[Array, jtshape], " T"]
y: PyTree[Float[Array, jtshape], " T"]
x1 = jr.normal(getkey(), shape)
y1 = jr.normal(getkey(), shape)
x2 = jr.normal(getkey(), (5,))
y2 = jr.normal(getkey(), (5,))
A(x1, y1)
A((x1, x2), (y1, y2))
with pytest.raises(ParamError):
A(x1, y2)
with pytest.raises(ParamError):
A((x1, x2), (y2, y1))
def test_treepath_dependence_missing_structure_annotation(jaxtyp, typecheck, getkey):
@jaxtyp(typecheck)
def f(x: PyTree[Float[Array, "?foo"], " T"], y: PyTree[Float[Array, "?foo"]]):
pass
x1 = jr.normal(getkey(), (2,))
y1 = jr.normal(getkey(), (2,))
with pytest.raises(AnnotationError, match="except when contained with structured"):
f(x1, y1)
def test_treepath_dependence_multiple_structure_annotation(jaxtyp, typecheck, getkey):
@jaxtyp(typecheck)
def f(x: PyTree[PyTree[Float[Array, "?foo"], " S"], " T"]):
pass
x1 = jr.normal(getkey(), (2,))
with pytest.raises(AnnotationError, match="ambiguous which PyTree"):
f(x1)
+25
View File
@@ -0,0 +1,25 @@
import cloudpickle
import numpy as np
try:
import torch
except ImportError:
torch = None
from jaxtyping import AbstractArray, Array, Shaped
def test_pickle():
x = cloudpickle.dumps(Shaped[Array, ""])
cloudpickle.loads(x)
y = cloudpickle.dumps(AbstractArray)
cloudpickle.loads(y)
z = cloudpickle.dumps(Shaped[np.ndarray, ""])
cloudpickle.loads(z)
if torch is not None:
w = cloudpickle.dumps(Shaped[torch.Tensor, ""])
cloudpickle.loads(w)
+13
View File
@@ -0,0 +1,13 @@
# Tensorflow dependency kept in a separate file, so that we can optionally exclude it
# more easily.
import tensorflow as tf
from jaxtyping import UInt
def test_tf_dtype():
x = tf.constant(1, dtype=tf.uint8)
y = tf.constant(1, dtype=tf.float32)
hint = UInt[tf.Tensor, "..."]
assert isinstance(x, hint)
assert not isinstance(y, hint)
+26 -4
View File
@@ -25,9 +25,21 @@ from typeguard import typechecked
from jaxtyping import Array, Float, jaxtyped from jaxtyping import Array, Float, jaxtyped
def test_threading(): class _ErrorableThread(threading.Thread):
@jaxtyped def run(self):
@typechecked 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(typechecker=typechecked)
def add(x: Float[Array, "a b"], y: Float[Array, "a b"]) -> Float[Array, "a b"]: def add(x: Float[Array, "a b"], y: Float[Array, "a b"]) -> Float[Array, "a b"]:
return x + y return x + y
@@ -36,6 +48,16 @@ def test_threading():
b = jnp.array([[2.0, 3.0]]) b = jnp.array([[2.0, 3.0]])
add(a, b) add(a, b)
thread = threading.Thread(target=run) thread = _ErrorableThread(target=run)
thread.start()
thread.join()
def test_threading_nojaxtyped():
def run():
a = jnp.array([[1.0, 2.0]])
assert isinstance(a, Float[Array, "..."])
thread = _ErrorableThread(target=run)
thread.start() thread.start()
thread.join() thread.join()