8000 Merge pull request #451 from njsmith/unsafe-cast-deprecation · numpy/numpy@c8010d0 · GitHub
[go: up one dir, main page]

Skip to content

Commit c8010d0

Browse files
committed
Merge pull request #451 from njsmith/unsafe-cast-deprecation
Unsafe cast deprecation
2 parents fd63e8f + f18987a commit c8010d0

File tree

9 files changed

+130
-12
lines changed

9 files changed

+130
-12
lines changed

doc/release/1.7.0-notes.rst

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,14 @@ np.diagonal, numpy 1.7 produces a FutureWarning if it detects
3333
that you may be attemping to write to such an array. See the documentation
3434
for array indexing for details.
3535

36-
The default casting rule for UFunc out= parameters has been changed from
37-
'unsafe' to 'same_kind'. Most usages which violate the 'same_kind'
38-
rule are likely bugs, so this change may expose previously undetected
39-
errors in projects that depend on NumPy.
36+
In a future version of numpy, the default casting rule for UFunc out=
37+
parameters will be changed from 'unsafe' to 'same_kind'. (This also
38+
applies to in-place operations like a += b, which is equivalent to
39+
np.add(a, b, out=a).) Most usages which violate the 'same_kind' rule
40+
are likely bugs, so this change may expose previously undetected
41+
errors in projects that depend on NumPy. In this version of numpy,
42+
such usages will continue to succeed, but will raise a
43+
DeprecationWarning.
4044

4145
Full-array boolean indexing has been optimized to use a different,
4246
optimized code path. This code path should produce the same results,

doc/source/reference/ufuncs.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,12 @@ advanced usage and will not typically be used.
309309
'equiv', 'safe', 'same_kind', or 'unsafe'. See :func:`can_cast` for
310310
explanations of the parameter values.
311311

312+
In a future version of numpy, this argument will default to
313+
'same_kind'. As part of this transition, starting in version 1.7,
314+
ufuncs will produce a DeprecationWarning for calls which are
315+
allowed under the 'unsafe' rules, but not under the 'same_kind'
316+
rules.
317+
312318
*order*
313319

314320
.. versionadded:: 1.6

numpy/core/code_generators/numpy_api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414

1515
multiarray_global_vars = {
1616
'NPY_NUMUSERTYPES': 7,
17+
'NPY_DEFAULT_ASSIGN_CASTING': 292,
1718
}
1819

1920
multiarray_global_vars_types = {
2021
'NPY_NUMUSERTYPES': 'int',
22+
'NPY_DEFAULT_ASSIGN_CASTING': 'NPY_CASTING',
2123
}
2224

2325
multiarray_scalar_bool_values = {

numpy/core/include/numpy/ndarraytypes.h

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,14 @@ typedef enum {
199199
/* Allow safe casts or casts within the same kind */
200200
NPY_SAME_KIND_CASTING=3,
201201
/* Allow any casts */
202-
NPY_UNSAFE_CASTING=4
203-
} NPY_CASTING;
202+
NPY_UNSAFE_CASTING=4,
204203

205-
/* The default casting to use for typical assignment operations */
206-
#define NPY_DEFAULT_ASSIGN_CASTING NPY_SAME_KIND_CASTING
204+
/*
205+
* Temporary internal definition only, will be removed in upcoming
206+
* release, see below
207+
* */
208+
NPY_INTERNAL_UNSAFE_CASTING_BUT_WARN_UNLESS_SAME_KIND = 100,
209+
} NPY_CASTING;
207210

208211
typedef enum {
209212
NPY_CLIP=0,

numpy/core/src/multiarray/common.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@
1313
#include "common.h"
1414
#include "buffer.h"
1515

16+
/*
17+
* The casting to use for implicit assignment operations resulting from
18+
* in-place operations (like +=) and out= arguments. (Notice that this
19+
* variable is misnamed, but it's part of the public API so I'm not sure we
20+
* can just change it. Maybe someone should try and see if anyone notices.
21+
*/
22+
/*
23+
* In numpy 1.6 and earlier, this was NPY_UNSAFE_CASTING. In a future
24+
* release, it will become NPY_SAME_KIND_CASTING. Right now, during the
25+
* transitional period, we continue to follow the NPY_UNSAFE_CASTING rules (to
26+
* avoid breaking people's code), but we also check for whether the cast would
27+
* be allowed under the NPY_SAME_KIND_CASTING rules, and if not we issue a
28+
* warning (that people's code will be broken in a future release.)
29+
*/
30+
NPY_NO_EXPORT NPY_CASTING NPY_DEFAULT_ASSIGN_CASTING = NPY_INTERNAL_UNSAFE_CASTING_BUT_WARN_UNLESS_SAME_KIND;
31+
1632

1733
NPY_NO_EXPORT PyArray_Descr *
1834
_array_find_python_scalar_type(PyObject *op)

numpy/core/src/multiarray/convert_datatype.c

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,12 +503,43 @@ type_num_unsigned_to_signed(int type_num)
503503
}
504504
}
505505

506+
/*
507+
* NOTE: once the UNSAFE_CASTING -> SAME_KIND_CASTING transition is over,
508+
* we should remove NPY_INTERNAL_UNSAFE_CASTING_BUT_WARN_UNLESS_SAME_KIND
509+
* and PyArray_CanCastTypeTo_impl should be renamed back to
510+
* PyArray_CanCastTypeTo.
511+
*/
512+
static npy_bool
513+
PyArray_CanCastTypeTo_impl(PyArray_Descr *from, PyArray_Descr *to,
514+
NPY_CASTING casting);
515+
506516
/*NUMPY_API
507517
* Returns true if data of type 'from' may be cast to data of type
508518
* 'to' according to the rule 'casting'.
509519
*/
510520
NPY_NO_EXPORT npy_bool
511521
PyArray_CanCastTypeTo(PyArray_Descr *from, PyArray_Descr *to,
522+
NPY_CASTING casting)
523+
{
524+
if (casting == NPY_INTERNAL_UNSAFE_CASTING_BUT_WARN_UNLESS_SAME_KIND) {
525+
npy_bool unsafe_ok, same_kind_ok;
526+
unsafe_ok = PyArray_CanCastTypeTo_impl(from, to, NPY_UNSAFE_CASTING);
527+
same_kind_ok = PyArray_CanCastTypeTo_impl(from, to,
528+
NPY_SAME_KIND_CASTING);
529+
if (unsafe_ok && !same_kind_ok) {
530+
DEPRECATE("Implicitly casting between incompatible kinds. In "
531+
"a future numpy release, this will raise an error. "
532+
"Use casting=\"unsafe\" if this is intentional.");
533+
}
534+
return unsafe_ok;
535+
}
536+
else {
537+
return PyArray_CanCastTypeTo_impl(from, to, casting);
538+
}
539+
}
540+
541+
static npy_bool
542+
PyArray_CanCastTypeTo_impl(PyArray_Descr *from, PyArray_Descr *to,
512543
NPY_CASTING casting)
513544
{
514545
/* If unsafe casts are allowed */

numpy/core/tests/test_ufunc.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,5 +742,23 @@ def t(expect, func, n, m):
742742
uf.accumulate(np.zeros((30, 30)), axis=0)
743743
uf.accumulate(np.zeros((0, 0)), axis=0)
744744

745+
def test_safe_casting(self):
746+
# In old versions of numpy, in-place operations used the 'unsafe'
747+
# casting rules. In some future version, 'same_kind' will become the
748+
# default.
749+
a = np.array([1, 2, 3], dtype=int)
750+
# Non-in-place addition is fine
751+
assert_array_equal(assert_no_warnings(np.add, a, 1.1),
752+
[2.1, 3.1, 4.1])
753+
assert_warns(DeprecationWarning, np.add, a, 1.1, out=a)
754+
assert_array_equal(a, [2, 3, 4])
755+
def add_inplace(a, b):
756+
a += b
757+
assert_warns(DeprecationWarning, add_inplace, a, 1.1)
758+
assert_array_equal(a, [3, 4, 5])
759+
# Make sure that explicitly overriding the warning is allowed:
760+
assert_no_warnings(np.add, a, 1.1, out=a, casting="unsafe")
761+
assert_array_equal(a, [4, 5, 6])
762+
745763
if __name__ == "__main__":
746764
run_module_suite()

numpy/testing/tests/test_utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,11 +317,15 @@ class TestWarns(unittest.TestCase):
317317
def test_warn(self):
318318
def f():
319319
warnings.warn("yo")
320+
return 3
320321

321322
before_filters = sys.modules['warnings'].filters[:]
322-
assert_warns(UserWarning, f)
323+
assert_equal(assert_warns(UserWarning, f), 3)
323324
after_filters = sys.modules['warnings'].filters
324325

326+
assert_raises(AssertionError, assert_no_warnings, f)
327+
assert_equal(assert_no_warnings(lambda x: x, 1), 1)
328+
325329
# Check that the warnings state is unchanged
326330
assert_equal(before_filters, after_filters,
327331
"assert_warns does not preserver warnings state")

numpy/testing/utils.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
'decorate_methods', 'jiffies', 'memusage', 'print_assert_equal',
1717
'raises', 'rand', 'rundocs', 'runstring', 'verbose', 'measure',
1818
'assert_', 'assert_array_almost_equal_nulp',
19-
'assert_array_max_ulp', 'assert_warns', 'assert_allclose']
19+
'assert_array_max_ulp', 'assert_warns', 'assert_no_warnings',
20+
'assert_allclose']
2021

2122
verbose = 0
2223

@@ -1464,7 +1465,7 @@ def assert_warns(warning_class, func, *args, **kw):
14641465
14651466
Returns
14661467
-------
1467-
None
1468+
The value returned by `func`.
14681469
14691470
"""
14701471

@@ -1474,7 +1475,7 @@ def assert_warns(warning_class, func, *args, **kw):
14741475
l = ctx.__enter__()
14751476
warnings.simplefilter('always')
14761477
try:
1477-
func(*args, **kw)
1478+
result = func(*args, **kw)
14781479
if not len(l) > 0:
14791480
raise AssertionError("No warning raised when calling %s"
14801481
% func.__name__)
@@ -1483,3 +1484,36 @@ def assert_warns(warning_class, func, *args, **kw):
14831484
"%s( is %s)" % (func.__name__, warning_class, l[0]))
14841485
finally:
14851486
ctx.__exit__()
1487+
return result
1488+
1489+
def assert_no_warnings(func, *args, **kw):
1490+
"""
1491+
Fail if the given callable produces any warnings.
1492+
1493+
Parameters
1494+
----------
1495+
func : callable
1496+
The callable to test.
1497+
\\*args : Arguments
1498+
Arguments passed to `func`.
1499+
\\*\\*kwargs : Kwargs
1500+
Keyword arguments passed to `func`.
1501+
1502+
Returns
1503+
-------
1504+
The value returned by `func`.
1505+
1506+
"""
1507+
# XXX: once we may depend on python >= 2.6, this can be replaced by the
1508+
# warnings module context manager.
1509+
ctx = WarningManager(record=True)
1510+
l = ctx.__enter__()
1511+
warnings.simplefilter('always')
1512+
try:
1513+
result = func(*args, **kw)
1514+
if len(l) > 0:
1515+
raise AssertionError("Got warnings when calling %s: %s"
1516+
% (func.__name__, l))
1517+
finally:
1518+
ctx.__exit__()
1519+
return result

0 commit comments

Comments
 (0)
0