Skip to content

Commit 285622f

Browse files
committed
ENH: Index inherits from FrozenNDArray + add FrozenList
* `FrozenNDArray` - thin wrapper around ndarray that disallows setting methods (will be used for levels on `MultiIndex`) * `FrozenList` - thin wrapper around list that disallows setting methods (needed because of type checks elsewhere) Index inherits from FrozenNDArray now and also actually copies for deepcopy. Assumption is that underlying array is still immutable-ish
1 parent bdf270c commit 285622f

File tree

7 files changed

+215
-21
lines changed

7 files changed

+215
-21
lines changed

pandas/core/base.py

+87-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""
2-
Base class(es) for all pandas objects.
2+
Base and utility classes for pandas objects.
33
"""
44
from pandas import compat
5+
import numpy as np
56

67
class StringMixin(object):
78
"""implements string methods so long as object defines a `__unicode__` method.
@@ -56,3 +57,88 @@ def __unicode__(self):
5657
"""
5758
# Should be overwritten by base classes
5859
return object.__repr__(self)
60+
61+
class FrozenList(PandasObject, list):
62+
"""
63+
Container that doesn't allow setting item *but*
64+
because it's technically non-hashable, will be used
65+
for lookups, appropriately, etc.
66+
"""
67+
# Sidenote: This has to be of type list, otherwise it messes up PyTables typechecks
68+
69+
def __add__(self, other):
70+
if isinstance(other, tuple):
71+
other = list(other)
72+
return self.__class__(super(FrozenList, self).__add__(other))
73+
74+
__iadd__ = __add__
75+
76+
# Python 2 compat
77+
def __getslice__(self, i, j):
78+
return self.__class__(super(FrozenList, self).__getslice__(i, j))
79+
80+
def __getitem__(self, n):
81+
# Python 3 compat
82+
if isinstance(n, slice):
83+
return self.__class__(super(FrozenList, self).__getitem__(n))
84+
return super(FrozenList, self).__getitem__(n)
85+
86+
def __radd__(self, other):
87+
if isinstance(other, tuple):
88+
other = list(other)
89+
return self.__class__(other + list(self))
90+
91+
def __eq__(self, other):
92+
if isinstance(other, (tuple, FrozenList)):
93+
other = list(other)
94+
return super(FrozenList, self).__eq__(other)
95+
96+
__req__ = __eq__
97+
98+
def __mul__(self, other):
99+
return self.__class__(super(FrozenList, self).__mul__(other))
100+
101+
__imul__ = __mul__
102+
103+
def __hash__(self):
104+
return hash(tuple(self))
105+
106+
def _disabled(self, *args, **kwargs):
107+
"""This method will not function because object is immutable."""
108+
raise TypeError("'%s' does not support mutable operations." %
109+
self.__class__)
110+
111+
def __unicode__(self):
112+
from pandas.core.common import pprint_thing
113+
return "%s(%s)" % (self.__class__.__name__,
114+
pprint_thing(self, quote_strings=True,
115+
escape_chars=('\t', '\r', '\n')))
116+
117+
__setitem__ = __setslice__ = __delitem__ = __delslice__ = _disabled
118+
pop = append = extend = remove = sort = insert = _disabled
119+
120+
121+
class FrozenNDArray(PandasObject, np.ndarray):
122+
123+
# no __array_finalize__ for now because no metadata
124+
def __new__(cls, data, dtype=None, copy=False):
125+
if copy is None:
126+
copy = not isinstance(data, FrozenNDArray)
127+
res = np.array(data, dtype=dtype, copy=copy).view(cls)
128+
return res
129+
130+
def _disabled(self, *args, **kwargs):
131+
"""This method will not function because object is immutable."""
132+
raise TypeError("'%s' does not support mutable operations." %
133+
self.__class__)
134+
135+
__setitem__ = __setslice__ = __delitem__ = __delslice__ = _disabled
136+
put = itemset = fill = _disabled
137+
138+
def _shallow_copy(self):
139+
return self.view()
140+
141+
def values(self):
142+
"""returns *copy* of underlying array"""
143+
arr = self.view(np.ndarray).copy()
144+
return arr

pandas/core/common.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,16 @@
88

99
from numpy.lib.format import read_array, write_array
1010
import numpy as np
11-
1211
import pandas.algos as algos
1312
import pandas.lib as lib
1413
import pandas.tslib as tslib
1514

1615
from pandas import compat
1716
from pandas.compat import StringIO, BytesIO, range, long, u, zip, map
18-
19-
2017
from pandas.core.config import get_option
2118
from pandas.core import array as pa
2219

20+
2321
# XXX: HACK for NumPy 1.5.1 to suppress warnings
2422
try:
2523
np.seterr(all='ignore')

pandas/core/index.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import pandas.algos as _algos
1010
import pandas.index as _index
1111
from pandas.lib import Timestamp
12-
from pandas.core.base import PandasObject
12+
from pandas.core.base import FrozenList, FrozenNDArray
1313

1414
from pandas.util.decorators import cache_readonly
1515
from pandas.core.common import isnull
@@ -47,7 +47,7 @@ def _shouldbe_timestamp(obj):
4747
or tslib.is_timestamp_array(obj))
4848

4949

50-
class Index(PandasObject, np.ndarray):
50+
class Index(FrozenNDArray):
5151
"""
5252
Immutable ndarray implementing an ordered, sliceable set. The basic object
5353
storing axis labels for all pandas objects
@@ -313,7 +313,7 @@ def __deepcopy__(self, memo={}):
313313
"""
314314
Index is not mutable, so disabling deepcopy
315315
"""
316-
return self
316+
return self._shallow_copy()
317317

318318
def __contains__(self, key):
319319
hash(key)
@@ -326,9 +326,6 @@ def __contains__(self, key):
326326
def __hash__(self):
327327
return hash(self.view(np.ndarray))
328328

329-
def __setitem__(self, key, value):
330-
raise TypeError(str(self.__class__) + ' does not support item assignment')
331-
332329
def __getitem__(self, key):
333330
"""Override numpy.ndarray's __getitem__ method to work as desired"""
334331
arr_idx = self.view(np.ndarray)

pandas/io/tests/test_parsers.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,11 @@ def test_parse_dates_column_list(self):
817817
expected = self.read_csv(StringIO(data), sep=";", index_col=lrange(4))
818818

819819
lev = expected.index.levels[0]
820-
expected.index.levels[0] = lev.to_datetime(dayfirst=True)
820+
levels = list(expected.index.levels)
821+
levels[0] = lev.to_datetime(dayfirst=True)
822+
# hack to get this to work - remove for final test
823+
levels[0].name = lev.name
824+
expected.index.levels = levels
821825
expected['aux_date'] = to_datetime(expected['aux_date'],
822826
dayfirst=True)
823827
expected['aux_date'] = lmap(Timestamp, expected['aux_date'])
@@ -2144,14 +2148,14 @@ def test_usecols_dtypes(self):
21442148
4,5,6
21452149
7,8,9
21462150
10,11,12"""
2147-
result = self.read_csv(StringIO(data), usecols=(0, 1, 2),
2148-
names=('a', 'b', 'c'),
2151+
result = self.read_csv(StringIO(data), usecols=(0, 1, 2),
2152+
names=('a', 'b', 'c'),
21492153
header=None,
21502154
converters={'a': str},
21512155
dtype={'b': int, 'c': float},
2152-
)
2156+
)
21532157
result2 = self.read_csv(StringIO(data), usecols=(0, 2),
2154-
names=('a', 'b', 'c'),
2158+
names=('a', 'b', 'c'),
21552159
header=None,
21562160
converters={'a': str},
21572161
dtype={'b': int, 'c': float},

pandas/tests/test_base.py

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import re
2+
import unittest
3+
import numpy as np
4+
from pandas.core.base import FrozenList, FrozenNDArray
5+
from pandas.util.testing import assertRaisesRegexp, assert_isinstance
6+
7+
8+
class CheckImmutable(object):
9+
mutable_regex = re.compile('does not support mutable operations')
10+
11+
def check_mutable_error(self, *args, **kwargs):
12+
# pass whatever functions you normally would to assertRaises (after the Exception kind)
13+
assertRaisesRegexp(TypeError, self.mutable_regex, *args, **kwargs)
14+
15+
def test_no_mutable_funcs(self):
16+
def setitem(): self.container[0] = 5
17+
18+
self.check_mutable_error(setitem)
19+
20+
def setslice(): self.container[1:2] = 3
21+
22+
self.check_mutable_error(setslice)
23+
24+
def delitem(): del self.container[0]
25+
26+
self.check_mutable_error(delitem)
27+
28+
def delslice(): del self.container[0:3]
29+
30+
self.check_mutable_error(delslice)
31+
mutable_methods = getattr(self, "mutable_methods", [])
32+
for meth in mutable_methods:
33+
self.check_mutable_error(getattr(self.container, meth))
34+
35+
def test_slicing_maintains_type(self):
36+
result = self.container[1:2]
37+
expected = self.lst[1:2]
38+
self.check_result(result, expected)
39+
40+
def check_result(self, result, expected, klass=None):
41+
klass = klass or self.klass
42+
assert_isinstance(result, klass)
43+
self.assertEqual(result, expected)
44+
45+
46+
class TestFrozenList(CheckImmutable, unittest.TestCase):
47+
mutable_methods = ('extend', 'pop', 'remove', 'insert')
48+
49+
def setUp(self):
50+
self.lst = [1, 2, 3, 4, 5]
51+
self.container = FrozenList(self.lst)
52+
self.klass = FrozenList
53+
54+
def test_add(self):
55+
result = self.container + (1, 2, 3)
56+
expected = FrozenList(self.lst + [1, 2, 3])
57+
self.check_result(result, expected)
58+
59+
result = (1, 2, 3) + self.container
60+
expected = FrozenList([1, 2, 3] + self.lst)
61+
self.check_result(result, expected)
62+
63+
def test_inplace(self):
64+
q = r = self.container
65+
q += [5]
66+
self.check_result(q, self.lst + [5])
67+
# other shouldn't be mutated
68+
self.check_result(r, self.lst)
69+
70+
71+
class TestFrozenNDArray(CheckImmutable, unittest.TestCase):
72+
mutable_methods = ('put', 'itemset', 'fill')
73+
74+
def setUp(self):
75+
self.lst = [3, 5, 7, -2]
76+
self.container = FrozenNDArray(self.lst)
77+
self.klass = FrozenNDArray
78+
79+
def test_shallow_copying(self):
80+
original = self.container.copy()
81+
assert_isinstance(self.container.view(), FrozenNDArray)
82+
self.assert_(not isinstance(self.container.view(np.ndarray), FrozenNDArray))
83+
self.assert_(self.container.view() is not self.container)
84+
self.assert_(np.array_equal(self.container, original))
85+
# shallow copy should be the same too
86+
assert_isinstance(self.container._shallow_copy(), FrozenNDArray)
87+
# setting should not be allowed
88+
def testit(container): container[0] = 16
89+
90+
self.check_mutable_error(testit, self.container)
91+
92+
def test_values(self):
93+
original = self.container.view(np.ndarray).copy()
94+
n = original[0] + 15
95+
vals = self.container.values()
96+
self.assert_(np.array_equal(original, vals))
97+
self.assert_(original is not vals)
98+
vals[0] = n
99+
self.assert_(np.array_equal(self.container, original))
100+
self.assertEqual(vals[0], n)
101+
102+
103+
if __name__ == '__main__':
104+
import nose
105+
106+
nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'],
107+
# '--with-coverage', '--cover-package=pandas.core'],
108+
exit=False)

pandas/tests/test_common.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
11
from datetime import datetime
22
import re
3+
import unittest
34

45
import nose
56
from nose.tools import assert_equal
67
import unittest
8+
import numpy as np
9+
from pandas.tslib import iNaT
710

811
from pandas import Series, DataFrame, date_range, DatetimeIndex, Timestamp
912
from pandas.compat import range, long, lrange, lmap, u
1013
from pandas.core.common import notnull, isnull
14+
import pandas.compat as compat
1115
import pandas.core.common as com
1216
import pandas.util.testing as tm
1317
import pandas.core.config as cf
1418

15-
import numpy as np
16-
17-
from pandas.tslib import iNaT
18-
from pandas import compat
19-
2019
_multiprocess_can_split_ = True
2120

2221

@@ -782,6 +781,7 @@ def test_2d_datetime64(self):
782781
expected[:, [2, 4]] = datetime(2007, 1, 1)
783782
tm.assert_almost_equal(result, expected)
784783

784+
785785
if __name__ == '__main__':
786786
nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'],
787787
exit=False)

pandas/tests/test_index.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ def test_deepcopy(self):
5757
from copy import deepcopy
5858

5959
copy = deepcopy(self.strIndex)
60-
self.assert_(copy is self.strIndex)
60+
self.assert_(copy is not self.strIndex)
61+
self.assert_(copy.equals(self.strIndex))
6162

6263
def test_duplicates(self):
6364
idx = Index([0, 0, 0])

0 commit comments

Comments
 (0)