123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875 |
- """
- The config module holds package-wide configurables and provides
- a uniform API for working with them.
- Overview
- ========
- This module supports the following requirements:
- - options are referenced using keys in dot.notation, e.g. "x.y.option - z".
- - keys are case-insensitive.
- - functions should accept partial/regex keys, when unambiguous.
- - options can be registered by modules at import time.
- - options can be registered at init-time (via core.config_init)
- - options have a default value, and (optionally) a description and
- validation function associated with them.
- - options can be deprecated, in which case referencing them
- should produce a warning.
- - deprecated options can optionally be rerouted to a replacement
- so that accessing a deprecated option reroutes to a differently
- named option.
- - options can be reset to their default value.
- - all option can be reset to their default value at once.
- - all options in a certain sub - namespace can be reset at once.
- - the user can set / get / reset or ask for the description of an option.
- - a developer can register and mark an option as deprecated.
- - you can register a callback to be invoked when the option value
- is set or reset. Changing the stored value is considered misuse, but
- is not verboten.
- Implementation
- ==============
- - Data is stored using nested dictionaries, and should be accessed
- through the provided API.
- - "Registered options" and "Deprecated options" have metadata associated
- with them, which are stored in auxiliary dictionaries keyed on the
- fully-qualified key, e.g. "x.y.z.option".
- - the config_init module is imported by the package's __init__.py file.
- placing any register_option() calls there will ensure those options
- are available as soon as pandas is loaded. If you use register_option
- in a module, it will only be available after that module is imported,
- which you should be aware of.
- - `config_prefix` is a context_manager (for use with the `with` keyword)
- which can save developers some typing, see the docstring.
- """
- from __future__ import annotations
- from collections import namedtuple
- from contextlib import (
- ContextDecorator,
- contextmanager,
- )
- import re
- from typing import (
- Any,
- Callable,
- Iterable,
- cast,
- )
- import warnings
- from pandas._typing import F
- DeprecatedOption = namedtuple("DeprecatedOption", "key msg rkey removal_ver")
- RegisteredOption = namedtuple("RegisteredOption", "key defval doc validator cb")
- # holds deprecated option metadata
- _deprecated_options: dict[str, DeprecatedOption] = {}
- # holds registered option metadata
- _registered_options: dict[str, RegisteredOption] = {}
- # holds the current values for registered options
- _global_config: dict[str, Any] = {}
- # keys which have a special meaning
- _reserved_keys: list[str] = ["all"]
- class OptionError(AttributeError, KeyError):
- """
- Exception for pandas.options, backwards compatible with KeyError
- checks
- """
- #
- # User API
- def _get_single_key(pat: str, silent: bool) -> str:
- keys = _select_options(pat)
- if len(keys) == 0:
- if not silent:
- _warn_if_deprecated(pat)
- raise OptionError(f"No such keys(s): {repr(pat)}")
- if len(keys) > 1:
- raise OptionError("Pattern matched multiple keys")
- key = keys[0]
- if not silent:
- _warn_if_deprecated(key)
- key = _translate_key(key)
- return key
- def _get_option(pat: str, silent: bool = False):
- key = _get_single_key(pat, silent)
- # walk the nested dict
- root, k = _get_root(key)
- return root[k]
- def _set_option(*args, **kwargs) -> None:
- # must at least 1 arg deal with constraints later
- nargs = len(args)
- if not nargs or nargs % 2 != 0:
- raise ValueError("Must provide an even number of non-keyword arguments")
- # default to false
- silent = kwargs.pop("silent", False)
- if kwargs:
- kwarg = list(kwargs.keys())[0]
- raise TypeError(f'_set_option() got an unexpected keyword argument "{kwarg}"')
- for k, v in zip(args[::2], args[1::2]):
- key = _get_single_key(k, silent)
- o = _get_registered_option(key)
- if o and o.validator:
- o.validator(v)
- # walk the nested dict
- root, k = _get_root(key)
- root[k] = v
- if o.cb:
- if silent:
- with warnings.catch_warnings(record=True):
- o.cb(key)
- else:
- o.cb(key)
- def _describe_option(pat: str = "", _print_desc: bool = True):
- keys = _select_options(pat)
- if len(keys) == 0:
- raise OptionError("No such keys(s)")
- s = "\n".join(_build_option_description(k) for k in keys)
- if _print_desc:
- print(s)
- else:
- return s
- def _reset_option(pat: str, silent: bool = False) -> None:
- keys = _select_options(pat)
- if len(keys) == 0:
- raise OptionError("No such keys(s)")
- if len(keys) > 1 and len(pat) < 4 and pat != "all":
- raise ValueError(
- "You must specify at least 4 characters when "
- "resetting multiple keys, use the special keyword "
- '"all" to reset all the options to their default value'
- )
- for k in keys:
- _set_option(k, _registered_options[k].defval, silent=silent)
- def get_default_val(pat: str):
- key = _get_single_key(pat, silent=True)
- return _get_registered_option(key).defval
- class DictWrapper:
- """provide attribute-style access to a nested dict"""
- def __init__(self, d: dict[str, Any], prefix: str = ""):
- object.__setattr__(self, "d", d)
- object.__setattr__(self, "prefix", prefix)
- def __setattr__(self, key: str, val: Any) -> None:
- prefix = object.__getattribute__(self, "prefix")
- if prefix:
- prefix += "."
- prefix += key
- # you can't set new keys
- # can you can't overwrite subtrees
- if key in self.d and not isinstance(self.d[key], dict):
- _set_option(prefix, val)
- else:
- raise OptionError("You can only set the value of existing options")
- def __getattr__(self, key: str):
- prefix = object.__getattribute__(self, "prefix")
- if prefix:
- prefix += "."
- prefix += key
- try:
- v = object.__getattribute__(self, "d")[key]
- except KeyError as err:
- raise OptionError("No such option") from err
- if isinstance(v, dict):
- return DictWrapper(v, prefix)
- else:
- return _get_option(prefix)
- def __dir__(self) -> Iterable[str]:
- return list(self.d.keys())
- # For user convenience, we'd like to have the available options described
- # in the docstring. For dev convenience we'd like to generate the docstrings
- # dynamically instead of maintaining them by hand. To this, we use the
- # class below which wraps functions inside a callable, and converts
- # __doc__ into a property function. The doctsrings below are templates
- # using the py2.6+ advanced formatting syntax to plug in a concise list
- # of options, and option descriptions.
- class CallableDynamicDoc:
- def __init__(self, func, doc_tmpl):
- self.__doc_tmpl__ = doc_tmpl
- self.__func__ = func
- def __call__(self, *args, **kwds):
- return self.__func__(*args, **kwds)
- @property
- def __doc__(self):
- opts_desc = _describe_option("all", _print_desc=False)
- opts_list = pp_options_list(list(_registered_options.keys()))
- return self.__doc_tmpl__.format(opts_desc=opts_desc, opts_list=opts_list)
- _get_option_tmpl = """
- get_option(pat)
- Retrieves the value of the specified option.
- Available options:
- {opts_list}
- Parameters
- ----------
- pat : str
- Regexp which should match a single option.
- Note: partial matches are supported for convenience, but unless you use the
- full option name (e.g. x.y.z.option_name), your code may break in future
- versions if new options with similar names are introduced.
- Returns
- -------
- result : the value of the option
- Raises
- ------
- OptionError : if no such option exists
- Notes
- -----
- The available options with its descriptions:
- {opts_desc}
- """
- _set_option_tmpl = """
- set_option(pat, value)
- Sets the value of the specified option.
- Available options:
- {opts_list}
- Parameters
- ----------
- pat : str
- Regexp which should match a single option.
- Note: partial matches are supported for convenience, but unless you use the
- full option name (e.g. x.y.z.option_name), your code may break in future
- versions if new options with similar names are introduced.
- value : object
- New value of option.
- Returns
- -------
- None
- Raises
- ------
- OptionError if no such option exists
- Notes
- -----
- The available options with its descriptions:
- {opts_desc}
- """
- _describe_option_tmpl = """
- describe_option(pat, _print_desc=False)
- Prints the description for one or more registered options.
- Call with not arguments to get a listing for all registered options.
- Available options:
- {opts_list}
- Parameters
- ----------
- pat : str
- Regexp pattern. All matching keys will have their description displayed.
- _print_desc : bool, default True
- If True (default) the description(s) will be printed to stdout.
- Otherwise, the description(s) will be returned as a unicode string
- (for testing).
- Returns
- -------
- None by default, the description(s) as a unicode string if _print_desc
- is False
- Notes
- -----
- The available options with its descriptions:
- {opts_desc}
- """
- _reset_option_tmpl = """
- reset_option(pat)
- Reset one or more options to their default value.
- Pass "all" as argument to reset all options.
- Available options:
- {opts_list}
- Parameters
- ----------
- pat : str/regex
- If specified only options matching `prefix*` will be reset.
- Note: partial matches are supported for convenience, but unless you
- use the full option name (e.g. x.y.z.option_name), your code may break
- in future versions if new options with similar names are introduced.
- Returns
- -------
- None
- Notes
- -----
- The available options with its descriptions:
- {opts_desc}
- """
- # bind the functions with their docstrings into a Callable
- # and use that as the functions exposed in pd.api
- get_option = CallableDynamicDoc(_get_option, _get_option_tmpl)
- set_option = CallableDynamicDoc(_set_option, _set_option_tmpl)
- reset_option = CallableDynamicDoc(_reset_option, _reset_option_tmpl)
- describe_option = CallableDynamicDoc(_describe_option, _describe_option_tmpl)
- options = DictWrapper(_global_config)
- #
- # Functions for use by pandas developers, in addition to User - api
- class option_context(ContextDecorator):
- """
- Context manager to temporarily set options in the `with` statement context.
- You need to invoke as ``option_context(pat, val, [(pat, val), ...])``.
- Examples
- --------
- >>> with option_context('display.max_rows', 10, 'display.max_columns', 5):
- ... ...
- """
- def __init__(self, *args):
- if len(args) % 2 != 0 or len(args) < 2:
- raise ValueError(
- "Need to invoke as option_context(pat, val, [(pat, val), ...])."
- )
- self.ops = list(zip(args[::2], args[1::2]))
- def __enter__(self):
- self.undo = [(pat, _get_option(pat, silent=True)) for pat, val in self.ops]
- for pat, val in self.ops:
- _set_option(pat, val, silent=True)
- def __exit__(self, *args):
- if self.undo:
- for pat, val in self.undo:
- _set_option(pat, val, silent=True)
- def register_option(
- key: str,
- defval: object,
- doc: str = "",
- validator: Callable[[Any], Any] | None = None,
- cb: Callable[[str], Any] | None = None,
- ) -> None:
- """
- Register an option in the package-wide pandas config object
- Parameters
- ----------
- key : str
- Fully-qualified key, e.g. "x.y.option - z".
- defval : object
- Default value of the option.
- doc : str
- Description of the option.
- validator : Callable, optional
- Function of a single argument, should raise `ValueError` if
- called with a value which is not a legal value for the option.
- cb
- a function of a single argument "key", which is called
- immediately after an option value is set/reset. key is
- the full name of the option.
- Raises
- ------
- ValueError if `validator` is specified and `defval` is not a valid value.
- """
- import keyword
- import tokenize
- key = key.lower()
- if key in _registered_options:
- raise OptionError(f"Option '{key}' has already been registered")
- if key in _reserved_keys:
- raise OptionError(f"Option '{key}' is a reserved key")
- # the default value should be legal
- if validator:
- validator(defval)
- # walk the nested dict, creating dicts as needed along the path
- path = key.split(".")
- for k in path:
- if not re.match("^" + tokenize.Name + "$", k):
- raise ValueError(f"{k} is not a valid identifier")
- if keyword.iskeyword(k):
- raise ValueError(f"{k} is a python keyword")
- cursor = _global_config
- msg = "Path prefix to option '{option}' is already an option"
- for i, p in enumerate(path[:-1]):
- if not isinstance(cursor, dict):
- raise OptionError(msg.format(option=".".join(path[:i])))
- if p not in cursor:
- cursor[p] = {}
- cursor = cursor[p]
- if not isinstance(cursor, dict):
- raise OptionError(msg.format(option=".".join(path[:-1])))
- cursor[path[-1]] = defval # initialize
- # save the option metadata
- _registered_options[key] = RegisteredOption(
- key=key, defval=defval, doc=doc, validator=validator, cb=cb
- )
- def deprecate_option(
- key: str, msg: str | None = None, rkey: str | None = None, removal_ver=None
- ) -> None:
- """
- Mark option `key` as deprecated, if code attempts to access this option,
- a warning will be produced, using `msg` if given, or a default message
- if not.
- if `rkey` is given, any access to the key will be re-routed to `rkey`.
- Neither the existence of `key` nor that if `rkey` is checked. If they
- do not exist, any subsequence access will fail as usual, after the
- deprecation warning is given.
- Parameters
- ----------
- key : str
- Name of the option to be deprecated.
- must be a fully-qualified option name (e.g "x.y.z.rkey").
- msg : str, optional
- Warning message to output when the key is referenced.
- if no message is given a default message will be emitted.
- rkey : str, optional
- Name of an option to reroute access to.
- If specified, any referenced `key` will be
- re-routed to `rkey` including set/get/reset.
- rkey must be a fully-qualified option name (e.g "x.y.z.rkey").
- used by the default message if no `msg` is specified.
- removal_ver : optional
- Specifies the version in which this option will
- be removed. used by the default message if no `msg` is specified.
- Raises
- ------
- OptionError
- If the specified key has already been deprecated.
- """
- key = key.lower()
- if key in _deprecated_options:
- raise OptionError(f"Option '{key}' has already been defined as deprecated.")
- _deprecated_options[key] = DeprecatedOption(key, msg, rkey, removal_ver)
- #
- # functions internal to the module
- def _select_options(pat: str) -> list[str]:
- """
- returns a list of keys matching `pat`
- if pat=="all", returns all registered options
- """
- # short-circuit for exact key
- if pat in _registered_options:
- return [pat]
- # else look through all of them
- keys = sorted(_registered_options.keys())
- if pat == "all": # reserved key
- return keys
- return [k for k in keys if re.search(pat, k, re.I)]
- def _get_root(key: str) -> tuple[dict[str, Any], str]:
- path = key.split(".")
- cursor = _global_config
- for p in path[:-1]:
- cursor = cursor[p]
- return cursor, path[-1]
- def _is_deprecated(key: str) -> bool:
- """Returns True if the given option has been deprecated"""
- key = key.lower()
- return key in _deprecated_options
- def _get_deprecated_option(key: str):
- """
- Retrieves the metadata for a deprecated option, if `key` is deprecated.
- Returns
- -------
- DeprecatedOption (namedtuple) if key is deprecated, None otherwise
- """
- try:
- d = _deprecated_options[key]
- except KeyError:
- return None
- else:
- return d
- def _get_registered_option(key: str):
- """
- Retrieves the option metadata if `key` is a registered option.
- Returns
- -------
- RegisteredOption (namedtuple) if key is deprecated, None otherwise
- """
- return _registered_options.get(key)
- def _translate_key(key: str) -> str:
- """
- if key id deprecated and a replacement key defined, will return the
- replacement key, otherwise returns `key` as - is
- """
- d = _get_deprecated_option(key)
- if d:
- return d.rkey or key
- else:
- return key
- def _warn_if_deprecated(key: str) -> bool:
- """
- Checks if `key` is a deprecated option and if so, prints a warning.
- Returns
- -------
- bool - True if `key` is deprecated, False otherwise.
- """
- d = _get_deprecated_option(key)
- if d:
- if d.msg:
- print(d.msg)
- warnings.warn(d.msg, FutureWarning)
- else:
- msg = f"'{key}' is deprecated"
- if d.removal_ver:
- msg += f" and will be removed in {d.removal_ver}"
- if d.rkey:
- msg += f", please use '{d.rkey}' instead."
- else:
- msg += ", please refrain from using it."
- warnings.warn(msg, FutureWarning)
- return True
- return False
- def _build_option_description(k: str) -> str:
- """Builds a formatted description of a registered option and prints it"""
- o = _get_registered_option(k)
- d = _get_deprecated_option(k)
- s = f"{k} "
- if o.doc:
- s += "\n".join(o.doc.strip().split("\n"))
- else:
- s += "No description available."
- if o:
- s += f"\n [default: {o.defval}] [currently: {_get_option(k, True)}]"
- if d:
- rkey = d.rkey or ""
- s += "\n (Deprecated"
- s += f", use `{rkey}` instead."
- s += ")"
- return s
- def pp_options_list(keys: Iterable[str], width=80, _print: bool = False):
- """Builds a concise listing of available options, grouped by prefix"""
- from itertools import groupby
- from textwrap import wrap
- def pp(name: str, ks: Iterable[str]) -> list[str]:
- pfx = "- " + name + ".[" if name else ""
- ls = wrap(
- ", ".join(ks),
- width,
- initial_indent=pfx,
- subsequent_indent=" ",
- break_long_words=False,
- )
- if ls and ls[-1] and name:
- ls[-1] = ls[-1] + "]"
- return ls
- ls: list[str] = []
- singles = [x for x in sorted(keys) if x.find(".") < 0]
- if singles:
- ls += pp("", singles)
- keys = [x for x in keys if x.find(".") >= 0]
- for k, g in groupby(sorted(keys), lambda x: x[: x.rfind(".")]):
- ks = [x[len(k) + 1 :] for x in list(g)]
- ls += pp(k, ks)
- s = "\n".join(ls)
- if _print:
- print(s)
- else:
- return s
- #
- # helpers
- @contextmanager
- def config_prefix(prefix):
- """
- contextmanager for multiple invocations of API with a common prefix
- supported API functions: (register / get / set )__option
- Warning: This is not thread - safe, and won't work properly if you import
- the API functions into your module using the "from x import y" construct.
- Example
- -------
- import pandas._config.config as cf
- with cf.config_prefix("display.font"):
- cf.register_option("color", "red")
- cf.register_option("size", " 5 pt")
- cf.set_option(size, " 6 pt")
- cf.get_option(size)
- ...
- etc'
- will register options "display.font.color", "display.font.size", set the
- value of "display.font.size"... and so on.
- """
- # Note: reset_option relies on set_option, and on key directly
- # it does not fit in to this monkey-patching scheme
- global register_option, get_option, set_option, reset_option
- def wrap(func: F) -> F:
- def inner(key: str, *args, **kwds):
- pkey = f"{prefix}.{key}"
- return func(pkey, *args, **kwds)
- return cast(F, inner)
- _register_option = register_option
- _get_option = get_option
- _set_option = set_option
- set_option = wrap(set_option)
- get_option = wrap(get_option)
- register_option = wrap(register_option)
- yield None
- set_option = _set_option
- get_option = _get_option
- register_option = _register_option
- # These factories and methods are handy for use as the validator
- # arg in register_option
- def is_type_factory(_type: type[Any]) -> Callable[[Any], None]:
- """
- Parameters
- ----------
- `_type` - a type to be compared against (e.g. type(x) == `_type`)
- Returns
- -------
- validator - a function of a single argument x , which raises
- ValueError if type(x) is not equal to `_type`
- """
- def inner(x) -> None:
- if type(x) != _type:
- raise ValueError(f"Value must have type '{_type}'")
- return inner
- def is_instance_factory(_type) -> Callable[[Any], None]:
- """
- Parameters
- ----------
- `_type` - the type to be checked against
- Returns
- -------
- validator - a function of a single argument x , which raises
- ValueError if x is not an instance of `_type`
- """
- if isinstance(_type, (tuple, list)):
- _type = tuple(_type)
- type_repr = "|".join(map(str, _type))
- else:
- type_repr = f"'{_type}'"
- def inner(x) -> None:
- if not isinstance(x, _type):
- raise ValueError(f"Value must be an instance of {type_repr}")
- return inner
- def is_one_of_factory(legal_values) -> Callable[[Any], None]:
- callables = [c for c in legal_values if callable(c)]
- legal_values = [c for c in legal_values if not callable(c)]
- def inner(x) -> None:
- if x not in legal_values:
- if not any(c(x) for c in callables):
- uvals = [str(lval) for lval in legal_values]
- pp_values = "|".join(uvals)
- msg = f"Value must be one of {pp_values}"
- if len(callables):
- msg += " or a callable"
- raise ValueError(msg)
- return inner
- def is_nonnegative_int(value: int | None) -> None:
- """
- Verify that value is None or a positive int.
- Parameters
- ----------
- value : None or int
- The `value` to be checked.
- Raises
- ------
- ValueError
- When the value is not None or is a negative integer
- """
- if value is None:
- return
- elif isinstance(value, int):
- if value >= 0:
- return
- msg = "Value must be a nonnegative integer or None"
- raise ValueError(msg)
- # common type validators, for convenience
- # usage: register_option(... , validator = is_int)
- is_int = is_type_factory(int)
- is_bool = is_type_factory(bool)
- is_float = is_type_factory(float)
- is_str = is_type_factory(str)
- is_text = is_instance_factory((str, bytes))
- def is_callable(obj) -> bool:
- """
- Parameters
- ----------
- `obj` - the object to be checked
- Returns
- -------
- validator - returns True if object is callable
- raises ValueError otherwise.
- """
- if not callable(obj):
- raise ValueError("Value must be a callable")
- return True
|