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.