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