Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,5 @@ cython_debug/
#.idea/
/docs/object_filtering.ebnf
/dist_old
/.vscode
/.vscode
temp/
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Object Filtering

![Build Status](https://github.com/KyberCritter/Object-Filtering/actions/workflows/python-package.yml/badge.svg?branch=main)
![Python Versions](https://img.shields.io/badge/python-3.10--3.13-blue)
![Python Versions](https://img.shields.io/badge/python-3.10--3.14-blue)

A Python module for determining whether arbitrary Python objects meet a set of defined criteria. Filters use JSON to represent a set of criteria that objects must meet. Filters can be arbitrarily nested and can contain conditional logic.

Expand All @@ -23,6 +23,6 @@ See `/docs/filter_specifications.md` for details on filter implementation.

## License

(c) 2025 Scott Ratchford.
(c) 2026 Scott Ratchford.

`object_filtering` is licensed under the MIT License. See `LICENSE.txt` for details.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ packages = ["object_filtering"]

[project]
name = "object_filtering"
version = "0.2.0"
version = "0.3.0"
authors = [
{ name="Scott Ratchford", email="object_filtering@scottratchford.com" },
]
Expand Down
7 changes: 7 additions & 0 deletions src/object_filtering/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
VALID_MULTI_VALUE_BEHAVIORS,
SPECIAL_VARIABLES,
CLASS_VARIABLE_OPERATORS,
_LogicalExpressionBase,
ObjectFilter,
Rule,
GroupExpression,
Expand Down Expand Up @@ -37,6 +38,12 @@
execute_filter_list_on_array,
execute_filter_list_on_object_get_first_success,
ObjectWrapper,
Operator,
LogicalOperator,
MultiValueBehavior,
ClassVariableOperator,
logical_expression_from_dict,
dict_to_logical_expression,
)

from .natural_language import (
Expand Down
101 changes: 61 additions & 40 deletions src/object_filtering/natural_language/natural_language.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
# (c) 2025 Scott Ratchford
# (c) 2026 Scott Ratchford
# This file is licensed under the MIT License. See LICENSE.txt for details.

#!/usr/bin/env python3
import json
import sys
from ..object_filtering import (
ObjectFilter, Rule, GroupExpression, ConditionalExpression,
LogicalExpression,
LogicalExpression, SPECIAL_VARIABLES,
)


# Map operators to English phrases
OPERATOR_MAP = {
"<": "is less than",
Expand All @@ -19,12 +17,37 @@
">": "is greater than"
}

# Map special variables to English phrases
SPECIAL_VARIABLE_MAP = {
"$CLASS$": {
"==": "the object is a {value}.",
"!=": "the object is not a {value}.",
},
}

def _explain_special_variable(rule: Rule) -> str:
"""Convert a Rule with a special variable criterion into a natural
language phrase.

Args:
rule (Rule): A Rule whose criterion is a special variable.

Returns:
str: A natural language description of the rule.
"""
templates = SPECIAL_VARIABLE_MAP.get(rule.criterion)
if templates and rule.operator in templates:
return templates[rule.operator].format(value=rule.comparison_value)
return f"{rule.criterion} {OPERATOR_MAP[rule.operator]} {rule.comparison_value}."

def explain_expression(expr: LogicalExpression, depth: int = 0) -> str:
"""Recursively convert a logical expression into an indented, multi-line English-language description.
"""Recursively convert a logical expression into an indented, multi-line
English-language description.

Args:
expr (LogicalExpression): The LogicalExpression to explain.
depth (int, optional): Depth of the expression within a larger expression. Defaults to 0.
depth (int, optional): Depth of the expression within a larger
expression. Defaults to 0.

Raises:
TypeError: If the type of expr is not LogicalExpression.
Expand All @@ -39,28 +62,29 @@ def explain_expression(expr: LogicalExpression, depth: int = 0) -> str:
text = "This condition is always true." if expr else "This condition is always false."
return f"{indent}{text}"

# Rule objects or dicts
if isinstance(expr, (Rule, dict)) and set(expr.keys()).issuperset({"criterion", "operator", "comparison_value"}):
criterion = expr["criterion"]
op = expr["operator"]
val = expr["comparison_value"]
# Rule objects
if isinstance(expr, Rule):
# Special variable handling
if expr.criterion in SPECIAL_VARIABLES:
text = _explain_special_variable(expr)
return f"{indent}{text}"

params = expr.get("parameters", [])
if params:
if len(params) == 1:
param_str = f" with parameter {params[0]}"
else:
joined = ", ".join(map(str, params[:-1]))
param_str = f" with parameters {joined} and {params[-1]}"
text = f"The result of calling {criterion}{param_str} {OPERATOR_MAP[op]} {val}."
text = f"The result of calling {expr.criterion}{param_str} {OPERATOR_MAP[expr.operator]} {expr.comparison_value}."
else:
text = f"{criterion} {OPERATOR_MAP[op]} {val}."
text = f"{expr.criterion} {OPERATOR_MAP[expr.operator]} {expr.comparison_value}."
return f"{indent}{text}"

# GroupExpression objects or dicts
if isinstance(expr, (GroupExpression, dict)) and "logical_operator" in expr:
conj = expr["logical_operator"]
# GroupExpression objects
if isinstance(expr, GroupExpression):
parts = expr.get("logical_expressions", [])
if conj == "and":
if expr.logical_operator == "and":
header = "All of the following conditions must be met:"
else:
header = "At least one of the following conditions must be met:"
Expand All @@ -70,22 +94,24 @@ def explain_expression(expr: LogicalExpression, depth: int = 0) -> str:
lines.append(f"{indent} - {sub_text}")
return "\n".join(lines)

# ConditionalExpression objects or dicts
if isinstance(expr, (ConditionalExpression, dict)) and set(expr.keys()).issuperset({"if", "then", "else"}):
cond_lines = explain_expression(expr["if"], depth + 1).strip()
then_lines = explain_expression(expr["then"], depth + 1).strip()
else_lines = explain_expression(expr["else"], depth + 1).strip()
lines = [f"{indent}If the following condition holds:",
f"{indent} - {cond_lines}",
f"{indent}Then:",
f"{indent} - {then_lines}",
f"{indent}Otherwise:",
f"{indent} - {else_lines}"]
# ConditionalExpression objects
if isinstance(expr, ConditionalExpression):
cond_lines = explain_expression(expr._if, depth + 1).strip()
then_lines = explain_expression(expr._then, depth + 1).strip()
else_lines = explain_expression(expr._else, depth + 1).strip()
lines = [
f"{indent}If the following condition holds:",
f"{indent} - {cond_lines}",
f"{indent}Then:",
f"{indent} - {then_lines}",
f"{indent}Otherwise:",
f"{indent} - {else_lines}"
]
return "\n".join(lines)

raise TypeError(f"Unsupported expression type: {expr}")

def explain_filter(obj_filter: ObjectFilter) -> str:
def explain_filter(object_filter: ObjectFilter) -> str:
"""Generate an English explanation of the entire ObjectFilter.

Args:
Expand All @@ -94,17 +120,12 @@ def explain_filter(obj_filter: ObjectFilter) -> str:
Returns:
str: An English-language description of the ObjectFilter.
"""
name = obj_filter.get("name", "(unnamed)")
desc = obj_filter.get("description", "")
types = obj_filter.get("object_types", [])
expr = obj_filter.get("logical_expression", True)

header = f"Filter \"{name}\": {desc}".strip()
if len(types) > 1:
type_list = ", ".join(types[:-1]) + f", and {types[-1]}"
header = f"Filter \"{object_filter.name}\": {object_filter.description}".strip()
if len(object_filter.object_types) > 1:
type_list = ", ".join(object_filter.object_types[:-1]) + f", and {object_filter.object_types[-1]}"
applies = f"This filter applies to objects of types: {type_list}."
else:
applies = f"This filter applies to objects of type: {types[0]}."
applies = f"This filter applies to objects of type: {object_filter.object_types[0]}."

criteria = explain_expression(expr)
criteria = explain_expression(object_filter.logical_expression)
return f"{header}\n{applies}\n{criteria}"
Loading