diff --git a/doc/_templates/autosummary/accessor_attribute.rst b/doc/_templates/autosummary/accessor_attribute.rst new file mode 100644 index 0000000000000..e38a9f22f9d99 --- /dev/null +++ b/doc/_templates/autosummary/accessor_attribute.rst @@ -0,0 +1,6 @@ +{{ fullname }} +{{ underline }} + +.. currentmodule:: {{ module.split('.')[0] }} + +.. autoaccessorattribute:: {{ [module.split('.')[1], objname]|join('.') }} \ No newline at end of file diff --git a/doc/_templates/autosummary/accessor_method.rst b/doc/_templates/autosummary/accessor_method.rst new file mode 100644 index 0000000000000..8175d8615ceb2 --- /dev/null +++ b/doc/_templates/autosummary/accessor_method.rst @@ -0,0 +1,6 @@ +{{ fullname }} +{{ underline }} + +.. currentmodule:: {{ module.split('.')[0] }} + +.. autoaccessormethod:: {{ [module.split('.')[1], objname]|join('.') }} \ No newline at end of file diff --git a/doc/source/api.rst b/doc/source/api.rst index 9d40fe9114f97..a8097f2648c4b 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -449,114 +449,113 @@ Datetimelike Properties ``Series.dt`` can be used to access the values of the series as datetimelike and return several properties. -Due to implementation details the methods show up here as methods of the -``DatetimeProperties/PeriodProperties/TimedeltaProperties`` classes. These can be accessed like ``Series.dt.``. - -.. currentmodule:: pandas.tseries.common +These can be accessed like ``Series.dt.``. **Datetime Properties** .. autosummary:: :toctree: generated/ - - DatetimeProperties.date - DatetimeProperties.time - DatetimeProperties.year - DatetimeProperties.month - DatetimeProperties.day - DatetimeProperties.hour - DatetimeProperties.minute - DatetimeProperties.second - DatetimeProperties.microsecond - DatetimeProperties.nanosecond - DatetimeProperties.second - DatetimeProperties.weekofyear - DatetimeProperties.dayofweek - DatetimeProperties.weekday - DatetimeProperties.dayofyear - DatetimeProperties.quarter - DatetimeProperties.is_month_start - DatetimeProperties.is_month_end - DatetimeProperties.is_quarter_start - DatetimeProperties.is_quarter_end - DatetimeProperties.is_year_start - DatetimeProperties.is_year_end + :template: autosummary/accessor_attribute.rst + + Series.dt.date + Series.dt.time + Series.dt.year + Series.dt.month + Series.dt.day + Series.dt.hour + Series.dt.minute + Series.dt.second + Series.dt.microsecond + Series.dt.nanosecond + Series.dt.second + Series.dt.weekofyear + Series.dt.dayofweek + Series.dt.weekday + Series.dt.dayofyear + Series.dt.quarter + Series.dt.is_month_start + Series.dt.is_month_end + Series.dt.is_quarter_start + Series.dt.is_quarter_end + Series.dt.is_year_start + Series.dt.is_year_end **Datetime Methods** .. autosummary:: :toctree: generated/ + :template: autosummary/accessor_method.rst - DatetimeProperties.to_period - DatetimeProperties.to_pydatetime - DatetimeProperties.tz_localize - DatetimeProperties.tz_convert + Series.dt.to_period + Series.dt.to_pydatetime + Series.dt.tz_localize + Series.dt.tz_convert **Timedelta Properties** .. autosummary:: :toctree: generated/ + :template: autosummary/accessor_attribute.rst - TimedeltaProperties.days - TimedeltaProperties.seconds - TimedeltaProperties.microseconds - TimedeltaProperties.nanoseconds - TimedeltaProperties.components + Series.dt.days + Series.dt.seconds + Series.dt.microseconds + Series.dt.nanoseconds + Series.dt.components **Timedelta Methods** .. autosummary:: :toctree: generated/ + :template: autosummary/accessor_method.rst - TimedeltaProperties.to_pytimedelta + Series.dt.to_pytimedelta String handling ~~~~~~~~~~~~~~~ ``Series.str`` can be used to access the values of the series as -strings and apply several methods to it. Due to implementation -details the methods show up here as methods of the -``StringMethods`` class. These can be acccessed like ``Series.str.``. +strings and apply several methods to it. These can be acccessed like +``Series.str.``. .. currentmodule:: pandas.core.strings .. autosummary:: :toctree: generated/ - - StringMethods.cat - StringMethods.center - StringMethods.contains - StringMethods.count - StringMethods.decode - StringMethods.encode - StringMethods.endswith - StringMethods.extract - StringMethods.findall - StringMethods.get - StringMethods.join - StringMethods.len - StringMethods.lower - StringMethods.lstrip - StringMethods.match - StringMethods.pad - StringMethods.repeat - StringMethods.replace - StringMethods.rstrip - StringMethods.slice - StringMethods.slice_replace - StringMethods.split - StringMethods.startswith - StringMethods.strip - StringMethods.title - StringMethods.upper - StringMethods.get_dummies + :template: autosummary/accessor_method.rst + + Series.str.cat + Series.str.center + Series.str.contains + Series.str.count + Series.str.decode + Series.str.encode + Series.str.endswith + Series.str.extract + Series.str.findall + Series.str.get + Series.str.join + Series.str.len + Series.str.lower + Series.str.lstrip + Series.str.match + Series.str.pad + Series.str.repeat + Series.str.replace + Series.str.rstrip + Series.str.slice + Series.str.slice_replace + Series.str.split + Series.str.startswith + Series.str.strip + Series.str.title + Series.str.upper + Series.str.get_dummies .. _api.categorical: Categorical ~~~~~~~~~~~ -.. currentmodule:: pandas.core.categorical - If the Series is of dtype ``category``, ``Series.cat`` can be used to change the the categorical data. This accessor is similar to the ``Series.dt`` or ``Series.str`` and has the following usable methods and properties (all available as ``Series.cat.``). @@ -595,7 +594,6 @@ the Categorical back to a numpy array, so levels and order information is not pr Plotting ~~~~~~~~ -.. currentmodule:: pandas .. autosummary:: :toctree: generated/ diff --git a/doc/source/conf.py b/doc/source/conf.py index dd225dba7079a..fcb9c3fdd0016 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -297,6 +297,73 @@ 'pd.options.display.encoding="utf8"' ] + +# Add custom Documenter to handle attributes/methods of an AccessorProperty +# eg pandas.Series.str and pandas.Series.dt (see GH9322) + +from sphinx.util import rpartition +from sphinx.ext.autodoc import Documenter, MethodDocumenter, AttributeDocumenter + + +class AccessorLevelDocumenter(Documenter): + """ + Specialized Documenter subclass for objects on accessor level (methods, + attributes). + """ + + # This is the simple straightforward version + # modname is None, base the last elements (eg 'hour') + # and path the part before (eg 'Series.dt') + # def resolve_name(self, modname, parents, path, base): + # modname = 'pandas' + # mod_cls = path.rstrip('.') + # mod_cls = mod_cls.split('.') + # + # return modname, mod_cls + [base] + + def resolve_name(self, modname, parents, path, base): + if modname is None: + if path: + mod_cls = path.rstrip('.') + else: + mod_cls = None + # if documenting a class-level object without path, + # there must be a current class, either from a parent + # auto directive ... + mod_cls = self.env.temp_data.get('autodoc:class') + # ... or from a class directive + if mod_cls is None: + mod_cls = self.env.temp_data.get('py:class') + # ... if still None, there's no way to know + if mod_cls is None: + return None, [] + # HACK: this is added in comparison to ClassLevelDocumenter + # mod_cls still exists of class.accessor, so an extra + # rpartition is needed + modname, accessor = rpartition(mod_cls, '.') + modname, cls = rpartition(modname, '.') + parents = [cls, accessor] + # if the module name is still missing, get it like above + if not modname: + modname = self.env.temp_data.get('autodoc:module') + if not modname: + modname = self.env.temp_data.get('py:module') + # ... else, it stays None, which means invalid + return modname, parents + [base] + + +class AccessorAttributeDocumenter(AccessorLevelDocumenter, AttributeDocumenter): + + objtype = 'accessorattribute' + directivetype = 'attribute' + + +class AccessorMethodDocumenter(AccessorLevelDocumenter, MethodDocumenter): + + objtype = 'accessormethod' + directivetype = 'method' + + # remove the docstring of the flags attribute (inherited from numpy ndarray) # because these give doc build errors (see GH issue 5331) def remove_flags_docstring(app, what, name, obj, options, lines): @@ -305,3 +372,5 @@ def remove_flags_docstring(app, what, name, obj, options, lines): def setup(app): app.connect("autodoc-process-docstring", remove_flags_docstring) + app.add_autodocumenter(AccessorAttributeDocumenter) + app.add_autodocumenter(AccessorMethodDocumenter) diff --git a/doc/source/reshaping.rst b/doc/source/reshaping.rst index ddbfc60a5dfe7..dc13ce3e5c4da 100644 --- a/doc/source/reshaping.rst +++ b/doc/source/reshaping.rst @@ -478,7 +478,7 @@ This function is often used along with discretization functions like ``cut``: get_dummies(cut(values, bins)) -See also :func:`Series.str.get_dummies `. +See also :func:`Series.str.get_dummies `. .. versionadded:: 0.15.0 diff --git a/doc/source/text.rst b/doc/source/text.rst index 7032f5ff648a7..eb11cfb1248a9 100644 --- a/doc/source/text.rst +++ b/doc/source/text.rst @@ -204,27 +204,27 @@ Method Summary :header: "Method", "Description" :widths: 20, 80 - :meth:`~core.strings.StringMethods.cat`,Concatenate strings - :meth:`~core.strings.StringMethods.split`,Split strings on delimiter - :meth:`~core.strings.StringMethods.get`,Index into each element (retrieve i-th element) - :meth:`~core.strings.StringMethods.join`,Join strings in each element of the Series with passed separator - :meth:`~core.strings.StringMethods.contains`,Return boolean array if each string contains pattern/regex - :meth:`~core.strings.StringMethods.replace`,Replace occurrences of pattern/regex with some other string - :meth:`~core.strings.StringMethods.repeat`,Duplicate values (``s.str.repeat(3)`` equivalent to ``x * 3``) - :meth:`~core.strings.StringMethods.pad`,"Add whitespace to left, right, or both sides of strings" - :meth:`~core.strings.StringMethods.center`,Equivalent to ``pad(side='both')`` - :meth:`~core.strings.StringMethods.wrap`,Split long strings into lines with length less than a given width - :meth:`~core.strings.StringMethods.slice`,Slice each string in the Series - :meth:`~core.strings.StringMethods.slice_replace`,Replace slice in each string with passed value - :meth:`~core.strings.StringMethods.count`,Count occurrences of pattern - :meth:`~core.strings.StringMethods.startswith`,Equivalent to ``str.startswith(pat)`` for each element - :meth:`~core.strings.StringMethods.endswith`,Equivalent to ``str.endswith(pat)`` for each element - :meth:`~core.strings.StringMethods.findall`,Compute list of all occurrences of pattern/regex for each string - :meth:`~core.strings.StringMethods.match`,"Call ``re.match`` on each element, returning matched groups as list" - :meth:`~core.strings.StringMethods.extract`,"Call ``re.match`` on each element, as ``match`` does, but return matched groups as strings for convenience." - :meth:`~core.strings.StringMethods.len`,Compute string lengths - :meth:`~core.strings.StringMethods.strip`,Equivalent to ``str.strip`` - :meth:`~core.strings.StringMethods.rstrip`,Equivalent to ``str.rstrip`` - :meth:`~core.strings.StringMethods.lstrip`,Equivalent to ``str.lstrip`` - :meth:`~core.strings.StringMethods.lower`,Equivalent to ``str.lower`` - :meth:`~core.strings.StringMethods.upper`,Equivalent to ``str.upper`` + :meth:`~Series.str.cat`,Concatenate strings + :meth:`~Series.str.split`,Split strings on delimiter + :meth:`~Series.str.get`,Index into each element (retrieve i-th element) + :meth:`~Series.str.join`,Join strings in each element of the Series with passed separator + :meth:`~Series.str.contains`,Return boolean array if each string contains pattern/regex + :meth:`~Series.str.replace`,Replace occurrences of pattern/regex with some other string + :meth:`~Series.str.repeat`,Duplicate values (``s.str.repeat(3)`` equivalent to ``x * 3``) + :meth:`~Series.str.pad`,"Add whitespace to left, right, or both sides of strings" + :meth:`~Series.str.center`,Equivalent to ``pad(side='both')`` + :meth:`~Series.str.wrap`,Split long strings into lines with length less than a given width + :meth:`~Series.str.slice`,Slice each string in the Series + :meth:`~Series.str.slice_replace`,Replace slice in each string with passed value + :meth:`~Series.str.count`,Count occurrences of pattern + :meth:`~Series.str.startswith`,Equivalent to ``str.startswith(pat)`` for each element + :meth:`~Series.str.endswith`,Equivalent to ``str.endswith(pat)`` for each element + :meth:`~Series.str.findall`,Compute list of all occurrences of pattern/regex for each string + :meth:`~Series.str.match`,"Call ``re.match`` on each element, returning matched groups as list" + :meth:`~Series.str.extract`,"Call ``re.match`` on each element, as ``match`` does, but return matched groups as strings for convenience." + :meth:`~Series.str.len`,Compute string lengths + :meth:`~Series.str.strip`,Equivalent to ``str.strip`` + :meth:`~Series.str.rstrip`,Equivalent to ``str.rstrip`` + :meth:`~Series.str.lstrip`,Equivalent to ``str.lstrip`` + :meth:`~Series.str.lower`,Equivalent to ``str.lower`` + :meth:`~Series.str.upper`,Equivalent to ``str.upper`` diff --git a/doc/source/whatsnew/v0.16.0.txt b/doc/source/whatsnew/v0.16.0.txt index b3ac58a9fb84a..abd9d5850ff7e 100644 --- a/doc/source/whatsnew/v0.16.0.txt +++ b/doc/source/whatsnew/v0.16.0.txt @@ -107,6 +107,8 @@ Enhancements - ``Timedelta`` will now accept nanoseconds keyword in constructor (:issue:`9273`) +- Added auto-complete for ``Series.str.``, ``Series.dt.`` and ``Series.cat.`` (:issue:`9322`) + Performance ~~~~~~~~~~~ @@ -195,6 +197,7 @@ Bug Fixes - Bug in groupby ``.nth()`` with a multiple column groupby (:issue:`8979`) - Bug in ``DataFrame.where`` and ``Series.where`` coerce numerics to string incorrectly (:issue:`9280`) - Bug in ``DataFrame.where`` and ``Series.where`` raise ``ValueError`` when string list-like is passed. (:issue:`9280`) +- Accessing ``Series.str`` methods on with non-string values now raises ``TypeError`` instead of producing incorrect results (:issue:`9184`) - Fixed division by zero error for ``Series.kurt()`` when all values are equal (:issue:`9197`) diff --git a/pandas/core/base.py b/pandas/core/base.py index c3b3024a16d0c..dde2e74132c4b 100644 --- a/pandas/core/base.py +++ b/pandas/core/base.py @@ -166,6 +166,28 @@ def f(self, *args, **kwargs): if not hasattr(cls, name): setattr(cls,name,f) + +class AccessorProperty(object): + """Descriptor for implementing accessor properties like Series.str + """ + def __init__(self, accessor_cls, construct_accessor): + self.accessor_cls = accessor_cls + self.construct_accessor = construct_accessor + self.__doc__ = accessor_cls.__doc__ + + def __get__(self, instance, owner=None): + if instance is None: + # this ensures that Series.str. is well defined + return self.accessor_cls + return self.construct_accessor(instance) + + def __set__(self, instance, value): + raise AttributeError("can't set attribute") + + def __delete__(self, instance): + raise AttributeError("can't delete attribute") + + class FrozenList(PandasObject, list): """ diff --git a/pandas/core/categorical.py b/pandas/core/categorical.py index fe8b1079f0942..28c9d096e06d3 100644 --- a/pandas/core/categorical.py +++ b/pandas/core/categorical.py @@ -829,7 +829,7 @@ def searchsorted(self, v, side='left', sorter=None): array([3, 4]) # eggs before milk >>> x = pd.Categorical(['apple', 'bread', 'bread', 'cheese', 'milk', 'donuts' ]) >>> x.searchsorted(['bread', 'eggs'], side='right', sorter=[0, 1, 2, 3, 5, 4]) - array([3, 5]) # eggs after donuts, after switching milk and donuts + array([3, 5]) # eggs after donuts, after switching milk and donuts """ if not self.ordered: raise ValueError("searchsorted requires an ordered Categorical.") diff --git a/pandas/core/series.py b/pandas/core/series.py index 60b601a462520..ca401518af66d 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -27,7 +27,10 @@ from pandas.core.indexing import _check_bool_indexer, _maybe_convert_indices from pandas.core import generic, base from pandas.core.internals import SingleBlockManager -from pandas.core.categorical import Categorical +from pandas.core.categorical import Categorical, CategoricalAccessor +from pandas.core.strings import StringMethods +from pandas.tseries.common import (maybe_to_datetimelike, + CombinedDatetimelikeProperties) from pandas.tseries.index import DatetimeIndex from pandas.tseries.tdi import TimedeltaIndex from pandas.tseries.period import PeriodIndex, Period @@ -2042,7 +2045,8 @@ def apply(self, func, convert_dtype=True, args=(), **kwds): y : Series or DataFrame if func returns a Series """ if len(self) == 0: - return Series() + return self._constructor(dtype=self.dtype, + index=self.index).__finalize__(self) if kwds or args and not isinstance(func, np.ufunc): f = lambda x: func(x, *args, **kwds) @@ -2452,11 +2456,6 @@ def asof(self, where): new_values = com.take_1d(values, locs) return self._constructor(new_values, index=where).__finalize__(self) - @cache_readonly - def str(self): - from pandas.core.strings import StringMethods - return StringMethods(self) - def to_timestamp(self, freq=None, how='start', copy=True): """ Cast to datetimeindex of timestamps, at *beginning* of period @@ -2502,27 +2501,41 @@ def to_period(self, freq=None, copy=True): return self._constructor(new_values, index=new_index).__finalize__(self) + #------------------------------------------------------------------------------ + # string methods + + def _make_str_accessor(self): + if not com.is_object_dtype(self.dtype): + # this really should exclude all series with any non-string values, + # but that isn't practical for performance reasons until we have a + # str dtype (GH 9343) + raise TypeError("Can only use .str accessor with string values, " + "which use np.object_ dtype in pandas") + return StringMethods(self) + + str = base.AccessorProperty(StringMethods, _make_str_accessor) + #------------------------------------------------------------------------------ # Datetimelike delegation methods - @cache_readonly - def dt(self): - from pandas.tseries.common import maybe_to_datetimelike + def _make_dt_accessor(self): try: return maybe_to_datetimelike(self) except (Exception): raise TypeError("Can only use .dt accessor with datetimelike values") + dt = base.AccessorProperty(CombinedDatetimelikeProperties, _make_dt_accessor) + #------------------------------------------------------------------------------ # Categorical methods - @cache_readonly - def cat(self): - from pandas.core.categorical import CategoricalAccessor + def _make_cat_accessor(self): if not com.is_categorical_dtype(self.dtype): raise TypeError("Can only use .cat accessor with a 'category' dtype") return CategoricalAccessor(self.values, self.index) + cat = base.AccessorProperty(CategoricalAccessor, _make_cat_accessor) + Series._setup_axes(['index'], info_axis=0, stat_axis=0, aliases={'rows': 0}) Series._add_numeric_operations() diff --git a/pandas/core/strings.py b/pandas/core/strings.py index 9d4994e0f2de9..75d10654977cd 100644 --- a/pandas/core/strings.py +++ b/pandas/core/strings.py @@ -2,8 +2,6 @@ from pandas.compat import zip from pandas.core.common import isnull, _values_from_object -from pandas.core.series import Series -from pandas.core.frame import DataFrame import pandas.compat as compat import re import pandas.lib as lib @@ -12,6 +10,8 @@ def _get_array_list(arr, others): + from pandas.core.series import Series + if len(others) and isinstance(_values_from_object(others)[0], (list, np.ndarray, Series)): arrays = [arr] + list(others) @@ -95,6 +95,8 @@ def _na_map(f, arr, na_result=np.nan, dtype=object): def _map(f, arr, na_mask=False, na_value=np.nan, dtype=object): + from pandas.core.series import Series + if not len(arr): return np.ndarray(0, dtype=dtype) @@ -459,6 +461,9 @@ def str_extract(arr, pat, flags=0): 2 NaN NaN """ + from pandas.core.series import Series + from pandas.core.frame import DataFrame + regex = re.compile(pat, flags=flags) # just to be safe, check this if regex.groups == 0: @@ -510,6 +515,8 @@ def str_get_dummies(arr, sep='|'): See also ``pd.get_dummies``. """ + from pandas.core.frame import DataFrame + # TODO remove this hack? arr = arr.fillna('') try: @@ -643,6 +650,9 @@ def str_split(arr, pat=None, n=None, return_type='series'): ------- split : array """ + from pandas.core.series import Series + from pandas.core.frame import DataFrame + if return_type not in ('series', 'frame'): raise ValueError("return_type must be {'series', 'frame'}") if pat is None: @@ -949,6 +959,9 @@ def __iter__(self): g = self.get(i) def _wrap_result(self, result): + from pandas.core.series import Series + from pandas.core.frame import DataFrame + if not hasattr(result, 'ndim'): return result elif result.ndim == 1: diff --git a/pandas/tests/test_categorical.py b/pandas/tests/test_categorical.py index 4852e142d2f29..7a8d5e0ac0032 100644 --- a/pandas/tests/test_categorical.py +++ b/pandas/tests/test_categorical.py @@ -909,8 +909,8 @@ def test_searchsorted(self): exp = np.array([1]) self.assert_numpy_array_equal(res, exp) self.assert_numpy_array_equal(res, chk) - - # Searching for a value that is not present in the Categorical + + # Searching for a value that is not present in the Categorical res = c1.searchsorted(['bread', 'eggs']) chk = s1.searchsorted(['bread', 'eggs']) exp = np.array([1, 4]) @@ -927,7 +927,7 @@ def test_searchsorted(self): # As above, but with a sorter array to reorder an unsorted array res = c2.searchsorted(['bread', 'eggs'], side='right', sorter=[0, 1, 2, 3, 5, 4]) chk = s2.searchsorted(['bread', 'eggs'], side='right', sorter=[0, 1, 2, 3, 5, 4]) - exp = np.array([3, 5]) # eggs after donuts, after switching milk and donuts + exp = np.array([3, 5]) # eggs after donuts, after switching milk and donuts self.assert_numpy_array_equal(res, exp) self.assert_numpy_array_equal(res, chk) @@ -2516,6 +2516,15 @@ def get_dir(s): results = get_dir(s) tm.assert_almost_equal(results,list(sorted(set(ok_for_cat)))) + def test_cat_accessor_api(self): + # GH 9322 + from pandas.core.categorical import CategoricalAccessor + self.assertIs(Series.cat, CategoricalAccessor) + s = Series(list('aabbcde')).astype('category') + self.assertIsInstance(s.cat, CategoricalAccessor) + with tm.assertRaisesRegexp(TypeError, "only use .cat accessor"): + Series([1]).cat + def test_pickle_v0_14_1(self): cat = pd.Categorical(values=['a', 'b', 'c'], categories=['a', 'b', 'c', 'd'], diff --git a/pandas/tests/test_series.py b/pandas/tests/test_series.py index a5de26da1606a..3f5f14a46d3c3 100644 --- a/pandas/tests/test_series.py +++ b/pandas/tests/test_series.py @@ -231,6 +231,18 @@ def test_valid_dt_with_missing_values(self): expected = Series([time(0),time(0),np.nan,time(0),time(0)],dtype='object') tm.assert_series_equal(result, expected) + def test_dt_accessor_api(self): + # GH 9322 + from pandas.tseries.common import (CombinedDatetimelikeProperties, + DatetimeProperties) + self.assertIs(Series.dt, CombinedDatetimelikeProperties) + + s = Series(date_range('2000-01-01', periods=3)) + self.assertIsInstance(s.dt, DatetimeProperties) + + with tm.assertRaisesRegexp(TypeError, "only use .dt accessor"): + Series([1]).dt + def test_binop_maybe_preserve_name(self): # names match, preserve @@ -5402,9 +5414,14 @@ def test_apply(self): tm.assert_frame_equal(result, expected) # empty series - s = Series() + s = Series(dtype=object, name='foo', index=pd.Index([], name='bar')) rs = s.apply(lambda x: x) tm.assert_series_equal(s, rs) + # check all metadata (GH 9322) + self.assertIsNot(s, rs) + self.assertIs(s.index, rs.index) + self.assertEqual(s.dtype, rs.dtype) + self.assertEqual(s.name, rs.name) # index but no data s = Series(index=[1, 2, 3]) diff --git a/pandas/tests/test_strings.py b/pandas/tests/test_strings.py index 50dba3bc7218a..b8f1a6ac342af 100644 --- a/pandas/tests/test_strings.py +++ b/pandas/tests/test_strings.py @@ -32,8 +32,13 @@ class TestStringMethods(tm.TestCase): def test_api(self): - # GH 6106 - self.assertIsNone(Series.str) + # GH 6106, GH 9322 + self.assertIs(Series.str, strings.StringMethods) + self.assertIsInstance(Series(['']).str, strings.StringMethods) + + # GH 9184 + with tm.assertRaisesRegexp(TypeError, "only use .str accessor"): + Series([1]).str def test_iter(self): # GH3638 @@ -79,26 +84,6 @@ def test_iter_single_element(self): self.assertFalse(i) assert_series_equal(ds, s) - def test_iter_numeric_try_string(self): - # behavior identical to empty series - dsi = Series(lrange(4)) - - i, s = 100, 'h' - - for i, s in enumerate(dsi.str): - pass - - self.assertEqual(i, 100) - self.assertEqual(s, 'h') - - dsf = Series(np.arange(4.)) - - for i, s in enumerate(dsf.str): - pass - - self.assertEqual(i, 100) - self.assertEqual(s, 'h') - def test_iter_object_try_string(self): ds = Series([slice(None, randint(10), randint(10, 20)) for _ in range(4)]) diff --git a/pandas/tseries/common.py b/pandas/tseries/common.py index 7f6a0bc60dd57..2ceece087387e 100644 --- a/pandas/tseries/common.py +++ b/pandas/tseries/common.py @@ -3,7 +3,9 @@ import numpy as np from pandas.core.base import PandasDelegate from pandas.core import common as com -from pandas import Series, DatetimeIndex, PeriodIndex, TimedeltaIndex +from pandas.tseries.index import DatetimeIndex +from pandas.tseries.period import PeriodIndex +from pandas.tseries.tdi import TimedeltaIndex from pandas import lib, tslib from pandas.core.common import (_NS_DTYPE, _TD_DTYPE, is_period_arraylike, is_datetime_arraylike, is_integer_dtype, is_list_like, @@ -35,6 +37,7 @@ def maybe_to_datetimelike(data, copy=False): DelegatedClass """ + from pandas import Series if not isinstance(data, Series): raise TypeError("cannot convert an object of type {0} to a datetimelike index".format(type(data))) @@ -59,6 +62,8 @@ def __init__(self, values, index): self.index = index def _delegate_property_get(self, name): + from pandas import Series + result = getattr(self.values,name) # maybe need to upcast (ints) @@ -82,6 +87,8 @@ def _delegate_property_set(self, name, value, *args, **kwargs): "supported. Change values on the original.") def _delegate_method(self, name, *args, **kwargs): + from pandas import Series + method = getattr(self.values, name) result = method(*args, **kwargs) @@ -175,6 +182,14 @@ class PeriodProperties(Properties): accessors=PeriodIndex._datetimelike_ops, typ='property') + +class CombinedDatetimelikeProperties(DatetimeProperties, TimedeltaProperties): + # This class is never instantiated, and exists solely for the benefit of + # the Series.dt class property. For Series objects, .dt will always be one + # of the more specific classes above. + __doc__ = DatetimeProperties.__doc__ + + def _concat_compat(to_concat, axis=0): """ provide concatenation of an datetimelike array of arrays each of which is a single