diff options
Diffstat (limited to 'numpy')
-rw-r--r-- | numpy/core/_internal.py | 34 | ||||
-rw-r--r-- | numpy/core/shape_base.py | 65 | ||||
-rw-r--r-- | numpy/core/src/multiarray/alloc.c | 9 | ||||
-rw-r--r-- | numpy/core/src/multiarray/convert_datatype.c | 14 | ||||
-rw-r--r-- | numpy/core/src/multiarray/item_selection.c | 22 | ||||
-rw-r--r-- | numpy/distutils/command/config.py | 33 | ||||
-rw-r--r-- | numpy/distutils/fcompiler/ibm.py | 13 | ||||
-rw-r--r-- | numpy/f2py/__init__.py | 53 | ||||
-rw-r--r-- | numpy/f2py/tests/test_quoted_character.py | 110 | ||||
-rw-r--r-- | numpy/lib/npyio.py | 16 | ||||
-rw-r--r-- | numpy/lib/tests/test_arraypad.py | 15 | ||||
-rw-r--r-- | numpy/lib/tests/test_io.py | 6 | ||||
-rw-r--r-- | numpy/ma/core.py | 6 |
13 files changed, 321 insertions, 75 deletions
diff --git a/numpy/core/_internal.py b/numpy/core/_internal.py index 48ede14d0..9b8f5aef3 100644 --- a/numpy/core/_internal.py +++ b/numpy/core/_internal.py @@ -1,5 +1,5 @@ """ -A place for code to be called from core C-code. +A place for internal code Some things are more easily handled Python. @@ -808,3 +808,35 @@ def _is_from_ctypes(obj): return 'ctypes' in ctype_base.__module__ except Exception: return False + + +class recursive(object): + ''' + A decorator class for recursive nested functions. + Naive recursive nested functions hold a reference to themselves: + + def outer(*args): + def stringify_leaky(arg0, *arg1): + if len(arg1) > 0: + return stringify_leaky(*arg1) # <- HERE + return str(arg0) + stringify_leaky(*args) + + This design pattern creates a reference cycle that is difficult for a + garbage collector to resolve. The decorator class prevents the + cycle by passing the nested function in as an argument `self`: + + def outer(*args): + @recursive + def stringify(self, arg0, *arg1): + if len(arg1) > 0: + return self(*arg1) + return str(arg0) + stringify(*args) + + ''' + def __init__(self, func): + self.func = func + def __call__(self, *args, **kwargs): + return self.func(self, *args, **kwargs) + diff --git a/numpy/core/shape_base.py b/numpy/core/shape_base.py index 319c25088..30919ed7e 100644 --- a/numpy/core/shape_base.py +++ b/numpy/core/shape_base.py @@ -7,6 +7,7 @@ __all__ = ['atleast_1d', 'atleast_2d', 'atleast_3d', 'block', 'hstack', from . import numeric as _nx from .numeric import array, asanyarray, newaxis from .multiarray import normalize_axis_index +from ._internal import recursive def atleast_1d(*arys): """ @@ -360,6 +361,14 @@ def stack(arrays, axis=0, out=None): return _nx.concatenate(expanded_arrays, axis=axis, out=out) +def _block_format_index(index): + """ + Convert a list of indices ``[0, 1, 2]`` into ``"arrays[0][1][2]"``. + """ + idx_str = ''.join('[{}]'.format(i) for i in index if i is not None) + return 'arrays' + idx_str + + def _block_check_depths_match(arrays, parent_index=[]): """ Recursive function checking that the depths of nested lists in `arrays` @@ -370,19 +379,23 @@ def _block_check_depths_match(arrays, parent_index=[]): for each innermost list, in case an error needs to be raised, so that the index of the offending list can be printed as part of the error. - The parameter `parent_index` is the full index of `arrays` within the - nested lists passed to _block_check_depths_match at the top of the - recursion. - The return value is a pair. The first item returned is the full index - of an element (specifically the first element) from the bottom of the - nesting in `arrays`. An empty list at the bottom of the nesting is - represented by a `None` index. - The second item is the maximum of the ndims of the arrays nested in - `arrays`. + Parameters + ---------- + arrays : nested list of arrays + The arrays to check + parent_index : list of int + The full index of `arrays` within the nested lists passed to + `_block_check_depths_match` at the top of the recursion. + + Returns + ------- + first_index : list of int + The full index of an element from the bottom of the nesting in + `arrays`. If any element at the bottom is an empty list, this will + refer to it, and the last index along the empty axis will be `None`. + max_arr_ndim : int + The maximum of the ndims of the arrays nested in `arrays`. """ - def format_index(index): - idx_str = ''.join('[{}]'.format(i) for i in index if i is not None) - return 'arrays' + idx_str if type(arrays) is tuple: # not strictly necessary, but saves us from: # - more than one way to do things - no point treating tuples like @@ -393,7 +406,7 @@ def _block_check_depths_match(arrays, parent_index=[]): '{} is a tuple. ' 'Only lists can be used to arrange blocks, and np.block does ' 'not allow implicit conversion from tuple to ndarray.'.format( - format_index(parent_index) + _block_format_index(parent_index) ) ) elif type(arrays) is list and len(arrays) > 0: @@ -410,9 +423,12 @@ def _block_check_depths_match(arrays, parent_index=[]): "{}, but there is an element at depth {} ({})".format( len(first_index), len(index), - format_index(index) + _block_format_index(index) ) ) + # propagate our flag that indicates an empty list at the bottom + if index[-1] is None: + first_index = index return first_index, max_arr_ndim elif type(arrays) is list and len(arrays) == 0: # We've 'bottomed out' on an empty list @@ -435,24 +451,17 @@ def _block(arrays, max_depth, result_ndim): # ones to `a.shape` as necessary return array(a, ndmin=ndim, copy=False, subok=True) - def block_recursion(arrays, depth=0): + @recursive + def block_recursion(self, arrays, depth=0): if depth < max_depth: - if len(arrays) == 0: - raise ValueError('Lists cannot be empty') - arrs = [block_recursion(arr, depth+1) for arr in arrays] + arrs = [self(arr, depth+1) for arr in arrays] return _nx.concatenate(arrs, axis=-(max_depth-depth)) else: # We've 'bottomed out' - arrays is either a scalar or an array # type(arrays) is not list return atleast_nd(arrays, result_ndim) - try: - return block_recursion(arrays) - finally: - # recursive closures have a cyclic reference to themselves, which - # requires gc to collect (gh-10620). To avoid this problem, for - # performance and PyPy friendliness, we break the cycle: - block_recursion = None + return block_recursion(arrays) def block(arrays): @@ -605,4 +614,10 @@ def block(arrays): """ bottom_index, arr_ndim = _block_check_depths_match(arrays) list_ndim = len(bottom_index) + if bottom_index and bottom_index[-1] is None: + raise ValueError( + 'List at {} cannot be empty'.format( + _block_format_index(bottom_index) + ) + ) return _block(arrays, list_ndim, max(arr_ndim, list_ndim)) diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index ae4b81cf5..fe957bf16 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -35,6 +35,13 @@ typedef struct { static cache_bucket datacache[NBUCKETS]; static cache_bucket dimcache[NBUCKETS_DIM]; +/* as the cache is managed in global variables verify the GIL is held */ +#if defined(NPY_PY3K) +#define NPY_CHECK_GIL_HELD() PyGILState_Check() +#else +#define NPY_CHECK_GIL_HELD() 1 +#endif + /* * very simplistic small memory block cache to avoid more expensive libc * allocations @@ -47,6 +54,7 @@ _npy_alloc_cache(npy_uintp nelem, npy_uintp esz, npy_uint msz, { assert((esz == 1 && cache == datacache) || (esz == sizeof(npy_intp) && cache == dimcache)); + assert(NPY_CHECK_GIL_HELD()); if (nelem < msz) { if (cache[nelem].available > 0) { return cache[nelem].ptrs[--(cache[nelem].available)]; @@ -75,6 +83,7 @@ static NPY_INLINE void _npy_free_cache(void * p, npy_uintp nelem, npy_uint msz, cache_bucket * cache, void (*dealloc)(void *)) { + assert(NPY_CHECK_GIL_HELD()); if (p != NULL && nelem < msz) { if (cache[nelem].available < NCACHE) { cache[nelem].ptrs[cache[nelem].available++] = p; diff --git a/numpy/core/src/multiarray/convert_datatype.c b/numpy/core/src/multiarray/convert_datatype.c index 0d79f294c..3df764a48 100644 --- a/numpy/core/src/multiarray/convert_datatype.c +++ b/numpy/core/src/multiarray/convert_datatype.c @@ -149,11 +149,6 @@ PyArray_AdaptFlexibleDType(PyObject *data_obj, PyArray_Descr *data_dtype, { PyArray_DatetimeMetaData *meta; int flex_type_num; - PyArrayObject *arr = NULL; - PyArray_Descr *dtype = NULL; - int ndim = 0; - npy_intp dims[NPY_MAXDIMS]; - int result; if (*flex_dtype == NULL) { if (!PyErr_Occurred()) { @@ -168,7 +163,7 @@ PyArray_AdaptFlexibleDType(PyObject *data_obj, PyArray_Descr *data_dtype, /* Flexible types with expandable size */ if (PyDataType_ISUNSIZED(*flex_dtype)) { - /* First replace the flex dtype */ + /* First replace the flex_dtype */ PyArray_DESCR_REPLACE(*flex_dtype); if (*flex_dtype == NULL) { return; @@ -259,6 +254,11 @@ PyArray_AdaptFlexibleDType(PyObject *data_obj, PyArray_Descr *data_dtype, * GetArrayParamsFromObject won't iterate over * array. */ + PyArray_Descr *dtype = NULL; + PyArrayObject *arr = NULL; + int result; + int ndim = 0; + npy_intp dims[NPY_MAXDIMS]; list = PyArray_ToList((PyArrayObject *)data_obj); result = PyArray_GetArrayParamsFromObject( list, @@ -273,6 +273,8 @@ PyArray_AdaptFlexibleDType(PyObject *data_obj, PyArray_Descr *data_dtype, size = dtype->elsize; } } + Py_XDECREF(dtype); + Py_XDECREF(arr); Py_DECREF(list); } else if (PyArray_IsPythonScalar(data_obj)) { diff --git a/numpy/core/src/multiarray/item_selection.c b/numpy/core/src/multiarray/item_selection.c index 141b2d922..925585704 100644 --- a/numpy/core/src/multiarray/item_selection.c +++ b/numpy/core/src/multiarray/item_selection.c @@ -833,8 +833,6 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, } size = it->size; - NPY_BEGIN_THREADS_DESCR(PyArray_DESCR(op)); - if (needcopy) { buffer = npy_alloc_cache(N * elsize); if (buffer == NULL) { @@ -843,6 +841,8 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, } } + NPY_BEGIN_THREADS_DESCR(PyArray_DESCR(op)); + while (size--) { char *bufptr = it->dataptr; @@ -917,8 +917,8 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, } fail: - npy_free_cache(buffer, N * elsize); NPY_END_THREADS_DESCR(PyArray_DESCR(op)); + npy_free_cache(buffer, N * elsize); if (ret < 0 && !PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ PyErr_NoMemory(); @@ -979,8 +979,6 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, } size = it->size; - NPY_BEGIN_THREADS_DESCR(PyArray_DESCR(op)); - if (needcopy) { valbuffer = npy_alloc_cache(N * elsize); if (valbuffer == NULL) { @@ -997,6 +995,8 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, } } + NPY_BEGIN_THREADS_DESCR(PyArray_DESCR(op)); + while (size--) { char *valptr = it->dataptr; npy_intp *idxptr = (npy_intp *)rit->dataptr; @@ -1080,9 +1080,9 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, } fail: + NPY_END_THREADS_DESCR(PyArray_DESCR(op)); npy_free_cache(valbuffer, N * elsize); npy_free_cache(idxbuffer, N * sizeof(npy_intp)); - NPY_END_THREADS_DESCR(PyArray_DESCR(op)); if (ret < 0) { if (!PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ @@ -1498,13 +1498,13 @@ PyArray_LexSort(PyObject *sort_keys, int axis) char *valbuffer, *indbuffer; int *swaps; - valbuffer = npy_alloc_cache(N * maxelsize); + valbuffer = PyDataMem_NEW(N * maxelsize); if (valbuffer == NULL) { goto fail; } - indbuffer = npy_alloc_cache(N * sizeof(npy_intp)); + indbuffer = PyDataMem_NEW(N * sizeof(npy_intp)); if (indbuffer == NULL) { - npy_free_cache(indbuffer, N * sizeof(npy_intp)); + PyDataMem_FREE(indbuffer); goto fail; } swaps = malloc(n*sizeof(int)); @@ -1547,8 +1547,8 @@ PyArray_LexSort(PyObject *sort_keys, int axis) sizeof(npy_intp), N, sizeof(npy_intp)); PyArray_ITER_NEXT(rit); } - npy_free_cache(valbuffer, N * maxelsize); - npy_free_cache(indbuffer, N * sizeof(npy_intp)); + PyDataMem_FREE(valbuffer); + PyDataMem_FREE(indbuffer); free(swaps); } else { diff --git a/numpy/distutils/command/config.py b/numpy/distutils/command/config.py index 74a05e6a8..6b904d6ef 100644 --- a/numpy/distutils/command/config.py +++ b/numpy/distutils/command/config.py @@ -7,6 +7,7 @@ from __future__ import division, absolute_import, print_function import os, signal import warnings import sys +import subprocess from distutils.command.config import config as old_config from distutils.command.config import LANG_EXT @@ -14,7 +15,7 @@ from distutils import log from distutils.file_util import copy_file from distutils.ccompiler import CompileError, LinkError import distutils -from numpy.distutils.exec_command import exec_command +from numpy.distutils.exec_command import filepath_from_subprocess_output from numpy.distutils.mingw32ccompiler import generate_manifest from numpy.distutils.command.autodist import (check_gcc_function_attribute, check_gcc_variable_attribute, @@ -121,9 +122,13 @@ Original exception was: %s, and the Compiler class was %s # correct path when compiling in Cygwin but with # normal Win Python if d.startswith('/usr/lib'): - s, o = exec_command(['cygpath', '-w', d], - use_tee=False) - if not s: d = o + try: + d = subprocess.check_output(['cygpath', + '-w', d]) + except (OSError, subprocess.CalledProcessError): + pass + else: + d = filepath_from_subprocess_output(d) library_dirs.append(d) for libname in self.fcompiler.libraries or []: if libname not in libraries: @@ -449,8 +454,24 @@ int main (void) grabber.restore() raise exe = os.path.join('.', exe) - exitstatus, output = exec_command(exe, execute_in='.', - use_tee=use_tee) + try: + # specify cwd arg for consistency with + # historic usage pattern of exec_command() + # also, note that exe appears to be a string, + # which exec_command() handled, but we now + # use a list for check_output() -- this assumes + # that exe is always a single command + output = subprocess.check_output([exe], cwd='.') + except subprocess.CalledProcessError as exc: + exitstatus = exc.returncode + output = '' + except OSError: + # preserve the EnvironmentError exit status + # used historically in exec_command() + exitstatus = 127 + output = '' + else: + output = filepath_from_subprocess_output(output) if hasattr(os, 'WEXITSTATUS'): exitcode = os.WEXITSTATUS(exitstatus) if os.WIFSIGNALED(exitstatus): diff --git a/numpy/distutils/fcompiler/ibm.py b/numpy/distutils/fcompiler/ibm.py index d0c2202d4..c4cb2fca7 100644 --- a/numpy/distutils/fcompiler/ibm.py +++ b/numpy/distutils/fcompiler/ibm.py @@ -3,9 +3,10 @@ from __future__ import division, absolute_import, print_function import os import re import sys +import subprocess from numpy.distutils.fcompiler import FCompiler -from numpy.distutils.exec_command import exec_command, find_executable +from numpy.distutils.exec_command import find_executable from numpy.distutils.misc_util import make_temp_file from distutils import log @@ -35,9 +36,13 @@ class IBMFCompiler(FCompiler): lslpp = find_executable('lslpp') xlf = find_executable('xlf') if os.path.exists(xlf) and os.path.exists(lslpp): - s, o = exec_command(lslpp + ' -Lc xlfcmp') - m = re.search(r'xlfcmp:(?P<version>\d+([.]\d+)+)', o) - if m: version = m.group('version') + try: + o = subprocess.check_output([lslpp, '-Lc', 'xlfcmp']) + except (OSError, subprocess.CalledProcessError): + pass + else: + m = re.search(r'xlfcmp:(?P<version>\d+([.]\d+)+)', o) + if m: version = m.group('version') xlf_dir = '/etc/opt/ibmcmp/xlf' if version is None and os.path.isdir(xlf_dir): diff --git a/numpy/f2py/__init__.py b/numpy/f2py/__init__.py index fbb64f762..23a4b7c41 100644 --- a/numpy/f2py/__init__.py +++ b/numpy/f2py/__init__.py @@ -7,6 +7,10 @@ from __future__ import division, absolute_import, print_function __all__ = ['run_main', 'compile', 'f2py_testing'] import sys +import subprocess +import os + +import numpy as np from . import f2py2e from . import f2py_testing @@ -32,8 +36,12 @@ def compile(source, Fortran source of module / subroutine to compile modulename : str, optional The name of the compiled python module - extra_args : str, optional + extra_args : str or list, optional Additional parameters passed to f2py + + .. versionchanged:: 1.16.0 + A list of args may also be provided. + verbose : bool, optional Print f2py output to screen source_fn : str, optional @@ -48,25 +56,48 @@ def compile(source, .. versionadded:: 1.11.0 """ - from numpy.distutils.exec_command import exec_command import tempfile + import shlex + if source_fn is None: - f = tempfile.NamedTemporaryFile(suffix=extension) + f, fname = tempfile.mkstemp(suffix=extension) + # f is a file descriptor so need to close it + # carefully -- not with .close() directly + os.close(f) else: - f = open(source_fn, 'w') + fname = source_fn try: - f.write(source) - f.flush() + with open(fname, 'w') as f: + f.write(str(source)) + + args = ['-c', '-m', modulename, f.name] + + if isinstance(extra_args, np.compat.basestring): + is_posix = (os.name == 'posix') + extra_args = shlex.split(extra_args, posix=is_posix) + + args.extend(extra_args) - args = ' -c -m {} {} {}'.format(modulename, f.name, extra_args) - c = '{} -c "import numpy.f2py as f2py2e;f2py2e.main()" {}' - c = c.format(sys.executable, args) - status, output = exec_command(c) + c = [sys.executable, + '-c', + 'import numpy.f2py as f2py2e;f2py2e.main()'] + args + try: + output = subprocess.check_output(c) + except subprocess.CalledProcessError as exc: + status = exc.returncode + output = '' + except OSError: + # preserve historic status code used by exec_command() + status = 127 + output = '' + else: + status = 0 if verbose: print(output) finally: - f.close() + if source_fn is None: + os.remove(fname) return status from numpy._pytesttester import PytestTester diff --git a/numpy/f2py/tests/test_quoted_character.py b/numpy/f2py/tests/test_quoted_character.py index 09fc7328f..c41be3a80 100644 --- a/numpy/f2py/tests/test_quoted_character.py +++ b/numpy/f2py/tests/test_quoted_character.py @@ -1,8 +1,13 @@ from __future__ import division, absolute_import, print_function import sys +import os +import uuid +from importlib import import_module import pytest +import numpy.f2py + from numpy.testing import assert_equal from . import util @@ -28,3 +33,108 @@ Cf2py intent(out) OUT1, OUT2, OUT3, OUT4, OUT5, OUT6 reason='Fails with MinGW64 Gfortran (Issue #9673)') def test_quoted_character(self): assert_equal(self.module.foo(), (b"'", b'"', b';', b'!', b'(', b')')) + +@pytest.mark.xfail(sys.version_info[0] < 3 and os.name == 'nt', + reason="our Appveyor CI configuration does not" + " have Fortran compilers available for" + " Python 2.x") +@pytest.mark.parametrize("extra_args", [ + # extra_args can be a list as of gh-11937 + ['--noopt', '--debug'], + # test for string as well, using the same + # fcompiler options + '--noopt --debug', + # also test absence of extra_args + '', + ]) +def test_f2py_init_compile(extra_args): + # flush through the f2py __init__ + # compile() function code path + # as a crude test for input handling + # following migration from exec_command() + # to subprocess.check_output() in gh-11937 + + # the Fortran 77 syntax requires 6 spaces + # before any commands, but more space may + # be added; gfortran can also compile + # with --ffree-form to remove the indentation + # requirement; here, the Fortran source is + # formatted to roughly match an example from + # the F2PY User Guide + fsource = ''' + integer function foo() + foo = 10 + 5 + return + end + ''' + # use various helper functions in util.py to + # enable robust build / compile and + # reimport cycle in test suite + d = util.get_module_dir() + modulename = util.get_temp_module_name() + + cwd = os.getcwd() + target = os.path.join(d, str(uuid.uuid4()) + '.f') + # try running compile() with and without a + # source_fn provided so that the code path + # where a temporary file for writing Fortran + # source is created is also explored + for source_fn in [target, None]: + + # mimic the path changing behavior used + # by build_module() in util.py, but don't + # actually use build_module() because it + # has its own invocation of subprocess + # that circumvents the f2py.compile code + # block under test + try: + os.chdir(d) + ret_val = numpy.f2py.compile(fsource, + modulename=modulename, + extra_args=extra_args, + source_fn=source_fn) + finally: + os.chdir(cwd) + + # check for compile success return value + assert_equal(ret_val, 0) + + # we are not currently able to import the + # Python-Fortran interface module on Windows / + # Appveyor, even though we do get successful + # compilation on that platform with Python 3.x + if os.name != 'nt': + # check for sensible result of Fortran function; + # that means we can import the module name in Python + # and retrieve the result of the sum operation + return_check = import_module(modulename) + calc_result = return_check.foo() + assert_equal(calc_result, 15) + +def test_f2py_init_compile_failure(): + # verify an appropriate integer status + # value returned by f2py.compile() when + # invalid Fortran is provided + ret_val = numpy.f2py.compile(b"invalid") + assert_equal(ret_val, 1) + +def test_f2py_init_compile_bad_cmd(): + # verify that usage of invalid command in + # f2py.compile() returns status value of 127 + # for historic consistency with exec_command() + # error handling + + # patch the sys Python exe path temporarily to + # induce an OSError downstream + # NOTE: how bad of an idea is this patching? + try: + temp = sys.executable + sys.executable = 'does not exist' + + # the OSError should take precedence over the invalid + # Fortran + ret_val = numpy.f2py.compile(b"invalid") + + assert_equal(ret_val, 127) + finally: + sys.executable = temp diff --git a/numpy/lib/npyio.py b/numpy/lib/npyio.py index d8cfbf769..9a7b244ac 100644 --- a/numpy/lib/npyio.py +++ b/numpy/lib/npyio.py @@ -13,6 +13,7 @@ import numpy as np from . import format from ._datasource import DataSource from numpy.core.multiarray import packbits, unpackbits +from numpy.core._internal import recursive from ._iotools import ( LineSplitter, NameValidator, StringConverter, ConverterError, ConverterLockError, ConversionWarning, _is_string_like, @@ -944,7 +945,8 @@ def loadtxt(fname, dtype=float, comments='#', delimiter=None, fencoding = locale.getpreferredencoding() # not to be confused with the flatten_dtype we import... - def flatten_dtype_internal(dt): + @recursive + def flatten_dtype_internal(self, dt): """Unpack a structured data-type, and produce re-packing info.""" if dt.names is None: # If the dtype is flattened, return. @@ -964,7 +966,7 @@ def loadtxt(fname, dtype=float, comments='#', delimiter=None, packing = [] for field in dt.names: tp, bytes = dt.fields[field] - flat_dt, flat_packing = flatten_dtype_internal(tp) + flat_dt, flat_packing = self(tp) types.extend(flat_dt) # Avoid extra nesting for subarrays if tp.ndim > 0: @@ -973,7 +975,8 @@ def loadtxt(fname, dtype=float, comments='#', delimiter=None, packing.append((len(flat_dt), flat_packing)) return (types, packing) - def pack_items(items, packing): + @recursive + def pack_items(self, items, packing): """Pack items into nested lists based on re-packing info.""" if packing is None: return items[0] @@ -985,7 +988,7 @@ def loadtxt(fname, dtype=float, comments='#', delimiter=None, start = 0 ret = [] for length, subpacking in packing: - ret.append(pack_items(items[start:start+length], subpacking)) + ret.append(self(items[start:start+length], subpacking)) start += length return tuple(ret) @@ -1111,11 +1114,6 @@ def loadtxt(fname, dtype=float, comments='#', delimiter=None, finally: if fown: fh.close() - # recursive closures have a cyclic reference to themselves, which - # requires gc to collect (gh-10620). To avoid this problem, for - # performance and PyPy friendliness, we break the cycle: - flatten_dtype_internal = None - pack_items = None if X is None: X = np.array([], dtype) diff --git a/numpy/lib/tests/test_arraypad.py b/numpy/lib/tests/test_arraypad.py index 8ef9530a8..e62fccaa0 100644 --- a/numpy/lib/tests/test_arraypad.py +++ b/numpy/lib/tests/test_arraypad.py @@ -519,6 +519,21 @@ class TestConstant(object): expected = np.full(7, int64_max, dtype=np.int64) assert_array_equal(test, expected) + def test_check_object_array(self): + arr = np.empty(1, dtype=object) + obj_a = object() + arr[0] = obj_a + obj_b = object() + obj_c = object() + arr = np.pad(arr, pad_width=1, mode='constant', + constant_values=(obj_b, obj_c)) + + expected = np.empty((3,), dtype=object) + expected[0] = obj_b + expected[1] = obj_a + expected[2] = obj_c + + assert_array_equal(arr, expected) class TestLinearRamp(object): def test_check_simple(self): diff --git a/numpy/lib/tests/test_io.py b/numpy/lib/tests/test_io.py index 711415c6a..d9fed13dc 100644 --- a/numpy/lib/tests/test_io.py +++ b/numpy/lib/tests/test_io.py @@ -2412,3 +2412,9 @@ def test_load_refcount(): with assert_no_gc_cycles(): np.load(f) + + f.seek(0) + dt = [("a", 'u1', 2), ("b", 'u1', 2)] + with assert_no_gc_cycles(): + x = np.loadtxt(TextIO("0 1 2 3"), dtype=dt) + assert_equal(x, np.array([((0, 1), (2, 3))], dtype=dt)) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index 65ce967ae..a6c3e64d6 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -46,6 +46,7 @@ from numpy.compat import ( from numpy import expand_dims from numpy.core.multiarray import normalize_axis_index from numpy.core.numeric import normalize_axis_tuple +from numpy.core._internal import recursive if sys.version_info[0] >= 3: @@ -1729,12 +1730,13 @@ def mask_or(m1, m2, copy=False, shrink=True): """ - def _recursive_mask_or(m1, m2, newmask): + @recursive + def _recursive_mask_or(self, m1, m2, newmask): names = m1.dtype.names for name in names: current1 = m1[name] if current1.dtype.names is not None: - _recursive_mask_or(current1, m2[name], newmask[name]) + self(current1, m2[name], newmask[name]) else: umath.logical_or(current1, m2[name], newmask[name]) return |