Working with the AST

Now that our OData query has been parsed to an AST, how do we work with it? The Visitor Pattern is a popular way to walk tree structures such as AST’s and modify or transform them to another representation. odata-query contains the NodeVisitor and NodeTransformer base classes that implement this pattern, as well as some concrete implementations.

NodeVisitor

A odata_query.visitor.NodeVisitor is a class that walks an AST (depth-first by default) and calls a visit_{node_type} method on each 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 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 odata_query.django.django_q.AstToDjangoQVisitor or the odata_query.sqlalchemy.orm.AstToSqlAlchemyOrmVisitor implementations. They transform an AST to Django and SQLAlchemy ORM queries respectively.

NodeTransformer

A odata_query.visitor.NodeTransformer is very similar to a NodeVisitor, with one difference: The visit_ methods should return an odata_query.ast._Node, which will replace the node that is being visited. This allows NodeTransformer’s to modify the AST while it’s being traversed. For example, the following 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 odata_query.rewrite.AliasRewriter. This transformer looks for aliases in identifiers and attributes, and replaces them with their full names.

Included Visitors

class odata_query.rewrite.AliasRewriter(field_aliases: Dict[str, str], lexer: Optional[odata_query.grammar.ODataLexer] = None, parser: Optional[odata_query.grammar.ODataParser] = None)[source]

A NodeTransformer that replaces aliases in the AST with their aliased identifiers or attributes.

Parameters
  • field_aliases – A mapping of aliases to their full name. These can be identifiers, attributes, and even function calls in odata syntax.

  • lexer – Optional lexer instance to use. If not passed, will construct the default one.

  • parser – Optional parser instance to use. If not passed, will construct the default one.

class odata_query.roundtrip.AstToODataVisitor[source]

NodeVisitor that transforms an AST back into an OData query string.