Skip to content

Commit 2b78722

Browse files
authored
Add a context manager as an alternative to monkey patching (#91)
* Add a context manager for default dims * Add a docstring and fix lint issues * Add default linalg dims to package exports * add type hints * Handle all iterables * Add missing contextmanager wrapper * Resolve PR comments
1 parent 85199d3 commit 2b78722

3 files changed

Lines changed: 81 additions & 6 deletions

File tree

src/xarray_einstats/linalg.py

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88
example usage.
99
1010
The functions that are not available via the accessor are ``einsum``, ``einsum_path``,
11-
``matmul`` and ``get_default_dims``.
11+
``matmul``, ``get_default_dims`` and ``default_dims``.
1212
1313
"""
1414

15+
import sys
16+
from collections.abc import Iterable
17+
from contextlib import contextmanager
18+
1519
import numpy as np
1620
import xarray as xr
1721

@@ -35,6 +39,7 @@
3539
"solve",
3640
"inv",
3741
"pinv",
42+
"default_dims",
3843
]
3944

4045

@@ -109,6 +114,10 @@ def get_default_dims(dims1, dims2):
109114
110115
You can still use ``dims`` explicitly to override those defaults.
111116
117+
.. note::
118+
Monkeypatching ``get_default_dims`` directly works but is error-prone.
119+
Consider using the :func:`default_dims` context manager instead.
120+
112121
"""
113122
raise MissingMonkeypatchError()
114123

@@ -119,12 +128,60 @@ def _attempt_default_dims(func, da1_dims, da2_dims=None):
119128
aux = get_default_dims(da1_dims, da2_dims)
120129
except MissingMonkeypatchError:
121130
raise TypeError(
122-
f"{func} missing required argument dims. You must monkeypatch "
123-
"xarray_einstats.linalg.get_default_dims for dims=None to be supported"
131+
f"{func} missing required argument dims. Use "
132+
"xarray_einstats.linalg.default_dims context manager or pass dims explicitly"
124133
) from None
125134
return aux
126135

127136

137+
@contextmanager
138+
def default_dims(func_or_dims):
139+
"""Context manager to temporarily set the default dimensions for linalg functions.
140+
141+
Safer alternative to monkey patching :func:`get_default_dims`,
142+
as it ensures that the original function is restored even if an error occurs
143+
within the context.
144+
145+
Parameters
146+
----------
147+
func_or_dims : callable or iterable
148+
If a callable is provided, it should take the same arguments as :func:`get_default_dims`
149+
and return the default dimensions based on those arguments.
150+
If an iterable is provided, it will be used as the default dimensions
151+
regardless of the input arguments.
152+
153+
See Also
154+
--------
155+
get_default_dims
156+
157+
Examples
158+
--------
159+
Set the default dims to ``("dim", "dim2")`` for the duration of the ``with`` block:
160+
161+
.. code-block:: python
162+
163+
from xarray_einstats import linalg, tutorial
164+
da = tutorial.generate_matrices_dataarray(5)
165+
166+
with linalg.default_dims(("dim", "dim2")):
167+
linalg.inv(da)
168+
169+
"""
170+
_linalg = sys.modules[__name__]
171+
original_get_default_dims = _linalg.get_default_dims
172+
173+
def func(*args):
174+
if isinstance(func_or_dims, Iterable):
175+
return func_or_dims
176+
return func_or_dims(*args)
177+
178+
_linalg.get_default_dims = func
179+
try:
180+
yield
181+
finally:
182+
_linalg.get_default_dims = original_get_default_dims
183+
184+
128185
class PairHandler:
129186
def __init__(self, all_dims, keep_dims):
130187
self.potential_out_dims = keep_dims.union(all_dims)

src/xarray_einstats/linalg.pyi

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# File generated with docstub
22

33
import numbers
4-
from collections.abc import Hashable, Sequence
5-
from typing import Literal
4+
from collections.abc import Hashable, Iterable, Sequence
5+
from contextlib import contextmanager
6+
from typing import Callable, Generator, Literal
67

78
import numpy as np
89
import xarray
9-
import xarray as xr
1010
from _typeshed import Incomplete
1111
from numpy.typing import NDArray
1212

@@ -30,6 +30,7 @@ __all__ = [
3030
"solve",
3131
"inv",
3232
"pinv",
33+
"default_dims",
3334
]
3435

3536
class MissingMonkeypatchError(Exception):
@@ -195,3 +196,7 @@ def pinv(
195196
hermitian: bool = ...,
196197
**kwargs: Incomplete,
197198
) -> xarray.DataArray: ...
199+
@contextmanager
200+
def default_dims(
201+
func_or_dims: Callable | Iterable,
202+
) -> Generator[None, None, None]: ...

tests/test_linalg.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,19 @@ def default_dims(dims1, dims2): # pylint: disable=unused-argument
7979
assert out.dims == matrices.dims
8080

8181

82+
def test_default_dims_context_manager(matrices):
83+
with pytest.raises(TypeError, match="missing required argument dims"):
84+
inv(matrices)
85+
86+
with linalg.default_dims(("dim", "dim2")):
87+
out = inv(matrices)
88+
assert out.dims == matrices.dims
89+
90+
# outside the context, it should raise again
91+
with pytest.raises(TypeError, match="missing required argument dims"):
92+
inv(matrices)
93+
94+
8295
class TestEinsumFamily:
8396
# raw_einsum calls einsum, so the tests on raw_einsum also cover einsum, then
8497
# there are some specific ones for various reasons,

0 commit comments

Comments
 (0)