From 0b094b20f204fe6bdf45e9914d71b69b14fc337b Mon Sep 17 00:00:00 2001 From: y-p Date: Sun, 21 Oct 2012 04:33:31 +0200 Subject: [PATCH 1/5] ENH: Make `pprint_nest_depth` settable via print_setoptions, update docstring. --- pandas/core/format.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pandas/core/format.py b/pandas/core/format.py index 4230f3c19aba6..4bf3af377447e 100644 --- a/pandas/core/format.py +++ b/pandas/core/format.py @@ -941,7 +941,7 @@ def set_printoptions(precision=None, column_space=None, max_rows=None, max_columns=None, colheader_justify=None, max_colwidth=None, notebook_repr_html=None, date_dayfirst=None, date_yearfirst=None, - multi_sparse=None, encoding=None): + pprint_nest_depth=None,multi_sparse=None, encoding=None): """ Alter default behavior of DataFrame.toString @@ -965,6 +965,10 @@ def set_printoptions(precision=None, column_space=None, max_rows=None, When True, prints and parses dates with the day first, eg 20/01/2005 date_yearfirst : boolean When True, prints and parses dates with the year first, eg 2005/01/20 + pprint_nest_depth : int + Defaults to 3. + Controls the number of nested levels to process when pretty-printing + nested sequences. multi_sparse : boolean Default True, "sparsify" MultiIndex display (don't display repeated elements in outer levels within groups) @@ -987,6 +991,8 @@ def set_printoptions(precision=None, column_space=None, max_rows=None, print_config.date_dayfirst = date_dayfirst if date_yearfirst is not None: print_config.date_yearfirst = date_yearfirst + if pprint_nest_depth is not None: + print_config.pprint_nest_depth = pprint_nest_depth if multi_sparse is not None: print_config.multi_sparse = multi_sparse if encoding is not None: From cd8c44656c2f499548565d06b22b05a2e06d9d8f Mon Sep 17 00:00:00 2001 From: y-p Date: Mon, 26 Nov 2012 00:38:51 +0200 Subject: [PATCH 2/5] ENH: Add core.config module for managing package-wide configurables --- pandas/core/config.py | 497 +++++++++++++++++++++++++++++++++++++ pandas/core/config_init.py | 103 ++++++++ 2 files changed, 600 insertions(+) create mode 100644 pandas/core/config.py create mode 100644 pandas/core/config_init.py diff --git a/pandas/core/config.py b/pandas/core/config.py new file mode 100644 index 0000000000000..09c1a5f37383d --- /dev/null +++ b/pandas/core/config.py @@ -0,0 +1,497 @@ +""" +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". +- 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. + +Implementation +============== + +- Data is stored using nested dictionaries, and should be accessed + through the provided API. + +- "Registered options" and "Deprecated options" have metadata associcated + with them, which are stored in auxilary 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. + +""" + +import re + +from collections import namedtuple +import warnings + +DeprecatedOption = namedtuple("DeprecatedOption", "key msg rkey removal_ver") +RegisteredOption = namedtuple("RegisteredOption", "key defval doc validator") + +__deprecated_options = {} # holds deprecated option metdata +__registered_options = {} # holds registered option metdata +__global_config = {} # holds the current values for registered options + +########################################## +# User API + + +def get_option(key): + """Retrieves the value of the specified option + + Parameters + ---------- + key - str, a fully - qualified option name , e.g. "x.y.z.option" + + Returns + ------- + result - the value of the option + + Raises + ------ + KeyError if no such option exists + """ + + _warn_if_deprecated(key) + key = _translate_key(key) + + # walk the nested dict + root, k = _get_root(key) + + return root[k] + + +def set_option(key, value): + """Sets the value of the specified option + + Parameters + ---------- + key - str, a fully - qualified option name , e.g. "x.y.z.option" + + Returns + ------- + None + + Raises + ------ + KeyError if no such option exists + """ + _warn_if_deprecated(key) + key = _translate_key(key) + + o = _get_registered_option(key) + if o and o.validator: + o.validator(value) + + # walk the nested dict + root, k = _get_root(key) + + root[k] = value + + +def _get_option_desription(key): + """Prints the description associated with the specified option + + Parameters + ---------- + key - str, a fully - qualified option name , e.g. "x.y.z.option" + + Returns + ------- + None + + Raises + ------ + KeyError if no such option exists + """ + _warn_if_deprecated(key) + key = _translate_key(key) + +def describe_options(pat="",_print_desc=True): + """ Prints the description for one or more registered options + + Parameters + ---------- + pat - str, a regexp pattern. All matching keys will have their + description displayed. + + _print_desc - 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 + + """ + s=u"" + if pat in __registered_options.keys(): # exact key name? + s = _build_option_description(pat) + else: + for k in sorted(__registered_options.keys()): # filter by pat + if re.search(pat,k): + s += _build_option_description(k) + + if s == u"": + raise KeyError("No such keys(s)") + + if _print_desc: + print(s) + else: + return(s) + +def reset_option(key): + """ Reset a single option to it's default value """ + set_option(key, __registered_options[key].defval) + + +def reset_options(prefix=""): + """ Resets all registered options to their default value + + Parameters + ---------- + prefix - str, if specified only options matching `prefix`* will be reset + + Returns + ------- + None + + """ + + for k in __registered_options.keys(): + if k[:len(prefix)] == prefix: + reset_option(k) + + +###################################################### +# Functions for use by pandas developers, in addition to User - api + + +def register_option(key, defval, doc="", validator=None): + """Register an option in the package-wide pandas config object + + Parameters + ---------- + key - a fully-qualified key, e.g. "x.y.option - z". + defval - the default value of the option + doc - a string description of the option + validator - a function of a single argument, should raise `ValueError` if + called with a value which is not a legal value for the option. + + Returns + ------- + Nothing. + + Raises + ------ + ValueError if `validator` is specified and `defval` is not a valid value. + + """ + + + if key in __registered_options: + raise KeyError("Option '%s' has already been registered" % 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(".") + cursor = __global_config + for i,p in enumerate(path[:-1]): + if not isinstance(cursor,dict): + raise KeyError("Path prefix to option '%s' is already an option" %\ + ".".join(path[:i])) + if not cursor.has_key(p): + cursor[p] = {} + cursor = cursor[p] + + if not isinstance(cursor,dict): + raise KeyError("Path prefix to option '%s' is already an 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) + + +def deprecate_option(key, msg=None, rkey=None, removal_ver=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 - the name of the option to be deprecated. must be a fully-qualified + option name (e.g "x.y.z.rkey"). + + msg - (Optional) a warning message to output when the key is referenced. + if no message is given a default message will be emitted. + + rkey - (Optional) the 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. + + Returns + ------- + Nothing + + Raises + ------ + KeyError - if key has already been deprecated. + + """ + if key in __deprecated_options: + raise KeyError("Option '%s' has already been defined as deprecated." % key) + + __deprecated_options[key] = DeprecatedOption(key, msg, rkey,removal_ver) + +################################ +# functions internal to the module + + +def _get_root(key): + path = key.split(".") + cursor = __global_config + for p in path[:-1]: + cursor = cursor[p] + return cursor, path[-1] + + +def _is_deprecated(key): + """ Returns True if the given option has been deprecated """ + return __deprecated_options.has_key(key) + + +def _get_deprecated_option(key): + """ + 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): + """ + Retrieves the option metadata if `key` is a registered option. + + Returns + ------- + RegisteredOption (namedtuple) if key is deprecated, None otherwise + """ + try: + d = __registered_options[key] + except KeyError: + return None + else: + return d + + +def _translate_key(key): + """ + 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): + """ + 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: + warnings.warn(d.msg, DeprecationWarning) + else: + msg = "'%s' is deprecated" % key + if d.removal_ver: + msg += " and will be removed in %s" % d.removal_ver + if d.rkey: + msg += (", please use '%s' instead." % (d.rkey)) + else: + msg += (", please refrain from using it.") + + warnings.warn(msg, DeprecationWarning) + return True + return False + +def _build_option_description(k): + """ Builds a formatted description of a registered option and prints it """ + + o = _get_registered_option(k) + d = _get_deprecated_option(k) + s = u'%s: ' %k + if o.doc: + s += "\n" +"\n ".join(o.doc.split("\n")) + else: + s += "No description available.\n" + + if d: + s += u"\n\t(Deprecated" + s += u", use `%s` instead." % d.rkey if d.rkey else "" + s += u")\n" + + s += "\n" + return(s) + + +############## +# helpers + +from contextlib import contextmanager + + +@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.core.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): + def inner(key, *args, **kwds): + pkey="%s.%s" % (prefix, key) + return func(pkey, *args, **kwds) + return 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 + 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): + """ + + Parameters + ---------- + `_type` - a type to be compared against (e.g. type(x) == `_type`) + + Returns + ------- + validator - a function of a single argument x , which returns the + True if type(x) is equal to `_type` + + """ + def inner(x): + if type(x) != _type: + raise ValueError("Value must have type '%s'" % str(_type)) + + return inner + + +def is_instance_factory(_type): + """ + + Parameters + ---------- + `_type` - the type to be checked against + + Returns + ------- + validator - a function of a single argument x , which returns the + True if x is an instance of `_type` + + """ + def inner(x): + if not isinstance(x, _type): + raise ValueError("Value must be an instance of '%s'" % str(_type)) + + return inner + +# 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_unicode = is_type_factory(unicode) +is_text = is_instance_factory(basestring) diff --git a/pandas/core/config_init.py b/pandas/core/config_init.py new file mode 100644 index 0000000000000..b0279a94983d9 --- /dev/null +++ b/pandas/core/config_init.py @@ -0,0 +1,103 @@ +from __future__ import with_statement # support python 2.5 + +import pandas.core.config as cf +from pandas.core.config import is_int,is_bool,is_text,is_float +from pandas.core.format import detect_console_encoding + +""" +This module is imported from the pandas package __init__.py file +in order to ensure that the core.config options registered here will +be available as soon as the user loads the package. if register_option +is invoked inside specific modules, they will not be registered until that +module is imported, which may or may not be a problem. + +If you need to make sure options are available even before a certain +module is imported, register them here rather then in the module. + +""" + + +########################################### +# options from the "print_config" namespace + +pc_precision_doc=""" +: int + Floating point output precision (number of significant digits). This is + only a suggestion +""" + +pc_colspace_doc=""" +: int + Default space for DataFrame columns, defaults to 12 +""" + +pc_max_rows_doc=""" +: int +""" + +pc_max_cols_doc=""" +: int + max_rows and max_columns are used in __repr__() methods to decide if + to_string() or info() is used to render an object to a string. + Either one, or both can be set to 0 (experimental). Pandas will figure + out how big the terminal is and will not display more rows or/and + columns that can fit on it. +""" + +pc_nb_repr_h_doc=""" +: boolean + When True (default), IPython notebook will use html representation for + pandas objects (if it is available). +""" + +pc_date_dayfirst_doc=""" +: boolean + When True, prints and parses dates with the day first, eg 20/01/2005 +""" + +pc_date_yearfirst_doc=""" +: boolean + When True, prints and parses dates with the year first, eg 2005/01/20 +""" + +pc_pprint_nest_depth=""" +: int + Defaults to 3. + Controls the number of nested levels to process when pretty-printing +""" + +pc_multi_sparse_doc=""" +: boolean + Default True, "sparsify" MultiIndex display (don't display repeated + elements in outer levels within groups) +""" + +pc_encoding_doc=""" +: str/unicode + Defaults to the detected encoding of the console. + Specifies the encoding to be used for strings returned by to_string, + these are generally strings meant to be displayed on the console. +""" + +with cf.config_prefix('print_config'): + cf.register_option('precision', 7, pc_precision_doc, validator=is_int) + cf.register_option('digits', 7, validator=is_int) + cf.register_option('float_format', None) + cf.register_option('column_space', 12, validator=is_int) + cf.register_option('max_rows', 200, pc_max_rows_doc, validator=is_int) + cf.register_option('max_colwidth', 50, validator=is_int) + cf.register_option('max_columns', 0, pc_max_cols_doc, validator=is_int) + cf.register_option('colheader_justify', 'right', + validator=is_text) + cf.register_option('notebook_repr_html', True, pc_nb_repr_h_doc, + validator=is_bool) + cf.register_option('date_dayfirst', False, pc_date_dayfirst_doc, + validator=is_bool) + cf.register_option('date_yearfirst', False, pc_date_yearfirst_doc, + validator=is_bool) + cf.register_option('pprint_nest_depth', 3, pc_pprint_nest_depth, + validator=is_int) + cf.register_option('multi_sparse', True, pc_multi_sparse_doc, + validator=is_bool) + cf.register_option('encoding', detect_console_encoding(), pc_encoding_doc, + validator=is_text) From 0167e246b74789cc0181b603520ec7f58ef7b5fe Mon Sep 17 00:00:00 2001 From: y-p Date: Thu, 15 Nov 2012 17:14:58 +0200 Subject: [PATCH 3/5] ENH: Add new core.config API functions to the pandas top level module --- pandas/core/api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pandas/core/api.py b/pandas/core/api.py index 8cf3b7f4cbda4..469f3683113ec 100644 --- a/pandas/core/api.py +++ b/pandas/core/api.py @@ -29,3 +29,6 @@ # legacy from pandas.core.daterange import DateRange # deprecated import pandas.core.datetools as datetools + +from pandas.core.config import get_option,set_option,reset_option,\ + reset_options,describe_options From 5719a5aa102077c8b49498d3d7f3e6fc516fa590 Mon Sep 17 00:00:00 2001 From: y-p Date: Sat, 3 Nov 2012 16:32:42 +0200 Subject: [PATCH 4/5] ENH: Migrate print_config usage to use core.config, register options on pkg load --- pandas/__init__.py | 3 + pandas/core/common.py | 15 +++-- pandas/core/format.py | 130 ++++++++++++++---------------------- pandas/core/frame.py | 11 +-- pandas/core/index.py | 4 +- pandas/core/series.py | 5 +- pandas/tests/test_format.py | 9 +-- pandas/tseries/tools.py | 6 +- 8 files changed, 82 insertions(+), 101 deletions(-) diff --git a/pandas/__init__.py b/pandas/__init__.py index 3760e3fbc434b..df37b44cc6a7d 100644 --- a/pandas/__init__.py +++ b/pandas/__init__.py @@ -22,6 +22,9 @@ from pandas.version import version as __version__ from pandas.info import __doc__ +# let init-time option registration happen +import pandas.core.config_init + from pandas.core.api import * from pandas.sparse.api import * from pandas.stats.api import * diff --git a/pandas/core/common.py b/pandas/core/common.py index 02223b05fc2f9..c86ee34f26d47 100644 --- a/pandas/core/common.py +++ b/pandas/core/common.py @@ -19,6 +19,8 @@ from pandas.util.py3compat import StringIO, BytesIO +from pandas.core.config import get_option + # XXX: HACK for NumPy 1.5.1 to suppress warnings try: np.seterr(all='ignore') @@ -1113,7 +1115,7 @@ def in_interactive_session(): # 2) If you need to send something to the console, use console_encode(). # # console_encode() should (hopefully) choose the right encoding for you -# based on the encoding set in fmt.print_config.encoding. +# based on the encoding set in option "print_config.encoding" # # 3) if you need to write something out to file, use # pprint_thing_encoded(encoding). @@ -1165,16 +1167,17 @@ def pprint_thing(thing, _nest_lvl=0): result - unicode object on py2, str on py3. Always Unicode. """ - from pandas.core.format import print_config + if thing is None: result = '' elif (py3compat.PY3 and hasattr(thing,'__next__')) or \ hasattr(thing,'next'): return unicode(thing) elif (isinstance(thing, dict) and - _nest_lvl < print_config.pprint_nest_depth): + _nest_lvl < get_option("print_config.pprint_nest_depth")): result = _pprint_dict(thing, _nest_lvl) - elif _is_sequence(thing) and _nest_lvl < print_config.pprint_nest_depth: + elif _is_sequence(thing) and _nest_lvl < \ + get_option("print_config.pprint_nest_depth"): result = _pprint_seq(thing, _nest_lvl) else: # when used internally in the package, everything @@ -1202,7 +1205,6 @@ def pprint_thing_encoded(object, encoding='utf-8', errors='replace'): def console_encode(object): - from pandas.core.format import print_config """ this is the sanctioned way to prepare something for sending *to the console*, it delegates to pprint_thing() to get @@ -1210,4 +1212,5 @@ def console_encode(object): set in print_config.encoding. Use this everywhere where you output to the console. """ - return pprint_thing_encoded(object, print_config.encoding) + return pprint_thing_encoded(object, + get_option("print_config.encoding")) diff --git a/pandas/core/format.py b/pandas/core/format.py index 4bf3af377447e..0a91be9908172 100644 --- a/pandas/core/format.py +++ b/pandas/core/format.py @@ -11,7 +11,8 @@ from pandas.core.common import adjoin, isnull, notnull from pandas.core.index import MultiIndex, _ensure_index from pandas.util import py3compat - +from pandas.core.config import get_option, set_option, \ + reset_options import pandas.core.common as com import pandas.lib as lib @@ -69,7 +70,7 @@ def __init__(self, series, buf=None, header=True, length=True, self.header = header if float_format is None: - float_format = print_config.float_format + float_format = get_option("print_config.float_format") self.float_format = float_format def _get_footer(self): @@ -145,11 +146,11 @@ def to_string(self): _strlen = len else: def _encode_diff(x): - return len(x) - len(x.decode(print_config.encoding)) + return len(x) - len(x.decode(get_option("print_config.encoding"))) def _strlen(x): try: - return len(x.decode(print_config.encoding)) + return len(x.decode(get_option("print_config.encoding"))) except UnicodeError: return len(x) @@ -176,7 +177,7 @@ def __init__(self, frame, buf=None, columns=None, col_space=None, self.show_index_names = index_names if sparsify is None: - sparsify = print_config.multi_sparse + sparsify = get_option("print_config.multi_sparse") self.sparsify = sparsify @@ -188,7 +189,7 @@ def __init__(self, frame, buf=None, columns=None, col_space=None, self.index = index if justify is None: - self.justify = print_config.colheader_justify + self.justify = get_option("print_config.colheader_justify") else: self.justify = justify @@ -697,13 +698,13 @@ def format_array(values, formatter, float_format=None, na_rep='NaN', fmt_klass = GenericArrayFormatter if space is None: - space = print_config.column_space + space = get_option("print_config.column_space") if float_format is None: - float_format = print_config.float_format + float_format = get_option("print_config.float_format") if digits is None: - digits = print_config.precision + digits = get_option("print_config.precision") fmt_obj = fmt_klass(values, digits, na_rep=na_rep, float_format=float_format, @@ -739,9 +740,9 @@ def _have_unicode(self): def _format_strings(self, use_unicode=False): if self.float_format is None: - float_format = print_config.float_format + float_format = get_option("print_config.float_format") if float_format is None: - fmt_str = '%% .%dg' % print_config.precision + fmt_str = '%% .%dg' % get_option("print_config.precision") float_format = lambda x: fmt_str % x else: float_format = self.float_format @@ -863,7 +864,7 @@ def _make_fixed_width(strings, justify='right', minimum=None): if minimum is not None: max_len = max(minimum, max_len) - conf_max = print_config.max_colwidth + conf_max = get_option("print_config.max_colwidth") if conf_max is not None and max_len > conf_max: max_len = conf_max @@ -974,34 +975,56 @@ def set_printoptions(precision=None, column_space=None, max_rows=None, elements in outer levels within groups) """ if precision is not None: - print_config.precision = precision + set_option("print_config.precision", precision) if column_space is not None: - print_config.column_space = column_space + set_option("print_config.column_space", column_space) if max_rows is not None: - print_config.max_rows = max_rows + set_option("print_config.max_rows", max_rows) if max_colwidth is not None: - print_config.max_colwidth = max_colwidth + set_option("print_config.max_colwidth", max_colwidth) if max_columns is not None: - print_config.max_columns = max_columns + set_option("print_config.max_columns", max_columns) if colheader_justify is not None: - print_config.colheader_justify = colheader_justify + set_option("print_config.colheader_justify", colheader_justify) if notebook_repr_html is not None: - print_config.notebook_repr_html = notebook_repr_html + set_option("print_config.notebook_repr_html", notebook_repr_html) if date_dayfirst is not None: - print_config.date_dayfirst = date_dayfirst + set_option("print_config.date_dayfirst", date_dayfirst) if date_yearfirst is not None: - print_config.date_yearfirst = date_yearfirst + set_option("print_config.date_yearfirst", date_yearfirst) if pprint_nest_depth is not None: - print_config.pprint_nest_depth = pprint_nest_depth + set_option("print_config.pprint_nest_depth", pprint_nest_depth) if multi_sparse is not None: - print_config.multi_sparse = multi_sparse + set_option("print_config.multi_sparse", multi_sparse) if encoding is not None: - print_config.encoding = encoding - + set_option("print_config.encoding", encoding) def reset_printoptions(): - print_config.reset() + reset_options("print_config.") + +def detect_console_encoding(): + """ + Try to find the most capable encoding supported by the console. + slighly modified from the way IPython handles the same issue. + """ + import locale + + encoding = None + try: + encoding=sys.stdin.encoding + except AttributeError: + pass + if not encoding or encoding =='ascii': # try again for something better + try: + encoding = locale.getpreferredencoding() + except Exception: + pass + + if not encoding: # when all else fails. this will usually be "ascii" + encoding = sys.getdefaultencoding() + + return encoding class EngFormatter(object): """ @@ -1109,59 +1132,8 @@ def set_eng_float_format(precision=None, accuracy=3, use_eng_prefix=False): "being renamed to 'accuracy'", FutureWarning) accuracy = precision - print_config.float_format = EngFormatter(accuracy, use_eng_prefix) - print_config.column_space = max(12, accuracy + 9) - - -class _GlobalPrintConfig(object): - """ - Holds the console formatting settings for DataFrame and friends - """ - - def __init__(self): - self.precision = self.digits = 7 - self.float_format = None - self.column_space = 12 - self.max_rows = 200 - self.max_colwidth = 50 - self.max_columns = 0 - self.colheader_justify = 'right' - self.notebook_repr_html = True - self.date_dayfirst = False - self.date_yearfirst = False - self.pprint_nest_depth = 3 - self.multi_sparse = True - self.encoding = self.detect_encoding() - - def detect_encoding(self): - """ - Try to find the most capable encoding supported by the console. - slighly modified from the way IPython handles the same issue. - """ - import locale - - encoding = None - try: - encoding = sys.stdin.encoding - except AttributeError: - pass - - if not encoding or encoding == 'ascii': # try again for better - try: - encoding = locale.getpreferredencoding() - except Exception: - pass - - if not encoding: # when all else fails. this will usually be "ascii" - encoding = sys.getdefaultencoding() - - return encoding - - def reset(self): - self.__init__() - -print_config = _GlobalPrintConfig() - + set_option("print_config.float_format", EngFormatter(accuracy, use_eng_prefix)) + set_option("print_config.column_space", max(12, accuracy + 9)) def _put_lines(buf, lines): if any(isinstance(x, unicode) for x in lines): diff --git a/pandas/core/frame.py b/pandas/core/frame.py index aeed377e35fa2..df764bb36a3c0 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -44,6 +44,8 @@ import pandas.core.nanops as nanops import pandas.lib as lib +from pandas.core.config import get_option + #---------------------------------------------------------------------- # Docstring templates @@ -579,12 +581,11 @@ def _need_info_repr_(self): Check if it is needed to use info/summary view to represent a particular DataFrame. """ - config = fmt.print_config terminal_width, terminal_height = get_terminal_size() - max_rows = (terminal_height if config.max_rows == 0 - else config.max_rows) - max_columns = config.max_columns + max_rows = (terminal_height if get_option("print_config.max_rows") == 0 + else get_option("print_config.max_rows")) + max_columns = get_option("print_config.max_columns") if max_columns > 0: if len(self.index) <= max_rows and \ @@ -628,7 +629,7 @@ def _repr_html_(self): Return a html representation for a particular DataFrame. Mainly for IPython notebook. """ - if fmt.print_config.notebook_repr_html: + if get_option("print_config.notebook_repr_html"): if self._need_info_repr_(): return None else: diff --git a/pandas/core/index.py b/pandas/core/index.py index 035d2531f382f..83f4d26fd7fb2 100644 --- a/pandas/core/index.py +++ b/pandas/core/index.py @@ -13,6 +13,7 @@ import pandas._algos as _algos from pandas.lib import Timestamp from pandas.util import py3compat +from pandas.core.config import get_option __all__ = ['Index'] @@ -1514,8 +1515,7 @@ def format(self, space=2, sparsify=None, adjoin=True, names=False, result_levels.append(level) if sparsify is None: - import pandas.core.format as fmt - sparsify = fmt.print_config.multi_sparse + sparsify = get_option("print_config.multi_sparse") if sparsify: # little bit of a kludge job for #1217 diff --git a/pandas/core/series.py b/pandas/core/series.py index 8101dace1a15f..1a3baa223f0a4 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -33,6 +33,7 @@ from pandas.util.decorators import Appender, Substitution, cache_readonly from pandas.compat.scipy import scoreatpercentile as _quantile +from pandas.core.config import get_option __all__ = ['Series', 'TimeSeries'] @@ -914,8 +915,8 @@ def reset_index(self, level=None, drop=False, name=None, inplace=False): def __repr__(self): """Clean string representation of a Series""" width, height = get_terminal_size() - max_rows = (height if fmt.print_config.max_rows == 0 - else fmt.print_config.max_rows) + max_rows = (height if get_option("print_config.max_rows") == 0 + else get_option("print_config.max_rows")) if len(self.index) > (max_rows or 1000): result = self._tidy_repr(min(30, max_rows - 4)) elif len(self.index) > 0: diff --git a/pandas/tests/test_format.py b/pandas/tests/test_format.py index 542e5ee964362..7238ae134252b 100644 --- a/pandas/tests/test_format.py +++ b/pandas/tests/test_format.py @@ -19,6 +19,7 @@ import pandas.util.testing as tm import pandas import pandas as pd +from pandas.core.config import set_option,get_option _frame = DataFrame(tm.getSeriesData()) @@ -64,7 +65,7 @@ def test_repr_tuples(self): def test_repr_truncation(self): max_len = 20 - fmt.print_config.max_colwidth = max_len + set_option("print_config.max_colwidth", max_len) df = DataFrame({'A': np.random.randn(10), 'B': [tm.rands(np.random.randint(max_len - 1, max_len + 1)) for i in range(10)]}) @@ -76,10 +77,10 @@ def test_repr_truncation(self): else: self.assert_('...' not in line) - fmt.print_config.max_colwidth = None + set_option("print_config.max_colwidth", 999999) self.assert_('...' not in repr(df)) - fmt.print_config.max_colwidth = max_len + 2 + set_option("print_config.max_colwidth", max_len + 2) self.assert_('...' not in repr(df)) def test_repr_should_return_str (self): @@ -425,7 +426,7 @@ def test_to_string_float_formatting(self): assert(df_s == expected) fmt.reset_printoptions() - self.assertEqual(fmt.print_config.precision, 7) + self.assertEqual(get_option("print_config.precision"), 7) df = DataFrame({'x': [1e9, 0.2512]}) df_s = df.to_string() diff --git a/pandas/tseries/tools.py b/pandas/tseries/tools.py index 9e1c451c42887..dbb75f1e749c0 100644 --- a/pandas/tseries/tools.py +++ b/pandas/tseries/tools.py @@ -152,7 +152,7 @@ def parse_time_string(arg, freq=None, dayfirst=None, yearfirst=None): ------- datetime, datetime/dateutil.parser._result, str """ - from pandas.core.format import print_config + from pandas.core.config import get_option from pandas.tseries.offsets import DateOffset from pandas.tseries.frequencies import (_get_rule_month, _month_numbers, _get_freq_str) @@ -221,9 +221,9 @@ def parse_time_string(arg, freq=None, dayfirst=None, yearfirst=None): return mresult if dayfirst is None: - dayfirst = print_config.date_dayfirst + dayfirst = get_option("print_config.date_dayfirst") if yearfirst is None: - yearfirst = print_config.date_yearfirst + yearfirst = get_option("print_config.date_yearfirst") try: parsed = parse(arg, dayfirst=dayfirst, yearfirst=yearfirst) From 5e98eaef97886475d3902a591384360622996c17 Mon Sep 17 00:00:00 2001 From: y-p Date: Thu, 15 Nov 2012 17:33:09 +0200 Subject: [PATCH 5/5] TST: Add tests/test_config.py for new core.config module --- pandas/tests/test_config.py | 267 ++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 pandas/tests/test_config.py diff --git a/pandas/tests/test_config.py b/pandas/tests/test_config.py new file mode 100644 index 0000000000000..862b0d29ffcdf --- /dev/null +++ b/pandas/tests/test_config.py @@ -0,0 +1,267 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from __future__ import with_statement # support python 2.5 +import pandas as pd +import unittest +import warnings +import nose + +class TestConfig(unittest.TestCase): + + def __init__(self,*args): + super(TestConfig,self).__init__(*args) + + from copy import deepcopy + self.cf = pd.core.config + self.gc=deepcopy(getattr(self.cf, '__global_config')) + self.do=deepcopy(getattr(self.cf, '__deprecated_options')) + self.ro=deepcopy(getattr(self.cf, '__registered_options')) + + def setUp(self): + setattr(self.cf, '__global_config', {}) + setattr(self.cf, '__deprecated_options', {}) + setattr(self.cf, '__registered_options', {}) + + def tearDown(self): + setattr(self.cf, '__global_config',self.gc) + setattr(self.cf, '__deprecated_options', self.do) + setattr(self.cf, '__registered_options', self.ro) + + def test_api(self): + + #the pandas object exposes the user API + self.assertTrue(hasattr(pd, 'get_option')) + self.assertTrue(hasattr(pd, 'set_option')) + self.assertTrue(hasattr(pd, 'reset_option')) + self.assertTrue(hasattr(pd, 'reset_options')) + self.assertTrue(hasattr(pd, 'describe_options')) + + def test_register_option(self): + self.cf.register_option('a', 1, 'doc') + + # can't register an already registered option + self.assertRaises(KeyError, self.cf.register_option, 'a', 1, 'doc') + + # can't register an already registered option + self.assertRaises(KeyError, self.cf.register_option, 'a.b.c.d1', 1, + 'doc') + self.assertRaises(KeyError, self.cf.register_option, 'a.b.c.d2', 1, + 'doc') + + # we can register options several levels deep + # without predefining the intermediate steps + # and we can define differently named options + # in the same namespace + self.cf.register_option('k.b.c.d1', 1, 'doc') + self.cf.register_option('k.b.c.d2', 1, 'doc') + + def test_describe_options(self): + self.cf.register_option('a', 1, 'doc') + self.cf.register_option('b', 1, 'doc2') + self.cf.deprecate_option('b') + + self.cf.register_option('c.d.e1', 1, 'doc3') + self.cf.register_option('c.d.e2', 1, 'doc4') + self.cf.register_option('f', 1) + self.cf.register_option('g.h', 1) + self.cf.deprecate_option('g.h',rkey="blah") + + # non-existent keys raise KeyError + self.assertRaises(KeyError, self.cf.describe_options, 'no.such.key') + + # we can get the description for any key we registered + self.assertTrue('doc' in self.cf.describe_options('a',_print_desc=False)) + self.assertTrue('doc2' in self.cf.describe_options('b',_print_desc=False)) + self.assertTrue('precated' in self.cf.describe_options('b',_print_desc=False)) + + self.assertTrue('doc3' in self.cf.describe_options('c.d.e1',_print_desc=False)) + self.assertTrue('doc4' in self.cf.describe_options('c.d.e2',_print_desc=False)) + + # if no doc is specified we get a default message + # saying "description not available" + self.assertTrue('vailable' in self.cf.describe_options('f',_print_desc=False)) + self.assertTrue('vailable' in self.cf.describe_options('g.h',_print_desc=False)) + self.assertTrue('precated' in self.cf.describe_options('g.h',_print_desc=False)) + self.assertTrue('blah' in self.cf.describe_options('g.h',_print_desc=False)) + + def test_get_option(self): + self.cf.register_option('a', 1, 'doc') + self.cf.register_option('b.a', 'hullo', 'doc2') + self.cf.register_option('b.b', None, 'doc2') + + # gets of existing keys succeed + self.assertEqual(self.cf.get_option('a'), 1) + self.assertEqual(self.cf.get_option('b.a'), 'hullo') + self.assertTrue(self.cf.get_option('b.b') is None) + + # gets of non-existent keys fail + self.assertRaises(KeyError, self.cf.get_option, 'no_such_option') + + def test_set_option(self): + self.cf.register_option('a', 1, 'doc') + self.cf.register_option('b.a', 'hullo', 'doc2') + self.cf.register_option('b.b', None, 'doc2') + + self.assertEqual(self.cf.get_option('a'), 1) + self.assertEqual(self.cf.get_option('b.a'), 'hullo') + self.assertTrue(self.cf.get_option('b.b') is None) + + self.cf.set_option('a', 2) + self.cf.set_option('b.a', 'wurld') + self.cf.set_option('b.b', 1.1) + + self.assertEqual(self.cf.get_option('a'), 2) + self.assertEqual(self.cf.get_option('b.a'), 'wurld') + self.assertEqual(self.cf.get_option('b.b'), 1.1) + + self.assertRaises(KeyError, self.cf.set_option, 'no.such.key', None) + + def test_validation(self): + self.cf.register_option('a', 1, 'doc', validator=self.cf.is_int) + self.cf.register_option('b.a', 'hullo', 'doc2', + validator=self.cf.is_text) + self.assertRaises(ValueError, self.cf.register_option, 'a.b.c.d2', + 'NO', 'doc', validator=self.cf.is_int) + + self.cf.set_option('a', 2) # int is_int + self.cf.set_option('b.a', 'wurld') # str is_str + + self.assertRaises(ValueError, self.cf.set_option, 'a', None) # None not is_int + self.assertRaises(ValueError, self.cf.set_option, 'a', 'ab') + self.assertRaises(ValueError, self.cf.set_option, 'b.a', 1) + + def test_reset_option(self): + self.cf.register_option('a', 1, 'doc', validator=self.cf.is_int) + self.cf.register_option('b.a', 'hullo', 'doc2', + validator=self.cf.is_str) + self.assertEqual(self.cf.get_option('a'), 1) + self.assertEqual(self.cf.get_option('b.a'), 'hullo') + + self.cf.set_option('a', 2) + self.cf.set_option('b.a', 'wurld') + self.assertEqual(self.cf.get_option('a'), 2) + self.assertEqual(self.cf.get_option('b.a'), 'wurld') + + self.cf.reset_option('a') + self.assertEqual(self.cf.get_option('a'), 1) + self.assertEqual(self.cf.get_option('b.a'), 'wurld') + self.cf.reset_option('b.a') + self.assertEqual(self.cf.get_option('a'), 1) + self.assertEqual(self.cf.get_option('b.a'), 'hullo') + + def test_reset_options(self): + self.cf.register_option('a', 1, 'doc', validator=self.cf.is_int) + self.cf.register_option('b.a', 'hullo', 'doc2', + validator=self.cf.is_str) + self.assertEqual(self.cf.get_option('a'), 1) + self.assertEqual(self.cf.get_option('b.a'), 'hullo') + + self.cf.set_option('a', 2) + self.cf.set_option('b.a', 'wurld') + self.assertEqual(self.cf.get_option('a'), 2) + self.assertEqual(self.cf.get_option('b.a'), 'wurld') + + self.cf.reset_options() + self.assertEqual(self.cf.get_option('a'), 1) + self.assertEqual(self.cf.get_option('b.a'), 'hullo') + + + def test_deprecate_option(self): + import sys + self.cf.deprecate_option('c') # we can deprecate non-existent options + + # testing warning with catch_warning was only added in 2.6 + if sys.version_info[:2]<(2,6): + raise nose.SkipTest() + + self.assertTrue(self.cf._is_deprecated('c')) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + try: + self.cf.get_option('c') + except KeyError: + pass + else: + self.fail("Nonexistent option didn't raise KeyError") + + self.assertEqual(len(w), 1) # should have raised one warning + self.assertTrue('deprecated' in str(w[-1])) # we get the default message + + self.cf.register_option('a', 1, 'doc', validator=self.cf.is_int) + self.cf.register_option('b.a', 'hullo', 'doc2') + self.cf.register_option('c', 'hullo', 'doc2') + + self.cf.deprecate_option('a', removal_ver='nifty_ver') + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + self.cf.get_option('a') + + self.assertEqual(len(w), 1) # should have raised one warning + self.assertTrue('eprecated' in str(w[-1])) # we get the default message + self.assertTrue('nifty_ver' in str(w[-1])) # with the removal_ver quoted + + self.assertRaises(KeyError, self.cf.deprecate_option, 'a') # can't depr. twice + + self.cf.deprecate_option('b.a', 'zounds!') + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + self.cf.get_option('b.a') + + self.assertEqual(len(w), 1) # should have raised one warning + self.assertTrue('zounds!' in str(w[-1])) # we get the custom message + + # test rerouting keys + self.cf.register_option('d.a', 'foo', 'doc2') + self.cf.register_option('d.dep', 'bar', 'doc2') + self.assertEqual(self.cf.get_option('d.a'), 'foo') + self.assertEqual(self.cf.get_option('d.dep'), 'bar') + + self.cf.deprecate_option('d.dep', rkey='d.a') # reroute d.dep to d.a + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + self.assertEqual(self.cf.get_option('d.dep'), 'foo') + + self.assertEqual(len(w), 1) # should have raised one warning + self.assertTrue('eprecated' in str(w[-1])) # we get the custom message + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + self.cf.set_option('d.dep', 'baz') # should overwrite "d.a" + + self.assertEqual(len(w), 1) # should have raised one warning + self.assertTrue('eprecated' in str(w[-1])) # we get the custom message + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + self.assertEqual(self.cf.get_option('d.dep'), 'baz') + + self.assertEqual(len(w), 1) # should have raised one warning + self.assertTrue('eprecated' in str(w[-1])) # we get the custom message + + def test_config_prefix(self): + with self.cf.config_prefix("base"): + self.cf.register_option('a',1,"doc1") + self.cf.register_option('b',2,"doc2") + self.assertEqual(self.cf.get_option('a'), 1) + self.assertEqual(self.cf.get_option('b'), 2) + + self.cf.set_option('a',3) + self.cf.set_option('b',4) + self.assertEqual(self.cf.get_option('a'), 3) + self.assertEqual(self.cf.get_option('b'), 4) + + self.assertEqual(self.cf.get_option('base.a'), 3) + self.assertEqual(self.cf.get_option('base.b'), 4) + self.assertTrue('doc1' in self.cf.describe_options('base.a',_print_desc=False)) + self.assertTrue('doc2' in self.cf.describe_options('base.b',_print_desc=False)) + + self.cf.reset_option('base.a') + self.cf.reset_option('base.b') + + with self.cf.config_prefix("base"): + self.assertEqual(self.cf.get_option('a'), 1) + self.assertEqual(self.cf.get_option('b'), 2) + + +# fmt.reset_printoptions and fmt.set_printoptions were altered +# to use core.config, test_format exercises those paths.