orchestrata_tools.test_generator.testgen.conditions
Reference › orchestrata_tools › test_generator › testgen › conditions
This module derives concrete input values from the text of a condition, without executing anything. Given a condition such as not tag, b == 0, value is None, user_id < 0 or tag in self.tags, it returns a ConditionInputs record with true_inputs (values that make the condition true), false_inputs, any setup_code lines needed before the call, and a notes string. analyse(condition, args_info) is the entry point: it parses the condition and uses the function's argument information to choose values. It handles the common guard patterns of real Python code. The module is self-contained, using the standard ast module and its sibling types, and is imported only by branch. Tests are tests/test_orchestrata_tools_testgen_conditions_coverage.py and ..._units.py.
At a glance
| Source | src/orchestrata_tools/test_generator/testgen/conditions.py |
| Lines of code | 382 |
| Classes | 1 |
| Functions | 11 |
| Imports (internal) | orchestrata_tools.test_generator.testgen.types |
| Imported by | orchestrata_tools.test_generator.testgen.branch |
| Tests that import it | tests/test_orchestrata_tools_testgen_conditions_coverage.py, tests/test_orchestrata_tools_testgen_units.py |
Class diagram
Dependency diagram
API
Constants
| Name | Type | Value |
|---|---|---|
_EMPTY |
ConditionInputs |
ConditionInputs({}, {}, [], '') |
Classes
ConditionInputs (dataclass)
Decorators: dataclass · Line 32
Concrete values derived from one condition expression.
| Attribute | Type | Default |
|---|---|---|
true_inputs |
dict[str, str] |
|
false_inputs |
dict[str, str] |
|
setup_code |
list[str] |
|
notes |
str |
Functions
| Function | Signature | Summary |
|---|---|---|
analyse |
analyse(condition: str, args_info: dict) -> ConditionInputs |
Parse condition and return ConditionInputs for the true and false paths. |
_analyse_node |
_analyse_node(node: ast.expr, ann_map: dict[str, str], raw: str) -> ConditionInputs |
|
_build_ann_map |
_build_ann_map(args_info: dict) -> dict[str, str] |
name → annotation string from the args dict. |
_name |
_name(node: ast.expr) -> str |
Extract a simple name string from a Name or Attribute node. |
_const_repr |
_const_repr(node: ast.expr) -> str |
Return the source repr of a constant node. |
_typed_value |
_typed_value(name: str, ann_map: dict[str, str], override_type: str | None = None) -> str |
Return a valid sentinel value for a parameter, respecting annotation. |
_other_value |
_other_value(val: str, name: str, ann_map: dict[str, str]) -> str |
Return a value that is NOT equal to val. |
_comparison_values |
_comparison_values(op: ast.cmpop, val_repr: str) -> tuple[str, str] |
Given a comparison operator and the RHS value, return (true_value, false_value) for the LHS variable. |
_op_str |
_op_str(op: ast.cmpop) -> str |
|
_truthy |
_truthy(ann: str | None) -> str |
A truthy value for the given annotation. |
_falsy |
_falsy(ann: str | None) -> str |
A falsy value for the given annotation. |
Source
"""conditions.py — Derives concrete input values from AST condition expressions.
Given a condition string like "not tag", "b == 0", "value is None",
"user_id < 0", "tag in self.tags" — returns:
ConditionInputs(
true_inputs = {"tag": '""'}, # inputs that make condition True
false_inputs = {"tag": '"hello"'}, # inputs that make condition False
setup_code = [], # extra lines needed before the call
notes = "empty string triggers the branch"
)
Handles the most common guard patterns in real Python code without
needing to execute anything.
Restored 2026-08-19 from ``scripts/ast_serializer/testgen`` (deleted
2026-07-09, commit 6a186c9d), unchanged in substance: this module was
already self-contained (stdlib ``ast`` + its own sibling ``.types``), with
no dependency on the deleted parser's ``StatementRecord``/``ModuleRecord``
tree — only its imports moved to absolute paths.
"""
from __future__ import annotations
import ast
from dataclasses import dataclass
from orchestrata_tools.test_generator.testgen.types import sentinel
@dataclass
class ConditionInputs:
"""Concrete values derived from one condition expression."""
true_inputs: dict[str, str] # arg_name → value_repr (makes condition True)
false_inputs: dict[str, str] # arg_name → value_repr (makes condition False)
setup_code: list[str] # lines to emit before the call (e.g. pre-populate a list)
notes: str # human-readable explanation
_EMPTY: ConditionInputs = ConditionInputs({}, {}, [], "")
def analyse(condition: str, args_info: dict) -> ConditionInputs:
"""
Parse `condition` and return ConditionInputs for the true and false paths.
`args_info` is the ``args`` dict built by
``orchestrata_tools.test_generator.testgen.adapter.args_info_from_ast`` —
used to map parameter names to their type annotations.
"""
if not condition:
return _EMPTY
ann_map = _build_ann_map(args_info)
try:
tree = ast.parse(condition, mode="eval")
node = tree.body
except SyntaxError:
return _EMPTY
return _analyse_node(node, ann_map, condition)
# ---------------------------------------------------------------------------
# Dispatcher
# ---------------------------------------------------------------------------
def _analyse_node(node: ast.expr, ann_map: dict[str, str], raw: str) -> ConditionInputs:
# not X
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
inner = _analyse_node(node.operand, ann_map, raw)
# Flip true/false
return ConditionInputs(
true_inputs=inner.false_inputs,
false_inputs=inner.true_inputs,
setup_code=inner.setup_code,
notes=f"negation of: {inner.notes}",
)
# X is None / X is not None
if isinstance(node, ast.Compare) and len(node.ops) == 1:
if isinstance(node.ops[0], ast.Is):
if (
isinstance(node.comparators[0], ast.Constant)
and node.comparators[0].value is None
):
name = _name(node.left)
return ConditionInputs(
true_inputs={name: "None"} if name else {},
false_inputs={name: _typed_value(name, ann_map)} if name else {},
setup_code=[],
notes=f"{name} is None",
)
if isinstance(node.ops[0], ast.IsNot):
if (
isinstance(node.comparators[0], ast.Constant)
and node.comparators[0].value is None
):
name = _name(node.left)
return ConditionInputs(
true_inputs={name: _typed_value(name, ann_map)} if name else {},
false_inputs={name: "None"} if name else {},
setup_code=[],
notes=f"{name} is not None",
)
# X == value / X != value
if isinstance(node.ops[0], (ast.Eq, ast.NotEq)):
name = _name(node.left)
val = _const_repr(node.comparators[0])
other = _other_value(val, name, ann_map)
if isinstance(node.ops[0], ast.Eq):
return ConditionInputs(
true_inputs={name: val} if name else {},
false_inputs={name: other} if name else {},
setup_code=[],
notes=f"{name} equals {val}",
)
else:
return ConditionInputs(
true_inputs={name: other} if name else {},
false_inputs={name: val} if name else {},
setup_code=[],
notes=f"{name} not equal to {val}",
)
# X < val / X <= val / X > val / X >= val
if isinstance(node.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)):
name = _name(node.left)
val = _const_repr(node.comparators[0])
op = node.ops[0]
true_v, false_v = _comparison_values(op, val)
return ConditionInputs(
true_inputs={name: true_v} if name else {},
false_inputs={name: false_v} if name else {},
setup_code=[],
notes=f"{name} {_op_str(op)} {val}",
)
# X in Y / X not in Y
if isinstance(node.ops[0], (ast.In, ast.NotIn)):
item = _name(node.left)
coll = _name(node.comparators[0])
item_val = _typed_value(item, ann_map) if item else '"item"'
is_in = isinstance(node.ops[0], ast.In)
if is_in:
# true: collection contains item
# false: collection does not contain item
setup_true = [f"# pre-populate {coll} so it contains {item_val}"]
return ConditionInputs(
true_inputs={item: item_val} if item else {},
false_inputs={item: item_val} if item else {},
setup_code=setup_true,
notes=f"{item} found in {coll} (true) vs not found (false)",
)
else:
return ConditionInputs(
true_inputs={item: item_val} if item else {},
false_inputs={item: item_val} if item else {},
setup_code=[f"# ensure {coll} does NOT contain {item_val}"],
notes=f"{item} not in {coll}",
)
# isinstance(x, T)
if isinstance(node, ast.Call):
func = ast.unparse(node.func)
if func == "isinstance" and len(node.args) == 2:
name = _name(node.args[0])
tp = ast.unparse(node.args[1])
tv = _typed_value(name, ann_map, override_type=tp)
return ConditionInputs(
true_inputs={name: tv} if name else {},
false_inputs={name: '"wrong_type"'} if name else {},
setup_code=[],
notes=f"isinstance({name}, {tp})",
)
# hasattr(obj, 'attr')
if isinstance(node, ast.Call):
func = ast.unparse(node.func)
if func == "hasattr" and len(node.args) == 2:
return ConditionInputs(
true_inputs={},
false_inputs={},
setup_code=["# ensure obj has/lacks the attribute"],
notes=f"hasattr({ast.unparse(node.args[0])}, {ast.unparse(node.args[1])})",
)
# bool(x) / truthiness: just a Name node e.g. "if value:"
if isinstance(node, ast.Name):
name = node.id
ann = ann_map.get(name)
return ConditionInputs(
true_inputs={name: _truthy(ann)},
false_inputs={name: _falsy(ann)},
setup_code=[],
notes=f"{name} is truthy / falsy",
)
# Attribute access: self.flag, obj.active, etc.
if isinstance(node, ast.Attribute):
attr = ast.unparse(node)
return ConditionInputs(
true_inputs={},
false_inputs={},
setup_code=[f"# arrange so that {attr} is True / False"],
notes=f"attribute condition: {attr}",
)
# Boolean AND / OR — analyse each operand independently
if isinstance(node, ast.BoolOp):
parts = [_analyse_node(v, ann_map, raw) for v in node.values]
if isinstance(node.op, ast.And):
# All must be true for true path; any false for false path
merged_true: dict[str, str] = {}
merged_false: dict[str, str] = {}
setup: list[str] = []
for p in parts:
merged_true.update(p.true_inputs)
if not merged_false:
merged_false.update(p.false_inputs)
setup.extend(p.setup_code)
return ConditionInputs(
merged_true, merged_false, setup, f"AND condition: {raw}"
)
else:
# Any true for true path; all false for false path
merged_true = {}
merged_false = {}
setup = []
for p in parts:
if not merged_true:
merged_true.update(p.true_inputs)
merged_false.update(p.false_inputs)
setup.extend(p.setup_code)
return ConditionInputs(
merged_true, merged_false, setup, f"OR condition: {raw}"
)
return _EMPTY
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _build_ann_map(args_info: dict) -> dict[str, str]:
"""name → annotation string from the args dict."""
m: dict[str, str] = {}
for group in ("posonlyargs", "args", "kwonlyargs"):
for a in args_info.get(group, []):
if a.get("annotation"):
m[a["name"]] = a["annotation"]
return m
def _name(node: ast.expr) -> str:
"""Extract a simple name string from a Name or Attribute node."""
if isinstance(node, ast.Name):
return node.id
if isinstance(node, ast.Attribute):
# e.g. self.value → skip self-attributes for input mapping
return ""
return ""
def _const_repr(node: ast.expr) -> str:
"""Return the source repr of a constant node."""
if isinstance(node, ast.Constant):
return repr(node.value)
try:
return ast.unparse(node)
except Exception:
return "..."
def _typed_value(
name: str, ann_map: dict[str, str], override_type: str | None = None
) -> str:
"""Return a valid sentinel value for a parameter, respecting annotation."""
ann = override_type or ann_map.get(name)
return sentinel(ann)
def _other_value(val: str, name: str, ann_map: dict[str, str]) -> str:
"""Return a value that is NOT equal to val."""
ann = ann_map.get(name)
# Simple heuristics
if val == "0":
return "1"
if val == "True":
return "False"
if val == "False":
return "True"
if val == "None":
return sentinel(ann)
if val.startswith('"') or val.startswith("'"):
return '"different_value"'
try:
n = int(val)
return str(n + 1)
except ValueError:
pass
return sentinel(ann)
def _comparison_values(op: ast.cmpop, val_repr: str) -> tuple[str, str]:
"""
Given a comparison operator and the RHS value, return
(true_value, false_value) for the LHS variable.
"""
# Annotated `float` so the fallback below is a widening, not a retype:
# `int` is acceptable wherever `float` is, and the value stays an int at
# runtime when the first parse succeeds — so `str(n - 1)` still renders
# "4", never "4.0".
n: float
try:
n = int(val_repr)
except ValueError, TypeError:
try:
n = float(val_repr)
except ValueError, TypeError:
return (val_repr, f"not_{val_repr}")
if isinstance(op, ast.Lt):
return (str(n - 1), str(n))
if isinstance(op, ast.LtE):
return (str(n), str(n + 1))
if isinstance(op, ast.Gt):
return (str(n + 1), str(n))
if isinstance(op, ast.GtE):
return (str(n), str(n - 1))
return (val_repr, str(n + 1))
def _op_str(op: ast.cmpop) -> str:
return {
ast.Lt: "<",
ast.LtE: "<=",
ast.Gt: ">",
ast.GtE: ">=",
ast.Eq: "==",
ast.NotEq: "!=",
}.get(type(op), "?")
def _truthy(ann: str | None) -> str:
"""A truthy value for the given annotation."""
if ann == "str":
return '"non_empty"'
if ann == "int":
return "1"
if ann == "float":
return "1.0"
if ann and ann.startswith(("list", "List")):
return '["item"]'
if ann and ann.startswith(("dict", "Dict")):
return '{"key": "value"}'
return sentinel(ann) or "True"
def _falsy(ann: str | None) -> str:
"""A falsy value for the given annotation."""
if ann == "str":
return '""'
if ann == "int":
return "0"
if ann == "float":
return "0.0"
if ann and ann.startswith(("list", "List")):
return "[]"
if ann and ann.startswith(("dict", "Dict")):
return "{}"
if ann and "Optional" in ann:
return "None"
return "None"