ATLAS Offline Software
Loading...
Searching...
No Matches
AutogenDocumentation.py
Go to the documentation of this file.
1# Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
2#
3# @author Baptiste Ravina
4"""
5Core methods to extract options information from ConfigBlock classes and merge with
6output variables metadata from a YAML file.
7"""
8
9import inspect
10import yaml
11import re
12from typing import Any, Dict, List, Type, Optional, Union
13
14from AthenaCommon.Utils.unixtools import find_datafile
15
16
17def load_output_variables(yaml_filepath: str) -> Dict[str, List[Dict[str, Any]]]:
18 """
19 Load output variables metadata from YAML file.
20
21 Expected YAML format:
22 BlockClassName:
23 - name: variable_name
24 description: Variable description
25 toggled_by: Optional condition description
26
27 Args:
28 yaml_filepath: Path to the YAML file
29
30 Returns:
31 Dictionary mapping block class names to their output variables
32 """
33 with open(yaml_filepath, "r") as f:
34 data = yaml.safe_load(f)
35 return data if data else {}
36
37
38def extract_block_options(block_class: Type) -> Dict[str, Any]:
39 """
40 Extract options information from a ConfigBlock subclass.
41
42 Args:
43 block_class: A class that inherits from ConfigBlock
44
45 Returns:
46 A dictionary containing the class name and its options
47 """
48 # Create a temporary instance to access the options
49 instance = block_class()
50
51 # Get the options dictionary
52 options_dict = instance.getOptions()
53
54 # Extract information for each option
55 options_list = []
56 for option_name, option_obj in options_dict.items():
57 # skip some specific options
58 if option_name in ["groupName", "propertyOverrides", "ignoreDependencies"]:
59 continue
60 option_info = {
61 "label": option_name,
62 "type": option_obj.type.__name__ if option_obj.type is not None else "None",
63 "default": option_obj.default,
64 "info": option_obj.info,
65 "required": option_obj.required,
66 "noneAction": option_obj.noneAction,
67 "physicalUnit": interpret_physical_unit(option_obj.info),
68 }
69 # Check if this option has expert mode settings
70 if (
71 hasattr(instance, "_expertModeSettings")
72 and option_name in instance._expertModeSettings
73 ):
74 expert_rule = instance._expertModeSettings[option_name]
75 if not isinstance(expert_rule, list):
76 expert_rule = [expert_rule]
77 else:
78 expert_rule = None
79 option_info["expertMode"] = expert_rule
80 options_list.append(option_info)
81
82 return {
83 "class": block_class.__name__,
84 "module": block_class.__module__,
85 "docstring": inspect.getdoc(block_class),
86 "options": options_list,
87 }
88
89
90def interpret_physical_unit(info: str) -> Optional[str]:
91 """
92 Extract a physical unit from an info string.
93 Currently looks for energy units like MeV or GeV.
94
95 Args:
96 info: The information string from an option.
97
98 Returns:
99 The detected unit as a string ("MeV", "GeV", etc.) or None if no unit is found.
100 """
101 if not info:
102 return None
103
104 # Check for specific units
105 patterns = [
106 r"\[MeV\]",
107 r"\‍(MeV\‍)",
108 r"\‍(in MeV\‍)",
109 r"\[in MeV\]",
110 r"\[GeV\]",
111 r"\‍(GeV\‍)",
112 r"\‍(in GeV\‍)",
113 r"\[in GeV\]",
114 r"\[mm\]",
115 r"\‍(mm\‍)",
116 r"\‍(in mm\‍)",
117 r"\[in mm\]",
118 ]
119 for pattern in patterns:
120 if re.search(pattern, info):
121 if "MeV" in pattern:
122 return "MeV"
123 elif "GeV" in pattern:
124 return "GeV"
125 elif "mm" in pattern:
126 return "mm"
127 return None
128
129
130def process_info_links(info: str) -> str:
131 """
132 Scan an info string for backtick-enclosed substrings of the form `A::B`.
133 Turn them into a link to the appropriate module/files.
134
135 Args:
136 info: The input info string.
137
138 Returns:
139 The processed string with Markdown links where applicable.
140 """
141 if not info:
142 return info
143
144 # Regex to match `A::B` inside backticks
145 pattern = r"`([^`]+)::([^`]+)`"
146
147 def replace_match(match):
148 A, B = match.group(1), match.group(2)
149 if A == "CP" or A == "ORUtils":
150 url = f"https://acode-browser1.usatlas.bnl.gov/lxr/search?%21v=head&_filestring=**{B}**&_string="
151 return f"[`{A}::{B}`]({url})"
152 elif A == "xAOD" or A == "AthOnnx":
153 url = f"https://acode-browser1.usatlas.bnl.gov/lxr/ident?v=head&_i={B}&_identdefonly=1&_remember=1"
154 else:
155 # TODO: any other cases to handle?
156 return f"`{A}::{B}`"
157
158 return re.sub(pattern, replace_match, info)
159
160
161def link_jira_tickets(info: str) -> str:
162 """
163 Convert JIRA ticket references in a string to Markdown links.
164
165 - JIRA tickets are of the form: all-caps letters, a dash, then digits (e.g., ATLASG-2358)
166 - Converted to Markdown links: [ATLASG-2358](https://its.cern.ch/jira/browse/ATLASG-2358)
167
168 Args:
169 info: Input string that may contain JIRA tickets.
170
171 Returns:
172 The string with JIRA tickets converted to Markdown links.
173 """
174 if not info:
175 return info
176
177 # Regex pattern: one or more uppercase letters, dash, one or more digits
178 pattern = r"\b([A-Z]+-\d+)\b"
179
180 def replace_match(match):
181 ticket = match.group(1)
182 url = f"https://its.cern.ch/jira/browse/{ticket}"
183 return f"[{ticket}]({url})"
184
185 return re.sub(pattern, replace_match, info)
186
187
189 block_classes: List[Type], output_vars_yaml: Optional[Union[str, List[str]]] = None
190) -> List[Dict[str, Any]]:
191 """
192 Extract options information from a list of ConfigBlock classes and merge
193 with output variables metadata.
194
195 Args:
196 block_classes: List of classes that inherit from ConfigBlock
197 output_vars_yaml: Optional path to YAML file or list of paths to YAML files
198 containing output variables. If multiple files provided,
199 their contents will be merged. Files are located using
200 find_datafile.
201
202 Returns:
203 List of dictionaries, each containing information about a block class
204 """
205 # Load output variables if YAML file(s) provided
206 output_vars_map = {}
207 if output_vars_yaml:
208 # Normalize to list for uniform processing
209 yaml_files = (
210 [output_vars_yaml]
211 if isinstance(output_vars_yaml, str)
212 else output_vars_yaml
213 )
214
215 # Load and merge all YAML files
216 for yaml_file in yaml_files:
217 # Locate the file
218 print(yaml_file)
219 resolved_path = find_datafile(yaml_file)
220 if resolved_path is None:
221 raise FileNotFoundError(f"Could not locate YAML file: {yaml_file}")
222
223 file_vars = load_output_variables(resolved_path)
224 # Merge with existing map (later files can override earlier ones)
225 for class_name, variables in file_vars.items():
226 if class_name in output_vars_map:
227 # Merge variable lists, avoiding duplicates if needed
228 output_vars_map[class_name].extend(variables)
229 else:
230 output_vars_map[class_name] = variables
231
232 results = []
233 for block_class in block_classes:
234 info = extract_block_options(block_class)
235 # Merge output variables if available
236 class_name = block_class.__name__
237 info["output_variables"] = output_vars_map.get(class_name, [])
238
239 results.append(info)
240
241 return results
242
243
244def save_as_yaml(data: List[Dict[str, Any]], filepath: str) -> None:
245 """Save extracted data as YAML."""
246 with open(filepath, "w") as f:
247 yaml.dump(data, f, default_flow_style=False, sort_keys=False)
248 print(f"Saved YAML to {filepath}")
249
250
251def generate_block_markdown(block_info: Dict[str, Any]) -> str:
252 """
253 Generate Markdown documentation for a single block.
254
255 Args:
256 block_info: Dictionary containing block information with keys:
257 - class: Block class name
258 - module: Module containing the block
259 - options: List of option dictionaries
260 - output_variables: List of output variable dictionaries
261
262 Returns:
263 Markdown string for this block
264 """
265 markdown = ""
266
267 # Options section
268 if block_info.get("options"):
269 for opt in block_info["options"]:
270 name = opt["label"]
271
272 # Skip these settings unless they are True
273 if name in ["skipOnData", "skipOnMC"]:
274 if not opt["default"] is True:
275 continue
276 # Skip these settings unless they are set
277 if name in ["onlyForDSIDs"]:
278 if not opt["default"] is []:
279 continue
280
281 # Option label with type, expert flag, required flag
282 label = f"`{opt['label']}` ({opt['type']})"
283 if opt["expertMode"] is not None:
284 expertOptions = list(opt["expertMode"])
285 label += f" **[expert-only options: {','.join(['`' + str(x) + '`' for x in expertOptions])}]**"
286 if opt["required"] is True or opt["noneAction"] != "ignore":
287 label += " **[REQUIRED]**"
288
289 markdown += f"{label}\n"
290 info_string = opt["info"]
291 info_string = process_info_links(info_string)
292 info_string = link_jira_tickets(info_string)
293 markdown += f": {info_string}"
294
295 if opt.get("default") != "":
296 default_val = opt["default"]
297 default_str = repr(default_val)
298
299 # Add unit information if available
300 unit = opt.get("physicalUnit")
301 if unit is None or default_val is None:
302 default_display = f"`{default_str}`"
303 elif unit == "GeV":
304 default_display = f"`{default_str}` GeV"
305 elif unit == "MeV":
306 # Convert MeV to GeV for simplified display
307 try:
308 if isinstance(default_val, (list, tuple)):
309 converted = [float(x) / 1000 for x in default_val]
310 converted_str = (
311 "[" + ", ".join(f"{x}" for x in converted) + "]"
312 )
313 else:
314 converted = float(default_val) / 1000
315 converted_str = f"{converted}"
316 except (TypeError, ValueError):
317 converted_str = "?"
318 default_display = f"`{default_str}` MeV (`{converted_str}` GeV)"
319 else:
320 default_display = f"`{default_str}` {unit}"
321
322 markdown += f" Default: {default_display}."
323
324 markdown += "\n\n"
325
326 # Output variables section
327 if block_info.get("output_variables"):
328 # Separate variables into always-saved and toggled
329 always_saved = []
330 toggled_vars = {} # toggled_by condition -> list of variables
331
332 for var in block_info["output_variables"]:
333 if var.get("toggled_by"):
334 condition = var["toggled_by"]
335 if condition not in toggled_vars:
336 toggled_vars[condition] = []
337 toggled_vars[condition].append(var)
338 else:
339 always_saved.append(var)
340
341 # Always-saved variables section
342 if always_saved:
343 markdown += '!!! success "Registers the following variables:"\n'
344 for var in always_saved:
345 var_name = var.get("name", "N/A")
346 var_desc = var.get("description", "")
347 markdown += f" - `{var_name}`: {var_desc}\n"
348 markdown += "\n"
349
350 # Toggled variables sections
351 for condition, vars_list in toggled_vars.items():
352 markdown += (
353 f'!!! success "Additional variables toggled by `{condition}`:"\n'
354 )
355 for var in vars_list:
356 var_name = var.get("name", "N/A")
357 var_desc = var.get("description", "")
358 markdown += f" - `{var_name}`: {var_desc}\n"
359 markdown += "\n"
360 else:
361 print(
362 f" Block {block_info.get('class')} didn't register any output variables."
363 )
364
365 return markdown
366
367
369 input_filepath: str, output_filepath: str, block_data: List[Dict[str, Any]]
370) -> None:
371 """
372 Process an input markdown file, replacing AUTOGEN<BlockName> markers
373 with generated block documentation.
374
375 Looks for lines of the form "AUTOGEN<BlockName>" and replaces them
376 with the generated markdown for that block. All other content is
377 left untouched.
378
379 Args:
380 input_filepath: Path to the input markdown file
381 output_filepath: Path to write the processed output
382 block_data: List of extracted block information dictionaries
383
384 Raises:
385 FileNotFoundError: If input file does not exist
386 ValueError: If a referenced block is not found in block_data
387 """
388 # Create a mapping of block names to their markdown
389 block_markdown_map = {
390 block["class"]: generate_block_markdown(block) for block in block_data
391 }
392
393 # Read input file
394 with open(input_filepath, "r") as f:
395 lines = f.readlines()
396
397 output_lines = []
398 for line in lines:
399 stripped = line.strip()
400
401 # Check if this line is an AUTOGEN marker
402 if stripped.startswith("AUTOGEN<") and stripped.endswith(">"):
403 # Extract block name from AUTOGEN<BlockName>
404 block_name = stripped[8:-1] # Remove 'AUTOGEN<' and '>'
405
406 if block_name in block_markdown_map:
407 output_lines.append(block_markdown_map[block_name])
408 else:
409 raise ValueError(
410 f"Block '{block_name}' not found in extracted block data"
411 )
412 else:
413 output_lines.append(line)
414
415 # Write output file
416 with open(output_filepath, "w") as f:
417 f.writelines(output_lines)
418
419 print(f"Processed markdown saved to {output_filepath}")
void print(char *figname, TCanvas *c1)
Dict[str, Any] extract_block_options(Type block_class)
Optional[str] interpret_physical_unit(str info)
List[Dict[str, Any]] extract_from_classes(List[Type] block_classes, Optional[Union[str, List[str]]] output_vars_yaml=None)
None process_markdown_with_autogen(str input_filepath, str output_filepath, List[Dict[str, Any]] block_data)
Dict[str, List[Dict[str, Any]]] load_output_variables(str yaml_filepath)
None save_as_yaml(List[Dict[str, Any]] data, str filepath)
str generate_block_markdown(Dict[str, Any] block_info)