Skip to content

Commit 76f8f9f

Browse files
Merge pull request #753 from effigies/fix/warning_filters
MRG: Safer warning registry manipulation when checking for overflows This approach unconditionally prepends the filter, which hopefully reduces the odds that removing it at the end will interfere with any other threads attempts to modify the registry. In particular, it should be re-entrant, so multiple threads calling this same function should not interfere with each other unless they happen to race on modifying the list. We could add a lock around the insertion/removal to avoid self-interference, though as noted in the other thread, no other libraries will respect it. Looking at Python 3.4+, adding filters to the filter list is followed by a call to `warnings._filters_mutated()`, so I'm adding that behavior in, even though it's not part of the API. When Python 2 is dropped, the `getattr` part can be removed, though perhaps it's safer to do this for private functions anyway. Closes #616
2 parents e8935b0 + fb082fa commit 76f8f9f

File tree

2 files changed

+72
-18
lines changed

2 files changed

+72
-18
lines changed

nibabel/tests/test_volumeutils.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import itertools
2020
import gzip
2121
import bz2
22+
import threading
23+
import time
2224

2325
import numpy as np
2426

@@ -45,6 +47,7 @@
4547
rec2dict,
4648
_dt_min_max,
4749
_write_data,
50+
_ftype4scaled_finite,
4851
)
4952
from ..openers import Opener, BZ2File
5053
from ..casting import (floor_log2, type_info, OK_FLOATS, shared_range)
@@ -1245,3 +1248,50 @@ def read(self, n_bytes):
12451248
'Expected {0} bytes, got {1} bytes from {2}\n'
12461249
' - could the file be damaged?'.format(
12471250
11390625000000000000, 0, 'object'))
1251+
1252+
1253+
def test__ftype4scaled_finite_warningfilters():
1254+
# This test checks our ability to properly manage the thread-unsafe
1255+
# warnings filter list.
1256+
1257+
# _ftype4scaled_finite always operates on one-or-two element arrays
1258+
# Ensure that an overflow will happen for < float64
1259+
finfo = np.finfo(np.float32)
1260+
tst_arr = np.array((finfo.min, finfo.max), dtype=np.float32)
1261+
1262+
go = threading.Event()
1263+
stop = threading.Event()
1264+
err = []
1265+
class MakeTotalDestroy(threading.Thread):
1266+
def run(self):
1267+
# Restore the warnings filters when we're done testing
1268+
with warnings.catch_warnings():
1269+
go.set()
1270+
while not stop.is_set():
1271+
warnings.filters[:] = []
1272+
time.sleep(0)
1273+
class CheckScaling(threading.Thread):
1274+
def run(self):
1275+
go.wait()
1276+
# Give ourselves a few bites at the apple
1277+
# 200 loops through the function takes ~10ms
1278+
# The highest number of iterations I've seen before hitting interference
1279+
# is 131, with 99% under 30, so this should be reasonably reliable.
1280+
for i in range(200):
1281+
try:
1282+
# Use float16 to ensure two failures and increase time in function
1283+
_ftype4scaled_finite(tst_arr, 2.0, 1.0, default=np.float16)
1284+
except Exception as e:
1285+
err.append(e)
1286+
break
1287+
stop.set()
1288+
1289+
thread_a = CheckScaling()
1290+
thread_b = MakeTotalDestroy()
1291+
thread_a.start()
1292+
thread_b.start()
1293+
thread_a.join()
1294+
thread_b.join()
1295+
1296+
if err:
1297+
raise err[0]

nibabel/volumeutils.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1334,26 +1334,30 @@ def _ftype4scaled_finite(tst_arr, slope, inter, direction='read',
13341334
tst_arr = np.atleast_1d(tst_arr)
13351335
slope = np.atleast_1d(slope)
13361336
inter = np.atleast_1d(inter)
1337-
warnings.filterwarnings('ignore', '.*overflow.*', RuntimeWarning)
1338-
try:
1339-
for ftype in OK_FLOATS[def_ind:]:
1340-
tst_trans = tst_arr.copy()
1341-
slope = slope.astype(ftype)
1342-
inter = inter.astype(ftype)
1343-
if direction == 'read': # as in reading of image from disk
1344-
if slope != 1.0:
1345-
tst_trans = tst_trans * slope
1346-
if inter != 0.0:
1347-
tst_trans = tst_trans + inter
1348-
elif direction == 'write':
1349-
if inter != 0.0:
1350-
tst_trans = tst_trans - inter
1351-
if slope != 1.0:
1352-
tst_trans = tst_trans / slope
1337+
overflow_filter = ('error', '.*overflow.*', RuntimeWarning)
1338+
for ftype in OK_FLOATS[def_ind:]:
1339+
tst_trans = tst_arr.copy()
1340+
slope = slope.astype(ftype)
1341+
inter = inter.astype(ftype)
1342+
try:
1343+
with warnings.catch_warnings():
1344+
# Error on overflows to short circuit the logic
1345+
warnings.filterwarnings(*overflow_filter)
1346+
if direction == 'read': # as in reading of image from disk
1347+
if slope != 1.0:
1348+
tst_trans = tst_trans * slope
1349+
if inter != 0.0:
1350+
tst_trans = tst_trans + inter
1351+
elif direction == 'write':
1352+
if inter != 0.0:
1353+
tst_trans = tst_trans - inter
1354+
if slope != 1.0:
1355+
tst_trans = tst_trans / slope
1356+
# Double-check that result is finite
13531357
if np.all(np.isfinite(tst_trans)):
13541358
return ftype
1355-
finally:
1356-
warnings.filters.pop(0)
1359+
except RuntimeWarning:
1360+
pass
13571361
raise ValueError('Overflow using highest floating point type')
13581362

13591363

0 commit comments

Comments
 (0)