Converters

The examples discussed here are already implemented and are given just as a simple illustration of usage.

Expressions

The @lookup_property decorator uses a single-dispatch function lookup_property.converters.expressions.expression_to_ast to register a number of converters for transforming Django ORM expressions to equivalent python statement AST.

Let's take a look at our example from before:

from lookup_property import lookup_property
from django.db import models
from django.db.models import Value
from django.db.models.functions import Concat

class Student(models.Model):
    first_name = models.CharField(max_length=256)
    last_name = models.CharField(max_length=256)

    @lookup_property
    def full_name(self):
        return Concat("first_name", Value(" "), "last_name")

Here are the converters required to transform this to a python statement AST:

import ast
from django.db import models
from django.db.models import functions
from lookup_property import expression_to_ast, State
from lookup_property.converters.utils import ast_property

@expression_to_ast.register
def _(expression: str, state: State) -> ast.Constant:
    # Called by converters for Value and ConcatPair to convert
    # the strings they contain to ast constants.
    return ast.Constant(value=expression)

@expression_to_ast.register
def _(expression: models.Value, state: State) -> ast.AST:
    # Convert the `value` inside the Value-class to ast constant.
    # Notice `expression_to_ast` is called recursively.
    return expression_to_ast(expression.value, state)

@expression_to_ast.register
def _(expression: models.F, state: State) -> ast.Attribute:
    # Convert F-objects to self-attributes.
    # This is such a common operation that the library provides a utility for it.
    return ast_property(expression.name)

@expression_to_ast.register
def _(expression: functions.Concat, state: State) -> ast.AST:
    # Concat is composed of nested ConcatPair-expressions:
    # Concat((ConcatPair("foo", ConcatPair("bar", "baz"))))
    # Convert the concat pairs recursively.
    source_expressions = expression.get_source_expressions()
    return expression_to_ast(source_expressions[0], state)

@expression_to_ast.register
def _(expression: functions.ConcatPair, state: State) -> ast.BinOp:
    # First convert the left and right ConcatPair elements
    # to their AST nodes (string constants in this case)
    # and then compose them into a BinOp (left + right)
    source_expressions = expression.get_source_expressions()
    left = expression_to_ast(source_expressions[0], state)
    right = expression_to_ast(source_expressions[1], state)
    return ast.BinOp(left=left, op=ast.Add(), right=right)

Lookups

To convert lookups inside L and Q expressions, there is another single-dispatch function lookup_property.converters.lookups.lookup_to_ast.

For example, to convert the following lookup-property:

from lookup_property import lookup_property
from django.db import models

class Student(models.Model):
    name = models.CharField(max_length=256)

    @lookup_property
    def start_with_letter_a(self):
        return models.Q(name__startswith="A")

Here's the lookup converter required to transform this to a python statement AST:

import ast
from django.db.models import lookups
from lookup_property import lookup_to_ast, expression_to_ast, State
from lookup_property.converters.utils import ast_method

# This is a custom single-dispatch function, and registering works
# a bit differently: `lookup` keyword must be specified for register.
# Q(foo__bar__lookup=val) -> (attrs: ["foo", "bar"], value: val)
@lookup_to_ast.register(lookup=lookups.StartsWith.lookup_name)
def _(attrs: list[str], value: str, state: State) -> ast.Call:
    # `ast_method` is another utility function, which outputs the AST
    # for a method like: self.foo.bar.method(*args, **kwargs)
    return ast_method("startswith", attrs, expression_to_ast(value, state))

Casts

Yet another single-dispatch function exists for the Cast operation, and can be used to select a proper callable for conversion.

import ast
from django.db import models
from lookup_property import State, convert_django_field

@convert_django_field.register
def _(field: models.BooleanField, state: State) -> ast.Name:
    # Should return the name of the function that can be used
    # to convert to the given model field type
    return ast.Name(id="bool", ctx=ast.Load())