Skip to content

Commit 44b8a4b

Browse files
committed
Make it possible to use rc_context as a decorator.
See changelog note. There are also other advantages with contextmanager, e.g. reusing a context multiple times works ``` ctx = rc_context(...) with ctx: ... # in the context ... # out of the context with ctx: ... # in the context again ``` (with a test for the decorator form, which is equivalent) but in practice this will often run into the issue of early/late resolution of rcParams, so I didn't mention it in the changelog. xkcd() still needs to be implemented manually in terms of `__enter__`/`__exit__` but at least we know that creation of the contextmanager cannot fail, so things are simpler. Also we don't need to check for whether the backend has been resolved because rcParams now just prevent re-setting the backend to auto_backend_sentinel.
1 parent 030157c commit 44b8a4b

File tree

5 files changed

+75
-66
lines changed

5 files changed

+75
-66
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
`matplotlib.rc_context` is now a `contextlib.contextmanager`
2+
````````````````````````````````````````````````````````````
3+
4+
`matplotlib.rc_context` can now be used as a decorator (technically, it is now
5+
implemented as a `contextlib.contextmanager`), e.g. ::
6+
7+
@rc_context({"lines.linewidth": 2})
8+
def some_function(...):
9+
...

doc/users/prev_whats_new/whats_new_1.3.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ New plotting features
8686
To give your plots a sense of authority that they may be missing,
8787
Michael Droettboom (inspired by the work of many others in
8888
:ghpull:`1329`) has added an `xkcd-style <http://xkcd.com/>`__ sketch
89-
plotting mode. To use it, simply call :func:`matplotlib.pyplot.xkcd`
89+
plotting mode. To use it, simply call `matplotlib.pyplot.xkcd`
9090
before creating your plot. For really fine control, it is also possible
9191
to modify each artist's sketch parameters individually with
9292
:meth:`matplotlib.artist.Artist.set_sketch_params`.

lib/matplotlib/__init__.py

Lines changed: 14 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,7 +1140,8 @@ def rc_file(fname, *, use_default_template=True):
11401140
if k not in STYLE_BLACKLIST})
11411141

11421142

1143-
class rc_context:
1143+
@contextlib.contextmanager
1144+
def rc_context(rc=None, fname=None):
11441145
"""
11451146
Return a context manager for managing rc settings.
11461147
@@ -1157,49 +1158,24 @@ class rc_context:
11571158
with mpl.rc_context(rc={'text.usetex': True}, fname='screen.rc'):
11581159
plt.plot(x, a)
11591160
1160-
The 'rc' dictionary takes precedence over the settings loaded from
1161-
'fname'. Passing a dictionary only is also valid. For example a
1162-
common usage is::
1161+
The *rc* dictionary takes precedence over the settings loaded from *fname*.
1162+
Passing a dictionary only is also valid. For example, a common usage is::
11631163
1164-
with mpl.rc_context(rc={'interactive': False}):
1164+
with mpl.rc_context({'interactive': False}):
11651165
fig, ax = plt.subplots()
11661166
ax.plot(range(3), range(3))
11671167
fig.savefig('A.png', format='png')
11681168
plt.close(fig)
11691169
"""
1170-
# While it may seem natural to implement rc_context using
1171-
# contextlib.contextmanager, that would entail always calling the finally:
1172-
# clause of the contextmanager (which restores the original rcs) including
1173-
# during garbage collection; as a result, something like `plt.xkcd();
1174-
# gc.collect()` would result in the style being lost (as `xkcd()` is
1175-
# implemented on top of rc_context, and nothing is holding onto context
1176-
# manager except possibly circular references.
1177-
1178-
def __init__(self, rc=None, fname=None):
1179-
self._orig = rcParams.copy()
1180-
try:
1181-
if fname:
1182-
rc_file(fname)
1183-
if rc:
1184-
rcParams.update(rc)
1185-
except Exception:
1186-
self.__fallback()
1187-
raise
1188-
1189-
def __fallback(self):
1190-
# If anything goes wrong, revert to the original rcs.
1191-
updated_backend = self._orig['backend']
1192-
dict.update(rcParams, self._orig)
1193-
# except for the backend. If the context block triggered resolving
1194-
# the auto backend resolution keep that value around
1195-
if self._orig['backend'] is rcsetup._auto_backend_sentinel:
1196-
rcParams['backend'] = updated_backend
1197-
1198-
def __enter__(self):
1199-
return self
1200-
1201-
def __exit__(self, exc_type, exc_value, exc_tb):
1202-
self.__fallback()
1170+
orig = rcParams.copy()
1171+
try:
1172+
if fname:
1173+
rc_file(fname)
1174+
if rc:
1175+
rcParams.update(rc)
1176+
yield
1177+
finally:
1178+
dict.update(rcParams, orig) # Revert to the original rcs.
12031179

12041180

12051181
@cbook._rename_parameter("3.1", "arg", "backend")

lib/matplotlib/pyplot.py

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -365,12 +365,11 @@ def setp(obj, *args, **kwargs):
365365

366366
def xkcd(scale=1, length=100, randomness=2):
367367
"""
368-
Turn on `xkcd <https://xkcd.com/>`_ sketch-style drawing mode.
369-
This will only have effect on things drawn after this function is
370-
called.
368+
Turn on `xkcd <https://xkcd.com/>`_ sketch-style drawing mode. This will
369+
only have effect on things drawn after this function is called.
371370
372371
For best results, the "Humor Sans" font should be installed: it is
373-
not included with matplotlib.
372+
not included with Matplotlib.
374373
375374
Parameters
376375
----------
@@ -397,29 +396,46 @@ def xkcd(scale=1, length=100, randomness=2):
397396
# This figure will be in regular style
398397
fig2 = plt.figure()
399398
"""
400-
if rcParams['text.usetex']:
401-
raise RuntimeError(
402-
"xkcd mode is not compatible with text.usetex = True")
403-
404-
from matplotlib import patheffects
405-
return rc_context({
406-
'font.family': ['xkcd', 'xkcd Script', 'Humor Sans', 'Comic Neue',
407-
'Comic Sans MS'],
408-
'font.size': 14.0,
409-
'path.sketch': (scale, length, randomness),
410-
'path.effects': [patheffects.withStroke(linewidth=4, foreground="w")],
411-
'axes.linewidth': 1.5,
412-
'lines.linewidth': 2.0,
413-
'figure.facecolor': 'white',
414-
'grid.linewidth': 0.0,
415-
'axes.grid': False,
416-
'axes.unicode_minus': False,
417-
'axes.edgecolor': 'black',
418-
'xtick.major.size': 8,
419-
'xtick.major.width': 3,
420-
'ytick.major.size': 8,
421-
'ytick.major.width': 3,
422-
})
399+
return _xkcd(scale, length, randomness)
400+
401+
402+
class _xkcd:
403+
# This cannot be implemented in terms of rc_context() because this needs to
404+
# work as a non-contextmanager too.
405+
406+
def __init__(self, scale, length, randomness):
407+
self._orig = rcParams.copy()
408+
409+
if rcParams['text.usetex']:
410+
raise RuntimeError(
411+
"xkcd mode is not compatible with text.usetex = True")
412+
413+
from matplotlib import patheffects
414+
rcParams.update({
415+
'font.family': ['xkcd', 'xkcd Script', 'Humor Sans', 'Comic Neue',
416+
'Comic Sans MS'],
417+
'font.size': 14.0,
418+
'path.sketch': (scale, length, randomness),
419+
'path.effects': [
420+
patheffects.withStroke(linewidth=4, foreground="w")],
421+
'axes.linewidth': 1.5,
422+
'lines.linewidth': 2.0,
423+
'figure.facecolor': 'white',
424+
'grid.linewidth': 0.0,
425+
'axes.grid': False,
426+
'axes.unicode_minus': False,
427+
'axes.edgecolor': 'black',
428+
'xtick.major.size': 8,
429+
'xtick.major.width': 3,
430+
'ytick.major.size': 8,
431+
'ytick.major.width': 3,
432+
})
433+
434+
def __enter__(self):
435+
return self
436+
437+
def __exit__(self, *args):
438+
dict.update(rcParams, self._orig)
423439

424440

425441
## Figures ##

lib/matplotlib/tests/test_rcparams.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ def test_rcparams(tmpdir):
5454
assert mpl.rcParams['lines.linewidth'] == 44
5555
assert mpl.rcParams['lines.linewidth'] == linewidth
5656

57+
# test context as decorator (and test reusability, by calling func twice)
58+
@mpl.rc_context({'lines.linewidth': 44})
59+
def func():
60+
assert mpl.rcParams['lines.linewidth'] == 44
61+
62+
func()
63+
func()
64+
5765
# test rc_file
5866
mpl.rc_file(rcpath)
5967
assert mpl.rcParams['lines.linewidth'] == 33

0 commit comments

Comments
 (0)