1""" pydevd_vars deals with variables:
2    resolution/conversion to XML.
3"""
4import math
5import pickle
6
7from _pydev_bundle.pydev_imports import quote
8from _pydev_imps._pydev_saved_modules import thread
9from _pydevd_bundle.pydevd_constants import get_frame, get_current_thread_id, xrange, NUMPY_NUMERIC_TYPES, NUMPY_FLOATING_POINT_TYPES
10from _pydevd_bundle.pydevd_custom_frames import get_custom_frame
11from _pydevd_bundle.pydevd_xml import ExceptionOnEvaluate, get_type, var_to_xml
12
13try:
14    from StringIO import StringIO
15except ImportError:
16    from io import StringIO
17import sys  # @Reimport
18
19try:
20    from collections import OrderedDict
21except:
22    OrderedDict = dict
23
24from _pydev_imps._pydev_saved_modules import threading
25import traceback
26from _pydevd_bundle import pydevd_save_locals
27from _pydev_bundle.pydev_imports import Exec, execfile
28from _pydevd_bundle.pydevd_utils import to_string, VariableWithOffset
29
30SENTINEL_VALUE = []
31DEFAULT_DF_FORMAT = "s"
32
33# ------------------------------------------------------------------------------------------------------ class for errors
34
35class VariableError(RuntimeError): pass
36
37
38class FrameNotFoundError(RuntimeError): pass
39
40
41def _iter_frames(initialFrame):
42    '''NO-YIELD VERSION: Iterates through all the frames starting at the specified frame (which will be the first returned item)'''
43    # cannot use yield
44    frames = []
45
46    while initialFrame is not None:
47        frames.append(initialFrame)
48        initialFrame = initialFrame.f_back
49
50    return frames
51
52
53def dump_frames(thread_id):
54    sys.stdout.write('dumping frames\n')
55    if thread_id != get_current_thread_id(threading.currentThread()):
56        raise VariableError("find_frame: must execute on same thread")
57
58    curFrame = get_frame()
59    for frame in _iter_frames(curFrame):
60        sys.stdout.write('%s\n' % pickle.dumps(frame))
61
62
63# ===============================================================================
64# AdditionalFramesContainer
65# ===============================================================================
66class AdditionalFramesContainer:
67    lock = thread.allocate_lock()
68    additional_frames = {}  # dict of dicts
69
70
71def add_additional_frame_by_id(thread_id, frames_by_id):
72    AdditionalFramesContainer.additional_frames[thread_id] = frames_by_id
73
74
75addAdditionalFrameById = add_additional_frame_by_id  # Backward compatibility
76
77
78def remove_additional_frame_by_id(thread_id):
79    del AdditionalFramesContainer.additional_frames[thread_id]
80
81
82removeAdditionalFrameById = remove_additional_frame_by_id  # Backward compatibility
83
84
85def has_additional_frames_by_id(thread_id):
86    return thread_id in AdditionalFramesContainer.additional_frames
87
88
89def get_additional_frames_by_id(thread_id):
90    return AdditionalFramesContainer.additional_frames.get(thread_id)
91
92
93def find_frame(thread_id, frame_id):
94    """ returns a frame on the thread that has a given frame_id """
95    try:
96        curr_thread_id = get_current_thread_id(threading.currentThread())
97        if thread_id != curr_thread_id:
98            try:
99                return get_custom_frame(thread_id, frame_id)  # I.e.: thread_id could be a stackless frame id + thread_id.
100            except:
101                pass
102
103            raise VariableError("find_frame: must execute on same thread (%s != %s)" % (thread_id, curr_thread_id))
104
105        lookingFor = int(frame_id)
106
107        if AdditionalFramesContainer.additional_frames:
108            if thread_id in AdditionalFramesContainer.additional_frames:
109                frame = AdditionalFramesContainer.additional_frames[thread_id].get(lookingFor)
110
111                if frame is not None:
112                    return frame
113
114        curFrame = get_frame()
115        if frame_id == "*":
116            return curFrame  # any frame is specified with "*"
117
118        frameFound = None
119
120        for frame in _iter_frames(curFrame):
121            if lookingFor == id(frame):
122                frameFound = frame
123                del frame
124                break
125
126            del frame
127
128        # Important: python can hold a reference to the frame from the current context
129        # if an exception is raised, so, if we don't explicitly add those deletes
130        # we might have those variables living much more than we'd want to.
131
132        # I.e.: sys.exc_info holding reference to frame that raises exception (so, other places
133        # need to call sys.exc_clear())
134        del curFrame
135
136        if frameFound is None:
137            msgFrames = ''
138            i = 0
139
140            for frame in _iter_frames(get_frame()):
141                i += 1
142                msgFrames += str(id(frame))
143                if i % 5 == 0:
144                    msgFrames += '\n'
145                else:
146                    msgFrames += '  -  '
147
148# Note: commented this error message out (it may commonly happen
149# if a message asking for a frame is issued while a thread is paused
150# but the thread starts running before the message is actually
151# handled).
152# Leaving code to uncomment during tests.
153#             err_msg = '''find_frame: frame not found.
154#     Looking for thread_id:%s, frame_id:%s
155#     Current     thread_id:%s, available frames:
156#     %s\n
157#     ''' % (thread_id, lookingFor, curr_thread_id, msgFrames)
158#
159#             sys.stderr.write(err_msg)
160            return None
161
162        return frameFound
163    except:
164        import traceback
165        traceback.print_exc()
166        return None
167
168
169def getVariable(thread_id, frame_id, scope, attrs):
170    """
171    returns the value of a variable
172
173    :scope: can be BY_ID, EXPRESSION, GLOBAL, LOCAL, FRAME
174
175    BY_ID means we'll traverse the list of all objects alive to get the object.
176
177    :attrs: after reaching the proper scope, we have to get the attributes until we find
178            the proper location (i.e.: obj\tattr1\tattr2).
179
180    :note: when BY_ID is used, the frame_id is considered the id of the object to find and
181           not the frame (as we don't care about the frame in this case).
182    """
183    if scope == 'BY_ID':
184        if thread_id != get_current_thread_id(threading.currentThread()):
185            raise VariableError("getVariable: must execute on same thread")
186
187        try:
188            import gc
189            objects = gc.get_objects()
190        except:
191            pass  # Not all python variants have it.
192        else:
193            frame_id = int(frame_id)
194            for var in objects:
195                if id(var) == frame_id:
196                    if attrs is not None:
197                        attrList = attrs.split('\t')
198                        for k in attrList:
199                            _type, _typeName, resolver = get_type(var)
200                            var = resolver.resolve(var, k)
201
202                    return var
203
204        # If it didn't return previously, we coudn't find it by id (i.e.: alrceady garbage collected).
205        sys.stderr.write('Unable to find object with id: %s\n' % (frame_id,))
206        return None
207
208    frame = find_frame(thread_id, frame_id)
209    if frame is None:
210        return {}
211
212    if attrs is not None:
213        attrList = attrs.split('\t')
214    else:
215        attrList = []
216
217    for attr in attrList:
218        attr.replace("@_@TAB_CHAR@_@", '\t')
219
220    if scope == 'EXPRESSION':
221        for count in xrange(len(attrList)):
222            if count == 0:
223                # An Expression can be in any scope (globals/locals), therefore it needs to evaluated as an expression
224                var = evaluate_expression(thread_id, frame_id, attrList[count], False)
225            else:
226                _type, _typeName, resolver = get_type(var)
227                var = resolver.resolve(var, attrList[count])
228    else:
229        if scope == "GLOBAL":
230            var = frame.f_globals
231            del attrList[0]  # globals are special, and they get a single dummy unused attribute
232        else:
233            # in a frame access both locals and globals as Python does
234            var = {}
235            var.update(frame.f_globals)
236            var.update(frame.f_locals)
237
238        for k in attrList:
239            _type, _typeName, resolver = get_type(var)
240            var = resolver.resolve(var, k)
241
242    return var
243
244
245def get_offset(attrs):
246    """
247    Extract offset from the given attributes.
248
249    :param attrs: The string of a compound variable fields split by tabs.
250      If an offset is given, it must go the first element.
251    :return: The value of offset if given or 0.
252    """
253    offset = 0
254    if attrs is not None:
255        try:
256            offset = int(attrs.split('\t')[0])
257        except ValueError:
258            pass
259    return offset
260
261
262def resolve_compound_variable_fields(thread_id, frame_id, scope, attrs):
263    """
264    Resolve compound variable in debugger scopes by its name and attributes
265
266    :param thread_id: id of the variable's thread
267    :param frame_id: id of the variable's frame
268    :param scope: can be BY_ID, EXPRESSION, GLOBAL, LOCAL, FRAME
269    :param attrs: after reaching the proper scope, we have to get the attributes until we find
270            the proper location (i.e.: obj\tattr1\tattr2)
271    :return: a dictionary of variables's fields
272
273    :note: PyCharm supports progressive loading of large collections and uses the `attrs`
274           parameter to pass the offset, e.g. 300\t\\obj\tattr1\tattr2 should return
275           the value of attr2 starting from the 300th element. This hack makes it possible
276           to add the support of progressive loading without extending of the protocol.
277    """
278    offset = get_offset(attrs)
279
280    orig_attrs, attrs = attrs, attrs.split('\t', 1)[1] if offset else attrs
281
282    var = getVariable(thread_id, frame_id, scope, attrs)
283
284    try:
285        _type, _typeName, resolver = get_type(var)
286        return _typeName, resolver.get_dictionary(VariableWithOffset(var, offset) if offset else var)
287    except:
288        sys.stderr.write('Error evaluating: thread_id: %s\nframe_id: %s\nscope: %s\nattrs: %s\n' % (
289            thread_id, frame_id, scope, orig_attrs,))
290        traceback.print_exc()
291
292
293def resolve_var_object(var, attrs):
294    """
295    Resolve variable's attribute
296
297    :param var: an object of variable
298    :param attrs: a sequence of variable's attributes separated by \t (i.e.: obj\tattr1\tattr2)
299    :return: a value of resolved variable's attribute
300    """
301    if attrs is not None:
302        attr_list = attrs.split('\t')
303    else:
304        attr_list = []
305    for k in attr_list:
306        type, _typeName, resolver = get_type(var)
307        var = resolver.resolve(var, k)
308    return var
309
310
311def resolve_compound_var_object_fields(var, attrs):
312    """
313    Resolve compound variable by its object and attributes
314
315    :param var: an object of variable
316    :param attrs: a sequence of variable's attributes separated by \t (i.e.: obj\tattr1\tattr2)
317    :return: a dictionary of variables's fields
318    """
319    offset = get_offset(attrs)
320
321    attrs = attrs.split('\t', 1)[1] if offset else attrs
322
323    attr_list = attrs.split('\t')
324
325    for k in attr_list:
326        type, _typeName, resolver = get_type(var)
327        var = resolver.resolve(var, k)
328
329    try:
330        type, _typeName, resolver = get_type(var)
331        return resolver.get_dictionary(VariableWithOffset(var, offset) if offset else var)
332    except:
333        traceback.print_exc()
334
335
336def custom_operation(thread_id, frame_id, scope, attrs, style, code_or_file, operation_fn_name):
337    """
338    We'll execute the code_or_file and then search in the namespace the operation_fn_name to execute with the given var.
339
340    code_or_file: either some code (i.e.: from pprint import pprint) or a file to be executed.
341    operation_fn_name: the name of the operation to execute after the exec (i.e.: pprint)
342    """
343    expressionValue = getVariable(thread_id, frame_id, scope, attrs)
344
345    try:
346        namespace = {'__name__': '<custom_operation>'}
347        if style == "EXECFILE":
348            namespace['__file__'] = code_or_file
349            execfile(code_or_file, namespace, namespace)
350        else:  # style == EXEC
351            namespace['__file__'] = '<customOperationCode>'
352            Exec(code_or_file, namespace, namespace)
353
354        return str(namespace[operation_fn_name](expressionValue))
355    except:
356        traceback.print_exc()
357
358
359def eval_in_context(expression, globals, locals):
360    result = None
361    try:
362        result = eval(expression, globals, locals)
363    except Exception:
364        s = StringIO()
365        traceback.print_exc(file=s)
366        result = s.getvalue()
367
368        try:
369            try:
370                etype, value, tb = sys.exc_info()
371                result = value
372            finally:
373                etype = value = tb = None
374        except:
375            pass
376
377        result = ExceptionOnEvaluate(result)
378
379        # Ok, we have the initial error message, but let's see if we're dealing with a name mangling error...
380        try:
381            if '__' in expression:
382                # Try to handle '__' name mangling...
383                split = expression.split('.')
384                curr = locals.get(split[0])
385                for entry in split[1:]:
386                    if entry.startswith('__') and not hasattr(curr, entry):
387                        entry = '_%s%s' % (curr.__class__.__name__, entry)
388                    curr = getattr(curr, entry)
389
390                result = curr
391        except:
392            pass
393    return result
394
395
396def evaluate_expression(thread_id, frame_id, expression, doExec):
397    '''returns the result of the evaluated expression
398    @param doExec: determines if we should do an exec or an eval
399    '''
400    frame = find_frame(thread_id, frame_id)
401    if frame is None:
402        return
403
404    # Not using frame.f_globals because of https://sourceforge.net/tracker2/?func=detail&aid=2541355&group_id=85796&atid=577329
405    # (Names not resolved in generator expression in method)
406    # See message: http://mail.python.org/pipermail/python-list/2009-January/526522.html
407    updated_globals = {}
408    updated_globals.update(frame.f_globals)
409    updated_globals.update(frame.f_locals)  # locals later because it has precedence over the actual globals
410
411    try:
412        expression = str(expression.replace('@LINE@', '\n'))
413
414        if doExec:
415            try:
416                # try to make it an eval (if it is an eval we can print it, otherwise we'll exec it and
417                # it will have whatever the user actually did)
418                compiled = compile(expression, '<string>', 'eval')
419            except:
420                Exec(expression, updated_globals, frame.f_locals)
421                pydevd_save_locals.save_locals(frame)
422            else:
423                result = eval(compiled, updated_globals, frame.f_locals)
424                if result is not None:  # Only print if it's not None (as python does)
425                    sys.stdout.write('%s\n' % (result,))
426            return
427
428        else:
429            return eval_in_context(expression, updated_globals, frame.f_locals)
430    finally:
431        # Should not be kept alive if an exception happens and this frame is kept in the stack.
432        del updated_globals
433        del frame
434
435
436def change_attr_expression(thread_id, frame_id, attr, expression, dbg, value=SENTINEL_VALUE):
437    '''Changes some attribute in a given frame.
438    '''
439    frame = find_frame(thread_id, frame_id)
440    if frame is None:
441        return
442
443    try:
444        expression = expression.replace('@LINE@', '\n')
445
446        if dbg.plugin and value is SENTINEL_VALUE:
447            result = dbg.plugin.change_variable(frame, attr, expression)
448            if result:
449                return result
450
451        if value is SENTINEL_VALUE:
452            # It is possible to have variables with names like '.0', ',,,foo', etc in scope by setting them with
453            # `sys._getframe().f_locals`. In particular, the '.0' variable name is used to denote the list iterator when we stop in
454            # list comprehension expressions. This variable evaluates to 0. by `eval`, which is not what we want and this is the main
455            # reason we have to check if the expression exists in the global and local scopes before trying to evaluate it.
456            value = frame.f_locals.get(expression) or frame.f_globals.get(expression) or eval(expression, frame.f_globals, frame.f_locals)
457
458        if attr[:7] == "Globals":
459            attr = attr[8:]
460            if attr in frame.f_globals:
461                frame.f_globals[attr] = value
462                return frame.f_globals[attr]
463        else:
464            if pydevd_save_locals.is_save_locals_available():
465                frame.f_locals[attr] = value
466                pydevd_save_locals.save_locals(frame)
467                return frame.f_locals[attr]
468
469            # default way (only works for changing it in the topmost frame)
470            result = value
471            Exec('%s=%s' % (attr, expression), frame.f_globals, frame.f_locals)
472            return result
473
474    except Exception:
475        traceback.print_exc()
476
477
478MAXIMUM_ARRAY_SIZE = 100
479
480
481def array_to_xml(array, name, roffset, coffset, rows, cols, format):
482    array, xml, r, c, f = array_to_meta_xml(array, name, format)
483    format = '%' + f
484    if rows == -1 and cols == -1:
485        rows = r
486        cols = c
487
488    rows = min(rows, MAXIMUM_ARRAY_SIZE)
489    cols = min(cols, MAXIMUM_ARRAY_SIZE)
490
491    # there is no obvious rule for slicing (at least 5 choices)
492    if len(array) == 1 and (rows > 1 or cols > 1):
493        array = array[0]
494    if array.size > len(array):
495        array = array[roffset:, coffset:]
496        rows = min(rows, len(array))
497        cols = min(cols, len(array[0]))
498        if len(array) == 1:
499            array = array[0]
500    elif array.size == len(array):
501        if roffset == 0 and rows == 1:
502            array = array[coffset:]
503            cols = min(cols, len(array))
504        elif coffset == 0 and cols == 1:
505            array = array[roffset:]
506            rows = min(rows, len(array))
507
508    def get_value(row, col):
509        value = array
510        if rows == 1 or cols == 1:
511            if rows == 1 and cols == 1:
512                value = array[0]
513            else:
514                value = array[(col if rows == 1 else row)]
515                if "ndarray" in str(type(value)):
516                    value = value[0]
517        else:
518            value = array[row][col]
519        return value
520    xml += array_data_to_xml(rows, cols, lambda r: (get_value(r, c) for c in range(cols)), format)
521    return xml
522
523
524class ExceedingArrayDimensionsException(Exception):
525    pass
526
527
528def array_to_meta_xml(array, name, format):
529    type = array.dtype.kind
530    slice = name
531    l = len(array.shape)
532
533    # initial load, compute slice
534    if format == '%':
535        if l > 2:
536            slice += '[0]' * (l - 2)
537            for r in range(l - 2):
538                array = array[0]
539        if type == 'f':
540            format = '.5f'
541        elif type == 'i' or type == 'u':
542            format = 'd'
543        else:
544            format = 's'
545    else:
546        format = format.replace('%', '')
547
548    l = len(array.shape)
549    reslice = ""
550    if l > 2:
551        raise ExceedingArrayDimensionsException()
552    elif l == 1:
553        # special case with 1D arrays arr[i, :] - row, but arr[:, i] - column with equal shape and ndim
554        # http://stackoverflow.com/questions/16837946/numpy-a-2-rows-1-column-file-loadtxt-returns-1row-2-columns
555        # explanation: http://stackoverflow.com/questions/15165170/how-do-i-maintain-row-column-orientation-of-vectors-in-numpy?rq=1
556        # we use kind of a hack - get information about memory from C_CONTIGUOUS
557        is_row = array.flags['C_CONTIGUOUS']
558
559        if is_row:
560            rows = 1
561            cols = len(array)
562            if cols < len(array):
563                reslice = '[0:%s]' % (cols)
564            array = array[0:cols]
565        else:
566            cols = 1
567            rows = len(array)
568            if rows < len(array):
569                reslice = '[0:%s]' % (rows)
570            array = array[0:rows]
571    elif l == 2:
572        rows = array.shape[-2]
573        cols = array.shape[-1]
574        if cols < array.shape[-1] or rows < array.shape[-2]:
575            reslice = '[0:%s, 0:%s]' % (rows, cols)
576        array = array[0:rows, 0:cols]
577
578    # avoid slice duplication
579    if not slice.endswith(reslice):
580        slice += reslice
581
582    bounds = (0, 0)
583    if type in NUMPY_NUMERIC_TYPES:
584        bounds = (array.min(), array.max())
585    return array, slice_to_xml(slice, rows, cols, format, type, bounds), rows, cols, format
586
587
588def get_column_formatter_by_type(initial_format, column_type):
589    if column_type in NUMPY_NUMERIC_TYPES and initial_format:
590        if column_type in NUMPY_FLOATING_POINT_TYPES and initial_format.strip() == DEFAULT_DF_FORMAT:
591            # use custom formatting for floats when default formatting is set
592            return array_default_format(column_type)
593        return initial_format
594    else:
595        return array_default_format(column_type)
596
597
598def get_formatted_row_elements(row, iat, dim, cols, format, dtypes):
599    for c in range(cols):
600        val = iat[row, c] if dim > 1 else iat[row]
601        col_formatter = get_column_formatter_by_type(format, dtypes[c])
602        try:
603            yield ("%" + col_formatter) % val
604        except TypeError:
605            yield ("%" + DEFAULT_DF_FORMAT) % val
606
607
608def array_default_format(type):
609    if type == 'f':
610        return '.5f'
611    elif type == 'i' or type == 'u':
612        return 'd'
613    else:
614        return 's'
615
616
617def get_label(label):
618    return str(label) if not isinstance(label, tuple) else '/'.join(map(str, label))
619
620
621def dataframe_to_xml(df, name, roffset, coffset, rows, cols, format):
622    """
623    :type df: pandas.core.frame.DataFrame
624    :type name: str
625    :type coffset: int
626    :type roffset: int
627    :type rows: int
628    :type cols: int
629    :type format: str
630
631
632    """
633    dim = len(df.axes)
634    num_rows = df.shape[0]
635    num_cols = df.shape[1] if dim > 1 else 1
636    format = format.replace('%', '')
637
638    if not format:
639        if num_rows > 0 and num_cols == 1:  # series or data frame with one column
640            try:
641                kind = df.dtype.kind
642            except AttributeError:
643                try:
644                    kind = df.dtypes[0].kind
645                except (IndexError, KeyError):
646                    kind = 'O'
647            format = array_default_format(kind)
648        else:
649            format = array_default_format(DEFAULT_DF_FORMAT)
650
651    xml = slice_to_xml(name, num_rows, num_cols, format, "", (0, 0))
652
653    if (rows, cols) == (-1, -1):
654        rows, cols = num_rows, num_cols
655
656    rows = min(rows, MAXIMUM_ARRAY_SIZE)
657    cols = min(cols, MAXIMUM_ARRAY_SIZE, num_cols)
658    # need to precompute column bounds here before slicing!
659    col_bounds = [None] * cols
660    dtypes = [None] * cols
661    if dim > 1:
662        for col in range(cols):
663            dtype = df.dtypes.iloc[coffset + col].kind
664            dtypes[col] = dtype
665            if dtype in NUMPY_NUMERIC_TYPES:
666                cvalues = df.iloc[:, coffset + col]
667                bounds = (cvalues.min(), cvalues.max())
668            else:
669                bounds = (0, 0)
670            col_bounds[col] = bounds
671    else:
672        dtype = df.dtype.kind
673        dtypes[0] = dtype
674        col_bounds[0] = (df.min(), df.max()) if dtype in NUMPY_NUMERIC_TYPES else (0, 0)
675
676    df = df.iloc[roffset: roffset + rows, coffset: coffset + cols] if dim > 1 else df.iloc[roffset: roffset + rows]
677    rows = df.shape[0]
678    cols = df.shape[1] if dim > 1 else 1
679
680    def col_to_format(c):
681        return get_column_formatter_by_type(format, dtypes[c])
682
683    iat = df.iat if dim == 1 or len(df.columns.unique()) == len(df.columns) else df.iloc
684
685    def formatted_row_elements(row):
686        return get_formatted_row_elements(row, iat, dim, cols, format, dtypes)
687
688    xml += header_data_to_xml(rows, cols, dtypes, col_bounds, col_to_format, df, dim)
689    xml += array_data_to_xml(rows, cols, formatted_row_elements, format)
690    return xml
691
692
693def array_data_to_xml(rows, cols, get_row, format):
694    xml = "<arraydata rows=\"%s\" cols=\"%s\"/>\n" % (rows, cols)
695    for row in range(rows):
696        xml += "<row index=\"%s\"/>\n" % to_string(row)
697        for value in get_row(row):
698            xml += var_to_xml(value, '', format=format)
699    return xml
700
701
702def slice_to_xml(slice, rows, cols, format, type, bounds):
703    return '<array slice=\"%s\" rows=\"%s\" cols=\"%s\" format=\"%s\" type=\"%s\" max=\"%s\" min=\"%s\"/>' % \
704           (slice, rows, cols, quote(format), type, bounds[1], bounds[0])
705
706
707def header_data_to_xml(rows, cols, dtypes, col_bounds, col_to_format, df, dim):
708    xml = "<headerdata rows=\"%s\" cols=\"%s\">\n" % (rows, cols)
709    for col in range(cols):
710        col_label = quote(get_label(df.axes[1].values[col]) if dim > 1 else str(col))
711        bounds = col_bounds[col]
712        col_format = "%" + col_to_format(col)
713        xml += '<colheader index=\"%s\" label=\"%s\" type=\"%s\" format=\"%s\" max=\"%s\" min=\"%s\" />\n' % \
714               (str(col), col_label, dtypes[col], col_to_format(col), col_format % bounds[1], col_format % bounds[0])
715    for row in range(rows):
716        xml += "<rowheader index=\"%s\" label = \"%s\"/>\n" % (str(row), get_label(df.axes[0].values[row]))
717    xml += "</headerdata>\n"
718    return xml
719
720
721def is_able_to_format_number(format):
722    try:
723        format % math.pi
724    except Exception:
725        return False
726    return True
727
728
729TYPE_TO_XML_CONVERTERS = {
730    "ndarray": array_to_xml,
731    "DataFrame": dataframe_to_xml,
732    "Series": dataframe_to_xml,
733    "GeoDataFrame": dataframe_to_xml,
734    "GeoSeries": dataframe_to_xml
735}
736
737
738def table_like_struct_to_xml(array, name, roffset, coffset, rows, cols, format):
739    _, type_name, _ = get_type(array)
740    format = format if is_able_to_format_number(format) else '%'
741    if type_name in TYPE_TO_XML_CONVERTERS:
742        return "<xml>%s</xml>" % TYPE_TO_XML_CONVERTERS[type_name](array, name, roffset, coffset, rows, cols, format)
743    else:
744        raise VariableError("type %s not supported" % type_name)
745
746