1# -*- coding: utf-8 -*-
2
3########################################################################
4#
5# License: BSD
6# Created: December 30, 2006
7# Author: Ivan Vilata i Balaguer - ivan at selidor dot net
8#
9# $Id$
10#
11########################################################################
12
13"""Utilities for handling different array flavors in PyTables.
14
15Variables
16=========
17
18`__docformat`__
19    The format of documentation strings in this module.
20`internal_flavor`
21    The flavor used internally by PyTables.
22`all_flavors`
23    List of all flavors available to PyTables.
24`alias_map`
25    Maps old flavor names to the most similar current flavor.
26`description_map`
27    Maps flavors to short descriptions of their supported objects.
28`identifier_map`
29    Maps flavors to functions that can identify their objects.
30
31    The function associated with a given flavor will return a true
32    value if the object passed to it can be identified as being of
33    that flavor.
34
35    See the `flavor_of()` function for a friendlier interface to
36    flavor identification.
37
38`converter_map`
39    Maps (source, destination) flavor pairs to converter functions.
40
41    Converter functions get an array of the source flavor and return
42    an array of the destination flavor.
43
44    See the `array_of_flavor()` and `flavor_to_flavor()` functions for
45    friendlier interfaces to flavor conversion.
46
47"""
48
49# Imports
50# =======
51import warnings
52
53from .exceptions import FlavorError, FlavorWarning
54
55
56# Public variables
57# ================
58__docformat__ = 'reStructuredText'
59"""The format of documentation strings in this module."""
60
61internal_flavor = 'numpy'
62"""The flavor used internally by PyTables."""
63
64# This is very slightly slower than a set for a small number of values
65# in terms of (infrequent) lookup time, but allows `flavor_of()`
66# (which may be called much more frequently) to check for flavors in
67# order, beginning with the most common one.
68all_flavors = []  # filled as flavors are registered
69"""List of all flavors available to PyTables."""
70
71alias_map = {}  # filled as flavors are registered
72"""Maps old flavor names to the most similar current flavor."""
73
74description_map = {}  # filled as flavors are registered
75"""Maps flavors to short descriptions of their supported objects."""
76
77identifier_map = {}  # filled as flavors are registered
78"""Maps flavors to functions that can identify their objects.
79
80The function associated with a given flavor will return a true value
81if the object passed to it can be identified as being of that flavor.
82
83See the `flavor_of()` function for a friendlier interface to flavor
84identification.
85"""
86
87converter_map = {}  # filled as flavors are registered
88"""Maps (source, destination) flavor pairs to converter functions.
89
90Converter functions get an array of the source flavor and return an
91array of the destination flavor.
92
93See the `array_of_flavor()` and `flavor_to_flavor()` functions for
94friendlier interfaces to flavor conversion.
95"""
96
97
98# Public functions
99# ================
100def check_flavor(flavor):
101    """Raise a ``FlavorError`` if the `flavor` is not valid."""
102
103    if flavor not in all_flavors:
104        available_flavs = ", ".join(flav for flav in all_flavors)
105        raise FlavorError(
106            "flavor ``%s`` is unsupported or unavailable; "
107            "available flavors in this system are: %s"
108            % (flavor, available_flavs))
109
110
111def array_of_flavor2(array, src_flavor, dst_flavor):
112    """Get a version of the given `array` in a different flavor.
113
114    The input `array` must be of the given `src_flavor`, and the
115    returned array will be of the indicated `dst_flavor`.  Both
116    flavors may be the same, but it is not guaranteed that the
117    returned array will be the same object as the input one in this
118    case.
119
120    If the conversion is not supported, a ``FlavorError`` is raised.
121
122    """
123
124    convkey = (src_flavor, dst_flavor)
125    if convkey not in converter_map:
126        raise FlavorError("conversion from flavor ``%s`` to flavor ``%s`` "
127                          "is unsupported or unavailable in this system"
128                          % (src_flavor, dst_flavor))
129
130    convfunc = converter_map[convkey]
131    return convfunc(array)
132
133
134def flavor_to_flavor(array, src_flavor, dst_flavor):
135    """Get a version of the given `array` in a different flavor.
136
137    The input `array` must be of the given `src_flavor`, and the
138    returned array will be of the indicated `dst_flavor` (see below
139    for an exception to this).  Both flavors may be the same, but it
140    is not guaranteed that the returned array will be the same object
141    as the input one in this case.
142
143    If the conversion is not supported, a `FlavorWarning` is issued
144    and the input `array` is returned as is.
145
146    """
147
148    try:
149        return array_of_flavor2(array, src_flavor, dst_flavor)
150    except FlavorError as fe:
151        warnings.warn("%s; returning an object of the ``%s`` flavor instead"
152                      % (fe.args[0], src_flavor), FlavorWarning)
153        return array
154
155
156def internal_to_flavor(array, dst_flavor):
157    """Get a version of the given `array` in a different `dst_flavor`.
158
159    The input `array` must be of the internal flavor, and the returned
160    array will be of the given `dst_flavor`.  See `flavor_to_flavor()`
161    for more information.
162
163    """
164
165    return flavor_to_flavor(array, internal_flavor, dst_flavor)
166
167
168def array_as_internal(array, src_flavor):
169    """Get a version of the given `array` in the internal flavor.
170
171    The input `array` must be of the given `src_flavor`, and the
172    returned array will be of the internal flavor.
173
174    If the conversion is not supported, a ``FlavorError`` is raised.
175
176    """
177
178    return array_of_flavor2(array, src_flavor, internal_flavor)
179
180
181def flavor_of(array):
182    """Identify the flavor of a given `array`.
183
184    If the `array` can not be matched with any flavor, a ``TypeError``
185    is raised.
186
187    """
188
189    for flavor in all_flavors:
190        if identifier_map[flavor](array):
191            return flavor
192    type_name = type(array).__name__
193    supported_descs = "; ".join(description_map[fl] for fl in all_flavors)
194    raise TypeError(
195        "objects of type ``%s`` are not supported in this context, sorry; "
196        "supported objects are: %s" % (type_name, supported_descs))
197
198
199def array_of_flavor(array, dst_flavor):
200    """Get a version of the given `array` in a different `dst_flavor`.
201
202    The flavor of the input `array` is guessed, and the returned array
203    will be of the given `dst_flavor`.
204
205    If the conversion is not supported, a ``FlavorError`` is raised.
206
207    """
208
209    return array_of_flavor2(array, flavor_of(array), dst_flavor)
210
211
212def restrict_flavors(keep=['python']):
213    """Disable all flavors except those in keep.
214
215    Providing an empty keep sequence implies disabling all flavors (but the
216    internal one).  If the sequence is not specified, only optional flavors are
217    disabled.
218
219    .. important:: Once you disable a flavor, it can not be enabled again.
220
221    """
222
223    keep = set(keep).union([internal_flavor])
224    remove = set(all_flavors).difference(keep)
225    for flavor in remove:
226        _disable_flavor(flavor)
227
228
229# Flavor registration
230# ===================
231#
232# The order in which flavors appear in `all_flavors` determines the
233# order in which they will be tested for by `flavor_of()`, so place
234# most frequent flavors first.
235import numpy
236all_flavors.append('numpy')  # this is the internal flavor
237
238all_flavors.append('python')  # this is always supported
239
240
241def _register_aliases():
242    """Register aliases of *available* flavors."""
243
244    for flavor in all_flavors:
245        aliases = eval('_%s_aliases' % flavor)
246        for alias in aliases:
247            alias_map[alias] = flavor
248
249
250def _register_descriptions():
251    """Register descriptions of *available* flavors."""
252    for flavor in all_flavors:
253        description_map[flavor] = eval('_%s_desc' % flavor)
254
255
256def _register_identifiers():
257    """Register identifier functions of *available* flavors."""
258
259    for flavor in all_flavors:
260        identifier_map[flavor] = eval('_is_%s' % flavor)
261
262
263def _register_converters():
264    """Register converter functions between *available* flavors."""
265
266    def identity(array):
267        return array
268    for src_flavor in all_flavors:
269        for dst_flavor in all_flavors:
270            # Converters with the same source and destination flavor
271            # are used when available, since they may perform some
272            # optimizations on the resulting array (e.g. making it
273            # contiguous).  Otherwise, an identity function is used.
274            convfunc = None
275            try:
276                convfunc = eval('_conv_%s_to_%s' % (src_flavor, dst_flavor))
277            except NameError:
278                if src_flavor == dst_flavor:
279                    convfunc = identity
280            if convfunc:
281                converter_map[(src_flavor, dst_flavor)] = convfunc
282
283
284def _register_all():
285    """Register all *available* flavors."""
286
287    _register_aliases()
288    _register_descriptions()
289    _register_identifiers()
290    _register_converters()
291
292
293def _deregister_aliases(flavor):
294    """Deregister aliases of a given `flavor` (no checks)."""
295
296    rm_aliases = []
297    for (an_alias, a_flavor) in alias_map.items():
298        if a_flavor == flavor:
299            rm_aliases.append(an_alias)
300    for an_alias in rm_aliases:
301        del alias_map[an_alias]
302
303
304def _deregister_description(flavor):
305    """Deregister description of a given `flavor` (no checks)."""
306
307    del description_map[flavor]
308
309
310def _deregister_identifier(flavor):
311    """Deregister identifier function of a given `flavor` (no checks)."""
312
313    del identifier_map[flavor]
314
315
316def _deregister_converters(flavor):
317    """Deregister converter functions of a given `flavor` (no checks)."""
318
319    rm_flavor_pairs = []
320    for flavor_pair in converter_map:
321        if flavor in flavor_pair:
322            rm_flavor_pairs.append(flavor_pair)
323    for flavor_pair in rm_flavor_pairs:
324        del converter_map[flavor_pair]
325
326
327def _disable_flavor(flavor):
328    """Completely disable the given `flavor` (no checks)."""
329
330    _deregister_aliases(flavor)
331    _deregister_description(flavor)
332    _deregister_identifier(flavor)
333    _deregister_converters(flavor)
334    all_flavors.remove(flavor)
335
336
337# Implementation of flavors
338# =========================
339_python_aliases = [
340    'List', 'Tuple',
341    'Int', 'Float', 'String',
342    'VLString', 'Object',
343]
344_python_desc = ("homogeneous list or tuple, "
345                "integer, float, complex or bytes")
346
347
348def _is_python(array):
349    return isinstance(array, (tuple, list, int, float, complex, bytes))
350
351_numpy_aliases = []
352_numpy_desc = "NumPy array, record or scalar"
353
354
355def _is_numpy(array):
356    return isinstance(array, (numpy.ndarray, numpy.generic))
357
358
359def _numpy_contiguous(convfunc):
360    """Decorate `convfunc` to return a *contiguous* NumPy array.
361
362    Note: When arrays are 0-strided, the copy is avoided.  This allows
363    to use `array` to still carry info about the dtype and shape.
364    """
365
366    def conv_to_numpy(array):
367        nparr = convfunc(array)
368        if (hasattr(nparr, 'flags') and
369            not nparr.flags.contiguous and
370            sum(nparr.strides) != 0):
371            nparr = nparr.copy()  # copying the array makes it contiguous
372        return nparr
373    conv_to_numpy.__name__ = convfunc.__name__
374    conv_to_numpy.__doc__ = convfunc.__doc__
375    return conv_to_numpy
376
377
378@_numpy_contiguous
379def _conv_numpy_to_numpy(array):
380    # Passes contiguous arrays through and converts scalars into
381    # scalar arrays.
382    nparr = numpy.asarray(array)
383    if nparr.dtype.kind == 'U':
384        # from Python 3 loads of common strings are disguised as Unicode
385        try:
386            # try to convert to basic 'S' type
387            return nparr.astype('S')
388        except UnicodeEncodeError:
389            pass  # pass on true Unicode arrays downstream in case it can be handled in the future
390    return nparr
391
392
393@_numpy_contiguous
394def _conv_python_to_numpy(array):
395    nparr = numpy.array(array)
396    if nparr.dtype.kind == 'U':
397        # from Python 3 loads of common strings are disguised as Unicode
398        try:
399            # try to convert to basic 'S' type
400            return nparr.astype('S')
401        except UnicodeEncodeError:
402            pass  # pass on true Unicode arrays downstream in case it can be handled in the future
403    return nparr
404
405
406def _conv_numpy_to_python(array):
407    if array.shape != ():
408        # Lists are the default for returning multidimensional objects
409        array = array.tolist()
410    else:
411        # 0-dim or scalar case
412        array = array.item()
413    return array
414
415# Now register everything related with *available* flavors.
416_register_all()
417
418
419# Main part
420# =========
421def _test():
422    """Run ``doctest`` on this module."""
423
424    import doctest
425    doctest.testmod()
426
427
428if __name__ == '__main__':
429    _test()
430