Compare commits

..
5 Commits
Author SHA1 Message Date
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
15 changed files with 238 additions and 126 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
-7
View File
@@ -1,7 +0,0 @@
[settings]
force_alphabetical_sort_within_sections=true
lines_after_imports=2
profile=black
combine_as_imports=True
treat_comments_as_code=true
extra_standard_library=typing_extensions
+3 -13
View File
@@ -22,17 +22,7 @@ repos:
rev: 22.3.0 rev: 22.3.0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/nbQA-dev/nbQA - repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 1.6.3 rev: 'v0.0.255'
hooks: hooks:
- id: nbqa-black - id: ruff
- id: nbqa-isort
- id: nbqa-flake8
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/pycqa/flake8
rev: 4.0.1
hooks:
- id: flake8
+1
View File
@@ -17,6 +17,7 @@ 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 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 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 _ _"`. - Prepend `_` to a dimension to disable any runtime checking of that dimension (so that it can be used just as documentation). This can also be used as just `_` on its own: e.g. `"b c _ _"`.
- Documentation-only names (i.e. they're ignored by jaxtyping) can be handled by prepending a name followed by `=` e.g. `Float[Array, "rows=4 cols=3"]`.
When using multiple modifiers, their order does not matter. When using multiple modifiers, their order does not matter.
+2 -2
View File
@@ -12,13 +12,13 @@ jaxtyping and `jax.jit` synergise beautifully.
When calling JAX operations wrapped in a `jax.jit`, then the dtype/shape-checking will happen at trace time. (When JAX traces your function prior to compiling it.) The actual compiled code does not have any dtype/shape-checking, and will therefore still be just as fast as before! 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.
## 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)?
+2 -2
View File
@@ -110,8 +110,8 @@ if typing.TYPE_CHECKING:
# anything. (I believe this is sometimes called `Unknown`.) Thus, this odd-looking # anything. (I believe this is sometimes called `Unknown`.) Thus, this odd-looking
# annotation, which static type checkers aren't smart enough to resolve. # annotation, which static type checkers aren't smart enough to resolve.
elif has_jax: elif has_jax:
from .pytree_type import PyTree from .pytree_type import PyTree as PyTree # noqa: F401
del has_jax del has_jax
__version__ = "0.2.14" __version__ = "0.2.15"
+24 -11
View File
@@ -19,6 +19,8 @@
import enum import enum
import functools as ft import functools as ft
import sys
import types
import typing import typing
from typing import ( from typing import (
Any, Any,
@@ -267,6 +269,11 @@ class AbstractArray(metaclass=_MetaAbstractArray):
_not_made = object() _not_made = object()
_union_types = [typing.Union]
if sys.version_info >= (3, 10):
_union_types.append(types.UnionType)
@ft.lru_cache(maxsize=None) @ft.lru_cache(maxsize=None)
def _make_array(array_type, dim_str, dtypes, name): def _make_array(array_type, dim_str, dtypes, name):
if not isinstance(dim_str, str): if not isinstance(dim_str, str):
@@ -277,8 +284,10 @@ def _make_array(array_type, dim_str, dtypes, name):
dims = [] dims = []
index_variadic = None index_variadic = None
for index, elem in enumerate(dim_str.split()): for index, elem in enumerate(dim_str.split()):
if "," in elem: if "," in elem and "(" not in elem:
# Common mistake # Common mistake.
# Disable in the case that there's brackets to allow for function calls,
# e.g. `min(foo,bar)`, in symbolic dimensions.
raise ValueError("Dimensions should be separated with spaces, not commas") raise ValueError("Dimensions should be separated with spaces, not commas")
if elem.endswith("#"): if elem.endswith("#"):
raise ValueError( raise ValueError(
@@ -329,17 +338,21 @@ def _make_array(array_type, dim_str, dtypes, name):
) )
anonymous = True anonymous = True
elem = elem[1:] 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: else:
break break
try: if len(elem) == 0 or elem.isidentifier():
elem = int(elem) dim_type = _DimType.named
except ValueError:
if len(elem) == 0 or elem.isidentifier():
dim_type = _DimType.named
else:
dim_type = _DimType.symbolic
else: else:
dim_type = _DimType.fixed try:
elem = int(elem)
except ValueError:
dim_type = _DimType.symbolic
else:
dim_type = _DimType.fixed
if variadic: if variadic:
if index_variadic is not None: if index_variadic is not None:
@@ -460,7 +473,7 @@ class _MetaAbstractDtype(type):
) )
array_type, dim_str = item array_type, dim_str = item
del item del item
if typing.get_origin(array_type) is typing.Union: if typing.get_origin(array_type) in _union_types:
out = [ out = [
_make_array(x, dim_str, cls.dtypes, cls.__name__) _make_array(x, dim_str, cls.dtypes, cls.__name__)
for x in typing.get_args(array_type) for x in typing.get_args(array_type)
+44 -30
View File
@@ -21,40 +21,20 @@ import dataclasses
import functools as ft import functools as ft
import inspect import inspect
import threading import threading
import types
import weakref import weakref
storage = threading.local() storage = threading.local()
_fns = weakref.WeakKeyDictionary() _jaxtyped_fns = weakref.WeakSet()
class _Jaxtyped:
def __init__(self, fn):
# Stored externally so that it doesn't get blatted in the `ft.wraps` below by
# a function that already has a `fn` attribute.
_fns[self] = fn
def __get__(self, instance, owner):
fn = _fns[self]
return ft.wraps(fn)(_Jaxtyped(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(({}, {}, {}))
fn = _fns[self]
try:
return fn(*args, **kwargs)
finally:
memo_stack.pop()
def jaxtyped(fn): def jaxtyped(fn):
if inspect.isclass(fn): # allow decorators on class definitions if type(fn) is types.FunctionType and fn in _jaxtyped_fns:
return fn
elif inspect.isclass(fn): # allow decorators on class definitions
if dataclasses.is_dataclass(fn): if dataclasses.is_dataclass(fn):
init = jaxtyped(fn.__init__) init = jaxtyped(fn.__init__)
fn.__init__ = init fn.__init__ = init
@@ -63,8 +43,44 @@ def jaxtyped(fn):
raise ValueError( raise ValueError(
"jaxtyped may only be added as a class decorator to dataclasses" "jaxtyped may only be added as a class decorator to dataclasses"
) )
# It'd be lovely if we could handle arbitrary descriptors, and not just the builtin
# ones. Unfortunately that means returning a class instance with a __get__ method,
# and that turns out to break loads of other things. See beartype issue #211 and
# jaxtyping issue #71.
elif isinstance(fn, classmethod):
return classmethod(jaxtyped(fn.__func__))
elif isinstance(fn, staticmethod):
return staticmethod(jaxtyped(fn.__func__))
elif isinstance(fn, property):
if fn.fget is None:
fget = None
else:
fget = jaxtyped(fn.fget)
if fn.fset is None:
fset = None
else:
fset = jaxtyped(fn.fset)
if fn.fdel is None:
fdel = None
else:
fdel = jaxtyped(fn.fdel)
return property(fget=fget, fset=fset, fdel=fdel)
else: else:
return ft.wraps(fn)(_Jaxtyped(fn))
@ft.wraps(fn)
def wrapped_fn(*args, **kwargs):
try:
memo_stack = storage.memo_stack
except AttributeError:
memo_stack = storage.memo_stack = []
memo_stack.append(({}, {}, {}))
try:
return fn(*args, **kwargs)
finally:
memo_stack.pop()
_jaxtyped_fns.add(wrapped_fn)
return wrapped_fn
def _jaxtyped_typechecker(typechecker): def _jaxtyped_typechecker(typechecker):
@@ -77,10 +93,8 @@ def _jaxtyped_typechecker(typechecker):
def _wrapper(kls): def _wrapper(kls):
assert inspect.isclass(kls) assert inspect.isclass(kls)
if dataclasses.is_dataclass(kls): if dataclasses.is_dataclass(kls):
if type(kls.__init__) is not _Jaxtyped: init = jaxtyped(typechecker(kls.__init__))
# Extra `if` check to work around beartype bug #211 kls.__init__ = init
init = jaxtyped(typechecker(kls.__init__))
kls.__init__ = init
return kls return kls
return _wrapper return _wrapper
+10
View File
@@ -0,0 +1,10 @@
[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
@@ -17,4 +17,4 @@
# 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 . import another_file from . import another_file # noqa: F401
+26 -25
View File
@@ -17,9 +17,7 @@
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import dataclasses
import equinox as eqx
import jax.numpy as jnp import jax.numpy as jnp
import pytest import pytest
@@ -37,26 +35,29 @@ with pytest.raises(ParamError):
g(jnp.array(1)) g(jnp.array(1))
class M(eqx.Module): # Typeguard 3.0 no longer supports this.
foo: int #
bar: Float32[jnp.ndarray, " a"] # class M(eqx.Module):
# foo: int
# bar: Float32[jnp.ndarray, " a"]
M(1, jnp.array([1.0])) #
with pytest.raises(ParamError): #
M(1.0, jnp.array([1.0])) # M(1, jnp.array([1.0]))
with pytest.raises(ParamError): # with pytest.raises(ParamError):
M(1, jnp.array(1.0)) # M(1.0, jnp.array([1.0]))
# with pytest.raises(ParamError):
# M(1, jnp.array(1.0))
@dataclasses.dataclass #
class D: #
foo: int #
bar: Float32[jnp.ndarray, " a"] # @dataclasses.dataclass
# class D:
# foo: int
D(1, jnp.array([1.0])) # bar: Float32[jnp.ndarray, " a"]
with pytest.raises(ParamError): #
D(1.0, jnp.array([1.0])) #
with pytest.raises(ParamError): # D(1, jnp.array([1.0]))
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))
+26 -25
View File
@@ -17,9 +17,7 @@
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import dataclasses
import equinox as eqx
import jax.numpy as jnp import jax.numpy as jnp
import pytest import pytest
@@ -37,26 +35,29 @@ with pytest.raises(ParamError):
g(jnp.array(1)) g(jnp.array(1))
class M(eqx.Module): # Typeguard 3.0 no longer supports this.
foo: int #
bar: Float32[jnp.ndarray, " a"] # class M(eqx.Module):
# foo: int
# bar: Float32[jnp.ndarray, " a"]
M(1, jnp.array([1.0])) #
with pytest.raises(ParamError): #
M(1.0, jnp.array([1.0])) # M(1, jnp.array([1.0]))
with pytest.raises(ParamError): # with pytest.raises(ParamError):
M(1, jnp.array(1.0)) # M(1.0, jnp.array([1.0]))
# with pytest.raises(ParamError):
# M(1, jnp.array(1.0))
@dataclasses.dataclass #
class D: #
foo: int #
bar: Float32[jnp.ndarray, " a"] # @dataclasses.dataclass
# class D:
# foo: int
D(1, jnp.array([1.0])) # bar: Float32[jnp.ndarray, " a"]
with pytest.raises(ParamError): #
D(1.0, jnp.array([1.0])) #
with pytest.raises(ParamError): # D(1, jnp.array([1.0]))
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))
+42
View File
@@ -17,6 +17,7 @@
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import sys
from typing import get_args, get_origin, Union from typing import get_args, get_origin, Union
import jax.numpy as jnp import jax.numpy as jnp
@@ -470,3 +471,44 @@ def test_subclass():
assert issubclass(Float[Array, ""], Array) assert issubclass(Float[Array, ""], Array)
assert issubclass(Float[np.ndarray, ""], np.ndarray) assert issubclass(Float[np.ndarray, ""], np.ndarray)
assert issubclass(Float[torch.Tensor, ""], torch.Tensor) 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))
+56 -5
View File
@@ -4,23 +4,74 @@ from jaxtyping import jaxtyped
class M(metaclass=abc.ABCMeta): class M(metaclass=abc.ABCMeta):
@jaxtyped
def f(self):
...
@jaxtyped @jaxtyped
@classmethod @classmethod
def f(cls): def g1(cls):
return 3 return 3
@classmethod
@jaxtyped
def g2(cls):
return 4
@jaxtyped
@staticmethod
def h1():
return 3
@staticmethod
@jaxtyped
def h2():
return 4
@jaxtyped @jaxtyped
@abc.abstractmethod @abc.abstractmethod
def g(self): def i1(self):
...
@abc.abstractmethod
@jaxtyped
def i2(self):
... ...
# Check that the @jaxtyped decorator doesn't blat the __get__ of @classmethod class N:
@jaxtyped
@property
def j1(self):
return 3
@property
@jaxtyped
def j2(self):
return 4
def test_identity():
assert M.f is M.f
def test_classmethod(): def test_classmethod():
assert M.f() == 3 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 # Check that the @jaxtyped decorator doesn't blat the __isabstractmethod__ of
# @abstractmethod # @abstractmethod
def test_abstractmethod(): def test_abstractmethod():
assert M.g.__isabstractmethod__ assert M.i1.__isabstractmethod__
assert M.i2.__isabstractmethod__
def test_property():
assert N().j1 == 3
assert N().j2 == 4
+1 -1
View File
@@ -78,7 +78,7 @@ def test_import_hook_beartype_full():
def test_import_hook_transitive(): def test_import_hook_transitive():
hook = install_import_hook( hook = install_import_hook(
"test.import_hook_tester_transitive", "typeguard.typechecked" "test.import_hook_tester_transitive", "beartype.beartype"
) )
with hook: with hook:
from . import import_hook_tester_transitive # noqa: F401 from . import import_hook_tester_transitive # noqa: F401