1""":mod:`numpy.ma..mrecords`
2
3Defines the equivalent of :class:`numpy.recarrays` for masked arrays,
4where fields can be accessed as attributes.
5Note that :class:`numpy.ma.MaskedArray` already supports structured datatypes
6and the masking of individual fields.
7
8.. moduleauthor:: Pierre Gerard-Marchant
9
10"""
11#  We should make sure that no field is called '_mask','mask','_fieldmask',
12#  or whatever restricted keywords.  An idea would be to no bother in the
13#  first place, and then rename the invalid fields with a trailing
14#  underscore. Maybe we could just overload the parser function ?
15
16import warnings
17
18import numpy as np
19from numpy import (
20        bool_, dtype, ndarray, recarray, array as narray
21        )
22from numpy.core.records import (
23        fromarrays as recfromarrays, fromrecords as recfromrecords
24        )
25
26_byteorderconv = np.core.records._byteorderconv
27
28import numpy.ma as ma
29from numpy.ma import (
30        MAError, MaskedArray, masked, nomask, masked_array, getdata,
31        getmaskarray, filled
32        )
33
34_check_fill_value = ma.core._check_fill_value
35
36
37__all__ = [
38    'MaskedRecords', 'mrecarray', 'fromarrays', 'fromrecords',
39    'fromtextfile', 'addfield',
40    ]
41
42reserved_fields = ['_data', '_mask', '_fieldmask', 'dtype']
43
44
45def _checknames(descr, names=None):
46    """
47    Checks that field names ``descr`` are not reserved keywords.
48
49    If this is the case, a default 'f%i' is substituted.  If the argument
50    `names` is not None, updates the field names to valid names.
51
52    """
53    ndescr = len(descr)
54    default_names = ['f%i' % i for i in range(ndescr)]
55    if names is None:
56        new_names = default_names
57    else:
58        if isinstance(names, (tuple, list)):
59            new_names = names
60        elif isinstance(names, str):
61            new_names = names.split(',')
62        else:
63            raise NameError(f'illegal input names {names!r}')
64        nnames = len(new_names)
65        if nnames < ndescr:
66            new_names += default_names[nnames:]
67    ndescr = []
68    for (n, d, t) in zip(new_names, default_names, descr.descr):
69        if n in reserved_fields:
70            if t[0] in reserved_fields:
71                ndescr.append((d, t[1]))
72            else:
73                ndescr.append(t)
74        else:
75            ndescr.append((n, t[1]))
76    return np.dtype(ndescr)
77
78
79def _get_fieldmask(self):
80    mdescr = [(n, '|b1') for n in self.dtype.names]
81    fdmask = np.empty(self.shape, dtype=mdescr)
82    fdmask.flat = tuple([False] * len(mdescr))
83    return fdmask
84
85
86class MaskedRecords(MaskedArray):
87    """
88
89    Attributes
90    ----------
91    _data : recarray
92        Underlying data, as a record array.
93    _mask : boolean array
94        Mask of the records. A record is masked when all its fields are
95        masked.
96    _fieldmask : boolean recarray
97        Record array of booleans, setting the mask of each individual field
98        of each record.
99    _fill_value : record
100        Filling values for each field.
101
102    """
103
104    def __new__(cls, shape, dtype=None, buf=None, offset=0, strides=None,
105                formats=None, names=None, titles=None,
106                byteorder=None, aligned=False,
107                mask=nomask, hard_mask=False, fill_value=None, keep_mask=True,
108                copy=False,
109                **options):
110
111        self = recarray.__new__(cls, shape, dtype=dtype, buf=buf, offset=offset,
112                                strides=strides, formats=formats, names=names,
113                                titles=titles, byteorder=byteorder,
114                                aligned=aligned,)
115
116        mdtype = ma.make_mask_descr(self.dtype)
117        if mask is nomask or not np.size(mask):
118            if not keep_mask:
119                self._mask = tuple([False] * len(mdtype))
120        else:
121            mask = np.array(mask, copy=copy)
122            if mask.shape != self.shape:
123                (nd, nm) = (self.size, mask.size)
124                if nm == 1:
125                    mask = np.resize(mask, self.shape)
126                elif nm == nd:
127                    mask = np.reshape(mask, self.shape)
128                else:
129                    msg = "Mask and data not compatible: data size is %i, " + \
130                          "mask size is %i."
131                    raise MAError(msg % (nd, nm))
132                copy = True
133            if not keep_mask:
134                self.__setmask__(mask)
135                self._sharedmask = True
136            else:
137                if mask.dtype == mdtype:
138                    _mask = mask
139                else:
140                    _mask = np.array([tuple([m] * len(mdtype)) for m in mask],
141                                     dtype=mdtype)
142                self._mask = _mask
143        return self
144
145    def __array_finalize__(self, obj):
146        # Make sure we have a _fieldmask by default
147        _mask = getattr(obj, '_mask', None)
148        if _mask is None:
149            objmask = getattr(obj, '_mask', nomask)
150            _dtype = ndarray.__getattribute__(self, 'dtype')
151            if objmask is nomask:
152                _mask = ma.make_mask_none(self.shape, dtype=_dtype)
153            else:
154                mdescr = ma.make_mask_descr(_dtype)
155                _mask = narray([tuple([m] * len(mdescr)) for m in objmask],
156                               dtype=mdescr).view(recarray)
157        # Update some of the attributes
158        _dict = self.__dict__
159        _dict.update(_mask=_mask)
160        self._update_from(obj)
161        if _dict['_baseclass'] == ndarray:
162            _dict['_baseclass'] = recarray
163        return
164
165    @property
166    def _data(self):
167        """
168        Returns the data as a recarray.
169
170        """
171        return ndarray.view(self, recarray)
172
173    @property
174    def _fieldmask(self):
175        """
176        Alias to mask.
177
178        """
179        return self._mask
180
181    def __len__(self):
182        """
183        Returns the length
184
185        """
186        # We have more than one record
187        if self.ndim:
188            return len(self._data)
189        # We have only one record: return the nb of fields
190        return len(self.dtype)
191
192    def __getattribute__(self, attr):
193        try:
194            return object.__getattribute__(self, attr)
195        except AttributeError:
196            # attr must be a fieldname
197            pass
198        fielddict = ndarray.__getattribute__(self, 'dtype').fields
199        try:
200            res = fielddict[attr][:2]
201        except (TypeError, KeyError) as e:
202            raise AttributeError(f'record array has no attribute {attr}') from e
203        # So far, so good
204        _localdict = ndarray.__getattribute__(self, '__dict__')
205        _data = ndarray.view(self, _localdict['_baseclass'])
206        obj = _data.getfield(*res)
207        if obj.dtype.names is not None:
208            raise NotImplementedError("MaskedRecords is currently limited to"
209                                      "simple records.")
210        # Get some special attributes
211        # Reset the object's mask
212        hasmasked = False
213        _mask = _localdict.get('_mask', None)
214        if _mask is not None:
215            try:
216                _mask = _mask[attr]
217            except IndexError:
218                # Couldn't find a mask: use the default (nomask)
219                pass
220            tp_len = len(_mask.dtype)
221            hasmasked = _mask.view((bool, ((tp_len,) if tp_len else ()))).any()
222        if (obj.shape or hasmasked):
223            obj = obj.view(MaskedArray)
224            obj._baseclass = ndarray
225            obj._isfield = True
226            obj._mask = _mask
227            # Reset the field values
228            _fill_value = _localdict.get('_fill_value', None)
229            if _fill_value is not None:
230                try:
231                    obj._fill_value = _fill_value[attr]
232                except ValueError:
233                    obj._fill_value = None
234        else:
235            obj = obj.item()
236        return obj
237
238    def __setattr__(self, attr, val):
239        """
240        Sets the attribute attr to the value val.
241
242        """
243        # Should we call __setmask__ first ?
244        if attr in ['mask', 'fieldmask']:
245            self.__setmask__(val)
246            return
247        # Create a shortcut (so that we don't have to call getattr all the time)
248        _localdict = object.__getattribute__(self, '__dict__')
249        # Check whether we're creating a new field
250        newattr = attr not in _localdict
251        try:
252            # Is attr a generic attribute ?
253            ret = object.__setattr__(self, attr, val)
254        except Exception:
255            # Not a generic attribute: exit if it's not a valid field
256            fielddict = ndarray.__getattribute__(self, 'dtype').fields or {}
257            optinfo = ndarray.__getattribute__(self, '_optinfo') or {}
258            if not (attr in fielddict or attr in optinfo):
259                raise
260        else:
261            # Get the list of names
262            fielddict = ndarray.__getattribute__(self, 'dtype').fields or {}
263            # Check the attribute
264            if attr not in fielddict:
265                return ret
266            if newattr:
267                # We just added this one or this setattr worked on an
268                # internal attribute.
269                try:
270                    object.__delattr__(self, attr)
271                except Exception:
272                    return ret
273        # Let's try to set the field
274        try:
275            res = fielddict[attr][:2]
276        except (TypeError, KeyError):
277            raise AttributeError(f'record array has no attribute {attr}')
278
279        if val is masked:
280            _fill_value = _localdict['_fill_value']
281            if _fill_value is not None:
282                dval = _localdict['_fill_value'][attr]
283            else:
284                dval = val
285            mval = True
286        else:
287            dval = filled(val)
288            mval = getmaskarray(val)
289        obj = ndarray.__getattribute__(self, '_data').setfield(dval, *res)
290        _localdict['_mask'].__setitem__(attr, mval)
291        return obj
292
293    def __getitem__(self, indx):
294        """
295        Returns all the fields sharing the same fieldname base.
296
297        The fieldname base is either `_data` or `_mask`.
298
299        """
300        _localdict = self.__dict__
301        _mask = ndarray.__getattribute__(self, '_mask')
302        _data = ndarray.view(self, _localdict['_baseclass'])
303        # We want a field
304        if isinstance(indx, str):
305            # Make sure _sharedmask is True to propagate back to _fieldmask
306            # Don't use _set_mask, there are some copies being made that
307            # break propagation Don't force the mask to nomask, that wreaks
308            # easy masking
309            obj = _data[indx].view(MaskedArray)
310            obj._mask = _mask[indx]
311            obj._sharedmask = True
312            fval = _localdict['_fill_value']
313            if fval is not None:
314                obj._fill_value = fval[indx]
315            # Force to masked if the mask is True
316            if not obj.ndim and obj._mask:
317                return masked
318            return obj
319        # We want some elements.
320        # First, the data.
321        obj = np.array(_data[indx], copy=False).view(mrecarray)
322        obj._mask = np.array(_mask[indx], copy=False).view(recarray)
323        return obj
324
325    def __setitem__(self, indx, value):
326        """
327        Sets the given record to value.
328
329        """
330        MaskedArray.__setitem__(self, indx, value)
331        if isinstance(indx, str):
332            self._mask[indx] = ma.getmaskarray(value)
333
334    def __str__(self):
335        """
336        Calculates the string representation.
337
338        """
339        if self.size > 1:
340            mstr = [f"({','.join([str(i) for i in s])})"
341                    for s in zip(*[getattr(self, f) for f in self.dtype.names])]
342            return f"[{', '.join(mstr)}]"
343        else:
344            mstr = [f"{','.join([str(i) for i in s])}"
345                    for s in zip([getattr(self, f) for f in self.dtype.names])]
346            return f"({', '.join(mstr)})"
347
348    def __repr__(self):
349        """
350        Calculates the repr representation.
351
352        """
353        _names = self.dtype.names
354        fmt = "%%%is : %%s" % (max([len(n) for n in _names]) + 4,)
355        reprstr = [fmt % (f, getattr(self, f)) for f in self.dtype.names]
356        reprstr.insert(0, 'masked_records(')
357        reprstr.extend([fmt % ('    fill_value', self.fill_value),
358                         '              )'])
359        return str("\n".join(reprstr))
360
361    def view(self, dtype=None, type=None):
362        """
363        Returns a view of the mrecarray.
364
365        """
366        # OK, basic copy-paste from MaskedArray.view.
367        if dtype is None:
368            if type is None:
369                output = ndarray.view(self)
370            else:
371                output = ndarray.view(self, type)
372        # Here again.
373        elif type is None:
374            try:
375                if issubclass(dtype, ndarray):
376                    output = ndarray.view(self, dtype)
377                    dtype = None
378                else:
379                    output = ndarray.view(self, dtype)
380            # OK, there's the change
381            except TypeError:
382                dtype = np.dtype(dtype)
383                # we need to revert to MaskedArray, but keeping the possibility
384                # of subclasses (eg, TimeSeriesRecords), so we'll force a type
385                # set to the first parent
386                if dtype.fields is None:
387                    basetype = self.__class__.__bases__[0]
388                    output = self.__array__().view(dtype, basetype)
389                    output._update_from(self)
390                else:
391                    output = ndarray.view(self, dtype)
392                output._fill_value = None
393        else:
394            output = ndarray.view(self, dtype, type)
395        # Update the mask, just like in MaskedArray.view
396        if (getattr(output, '_mask', nomask) is not nomask):
397            mdtype = ma.make_mask_descr(output.dtype)
398            output._mask = self._mask.view(mdtype, ndarray)
399            output._mask.shape = output.shape
400        return output
401
402    def harden_mask(self):
403        """
404        Forces the mask to hard.
405
406        """
407        self._hardmask = True
408
409    def soften_mask(self):
410        """
411        Forces the mask to soft
412
413        """
414        self._hardmask = False
415
416    def copy(self):
417        """
418        Returns a copy of the masked record.
419
420        """
421        copied = self._data.copy().view(type(self))
422        copied._mask = self._mask.copy()
423        return copied
424
425    def tolist(self, fill_value=None):
426        """
427        Return the data portion of the array as a list.
428
429        Data items are converted to the nearest compatible Python type.
430        Masked values are converted to fill_value. If fill_value is None,
431        the corresponding entries in the output list will be ``None``.
432
433        """
434        if fill_value is not None:
435            return self.filled(fill_value).tolist()
436        result = narray(self.filled().tolist(), dtype=object)
437        mask = narray(self._mask.tolist())
438        result[mask] = None
439        return result.tolist()
440
441    def __getstate__(self):
442        """Return the internal state of the masked array.
443
444        This is for pickling.
445
446        """
447        state = (1,
448                 self.shape,
449                 self.dtype,
450                 self.flags.fnc,
451                 self._data.tobytes(),
452                 self._mask.tobytes(),
453                 self._fill_value,
454                 )
455        return state
456
457    def __setstate__(self, state):
458        """
459        Restore the internal state of the masked array.
460
461        This is for pickling.  ``state`` is typically the output of the
462        ``__getstate__`` output, and is a 5-tuple:
463
464        - class name
465        - a tuple giving the shape of the data
466        - a typecode for the data
467        - a binary string for the data
468        - a binary string for the mask.
469
470        """
471        (ver, shp, typ, isf, raw, msk, flv) = state
472        ndarray.__setstate__(self, (shp, typ, isf, raw))
473        mdtype = dtype([(k, bool_) for (k, _) in self.dtype.descr])
474        self.__dict__['_mask'].__setstate__((shp, mdtype, isf, msk))
475        self.fill_value = flv
476
477    def __reduce__(self):
478        """
479        Return a 3-tuple for pickling a MaskedArray.
480
481        """
482        return (_mrreconstruct,
483                (self.__class__, self._baseclass, (0,), 'b',),
484                self.__getstate__())
485
486def _mrreconstruct(subtype, baseclass, baseshape, basetype,):
487    """
488    Build a new MaskedArray from the information stored in a pickle.
489
490    """
491    _data = ndarray.__new__(baseclass, baseshape, basetype).view(subtype)
492    _mask = ndarray.__new__(ndarray, baseshape, 'b1')
493    return subtype.__new__(subtype, _data, mask=_mask, dtype=basetype,)
494
495mrecarray = MaskedRecords
496
497
498###############################################################################
499#                             Constructors                                    #
500###############################################################################
501
502
503def fromarrays(arraylist, dtype=None, shape=None, formats=None,
504               names=None, titles=None, aligned=False, byteorder=None,
505               fill_value=None):
506    """
507    Creates a mrecarray from a (flat) list of masked arrays.
508
509    Parameters
510    ----------
511    arraylist : sequence
512        A list of (masked) arrays. Each element of the sequence is first converted
513        to a masked array if needed. If a 2D array is passed as argument, it is
514        processed line by line
515    dtype : {None, dtype}, optional
516        Data type descriptor.
517    shape : {None, integer}, optional
518        Number of records. If None, shape is defined from the shape of the
519        first array in the list.
520    formats : {None, sequence}, optional
521        Sequence of formats for each individual field. If None, the formats will
522        be autodetected by inspecting the fields and selecting the highest dtype
523        possible.
524    names : {None, sequence}, optional
525        Sequence of the names of each field.
526    fill_value : {None, sequence}, optional
527        Sequence of data to be used as filling values.
528
529    Notes
530    -----
531    Lists of tuples should be preferred over lists of lists for faster processing.
532
533    """
534    datalist = [getdata(x) for x in arraylist]
535    masklist = [np.atleast_1d(getmaskarray(x)) for x in arraylist]
536    _array = recfromarrays(datalist,
537                           dtype=dtype, shape=shape, formats=formats,
538                           names=names, titles=titles, aligned=aligned,
539                           byteorder=byteorder).view(mrecarray)
540    _array._mask.flat = list(zip(*masklist))
541    if fill_value is not None:
542        _array.fill_value = fill_value
543    return _array
544
545
546def fromrecords(reclist, dtype=None, shape=None, formats=None, names=None,
547                titles=None, aligned=False, byteorder=None,
548                fill_value=None, mask=nomask):
549    """
550    Creates a MaskedRecords from a list of records.
551
552    Parameters
553    ----------
554    reclist : sequence
555        A list of records. Each element of the sequence is first converted
556        to a masked array if needed. If a 2D array is passed as argument, it is
557        processed line by line
558    dtype : {None, dtype}, optional
559        Data type descriptor.
560    shape : {None,int}, optional
561        Number of records. If None, ``shape`` is defined from the shape of the
562        first array in the list.
563    formats : {None, sequence}, optional
564        Sequence of formats for each individual field. If None, the formats will
565        be autodetected by inspecting the fields and selecting the highest dtype
566        possible.
567    names : {None, sequence}, optional
568        Sequence of the names of each field.
569    fill_value : {None, sequence}, optional
570        Sequence of data to be used as filling values.
571    mask : {nomask, sequence}, optional.
572        External mask to apply on the data.
573
574    Notes
575    -----
576    Lists of tuples should be preferred over lists of lists for faster processing.
577
578    """
579    # Grab the initial _fieldmask, if needed:
580    _mask = getattr(reclist, '_mask', None)
581    # Get the list of records.
582    if isinstance(reclist, ndarray):
583        # Make sure we don't have some hidden mask
584        if isinstance(reclist, MaskedArray):
585            reclist = reclist.filled().view(ndarray)
586        # Grab the initial dtype, just in case
587        if dtype is None:
588            dtype = reclist.dtype
589        reclist = reclist.tolist()
590    mrec = recfromrecords(reclist, dtype=dtype, shape=shape, formats=formats,
591                          names=names, titles=titles,
592                          aligned=aligned, byteorder=byteorder).view(mrecarray)
593    # Set the fill_value if needed
594    if fill_value is not None:
595        mrec.fill_value = fill_value
596    # Now, let's deal w/ the mask
597    if mask is not nomask:
598        mask = np.array(mask, copy=False)
599        maskrecordlength = len(mask.dtype)
600        if maskrecordlength:
601            mrec._mask.flat = mask
602        elif mask.ndim == 2:
603            mrec._mask.flat = [tuple(m) for m in mask]
604        else:
605            mrec.__setmask__(mask)
606    if _mask is not None:
607        mrec._mask[:] = _mask
608    return mrec
609
610
611def _guessvartypes(arr):
612    """
613    Tries to guess the dtypes of the str_ ndarray `arr`.
614
615    Guesses by testing element-wise conversion. Returns a list of dtypes.
616    The array is first converted to ndarray. If the array is 2D, the test
617    is performed on the first line. An exception is raised if the file is
618    3D or more.
619
620    """
621    vartypes = []
622    arr = np.asarray(arr)
623    if arr.ndim == 2:
624        arr = arr[0]
625    elif arr.ndim > 2:
626        raise ValueError("The array should be 2D at most!")
627    # Start the conversion loop.
628    for f in arr:
629        try:
630            int(f)
631        except (ValueError, TypeError):
632            try:
633                float(f)
634            except (ValueError, TypeError):
635                try:
636                    complex(f)
637                except (ValueError, TypeError):
638                    vartypes.append(arr.dtype)
639                else:
640                    vartypes.append(np.dtype(complex))
641            else:
642                vartypes.append(np.dtype(float))
643        else:
644            vartypes.append(np.dtype(int))
645    return vartypes
646
647
648def openfile(fname):
649    """
650    Opens the file handle of file `fname`.
651
652    """
653    # A file handle
654    if hasattr(fname, 'readline'):
655        return fname
656    # Try to open the file and guess its type
657    try:
658        f = open(fname)
659    except IOError:
660        raise IOError(f"No such file: '{fname}'")
661    if f.readline()[:2] != "\\x":
662        f.seek(0, 0)
663        return f
664    f.close()
665    raise NotImplementedError("Wow, binary file")
666
667
668def fromtextfile(fname, delimitor=None, commentchar='#', missingchar='',
669                 varnames=None, vartypes=None):
670    """
671    Creates a mrecarray from data stored in the file `filename`.
672
673    Parameters
674    ----------
675    fname : {file name/handle}
676        Handle of an opened file.
677    delimitor : {None, string}, optional
678        Alphanumeric character used to separate columns in the file.
679        If None, any (group of) white spacestring(s) will be used.
680    commentchar : {'#', string}, optional
681        Alphanumeric character used to mark the start of a comment.
682    missingchar : {'', string}, optional
683        String indicating missing data, and used to create the masks.
684    varnames : {None, sequence}, optional
685        Sequence of the variable names. If None, a list will be created from
686        the first non empty line of the file.
687    vartypes : {None, sequence}, optional
688        Sequence of the variables dtypes. If None, it will be estimated from
689        the first non-commented line.
690
691
692    Ultra simple: the varnames are in the header, one line"""
693    # Try to open the file.
694    ftext = openfile(fname)
695
696    # Get the first non-empty line as the varnames
697    while True:
698        line = ftext.readline()
699        firstline = line[:line.find(commentchar)].strip()
700        _varnames = firstline.split(delimitor)
701        if len(_varnames) > 1:
702            break
703    if varnames is None:
704        varnames = _varnames
705
706    # Get the data.
707    _variables = masked_array([line.strip().split(delimitor) for line in ftext
708                               if line[0] != commentchar and len(line) > 1])
709    (_, nfields) = _variables.shape
710    ftext.close()
711
712    # Try to guess the dtype.
713    if vartypes is None:
714        vartypes = _guessvartypes(_variables[0])
715    else:
716        vartypes = [np.dtype(v) for v in vartypes]
717        if len(vartypes) != nfields:
718            msg = "Attempting to %i dtypes for %i fields!"
719            msg += " Reverting to default."
720            warnings.warn(msg % (len(vartypes), nfields), stacklevel=2)
721            vartypes = _guessvartypes(_variables[0])
722
723    # Construct the descriptor.
724    mdescr = [(n, f) for (n, f) in zip(varnames, vartypes)]
725    mfillv = [ma.default_fill_value(f) for f in vartypes]
726
727    # Get the data and the mask.
728    # We just need a list of masked_arrays. It's easier to create it like that:
729    _mask = (_variables.T == missingchar)
730    _datalist = [masked_array(a, mask=m, dtype=t, fill_value=f)
731                 for (a, m, t, f) in zip(_variables.T, _mask, vartypes, mfillv)]
732
733    return fromarrays(_datalist, dtype=mdescr)
734
735
736def addfield(mrecord, newfield, newfieldname=None):
737    """Adds a new field to the masked record array
738
739    Uses `newfield` as data and `newfieldname` as name. If `newfieldname`
740    is None, the new field name is set to 'fi', where `i` is the number of
741    existing fields.
742
743    """
744    _data = mrecord._data
745    _mask = mrecord._mask
746    if newfieldname is None or newfieldname in reserved_fields:
747        newfieldname = 'f%i' % len(_data.dtype)
748    newfield = ma.array(newfield)
749    # Get the new data.
750    # Create a new empty recarray
751    newdtype = np.dtype(_data.dtype.descr + [(newfieldname, newfield.dtype)])
752    newdata = recarray(_data.shape, newdtype)
753    # Add the existing field
754    [newdata.setfield(_data.getfield(*f), *f)
755         for f in _data.dtype.fields.values()]
756    # Add the new field
757    newdata.setfield(newfield._data, *newdata.dtype.fields[newfieldname])
758    newdata = newdata.view(MaskedRecords)
759    # Get the new mask
760    # Create a new empty recarray
761    newmdtype = np.dtype([(n, bool_) for n in newdtype.names])
762    newmask = recarray(_data.shape, newmdtype)
763    # Add the old masks
764    [newmask.setfield(_mask.getfield(*f), *f)
765         for f in _mask.dtype.fields.values()]
766    # Add the mask of the new field
767    newmask.setfield(getmaskarray(newfield),
768                     *newmask.dtype.fields[newfieldname])
769    newdata._mask = newmask
770    return newdata
771