diff --git a/RELEASE.rst b/RELEASE.rst index 26f3265029696..890425b729324 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -99,7 +99,7 @@ pandas 0.10.0 instead. This is a legacy hack and can lead to subtle bugs. - inf/-inf are no longer considered as NA by isnull/notnull. To be clear, this is legacy cruft from early pandas. This behavior can be globally re-enabled - using pandas.core.common.use_inf_as_na (#2050, #1919) + using the new option ``mode.use_inf_as_null`` (#2050, #1919) - ``pandas.merge`` will now default to ``sort=False``. For many use cases sorting the join keys is not necessary, and doing it by default is wasteful - ``names`` handling in file parsing: if explicit column `names` passed, diff --git a/doc/source/missing_data.rst b/doc/source/missing_data.rst index f514139a9170f..b8f3468f82098 100644 --- a/doc/source/missing_data.rst +++ b/doc/source/missing_data.rst @@ -61,7 +61,7 @@ arise and we wish to also consider that "missing" or "null". Until recently, for legacy reasons ``inf`` and ``-inf`` were also considered to be "null" in computations. This is no longer the case by -default; use the :func: `~pandas.core.common.use_inf_as_null` function to recover it. +default; use the ``mode.use_inf_as_null`` option to recover it. .. _missing.isnull: diff --git a/pandas/core/common.py b/pandas/core/common.py index d77d413d39571..386343dc38704 100644 --- a/pandas/core/common.py +++ b/pandas/core/common.py @@ -94,8 +94,8 @@ def isnull_old(obj): else: return obj is None -def use_inf_as_null(flag): - ''' +def _use_inf_as_null(key): + '''Option change callback for null/inf behaviour Choose which replacement for numpy.isnan / -numpy.isfinite is used. Parameters @@ -113,6 +113,7 @@ def use_inf_as_null(flag): * http://stackoverflow.com/questions/4859217/ programmatically-creating-variables-in-python/4859312#4859312 ''' + flag = get_option(key) if flag == True: globals()['isnull'] = isnull_old else: @@ -1179,7 +1180,7 @@ def in_interactive_session(): returns True if running under python/ipython interactive shell """ import __main__ as main - return not hasattr(main, '__file__') or get_option('test.interactive') + return not hasattr(main, '__file__') or get_option('mode.sim_interactive') def in_qtconsole(): """ diff --git a/pandas/core/config.py b/pandas/core/config.py index 9bd62c38fa8e5..5a2e6ef093f30 100644 --- a/pandas/core/config.py +++ b/pandas/core/config.py @@ -25,7 +25,9 @@ - all options in a certain sub - namespace can be reset at once. - the user can set / get / reset or ask for the description of an option. - a developer can register and mark an option as deprecated. - +- you can register a callback to be invoked when the the option value + is set or reset. Changing the stored value is considered misuse, but + is not verboten. Implementation ============== @@ -54,7 +56,7 @@ import warnings DeprecatedOption = namedtuple('DeprecatedOption', 'key msg rkey removal_ver') -RegisteredOption = namedtuple('RegisteredOption', 'key defval doc validator') +RegisteredOption = namedtuple('RegisteredOption', 'key defval doc validator cb') _deprecated_options = {} # holds deprecated option metdata _registered_options = {} # holds registered option metdata @@ -105,6 +107,9 @@ def _set_option(pat, value): root, k = _get_root(key) root[k] = value + if o and o.cb: + o.cb(key) + def _describe_option(pat='', _print_desc=True): @@ -270,7 +275,7 @@ def __doc__(self): ###################################################### # Functions for use by pandas developers, in addition to User - api -def register_option(key, defval, doc='', validator=None): +def register_option(key, defval, doc='', validator=None, cb=None): """Register an option in the package-wide pandas config object Parameters @@ -280,6 +285,9 @@ def register_option(key, defval, doc='', validator=None): 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. + cb - a function of a single argument "key", which is called + immediately after an option value is set/reset. key is + the full name of the option. Returns ------- @@ -321,7 +329,7 @@ def register_option(key, defval, doc='', validator=None): # save the option metadata _registered_options[key] = RegisteredOption(key=key, defval=defval, - doc=doc, validator=validator) + doc=doc, validator=validator,cb=cb) def deprecate_option(key, msg=None, rkey=None, removal_ver=None): diff --git a/pandas/core/config_init.py b/pandas/core/config_init.py index a854c012226cf..b2443e5b0b14e 100644 --- a/pandas/core/config_init.py +++ b/pandas/core/config_init.py @@ -139,10 +139,27 @@ cf.register_option('expand_frame_repr', True, pc_expand_repr_doc) cf.register_option('line_width', 80, pc_line_width_doc) -tc_interactive_doc=""" +tc_sim_interactive_doc=""" : boolean Default False Whether to simulate interactive mode for purposes of testing """ -with cf.config_prefix('test'): - cf.register_option('interactive', False, tc_interactive_doc) +with cf.config_prefix('mode'): + cf.register_option('sim_interactive', False, tc_sim_interactive_doc) + +use_inf_as_null_doc=""" +: boolean + True means treat None, NaN, INF, -INF as null (old way), + False means None and NaN are null, but INF, -INF are not null + (new way). +""" + +# we don't want to start importing evrything at the global context level +# or we'll hit circular deps. +def use_inf_as_null_cb(key): + from pandas.core.common import _use_inf_as_null + _use_inf_as_null(key) + +with cf.config_prefix('mode'): + cf.register_option('use_inf_as_null', False, use_inf_as_null_doc, + cb=use_inf_as_null_cb) diff --git a/pandas/tests/test_common.py b/pandas/tests/test_common.py index e8be2a5cc9c4c..1646d71164834 100644 --- a/pandas/tests/test_common.py +++ b/pandas/tests/test_common.py @@ -5,9 +5,10 @@ import unittest from pandas import Series, DataFrame, date_range, DatetimeIndex -from pandas.core.common import notnull, isnull, use_inf_as_null +from pandas.core.common import notnull, isnull import pandas.core.common as com import pandas.util.testing as tm +import pandas.core.config as cf import numpy as np @@ -29,15 +30,15 @@ def test_notnull(): assert not notnull(None) assert not notnull(np.NaN) - use_inf_as_null(False) + cf.set_option("mode.use_inf_as_null",False) assert notnull(np.inf) assert notnull(-np.inf) - use_inf_as_null(True) + cf.set_option("mode.use_inf_as_null",True) assert not notnull(np.inf) assert not notnull(-np.inf) - use_inf_as_null(False) + cf.set_option("mode.use_inf_as_null",False) float_series = Series(np.random.randn(5)) obj_series = Series(np.random.randn(5), dtype=object) diff --git a/pandas/tests/test_config.py b/pandas/tests/test_config.py index 814281dfc25e9..828bc85df4289 100644 --- a/pandas/tests/test_config.py +++ b/pandas/tests/test_config.py @@ -282,6 +282,31 @@ def test_config_prefix(self): self.assertEqual(self.cf.get_option('a'), 1) self.assertEqual(self.cf.get_option('b'), 2) + def test_callback(self): + k=[None] + v=[None] + def callback(key): + k.append(key) + v.append(self.cf.get_option(key)) + + self.cf.register_option('d.a', 'foo',cb=callback) + self.cf.register_option('d.b', 'foo',cb=callback) + + del k[-1],v[-1] + self.cf.set_option("d.a","fooz") + self.assertEqual(k[-1],"d.a") + self.assertEqual(v[-1],"fooz") + + del k[-1],v[-1] + self.cf.set_option("d.b","boo") + self.assertEqual(k[-1],"d.b") + self.assertEqual(v[-1],"boo") + + del k[-1],v[-1] + self.cf.reset_option("d.b") + self.assertEqual(k[-1],"d.b") + + # fmt.reset_printoptions and fmt.set_printoptions were altered # to use core.config, test_format exercises those paths. diff --git a/pandas/tests/test_format.py b/pandas/tests/test_format.py index 8b4866a643dde..7c93ece23237b 100644 --- a/pandas/tests/test_format.py +++ b/pandas/tests/test_format.py @@ -112,11 +112,11 @@ def test_repr_should_return_str (self): self.assertTrue(type(df.__repr__() == str)) # both py2 / 3 def test_repr_no_backslash(self): - pd.set_option('test.interactive', True) + pd.set_option('mode.sim_interactive', True) df = DataFrame(np.random.randn(10, 4)) self.assertTrue('\\' not in repr(df)) - pd.reset_option('test.interactive') + pd.reset_option('mode.sim_interactive') def test_to_string_repr_unicode(self): buf = StringIO() @@ -409,7 +409,7 @@ def test_frame_info_encoding(self): fmt.set_printoptions(max_rows=200) def test_wide_repr(self): - set_option('test.interactive', True) + set_option('mode.sim_interactive', True) col = lambda l, k: [tm.rands(k) for _ in xrange(l)] df = DataFrame([col(20, 25) for _ in range(10)]) set_option('print.expand_frame_repr', False) @@ -423,19 +423,19 @@ def test_wide_repr(self): self.assert_(len(wider_repr) < len(wide_repr)) reset_option('print.expand_frame_repr') - set_option('test.interactive', False) + set_option('mode.sim_interactive', False) set_option('print.line_width', 80) def test_wide_repr_wide_columns(self): - set_option('test.interactive', True) + set_option('mode.sim_interactive', True) df = DataFrame(randn(5, 3), columns=['a' * 90, 'b' * 90, 'c' * 90]) rep_str = repr(df) self.assert_(len(rep_str.splitlines()) == 20) - reset_option('test.interactive') + reset_option('mode.sim_interactive') def test_wide_repr_named(self): - set_option('test.interactive', True) + set_option('mode.sim_interactive', True) col = lambda l, k: [tm.rands(k) for _ in xrange(l)] df = DataFrame([col(20, 25) for _ in range(10)]) df.index.name = 'DataFrame Index' @@ -454,11 +454,11 @@ def test_wide_repr_named(self): self.assert_('DataFrame Index' in line) reset_option('print.expand_frame_repr') - set_option('test.interactive', False) + set_option('mode.sim_interactive', False) set_option('print.line_width', 80) def test_wide_repr_multiindex(self): - set_option('test.interactive', True) + set_option('mode.sim_interactive', True) col = lambda l, k: [tm.rands(k) for _ in xrange(l)] midx = pandas.MultiIndex.from_arrays([np.array(col(10, 5)), np.array(col(10, 5))]) @@ -479,11 +479,11 @@ def test_wide_repr_multiindex(self): self.assert_('Level 0 Level 1' in line) reset_option('print.expand_frame_repr') - set_option('test.interactive', False) + set_option('mode.sim_interactive', False) set_option('print.line_width', 80) def test_wide_repr_multiindex_cols(self): - set_option('test.interactive', True) + set_option('mode.sim_interactive', True) col = lambda l, k: [tm.rands(k) for _ in xrange(l)] midx = pandas.MultiIndex.from_arrays([np.array(col(10, 5)), np.array(col(10, 5))]) @@ -505,11 +505,11 @@ def test_wide_repr_multiindex_cols(self): self.assert_(len(wide_repr.splitlines()) == 14 * 10 - 1) reset_option('print.expand_frame_repr') - set_option('test.interactive', False) + set_option('mode.sim_interactive', False) set_option('print.line_width', 80) def test_wide_repr_unicode(self): - set_option('test.interactive', True) + set_option('mode.sim_interactive', True) col = lambda l, k: [tm.randu(k) for _ in xrange(l)] df = DataFrame([col(20, 25) for _ in range(10)]) set_option('print.expand_frame_repr', False) @@ -523,18 +523,18 @@ def test_wide_repr_unicode(self): self.assert_(len(wider_repr) < len(wide_repr)) reset_option('print.expand_frame_repr') - set_option('test.interactive', False) + set_option('mode.sim_interactive', False) set_option('print.line_width', 80) def test_wide_repr_wide_long_columns(self): - set_option('test.interactive', True) + set_option('mode.sim_interactive', True) df = DataFrame({'a': ['a'*30, 'b'*30], 'b': ['c'*70, 'd'*80]}) result = repr(df) self.assertTrue('ccccc' in result) self.assertTrue('ddddd' in result) - set_option('test.interactive', False) + set_option('mode.sim_interactive', False) def test_to_string(self): from pandas import read_table