forked from proplot-dev/proplot
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsettings.py
More file actions
346 lines (309 loc) · 12.4 KB
/
Copy pathsettings.py
File metadata and controls
346 lines (309 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
#!/usr/bin/env python3
"""
Utilities for global settings.
"""
from collections.abc import MutableMapping
from numbers import Integral, Real
import numpy as np
from cycler import Cycler
from matplotlib import RcParams
from matplotlib import rcParams as _rc_matplotlib
from matplotlib import rcParamsDefault as _rc_matplotlib_default
from . import ic # noqa: F401
from . import _not_none, validate, warnings
from .defaults import (
_rc_matplotlib_override,
_rc_proplot_definition,
_rc_proplot_removed,
_rc_proplot_renamed,
)
def _get_default_param(key):
"""
Get the default proplot rc parameter.
"""
# NOTE: This is used for the :rc: role when compiling docs and when saving
# proplotrc files. Includes custom proplot params and proplot overrides.
sentinel = object()
for dict_ in (
_rc_proplot_default,
_rc_matplotlib_override, # imposed defaults
_rc_matplotlib_default, # native defaults
):
value = dict_.get(key, sentinel)
if value is not sentinel:
return value
raise KeyError(f'Invalid key {key!r}.')
def _get_param_repr(value):
"""
Translate setting to a string suitable for saving.
"""
# NOTE: Never safe hex strings with leading '#'. In both matplotlibrc
# and proplotrc this will be read as comment character.
if value is None or isinstance(value, (str, bool, Integral)):
value = str(value)
if value[:1] == '#': # i.e. a HEX string
value = value[1:]
elif isinstance(value, Real):
value = str(round(value, 6)) # truncate decimals
elif isinstance(value, Cycler):
value = repr(value) # special case!
elif isinstance(value, (list, tuple, np.ndarray)):
value = ', '.join(map(_get_param_repr, value)) # sexy recursion
else:
value = None
return value
def _get_rst_table():
"""
Return the setting names and descriptions in an RST-style table.
"""
# Get descriptions
descrips = {
key: descrip for key, (_, _, descrip) in _rc_proplot_definition.items()
}
# Get header and divider
keylen = 4 + len(max((*_rc_proplot_definition, 'Key'), key=len))
vallen = len(max((*descrips.values(), 'Description'), key=len))
spaces = 2 * ' ' # spaces between each table column
prefix = '.. rst-class:: proplot-rctable\n\n'
header = 'Key' + spaces + ' ' * (keylen - 3) + 'Description\n'
divider = '=' * keylen + spaces + '=' * vallen + '\n'
# Combine components
string = prefix + divider + header + divider
for key, descrip in descrips.values():
line = '``' + key + '``' + spaces + ' ' * (keylen - len(key) - 4) + descrip
string += line + '\n'
string = string + divider.strip()
return string
def _get_yaml_section(kw, comment=True, description=False):
"""
Return the settings as a nicely tabulated YAML-style table.
"""
prefix = '# ' if comment else ''
data = []
for key, args in kw.items():
# Possibly append description
includes_descrip = isinstance(args, tuple) and len(args) == 3
if not description:
descrip = ''
value = args[0] if includes_descrip else args
elif includes_descrip:
value, validator, descrip = args
descrip = '# ' + descrip # skip the validator
else:
raise ValueError(f'Unexpected input {key}={args!r}.')
# Translate object to string
value = _get_param_repr(value)
if value is not None:
data.append((key, value, descrip))
else:
warnings._warn_proplot(
f'Failed to write rc setting {key} = {value!r}. Must be None, bool, '
'string, int, float, a list or tuple thereof, or a property cycler.'
)
# Generate string
string = ''
keylen = len(max(kw, key=len))
vallen = len(max((tup[1] for tup in data), key=len))
for key, value, descrip in data:
space1 = ' ' * (keylen - len(key) + 1)
space2 = ' ' * (vallen - len(value) + 2) if descrip else ''
string += f'{prefix}{key}:{space1}{value}{space2}{descrip}\n'
return string.strip()
def _get_yaml_table(changed=None, comment=None, description=False):
"""
Return the settings as a nicely tabulated YAML-style table.
"""
parts = [
'#--------------------------------------------------------------------',
'# Use this file to change the default proplot and matplotlib settings.',
'# The syntax is identical to matplotlibrc syntax. For details see:',
'# https://proplot.readthedocs.io/en/latest/configuration.html',
'# https://matplotlib.org/stable/tutorials/introductory/customizing.html',
'#--------------------------------------------------------------------',
]
# User settings
if changed is not None: # add always-uncommented user settings
table = _get_yaml_section(changed, comment=False)
parts.extend(('# Changed settings', table, ''))
# Proplot settings
kw = _rc_proplot_definition if description else _rc_proplot_default
table = _get_yaml_section(kw, description=description, comment=comment)
parts.extend(('# Proplot settings', table, ''))
# Matplotlib settings
kw = _rc_matplotlib_override
table = _get_yaml_section(kw, comment=comment)
parts.extend(('# Matplotlib settings', table, ''))
return '\n'.join(parts)
def _convert_grid_param(b, key):
"""
Translate an instruction to turn either major or minor gridlines on or off into a
boolean and string applied to :rcraw:`axes.grid` and :rcraw:`axes.grid.which`.
"""
ob = _rc_matplotlib['axes.grid']
owhich = _rc_matplotlib['axes.grid.which']
if b:
# Gridlines are already both on, or they are off only for the
# ones that we want to turn on. Turn on gridlines for both.
if (
owhich == 'both'
or key == 'grid' and owhich == 'minor'
or key == 'gridminor' and owhich == 'major'
):
which = 'both'
# Gridlines are off for both, or off for the ones that we
# don't want to turn on. We can just turn on these ones.
else:
which = owhich
else:
# Gridlines are already off, or they are on for the particular
# ones that we want to turn off. Instruct to turn both off.
if (
not ob
or key == 'grid' and owhich == 'major'
or key == 'gridminor' and owhich == 'minor'
):
which = 'both' # disable both sides
# Gridlines are currently on for major and minor ticks, so we
# instruct to turn on gridlines for the one we *don't* want off
elif owhich == 'both': # and ob is True, as already tested
# if gridminor=False, enable major, and vice versa
b = True
which = 'major' if key == 'gridminor' else 'minor'
# Gridlines are on for the ones that we *didn't* instruct to
# turn off, and off for the ones we do want to turn off. This
# just re-asserts the ones that are already on.
else:
b = True
which = owhich
return b, which
def _pop_settings(src, *, ignore_conflicts=True):
"""
Pop the rc setting names and mode for a `~Configurator.context` block.
"""
# NOTE: By default must ignore settings also present as function parameters
# and include deprecated settings in the list.
# NOTE: rc_mode == 2 applies only the updated params. A power user
# could use ax.format(rc_mode=0) to re-apply all the current settings
conflict_params = (
'backend',
'alpha', # deprecated
'color', # deprecated
'facecolor', # deprecated
'edgecolor', # deprecated
'linewidth', # deprecated
'basemap', # deprecated
'share', # deprecated
'span', # deprecated
'tight', # deprecated
'span', # deprecated
)
kw = src.pop('rc_kw', None) or {}
if 'mode' in src:
src['rc_mode'] = src.pop('mode')
warnings._warn_proplot(
"Keyword 'mode' was deprecated in v0.6. Please use 'rc_mode' instead."
)
mode = src.pop('rc_mode', None)
mode = _not_none(mode, 2) # only apply updated params by default
for key, value in tuple(src.items()):
name = _rc_nodots.get(key, None)
if ignore_conflicts and name in conflict_params:
name = None # former renamed settings
if name is not None:
kw[name] = src.pop(key)
return kw, mode
class _RcParams(MutableMapping, dict):
"""
A simple dictionary with locked inputs and validated assignments.
"""
# NOTE: By omitting __delitem__ in MutableMapping we effectively
# disable mutability. Also disables deleting items with pop().
def __init__(self, source, validate):
self._validate = validate
for key, value in source.items():
self.__setitem__(key, value) # trigger validation
def __repr__(self):
return RcParams.__repr__(self)
def __str__(self):
return RcParams.__repr__(self)
def __len__(self):
return dict.__len__(self)
def __iter__(self):
# NOTE: Proplot doesn't add deprecated args to dictionary so
# we don't have to suppress warning messages here.
yield from sorted(dict.__iter__(self))
def __getitem__(self, key):
key, _ = self._check_key(key)
return dict.__getitem__(self, key)
def __setitem__(self, key, value):
key, value = self._check_key(key, value)
if key not in self._validate:
raise KeyError(f'Invalid rc key {key!r}.')
try:
value = self._validate[key](value)
except (ValueError, TypeError) as error:
raise ValueError(f'Key {key!r}: {error}')
if key is not None:
dict.__setitem__(self, key, value)
@staticmethod
def _check_key(key, value=None):
# NOTE: If we assigned from the Configurator then the deprecated key will
# still propagate to the same 'children' as the new key.
# NOTE: This also translates values for special cases of renamed keys.
# Currently the special cases are 'basemap' and 'cartopy.autoextent'.
if key in _rc_proplot_renamed:
key_new, version = _rc_proplot_renamed[key]
warnings._warn_proplot(
f'The rc setting {key!r} was deprecated in version {version} and may be ' # noqa: E501
f'removed in {warnings._next_release()}. Please use {key_new!r} instead.' # noqa: E501
)
if key == 'basemap': # special case
value = ('cartopy', 'basemap')[int(bool(value))]
if key == 'cartopy.autoextent':
value = ('globe', 'auto')[int(bool(value))]
key = key_new
if key in _rc_proplot_removed:
info, version = _rc_proplot_removed[key]
raise KeyError(
f'The rc setting {key!r} was removed in version {version}.'
+ (info and ' ' + info)
)
return key, value
def copy(self):
source = {key: dict.__getitem__(self, key) for key in self}
return _RcParams(source, self._validate)
# Validate the default settings dictionaries using a custom proplot _RcParams and the
# original matplotlib RcParams. Also surreptitiously add proplot font settings to the
# font keys list (beoolean below always evalutes to True) font keys list during init.
_rc_proplot_default = {
key: value
for key, (value, _, _) in _rc_proplot_definition.items()
}
_rc_proplot_validate = {
key: validator
for key, (_, validator, _) in _rc_proplot_definition.items()
if not (validator is validate._validate_fontsize and validate.FONT_KEYS.add(key))
}
_rc_proplot_default = _RcParams(_rc_proplot_default, _rc_proplot_validate)
_rc_matplotlib_override = RcParams(_rc_matplotlib_override)
# Important joint matplotlib proplot constants
# NOTE: The 'nodots' dictionary should include removed and renamed settings
_rc_categories = {
'.'.join(name.split('.')[:i + 1])
for dict_ in (
_rc_proplot_default,
_rc_matplotlib_default
)
for name in dict_
for i in range(len(name.split('.')) - 1)
}
_rc_nodots = {
name.replace('.', ''): name
for dict_ in (
_rc_proplot_definition,
_rc_proplot_renamed,
_rc_proplot_removed,
_rc_matplotlib_default,
)
for name in dict_.keys()
}