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