Skip to content

Latest commit

 

History

History
93 lines (63 loc) · 3.39 KB

working-with-ast.rst

File metadata and controls

93 lines (63 loc) · 3.39 KB

Working with the AST

Now that our :ref:`OData query has been parsed <ref-parsing-odata>` to an :term:`AST`, how do we work with it? The Visitor Pattern is a popular way to walk tree structures such as :term:`AST`'s and modify or transform them to another representation. odata-query contains the :ref:`ref-node-visitor` and :ref:`ref-node-transformer` base classes that implement this pattern, as well as some concrete implementations.

NodeVisitor

A :py:class:`odata_query.visitor.NodeVisitor` is a class that walks an :term:`AST` (depth-first by default) and calls a visit_{node_type} method on each :py:class:`odata_query.ast._Node` it encounters. These methods can return whatever they want, making this a very flexible pattern! If no visit_ method is implemented for the type of the node the visitor will continue with the node's children if it has any, so you only need to implement what you explicitly need. A simple :py:class:`odata_query.visitor.NodeVisitor` that counts comparison expressions for example, might look like this:

class ComparisonCounter(NodeVisitor):
    def visit_Comparison(self, node: ast.Comparison) -> int:
        count_lhs = self.visit(node.left) or 0
        count_rhs = self.visit(node.right) or 0
        return 1 + count_lhs + count_rhs


count = ComparisonCounter().visit(my_ast)

This isn't the most useful implementation... For some more realistic examples, take a look at the :py:class:`odata_query.django.django_q.AstToDjangoQVisitor` or the :py:class:`odata_query.sqlalchemy.orm.AstToSqlAlchemyOrmVisitor` implementations. They transform an :term:`AST` to Django and SQLAlchemy ORM queries respectively.

NodeTransformer

A :py:class:`odata_query.visitor.NodeTransformer` is very similar to a :ref:`ref-node-visitor`, with one difference: The visit_ methods should return an :py:class:`odata_query.ast._Node`, which will replace the node that is being visited. This allows NodeTransformer's to modify the :term:`AST` while it's being traversed. For example, the following :py:class:`odata_query.visitor.NodeTransformer` would invert all 'less-than' comparisons to 'greater-than' and vice-versa:

class ComparisonInverter(NodeTransformer):
    def visit_Comparison(self, node: ast.Comparison) -> ast.Comparison:
        if node.comparator == ast.Lt():
            new_comparator = ast.Gt()
        elif node.comparator == ast.Gt():
            new_comparator = ast.Lt()
        else:
            new_comparator = node.comparator

        return ast.Comparison(new_comparator, node.left, node.right)


inverted = ComparisonInverter().visit(my_ast)

An interesting concrete implementation in odata-query is the :py:class:`odata_query.rewrite.AliasRewriter`. This transformer looks for aliases in identifiers and attributes, and replaces them with their full names.

Included Visitors

.. autoclass:: odata_query.django.django_q.AstToDjangoQVisitor
.. autoclass:: odata_query.sqlalchemy.orm.AstToSqlAlchemyOrmVisitor
.. autoclass:: odata_query.sqlalchemy.core.AstToSqlAlchemyCoreVisitor
.. autoclass:: odata_query.rewrite.AliasRewriter
.. autoclass:: odata_query.roundtrip.AstToODataVisitor