Coverage for src/renaissance/syntax_tree/batch_ast_processor.py: 98%
55 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-02 13:12 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-02 13:12 +0000
1"""AI: Process multiple ASTs in parallel by applying a shared action to each."""
3import concurrent.futures
4import re
5from collections.abc import Callable, Iterable, Sequence
6from functools import partial
7from typing import Any
9from .ast_factory import ASTFactory
10from .ast_node import ASTNode
11from .ast_processor import ASTProcessor
13AST_FACTORY_AND_ATU = tuple[ASTFactory, ASTNode]
14Action = Callable[[ASTProcessor], Callable[[], Any] | None]
15IterableProvider = Callable[[], Iterable[AST_FACTORY_AND_ATU]]
18class BatchASTProcessor:
19 """AI: Process multiple ASTs in parallel by applying a shared action to each."""
21 def __init__(self, in_memory: bool = False, max_processes: int = 4):
22 """Initialize the BatchASTProcessor.
24 Args:
25 in_memory (bool): Flag to indicate if processing should be done in memory. Defaults to False.
26 max_processes (int): The maximum number of processes to use. Defaults to 4.
28 """
29 self.in_memory: bool = in_memory
30 self.in_memory_files: dict[str, str] = {}
31 self.max_processes = max_processes
33 def once(
34 self,
35 iterable: Iterable[AST_FACTORY_AND_ATU] | IterableProvider,
36 actions: Action | Sequence[Action],
37 file_filter: str | re.Pattern[str] | None = None,
38 ) -> None:
39 """Process a given iterable of ATU objects or an IterableProvider with specified actions.
41 Args:
42 iterable (Iterable[ATU] | IterableProvider): The iterable or provider of ATU objects to process.
43 actions (Action | Sequence[Action]): The action or sequence of actions to apply to each item in the iterable.
44 file_filter (Optional[str | re.Pattern], optional): A filter to apply to file names. Defaults to None.
46 Returns:
47 bool: True if processing was successful, False otherwise.
49 """
50 iterable = iterable() if callable(iterable) else iterable
51 self.__process(iterable, actions, self.in_memory, file_filter)
53 def repeat(
54 self,
55 iterable_provider: IterableProvider,
56 actions: Action | Sequence[Action],
57 file_filter: str | re.Pattern[str] | None = None,
58 max_repeat: int = 5,
59 ) -> None:
60 """Repeats the processing of items provided by the iterableProvider until no changes left.
62 Up to a maximum number of times.
64 Args:
65 iterable_provider (IterableProvider): A provider that yields items to be processed.
66 actions (Action | Sequence[Action]): A single action or a sequence of actions to be performed on each item.
67 file_filter (Optional[str | re.Pattern], optional): A filter to apply to the files being processed. Defaults to None.
68 max_repeat (int, optional): The maximum number of times to repeat the processing. Defaults to 5.
70 Returns:
71 bool: True if the processing still yields changes, False otherwise.
73 """
74 self.__process(iterable_provider(), actions, self.in_memory, file_filter, max_repeat)
76 def __process(
77 self,
78 iterable: Iterable[tuple[ASTFactory, ASTNode]],
79 actions: Action | Sequence[Action],
80 in_memory: bool = False,
81 file_filter: str | re.Pattern[str] | None = None,
82 max_repeat: int = 1,
83 ) -> None:
84 filter_pattern = (
85 file_filter if isinstance(file_filter, re.Pattern) else re.compile(file_filter) if file_filter is not None else None
86 )
88 def is_eligible(item: tuple[ASTFactory, ASTNode]) -> bool:
89 return BatchASTProcessor.__eligible_file(filter_pattern, item)
91 actions = actions if isinstance(actions, Sequence) else [actions]
92 # use parallel processing possible here
93 partial_process_item = partial(
94 process_atu,
95 self=self,
96 actions=actions,
97 in_memory=in_memory,
98 max_repeat=max_repeat,
99 )
100 with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_processes) as executor:
101 for results in executor.map(partial_process_item, filter(is_eligible, iterable)):
102 for my_callable in results:
103 # the post-processing is done in the main thread
104 my_callable()
106 def _replace_if_in_memory(self, item: AST_FACTORY_AND_ATU) -> AST_FACTORY_AND_ATU:
107 if self.in_memory and self.in_memory_files.get(item[1].filename):
108 return item[0], item[0].create_from_text(
109 self.in_memory_files[item[1].filename],
110 item[1].filename,
111 )
112 return item
114 @staticmethod
115 def __eligible_file(file_filter: re.Pattern[str] | None, item: AST_FACTORY_AND_ATU) -> bool:
116 return file_filter is None or file_filter.match(item[1].filename) is not None
119def process_atu(
120 atu: AST_FACTORY_AND_ATU,
121 self: BatchASTProcessor,
122 actions: Sequence[Action],
123 in_memory: bool,
124 max_repeat: int,
125) -> Sequence[Callable[[], None]]:
126 """AI: Run the given actions against one ATU, repeating up to max_repeat times, and return deferred result callables."""
127 atu = self._replace_if_in_memory(atu)
128 ast_processor = ASTProcessor(atu[1], atu[0], in_memory)
129 results: list[Callable[[], None]] = []
131 for repeat in range(max_repeat):
132 for action in actions:
133 ast_processor.repeat_step = repeat
134 result = action(ast_processor)
135 if result:
136 results.append(result)
137 has_changed = ast_processor.has_changed()
138 if not has_changed:
139 return results
140 ast_processor = ast_processor.commit()
141 if self.in_memory:
142 self.in_memory_files[ast_processor.filename] = ast_processor.apply_to_string()
143 return results