Skip to content

Commit efa5052

Browse files
author
Joe Jevnik
committed
BUG: fix a bug in Series.__setitem__ that allowed the mutatation of read-only arrays
1 parent d98e982 commit efa5052

File tree

5 files changed

+60
-5
lines changed

5 files changed

+60
-5
lines changed

doc/source/whatsnew/v0.19.1.txt

+1
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@ Bug Fixes
4545

4646
- Bug in ``pd.concat`` where names of the ``keys`` were not propagated to the resulting ``MultiIndex`` (:issue:`14252`)
4747
- Bug in ``MultiIndex.set_levels`` where illegal level values were still set after raising an error (:issue:`13754`)
48+
- Bug in ``Series.__setitem__` which allowed mutating read-only arrays (:issue:`14359`).

pandas/lib.pyx

+9-3
Original file line numberDiff line numberDiff line change
@@ -980,7 +980,9 @@ def astype_intsafe(ndarray[object] arr, new_dtype):
980980
if is_datelike and checknull(v):
981981
result[i] = NPY_NAT
982982
else:
983-
util.set_value_at(result, i, v)
983+
# we can use the unsafe version because we know `result` is mutable
984+
# since it was created from `np.empty`
985+
util.set_value_at_unsafe(result, i, v)
984986

985987
return result
986988

@@ -991,7 +993,9 @@ cpdef ndarray[object] astype_unicode(ndarray arr):
991993
ndarray[object] result = np.empty(n, dtype=object)
992994

993995
for i in range(n):
994-
util.set_value_at(result, i, unicode(arr[i]))
996+
# we can use the unsafe version because we know `result` is mutable
997+
# since it was created from `np.empty`
998+
util.set_value_at_unsafe(result, i, unicode(arr[i]))
995999

9961000
return result
9971001

@@ -1002,7 +1006,9 @@ cpdef ndarray[object] astype_str(ndarray arr):
10021006
ndarray[object] result = np.empty(n, dtype=object)
10031007

10041008
for i in range(n):
1005-
util.set_value_at(result, i, str(arr[i]))
1009+
# we can use the unsafe version because we know `result` is mutable
1010+
# since it was created from `np.empty`
1011+
util.set_value_at_unsafe(result, i, str(arr[i]))
10061012

10071013
return result
10081014

pandas/src/util.pxd

+14-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,12 @@ cdef inline object get_value_at(ndarray arr, object loc):
5656

5757
return get_value_1d(arr, i)
5858

59-
cdef inline set_value_at(ndarray arr, object loc, object value):
59+
cdef inline set_value_at_unsafe(ndarray arr, object loc, object value):
60+
"""Sets a value into the array without checking the writeable flag.
61+
62+
This should be used when setting values in a loop, check the writeable
63+
flag above the loop and then eschew the check on each iteration.
64+
"""
6065
cdef:
6166
Py_ssize_t i, sz
6267
if is_float_object(loc):
@@ -73,6 +78,14 @@ cdef inline set_value_at(ndarray arr, object loc, object value):
7378

7479
assign_value_1d(arr, i, value)
7580

81+
cdef inline set_value_at(ndarray arr, object loc, object value):
82+
"""Sets a value into the array after checking that the array is mutable.
83+
"""
84+
if not cnp.PyArray_ISWRITEABLE(arr):
85+
raise ValueError('assignment destination is read-only')
86+
87+
set_value_at_unsafe(arr, loc, value)
88+
7689
cdef inline int is_contiguous(ndarray arr):
7790
return cnp.PyArray_CHKFLAGS(arr, cnp.NPY_C_CONTIGUOUS)
7891

pandas/tests/series/test_indexing.py

+34
Original file line numberDiff line numberDiff line change
@@ -1947,6 +1947,40 @@ def test_multilevel_preserve_name(self):
19471947
self.assertEqual(result.name, s.name)
19481948
self.assertEqual(result2.name, s.name)
19491949

1950+
def test_setitem_scalar_into_readonly_backing_data(self):
1951+
# GH14359: test that you cannot mutate a read only buffer
1952+
1953+
array = np.zeros(5)
1954+
array.flags.writeable = False # make the array immutable
1955+
series = Series(array)
1956+
1957+
for n in range(len(series)):
1958+
with self.assertRaises(ValueError):
1959+
series[n] = 1
1960+
1961+
self.assertEqual(
1962+
array[n],
1963+
0,
1964+
msg='even though the ValueError was raised, the underlying'
1965+
' array was still mutated!',
1966+
)
1967+
1968+
def test_setitem_slice_into_readonly_backing_data(self):
1969+
# GH14359: test that you cannot mutate a read only buffer
1970+
1971+
array = np.zeros(5)
1972+
array.flags.writeable = False # make the array immutable
1973+
series = Series(array)
1974+
1975+
with self.assertRaises(ValueError):
1976+
series[1:3] = 1
1977+
1978+
self.assertTrue(
1979+
not array.any(),
1980+
msg='even though the ValueError was raised, the underlying'
1981+
' array was still mutated!',
1982+
)
1983+
19501984
if __name__ == '__main__':
19511985
import nose
19521986
nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'],

setup.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,8 @@ def pxd(name):
476476
'pandas/src/period_helper.c']},
477477
index={'pyxfile': 'index',
478478
'sources': ['pandas/src/datetime/np_datetime.c',
479-
'pandas/src/datetime/np_datetime_strings.c']},
479+
'pandas/src/datetime/np_datetime_strings.c'],
480+
'pxdfiles': ['src/util']},
480481
algos={'pyxfile': 'algos',
481482
'pxdfiles': ['src/util'],
482483
'depends': _pxi_dep['algos']},

0 commit comments

Comments
 (0)