Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
_iotools.py901 linesDownload Raw Back to lib
1"""A collection of functions designed to help I/O with ascii files.
2
3"""
4__docformat__ = "restructuredtext en"
5
6import itertools
7
8import numpy as np
9import numpy._core.numeric as nx
10from numpy._utils import asbytes, asunicode
11
12
13def _decode_line(line, encoding=None):
14    """Decode bytes from binary input streams.
15
16    Defaults to decoding from 'latin1'.
17
18    Parameters
19    ----------
20    line : str or bytes
21         Line to be decoded.
22    encoding : str
23         Encoding used to decode `line`.
24
25    Returns
26    -------
27    decoded_line : str
28
29    """
30    if type(line) is bytes:
31        if encoding is None:
32            encoding = "latin1"
33        line = line.decode(encoding)
34
35    return line
36
37
38def _is_string_like(obj):
39    """
40    Check whether obj behaves like a string.
41    """
42    try:
43        obj + ''
44    except (TypeError, ValueError):
45        return False
46    return True
47
48
49def _is_bytes_like(obj):
50    """
51    Check whether obj behaves like a bytes object.
52    """
53    try:
54        obj + b''
55    except (TypeError, ValueError):
56        return False
57    return True
58
59
60def has_nested_fields(ndtype):
61    """
62    Returns whether one or several fields of a dtype are nested.
63
64    Parameters
65    ----------
66    ndtype : dtype
67        Data-type of a structured array.
68
69    Raises
70    ------
71    AttributeError
72        If `ndtype` does not have a `names` attribute.
73
74    Examples
75    --------
76    >>> import numpy as np
77    >>> dt = np.dtype([('name', 'S4'), ('x', float), ('y', float)])
78    >>> np.lib._iotools.has_nested_fields(dt)
79    False
80
81    """
82    return any(ndtype[name].names is not None for name in ndtype.names or ())
83
84
85def flatten_dtype(ndtype, flatten_base=False):
86    """
87    Unpack a structured data-type by collapsing nested fields and/or fields
88    with a shape.
89
90    Note that the field names are lost.
91
92    Parameters
93    ----------
94    ndtype : dtype
95        The datatype to collapse
96    flatten_base : bool, optional
97       If True, transform a field with a shape into several fields. Default is
98       False.
99
100    Examples
101    --------
102    >>> import numpy as np
103    >>> dt = np.dtype([('name', 'S4'), ('x', float), ('y', float),
104    ...                ('block', int, (2, 3))])
105    >>> np.lib._iotools.flatten_dtype(dt)
106    [dtype('S4'), dtype('float64'), dtype('float64'), dtype('int64')]
107    >>> np.lib._iotools.flatten_dtype(dt, flatten_base=True)
108    [dtype('S4'),
109     dtype('float64'),
110     dtype('float64'),
111     dtype('int64'),
112     dtype('int64'),
113     dtype('int64'),
114     dtype('int64'),
115     dtype('int64'),
116     dtype('int64')]
117
118    """
119    names = ndtype.names
120    if names is None:
121        if flatten_base:
122            return [ndtype.base] * int(np.prod(ndtype.shape))
123        return [ndtype.base]
124    else:
125        types = []
126        for field in names:
127            info = ndtype.fields[field]
128            flat_dt = flatten_dtype(info[0], flatten_base)
129            types.extend(flat_dt)
130        return types
131
132
133class LineSplitter:
134    """
135    Object to split a string at a given delimiter or at given places.
136
137    Parameters
138    ----------
139    delimiter : str, int, or sequence of ints, optional
140        If a string, character used to delimit consecutive fields.
141        If an integer or a sequence of integers, width(s) of each field.
142    comments : str, optional
143        Character used to mark the beginning of a comment. Default is '#'.
144    autostrip : bool, optional
145        Whether to strip each individual field. Default is True.
146
147    """
148
149    def autostrip(self, method):
150        """
151        Wrapper to strip each member of the output of `method`.
152
153        Parameters
154        ----------
155        method : function
156            Function that takes a single argument and returns a sequence of
157            strings.
158
159        Returns
160        -------
161        wrapped : function
162            The result of wrapping `method`. `wrapped` takes a single input
163            argument and returns a list of strings that are stripped of
164            white-space.
165
166        """
167        return lambda input: [_.strip() for _ in method(input)]
168
169    def __init__(self, delimiter=None, comments='#', autostrip=True,
170                 encoding=None):
171        delimiter = _decode_line(delimiter)
172        comments = _decode_line(comments)
173
174        self.comments = comments
175
176        # Delimiter is a character
177        if (delimiter is None) or isinstance(delimiter, str):
178            delimiter = delimiter or None
179            _handyman = self._delimited_splitter
180        # Delimiter is a list of field widths
181        elif hasattr(delimiter, '__iter__'):
182            _handyman = self._variablewidth_splitter
183            idx = np.cumsum([0] + list(delimiter))
184            delimiter = [slice(i, j) for (i, j) in itertools.pairwise(idx)]
185        # Delimiter is a single integer
186        elif int(delimiter):
187            (_handyman, delimiter) = (
188                    self._fixedwidth_splitter, int(delimiter))
189        else:
190            (_handyman, delimiter) = (self._delimited_splitter, None)
191        self.delimiter = delimiter
192        if autostrip:
193            self._handyman = self.autostrip(_handyman)
194        else:
195            self._handyman = _handyman
196        self.encoding = encoding
197
198    def _delimited_splitter(self, line):
199        """Chop off comments, strip, and split at delimiter. """
200        if self.comments is not None:
201            line = line.split(self.comments)[0]
202        line = line.strip(" \r\n")
203        if not line:
204            return []
205        return line.split(self.delimiter)
206
207    def _fixedwidth_splitter(self, line):
208        if self.comments is not None:
209            line = line.split(self.comments)[0]
210        line = line.strip("\r\n")
211        if not line:
212            return []
213        fixed = self.delimiter
214        slices = [slice(i, i + fixed) for i in range(0, len(line), fixed)]
215        return [line[s] for s in slices]
216
217    def _variablewidth_splitter(self, line):
218        if self.comments is not None:
219            line = line.split(self.comments)[0]
220        if not line:
221            return []
222        slices = self.delimiter
223        return [line[s] for s in slices]
224
225    def __call__(self, line):
226        return self._handyman(_decode_line(line, self.encoding))
227
228
229class NameValidator:
230    """
231    Object to validate a list of strings to use as field names.
232
233    The strings are stripped of any non alphanumeric character, and spaces
234    are replaced by '_'. During instantiation, the user can define a list
235    of names to exclude, as well as a list of invalid characters. Names in
236    the exclusion list are appended a '_' character.
237
238    Once an instance has been created, it can be called with a list of
239    names, and a list of valid names will be created.  The `__call__`
240    method accepts an optional keyword "default" that sets the default name
241    in case of ambiguity. By default this is 'f', so that names will
242    default to `f0`, `f1`, etc.
243
244    Parameters
245    ----------
246    excludelist : sequence, optional
247        A list of names to exclude. This list is appended to the default
248        list ['return', 'file', 'print']. Excluded names are appended an
249        underscore: for example, `file` becomes `file_` if supplied.
250    deletechars : str, optional
251        A string combining invalid characters that must be deleted from the
252        names.
253    case_sensitive : {True, False, 'upper', 'lower'}, optional
254        * If True, field names are case-sensitive.
255        * If False or 'upper', field names are converted to upper case.
256        * If 'lower', field names are converted to lower case.
257
258        The default value is True.
259    replace_space : '_', optional
260        Character(s) used in replacement of white spaces.
261
262    Notes
263    -----
264    Calling an instance of `NameValidator` is the same as calling its
265    method `validate`.
266
267    Examples
268    --------
269    >>> import numpy as np
270    >>> validator = np.lib._iotools.NameValidator()
271    >>> validator(['file', 'field2', 'with space', 'CaSe'])
272    ('file_', 'field2', 'with_space', 'CaSe')
273
274    >>> validator = np.lib._iotools.NameValidator(excludelist=['excl'],
275    ...                                           deletechars='q',
276    ...                                           case_sensitive=False)
277    >>> validator(['excl', 'field2', 'no_q', 'with space', 'CaSe'])
278    ('EXCL', 'FIELD2', 'NO_Q', 'WITH_SPACE', 'CASE')
279
280    """
281
282    defaultexcludelist = 'return', 'file', 'print'
283    defaultdeletechars = frozenset(r"""~!@#$%^&*()-=+~\|]}[{';: /?.>,<""")
284
285    def __init__(self, excludelist=None, deletechars=None,
286                 case_sensitive=None, replace_space='_'):
287        # Process the exclusion list ..
288        if excludelist is None:
289            excludelist = []
290        excludelist.extend(self.defaultexcludelist)
291        self.excludelist = excludelist
292        # Process the list of characters to delete
293        if deletechars is None:
294            delete = set(self.defaultdeletechars)
295        else:
296            delete = set(deletechars)
297        delete.add('"')
298        self.deletechars = delete
299        # Process the case option .....
300        if (case_sensitive is None) or (case_sensitive is True):
301            self.case_converter = lambda x: x
302        elif (case_sensitive is False) or case_sensitive.startswith('u'):
303            self.case_converter = lambda x: x.upper()
304        elif case_sensitive.startswith('l'):
305            self.case_converter = lambda x: x.lower()
306        else:
307            msg = f'unrecognized case_sensitive value {case_sensitive}.'
308            raise ValueError(msg)
309
310        self.replace_space = replace_space
311
312    def validate(self, names, defaultfmt="f%i", nbfields=None):
313        """
314        Validate a list of strings as field names for a structured array.
315
316        Parameters
317        ----------
318        names : sequence of str
319            Strings to be validated.
320        defaultfmt : str, optional
321            Default format string, used if validating a given string
322            reduces its length to zero.
323        nbfields : integer, optional
324            Final number of validated names, used to expand or shrink the
325            initial list of names.
326
327        Returns
328        -------
329        validatednames : list of str
330            The list of validated field names.
331
332        Notes
333        -----
334        A `NameValidator` instance can be called directly, which is the
335        same as calling `validate`. For examples, see `NameValidator`.
336
337        """
338        # Initial checks ..............
339        if (names is None):
340            if (nbfields is None):
341                return None
342            names = []
343        if isinstance(names, str):
344            names = [names, ]
345        if nbfields is not None:
346            nbnames = len(names)
347            if (nbnames < nbfields):
348                names = list(names) + [''] * (nbfields - nbnames)
349            elif (nbnames > nbfields):
350                names = names[:nbfields]
351        # Set some shortcuts ...........
352        deletechars = self.deletechars
353        excludelist = self.excludelist
354        case_converter = self.case_converter
355        replace_space = self.replace_space
356        # Initializes some variables ...
357        validatednames = []
358        seen = {}
359        nbempty = 0
360
361        for item in names:
362            item = case_converter(item).strip()
363            if replace_space:
364                item = item.replace(' ', replace_space)
365            item = ''.join([c for c in item if c not in deletechars])
366            if item == '':
367                item = defaultfmt % nbempty
368                while item in names:
369                    nbempty += 1
370                    item = defaultfmt % nbempty
371                nbempty += 1
372            elif item in excludelist:
373                item += '_'
374            cnt = seen.get(item, 0)
375            if cnt > 0:
376                validatednames.append(item + '_%d' % cnt)
377            else:
378                validatednames.append(item)
379            seen[item] = cnt + 1
380        return tuple(validatednames)
381
382    def __call__(self, names, defaultfmt="f%i", nbfields=None):
383        return self.validate(names, defaultfmt=defaultfmt, nbfields=nbfields)
384
385
386def str2bool(value):
387    """
388    Tries to transform a string supposed to represent a boolean to a boolean.
389
390    Parameters
391    ----------
392    value : str
393        The string that is transformed to a boolean.
394
395    Returns
396    -------
397    boolval : bool
398        The boolean representation of `value`.
399
400    Raises
401    ------
402    ValueError
403        If the string is not 'True' or 'False' (case independent)
404
405    Examples
406    --------
407    >>> import numpy as np
408    >>> np.lib._iotools.str2bool('TRUE')
409    True
410    >>> np.lib._iotools.str2bool('false')
411    False
412
413    """
414    value = value.upper()
415    if value == 'TRUE':
416        return True
417    elif value == 'FALSE':
418        return False
419    else:
420        raise ValueError("Invalid boolean")
421
422
423class ConverterError(Exception):
424    """
425    Exception raised when an error occurs in a converter for string values.
426
427    """
428    pass
429
430
431class ConverterLockError(ConverterError):
432    """
433    Exception raised when an attempt is made to upgrade a locked converter.
434
435    """
436    pass
437
438
439class ConversionWarning(UserWarning):
440    """
441    Warning issued when a string converter has a problem.
442
443    Notes
444    -----
445    In `genfromtxt` a `ConversionWarning` is issued if raising exceptions
446    is explicitly suppressed with the "invalid_raise" keyword.
447
448    """
449    pass
450
451
452class StringConverter:
453    """
454    Factory class for function transforming a string into another object
455    (int, float).
456
457    After initialization, an instance can be called to transform a string
458    into another object. If the string is recognized as representing a
459    missing value, a default value is returned.
460
461    Attributes
462    ----------
463    func : function
464        Function used for the conversion.
465    default : any
466        Default value to return when the input corresponds to a missing
467        value.
468    type : type
469        Type of the output.
470    _status : int
471        Integer representing the order of the conversion.
472    _mapper : sequence of tuples
473        Sequence of tuples (dtype, function, default value) to evaluate in
474        order.
475    _locked : bool
476        Holds `locked` parameter.
477
478    Parameters
479    ----------
480    dtype_or_func : {None, dtype, function}, optional
481        If a `dtype`, specifies the input data type, used to define a basic
482        function and a default value for missing data. For example, when
483        `dtype` is float, the `func` attribute is set to `float` and the
484        default value to `np.nan`.  If a function, this function is used to
485        convert a string to another object. In this case, it is recommended
486        to give an associated default value as input.
487    default : any, optional
488        Value to return by default, that is, when the string to be
489        converted is flagged as missing. If not given, `StringConverter`
490        tries to supply a reasonable default value.
491    missing_values : {None, sequence of str}, optional
492        ``None`` or sequence of strings indicating a missing value. If ``None``
493        then missing values are indicated by empty entries. The default is
494        ``None``.
495    locked : bool, optional
496        Whether the StringConverter should be locked to prevent automatic
497        upgrade or not. Default is False.
498
499    """
500    _mapper = [(nx.bool, str2bool, False),
501               (nx.int_, int, -1),]
502
503    # On 32-bit systems, we need to make sure that we explicitly include
504    # nx.int64 since ns.int_ is nx.int32.
505    if nx.dtype(nx.int_).itemsize < nx.dtype(nx.int64).itemsize:
506        _mapper.append((nx.int64, int, -1))
507
508    _mapper.extend([(nx.float64, float, nx.nan),
509                    (nx.complex128, complex, nx.nan + 0j),
510                    (nx.longdouble, nx.longdouble, nx.nan),
511                    # If a non-default dtype is passed, fall back to generic
512                    # ones (should only be used for the converter)
513                    (nx.integer, int, -1),
514                    (nx.floating, float, nx.nan),
515                    (nx.complexfloating, complex, nx.nan + 0j),
516                    # Last, try with the string types (must be last, because
517                    # `_mapper[-1]` is used as default in some cases)
518                    (nx.str_, asunicode, '???'),
519                    (nx.bytes_, asbytes, '???'),
520                    ])
521
522    @classmethod
523    def _getdtype(cls, val):
524        """Returns the dtype of the input variable."""
525        return np.array(val).dtype
526
527    @classmethod
528    def _getsubdtype(cls, val):
529        """Returns the type of the dtype of the input variable."""
530        return np.array(val).dtype.type
531
532    @classmethod
533    def _dtypeortype(cls, dtype):
534        """Returns dtype for datetime64 and type of dtype otherwise."""
535
536        # This is a bit annoying. We want to return the "general" type in most
537        # cases (ie. "string" rather than "S10"), but we want to return the
538        # specific type for datetime64 (ie. "datetime64[us]" rather than
539        # "datetime64").
540        if dtype.type == np.datetime64:
541            return dtype
542        return dtype.type
543
544    @classmethod
545    def upgrade_mapper(cls, func, default=None):
546        """
547        Upgrade the mapper of a StringConverter by adding a new function and
548        its corresponding default.
549
550        The input function (or sequence of functions) and its associated
551        default value (if any) is inserted in penultimate position of the
552        mapper.  The corresponding type is estimated from the dtype of the
553        default value.
554
555        Parameters
556        ----------
557        func : var
558            Function, or sequence of functions
559
560        Examples
561        --------
562        >>> import dateutil.parser
563        >>> import datetime
564        >>> dateparser = dateutil.parser.parse
565        >>> defaultdate = datetime.date(2000, 1, 1)
566        >>> StringConverter.upgrade_mapper(dateparser, default=defaultdate)
567        """
568        # Func is a single functions
569        if callable(func):
570            cls._mapper.insert(-1, (cls._getsubdtype(default), func, default))
571            return
572        elif hasattr(func, '__iter__'):
573            if isinstance(func[0], (tuple, list)):
574                for _ in func:
575                    cls._mapper.insert(-1, _)
576                return
577            if default is None:
578                default = [None] * len(func)
579            else:
580                default = list(default)
581                default.append([None] * (len(func) - len(default)))
582            for fct, dft in zip(func, default):
583                cls._mapper.insert(-1, (cls._getsubdtype(dft), fct, dft))
584
585    @classmethod
586    def _find_map_entry(cls, dtype):
587        # if a converter for the specific dtype is available use that
588        for i, (deftype, func, default_def) in enumerate(cls._mapper):
589            if dtype.type == deftype:
590                return i, (deftype, func, default_def)
591
592        # otherwise find an inexact match
593        for i, (deftype, func, default_def) in enumerate(cls._mapper):
594            if np.issubdtype(dtype.type, deftype):
595                return i, (deftype, func, default_def)
596
597        raise LookupError
598
599    def __init__(self, dtype_or_func=None, default=None, missing_values=None,
600                 locked=False):
601        # Defines a lock for upgrade
602        self._locked = bool(locked)
603        # No input dtype: minimal initialization
604        if dtype_or_func is None:
605            self.func = str2bool
606            self._status = 0
607            self.default = default or False
608            dtype = np.dtype('bool')
609        else:
610            # Is the input a np.dtype ?
611            try:
612                self.func = None
613                dtype = np.dtype(dtype_or_func)
614            except TypeError:
615                # dtype_or_func must be a function, then
616                if not callable(dtype_or_func):
617                    errmsg = ("The input argument `dtype` is neither a"
618                              " function nor a dtype (got '%s' instead)")
619                    raise TypeError(errmsg % type(dtype_or_func))
620                # Set the function
621                self.func = dtype_or_func
622                # If we don't have a default, try to guess it or set it to
623                # None
624                if default is None:
625                    try:
626                        default = self.func('0')
627                    except ValueError:
628                        default = None
629                dtype = self._getdtype(default)
630
631            # find the best match in our mapper
632            try:
633                self._status, (_, func, default_def) = self._find_map_entry(dtype)
634            except LookupError:
635                # no match
636                self.default = default
637                _, func, _ = self._mapper[-1]
638                self._status = 0
639            else:
640                # use the found default only if we did not already have one
641                if default is None:
642                    self.default = default_def
643                else:
644                    self.default = default
645
646            # If the input was a dtype, set the function to the last we saw
647            if self.func is None:
648                self.func = func
649
650            # If the status is 1 (int), change the function to
651            # something more robust.
652            if self.func == self._mapper[1][1]:
653                if issubclass(dtype.type, np.uint64):
654                    self.func = np.uint64
655                elif issubclass(dtype.type, np.int64):
656                    self.func = np.int64
657                else:
658                    self.func = lambda x: int(float(x))
659        # Store the list of strings corresponding to missing values.
660        if missing_values is None:
661            self.missing_values = {''}
662        else:
663            if isinstance(missing_values, str):
664                missing_values = missing_values.split(",")
665            self.missing_values = set(list(missing_values) + [''])
666
667        self._callingfunction = self._strict_call
668        self.type = self._dtypeortype(dtype)
669        self._checked = False
670        self._initial_default = default
671
672    def _loose_call(self, value):
673        try:
674            return self.func(value)
675        except ValueError:
676            return self.default
677
678    def _strict_call(self, value):
679        try:
680
681            # We check if we can convert the value using the current function
682            new_value = self.func(value)
683
684            # In addition to having to check whether func can convert the
685            # value, we also have to make sure that we don't get overflow
686            # errors for integers.
687            if self.func is int:
688                try:
689                    np.array(value, dtype=self.type)
690                except OverflowError:
691                    raise ValueError
692
693            # We're still here so we can now return the new value
694            return new_value
695
696        except ValueError:
697            if value.strip() in self.missing_values:
698                if not self._status:
699                    self._checked = False
700                return self.default
701            raise ValueError(f"Cannot convert string '{value}'")
702
703    def __call__(self, value):
704        return self._callingfunction(value)
705
706    def _do_upgrade(self):
707        # Raise an exception if we locked the converter...
708        if self._locked:
709            errmsg = "Converter is locked and cannot be upgraded"
710            raise ConverterLockError(errmsg)
711        _statusmax = len(self._mapper)
712        # Complains if we try to upgrade by the maximum
713        _status = self._status
714        if _status == _statusmax:
715            errmsg = "Could not find a valid conversion function"
716            raise ConverterError(errmsg)
717        elif _status < _statusmax - 1:
718            _status += 1
719        self.type, self.func, default = self._mapper[_status]
720        self._status = _status
721        if self._initial_default is not None:
722            self.default = self._initial_default
723        else:
724            self.default = default
725
726    def upgrade(self, value):
727        """
728        Find the best converter for a given string, and return the result.
729
730        The supplied string `value` is converted by testing different
731        converters in order. First the `func` method of the
732        `StringConverter` instance is tried, if this fails other available
733        converters are tried.  The order in which these other converters
734        are tried is determined by the `_status` attribute of the instance.
735
736        Parameters
737        ----------
738        value : str
739            The string to convert.
740
741        Returns
742        -------
743        out : any
744            The result of converting `value` with the appropriate converter.
745
746        """
747        self._checked = True
748        try:
749            return self._strict_call(value)
750        except ValueError:
751            self._do_upgrade()
752            return self.upgrade(value)
753
754    def iterupgrade(self, value):
755        self._checked = True
756        if not hasattr(value, '__iter__'):
757            value = (value,)
758        _strict_call = self._strict_call
759        try:
760            for _m in value:
761                _strict_call(_m)
762        except ValueError:
763            self._do_upgrade()
764            self.iterupgrade(value)
765
766    def update(self, func, default=None, testing_value=None,
767               missing_values='', locked=False):
768        """
769        Set StringConverter attributes directly.
770
771        Parameters
772        ----------
773        func : function
774            Conversion function.
775        default : any, optional
776            Value to return by default, that is, when the string to be
777            converted is flagged as missing. If not given,
778            `StringConverter` tries to supply a reasonable default value.
779        testing_value : str, optional
780            A string representing a standard input value of the converter.
781            This string is used to help defining a reasonable default
782            value.
783        missing_values : {sequence of str, None}, optional
784            Sequence of strings indicating a missing value. If ``None``, then
785            the existing `missing_values` are cleared. The default is ``''``.
786        locked : bool, optional
787            Whether the StringConverter should be locked to prevent
788            automatic upgrade or not. Default is False.
789
790        Notes
791        -----
792        `update` takes the same parameters as the constructor of
793        `StringConverter`, except that `func` does not accept a `dtype`
794        whereas `dtype_or_func` in the constructor does.
795
796        """
797        self.func = func
798        self._locked = locked
799
800        # Don't reset the default to None if we can avoid it
801        if default is not None:
802            self.default = default
803            self.type = self._dtypeortype(self._getdtype(default))
804        else:
805            try:
806                tester = func(testing_value or '1')
807            except (TypeError, ValueError):
808                tester = None
809            self.type = self._dtypeortype(self._getdtype(tester))
810
811        # Add the missing values to the existing set or clear it.
812        if missing_values is None:
813            # Clear all missing values even though the ctor initializes it to
814            # set(['']) when the argument is None.
815            self.missing_values = set()
816        else:
817            if not np.iterable(missing_values):
818                missing_values = [missing_values]
819            if not all(isinstance(v, str) for v in missing_values):
820                raise TypeError("missing_values must be strings or unicode")
821            self.missing_values.update(missing_values)
822
823
824def easy_dtype(ndtype, names=None, defaultfmt="f%i", **validationargs):
825    """
826    Convenience function to create a `np.dtype` object.
827
828    The function processes the input `dtype` and matches it with the given
829    names.
830
831    Parameters
832    ----------
833    ndtype : var
834        Definition of the dtype. Can be any string or dictionary recognized
835        by the `np.dtype` function, or a sequence of types.
836    names : str or sequence, optional
837        Sequence of strings to use as field names for a structured dtype.
838        For convenience, `names` can be a string of a comma-separated list
839        of names.
840    defaultfmt : str, optional
841        Format string used to define missing names, such as ``"f%i"``
842        (default) or ``"fields_%02i"``.
843    validationargs : optional
844        A series of optional arguments used to initialize a
845        `NameValidator`.
846
847    Examples
848    --------
849    >>> import numpy as np
850    >>> np.lib._iotools.easy_dtype(float)
851    dtype('float64')
852    >>> np.lib._iotools.easy_dtype("i4, f8")
853    dtype([('f0', '<i4'), ('f1', '<f8')])
854    >>> np.lib._iotools.easy_dtype("i4, f8", defaultfmt="field_%03i")
855    dtype([('field_000', '<i4'), ('field_001', '<f8')])
856
857    >>> np.lib._iotools.easy_dtype((int, float, float), names="a,b,c")
858    dtype([('a', '<i8'), ('b', '<f8'), ('c', '<f8')])
859    >>> np.lib._iotools.easy_dtype(float, names="a,b,c")
860    dtype([('a', '<f8'), ('b', '<f8'), ('c', '<f8')])
861
862    """
863    try:
864        ndtype = np.dtype(ndtype)
865    except TypeError:
866        validate = NameValidator(**validationargs)
867        nbfields = len(ndtype)
868        if names is None:
869            names = [''] * len(ndtype)
870        elif isinstance(names, str):
871            names = names.split(",")
872        names = validate(names, nbfields=nbfields, defaultfmt=defaultfmt)
873        ndtype = np.dtype({"formats": ndtype, "names": names})
874    else:
875        # Explicit names
876        if names is not None:
877            validate = NameValidator(**validationargs)
878            if isinstance(names, str):
879                names = names.split(",")
880            # Simple dtype: repeat to match the nb of names
881            if ndtype.names is None:
882                formats = tuple([ndtype.type] * len(names))
883                names = validate(names, defaultfmt=defaultfmt)
884                ndtype = np.dtype(list(zip(names, formats)))
885            # Structured dtype: just validate the names as needed
886            else:
887                ndtype.names = validate(names, nbfields=len(ndtype.names),
888                                        defaultfmt=defaultfmt)
889        # No implicit names
890        elif ndtype.names is not None:
891            validate = NameValidator(**validationargs)
892            # Default initial names : should we change the format ?
893            numbered_names = tuple(f"f{i}" for i in range(len(ndtype.names)))
894            if ((ndtype.names == numbered_names) and (defaultfmt != "f%i")):
895                ndtype.names = validate([''] * len(ndtype.names),
896                                        defaultfmt=defaultfmt)
897            # Explicit initial names : just validate
898            else:
899                ndtype.names = validate(ndtype.names, defaultfmt=defaultfmt)
900    return ndtype
901 
codekingpro/portable-devtools · Team Ai