config.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948
  1. """
  2. The config module holds package-wide configurables and provides
  3. a uniform API for working with them.
  4. Overview
  5. ========
  6. This module supports the following requirements:
  7. - options are referenced using keys in dot.notation, e.g. "x.y.option - z".
  8. - keys are case-insensitive.
  9. - functions should accept partial/regex keys, when unambiguous.
  10. - options can be registered by modules at import time.
  11. - options can be registered at init-time (via core.config_init)
  12. - options have a default value, and (optionally) a description and
  13. validation function associated with them.
  14. - options can be deprecated, in which case referencing them
  15. should produce a warning.
  16. - deprecated options can optionally be rerouted to a replacement
  17. so that accessing a deprecated option reroutes to a differently
  18. named option.
  19. - options can be reset to their default value.
  20. - all option can be reset to their default value at once.
  21. - all options in a certain sub - namespace can be reset at once.
  22. - the user can set / get / reset or ask for the description of an option.
  23. - a developer can register and mark an option as deprecated.
  24. - you can register a callback to be invoked when the option value
  25. is set or reset. Changing the stored value is considered misuse, but
  26. is not verboten.
  27. Implementation
  28. ==============
  29. - Data is stored using nested dictionaries, and should be accessed
  30. through the provided API.
  31. - "Registered options" and "Deprecated options" have metadata associated
  32. with them, which are stored in auxiliary dictionaries keyed on the
  33. fully-qualified key, e.g. "x.y.z.option".
  34. - the config_init module is imported by the package's __init__.py file.
  35. placing any register_option() calls there will ensure those options
  36. are available as soon as pandas is loaded. If you use register_option
  37. in a module, it will only be available after that module is imported,
  38. which you should be aware of.
  39. - `config_prefix` is a context_manager (for use with the `with` keyword)
  40. which can save developers some typing, see the docstring.
  41. """
  42. from __future__ import annotations
  43. from contextlib import (
  44. ContextDecorator,
  45. contextmanager,
  46. )
  47. import re
  48. from typing import (
  49. TYPE_CHECKING,
  50. Any,
  51. Callable,
  52. Generic,
  53. NamedTuple,
  54. cast,
  55. )
  56. import warnings
  57. from pandas._typing import (
  58. F,
  59. T,
  60. )
  61. from pandas.util._exceptions import find_stack_level
  62. if TYPE_CHECKING:
  63. from collections.abc import (
  64. Generator,
  65. Iterable,
  66. )
  67. class DeprecatedOption(NamedTuple):
  68. key: str
  69. msg: str | None
  70. rkey: str | None
  71. removal_ver: str | None
  72. class RegisteredOption(NamedTuple):
  73. key: str
  74. defval: object
  75. doc: str
  76. validator: Callable[[object], Any] | None
  77. cb: Callable[[str], Any] | None
  78. # holds deprecated option metadata
  79. _deprecated_options: dict[str, DeprecatedOption] = {}
  80. # holds registered option metadata
  81. _registered_options: dict[str, RegisteredOption] = {}
  82. # holds the current values for registered options
  83. _global_config: dict[str, Any] = {}
  84. # keys which have a special meaning
  85. _reserved_keys: list[str] = ["all"]
  86. class OptionError(AttributeError, KeyError):
  87. """
  88. Exception raised for pandas.options.
  89. Backwards compatible with KeyError checks.
  90. Examples
  91. --------
  92. >>> pd.options.context
  93. Traceback (most recent call last):
  94. OptionError: No such option
  95. """
  96. #
  97. # User API
  98. def _get_single_key(pat: str, silent: bool) -> str:
  99. keys = _select_options(pat)
  100. if len(keys) == 0:
  101. if not silent:
  102. _warn_if_deprecated(pat)
  103. raise OptionError(f"No such keys(s): {repr(pat)}")
  104. if len(keys) > 1:
  105. raise OptionError("Pattern matched multiple keys")
  106. key = keys[0]
  107. if not silent:
  108. _warn_if_deprecated(key)
  109. key = _translate_key(key)
  110. return key
  111. def _get_option(pat: str, silent: bool = False) -> Any:
  112. key = _get_single_key(pat, silent)
  113. # walk the nested dict
  114. root, k = _get_root(key)
  115. return root[k]
  116. def _set_option(*args, **kwargs) -> None:
  117. # must at least 1 arg deal with constraints later
  118. nargs = len(args)
  119. if not nargs or nargs % 2 != 0:
  120. raise ValueError("Must provide an even number of non-keyword arguments")
  121. # default to false
  122. silent = kwargs.pop("silent", False)
  123. if kwargs:
  124. kwarg = next(iter(kwargs.keys()))
  125. raise TypeError(f'_set_option() got an unexpected keyword argument "{kwarg}"')
  126. for k, v in zip(args[::2], args[1::2]):
  127. key = _get_single_key(k, silent)
  128. o = _get_registered_option(key)
  129. if o and o.validator:
  130. o.validator(v)
  131. # walk the nested dict
  132. root, k_root = _get_root(key)
  133. root[k_root] = v
  134. if o.cb:
  135. if silent:
  136. with warnings.catch_warnings(record=True):
  137. o.cb(key)
  138. else:
  139. o.cb(key)
  140. def _describe_option(pat: str = "", _print_desc: bool = True) -> str | None:
  141. keys = _select_options(pat)
  142. if len(keys) == 0:
  143. raise OptionError("No such keys(s)")
  144. s = "\n".join([_build_option_description(k) for k in keys])
  145. if _print_desc:
  146. print(s)
  147. return None
  148. return s
  149. def _reset_option(pat: str, silent: bool = False) -> None:
  150. keys = _select_options(pat)
  151. if len(keys) == 0:
  152. raise OptionError("No such keys(s)")
  153. if len(keys) > 1 and len(pat) < 4 and pat != "all":
  154. raise ValueError(
  155. "You must specify at least 4 characters when "
  156. "resetting multiple keys, use the special keyword "
  157. '"all" to reset all the options to their default value'
  158. )
  159. for k in keys:
  160. _set_option(k, _registered_options[k].defval, silent=silent)
  161. def get_default_val(pat: str):
  162. key = _get_single_key(pat, silent=True)
  163. return _get_registered_option(key).defval
  164. class DictWrapper:
  165. """provide attribute-style access to a nested dict"""
  166. d: dict[str, Any]
  167. def __init__(self, d: dict[str, Any], prefix: str = "") -> None:
  168. object.__setattr__(self, "d", d)
  169. object.__setattr__(self, "prefix", prefix)
  170. def __setattr__(self, key: str, val: Any) -> None:
  171. prefix = object.__getattribute__(self, "prefix")
  172. if prefix:
  173. prefix += "."
  174. prefix += key
  175. # you can't set new keys
  176. # can you can't overwrite subtrees
  177. if key in self.d and not isinstance(self.d[key], dict):
  178. _set_option(prefix, val)
  179. else:
  180. raise OptionError("You can only set the value of existing options")
  181. def __getattr__(self, key: str):
  182. prefix = object.__getattribute__(self, "prefix")
  183. if prefix:
  184. prefix += "."
  185. prefix += key
  186. try:
  187. v = object.__getattribute__(self, "d")[key]
  188. except KeyError as err:
  189. raise OptionError("No such option") from err
  190. if isinstance(v, dict):
  191. return DictWrapper(v, prefix)
  192. else:
  193. return _get_option(prefix)
  194. def __dir__(self) -> list[str]:
  195. return list(self.d.keys())
  196. # For user convenience, we'd like to have the available options described
  197. # in the docstring. For dev convenience we'd like to generate the docstrings
  198. # dynamically instead of maintaining them by hand. To this, we use the
  199. # class below which wraps functions inside a callable, and converts
  200. # __doc__ into a property function. The doctsrings below are templates
  201. # using the py2.6+ advanced formatting syntax to plug in a concise list
  202. # of options, and option descriptions.
  203. class CallableDynamicDoc(Generic[T]):
  204. def __init__(self, func: Callable[..., T], doc_tmpl: str) -> None:
  205. self.__doc_tmpl__ = doc_tmpl
  206. self.__func__ = func
  207. def __call__(self, *args, **kwds) -> T:
  208. return self.__func__(*args, **kwds)
  209. # error: Signature of "__doc__" incompatible with supertype "object"
  210. @property
  211. def __doc__(self) -> str: # type: ignore[override]
  212. opts_desc = _describe_option("all", _print_desc=False)
  213. opts_list = pp_options_list(list(_registered_options.keys()))
  214. return self.__doc_tmpl__.format(opts_desc=opts_desc, opts_list=opts_list)
  215. _get_option_tmpl = """
  216. get_option(pat)
  217. Retrieves the value of the specified option.
  218. Available options:
  219. {opts_list}
  220. Parameters
  221. ----------
  222. pat : str
  223. Regexp which should match a single option.
  224. Note: partial matches are supported for convenience, but unless you use the
  225. full option name (e.g. x.y.z.option_name), your code may break in future
  226. versions if new options with similar names are introduced.
  227. Returns
  228. -------
  229. result : the value of the option
  230. Raises
  231. ------
  232. OptionError : if no such option exists
  233. Notes
  234. -----
  235. Please reference the :ref:`User Guide <options>` for more information.
  236. The available options with its descriptions:
  237. {opts_desc}
  238. Examples
  239. --------
  240. >>> pd.get_option('display.max_columns') # doctest: +SKIP
  241. 4
  242. """
  243. _set_option_tmpl = """
  244. set_option(pat, value)
  245. Sets the value of the specified option.
  246. Available options:
  247. {opts_list}
  248. Parameters
  249. ----------
  250. pat : str
  251. Regexp which should match a single option.
  252. Note: partial matches are supported for convenience, but unless you use the
  253. full option name (e.g. x.y.z.option_name), your code may break in future
  254. versions if new options with similar names are introduced.
  255. value : object
  256. New value of option.
  257. Returns
  258. -------
  259. None
  260. Raises
  261. ------
  262. OptionError if no such option exists
  263. Notes
  264. -----
  265. Please reference the :ref:`User Guide <options>` for more information.
  266. The available options with its descriptions:
  267. {opts_desc}
  268. Examples
  269. --------
  270. >>> pd.set_option('display.max_columns', 4)
  271. >>> df = pd.DataFrame([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]])
  272. >>> df
  273. 0 1 ... 3 4
  274. 0 1 2 ... 4 5
  275. 1 6 7 ... 9 10
  276. [2 rows x 5 columns]
  277. >>> pd.reset_option('display.max_columns')
  278. """
  279. _describe_option_tmpl = """
  280. describe_option(pat, _print_desc=False)
  281. Prints the description for one or more registered options.
  282. Call with no arguments to get a listing for all registered options.
  283. Available options:
  284. {opts_list}
  285. Parameters
  286. ----------
  287. pat : str
  288. Regexp pattern. All matching keys will have their description displayed.
  289. _print_desc : bool, default True
  290. If True (default) the description(s) will be printed to stdout.
  291. Otherwise, the description(s) will be returned as a unicode string
  292. (for testing).
  293. Returns
  294. -------
  295. None by default, the description(s) as a unicode string if _print_desc
  296. is False
  297. Notes
  298. -----
  299. Please reference the :ref:`User Guide <options>` for more information.
  300. The available options with its descriptions:
  301. {opts_desc}
  302. Examples
  303. --------
  304. >>> pd.describe_option('display.max_columns') # doctest: +SKIP
  305. display.max_columns : int
  306. If max_cols is exceeded, switch to truncate view...
  307. """
  308. _reset_option_tmpl = """
  309. reset_option(pat)
  310. Reset one or more options to their default value.
  311. Pass "all" as argument to reset all options.
  312. Available options:
  313. {opts_list}
  314. Parameters
  315. ----------
  316. pat : str/regex
  317. If specified only options matching `prefix*` will be reset.
  318. Note: partial matches are supported for convenience, but unless you
  319. use the full option name (e.g. x.y.z.option_name), your code may break
  320. in future versions if new options with similar names are introduced.
  321. Returns
  322. -------
  323. None
  324. Notes
  325. -----
  326. Please reference the :ref:`User Guide <options>` for more information.
  327. The available options with its descriptions:
  328. {opts_desc}
  329. Examples
  330. --------
  331. >>> pd.reset_option('display.max_columns') # doctest: +SKIP
  332. """
  333. # bind the functions with their docstrings into a Callable
  334. # and use that as the functions exposed in pd.api
  335. get_option = CallableDynamicDoc(_get_option, _get_option_tmpl)
  336. set_option = CallableDynamicDoc(_set_option, _set_option_tmpl)
  337. reset_option = CallableDynamicDoc(_reset_option, _reset_option_tmpl)
  338. describe_option = CallableDynamicDoc(_describe_option, _describe_option_tmpl)
  339. options = DictWrapper(_global_config)
  340. #
  341. # Functions for use by pandas developers, in addition to User - api
  342. class option_context(ContextDecorator):
  343. """
  344. Context manager to temporarily set options in the `with` statement context.
  345. You need to invoke as ``option_context(pat, val, [(pat, val), ...])``.
  346. Examples
  347. --------
  348. >>> from pandas import option_context
  349. >>> with option_context('display.max_rows', 10, 'display.max_columns', 5):
  350. ... pass
  351. """
  352. def __init__(self, *args) -> None:
  353. if len(args) % 2 != 0 or len(args) < 2:
  354. raise ValueError(
  355. "Need to invoke as option_context(pat, val, [(pat, val), ...])."
  356. )
  357. self.ops = list(zip(args[::2], args[1::2]))
  358. def __enter__(self) -> None:
  359. self.undo = [(pat, _get_option(pat)) for pat, val in self.ops]
  360. for pat, val in self.ops:
  361. _set_option(pat, val, silent=True)
  362. def __exit__(self, *args) -> None:
  363. if self.undo:
  364. for pat, val in self.undo:
  365. _set_option(pat, val, silent=True)
  366. def register_option(
  367. key: str,
  368. defval: object,
  369. doc: str = "",
  370. validator: Callable[[object], Any] | None = None,
  371. cb: Callable[[str], Any] | None = None,
  372. ) -> None:
  373. """
  374. Register an option in the package-wide pandas config object
  375. Parameters
  376. ----------
  377. key : str
  378. Fully-qualified key, e.g. "x.y.option - z".
  379. defval : object
  380. Default value of the option.
  381. doc : str
  382. Description of the option.
  383. validator : Callable, optional
  384. Function of a single argument, should raise `ValueError` if
  385. called with a value which is not a legal value for the option.
  386. cb
  387. a function of a single argument "key", which is called
  388. immediately after an option value is set/reset. key is
  389. the full name of the option.
  390. Raises
  391. ------
  392. ValueError if `validator` is specified and `defval` is not a valid value.
  393. """
  394. import keyword
  395. import tokenize
  396. key = key.lower()
  397. if key in _registered_options:
  398. raise OptionError(f"Option '{key}' has already been registered")
  399. if key in _reserved_keys:
  400. raise OptionError(f"Option '{key}' is a reserved key")
  401. # the default value should be legal
  402. if validator:
  403. validator(defval)
  404. # walk the nested dict, creating dicts as needed along the path
  405. path = key.split(".")
  406. for k in path:
  407. if not re.match("^" + tokenize.Name + "$", k):
  408. raise ValueError(f"{k} is not a valid identifier")
  409. if keyword.iskeyword(k):
  410. raise ValueError(f"{k} is a python keyword")
  411. cursor = _global_config
  412. msg = "Path prefix to option '{option}' is already an option"
  413. for i, p in enumerate(path[:-1]):
  414. if not isinstance(cursor, dict):
  415. raise OptionError(msg.format(option=".".join(path[:i])))
  416. if p not in cursor:
  417. cursor[p] = {}
  418. cursor = cursor[p]
  419. if not isinstance(cursor, dict):
  420. raise OptionError(msg.format(option=".".join(path[:-1])))
  421. cursor[path[-1]] = defval # initialize
  422. # save the option metadata
  423. _registered_options[key] = RegisteredOption(
  424. key=key, defval=defval, doc=doc, validator=validator, cb=cb
  425. )
  426. def deprecate_option(
  427. key: str,
  428. msg: str | None = None,
  429. rkey: str | None = None,
  430. removal_ver: str | None = None,
  431. ) -> None:
  432. """
  433. Mark option `key` as deprecated, if code attempts to access this option,
  434. a warning will be produced, using `msg` if given, or a default message
  435. if not.
  436. if `rkey` is given, any access to the key will be re-routed to `rkey`.
  437. Neither the existence of `key` nor that if `rkey` is checked. If they
  438. do not exist, any subsequence access will fail as usual, after the
  439. deprecation warning is given.
  440. Parameters
  441. ----------
  442. key : str
  443. Name of the option to be deprecated.
  444. must be a fully-qualified option name (e.g "x.y.z.rkey").
  445. msg : str, optional
  446. Warning message to output when the key is referenced.
  447. if no message is given a default message will be emitted.
  448. rkey : str, optional
  449. Name of an option to reroute access to.
  450. If specified, any referenced `key` will be
  451. re-routed to `rkey` including set/get/reset.
  452. rkey must be a fully-qualified option name (e.g "x.y.z.rkey").
  453. used by the default message if no `msg` is specified.
  454. removal_ver : str, optional
  455. Specifies the version in which this option will
  456. be removed. used by the default message if no `msg` is specified.
  457. Raises
  458. ------
  459. OptionError
  460. If the specified key has already been deprecated.
  461. """
  462. key = key.lower()
  463. if key in _deprecated_options:
  464. raise OptionError(f"Option '{key}' has already been defined as deprecated.")
  465. _deprecated_options[key] = DeprecatedOption(key, msg, rkey, removal_ver)
  466. #
  467. # functions internal to the module
  468. def _select_options(pat: str) -> list[str]:
  469. """
  470. returns a list of keys matching `pat`
  471. if pat=="all", returns all registered options
  472. """
  473. # short-circuit for exact key
  474. if pat in _registered_options:
  475. return [pat]
  476. # else look through all of them
  477. keys = sorted(_registered_options.keys())
  478. if pat == "all": # reserved key
  479. return keys
  480. return [k for k in keys if re.search(pat, k, re.I)]
  481. def _get_root(key: str) -> tuple[dict[str, Any], str]:
  482. path = key.split(".")
  483. cursor = _global_config
  484. for p in path[:-1]:
  485. cursor = cursor[p]
  486. return cursor, path[-1]
  487. def _is_deprecated(key: str) -> bool:
  488. """Returns True if the given option has been deprecated"""
  489. key = key.lower()
  490. return key in _deprecated_options
  491. def _get_deprecated_option(key: str):
  492. """
  493. Retrieves the metadata for a deprecated option, if `key` is deprecated.
  494. Returns
  495. -------
  496. DeprecatedOption (namedtuple) if key is deprecated, None otherwise
  497. """
  498. try:
  499. d = _deprecated_options[key]
  500. except KeyError:
  501. return None
  502. else:
  503. return d
  504. def _get_registered_option(key: str):
  505. """
  506. Retrieves the option metadata if `key` is a registered option.
  507. Returns
  508. -------
  509. RegisteredOption (namedtuple) if key is deprecated, None otherwise
  510. """
  511. return _registered_options.get(key)
  512. def _translate_key(key: str) -> str:
  513. """
  514. if key id deprecated and a replacement key defined, will return the
  515. replacement key, otherwise returns `key` as - is
  516. """
  517. d = _get_deprecated_option(key)
  518. if d:
  519. return d.rkey or key
  520. else:
  521. return key
  522. def _warn_if_deprecated(key: str) -> bool:
  523. """
  524. Checks if `key` is a deprecated option and if so, prints a warning.
  525. Returns
  526. -------
  527. bool - True if `key` is deprecated, False otherwise.
  528. """
  529. d = _get_deprecated_option(key)
  530. if d:
  531. if d.msg:
  532. warnings.warn(
  533. d.msg,
  534. FutureWarning,
  535. stacklevel=find_stack_level(),
  536. )
  537. else:
  538. msg = f"'{key}' is deprecated"
  539. if d.removal_ver:
  540. msg += f" and will be removed in {d.removal_ver}"
  541. if d.rkey:
  542. msg += f", please use '{d.rkey}' instead."
  543. else:
  544. msg += ", please refrain from using it."
  545. warnings.warn(msg, FutureWarning, stacklevel=find_stack_level())
  546. return True
  547. return False
  548. def _build_option_description(k: str) -> str:
  549. """Builds a formatted description of a registered option and prints it"""
  550. o = _get_registered_option(k)
  551. d = _get_deprecated_option(k)
  552. s = f"{k} "
  553. if o.doc:
  554. s += "\n".join(o.doc.strip().split("\n"))
  555. else:
  556. s += "No description available."
  557. if o:
  558. s += f"\n [default: {o.defval}] [currently: {_get_option(k, True)}]"
  559. if d:
  560. rkey = d.rkey or ""
  561. s += "\n (Deprecated"
  562. s += f", use `{rkey}` instead."
  563. s += ")"
  564. return s
  565. def pp_options_list(keys: Iterable[str], width: int = 80, _print: bool = False):
  566. """Builds a concise listing of available options, grouped by prefix"""
  567. from itertools import groupby
  568. from textwrap import wrap
  569. def pp(name: str, ks: Iterable[str]) -> list[str]:
  570. pfx = "- " + name + ".[" if name else ""
  571. ls = wrap(
  572. ", ".join(ks),
  573. width,
  574. initial_indent=pfx,
  575. subsequent_indent=" ",
  576. break_long_words=False,
  577. )
  578. if ls and ls[-1] and name:
  579. ls[-1] = ls[-1] + "]"
  580. return ls
  581. ls: list[str] = []
  582. singles = [x for x in sorted(keys) if x.find(".") < 0]
  583. if singles:
  584. ls += pp("", singles)
  585. keys = [x for x in keys if x.find(".") >= 0]
  586. for k, g in groupby(sorted(keys), lambda x: x[: x.rfind(".")]):
  587. ks = [x[len(k) + 1 :] for x in list(g)]
  588. ls += pp(k, ks)
  589. s = "\n".join(ls)
  590. if _print:
  591. print(s)
  592. else:
  593. return s
  594. #
  595. # helpers
  596. @contextmanager
  597. def config_prefix(prefix: str) -> Generator[None, None, None]:
  598. """
  599. contextmanager for multiple invocations of API with a common prefix
  600. supported API functions: (register / get / set )__option
  601. Warning: This is not thread - safe, and won't work properly if you import
  602. the API functions into your module using the "from x import y" construct.
  603. Example
  604. -------
  605. import pandas._config.config as cf
  606. with cf.config_prefix("display.font"):
  607. cf.register_option("color", "red")
  608. cf.register_option("size", " 5 pt")
  609. cf.set_option(size, " 6 pt")
  610. cf.get_option(size)
  611. ...
  612. etc'
  613. will register options "display.font.color", "display.font.size", set the
  614. value of "display.font.size"... and so on.
  615. """
  616. # Note: reset_option relies on set_option, and on key directly
  617. # it does not fit in to this monkey-patching scheme
  618. global register_option, get_option, set_option
  619. def wrap(func: F) -> F:
  620. def inner(key: str, *args, **kwds):
  621. pkey = f"{prefix}.{key}"
  622. return func(pkey, *args, **kwds)
  623. return cast(F, inner)
  624. _register_option = register_option
  625. _get_option = get_option
  626. _set_option = set_option
  627. set_option = wrap(set_option)
  628. get_option = wrap(get_option)
  629. register_option = wrap(register_option)
  630. try:
  631. yield
  632. finally:
  633. set_option = _set_option
  634. get_option = _get_option
  635. register_option = _register_option
  636. # These factories and methods are handy for use as the validator
  637. # arg in register_option
  638. def is_type_factory(_type: type[Any]) -> Callable[[Any], None]:
  639. """
  640. Parameters
  641. ----------
  642. `_type` - a type to be compared against (e.g. type(x) == `_type`)
  643. Returns
  644. -------
  645. validator - a function of a single argument x , which raises
  646. ValueError if type(x) is not equal to `_type`
  647. """
  648. def inner(x) -> None:
  649. if type(x) != _type:
  650. raise ValueError(f"Value must have type '{_type}'")
  651. return inner
  652. def is_instance_factory(_type) -> Callable[[Any], None]:
  653. """
  654. Parameters
  655. ----------
  656. `_type` - the type to be checked against
  657. Returns
  658. -------
  659. validator - a function of a single argument x , which raises
  660. ValueError if x is not an instance of `_type`
  661. """
  662. if isinstance(_type, (tuple, list)):
  663. _type = tuple(_type)
  664. type_repr = "|".join(map(str, _type))
  665. else:
  666. type_repr = f"'{_type}'"
  667. def inner(x) -> None:
  668. if not isinstance(x, _type):
  669. raise ValueError(f"Value must be an instance of {type_repr}")
  670. return inner
  671. def is_one_of_factory(legal_values) -> Callable[[Any], None]:
  672. callables = [c for c in legal_values if callable(c)]
  673. legal_values = [c for c in legal_values if not callable(c)]
  674. def inner(x) -> None:
  675. if x not in legal_values:
  676. if not any(c(x) for c in callables):
  677. uvals = [str(lval) for lval in legal_values]
  678. pp_values = "|".join(uvals)
  679. msg = f"Value must be one of {pp_values}"
  680. if len(callables):
  681. msg += " or a callable"
  682. raise ValueError(msg)
  683. return inner
  684. def is_nonnegative_int(value: object) -> None:
  685. """
  686. Verify that value is None or a positive int.
  687. Parameters
  688. ----------
  689. value : None or int
  690. The `value` to be checked.
  691. Raises
  692. ------
  693. ValueError
  694. When the value is not None or is a negative integer
  695. """
  696. if value is None:
  697. return
  698. elif isinstance(value, int):
  699. if value >= 0:
  700. return
  701. msg = "Value must be a nonnegative integer or None"
  702. raise ValueError(msg)
  703. # common type validators, for convenience
  704. # usage: register_option(... , validator = is_int)
  705. is_int = is_type_factory(int)
  706. is_bool = is_type_factory(bool)
  707. is_float = is_type_factory(float)
  708. is_str = is_type_factory(str)
  709. is_text = is_instance_factory((str, bytes))
  710. def is_callable(obj) -> bool:
  711. """
  712. Parameters
  713. ----------
  714. `obj` - the object to be checked
  715. Returns
  716. -------
  717. validator - returns True if object is callable
  718. raises ValueError otherwise.
  719. """
  720. if not callable(obj):
  721. raise ValueError("Value must be a callable")
  722. return True