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