Coverage for src/renaissance/syntax_tree/recipe_ast_processor.py: 100%
82 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-15 10:34 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-15 10:34 +0000
1# Copyright (C) 2024-2026 TNO
2#
3# Licensed under the terms specified in the LICENSE file in the root directory
4# of this repository, also available at:
5# https://github.com/TNO/Renaissance.Py/blob/main/LICENSE
7"""AI: Decorator support for annotating recipe actions on AST processors."""
9import functools
10from collections.abc import Callable, Sequence
11from typing import Any, TypeVar
13from .ast_processor import ASTProcessor
14from .batch_ast_processor import BatchASTProcessor, IterableProvider
16TRecipe = TypeVar("TRecipe")
17TFunc = Callable[..., Any]
20def annotate_decorator(foreign_decorator: TFunc, name: str):
21 """AI: Wrap foreign_decorator so the decorated function also records its own decorator and recipe-action name."""
23 def new_decorator(func: TFunc) -> TFunc:
24 r = foreign_decorator(func) # apply foreignDecorator, like call to foreignDecorator(method) would have done
25 r.decorator = new_decorator # keep track of decorator
26 r.recipe_action = name
27 return r
29 new_decorator.__name__ = foreign_decorator.__name__
30 new_decorator.__doc__ = foreign_decorator.__doc__
31 return new_decorator
34def get_methods_with_decorator(cls: type, decorator: TFunc):
35 """AI: Yield all methods of cls that were annotated with the given recipe-action decorator."""
36 for maybe_decorated in cls.__dict__.values():
37 if hasattr(maybe_decorated, "recipe_action") and maybe_decorated.recipe_action == decorator.__name__:
38 yield maybe_decorated
41# Decorators
44def final_action() -> TFunc:
45 """AI: Decorator marking a recipe method as a final action, run once after all steps complete."""
47 def final_action_decorator(func: TFunc) -> TFunc:
48 @functools.wraps(func)
49 def final_action_wrapper(recipe: TFunc) -> None:
50 func(recipe)
52 return final_action_wrapper
54 return annotate_decorator(final_action_decorator, final_action.__name__)
57def recipe_step(order: int = 0, repeat: bool = False) -> TFunc:
58 """AI: Decorator marking a recipe method as a numbered step, optionally repeatable, in the recipe's workflow."""
60 def recipe_step_decorator(func: TFunc) -> TFunc:
61 @functools.wraps(func)
62 def recipe_step_wrapper(step: int, recipe: TFunc, ast_processor: ASTProcessor):
63 if step == order and (repeat or ast_processor.repeat_step == 0):
64 result = func(recipe, ast_processor)
66 def callable_result():
67 if result:
68 result()
69 return func.__name__
71 return callable_result()
72 return None
74 return recipe_step_wrapper
76 return annotate_decorator(recipe_step_decorator, recipe_step.__name__)
79def after_step(step: str) -> TFunc:
80 """AI: Decorator marking a recipe method to run only after the named step has already executed."""
82 def after_step_decorator(func: TFunc) -> TFunc:
83 @functools.wraps(func)
84 def after_step_wrapper(preceding_methods: Sequence[str], recipe: TFunc) -> None:
85 if step in preceding_methods:
86 func(recipe)
88 return after_step_wrapper
90 return annotate_decorator(after_step_decorator, after_step.__name__)
93class RecipeASTProcessor[TRecipe]:
94 """AI: Processor that runs a recipe's steps across a batch of files, optionally in parallel."""
96 def __init__(
97 self,
98 recipe: TRecipe,
99 iterable_provider: IterableProvider,
100 file_filter: str,
101 in_memory: bool = False,
102 max_processes: int = 4,
103 ) -> None:
104 """AI: Prepare a processor that runs a recipe's steps across a batch of files, optionally in parallel."""
105 self.__recipe: TRecipe = recipe
106 self.__batch_processor = BatchASTProcessor(in_memory=in_memory, max_processes=max_processes)
107 self.__iterableProvider = iterable_provider
108 self.__file_filter = file_filter
110 def run(self) -> None:
111 """AI: Run each of the recipe's decorated steps across the batch of files and return the collected results."""
112 actions: list[TFunc] = []
113 results: list[Any] = []
114 for idx, recipe_step_method in enumerate(get_methods_with_decorator(type(self.__recipe), recipe_step)):
115 results.append(None)
117 def recipe_action(ast_processor: ASTProcessor) -> None:
118 result = recipe_step_method(step, self.__recipe, ast_processor)
119 if result:
120 results[idx] = result
122 actions.append(recipe_action)
124 after_step_actions: list[TFunc] = []
125 for after_step_method in get_methods_with_decorator(self.__recipe.__class__, after_step):
127 def after_step_action() -> None:
128 after_step_method(results, self.__recipe)
130 after_step_actions.append(after_step_action)
132 step = 0
133 while len(actions) > 0:
134 for idx in range(len(results)):
135 results[idx] = None
136 self.__batch_processor.repeat(self.__iterableProvider, actions, self.__file_filter)
137 if all(result is None for result in results):
138 break
139 for after_step_action in after_step_actions:
140 after_step_action()
141 step += 1
143 for method in get_methods_with_decorator(self.__recipe.__class__, final_action):
144 method(self.__recipe)