From df5e9748606854d622a448b91797ce1ecdf3e9e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Sierro?= Date: Thu, 3 Aug 2023 10:26:56 +0200 Subject: [PATCH] better evaluator errors --- src/scgenerator/evaluator.py | 99 ++++++++++++++++++++---------------- src/scgenerator/io.py | 1 + 2 files changed, 57 insertions(+), 43 deletions(-) diff --git a/src/scgenerator/evaluator.py b/src/scgenerator/evaluator.py index 5f193ff..6568064 100644 --- a/src/scgenerator/evaluator.py +++ b/src/scgenerator/evaluator.py @@ -15,71 +15,66 @@ from scgenerator.utils import _mock_function, func_rewrite, get_arg_names, get_l class ErrorRecord(NamedTuple): error: Exception + lookup_stack: list[str] + rules_stack: list[Rule] traceback: str class EvaluatorError(Exception): - pass + target: str | None = None class NoValidRuleError(EvaluatorError): def __init__(self, target: str): - ... + self.target = target + super().__init__(f"no valid rule to compute {target!r}") class CyclicDependencyError(EvaluatorError): def __init__(self, target: str, lookup_stack: list[str]): - ... + self.target = target + cycle = " → ".join(lookup_stack) + super().__init__(f"cycle detected while computing {target!r}:\n{cycle}") class AllRulesExhaustedError(EvaluatorError): def __init__(self, target: str): - ... + self.target = target + super().__init__(f"tried every rule for {target!r} and no default value is set") class EvaluatorErrorTree: target: str - all_errors: dict[Type, ErrorRecord] - - level: int + all_errors: list[ErrorRecord] def __init__(self, target: str): self.target = target - self.all_errors = {} - self.level = 0 + self.all_errors = [] def __repr__(self) -> str: return f"{self.__class__.__name__}(target={self.target!r})" - def push(self): - self.level += 1 + def __len__(self) -> int: + return len(self.all_errors) - def pop(self): - self.level -= 1 - - def append(self, error: Exception): + def append(self, error: Exception, lookup_stack: list[str], rules_stack: list[Rule]): tr = traceback.format_exc().splitlines() - tr = "\n".join(tr[:1] + tr[3:]) # get rid of the `append` frame - self.all_errors[type(error)] = ErrorRecord(error, tr) + tr = "\n".join(tr[:1] + tr[3:]) # get rid of the `EvaluatorErrorTree.append` frame + self.all_errors.append(ErrorRecord(error, lookup_stack.copy(), rules_stack.copy(), tr)) def compile_error(self) -> EvaluatorError: - raise EvaluatorError(f"Couldn't compute {self.target}.") + failed_rules = set(rec.rules_stack[-1] for rec in self.all_errors if rec.rules_stack) + failed_rules = ("\n" + "-" * 80 + "\n").join(rule.pretty_format() for rule in failed_rules) - def summary(self) -> dict[str, int]: - return {k.__name__: len(v) for k, v in self.all_errors.items()} - - def format_summary(self) -> str: - types = [f"{v} errors of type {k!r}" for k, v in self.summary().items()] - return ", ".join(types[:-2] + [" and ".join(types[-2:])]) - - def details(self) -> str: - ... + raise EvaluatorError( + f"Couldn't compute {self.target}. {len(self)} rules failed.\n{failed_rules}" + ) class Rule: targets: dict[str, int] func: Callable - args: list[str] + args: tuple[str] conditions: dict[str, Any] mock_func: Callable @@ -88,7 +83,7 @@ class Rule: self, target: Union[str, list[Optional[str]]], func: Callable, - args: list[str] | None = None, + args: tuple[str] | None = None, priorities: Union[int, list[int]] | None = None, conditions: dict[str, str] | None = None, ): @@ -101,7 +96,7 @@ class Rule: self.targets = dict(zip(targets, priorities)) if args is None: args = get_arg_names(func) - self.args = args + self.args = tuple(args) self.mock_func = _mock_function(len(self.args), len(self.targets)) self.conditions = conditions or {} @@ -115,7 +110,14 @@ class Rule: ) def __eq__(self, other: Rule) -> bool: - return self.func == other.func + return ( + self.args == other.args + and tuple(self.targets) == tuple(other.targets) + and self.func == other.func + ) + + def __hash__(self) -> int: + return hash((self.args, self.func, tuple(self.targets))) def pretty_format(self) -> str: return io.format_graph(self.args, self.func_name, self.targets) @@ -274,10 +276,14 @@ class Evaluator: """ errors = EvaluatorErrorTree(target) param_chain_map = ChainMap(self.main_map) + lookup_stack = [] + rules_stack = [] try: - value = self._compute(target, check_only, param_chain_map, [], errors) + value = self._compute( + target, check_only, param_chain_map, lookup_stack, rules_stack, errors + ) except EvaluatorError as e: - errors.append(e) + errors.append(e, lookup_stack, rules_stack) raise errors.compile_error() from None self.merge_chain_map(param_chain_map) return value @@ -288,22 +294,28 @@ class Evaluator: check_only: bool, param_chain_map: MutableMapping[str, EvaluatedValue], lookup_stack: list[str], + rules_stack: list[Rule], errors: EvaluatorErrorTree, ) -> Any: - errors.push() if target in param_chain_map: return param_chain_map[target].value if target not in self.rules or len(self.rules[target]) == 0: raise NoValidRuleError(target) if target in lookup_stack: - raise CyclicDependencyError(target, lookup_stack.copy()) + raise CyclicDependencyError(target) lookup_stack.append(target) base_cm_length = len(param_chain_map.maps) for rule in self.rules[target]: - values = self.apply_rule(rule, check_only, param_chain_map, lookup_stack, errors) - if self.valid_values(values, rule, check_only, param_chain_map, lookup_stack, errors): + rules_stack.append(rule) + values = self.apply_rule( + rule, check_only, param_chain_map, lookup_stack, rules_stack, errors + ) + if self.valid_values( + values, rule, check_only, param_chain_map, lookup_stack, rules_stack, errors + ): break + rules_stack.pop() lookup_stack.pop() if values is None: @@ -314,7 +326,6 @@ class Evaluator: raise AllRulesExhaustedError(target) param_chain_map.maps.insert(0, values) - errors.pop() return values[target].value def valid_values( @@ -324,6 +335,7 @@ class Evaluator: check_only: bool, param_chain_map: MutableMapping[str, EvaluatedValue], lookup_stack: list[str], + rules_stack: list[Rule], errors: EvaluatorErrorTree, ) -> bool: if values is None: @@ -332,7 +344,7 @@ class Evaluator: return True for arg, target_value in rule.conditions.items(): - value = self._compute(arg, False, param_chain_map, lookup_stack, errors) + value = self._compute(arg, False, param_chain_map, lookup_stack, rules_stack, errors) if value != target_value: return False @@ -344,13 +356,14 @@ class Evaluator: check_only: bool, param_chain_map: MutableMapping[str, EvaluatedValue], lookup_stack: list[str], + rules_stack: list[Rule], errors: EvaluatorErrorTree, ) -> dict[str, EvaluatedValue] | None: try: for arg in rule.args: - self._compute(arg, check_only, param_chain_map, lookup_stack, errors) + self._compute(arg, check_only, param_chain_map, lookup_stack, rules_stack, errors) except Exception as e: - errors.append(e) + errors.append(e, lookup_stack, rules_stack) return None args = [param_chain_map[k].value for k in rule.args] @@ -360,7 +373,7 @@ class Evaluator: try: values = func(*args) except Exception as e: - self.record_error(e) + errors.append(e, lookup_stack, rules_stack) return None if not isinstance(values, tuple): @@ -380,7 +393,7 @@ class Evaluator: self, param_chain_map: MutableMapping[str, EvaluatedValue], target_size: int ): while len(param_chain_map.maps) > target_size: - param_chain_map.pop(0) + param_chain_map.maps.pop(0) def validate_condition(self, rule: Rule) -> bool: try: diff --git a/src/scgenerator/io.py b/src/scgenerator/io.py index f892f26..53ef5df 100644 --- a/src/scgenerator/io.py +++ b/src/scgenerator/io.py @@ -1,6 +1,7 @@ import datetime import json from pathlib import Path +from typing import Sequence import pkg_resources