1# cython: infer_types=True
2# cython: language_level=3
3# cython: auto_pickle=False
4
5#
6#   Tree visitor and transform framework
7#
8
9from __future__ import absolute_import, print_function
10
11import sys
12import inspect
13
14from . import TypeSlots
15from . import Builtin
16from . import Nodes
17from . import ExprNodes
18from . import Errors
19from . import DebugFlags
20from . import Future
21
22import cython
23
24
25cython.declare(_PRINTABLE=tuple)
26
27if sys.version_info[0] >= 3:
28    _PRINTABLE = (bytes, str, int, float)
29else:
30    _PRINTABLE = (str, unicode, long, int, float)
31
32
33class TreeVisitor(object):
34    """
35    Base class for writing visitors for a Cython tree, contains utilities for
36    recursing such trees using visitors. Each node is
37    expected to have a child_attrs iterable containing the names of attributes
38    containing child nodes or lists of child nodes. Lists are not considered
39    part of the tree structure (i.e. contained nodes are considered direct
40    children of the parent node).
41
42    visit_children visits each of the children of a given node (see the visit_children
43    documentation). When recursing the tree using visit_children, an attribute
44    access_path is maintained which gives information about the current location
45    in the tree as a stack of tuples: (parent_node, attrname, index), representing
46    the node, attribute and optional list index that was taken in each step in the path to
47    the current node.
48
49    Example:
50
51    >>> class SampleNode(object):
52    ...     child_attrs = ["head", "body"]
53    ...     def __init__(self, value, head=None, body=None):
54    ...         self.value = value
55    ...         self.head = head
56    ...         self.body = body
57    ...     def __repr__(self): return "SampleNode(%s)" % self.value
58    ...
59    >>> tree = SampleNode(0, SampleNode(1), [SampleNode(2), SampleNode(3)])
60    >>> class MyVisitor(TreeVisitor):
61    ...     def visit_SampleNode(self, node):
62    ...         print("in %s %s" % (node.value, self.access_path))
63    ...         self.visitchildren(node)
64    ...         print("out %s" % node.value)
65    ...
66    >>> MyVisitor().visit(tree)
67    in 0 []
68    in 1 [(SampleNode(0), 'head', None)]
69    out 1
70    in 2 [(SampleNode(0), 'body', 0)]
71    out 2
72    in 3 [(SampleNode(0), 'body', 1)]
73    out 3
74    out 0
75    """
76    def __init__(self):
77        super(TreeVisitor, self).__init__()
78        self.dispatch_table = {}
79        self.access_path = []
80
81    def dump_node(self, node):
82        ignored = list(node.child_attrs or []) + [
83            u'child_attrs', u'pos', u'gil_message', u'cpp_message', u'subexprs']
84        values = []
85        pos = getattr(node, 'pos', None)
86        if pos:
87            source = pos[0]
88            if source:
89                import os.path
90                source = os.path.basename(source.get_description())
91            values.append(u'%s:%s:%s' % (source, pos[1], pos[2]))
92        attribute_names = dir(node)
93        for attr in attribute_names:
94            if attr in ignored:
95                continue
96            if attr.startswith('_') or attr.endswith('_'):
97                continue
98            try:
99                value = getattr(node, attr)
100            except AttributeError:
101                continue
102            if value is None or value == 0:
103                continue
104            elif isinstance(value, list):
105                value = u'[...]/%d' % len(value)
106            elif not isinstance(value, _PRINTABLE):
107                continue
108            else:
109                value = repr(value)
110            values.append(u'%s = %s' % (attr, value))
111        return u'%s(%s)' % (node.__class__.__name__, u',\n    '.join(values))
112
113    def _find_node_path(self, stacktrace):
114        import os.path
115        last_traceback = stacktrace
116        nodes = []
117        while hasattr(stacktrace, 'tb_frame'):
118            frame = stacktrace.tb_frame
119            node = frame.f_locals.get(u'self')
120            if isinstance(node, Nodes.Node):
121                code = frame.f_code
122                method_name = code.co_name
123                pos = (os.path.basename(code.co_filename),
124                       frame.f_lineno)
125                nodes.append((node, method_name, pos))
126                last_traceback = stacktrace
127            stacktrace = stacktrace.tb_next
128        return (last_traceback, nodes)
129
130    def _raise_compiler_error(self, child, e):
131        trace = ['']
132        for parent, attribute, index in self.access_path:
133            node = getattr(parent, attribute)
134            if index is None:
135                index = ''
136            else:
137                node = node[index]
138                index = u'[%d]' % index
139            trace.append(u'%s.%s%s = %s' % (
140                parent.__class__.__name__, attribute, index,
141                self.dump_node(node)))
142        stacktrace, called_nodes = self._find_node_path(sys.exc_info()[2])
143        last_node = child
144        for node, method_name, pos in called_nodes:
145            last_node = node
146            trace.append(u"File '%s', line %d, in %s: %s" % (
147                pos[0], pos[1], method_name, self.dump_node(node)))
148        raise Errors.CompilerCrash(
149            getattr(last_node, 'pos', None), self.__class__.__name__,
150            u'\n'.join(trace), e, stacktrace)
151
152    @cython.final
153    def find_handler(self, obj):
154        # to resolve, try entire hierarchy
155        cls = type(obj)
156        pattern = "visit_%s"
157        mro = inspect.getmro(cls)
158        for mro_cls in mro:
159            handler_method = getattr(self, pattern % mro_cls.__name__, None)
160            if handler_method is not None:
161                return handler_method
162        print(type(self), cls)
163        if self.access_path:
164            print(self.access_path)
165            print(self.access_path[-1][0].pos)
166            print(self.access_path[-1][0].__dict__)
167        raise RuntimeError("Visitor %r does not accept object: %s" % (self, obj))
168
169    def visit(self, obj):
170        return self._visit(obj)
171
172    @cython.final
173    def _visit(self, obj):
174        try:
175            try:
176                handler_method = self.dispatch_table[type(obj)]
177            except KeyError:
178                handler_method = self.find_handler(obj)
179                self.dispatch_table[type(obj)] = handler_method
180            return handler_method(obj)
181        except Errors.CompileError:
182            raise
183        except Errors.AbortError:
184            raise
185        except Exception as e:
186            if DebugFlags.debug_no_exception_intercept:
187                raise
188            self._raise_compiler_error(obj, e)
189
190    @cython.final
191    def _visitchild(self, child, parent, attrname, idx):
192        self.access_path.append((parent, attrname, idx))
193        result = self._visit(child)
194        self.access_path.pop()
195        return result
196
197    def visitchildren(self, parent, attrs=None):
198        return self._visitchildren(parent, attrs)
199
200    @cython.final
201    @cython.locals(idx=int)
202    def _visitchildren(self, parent, attrs):
203        """
204        Visits the children of the given parent. If parent is None, returns
205        immediately (returning None).
206
207        The return value is a dictionary giving the results for each
208        child (mapping the attribute name to either the return value
209        or a list of return values (in the case of multiple children
210        in an attribute)).
211        """
212        if parent is None: return None
213        result = {}
214        for attr in parent.child_attrs:
215            if attrs is not None and attr not in attrs: continue
216            child = getattr(parent, attr)
217            if child is not None:
218                if type(child) is list:
219                    childretval = [self._visitchild(x, parent, attr, idx) for idx, x in enumerate(child)]
220                else:
221                    childretval = self._visitchild(child, parent, attr, None)
222                    assert not isinstance(childretval, list), 'Cannot insert list here: %s in %r' % (attr, parent)
223                result[attr] = childretval
224        return result
225
226
227class VisitorTransform(TreeVisitor):
228    """
229    A tree transform is a base class for visitors that wants to do stream
230    processing of the structure (rather than attributes etc.) of a tree.
231
232    It implements __call__ to simply visit the argument node.
233
234    It requires the visitor methods to return the nodes which should take
235    the place of the visited node in the result tree (which can be the same
236    or one or more replacement). Specifically, if the return value from
237    a visitor method is:
238
239    - [] or None; the visited node will be removed (set to None if an attribute and
240    removed if in a list)
241    - A single node; the visited node will be replaced by the returned node.
242    - A list of nodes; the visited nodes will be replaced by all the nodes in the
243    list. This will only work if the node was already a member of a list; if it
244    was not, an exception will be raised. (Typically you want to ensure that you
245    are within a StatListNode or similar before doing this.)
246    """
247    def visitchildren(self, parent, attrs=None, exclude=None):
248        # generic def entry point for calls from Python subclasses
249        if exclude is not None:
250            attrs = self._select_attrs(parent.child_attrs if attrs is None else attrs, exclude)
251        return self._process_children(parent, attrs)
252
253    @cython.final
254    def _select_attrs(self, attrs, exclude):
255        return [name for name in attrs if name not in exclude]
256
257    @cython.final
258    def _process_children(self, parent, attrs=None):
259        # fast cdef entry point for calls from Cython subclasses
260        result = self._visitchildren(parent, attrs)
261        for attr, newnode in result.items():
262            if type(newnode) is list:
263                newnode = self._flatten_list(newnode)
264            setattr(parent, attr, newnode)
265        return result
266
267    @cython.final
268    def _flatten_list(self, orig_list):
269        # Flatten the list one level and remove any None
270        newlist = []
271        for x in orig_list:
272            if x is not None:
273                if type(x) is list:
274                    newlist.extend(x)
275                else:
276                    newlist.append(x)
277        return newlist
278
279    def recurse_to_children(self, node):
280        self._process_children(node)
281        return node
282
283    def __call__(self, root):
284        return self._visit(root)
285
286
287class CythonTransform(VisitorTransform):
288    """
289    Certain common conventions and utilities for Cython transforms.
290
291     - Sets up the context of the pipeline in self.context
292     - Tracks directives in effect in self.current_directives
293    """
294    def __init__(self, context):
295        super(CythonTransform, self).__init__()
296        self.context = context
297
298    def __call__(self, node):
299        from . import ModuleNode
300        if isinstance(node, ModuleNode.ModuleNode):
301            self.current_directives = node.directives
302        return super(CythonTransform, self).__call__(node)
303
304    def visit_CompilerDirectivesNode(self, node):
305        old = self.current_directives
306        self.current_directives = node.directives
307        self._process_children(node)
308        self.current_directives = old
309        return node
310
311    def visit_Node(self, node):
312        self._process_children(node)
313        return node
314
315
316class ScopeTrackingTransform(CythonTransform):
317    # Keeps track of type of scopes
318    #scope_type: can be either of 'module', 'function', 'cclass', 'pyclass', 'struct'
319    #scope_node: the node that owns the current scope
320
321    def visit_ModuleNode(self, node):
322        self.scope_type = 'module'
323        self.scope_node = node
324        self._process_children(node)
325        return node
326
327    def visit_scope(self, node, scope_type):
328        prev = self.scope_type, self.scope_node
329        self.scope_type = scope_type
330        self.scope_node = node
331        self._process_children(node)
332        self.scope_type, self.scope_node = prev
333        return node
334
335    def visit_CClassDefNode(self, node):
336        return self.visit_scope(node, 'cclass')
337
338    def visit_PyClassDefNode(self, node):
339        return self.visit_scope(node, 'pyclass')
340
341    def visit_FuncDefNode(self, node):
342        return self.visit_scope(node, 'function')
343
344    def visit_CStructOrUnionDefNode(self, node):
345        return self.visit_scope(node, 'struct')
346
347
348class EnvTransform(CythonTransform):
349    """
350    This transformation keeps a stack of the environments.
351    """
352    def __call__(self, root):
353        self.env_stack = []
354        self.enter_scope(root, root.scope)
355        return super(EnvTransform, self).__call__(root)
356
357    def current_env(self):
358        return self.env_stack[-1][1]
359
360    def current_scope_node(self):
361        return self.env_stack[-1][0]
362
363    def global_scope(self):
364        return self.current_env().global_scope()
365
366    def enter_scope(self, node, scope):
367        self.env_stack.append((node, scope))
368
369    def exit_scope(self):
370        self.env_stack.pop()
371
372    def visit_FuncDefNode(self, node):
373        outer_attrs = node.outer_attrs
374        self.visitchildren(node, attrs=outer_attrs)
375        self.enter_scope(node, node.local_scope)
376        self.visitchildren(node, attrs=None, exclude=outer_attrs)
377        self.exit_scope()
378        return node
379
380    def visit_GeneratorBodyDefNode(self, node):
381        self._process_children(node)
382        return node
383
384    def visit_ClassDefNode(self, node):
385        self.enter_scope(node, node.scope)
386        self._process_children(node)
387        self.exit_scope()
388        return node
389
390    def visit_CStructOrUnionDefNode(self, node):
391        self.enter_scope(node, node.scope)
392        self._process_children(node)
393        self.exit_scope()
394        return node
395
396    def visit_ScopedExprNode(self, node):
397        if node.expr_scope:
398            self.enter_scope(node, node.expr_scope)
399            self._process_children(node)
400            self.exit_scope()
401        else:
402            self._process_children(node)
403        return node
404
405    def visit_CArgDeclNode(self, node):
406        # default arguments are evaluated in the outer scope
407        if node.default:
408            attrs = [attr for attr in node.child_attrs if attr != 'default']
409            self._process_children(node, attrs)
410            self.enter_scope(node, self.current_env().outer_scope)
411            self.visitchildren(node, ('default',))
412            self.exit_scope()
413        else:
414            self._process_children(node)
415        return node
416
417
418class NodeRefCleanupMixin(object):
419    """
420    Clean up references to nodes that were replaced.
421
422    NOTE: this implementation assumes that the replacement is
423    done first, before hitting any further references during
424    normal tree traversal.  This needs to be arranged by calling
425    "self.visitchildren()" at a proper place in the transform
426    and by ordering the "child_attrs" of nodes appropriately.
427    """
428    def __init__(self, *args):
429        super(NodeRefCleanupMixin, self).__init__(*args)
430        self._replacements = {}
431
432    def visit_CloneNode(self, node):
433        arg = node.arg
434        if arg not in self._replacements:
435            self.visitchildren(arg)
436        node.arg = self._replacements.get(arg, arg)
437        return node
438
439    def visit_ResultRefNode(self, node):
440        expr = node.expression
441        if expr is None or expr not in self._replacements:
442            self.visitchildren(node)
443            expr = node.expression
444        if expr is not None:
445            node.expression = self._replacements.get(expr, expr)
446        return node
447
448    def replace(self, node, replacement):
449        self._replacements[node] = replacement
450        return replacement
451
452
453find_special_method_for_binary_operator = {
454    '<':  '__lt__',
455    '<=': '__le__',
456    '==': '__eq__',
457    '!=': '__ne__',
458    '>=': '__ge__',
459    '>':  '__gt__',
460    '+':  '__add__',
461    '&':  '__and__',
462    '/':  '__div__',
463    '//': '__floordiv__',
464    '<<': '__lshift__',
465    '%':  '__mod__',
466    '*':  '__mul__',
467    '|':  '__or__',
468    '**': '__pow__',
469    '>>': '__rshift__',
470    '-':  '__sub__',
471    '^':  '__xor__',
472    'in': '__contains__',
473}.get
474
475
476find_special_method_for_unary_operator = {
477    'not': '__not__',
478    '~':   '__inv__',
479    '-':   '__neg__',
480    '+':   '__pos__',
481}.get
482
483
484class MethodDispatcherTransform(EnvTransform):
485    """
486    Base class for transformations that want to intercept on specific
487    builtin functions or methods of builtin types, including special
488    methods triggered by Python operators.  Must run after declaration
489    analysis when entries were assigned.
490
491    Naming pattern for handler methods is as follows:
492
493    * builtin functions: _handle_(general|simple|any)_function_NAME
494
495    * builtin methods: _handle_(general|simple|any)_method_TYPENAME_METHODNAME
496    """
497    # only visit call nodes and Python operations
498    def visit_GeneralCallNode(self, node):
499        self._process_children(node)
500        function = node.function
501        if not function.type.is_pyobject:
502            return node
503        arg_tuple = node.positional_args
504        if not isinstance(arg_tuple, ExprNodes.TupleNode):
505            return node
506        keyword_args = node.keyword_args
507        if keyword_args and not isinstance(keyword_args, ExprNodes.DictNode):
508            # can't handle **kwargs
509            return node
510        args = arg_tuple.args
511        return self._dispatch_to_handler(node, function, args, keyword_args)
512
513    def visit_SimpleCallNode(self, node):
514        self._process_children(node)
515        function = node.function
516        if function.type.is_pyobject:
517            arg_tuple = node.arg_tuple
518            if not isinstance(arg_tuple, ExprNodes.TupleNode):
519                return node
520            args = arg_tuple.args
521        else:
522            args = node.args
523        return self._dispatch_to_handler(node, function, args, None)
524
525    def visit_PrimaryCmpNode(self, node):
526        if node.cascade:
527            # not currently handled below
528            self._process_children(node)
529            return node
530        return self._visit_binop_node(node)
531
532    def visit_BinopNode(self, node):
533        return self._visit_binop_node(node)
534
535    def _visit_binop_node(self, node):
536        self._process_children(node)
537        # FIXME: could special case 'not_in'
538        special_method_name = find_special_method_for_binary_operator(node.operator)
539        if special_method_name:
540            operand1, operand2 = node.operand1, node.operand2
541            if special_method_name == '__contains__':
542                operand1, operand2 = operand2, operand1
543            elif special_method_name == '__div__':
544                if Future.division in self.current_env().global_scope().context.future_directives:
545                    special_method_name = '__truediv__'
546            obj_type = operand1.type
547            if obj_type.is_builtin_type:
548                type_name = obj_type.name
549            else:
550                type_name = "object"  # safety measure
551            node = self._dispatch_to_method_handler(
552                special_method_name, None, False, type_name,
553                node, None, [operand1, operand2], None)
554        return node
555
556    def visit_UnopNode(self, node):
557        self._process_children(node)
558        special_method_name = find_special_method_for_unary_operator(node.operator)
559        if special_method_name:
560            operand = node.operand
561            obj_type = operand.type
562            if obj_type.is_builtin_type:
563                type_name = obj_type.name
564            else:
565                type_name = "object"  # safety measure
566            node = self._dispatch_to_method_handler(
567                special_method_name, None, False, type_name,
568                node, None, [operand], None)
569        return node
570
571    ### dispatch to specific handlers
572
573    def _find_handler(self, match_name, has_kwargs):
574        try:
575            match_name.encode('ascii')
576        except UnicodeEncodeError:
577            # specifically when running the Cython compiler under Python 2
578            #  getattr can't take a unicode string.
579            #  Classes with unicode names won't have specific handlers and thus it
580            #  should be OK to return None.
581            # Doing the test here ensures that the same code gets run on
582            # Python 2 and 3
583            return None
584
585        call_type = has_kwargs and 'general' or 'simple'
586        handler = getattr(self, '_handle_%s_%s' % (call_type, match_name), None)
587        if handler is None:
588            handler = getattr(self, '_handle_any_%s' % match_name, None)
589        return handler
590
591    def _delegate_to_assigned_value(self, node, function, arg_list, kwargs):
592        assignment = function.cf_state[0]
593        value = assignment.rhs
594        if value.is_name:
595            if not value.entry or len(value.entry.cf_assignments) > 1:
596                # the variable might have been reassigned => play safe
597                return node
598        elif value.is_attribute and value.obj.is_name:
599            if not value.obj.entry or len(value.obj.entry.cf_assignments) > 1:
600                # the underlying variable might have been reassigned => play safe
601                return node
602        else:
603            return node
604        return self._dispatch_to_handler(
605            node, value, arg_list, kwargs)
606
607    def _dispatch_to_handler(self, node, function, arg_list, kwargs):
608        if function.is_name:
609            # we only consider functions that are either builtin
610            # Python functions or builtins that were already replaced
611            # into a C function call (defined in the builtin scope)
612            if not function.entry:
613                return node
614            entry = function.entry
615            is_builtin = (
616                entry.is_builtin or
617                entry is self.current_env().builtin_scope().lookup_here(function.name))
618            if not is_builtin:
619                if function.cf_state and function.cf_state.is_single:
620                    # we know the value of the variable
621                    # => see if it's usable instead
622                    return self._delegate_to_assigned_value(
623                        node, function, arg_list, kwargs)
624                if arg_list and entry.is_cmethod and entry.scope and entry.scope.parent_type.is_builtin_type:
625                    if entry.scope.parent_type is arg_list[0].type:
626                        # Optimised (unbound) method of a builtin type => try to "de-optimise".
627                        return self._dispatch_to_method_handler(
628                            entry.name, self_arg=None, is_unbound_method=True,
629                            type_name=entry.scope.parent_type.name,
630                            node=node, function=function, arg_list=arg_list, kwargs=kwargs)
631                return node
632            function_handler = self._find_handler(
633                "function_%s" % function.name, kwargs)
634            if function_handler is None:
635                return self._handle_function(node, function.name, function, arg_list, kwargs)
636            if kwargs:
637                return function_handler(node, function, arg_list, kwargs)
638            else:
639                return function_handler(node, function, arg_list)
640        elif function.is_attribute:
641            attr_name = function.attribute
642            if function.type.is_pyobject:
643                self_arg = function.obj
644            elif node.self and function.entry:
645                entry = function.entry.as_variable
646                if not entry or not entry.is_builtin:
647                    return node
648                # C implementation of a Python builtin method - see if we find further matches
649                self_arg = node.self
650                arg_list = arg_list[1:]  # drop CloneNode of self argument
651            else:
652                return node
653            obj_type = self_arg.type
654            is_unbound_method = False
655            if obj_type.is_builtin_type:
656                if obj_type is Builtin.type_type and self_arg.is_name and arg_list and arg_list[0].type.is_pyobject:
657                    # calling an unbound method like 'list.append(L,x)'
658                    # (ignoring 'type.mro()' here ...)
659                    type_name = self_arg.name
660                    self_arg = None
661                    is_unbound_method = True
662                else:
663                    type_name = obj_type.name
664            else:
665                type_name = "object"  # safety measure
666            return self._dispatch_to_method_handler(
667                attr_name, self_arg, is_unbound_method, type_name,
668                node, function, arg_list, kwargs)
669        else:
670            return node
671
672    def _dispatch_to_method_handler(self, attr_name, self_arg,
673                                    is_unbound_method, type_name,
674                                    node, function, arg_list, kwargs):
675        method_handler = self._find_handler(
676            "method_%s_%s" % (type_name, attr_name), kwargs)
677        if method_handler is None:
678            if (attr_name in TypeSlots.method_name_to_slot
679                    or attr_name in ['__new__', '__class__']):
680                method_handler = self._find_handler(
681                    "slot%s" % attr_name, kwargs)
682            if method_handler is None:
683                return self._handle_method(
684                    node, type_name, attr_name, function,
685                    arg_list, is_unbound_method, kwargs)
686        if self_arg is not None:
687            arg_list = [self_arg] + list(arg_list)
688        if kwargs:
689            result = method_handler(
690                node, function, arg_list, is_unbound_method, kwargs)
691        else:
692            result = method_handler(
693                node, function, arg_list, is_unbound_method)
694        return result
695
696    def _handle_function(self, node, function_name, function, arg_list, kwargs):
697        """Fallback handler"""
698        return node
699
700    def _handle_method(self, node, type_name, attr_name, function,
701                       arg_list, is_unbound_method, kwargs):
702        """Fallback handler"""
703        return node
704
705
706class RecursiveNodeReplacer(VisitorTransform):
707    """
708    Recursively replace all occurrences of a node in a subtree by
709    another node.
710    """
711    def __init__(self, orig_node, new_node):
712        super(RecursiveNodeReplacer, self).__init__()
713        self.orig_node, self.new_node = orig_node, new_node
714
715    def visit_CloneNode(self, node):
716        if node is self.orig_node:
717            return self.new_node
718        if node.arg is self.orig_node:
719            node.arg = self.new_node
720        return node
721
722    def visit_Node(self, node):
723        self._process_children(node)
724        if node is self.orig_node:
725            return self.new_node
726        else:
727            return node
728
729def recursively_replace_node(tree, old_node, new_node):
730    replace_in = RecursiveNodeReplacer(old_node, new_node)
731    replace_in(tree)
732
733
734class NodeFinder(TreeVisitor):
735    """
736    Find out if a node appears in a subtree.
737    """
738    def __init__(self, node):
739        super(NodeFinder, self).__init__()
740        self.node = node
741        self.found = False
742
743    def visit_Node(self, node):
744        if self.found:
745            pass  # short-circuit
746        elif node is self.node:
747            self.found = True
748        else:
749            self._visitchildren(node, None)
750
751def tree_contains(tree, node):
752    finder = NodeFinder(node)
753    finder.visit(tree)
754    return finder.found
755
756
757# Utils
758def replace_node(ptr, value):
759    """Replaces a node. ptr is of the form used on the access path stack
760    (parent, attrname, listidx|None)
761    """
762    parent, attrname, listidx = ptr
763    if listidx is None:
764        setattr(parent, attrname, value)
765    else:
766        getattr(parent, attrname)[listidx] = value
767
768
769class PrintTree(TreeVisitor):
770    """Prints a representation of the tree to standard output.
771    Subclass and override repr_of to provide more information
772    about nodes. """
773    def __init__(self, start=None, end=None):
774        TreeVisitor.__init__(self)
775        self._indent = ""
776        if start is not None or end is not None:
777            self._line_range = (start or 0, end or 2**30)
778        else:
779            self._line_range = None
780
781    def indent(self):
782        self._indent += "  "
783
784    def unindent(self):
785        self._indent = self._indent[:-2]
786
787    def __call__(self, tree, phase=None):
788        print("Parse tree dump at phase '%s'" % phase)
789        self.visit(tree)
790        return tree
791
792    # Don't do anything about process_list, the defaults gives
793    # nice-looking name[idx] nodes which will visually appear
794    # under the parent-node, not displaying the list itself in
795    # the hierarchy.
796    def visit_Node(self, node):
797        self._print_node(node)
798        self.indent()
799        self.visitchildren(node)
800        self.unindent()
801        return node
802
803    def visit_CloneNode(self, node):
804        self._print_node(node)
805        self.indent()
806        line = node.pos[1]
807        if self._line_range is None or self._line_range[0] <= line <= self._line_range[1]:
808            print("%s- %s: %s" % (self._indent, 'arg', self.repr_of(node.arg)))
809        self.indent()
810        self.visitchildren(node.arg)
811        self.unindent()
812        self.unindent()
813        return node
814
815    def _print_node(self, node):
816        line = node.pos[1]
817        if self._line_range is None or self._line_range[0] <= line <= self._line_range[1]:
818            if len(self.access_path) == 0:
819                name = "(root)"
820            else:
821                parent, attr, idx = self.access_path[-1]
822                if idx is not None:
823                    name = "%s[%d]" % (attr, idx)
824                else:
825                    name = attr
826            print("%s- %s: %s" % (self._indent, name, self.repr_of(node)))
827
828    def repr_of(self, node):
829        if node is None:
830            return "(none)"
831        else:
832            result = node.__class__.__name__
833            if isinstance(node, ExprNodes.NameNode):
834                result += "(type=%s, name=\"%s\")" % (repr(node.type), node.name)
835            elif isinstance(node, Nodes.DefNode):
836                result += "(name=\"%s\")" % node.name
837            elif isinstance(node, ExprNodes.AttributeNode):
838                result += "(type=%s, attribute=\"%s\")" % (repr(node.type), node.attribute)
839            elif isinstance(node, (ExprNodes.ConstNode, ExprNodes.PyConstNode)):
840                result += "(type=%s, value=%r)" % (repr(node.type), node.value)
841            elif isinstance(node, ExprNodes.ExprNode):
842                t = node.type
843                result += "(type=%s)" % repr(t)
844            elif node.pos:
845                pos = node.pos
846                path = pos[0].get_description()
847                if '/' in path:
848                    path = path.split('/')[-1]
849                if '\\' in path:
850                    path = path.split('\\')[-1]
851                result += "(pos=(%s:%s:%s))" % (path, pos[1], pos[2])
852
853            return result
854
855if __name__ == "__main__":
856    import doctest
857    doctest.testmod()
858