Skip to content

Commit e4d63b1

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 db55918 commit e4d63b1

File tree

5 files changed

+77
-66
lines changed

5 files changed

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

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
@@ -1035,7 +1035,8 @@ def rc_file(fname, *, use_default_template=True):
10351035
if k not in STYLE_BLACKLIST})
10361036

10371037

1038-
class rc_context:
1038+
@contextlib.contextmanager
1039+
def rc_context(rc=None, fname=None):
10391040
"""
10401041
Return a context manager for managing rc settings.
10411042
@@ -1052,49 +1053,24 @@ class rc_context:
10521053
with mpl.rc_context(rc={'text.usetex': True}, fname='screen.rc'):
10531054
plt.plot(x, a)
10541055
1055-
The 'rc' dictionary takes precedence over the settings loaded from
1056-
'fname'. Passing a dictionary only is also valid. For example a
1057-
common usage is::
1056+
The *rc* dictionary takes precedence over the settings loaded from *fname*.
1057+
Passing a dictionary only is also valid. For example, a common usage is::
10581058
1059-
with mpl.rc_context(rc={'interactive': False}):
1059+
with mpl.rc_context({'interactive': False}):
10601060
fig, ax = plt.subplots()
10611061
ax.plot(range(3), range(3))
10621062
fig.savefig('A.png', format='png')
10631063
plt.close(fig)
10641064
"""
1065-
# While it may seem natural to implement rc_context using
1066-
# contextlib.contextmanager, that would entail always calling the finally:
1067-
# clause of the contextmanager (which restores the original rcs) including
1068-
# during garbage collection; as a result, something like `plt.xkcd();
1069-
# gc.collect()` would result in the style being lost (as `xkcd()` is
1070-
# implemented on top of rc_context, and nothing is holding onto context
1071-
# manager except possibly circular references.
1072-
1073-
def __init__(self, rc=None, fname=None):
1074-
self._orig = rcParams.copy()
1075-
try:
1076-
if fname:
1077-
rc_file(fname)
1078-
if rc:
1079-
rcParams.update(rc)
1080-
except Exception:
1081-
self.__fallback()
1082-
raise
1083-
1084-
def __fallback(self):
1085-
# If anything goes wrong, revert to the original rcs.
1086-
updated_backend = self._orig['backend']
1087-
dict.update(rcParams, self._orig)
1088-
# except for the backend. If the context block triggered resolving
1089-
# the auto backend resolution keep that value around
1090-
if self._orig['backend'] is rcsetup._auto_backend_sentinel:
1091-
rcParams['backend'] = updated_backend
1092-
1093-
def __enter__(self):
1094-
return self
1095-
1096-
def __exit__(self, exc_type, exc_value, exc_tb):
1097-
self.__fallback()
1065+
orig = rcParams.copy()
1066+
try:
1067+
if fname:
1068+
rc_file(fname)
1069+
if rc:
1070+
rcParams.update(rc)
1071+
yield
1072+
finally:
1073+
dict.update(rcParams, orig) # Revert to the original rcs.
10981074

10991075

11001076
def use(backend, *, force=True):

lib/matplotlib/pyplot.py

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

409409
def xkcd(scale=1, length=100, randomness=2):
410410
"""
411-
Turn on `xkcd <https://xkcd.com/>`_ sketch-style drawing mode.
412-
This will only have effect on things drawn after this function is
413-
called.
411+
Turn on `xkcd <https://xkcd.com/>`_ sketch-style drawing mode. This will
412+
only have effect on things drawn after this function is called.
414413
415414
For best results, the "Humor Sans" font should be installed: it is
416-
not included with matplotlib.
415+
not included with Matplotlib.
417416
418417
Parameters
419418
----------
@@ -440,29 +439,46 @@ def xkcd(scale=1, length=100, randomness=2):
440439
# This figure will be in regular style
441440
fig2 = plt.figure()
442441
"""
443-
if rcParams['text.usetex']:
444-
raise RuntimeError(
445-
"xkcd mode is not compatible with text.usetex = True")
446-
447-
from matplotlib import patheffects
448-
return rc_context({
449-
'font.family': ['xkcd', 'xkcd Script', 'Humor Sans', 'Comic Neue',
450-
'Comic Sans MS'],
451-
'font.size': 14.0,
452-
'path.sketch': (scale, length, randomness),
453-
'path.effects': [patheffects.withStroke(linewidth=4, foreground="w")],
454-
'axes.linewidth': 1.5,
455-
'lines.linewidth': 2.0,
456-
'figure.facecolor': 'white',
457-
'grid.linewidth': 0.0,
458-
'axes.grid': False,
459-
'axes.unicode_minus': False,
460-
'axes.edgecolor': 'black',
461-
'xtick.major.size': 8,
462-
'xtick.major.width': 3,
463-
'ytick.major.size': 8,
464-
'ytick.major.width': 3,
465-
})
442+
return _xkcd(scale, length, randomness)
443+
444+
445+
class _xkcd:
446+
# This cannot be implemented in terms of rc_context() because this needs to
447+
# work as a non-contextmanager too.
448+
449+
def __init__(self, scale, length, randomness):
450+
self._orig = rcParams.copy()
451+
452+
if rcParams['text.usetex']:
453+
raise RuntimeError(
454+
"xkcd mode is not compatible with text.usetex = True")
455+
456+
from matplotlib import patheffects
457+
rcParams.update({
458+
'font.family': ['xkcd', 'xkcd Script', 'Humor Sans', 'Comic Neue',
459+
'Comic Sans MS'],
460+
'font.size': 14.0,
461+
'path.sketch': (scale, length, randomness),
462+
'path.effects': [
463+
patheffects.withStroke(linewidth=4, foreground="w")],
464+
'axes.linewidth': 1.5,
465+
'lines.linewidth': 2.0,
466+
'figure.facecolor': 'white',
467+
'grid.linewidth': 0.0,
468+
'axes.grid': False,
469+
'axes.unicode_minus': False,
470+
'axes.edgecolor': 'black',
471+
'xtick.major.size': 8,
472+
'xtick.major.width': 3,
473+
'ytick.major.size': 8,
474+
'ytick.major.width': 3,
475+
})
476+
477+
def __enter__(self):
478+
return self
479+
480+
def __exit__(self, *args):
481+
dict.update(rcParams, self._orig)
466482

467483

468484
## 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)