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