Lambda Filter Parser for Specification Pattern

Parser for Python lambda functions that converts them into Specification Pattern AST nodes.

Description

This module converts Python lambda functions into Specification Pattern AST nodes, inspired by the approach from hypothesis.internal.filtering and hypothesis.internal.lambda_sources.

This allows using predicate functions in the Specification Pattern while maintaining high performance.

Key Features

  • Simple comparisons - ==, !=, >, <, >=, <=

  • Logical operators - and, or, not

  • Arithmetic operators - +, -, *, /, %

  • Nested expressions - complex combinations of operators

  • Method-based operators - Eq(), Lt(), Gte(), IsNull(), etc.

  • Wildcard collections - any([list comprehension]) and any(generator)

  • Nested wildcards - any([any([...]) for ...]) - Wildcard inside Wildcard

  • Literal types - strings, numbers, boolean, float

Usage

Basic Examples

from ascetic_ddd.specification.domain.lambda_filter import parse
from ascetic_ddd.specification.domain.evaluate_visitor import EvaluateVisitor

# Simple comparison
spec = parse(lambda user: user.age > 25)

class DictContext:
    def __init__(self, data):
        self._data = data

    def get(self, key):
        return self._data[key]

user = DictContext({"age": 30})
visitor = EvaluateVisitor(user)
spec.accept(visitor)
print(visitor.result())  # True

Logical Operators

# AND
spec = parse(lambda user: user.age > 25 and user.active == True)

# OR
spec = parse(lambda user: user.age < 18 or user.age > 65)

# NOT
spec = parse(lambda user: not user.deleted)

# Complex expressions
spec = parse(lambda user: user.age >= 18 and user.age <= 65 and user.active == True)

Method-Based Operators

Method-based operators allow expressing comparisons as method calls on fields. Both operator syntax and method syntax produce identical AST nodes.

# Comparison methods
spec = parse(lambda user: user.age.Eq(30))       # Equal
spec = parse(lambda user: user.age.Ne(30))        # NotEqual
spec = parse(lambda user: user.age.Gt(25))        # GreaterThan
spec = parse(lambda user: user.age.Gte(25))       # GreaterThanEqual
spec = parse(lambda user: user.age.Lt(30))        # LessThan
spec = parse(lambda user: user.age.Lte(30))       # LessThanEqual

# Postfix methods
spec = parse(lambda user: user.email.IsNull())    # IsNull
spec = parse(lambda user: user.email.IsNotNull()) # IsNotNull

# Nested paths
spec = parse(lambda user: user.profile.age.Gte(18))

# Combined with logical operators
spec = parse(lambda user: user.age.Gte(18) and user.age.Lte(65))
spec = parse(lambda user: user.email.IsNull() or user.email.Eq(""))

# Inside wildcards
spec = parse(lambda store: any(item.price.Gt(500) for item in store.items))

Supported Method Aliases

Node

Method aliases

Equal

Equal(), Equals(), Eq()

NotEqual

NotEqual(), NotEquals(), Ne(), Neq()

LessThan

LessThan(), Lt()

LessThanEqual

LessThanOrEqual(), LessThanEqual(), Lte(), Le()

GreaterThan

GreaterThan(), Gt()

GreaterThanEqual

GreaterThanOrEqual(), GreaterThanEqual(), Gte(), Ge()

IsNull

IsNull()

IsNotNull

IsNotNull()

Wildcard Collections (any)

from ascetic_ddd.specification.domain.evaluate_visitor import CollectionContext

# Generator expression
spec = parse(lambda store: any(item.price > 500 for item in store.items))

item1 = DictContext({"name": "Laptop", "price": 999})
item2 = DictContext({"name": "Mouse", "price": 29})

items = CollectionContext([item1, item2])
store = DictContext({"items": items})

visitor = EvaluateVisitor(store)
spec.accept(visitor)
print(visitor.result())  # True (Laptop price > 500)
# List comprehension
spec = parse(lambda store: any([item.price > 500 for item in store.items]))

# Complex predicate
spec = parse(lambda store: any(
    item.price > 100 and item.available == True
    for item in store.items
))

Nested Wildcards

# Nested any - check items across all categories
spec = parse(lambda order: any([
    any([item.price > 100 for item in category.items])
    for category in order.categories
]))

# Create data structure
item1 = DictContext({"name": "Laptop", "price": 150})
item2 = DictContext({"name": "Mouse", "price": 50})
items = CollectionContext([item1, item2])
category = DictContext({"name": "Electronics", "items": items})

categories = CollectionContext([category])
order = DictContext({"id": 1, "categories": categories})

visitor = EvaluateVisitor(order)
spec.accept(visitor)
print(visitor.result())  # True (there is an item with price > 100)

Supported Features

Comparison Operators

  • == - Equal

  • != - Not equal

  • > - Greater than

  • < - Less than

  • >= - Greater than or equal

  • <= - Less than or equal

Logical Operators

  • and - Logical AND

  • or - Logical OR

  • not - Logical NOT

Arithmetic Operators

  • + - Addition

  • - - Subtraction

  • * - Multiplication

  • / - Division

  • % - Modulo

Collections

  • any(generator) - Converts to Wildcard

  • any([list comprehension]) - Converts to Wildcard

  • all(generator) - Converts to Wildcard

  • all([list comprehension]) - Converts to Wildcard

  • Nested wildcards - any([any([...]) for ...]) - Supported

Literal Types

# Strings
parse(lambda user: user.name == "Alice")

# Numbers
parse(lambda user: user.age > 25)
parse(lambda product: product.price > 99.99)

# Boolean
parse(lambda user: user.active == True)
parse(lambda user: user.deleted == False)

Arithmetic Operations

# Addition
parse(lambda user: user.age + 5 > 30)

# Subtraction
parse(lambda user: user.age - 5 >= 18)

# Multiplication
parse(lambda product: product.price * 2 > 100)

# Division
parse(lambda user: user.score / 2 >= 40)

# Modulo
parse(lambda user: user.id % 2 == 0)  # Even IDs

# Complex expressions
parse(lambda user: (user.age + 5) * 2 > 60)

Architecture

Parsing Process

Lambda Function
      |
[inspect.findsource] Extract source code
      |
[ast.parse] Parse into Python AST
      |
[_find_all_lambdas] Find lambda nodes
      |
[_convert_node] Convert to Specification AST
      |
Specification Nodes (And, Or, Equal, Field, Value, Wildcard, etc.)

Components

  1. LambdaParser - Main parser class

    • parse() - Finds lambda in source code

    • _convert_node() - Dispatches by AST node type

    • _convert_compare() - Comparison operators

    • _convert_bool_op() - Logical operators

    • _convert_call() - Function and method calls (any, all, Eq, IsNull, etc.)

    • _convert_method_comparison() - Method-based comparisons (receiver.Method(arg))

    • _convert_method_postfix() - Postfix methods (receiver.Method())

    • _convert_generator_to_wildcard() - Generator -> Wildcard

    • _convert_listcomp_to_wildcard() - List comprehension -> Wildcard

  2. Context Tracking

    • arg_name - Lambda argument name

    • _in_item_context - Flag for wildcard context

  3. AST Nodes Mapping

    ast.Compare + ast.Eq      -> Equal
    ast.Compare + ast.Gt      -> GreaterThan
    ast.Compare + ast.Lt      -> LessThan
    ast.BoolOp + ast.And      -> And
    ast.BoolOp + ast.Or       -> Or
    ast.UnaryOp + ast.Not     -> Not
    ast.BinOp + ast.Add       -> Add
    ast.BinOp + ast.Sub       -> Sub
    ast.BinOp + ast.Mult      -> Mul
    ast.BinOp + ast.Div       -> Div
    ast.BinOp + ast.Mod       -> Mod
    ast.Call + .Eq()           -> Equal
    ast.Call + .IsNull()       -> IsNull
    ast.Attribute              -> Field
    ast.Constant               -> Value
    ast.GeneratorExp           -> Wildcard
    ast.ListComp               -> Wildcard
    

AST Transformation Examples

Simple Comparison

lambda user: user.age > 25

# Transforms to:
GreaterThan(
    Field(GlobalScope(), "age"),
    Value(25)
)

Logical AND

lambda user: user.age > 25 and user.active == True

# Transforms to:
And(
    GreaterThan(Field(GlobalScope(), "age"), Value(25)),
    Equal(Field(GlobalScope(), "active"), Value(True))
)

Method-Based Comparison

lambda user: user.age.Gte(18)

# Transforms to:
GreaterThanEqual(
    Field(GlobalScope(), "age"),
    Value(18)
)

Postfix Method

lambda user: user.email.IsNull()

# Transforms to:
IsNull(
    Field(GlobalScope(), "email")
)

Wildcard

lambda store: any(item.price > 500 for item in store.items)

# Transforms to:
Wildcard(
    Object(GlobalScope(), "items"),
    GreaterThan(Field(Item(), "price"), Value(500))
)

Nested Wildcard

lambda order: any([
    any([item.price > 100 for item in category.items])
    for category in order.categories
])

# Transforms to:
Wildcard(
    Object(GlobalScope(), "categories"),
    Wildcard(
        Object(Item(), "items"),
        GreaterThan(Field(Item(), "price"), Value(100))
    )
)

Arithmetic Operations

lambda user: user.age + 5 > 30

# Transforms to:
GreaterThan(
    Add(Field(GlobalScope(), "age"), Value(5)),
    Value(30)
)
lambda user: user.id % 2 == 0

# Transforms to:
Equal(
    Mod(Field(GlobalScope(), "id"), Value(2)),
    Value(0)
)

Testing

# Run lambda parser tests
python -m unittest ascetic_ddd.specification.domain.tests.lambda_filter.test_lambda_parser -v

When to Use Lambda Filter

Choose Lambda Filter if:

  • You need IDE support and autocomplete

  • Static type checking matters (mypy, pyright)

  • You want native Python syntax without strings

  • You need refactoring support (rename fields, etc.)

  • Minimal external dependencies

Limitations

The current version does not support:

  • Nested lambda functions (parse(lambda user: (lambda x: x > 25)(user.age)))

  • Lambdas with multiple arguments

  • Slice operations (e.g., list[0:5])

  • Ternary operators (x if condition else y)

  • Bitwise operations (except <<, >>)

Inspiration

This module is inspired by approaches from:

  • hypothesis.internal.filtering - AST analysis of predicates

  • hypothesis.internal.lambda_sources - Lambda source code extraction

  • JSONPath parsers - Conversion to Specification AST