From 7c7340c8e9ffe32c8b8a45efdee551ed906cb53e Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Tue, 13 Jan 2026 12:07:48 +0100 Subject: [PATCH 01/36] Add Gaudi Functional C++ Class Generator script This script generates Gaudi Functional C++ classes with appropriate structure and boilerplate code based on user-defined specifications. --- k4FWCore/helpers/gaudi_gen.py | 325 ++++++++++++++++++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 k4FWCore/helpers/gaudi_gen.py diff --git a/k4FWCore/helpers/gaudi_gen.py b/k4FWCore/helpers/gaudi_gen.py new file mode 100644 index 000000000..cee635a6f --- /dev/null +++ b/k4FWCore/helpers/gaudi_gen.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python3 +""" +Gaudi Functional C++ Class Generator + +A user-friendly script to generate Gaudi Functional C++ classes with proper +structure and boilerplate code. +""" + +import argparse +import sys +from typing import List, Tuple + +# Functional type definitions +FUNCTIONAL_TYPES = { + 'consumer': { + 'base': 'Consumer', + 'description': 'One input, no output', + 'example': 'EventTimeMonitor, ProcStatusAbortMoni' + }, + 'producer': { + 'base': 'Producer', + 'description': 'No input, one or more outputs', + 'example': 'File IO, constant data generation' + }, + 'filter': { + 'base': 'FilterPredicate', + 'description': 'True/False output only', + 'example': 'HDRFilter, L0Filter, ODINFilter' + }, + 'transformer': { + 'base': 'Transformer', + 'description': 'One or more inputs, one output', + 'example': 'MySum, data transformation' + }, + 'multi_transformer': { + 'base': 'MultiTransformer', + 'description': 'One or more inputs, multiple outputs', + 'example': 'Complex data processing with multiple results' + }, + 'merging_transformer': { + 'base': 'MergingTransformer', + 'description': 'Identical inputs, one output', + 'example': 'TrackListMerger, InCaloAcceptanceAlg' + }, + 'splitting_transformer': { + 'base': 'SplittingTransformer', + 'description': 'One input, identical outputs', + 'example': 'HltRawBankDecoderBase' + }, + 'scalar_transformer': { + 'base': 'ScalarTransformer', + 'description': 'Vector to vector with 1-to-1 element mapping', + 'example': 'CaloElectronAlg, CaloSinglePhotonAlg' + } +} + + +def parse_arguments(): + """Parse command line arguments.""" + parser = argparse.ArgumentParser( + description='Generate Gaudi Functional C++ classes', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=_get_functional_types_help() + ) + + parser.add_argument('class_name', help='Name of the C++ class to generate') + parser.add_argument('functional_type', + choices=list(FUNCTIONAL_TYPES.keys()), + help='Type of functional to generate') + parser.add_argument('-i', '--inputs', nargs='*', default=[], + help='Input data specifications (format: "Type:Location:DefaultValue")') + parser.add_argument('-o', '--outputs', nargs='*', default=[], + help='Output data specifications (format: "Type:Location:DefaultValue")') + parser.add_argument('-n', '--namespace', default='', + help='Namespace for the class') + parser.add_argument('-f', '--output-file', + help='Output file name (default: .cpp)') + parser.add_argument('--header-only', action='store_true', + help='Generate header file instead of implementation') + + return parser.parse_args() + + +def _get_functional_types_help(): + """Generate help text for functional types.""" + help_text = "\nAvailable Functional Types:\n" + for key, info in FUNCTIONAL_TYPES.items(): + help_text += f"\n {key}:\n" + help_text += f" {info['description']}\n" + help_text += f" Example: {info['example']}\n" + return help_text + + +def parse_data_spec(spec: str) -> Tuple[str, str, str]: + """Parse data specification string into type, location, and default value.""" + parts = spec.split(':') + if len(parts) == 1: + return parts[0], '', '' + elif len(parts) == 2: + return parts[0], parts[1], '' + else: + return parts[0], parts[1], parts[2] + + +def generate_template_signature(functional_type: str, inputs: List[str], outputs: List[str]) -> str: + """Generate the template signature for the functional.""" + in_types = [parse_data_spec(i)[0] for i in inputs] + out_types = [parse_data_spec(o)[0] for o in outputs] + + if functional_type == 'consumer': + return f"void(const {in_types[0]}&)" if in_types else "void()" + elif functional_type == 'producer': + if len(out_types) == 1: + return f"{out_types[0]}()" + else: + return f"std::tuple<{', '.join(out_types)}>()" + elif functional_type == 'filter': + in_sig = ', '.join([f"const {t}&" for t in in_types]) + return f"bool({in_sig})" + elif functional_type == 'transformer': + in_sig = ', '.join([f"const {t}&" for t in in_types]) + return f"{out_types[0]}({in_sig})" + elif functional_type == 'multi_transformer': + in_sig = ', '.join([f"const {t}&" for t in in_types]) + out_sig = ', '.join(out_types) + return f"std::tuple<{out_sig}>({in_sig})" + elif functional_type == 'merging_transformer': + return f"{out_types[0]}(const std::vector<{in_types[0]}*>&)" + elif functional_type == 'splitting_transformer': + return f"std::vector<{out_types[0]}>(const {in_types[0]}&)" + elif functional_type == 'scalar_transformer': + return f"{out_types[0]}(const {in_types[0]}&)" + + return "" + + +def generate_constructor_inputs(inputs: List[str]) -> str: + """Generate input KeyValue list for constructor.""" + if not inputs: + return "" + + key_values = [] + for inp in inputs: + typ, loc, default = parse_data_spec(inp) + loc_name = loc if loc else f"{typ}Loc" + default_val = default if default else f"Input/{typ}" + key_values.append(f'KeyValue("{loc_name}", "{default_val}")') + + if len(key_values) == 1: + return key_values[0] + else: + return "{\n " + ",\n ".join(key_values) + " }" + + +def generate_constructor_outputs(outputs: List[str]) -> str: + """Generate output KeyValue list for constructor.""" + if not outputs: + return "" + + key_values = [] + for out in outputs: + typ, loc, default = parse_data_spec(out) + loc_name = loc if loc else f"{typ}Loc" + default_val = default if default else f"Output/{typ}" + key_values.append(f'KeyValue("{loc_name}", "{default_val}")') + + if len(key_values) == 1: + return key_values[0] + else: + return "{\n " + ",\n ".join(key_values) + " }" + + +def generate_operator_signature(functional_type: str, inputs: List[str], outputs: List[str]) -> str: + """Generate the operator() signature.""" + in_types = [parse_data_spec(i)[0] for i in inputs] + out_types = [parse_data_spec(o)[0] for o in outputs] + + if functional_type == 'consumer': + in_sig = f"const {in_types[0]}& input" if in_types else "" + return f"void operator()({in_sig}) const override" + elif functional_type == 'producer': + if len(out_types) == 1: + return f"{out_types[0]} operator()() const override" + else: + return f"std::tuple<{', '.join(out_types)}> operator()() const override" + elif functional_type == 'filter': + params = ', '.join([f"const {t}& in{i+1}" for i, t in enumerate(in_types)]) + return f"bool operator()({params}) const override" + elif functional_type == 'transformer': + params = ', '.join([f"const {t}& in{i+1}" for i, t in enumerate(in_types)]) + return f"{out_types[0]} operator()({params}) const override" + elif functional_type == 'multi_transformer': + params = ', '.join([f"const {t}& in{i+1}" for i, t in enumerate(in_types)]) + return f"std::tuple<{', '.join(out_types)}> operator()({params}) const override" + elif functional_type == 'merging_transformer': + return f"{out_types[0]} operator()(const std::vector<{in_types[0]}*>& inputs) const override" + elif functional_type == 'splitting_transformer': + return f"std::vector<{out_types[0]}> operator()(const {in_types[0]}& input) const override" + elif functional_type == 'scalar_transformer': + return f"{out_types[0]} operator()(const {in_types[0]}& input) const override" + + return "" + + +def generate_operator_body(functional_type: str, inputs: List[str], outputs: List[str]) -> str: + """Generate a template body for the operator().""" + if functional_type == 'consumer': + return " // Process input data here\n" + elif functional_type == 'producer': + out_types = [parse_data_spec(o)[0] for o in outputs] + if len(out_types) == 1: + return f" // Generate and return output data\n return {out_types[0]}{{}};\n" + else: + return f" // Generate and return output data\n return {{{', '.join([f'{t}{{}}' for t in out_types])}}};\n" + elif functional_type == 'filter': + return " // Apply filter logic and return true/false\n return true;\n" + elif functional_type in ['transformer', 'scalar_transformer']: + out_type = parse_data_spec(outputs[0])[0] + return f" // Transform input(s) to output\n return {out_type}{{}};\n" + elif functional_type == 'multi_transformer': + out_types = [parse_data_spec(o)[0] for o in outputs] + return f" // Transform inputs to multiple outputs\n return {{{', '.join([f'{t}{{}}' for t in out_types])}}};\n" + elif functional_type == 'merging_transformer': + out_type = parse_data_spec(outputs[0])[0] + return f" // Merge inputs into single output\n return {out_type}{{}};\n" + elif functional_type == 'splitting_transformer': + out_type = parse_data_spec(outputs[0])[0] + return f" // Split input into multiple outputs\n return std::vector<{out_type}>{{}};\n" + + return "" + + +def generate_class(class_name: str, functional_type: str, inputs: List[str], + outputs: List[str], namespace: str = '') -> str: + """Generate the complete C++ class code.""" + base_class = FUNCTIONAL_TYPES[functional_type]['base'] + template_sig = generate_template_signature(functional_type, inputs, outputs) + input_keyvalues = generate_constructor_inputs(inputs) + output_keyvalues = generate_constructor_outputs(outputs) + operator_sig = generate_operator_signature(functional_type, inputs, outputs) + operator_body = generate_operator_body(functional_type, inputs, outputs) + + # Build constructor initializer list + init_parts = [f"\n {base_class}(\n name,\n pSvc"] + if input_keyvalues: + init_parts.append(f", {input_keyvalues}") + if output_keyvalues: + init_parts.append(f",\n {output_keyvalues}") + init_parts.append(")") + + constructor_init = ''.join(init_parts) + + code = f"""// Generated by Gaudi Functional C++ Class Generator +#include "GaudiAlg/Functional.h" +#include "GaudiKernel/KeyValue.h" + +""" + + if namespace: + code += f"namespace {namespace} {{\n\n" + + code += f"""class {class_name} + : public Gaudi::Functional::{base_class}<{template_sig}> {{ + +public: + {class_name}(const std::string& name, ISvcLocator* pSvc) + :{constructor_init} {{}} + + {operator_sig} {{ +{operator_body} }} +}}; + +""" + + if namespace: + code += f"}} // namespace {namespace}\n\n" + + code += f"DECLARE_COMPONENT({class_name})\n" + + return code + + +def main(): + """Main entry point.""" + args = parse_arguments() + + # Validate inputs/outputs based on functional type + if args.functional_type == 'consumer' and not args.inputs: + print("Error: Consumer requires at least one input", file=sys.stderr) + return 1 + elif args.functional_type == 'producer' and not args.outputs: + print("Error: Producer requires at least one output", file=sys.stderr) + return 1 + elif args.functional_type in ['transformer', 'filter'] and (not args.inputs or not args.outputs): + print(f"Error: {args.functional_type} requires both inputs and outputs", file=sys.stderr) + return 1 + + # Generate the class + code = generate_class( + args.class_name, + args.functional_type, + args.inputs, + args.outputs, + args.namespace + ) + + # Determine output file + output_file = args.output_file + if not output_file: + ext = '.h' if args.header_only else '.cpp' + output_file = f"{args.class_name}{ext}" + + # Write to file or stdout + if output_file == '-': + print(code) + else: + with open(output_file, 'w') as f: + f.write(code) + print(f"Generated {output_file}") + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) From 0dfad57edfd90e564696df4584d57b4da4cc27a9 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Tue, 13 Jan 2026 17:00:01 +0100 Subject: [PATCH 02/36] Enhance gaudi_gen.py for k4FWCore support Updated the Gaudi Functional C++ Class Generator to support k4FWCore framework, improved input/output specifications, and added new functionalities for property parsing and class generation. --- k4FWCore/helpers/gaudi_gen.py | 437 +++++++++++++++++++++++----------- 1 file changed, 294 insertions(+), 143 deletions(-) diff --git a/k4FWCore/helpers/gaudi_gen.py b/k4FWCore/helpers/gaudi_gen.py index cee635a6f..e7e1c9558 100644 --- a/k4FWCore/helpers/gaudi_gen.py +++ b/k4FWCore/helpers/gaudi_gen.py @@ -3,54 +3,35 @@ Gaudi Functional C++ Class Generator A user-friendly script to generate Gaudi Functional C++ classes with proper -structure and boilerplate code. +structure and boilerplate code. Supports both Gaudi::Functional and k4FWCore variants. """ import argparse import sys -from typing import List, Tuple +import re +from typing import List, Tuple, Optional # Functional type definitions FUNCTIONAL_TYPES = { 'consumer': { 'base': 'Consumer', - 'description': 'One input, no output', + 'description': 'One or more inputs, no output', 'example': 'EventTimeMonitor, ProcStatusAbortMoni' }, 'producer': { 'base': 'Producer', 'description': 'No input, one or more outputs', - 'example': 'File IO, constant data generation' - }, - 'filter': { - 'base': 'FilterPredicate', - 'description': 'True/False output only', - 'example': 'HDRFilter, L0Filter, ODINFilter' + 'example': 'ExampleFunctionalProducerMultiple, file IO, constant data generation' }, 'transformer': { 'base': 'Transformer', - 'description': 'One or more inputs, one output', - 'example': 'MySum, data transformation' - }, - 'multi_transformer': { - 'base': 'MultiTransformer', - 'description': 'One or more inputs, multiple outputs', - 'example': 'Complex data processing with multiple results' - }, - 'merging_transformer': { - 'base': 'MergingTransformer', - 'description': 'Identical inputs, one output', - 'example': 'TrackListMerger, InCaloAcceptanceAlg' - }, - 'splitting_transformer': { - 'base': 'SplittingTransformer', - 'description': 'One input, identical outputs', - 'example': 'HltRawBankDecoderBase' + 'description': 'One or more inputs, one or more outputs', + 'example': 'Data transformation algorithms' }, - 'scalar_transformer': { - 'base': 'ScalarTransformer', - 'description': 'Vector to vector with 1-to-1 element mapping', - 'example': 'CaloElectronAlg, CaloSinglePhotonAlg' + 'filter': { + 'base': 'FilterPredicate', + 'description': 'One or more inputs, boolean output', + 'example': 'Event selection, filtering based on criteria' } } @@ -58,7 +39,7 @@ def parse_arguments(): """Parse command line arguments.""" parser = argparse.ArgumentParser( - description='Generate Gaudi Functional C++ classes', + description='Generate Gaudi/k4FWCore Functional C++ classes', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=_get_functional_types_help() ) @@ -68,15 +49,19 @@ def parse_arguments(): choices=list(FUNCTIONAL_TYPES.keys()), help='Type of functional to generate') parser.add_argument('-i', '--inputs', nargs='*', default=[], - help='Input data specifications (format: "Type:Location:DefaultValue")') + help='Input data specifications (format: "Type:Location" or just "Type")') parser.add_argument('-o', '--outputs', nargs='*', default=[], - help='Output data specifications (format: "Type:Location:DefaultValue")') + help='Output data specifications (format: "Type:Location" or just "Type")') parser.add_argument('-n', '--namespace', default='', help='Namespace for the class') parser.add_argument('-f', '--output-file', help='Output file name (default: .cpp)') - parser.add_argument('--header-only', action='store_true', - help='Generate header file instead of implementation') + parser.add_argument('--framework', choices=['gaudi', 'k4fwcore'], default='k4fwcore', + help='Target framework (default: k4fwcore)') + parser.add_argument('--struct', action='store_true', + help='Generate as struct instead of class') + parser.add_argument('-p', '--properties', nargs='*', default=[], + help='Gaudi properties (format: "Type:Name:Default:Description")') return parser.parse_args() @@ -88,81 +73,116 @@ def _get_functional_types_help(): help_text += f"\n {key}:\n" help_text += f" {info['description']}\n" help_text += f" Example: {info['example']}\n" + + help_text += "\n\nExample Usage:\n" + help_text += " # k4FWCore producer with multiple outputs\n" + help_text += " python gaudi_gen.py MyProducer producer \\\n" + help_text += " -o 'edm4hep::MCParticleCollection:MCParticles' \\\n" + help_text += " 'edm4hep::TrackCollection:Tracks' \\\n" + help_text += " --framework k4fwcore --struct\n\n" + help_text += " # Gaudi transformer\n" + help_text += " python gaudi_gen.py MyTransformer transformer \\\n" + help_text += " -i 'InputType:InputLoc' \\\n" + help_text += " -o 'OutputType:OutputLoc' \\\n" + help_text += " --framework gaudi\n" + return help_text -def parse_data_spec(spec: str) -> Tuple[str, str, str]: - """Parse data specification string into type, location, and default value.""" - parts = spec.split(':') - if len(parts) == 1: - return parts[0], '', '' - elif len(parts) == 2: - return parts[0], parts[1], '' - else: - return parts[0], parts[1], parts[2] +def parse_data_spec(spec: str) -> Tuple[str, str]: + """Parse data specification string into type and location. + Handles types with template parameters like podio::UserDataCollection + """ + # Find the last colon that's not inside angle brackets + depth = 0 + colon_pos = -1 + for i, char in enumerate(spec): + if char == '<': + depth += 1 + elif char == '>': + depth -= 1 + elif char == ':' and depth == 0: + colon_pos = i + + if colon_pos == -1: + return spec, '' + + return spec[:colon_pos], spec[colon_pos+1:] + +def parse_property_spec(spec: str) -> Tuple[str, str, str, str]: + """Parse property specification into type, name, default, description.""" + parts = spec.split(':', 3) + typ = parts[0] if len(parts) > 0 else 'int' + name = parts[1] if len(parts) > 1 else 'Property' + default = parts[2] if len(parts) > 2 else '0' + desc = parts[3] if len(parts) > 3 else 'Property description' + return typ, name, default, desc -def generate_template_signature(functional_type: str, inputs: List[str], outputs: List[str]) -> str: + +def generate_template_signature(functional_type: str, inputs: List[str], outputs: List[str], + framework: str) -> str: """Generate the template signature for the functional.""" in_types = [parse_data_spec(i)[0] for i in inputs] out_types = [parse_data_spec(o)[0] for o in outputs] if functional_type == 'consumer': - return f"void(const {in_types[0]}&)" if in_types else "void()" + in_sig = ', '.join([f"const {t}&" for t in in_types]) + return f"void({in_sig})" elif functional_type == 'producer': - if len(out_types) == 1: + if len(out_types) == 0: + return "void()" + elif len(out_types) == 1: return f"{out_types[0]}()" else: return f"std::tuple<{', '.join(out_types)}>()" + elif functional_type == 'transformer': + in_sig = ', '.join([f"const {t}&" for t in in_types]) if in_types else "" + if len(out_types) == 1: + return f"{out_types[0]}({in_sig})" + else: + out_sig = ', '.join(out_types) + return f"std::tuple<{out_sig}>({in_sig})" elif functional_type == 'filter': in_sig = ', '.join([f"const {t}&" for t in in_types]) return f"bool({in_sig})" - elif functional_type == 'transformer': - in_sig = ', '.join([f"const {t}&" for t in in_types]) - return f"{out_types[0]}({in_sig})" - elif functional_type == 'multi_transformer': - in_sig = ', '.join([f"const {t}&" for t in in_types]) - out_sig = ', '.join(out_types) - return f"std::tuple<{out_sig}>({in_sig})" - elif functional_type == 'merging_transformer': - return f"{out_types[0]}(const std::vector<{in_types[0]}*>&)" - elif functional_type == 'splitting_transformer': - return f"std::vector<{out_types[0]}>(const {in_types[0]}&)" - elif functional_type == 'scalar_transformer': - return f"{out_types[0]}(const {in_types[0]}&)" return "" -def generate_constructor_inputs(inputs: List[str]) -> str: - """Generate input KeyValue list for constructor.""" - if not inputs: - return "" +def generate_keyvalues_k4fwcore(data_specs: List[str], is_input: bool) -> str: + """Generate KeyValues initialization for k4FWCore.""" + if not data_specs: + return "{}" - key_values = [] - for inp in inputs: - typ, loc, default = parse_data_spec(inp) - loc_name = loc if loc else f"{typ}Loc" - default_val = default if default else f"Input/{typ}" - key_values.append(f'KeyValue("{loc_name}", "{default_val}")') + lines = [] + for spec in data_specs: + typ, loc = parse_data_spec(spec) + if not loc: + # Generate a default location name from type + # Remove Collection suffix and namespace + clean_name = typ.split('::')[-1].replace('Collection', '') + loc = clean_name + lines.append(f'KeyValues("{loc}", {{"{loc}"}})') - if len(key_values) == 1: - return key_values[0] + if len(lines) == 1: + return lines[0] else: - return "{\n " + ",\n ".join(key_values) + " }" + return "{\n " + ",\n ".join(lines) + "}" -def generate_constructor_outputs(outputs: List[str]) -> str: - """Generate output KeyValue list for constructor.""" - if not outputs: +def generate_keyvalue_gaudi(data_specs: List[str]) -> str: + """Generate KeyValue initialization for Gaudi.""" + if not data_specs: return "" key_values = [] - for out in outputs: - typ, loc, default = parse_data_spec(out) - loc_name = loc if loc else f"{typ}Loc" - default_val = default if default else f"Output/{typ}" - key_values.append(f'KeyValue("{loc_name}", "{default_val}")') + for spec in data_specs: + typ, loc = parse_data_spec(spec) + if not loc: + loc = f"{typ.split('::')[-1]}Loc" + default_val = loc + key_values.append(f'KeyValue("{loc}", "{default_val}")') if len(key_values) == 1: return key_values[0] @@ -170,104 +190,230 @@ def generate_constructor_outputs(outputs: List[str]) -> str: return "{\n " + ",\n ".join(key_values) + " }" +def generate_constructor(class_name: str, functional_type: str, inputs: List[str], + outputs: List[str], framework: str, base_class: str) -> str: + """Generate the constructor.""" + if framework == 'k4fwcore': + input_kv = generate_keyvalues_k4fwcore(inputs, True) + output_kv = generate_keyvalues_k4fwcore(outputs, False) + + return f""" {class_name}(const std::string& name, ISvcLocator* svcLoc) + : {base_class}(name, svcLoc, {input_kv}, + {output_kv}) {{}}""" + else: # gaudi + input_kv = generate_keyvalue_gaudi(inputs) + output_kv = generate_keyvalue_gaudi(outputs) + + init_parts = [f"\n {base_class}(\n name,\n pSvc"] + if input_kv: + init_parts.append(f", {input_kv}") + if output_kv: + init_parts.append(f",\n {output_kv}") + init_parts.append(")") + + constructor_init = ''.join(init_parts) + + return f""" {class_name}(const std::string& name, ISvcLocator* pSvc) + :{constructor_init} {{}}""" + + def generate_operator_signature(functional_type: str, inputs: List[str], outputs: List[str]) -> str: """Generate the operator() signature.""" in_types = [parse_data_spec(i)[0] for i in inputs] out_types = [parse_data_spec(o)[0] for o in outputs] if functional_type == 'consumer': - in_sig = f"const {in_types[0]}& input" if in_types else "" - return f"void operator()({in_sig}) const override" + params = ', '.join([f"const {t}& in{i+1}" for i, t in enumerate(in_types)]) + return f"void operator()({params}) const override" elif functional_type == 'producer': - if len(out_types) == 1: + if len(out_types) == 0: + return "void operator()() const override" + elif len(out_types) == 1: return f"{out_types[0]} operator()() const override" else: return f"std::tuple<{', '.join(out_types)}> operator()() const override" - elif functional_type == 'filter': - params = ', '.join([f"const {t}& in{i+1}" for i, t in enumerate(in_types)]) - return f"bool operator()({params}) const override" elif functional_type == 'transformer': params = ', '.join([f"const {t}& in{i+1}" for i, t in enumerate(in_types)]) - return f"{out_types[0]} operator()({params}) const override" - elif functional_type == 'multi_transformer': + if len(out_types) == 1: + return f"{out_types[0]} operator()({params}) const override" + else: + return f"std::tuple<{', '.join(out_types)}> operator()({params}) const override" + elif functional_type == 'filter': params = ', '.join([f"const {t}& in{i+1}" for i, t in enumerate(in_types)]) - return f"std::tuple<{', '.join(out_types)}> operator()({params}) const override" - elif functional_type == 'merging_transformer': - return f"{out_types[0]} operator()(const std::vector<{in_types[0]}*>& inputs) const override" - elif functional_type == 'splitting_transformer': - return f"std::vector<{out_types[0]}> operator()(const {in_types[0]}& input) const override" - elif functional_type == 'scalar_transformer': - return f"{out_types[0]} operator()(const {in_types[0]}& input) const override" + return f"bool operator()({params}) const override" return "" -def generate_operator_body(functional_type: str, inputs: List[str], outputs: List[str]) -> str: +def generate_operator_body(functional_type: str, outputs: List[str]) -> str: """Generate a template body for the operator().""" + out_types = [parse_data_spec(o)[0] for o in outputs] + if functional_type == 'consumer': - return " // Process input data here\n" + return " // Process input data here\n" elif functional_type == 'producer': - out_types = [parse_data_spec(o)[0] for o in outputs] - if len(out_types) == 1: - return f" // Generate and return output data\n return {out_types[0]}{{}};\n" + if len(out_types) == 0: + return " // Perform operations here\n" + elif len(out_types) == 1: + return f" // Generate and return output data\n auto output = {out_types[0]}();\n \n return output;\n" else: - return f" // Generate and return output data\n return {{{', '.join([f'{t}{{}}' for t in out_types])}}};\n" + lines = [] + for i, typ in enumerate(out_types): + lines.append(f" auto output{i+1} = {typ}();") + lines.append("\n // TODO: Fill output collections\n") + lines.append(f" return std::make_tuple({', '.join([f'std::move(output{i+1})' for i in range(len(out_types))])});") + return '\n'.join(lines) + '\n' elif functional_type == 'filter': - return " // Apply filter logic and return true/false\n return true;\n" - elif functional_type in ['transformer', 'scalar_transformer']: - out_type = parse_data_spec(outputs[0])[0] - return f" // Transform input(s) to output\n return {out_type}{{}};\n" - elif functional_type == 'multi_transformer': - out_types = [parse_data_spec(o)[0] for o in outputs] - return f" // Transform inputs to multiple outputs\n return {{{', '.join([f'{t}{{}}' for t in out_types])}}};\n" - elif functional_type == 'merging_transformer': - out_type = parse_data_spec(outputs[0])[0] - return f" // Merge inputs into single output\n return {out_type}{{}};\n" - elif functional_type == 'splitting_transformer': - out_type = parse_data_spec(outputs[0])[0] - return f" // Split input into multiple outputs\n return std::vector<{out_type}>{{}};\n" + return " // Apply filter logic and return true/false\n return true;\n" + elif functional_type == 'transformer': + if len(out_types) == 1: + return f" // Transform input(s) to output\n auto output = {out_types[0]}();\n \n return output;\n" + else: + lines = [] + for i, typ in enumerate(out_types): + lines.append(f" auto output{i+1} = {typ}();") + lines.append("\n // TODO: Fill output collections\n") + lines.append(f" return std::make_tuple({', '.join([f'std::move(output{i+1})' for i in range(len(out_types))])});") + return '\n'.join(lines) + '\n' return "" +def extract_edm_includes(typ: str) -> List[str]: + """Extract EDM4hep include files from a type string.""" + includes = [] + + # Handle podio types + if 'podio::UserDataCollection' in typ: + includes.append('#include "podio/UserDataCollection.h"') + return includes + + # Handle edm4hep types + if 'edm4hep::' in typ: + # Extract all collection types (handle nested templates) + # Match patterns like edm4hep::MCParticleCollection + pattern = r'edm4hep::(\w+Collection)' + matches = re.findall(pattern, typ) + for match in matches: + base_type = match.replace('Collection', '') + includes.append(f'#include "edm4hep/{base_type}Collection.h"') + + return includes + + +def generate_includes(functional_type: str, inputs: List[str], outputs: List[str], + framework: str, properties: List[str]) -> str: + """Generate include statements.""" + includes = [] + + if framework == 'k4fwcore': + includes.append('#include "k4FWCore/Consumer.h"' if functional_type == 'consumer' else + '#include "k4FWCore/Producer.h"' if functional_type == 'producer' else + '#include "k4FWCore/Transformer.h"' if functional_type == 'transformer' else + '#include "k4FWCore/FilterPredicate.h"') + else: + includes.append('#include "GaudiAlg/Functional.h"') + includes.append('#include "GaudiKernel/KeyValue.h"') + + if properties: + includes.append('#include "Gaudi/Property.h"') + + includes.append('') + + # Collect unique type includes + all_type_strings = [] + for inp in inputs: + typ, _ = parse_data_spec(inp) + all_type_strings.append(typ) + for out in outputs: + typ, _ = parse_data_spec(out) + all_type_strings.append(typ) + + # Generate includes for EDM4hep types + type_includes = [] + for typ in all_type_strings: + type_includes.extend(extract_edm_includes(typ)) + + if type_includes: + includes.extend(sorted(set(type_includes))) + includes.append('') + + includes.append('#include ') + + # Add tuple if multiple outputs + out_types = [parse_data_spec(o)[0] for o in outputs] + if len(out_types) > 1: + includes.append('#include ') + + return '\n'.join(includes) + + +def generate_properties(properties: List[str]) -> str: + """Generate Gaudi property declarations.""" + if not properties: + return "" + + lines = ["\n\nprivate:"] + for prop_spec in properties: + typ, name, default, desc = parse_property_spec(prop_spec) + lines.append(f' Gaudi::Property<{typ}> m_{name}{{this, "{name}", {default}, "{desc}"}};') + + return '\n'.join(lines) + + +def generate_return_type_alias(outputs: List[str]) -> Optional[str]: + """Generate return type alias if needed (for complex return types).""" + out_types = [parse_data_spec(o)[0] for o in outputs] + if len(out_types) > 1: + types_str = ',\n '.join(out_types) + return f"using retType =\n std::tuple<{types_str}>;\n\n" + return None + + def generate_class(class_name: str, functional_type: str, inputs: List[str], - outputs: List[str], namespace: str = '') -> str: + outputs: List[str], namespace: str, framework: str, + use_struct: bool, properties: List[str]) -> str: """Generate the complete C++ class code.""" base_class = FUNCTIONAL_TYPES[functional_type]['base'] - template_sig = generate_template_signature(functional_type, inputs, outputs) - input_keyvalues = generate_constructor_inputs(inputs) - output_keyvalues = generate_constructor_outputs(outputs) - operator_sig = generate_operator_signature(functional_type, inputs, outputs) - operator_body = generate_operator_body(functional_type, inputs, outputs) - # Build constructor initializer list - init_parts = [f"\n {base_class}(\n name,\n pSvc"] - if input_keyvalues: - init_parts.append(f", {input_keyvalues}") - if output_keyvalues: - init_parts.append(f",\n {output_keyvalues}") - init_parts.append(")") + if framework == 'k4fwcore': + base_class = f"k4FWCore::{base_class}" + else: + base_class = f"Gaudi::Functional::{base_class}" + + template_sig = generate_template_signature(functional_type, inputs, outputs, framework) + constructor = generate_constructor(class_name, functional_type, inputs, outputs, framework, base_class) + operator_sig = generate_operator_signature(functional_type, inputs, outputs) + operator_body = generate_operator_body(functional_type, outputs) + includes = generate_includes(functional_type, inputs, outputs, framework, properties) + prop_declarations = generate_properties(properties) + return_type_alias = generate_return_type_alias(outputs) - constructor_init = ''.join(init_parts) + class_keyword = "struct" if use_struct else "class" + public_keyword = "" if use_struct else "public:\n" code = f"""// Generated by Gaudi Functional C++ Class Generator -#include "GaudiAlg/Functional.h" -#include "GaudiKernel/KeyValue.h" +{includes} """ if namespace: code += f"namespace {namespace} {{\n\n" - code += f"""class {class_name} - : public Gaudi::Functional::{base_class}<{template_sig}> {{ + # Use retType alias if it exists + if return_type_alias: + code += return_type_alias + template_for_class = "retType()" + else: + template_for_class = template_sig + + code += f"""{class_keyword} {class_name} final : {base_class}<{template_for_class}> {{ -public: - {class_name}(const std::string& name, ISvcLocator* pSvc) - :{constructor_init} {{}} +{public_keyword}{constructor} - {operator_sig} {{ -{operator_body} }} + // This is the function that will be called to produce the data + {operator_sig} {{ +{operator_body} }}{prop_declarations} }}; """ @@ -291,8 +437,11 @@ def main(): elif args.functional_type == 'producer' and not args.outputs: print("Error: Producer requires at least one output", file=sys.stderr) return 1 - elif args.functional_type in ['transformer', 'filter'] and (not args.inputs or not args.outputs): - print(f"Error: {args.functional_type} requires both inputs and outputs", file=sys.stderr) + elif args.functional_type in ['transformer', 'filter'] and not args.inputs: + print(f"Error: {args.functional_type} requires at least one input", file=sys.stderr) + return 1 + elif args.functional_type == 'transformer' and not args.outputs: + print(f"Error: {args.functional_type} requires at least one output", file=sys.stderr) return 1 # Generate the class @@ -301,14 +450,16 @@ def main(): args.functional_type, args.inputs, args.outputs, - args.namespace + args.namespace, + args.framework, + args.struct, + args.properties ) # Determine output file output_file = args.output_file if not output_file: - ext = '.h' if args.header_only else '.cpp' - output_file = f"{args.class_name}{ext}" + output_file = f"{args.class_name}.cpp" # Write to file or stdout if output_file == '-': From 49e8064afe280dab80fb923491732889cb4d9fd3 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Tue, 13 Jan 2026 17:06:46 +0100 Subject: [PATCH 03/36] Refactor argument parsing for class generation --- k4FWCore/helpers/gaudi_gen.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/k4FWCore/helpers/gaudi_gen.py b/k4FWCore/helpers/gaudi_gen.py index e7e1c9558..889ddc041 100644 --- a/k4FWCore/helpers/gaudi_gen.py +++ b/k4FWCore/helpers/gaudi_gen.py @@ -58,8 +58,8 @@ def parse_arguments(): help='Output file name (default: .cpp)') parser.add_argument('--framework', choices=['gaudi', 'k4fwcore'], default='k4fwcore', help='Target framework (default: k4fwcore)') - parser.add_argument('--struct', action='store_true', - help='Generate as struct instead of class') + parser.add_argument('--class', dest='use_class', action='store_true', + help='Generate as class instead of struct (struct is default for k4fwcore)') parser.add_argument('-p', '--properties', nargs='*', default=[], help='Gaudi properties (format: "Type:Name:Default:Description")') @@ -75,12 +75,11 @@ def _get_functional_types_help(): help_text += f" Example: {info['example']}\n" help_text += "\n\nExample Usage:\n" - help_text += " # k4FWCore producer with multiple outputs\n" + help_text += " # k4FWCore producer with multiple outputs (generates struct by default)\n" help_text += " python gaudi_gen.py MyProducer producer \\\n" help_text += " -o 'edm4hep::MCParticleCollection:MCParticles' \\\n" - help_text += " 'edm4hep::TrackCollection:Tracks' \\\n" - help_text += " --framework k4fwcore --struct\n\n" - help_text += " # Gaudi transformer\n" + help_text += " 'edm4hep::TrackCollection:Tracks'\n\n" + help_text += " # Gaudi transformer (generates class by default)\n" help_text += " python gaudi_gen.py MyTransformer transformer \\\n" help_text += " -i 'InputType:InputLoc' \\\n" help_text += " -o 'OutputType:OutputLoc' \\\n" @@ -372,7 +371,7 @@ def generate_return_type_alias(outputs: List[str]) -> Optional[str]: def generate_class(class_name: str, functional_type: str, inputs: List[str], outputs: List[str], namespace: str, framework: str, - use_struct: bool, properties: List[str]) -> str: + use_class: bool, properties: List[str]) -> str: """Generate the complete C++ class code.""" base_class = FUNCTIONAL_TYPES[functional_type]['base'] @@ -389,8 +388,13 @@ def generate_class(class_name: str, functional_type: str, inputs: List[str], prop_declarations = generate_properties(properties) return_type_alias = generate_return_type_alias(outputs) - class_keyword = "struct" if use_struct else "class" - public_keyword = "" if use_struct else "public:\n" + # k4FWCore uses struct by default, Gaudi uses class by default + if framework == 'k4fwcore': + class_keyword = "class" if use_class else "struct" + public_keyword = "public:\n" if use_class else "" + else: + class_keyword = "struct" if not use_class else "class" + public_keyword = "" if not use_class else "public:\n" code = f"""// Generated by Gaudi Functional C++ Class Generator {includes} @@ -452,7 +456,7 @@ def main(): args.outputs, args.namespace, args.framework, - args.struct, + args.use_class, args.properties ) From ddf85c19e39d5d99e5f88e63c95d2f27023fa818 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Wed, 14 Jan 2026 16:05:25 +0100 Subject: [PATCH 04/36] Add command_line parameter to generate_class function Added command_line parameter to generate_class function and updated its usage in main. --- k4FWCore/helpers/gaudi_gen.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/k4FWCore/helpers/gaudi_gen.py b/k4FWCore/helpers/gaudi_gen.py index 889ddc041..ba0e449f6 100644 --- a/k4FWCore/helpers/gaudi_gen.py +++ b/k4FWCore/helpers/gaudi_gen.py @@ -371,7 +371,7 @@ def generate_return_type_alias(outputs: List[str]) -> Optional[str]: def generate_class(class_name: str, functional_type: str, inputs: List[str], outputs: List[str], namespace: str, framework: str, - use_class: bool, properties: List[str]) -> str: + use_class: bool, properties: List[str], command_line: str) -> str: """Generate the complete C++ class code.""" base_class = FUNCTIONAL_TYPES[functional_type]['base'] @@ -397,6 +397,7 @@ def generate_class(class_name: str, functional_type: str, inputs: List[str], public_keyword = "" if not use_class else "public:\n" code = f"""// Generated by Gaudi Functional C++ Class Generator +// Command: {command_line} {includes} """ @@ -448,6 +449,10 @@ def main(): print(f"Error: {args.functional_type} requires at least one output", file=sys.stderr) return 1 + # Reconstruct the command line for documentation + import shlex + command_line = ' '.join(shlex.quote(arg) for arg in sys.argv) + # Generate the class code = generate_class( args.class_name, @@ -457,7 +462,8 @@ def main(): args.namespace, args.framework, args.use_class, - args.properties + args.properties, + command_line ) # Determine output file @@ -478,3 +484,4 @@ def main(): if __name__ == '__main__': sys.exit(main()) + From e48e1ac5f2d43c2b50a9308eb3f2b2e232ccd157 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Thu, 15 Jan 2026 10:57:51 +0100 Subject: [PATCH 05/36] Refactor base class handling in gaudi_gen.py --- k4FWCore/helpers/gaudi_gen.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/k4FWCore/helpers/gaudi_gen.py b/k4FWCore/helpers/gaudi_gen.py index ba0e449f6..9ad26db18 100644 --- a/k4FWCore/helpers/gaudi_gen.py +++ b/k4FWCore/helpers/gaudi_gen.py @@ -190,20 +190,22 @@ def generate_keyvalue_gaudi(data_specs: List[str]) -> str: def generate_constructor(class_name: str, functional_type: str, inputs: List[str], - outputs: List[str], framework: str, base_class: str) -> str: + outputs: List[str], framework: str, base_class_short: str) -> str: """Generate the constructor.""" if framework == 'k4fwcore': input_kv = generate_keyvalues_k4fwcore(inputs, True) output_kv = generate_keyvalues_k4fwcore(outputs, False) + # For k4FWCore, use the short base class name (just "Producer", not "k4FWCore::Producer") + # because we're already inheriting from the fully qualified name return f""" {class_name}(const std::string& name, ISvcLocator* svcLoc) - : {base_class}(name, svcLoc, {input_kv}, + : {base_class_short}(name, svcLoc, {input_kv}, {output_kv}) {{}}""" else: # gaudi input_kv = generate_keyvalue_gaudi(inputs) output_kv = generate_keyvalue_gaudi(outputs) - init_parts = [f"\n {base_class}(\n name,\n pSvc"] + init_parts = [f"\n {base_class_short}(\n name,\n pSvc"] if input_kv: init_parts.append(f", {input_kv}") if output_kv: @@ -373,15 +375,16 @@ def generate_class(class_name: str, functional_type: str, inputs: List[str], outputs: List[str], namespace: str, framework: str, use_class: bool, properties: List[str], command_line: str) -> str: """Generate the complete C++ class code.""" - base_class = FUNCTIONAL_TYPES[functional_type]['base'] + base_class_short = FUNCTIONAL_TYPES[functional_type]['base'] if framework == 'k4fwcore': - base_class = f"k4FWCore::{base_class}" + base_class_full = f"k4FWCore::{base_class_short}" else: - base_class = f"Gaudi::Functional::{base_class}" + base_class_full = f"Gaudi::Functional::{base_class_short}" template_sig = generate_template_signature(functional_type, inputs, outputs, framework) - constructor = generate_constructor(class_name, functional_type, inputs, outputs, framework, base_class) + # Pass just the short name for constructor initialization + constructor = generate_constructor(class_name, functional_type, inputs, outputs, framework, base_class_short) operator_sig = generate_operator_signature(functional_type, inputs, outputs) operator_body = generate_operator_body(functional_type, outputs) includes = generate_includes(functional_type, inputs, outputs, framework, properties) @@ -412,7 +415,7 @@ def generate_class(class_name: str, functional_type: str, inputs: List[str], else: template_for_class = template_sig - code += f"""{class_keyword} {class_name} final : {base_class}<{template_for_class}> {{ + code += f"""{class_keyword} {class_name} final : {base_class_full}<{template_for_class}> {{ {public_keyword}{constructor} @@ -484,4 +487,3 @@ def main(): if __name__ == '__main__': sys.exit(main()) - From d9bdbe7745cd50fbe4a141282ecba088f6d0f663 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Fri, 27 Mar 2026 16:42:45 +0100 Subject: [PATCH 06/36] update the generator according to the comments --- k4FWCore/helpers/gaudi_gen.py | 1034 +++++++++++++++++++++++++++++++++ 1 file changed, 1034 insertions(+) diff --git a/k4FWCore/helpers/gaudi_gen.py b/k4FWCore/helpers/gaudi_gen.py index 9ad26db18..bda4c123a 100644 --- a/k4FWCore/helpers/gaudi_gen.py +++ b/k4FWCore/helpers/gaudi_gen.py @@ -1,6 +1,1040 @@ #!/usr/bin/env python3 """ Gaudi Functional C++ Class Generator +""" + +import argparse +import sys +import re +import shlex +from typing import List, Tuple + +# Configuration Constants +FUNCTIONAL_TYPES = { + 'consumer': {'base': 'Consumer', 'desc': 'In -> Void'}, + 'producer': {'base': 'Producer', 'desc': 'Void -> Out'}, + 'transformer': {'base': 'Transformer', 'desc': 'In -> Out'}, + 'filter': {'base': 'FilterPredicate', 'desc': 'In -> Bool'} +} + + +class GaudiGen: + def __init__(self, args): + self.args = args + self.command_line = ' '.join(shlex.quote(arg) for arg in sys.argv) + + def parse_data_spec(self, spec: str) -> Tuple[str, str]: + """ + Splits TYPE:key at the last separator colon outside angle brackets. + Skips C++ namespace '::' tokens so edm4hep::Foo:key splits correctly. + """ + depth, colon_pos = 0, -1 + for i, char in enumerate(spec): + if char == '<': + depth += 1 + elif char == '>': + depth -= 1 + elif char == ':' and depth == 0: + prev_colon = (i > 0 and spec[i - 1] == ':') + next_colon = (i + 1 < len(spec) and spec[i + 1] == ':') + if not prev_colon and not next_colon: + colon_pos = i + return (spec, '') if colon_pos == -1 else (spec[:colon_pos], spec[colon_pos + 1:]) + + def parse_runtime_input_spec(self, spec: str): + """ + Parses a --runtime-inputs spec of the form: + TYPE:KEY:Default0[,Default1,...] + Returns (type_str, key_str, [default, ...]) + Uses the FIRST separator colon (not inside <>) as the TYPE/KEY boundary, + and the second separator colon as the KEY/defaults boundary. + """ + # Find all separator colons (not inside <>, not part of ::) + depth, sep_positions = 0, [] + for i, ch in enumerate(spec): + if ch == '<': depth += 1 + elif ch == '>': depth -= 1 + elif ch == ':' and depth == 0: + prev = (i > 0 and spec[i-1] == ':') + nxt = (i+1 < len(spec) and spec[i+1] == ':') + if not prev and not nxt: + sep_positions.append(i) + if len(sep_positions) == 0: + return spec, self._default_key(spec), [self._default_key(spec)] + if len(sep_positions) == 1: + p = sep_positions[0] + typ, key = spec[:p], spec[p+1:] + return typ, key, [key] + # Two or more: first split = TYPE, second split = KEY, rest = defaults + p0, p1 = sep_positions[0], sep_positions[1] + typ = spec[:p0] + key = spec[p0+1:p1] + defaults = [d.strip() for d in spec[p1+1:].split(',')] + return typ, key, defaults + + def _default_key(self, typ: str) -> str: + """e.g. edm4hep::MCParticleCollection -> MCParticles""" + base = typ.split('::')[-1] + base = re.sub(r'Collection$', '', base) + return base + 's' + + # ------------------------------------------------------------------ + # Mode helpers + # ------------------------------------------------------------------ + def _is_k4(self) -> bool: + return self.args.framework == 'k4fwcore' + + def _base_class_alias(self) -> str: + """Emits the BaseClass_t alias for native Gaudi Functional.""" + return "using BaseClass_t = Gaudi::Functional::Traits::BaseClass_t;" + + def _use_ret_type_alias(self) -> bool: + """True for k4fwcore with multiple *fixed* outputs (not runtime).""" + return self._is_k4() and not self.args.runtime_outputs and len(self.args.outputs) > 1 + + def _is_runtime(self) -> bool: + """True when --runtime-outputs is set (dynamic vector return).""" + return bool(self.args.runtime_outputs) + + # ------------------------------------------------------------------ + # Type aliases + # ------------------------------------------------------------------ + def _collect_type_aliases(self) -> List[Tuple[str, str]]: + """ + Returns an ordered list of (alias, full_type) for each unique input type. + + Alias strategy (mirrors k4FWCore reference examples): + podio::UserDataCollection -> FloatColl + edm4hep::MCParticleCollection -> ParticleColl + edm4hep::SimTrackerHitCollection -> SimTrackerHitColl + edm4hep::TrackerHit3DCollection -> TrackerHitColl + edm4hep::TrackCollection -> TrackColl + edm4hep::ReconstructedParticleCollection -> RecoColl + edm4hep::RecoMCParticleLinkCollection -> LinkColl + + General rule: + 1. Template types (UserDataCollection): use T.capitalize() + Coll + 2. Names ending in 'LinkCollection': strip to stem before 'Link' + LinkColl -> LinkColl + 3. Names ending in 'HitCollection' : strip 'Collection', keep full stem + Coll + 4. Names ending in 'Collection': strip 'Collection' + last CamelCase word + Coll + Duplicates get a numeric suffix. + """ + seen_types: dict = {} + aliases: List[Tuple[str, str]] = [] + for spec in self.args.inputs: + typ, _ = self.parse_data_spec(spec) + if typ in seen_types: + continue + # Template types: podio::UserDataCollection -> FloatColl + inner = re.search(r'<([^>]+)>', typ) + if inner: + base = inner.group(1).strip().split('::')[-1].capitalize() + else: + last = typ.split('::')[-1] # e.g. SimTrackerHitCollection + stem = re.sub(r'Collection$', '', last) # e.g. SimTrackerHit + # LinkCollection -> LinkColl + if stem.endswith('Link'): + base = 'Link' + # *HitCollection or *Hit3DCollection -> keep full stem + elif re.search(r'Hit', stem): + # TrackerHit3D -> TrackerHit, SimTrackerHit -> SimTrackerHit + base = re.sub(r'\d+[A-Z]?$', '', stem) or stem + # Reconstructed* -> Reco, otherwise last CamelCase word + elif stem.startswith('Reconstructed'): + base = 'Reco' + else: + words = re.findall(r'[A-Z][a-z0-9]*', stem) + base = words[-1] if words else stem + alias = base + 'Coll' + existing_aliases = {a for a, _ in aliases} + suffix, candidate = 2, alias + while candidate in existing_aliases: + candidate = alias + str(suffix); suffix += 1 + alias = candidate + seen_types[typ] = alias + aliases.append((alias, typ)) + return aliases + + def _alias_for(self, typ: str, alias_map: dict) -> str: + return alias_map.get(typ, typ) + + # ------------------------------------------------------------------ + # Includes + # ------------------------------------------------------------------ + def get_includes(self) -> str: + inc = [] + + if self._is_k4(): + base = FUNCTIONAL_TYPES[self.args.functional_type]['base'] + inc.append(f'#include "k4FWCore/{base}.h"') + else: + base = FUNCTIONAL_TYPES[self.args.functional_type]['base'] + inc.append(f'#include "Gaudi/Functional/{base}.h"') + + if self.args.properties: + inc.append('#include "Gaudi/Property.h"') + + type_inc = set() + # Collect all type specs: fixed outputs, inputs, and runtime output type + all_specs = list(self.args.inputs) + list(self.args.outputs) + if self._is_runtime(): + all_specs.append(self.args.runtime_outputs) # bare type, no key + + for spec in all_specs: + typ, _ = self.parse_data_spec(spec) + if 'podio::UserDataCollection' in typ: + type_inc.add('#include "podio/UserDataCollection.h"') + if 'edm4hep::' in typ: + for match in re.findall(r'edm4hep::(\w+Collection)', typ): + type_inc.add(f'#include "edm4hep/{match}.h"') + + inc.extend(sorted(type_inc)) + inc.append('#include ') + if self.args.functional_type == 'consumer': + inc.append('#include ') + inc.append('#include ') + if getattr(self.args, 'event_context', False): + inc.extend(['#include ', '#include ', '#include ']) + if len(self.args.outputs) > 1: + inc.append('#include ') + # runtime mode needs + if self._is_runtime() or getattr(self.args, 'runtime_inputs', None): + inc.append('#include ') + return '\n'.join(inc) + + # ------------------------------------------------------------------ + # retType alias (multi-output k4fwcore, fixed collections only) + # ------------------------------------------------------------------ + def _ret_type_alias(self) -> str: + out_types = [self.parse_data_spec(o)[0] for o in self.args.outputs] + # Align continuation lines under the first type (after 'std::tuple<') + prefix = ' std::tuple<' + indent = ' ' * len(prefix) + joined = (',\n' + indent).join(out_types) + return f'using retType =\n{prefix}{joined}>;' + + # ------------------------------------------------------------------ + # C++ return type string used in signature and operator() + # ------------------------------------------------------------------ + def _cpp_return_type(self) -> str: + f_type = self.args.functional_type + out_types = [self.parse_data_spec(o)[0] for o in self.args.outputs] + + if f_type == 'consumer': + return 'void' + if f_type == 'filter': + return 'bool' + if self._is_runtime(): + elem = self.parse_data_spec(self.args.runtime_outputs)[0] + return f'std::vector<{elem}>' + if self._use_ret_type_alias(): + return 'retType' + if len(out_types) == 1: + return out_types[0] + return f"std::tuple<{', '.join(out_types)}>" + + # ------------------------------------------------------------------ + # Template signature e.g. Out(const In1&, const In2&) + # ------------------------------------------------------------------ + def generate_signature(self) -> str: + runtime_in_keys = {self.parse_runtime_input_spec(s)[1] + for s in getattr(self.args, 'runtime_inputs', []) or []} + use_aliases = getattr(self.args, 'type_aliases', False) + alias_map = {typ: alias for alias, typ in self._collect_type_aliases()} if use_aliases else {} + in_t = [] + for spec in self.args.inputs: + typ, key = self.parse_data_spec(spec) + key = key if key else self._default_key(typ) + disp = self._alias_for(typ, alias_map) + if key in runtime_in_keys: + in_t.append(f'const std::vector&') + else: + in_t.append(f'const {disp}&') + if getattr(self.args, 'event_context', False): + in_t = ['const EventContext&'] + in_t + ret = self._cpp_return_type() + sig = f"{ret}({', '.join(in_t)})" + if not self._is_k4(): + return f"{sig}, BaseClass_t" + return sig + + # ------------------------------------------------------------------ + # Constructor + # ------------------------------------------------------------------ + def _build_constructor_k4fwcore(self) -> str: + cls = self.args.class_name + base_short = FUNCTIONAL_TYPES[self.args.functional_type]['base'] + f_type = self.args.functional_type + + # ---- inputs block ---- + # runtime_inputs entries use KeyValues(name, {defaults...}), + # plain inputs use KeyValue(name, default). + # Mixed case always uses a brace-list. + # Build mapping key -> defaults list from --runtime-inputs specs + ri_map = {} # key -> [default, ...] + for s in (getattr(self.args, 'runtime_inputs', []) or []): + _, key, defaults = self.parse_runtime_input_spec(s) + ri_map[key] = defaults + ri_keys = set(ri_map.keys()) + + # Build mapping key -> default from --keyvalues-inputs specs + # These use KeyValues(name, {default}) but operator() type is still const TYPE& + kvi_map = {} # key -> default string + for s in (getattr(self.args, 'keyvalues_inputs', []) or []): + parts = s.split(':', 1) + k = parts[0] + val = parts[1] if len(parts) > 1 else k + kvi_map[k] = val + kvi_keys = set(kvi_map.keys()) + + if not self.args.inputs: + in_block = '{}' + else: + kv_list = [] + has_runtime_input = False + for spec in self.args.inputs: + typ, key = self.parse_data_spec(spec) + key = key if key else self._default_key(typ) + if key in ri_keys: + defaults = ri_map.get(key, [key]) + defs_str = ', '.join(f'"{d}"' for d in defaults) + kv_list.append(f'KeyValues("{key}", {{{defs_str}}})') + has_runtime_input = True + elif key in kvi_keys: + default = kvi_map.get(key, key) + kv_list.append(f'KeyValues("{key}", {{"{default}"}})') + elif getattr(self.args, 'all_keyvalues', False): + kv_list.append(f'KeyValues("{key}", {{"{key}"}})') + else: + kv_list.append(f'KeyValue("{key}", "{key}")') + # Single entry (KeyValue or KeyValues) stays bare; multiple use brace-list + if len(kv_list) == 1: + in_block = kv_list[0] + else: + indent = ' ' * 20 + in_block = '{\n' + indent + (',\n' + indent).join(kv_list) + ',\n' + ' ' * 16 + '}' + + # ---- outputs block ---- + if f_type in ('consumer', 'filter'): + # Consumer/filter have only inputs; pass bare KV or brace-list + ctor_args = in_block + + elif self._is_runtime(): + # Runtime: always KeyValues(...) in a brace-list, even if only one entry + # The property name is e.g. "OutputCollections"; the default value is + # the bare collection name without a trailing 's' added again. + typ, key = self.parse_data_spec(self.args.runtime_outputs) + key = key if key else self._default_key(typ) + out_block = f'{{KeyValues("OutputCollections", {{"{key}"}})}}' + ctor_args = f'{in_block}, {out_block}' + + elif not self.args.outputs: + ctor_args = f'{in_block}, {{}}' + + elif len(self.args.outputs) > 1: + # Multiple fixed outputs: brace-list of KeyValues(...) + # Reference style: + # : Producer(name, svcLoc, {}, + # { + # KeyValues("A", {"a"}), + # KeyValues("B", {"b"})}) {} + kv_items = [] + for spec in self.args.outputs: + typ, key = self.parse_data_spec(spec) + key = key if key else self._default_key(typ) + kv_items.append(f'KeyValues("{key}", {{"{key}"}})') + # Reference layout (17 spaces aligns all KeyValues entries): + # : Producer(name, svcLoc, {}, + # { + # KeyValues("A", {"a"}), + # KeyValues("B", {"b"})}) {} + kv_ind = ' ' * 17 + joined = (',\n' + kv_ind).join(kv_items) + cls_ = self.args.class_name + return ( + f' {cls_}(const std::string& name, ISvcLocator* svcLoc)\n' + f' : {base_short}(name, svcLoc, {in_block},\n' + f' {{\n' + f' {joined}}})' + f' {{}}' + ) + + else: + # Single fixed output: bare KeyValue(...) + typ, key = self.parse_data_spec(self.args.outputs[0]) + key = key if key else self._default_key(typ) + out_block = f'KeyValue("{key}", "{key}")' + ctor_args = f'{in_block}, {out_block}' + + return ( + f' {cls}(const std::string& name, ISvcLocator* svcLoc)\n' + f' : {base_short}(name, svcLoc, {ctor_args}) {{}}' + ) + + def _build_constructor_gaudi(self) -> str: + """ + Native Gaudi Functional constructor. + Single KV -> : Producer(name, svcLoc, KeyValue{"Name", "Default"}) + Multiple -> : Producer(name, svcLoc, {KeyValue{"A","a"}, KeyValue{"B","b"}}) + """ + cls = self.args.class_name + base_short = FUNCTIONAL_TYPES[self.args.functional_type]['base'] + f_type = self.args.functional_type + + def kv(spec: str) -> str: + typ, key = self.parse_data_spec(spec) + key = key if key else self._default_key(typ) + return f'KeyValue{{"{ key }", "{ key }"}}' + + in_kvs = [kv(s) for s in self.args.inputs] + out_kvs = [kv(s) for s in self.args.outputs] + all_kvs = in_kvs + out_kvs + + if not all_kvs: + ctor_args = '' + elif len(all_kvs) == 1: + ctor_args = all_kvs[0] + else: + ctor_args = '{' + ', '.join(all_kvs) + '}' + + sep = ', ' if ctor_args else '' + return ( + f' {cls}(const std::string& name, ISvcLocator* svcLoc)\n' + f' : {base_short}(name, svcLoc{sep}{ctor_args}) {{}}' + ) + + # ------------------------------------------------------------------ + # operator() signature + # ------------------------------------------------------------------ + def generate_op_sig(self) -> str: + f_type = self.args.functional_type + ret = self._cpp_return_type() + + runtime_in_keys = {self.parse_runtime_input_spec(s)[1] + for s in getattr(self.args, 'runtime_inputs', []) or []} + use_aliases = getattr(self.args, 'type_aliases', False) + alias_map = {typ: alias for alias, typ in self._collect_type_aliases()} if use_aliases else {} + params = [] + if getattr(self.args, 'event_context', False): + params.append('const EventContext& ctx') + for spec in self.args.inputs: + typ, key = self.parse_data_spec(spec) + key = key if key else self._default_key(typ) + disp = self._alias_for(typ, alias_map) + if key in runtime_in_keys: + params.append(f'const std::vector& {key}') + else: + params.append(f'const {disp}& {key}') + + param_str = ', '.join(params) + return f'{ret} operator()({param_str}) const override' + + def _default_return(self) -> str: + f_type = self.args.functional_type + out_types = [self.parse_data_spec(o)[0] for o in self.args.outputs] + + if f_type == 'consumer': + return '' + if f_type == 'filter': + return 'return false;' + if self._is_runtime(): + elem = self.parse_data_spec(self.args.runtime_outputs)[0] + return f'return std::vector<{elem}>{{}};' + if len(out_types) == 1: + return f'return {out_types[0]}{{}};' + # Multi-output: named locals + std::move (mirrors reference) + lines = [] + for i, t in enumerate(out_types, 1): + lines.append(f' auto output{i} = {t}();') + lines.append('') + lines.append(' // TODO: Fill output collections') + lines.append('') + moves = ', '.join(f'std::move(output{i})' for i in range(1, len(out_types)+1)) + lines.append(f' return std::make_tuple({moves}); // NOLINT') + return '\n'.join(lines) + + # ------------------------------------------------------------------ + # Runtime operator() body hint + # ------------------------------------------------------------------ + def _initialize_hint(self) -> List[str]: + """Emits an initialize() override skeleton when runtime inputs are present.""" + ri = getattr(self.args, 'runtime_inputs', None) or [] + if not ri: + return [] + lines = [ + " StatusCode initialize() override {", + " // Verify input locations set from Python", + ] + for i, spec in enumerate(self.args.inputs): + typ, key = self.parse_data_spec(spec) + key = key if key else self._default_key(typ) + lines += [ + f' // inputLocations({i}) or inputLocations("{key}") -> names of {key}', + ] + lines += [ + " return StatusCode::SUCCESS;", + " }", + ] + return lines + + def _consumer_body_hint(self) -> str: + """Skeleton body for Consumer showing Gaudi messaging and iteration idioms.""" + lines = [] + if getattr(self.args, 'event_context', False): + lines.append(' info() << "Event number is " << ctx.evt() << endmsg;') + if not self.args.inputs: + lines.append(' // TODO: Implement consumer logic') + return '\n'.join(lines) + for spec in self.args.inputs: + typ, key = self.parse_data_spec(spec) + key = key if key else self._default_key(typ) + lines += [ + f' debug() << "Received {key} with " << {key}.size() << " elements" << endmsg;', + f' for (const auto& elem : {key}) {{', + f' // TODO: process elem', + f' }}', + ] + return '\n'.join(lines) + + def _runtime_body_hint(self) -> str: + """Emits a skeleton body showing outputLocations() usage.""" + elem = self.parse_data_spec(self.args.runtime_outputs)[0] + return ( + f' const auto locs = outputLocations();\n' + f' std::vector<{elem}> outputCollections;\n' + f' for (size_t i = 0; i < locs.size(); ++i) {{\n' + f' auto coll = {elem}();\n' + f' // TODO: fill coll\n' + f' outputCollections.emplace_back(std::move(coll));\n' + f' }}\n' + f' return outputCollections;' + ) + + # ------------------------------------------------------------------ + # Private members (Gaudi only) + # ------------------------------------------------------------------ + def _build_data_handle_members(self) -> str: + lines = [] + for spec in self.args.inputs: + typ, key = self.parse_data_spec(spec) + key = key if key else self._default_key(typ) + lines.append( + f' DataObjectReadHandle<{typ}> m_input_{key}' + f'{{this, "{key}", "{key}"}};' + ) + for spec in self.args.outputs: + typ, key = self.parse_data_spec(spec) + key = key if key else self._default_key(typ) + lines.append( + f' DataObjectWriteHandle<{typ}> m_output_{key}' + f'{{this, "{key}", "{key}"}};' + ) + return '\n'.join(lines) + + # ------------------------------------------------------------------ + # Properties + # ------------------------------------------------------------------ + def _build_properties(self) -> str: + lines = [] + for prop in self.args.properties: + parts = prop.split(':', 3) + # Format: type:name:default:desc + ptype = parts[0] + pname = parts[1] if len(parts) > 1 else parts[0] + pdefault = parts[2] if len(parts) > 2 else '0' + pdesc = parts[3] if len(parts) > 3 else f'Example {pname} property' + # Avoid double m_ prefix if the user already included it + member = pname if pname.startswith('m_') else f'm_{pname}' + lines.append( + f' Gaudi::Property<{ptype}> {member}' + f'{{this, "{pname}", {pdefault}, "{pdesc}"}};' + ) + return '\n'.join(lines) + + # ------------------------------------------------------------------ + # Top-level code generator + # ------------------------------------------------------------------ + def generate_code(self) -> str: + base_short = FUNCTIONAL_TYPES[self.args.functional_type]['base'] + base_full = f"{'k4FWCore' if self._is_k4() else 'Gaudi::Functional'}::{base_short}" + + kw = "struct" if not self.args.use_class else "class" + access = "public:\n " if self.args.use_class else "" + + ctor = ( + self._build_constructor_k4fwcore() + if self._is_k4() + else self._build_constructor_gaudi() + ) + + op_sig = self.generate_op_sig() + ns_open = f'namespace {self.args.namespace} {{\n' if self.args.namespace else '' + ns_close = f'\n}} // namespace {self.args.namespace}\n' if self.args.namespace else '\n' + + lines = [ + "// Generated by Gaudi Functional C++ Class Generator", + f"// Command: {self.command_line}", + "", + self.get_includes(), + ] + + if not self._is_k4(): + lines += ["", self._base_class_alias()] + if self._use_ret_type_alias(): + lines += ["", "// Which type of collections we are producing", self._ret_type_alias()] + if getattr(self.args, 'type_aliases', False): + aliases = self._collect_type_aliases() + if aliases: + lines.append("") + lines.append("// Which type of collections we are reading") + for alias, typ in aliases: + lines.append(f"using {alias} = {typ};") + + lines += [ + "", + f"{ns_open}{kw} {self.args.class_name} final" + f" : {base_full}<{self.generate_signature()}> {{", + f" {access}// Constructor: KeyValues map to collection names, settable from Python", + ctor, + ] + + # initialize() skeleton for runtime-input consumers + init_lines = self._initialize_hint() + if init_lines: + lines += init_lines + + lines += [ + f" // This is the function that will be called to produce the data", + f" {op_sig} {{", + ] + + if self._is_runtime(): + lines.append(' ' + self._runtime_body_hint().replace('\n', '\n ')) + elif self.args.functional_type == 'consumer': + lines.append(self._consumer_body_hint()) + else: + default_ret = self._default_return() + if '\n' in default_ret: + lines.append(default_ret) + else: + ret_line = f'\n {default_ret}' if default_ret else '' + lines.append(f' // TODO: Implement logic{ret_line}') + + lines.append(" }") + + # Emit properties: under private: if --private-properties, else public + if self.args.properties: + if getattr(self.args, 'private_properties', False): + lines += ["", "private:", self._build_properties()] + else: + lines += ["", self._build_properties()] + + # Event-context: add finalize() override and mutable threading members + if getattr(self.args, 'event_context', False): + lines += [ + "", + " StatusCode finalize() override {", + " // TODO: finalise event-context state", + " return StatusCode::SUCCESS;", + " }", + "", + " mutable std::set m_eventNumbersSeen{};", + " mutable std::mutex m_mutex{};", + ] + + lines += [ + f"}};" + ns_close, + f"DECLARE_COMPONENT({self.args.class_name})", + ] + + return '\n'.join(lines) + + # ------------------------------------------------------------------ + # CMake generator + # ------------------------------------------------------------------ + def generate_cmake(self) -> str: + cls = self.args.class_name + src = f'{cls}.cpp' + + all_specs = list(self.args.inputs) + list(self.args.outputs) + if self._is_runtime(): + all_specs.append(self.args.runtime_outputs) + has_edm4hep = any('edm4hep::' in self.parse_data_spec(s)[0] for s in all_specs) + has_podio = any('podio::' in self.parse_data_spec(s)[0] for s in all_specs) + + find_pkgs: List[str] = [] + link_libs: List[str] = [] + + if self._is_k4(): + find_pkgs.append('find_package(k4FWCore REQUIRED)') + link_libs.append('k4FWCore::k4FWCore') + else: + find_pkgs.append('find_package(Gaudi REQUIRED)') + link_libs += ['Gaudi::GaudiAlgLib', 'Gaudi::GaudiKernel'] + + if has_edm4hep: + find_pkgs.append('find_package(EDM4HEP REQUIRED)') + link_libs.append('EDM4HEP::edm4hep') + if has_podio: + find_pkgs.append('find_package(podio REQUIRED)') + link_libs.append('podio::podio') + + find_block = '\n'.join(find_pkgs) + link_block = '\n '.join(link_libs) + + return ( + f"# Generated by Gaudi Gen\n" + f"# Command: {self.command_line}\n" + f"\n" + f"cmake_minimum_required(VERSION 3.15)\n" + f"project({cls}Plugin)\n" + f"\n" + f"{find_block}\n" + f"\n" + f"gaudi_add_module({cls}Plugin\n" + f" SOURCES {src}\n" + f" LINK\n" + f" {link_block}\n" + f")\n" + ) + + + +# ====================================================================== +# C++ formatter +# ====================================================================== + +def _split_top_level(s: str, delim: str = ',', openers: str = '<([{', closers: str = '>)]}') -> List[str]: + """Split string at top-level delimiter (not inside brackets).""" + parts, depth, buf = [], 0, '' + i = 0 + while i < len(s): + c = s[i] + if c in openers: depth += 1 + elif c in closers: depth -= 1 + if s[i:i+len(delim)] == delim and depth == 0: + parts.append(buf); buf = ''; i += len(delim); continue + buf += c; i += 1 + parts.append(buf) + return parts + + +def _find_matching(s: str, start: int, open_c: str, close_c: str) -> int: + """Return index of the closing char matching open_c at s[start].""" + depth = 0 + for i in range(start, len(s)): + if s[i] == open_c: depth += 1 + elif s[i] == close_c: + depth -= 1 + if depth == 0: return i + return -1 + + +def _wrap_angle_args(line: str, indent: str, col: int) -> str: + """ + Reformat prefixsuffix as + prefix< + A, + B, + C>suffix + where indent is the base indent of the continuation lines. + """ + lt = line.find('<') + if lt == -1: return line + gt = _find_matching(line, lt, '<', '>') + if gt == -1: return line + inner = line[lt+1:gt] + parts = [p.strip() for p in _split_top_level(inner)] + if len(parts) <= 1: return line + cont = indent + ' ' + joined = (',\n' + cont).join(parts) + return line[:lt+1] + '\n' + cont + joined + '\n' + indent + line[gt:] + + +def _wrap_paren_params(line: str, col: int) -> str: + """ + Wrap the LAST top-level (...) parameter list on the line. + Uses 6-space indent for continuation (matches reference style). + """ + # Find first ( that is not inside < > + depth_a, paren_s = 0, -1 + for i, c in enumerate(line): + if c == '<': depth_a += 1 + elif c == '>': depth_a -= 1 + elif c == '(' and depth_a == 0: paren_s = i; break + if paren_s == -1: return line + paren_e = _find_matching(line, paren_s, '(', ')') + if paren_e == -1: return line + inner = line[paren_s+1:paren_e] + parts = [p.strip() for p in _split_top_level(inner)] + if len(parts) <= 1: return line + suffix = line[paren_e+1:] + base = len(line) - len(line.lstrip()) + cont = ' ' * (base + 6) + joined = (',\n' + cont).join(parts) + return line[:paren_s+1] + '\n' + cont + joined + suffix + + +def format_cpp(code: str, col: int = 100) -> str: + """ + Wraps long lines in generated C++ to stay within `col` characters. + + Rules applied in order per line: + 1. struct Foo final : Base { -> split inheritance + wrap <> + 2. operator()(...) with long params -> wrap param list + 3. constructor / method with long params -> wrap param list + 4. Any line still over col with <> -> wrap angle args + """ + out = [] + for raw in code.splitlines(): + # NOLINT tag: emit as-is (strip the tag first) + if 'NOLINT' in raw: + out.append(raw.replace(' // NOLINT', '')); continue + if len(raw) <= col: + out.append(raw); continue + + line = raw + base_indent = ' ' * (len(line) - len(line.lstrip())) + + # ── Rule 1: struct Foo final : Base { ──────────────────── + m = re.match(r'^(struct \S+ final)\s*:\s*(.+)$', line) + if m: + struct_kw = m.group(1) + inheritance = m.group(2).rstrip() + cont = ' ' # 4-space indent before ':' + inh_line = cont + ': ' + inheritance + # Wrap angle args inside the inheritance line if needed + if len(inh_line) > col and '<' in inh_line: + lt = inh_line.find('<') + gt = _find_matching(inh_line, lt, '<', '>') + if gt != -1: + inner = inh_line[lt+1:gt] + # The sig looks like: RetType(param1, param2, ...)[, BaseClass_t] + # Split at top level to get [sig_part, BaseClass_t?] + top_parts = [p.strip() for p in _split_top_level(inner)] + # Expand the first part (the function signature) at its '(' + sig_part = top_parts[0] + paren_s = sig_part.find('(') + if paren_s != -1: + paren_e = _find_matching(sig_part, paren_s, '(', ')') + if paren_e != -1: + sig_inner = sig_part[paren_s+1:paren_e] + sig_params = [p.strip() for p in _split_top_level(sig_inner)] + if len(sig_params) > 1: + inner_indent = cont + ' ' # deep indent + sig_joined = (',\n' + inner_indent).join(sig_params) + sig_part = sig_part[:paren_s+1] + '\n' + inner_indent + sig_joined + sig_part[paren_e:] + top_parts[0] = sig_part + outer_indent = cont + ' ' + if len(top_parts) > 1: + joined = (',\n' + outer_indent).join(top_parts) + else: + joined = top_parts[0] + inh_line = inh_line[:lt+1] + '\n' + outer_indent + joined + inh_line[gt:] + for sub in (struct_kw + '\n' + inh_line).splitlines(): + out.append(sub) + continue + + # ── Rule 2: operator()(params) ──────────────────────────────── + # operator()() has two () pairs; we want the SECOND (the params) + if 'operator()' in line: + # Find the paren AFTER 'operator()' + op_pos = line.index('operator()') + search_from = op_pos + len('operator()') + paren_s = -1 + depth_a = 0 + for i in range(search_from, len(line)): + c = line[i] + if c == '<': depth_a += 1 + elif c == '>': depth_a -= 1 + elif c == '(' and depth_a == 0: paren_s = i; break + if paren_s != -1: + paren_e = _find_matching(line, paren_s, '(', ')') + if paren_e != -1: + inner = line[paren_s+1:paren_e] + parts = [p.strip() for p in _split_top_level(inner)] + if len(parts) > 1: + bi = len(line) - len(line.lstrip()) + cont = ' ' * (bi + 6) + joined = (',\n' + cont).join(parts) + line = line[:paren_s+1] + '\n' + cont + joined + line[paren_e:] + for sub in line.splitlines(): + out.append(sub) + continue + + # ── Rule 3: constructor / method param list ──────────────────── + is_fn = bool(re.match(r'^\s+\w[\w:<>*& ]+\(', line)) and '(' in line + if is_fn: + wrapped = _wrap_paren_params(line, col) + if wrapped != line: + for sub in wrapped.splitlines(): + out.append(sub) + continue + + # ── Rule 4: generic angle-bracket wrapping ───────────────────── + if '<' in line: + wrapped = _wrap_angle_args(line, base_indent, col) + if wrapped != line: + for sub in wrapped.splitlines(): + out.append(sub) + continue + + out.append(line) + return '\n'.join(out) + + +# ---------------------------------------------------------------------- +# CLI +# ---------------------------------------------------------------------- +def main(): + parser = argparse.ArgumentParser( + description='Generate Gaudi Functional C++ algorithm boilerplate.', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog='Functional types:\n' + '\n'.join( + f' {k}: {v["desc"]}' for k, v in FUNCTIONAL_TYPES.items() + ) + ) + + parser.add_argument('class_name', help='Name of the C++ class to generate') + parser.add_argument( + 'functional_type', + choices=list(FUNCTIONAL_TYPES.keys()), + help='Functional algorithm type' + ) + parser.add_argument( + '-i', '--inputs', + nargs='*', default=[], + metavar='TYPE:KEY', + help='Input collection specs, e.g. edm4hep::MCParticleCollection:mcParticles' + ) + parser.add_argument( + '-o', '--outputs', + nargs='*', default=[], + metavar='TYPE:KEY', + help='Fixed output collection specs, e.g. edm4hep::MCParticleCollection:MCParticles' + ) + parser.add_argument( + '--runtime-outputs', + dest='runtime_outputs', + default=None, + metavar='TYPE', + help=( + 'Enable runtime (dynamic) output collections returning std::vector. ' + 'e.g. --runtime-outputs edm4hep::MCParticleCollection. ' + 'Mutually exclusive with --outputs.' + ) + ) + parser.add_argument( + '-p', '--properties', + nargs='*', default=[], + metavar='NAME:TYPE:DEFAULT[:DESC]', + help='Properties, e.g. threshold:float:0.5 or ExampleInt:int:3:My description' + ) + parser.add_argument( + '--namespace', default='', + help='Optional C++ namespace to wrap the class in' + ) + parser.add_argument( + '--framework', + choices=['gaudi', 'k4fwcore'], + default='k4fwcore', + help='Target framework (default: k4fwcore)' + ) + parser.add_argument( + '--use-class', + action='store_true', default=False, + help='Use "class" keyword instead of "struct"' + ) + parser.add_argument( + '--output-file', + dest='output_file', default=None, + help='Override the output .cpp filename (default: .cpp)' + ) + parser.add_argument( + '--type-aliases', + dest='type_aliases', action='store_true', default=False, + help=( + 'Emit "using AliasColl = FullType;" aliases before the struct and use ' + 'them in the template signature and operator() parameters. ' + 'Alias names are derived automatically: ' + 'edm4hep::MCParticleCollection -> ParticleColl, ' + 'podio::UserDataCollection -> FloatColl.' + ) + ) + parser.add_argument( + '--private-properties', + dest='private_properties', action='store_true', default=False, + help='Place Gaudi::Property members under a private: access label' + ) + parser.add_argument( + '--all-keyvalues', + dest='all_keyvalues', action='store_true', default=False, + help=( + 'Use KeyValues(name, {"default"}) for ALL inputs instead of KeyValue(name, "default"). ' + 'Applies to any input not already covered by --runtime-inputs or --keyvalues-inputs.' + ) + ) + parser.add_argument( + '--keyvalues-inputs', + dest='keyvalues_inputs', nargs='*', default=None, + metavar='KEY[:Default]', + help=( + 'Declare one or more inputs using KeyValues(..., {"Default"}) instead of ' + 'KeyValue(..., "Default"). The parameter type in operator() remains const TYPE&. ' + 'KEY must match an --inputs entry. Optionally override the default: KEY:MyDefault. ' + 'Example: --keyvalues-inputs InputCollection:MCParticles' + ) + ) + parser.add_argument( + '--runtime-inputs', + dest='runtime_inputs', nargs='*', default=None, + metavar='TYPE:KEY:Default0[,Default1,...]', + help=( + 'Mark one or more inputs as runtime (dynamic) collections received as ' + 'std::vector&. The KEY must match a --inputs entry. ' + 'Defaults are comma-separated, e.g. ' + 'edm4hep::MCParticleCollection:InputSeveralCollections:MCParticles0,MCParticles1' + ) + ) + parser.add_argument( + '--event-context', + dest='event_context', action='store_true', default=False, + help='Pass EventContext as first argument to operator() (consumer only)' + ) + parser.add_argument( + '--cmake', + action='store_true', default=False, + help='Also generate a CMakeLists.txt alongside the .cpp file' + ) + + args = parser.parse_args() + + # Validation + if args.runtime_outputs and args.outputs: + parser.error('--runtime-outputs and --outputs are mutually exclusive.') + if args.runtime_outputs and args.framework != 'k4fwcore': + parser.error('--runtime-outputs is only supported with --framework k4fwcore.') + + gen = GaudiGen(args) + code = gen.generate_code() + + code = format_cpp(code) + cpp_file = args.output_file if args.output_file else f'{args.class_name}.cpp' + with open(cpp_file, 'w') as f: + f.write(code) + print(f'Written to {cpp_file}', file=sys.stderr) + + if args.cmake: + cmake_file = 'CMakeLists.txt' + with open(cmake_file, 'w') as f: + f.write(gen.generate_cmake()) + print(f'Written to {cmake_file}', file=sys.stderr) + + +if __name__ == '__main__': + main()#!/usr/bin/env python3 +""" +Gaudi Functional C++ Class Generator A user-friendly script to generate Gaudi Functional C++ classes with proper structure and boilerplate code. Supports both Gaudi::Functional and k4FWCore variants. From 723d5b44c208b7cbbeb56c74d57766e7a019bac4 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Tue, 31 Mar 2026 09:24:51 +0200 Subject: [PATCH 07/36] cleanup --- k4FWCore/helpers/gaudi_gen.py | 1036 ++------------------------------- 1 file changed, 36 insertions(+), 1000 deletions(-) diff --git a/k4FWCore/helpers/gaudi_gen.py b/k4FWCore/helpers/gaudi_gen.py index bda4c123a..20dd37343 100644 --- a/k4FWCore/helpers/gaudi_gen.py +++ b/k4FWCore/helpers/gaudi_gen.py @@ -24,10 +24,6 @@ def __init__(self, args): self.command_line = ' '.join(shlex.quote(arg) for arg in sys.argv) def parse_data_spec(self, spec: str) -> Tuple[str, str]: - """ - Splits TYPE:key at the last separator colon outside angle brackets. - Skips C++ namespace '::' tokens so edm4hep::Foo:key splits correctly. - """ depth, colon_pos = 0, -1 for i, char in enumerate(spec): if char == '<': @@ -42,14 +38,6 @@ def parse_data_spec(self, spec: str) -> Tuple[str, str]: return (spec, '') if colon_pos == -1 else (spec[:colon_pos], spec[colon_pos + 1:]) def parse_runtime_input_spec(self, spec: str): - """ - Parses a --runtime-inputs spec of the form: - TYPE:KEY:Default0[,Default1,...] - Returns (type_str, key_str, [default, ...]) - Uses the FIRST separator colon (not inside <>) as the TYPE/KEY boundary, - and the second separator colon as the KEY/defaults boundary. - """ - # Find all separator colons (not inside <>, not part of ::) depth, sep_positions = 0, [] for i, ch in enumerate(spec): if ch == '<': depth += 1 @@ -65,7 +53,6 @@ def parse_runtime_input_spec(self, spec: str): p = sep_positions[0] typ, key = spec[:p], spec[p+1:] return typ, key, [key] - # Two or more: first split = TYPE, second split = KEY, rest = defaults p0, p1 = sep_positions[0], sep_positions[1] typ = spec[:p0] key = spec[p0+1:p1] @@ -73,73 +60,39 @@ def parse_runtime_input_spec(self, spec: str): return typ, key, defaults def _default_key(self, typ: str) -> str: - """e.g. edm4hep::MCParticleCollection -> MCParticles""" base = typ.split('::')[-1] base = re.sub(r'Collection$', '', base) return base + 's' - # ------------------------------------------------------------------ - # Mode helpers - # ------------------------------------------------------------------ def _is_k4(self) -> bool: return self.args.framework == 'k4fwcore' def _base_class_alias(self) -> str: - """Emits the BaseClass_t alias for native Gaudi Functional.""" return "using BaseClass_t = Gaudi::Functional::Traits::BaseClass_t;" def _use_ret_type_alias(self) -> bool: - """True for k4fwcore with multiple *fixed* outputs (not runtime).""" return self._is_k4() and not self.args.runtime_outputs and len(self.args.outputs) > 1 def _is_runtime(self) -> bool: - """True when --runtime-outputs is set (dynamic vector return).""" return bool(self.args.runtime_outputs) - # ------------------------------------------------------------------ - # Type aliases - # ------------------------------------------------------------------ def _collect_type_aliases(self) -> List[Tuple[str, str]]: - """ - Returns an ordered list of (alias, full_type) for each unique input type. - - Alias strategy (mirrors k4FWCore reference examples): - podio::UserDataCollection -> FloatColl - edm4hep::MCParticleCollection -> ParticleColl - edm4hep::SimTrackerHitCollection -> SimTrackerHitColl - edm4hep::TrackerHit3DCollection -> TrackerHitColl - edm4hep::TrackCollection -> TrackColl - edm4hep::ReconstructedParticleCollection -> RecoColl - edm4hep::RecoMCParticleLinkCollection -> LinkColl - - General rule: - 1. Template types (UserDataCollection): use T.capitalize() + Coll - 2. Names ending in 'LinkCollection': strip to stem before 'Link' + LinkColl -> LinkColl - 3. Names ending in 'HitCollection' : strip 'Collection', keep full stem + Coll - 4. Names ending in 'Collection': strip 'Collection' + last CamelCase word + Coll - Duplicates get a numeric suffix. - """ seen_types: dict = {} aliases: List[Tuple[str, str]] = [] for spec in self.args.inputs: typ, _ = self.parse_data_spec(spec) if typ in seen_types: continue - # Template types: podio::UserDataCollection -> FloatColl inner = re.search(r'<([^>]+)>', typ) if inner: base = inner.group(1).strip().split('::')[-1].capitalize() else: - last = typ.split('::')[-1] # e.g. SimTrackerHitCollection - stem = re.sub(r'Collection$', '', last) # e.g. SimTrackerHit - # LinkCollection -> LinkColl + last = typ.split('::')[-1] + stem = re.sub(r'Collection$', '', last) if stem.endswith('Link'): base = 'Link' - # *HitCollection or *Hit3DCollection -> keep full stem elif re.search(r'Hit', stem): - # TrackerHit3D -> TrackerHit, SimTrackerHit -> SimTrackerHit base = re.sub(r'\d+[A-Z]?$', '', stem) or stem - # Reconstructed* -> Reco, otherwise last CamelCase word elif stem.startswith('Reconstructed'): base = 'Reco' else: @@ -158,68 +111,49 @@ def _collect_type_aliases(self) -> List[Tuple[str, str]]: def _alias_for(self, typ: str, alias_map: dict) -> str: return alias_map.get(typ, typ) - # ------------------------------------------------------------------ - # Includes - # ------------------------------------------------------------------ def get_includes(self) -> str: inc = [] - if self._is_k4(): base = FUNCTIONAL_TYPES[self.args.functional_type]['base'] inc.append(f'#include "k4FWCore/{base}.h"') else: base = FUNCTIONAL_TYPES[self.args.functional_type]['base'] inc.append(f'#include "Gaudi/Functional/{base}.h"') - if self.args.properties: inc.append('#include "Gaudi/Property.h"') - type_inc = set() - # Collect all type specs: fixed outputs, inputs, and runtime output type all_specs = list(self.args.inputs) + list(self.args.outputs) if self._is_runtime(): - all_specs.append(self.args.runtime_outputs) # bare type, no key - + all_specs.append(self.args.runtime_outputs) for spec in all_specs: typ, _ = self.parse_data_spec(spec) if 'podio::UserDataCollection' in typ: type_inc.add('#include "podio/UserDataCollection.h"') if 'edm4hep::' in typ: for match in re.findall(r'edm4hep::(\w+Collection)', typ): - type_inc.add(f'#include "edm4hep/{match}.h"') - + header = re.sub(r'Collection$', '', match) + type_inc.add(f'#include "edm4hep/{header}.h"') inc.extend(sorted(type_inc)) inc.append('#include ') - if self.args.functional_type == 'consumer': - inc.append('#include ') - inc.append('#include ') + # sstream/stdexcept only if consumer body uses them (not added by default) if getattr(self.args, 'event_context', False): inc.extend(['#include ', '#include ', '#include ']) if len(self.args.outputs) > 1: inc.append('#include ') - # runtime mode needs if self._is_runtime() or getattr(self.args, 'runtime_inputs', None): inc.append('#include ') return '\n'.join(inc) - # ------------------------------------------------------------------ - # retType alias (multi-output k4fwcore, fixed collections only) - # ------------------------------------------------------------------ def _ret_type_alias(self) -> str: out_types = [self.parse_data_spec(o)[0] for o in self.args.outputs] - # Align continuation lines under the first type (after 'std::tuple<') prefix = ' std::tuple<' indent = ' ' * len(prefix) joined = (',\n' + indent).join(out_types) return f'using retType =\n{prefix}{joined}>;' - # ------------------------------------------------------------------ - # C++ return type string used in signature and operator() - # ------------------------------------------------------------------ def _cpp_return_type(self) -> str: f_type = self.args.functional_type out_types = [self.parse_data_spec(o)[0] for o in self.args.outputs] - if f_type == 'consumer': return 'void' if f_type == 'filter': @@ -233,9 +167,6 @@ def _cpp_return_type(self) -> str: return out_types[0] return f"std::tuple<{', '.join(out_types)}>" - # ------------------------------------------------------------------ - # Template signature e.g. Out(const In1&, const In2&) - # ------------------------------------------------------------------ def generate_signature(self) -> str: runtime_in_keys = {self.parse_runtime_input_spec(s)[1] for s in getattr(self.args, 'runtime_inputs', []) or []} @@ -258,35 +189,24 @@ def generate_signature(self) -> str: return f"{sig}, BaseClass_t" return sig - # ------------------------------------------------------------------ - # Constructor - # ------------------------------------------------------------------ def _build_constructor_k4fwcore(self) -> str: cls = self.args.class_name base_short = FUNCTIONAL_TYPES[self.args.functional_type]['base'] + if self._is_k4() and self.args.functional_type == 'transformer' and len(self.args.outputs) > 1: + base_short = 'MultiTransformer' f_type = self.args.functional_type - - # ---- inputs block ---- - # runtime_inputs entries use KeyValues(name, {defaults...}), - # plain inputs use KeyValue(name, default). - # Mixed case always uses a brace-list. - # Build mapping key -> defaults list from --runtime-inputs specs - ri_map = {} # key -> [default, ...] + ri_map = {} for s in (getattr(self.args, 'runtime_inputs', []) or []): _, key, defaults = self.parse_runtime_input_spec(s) ri_map[key] = defaults ri_keys = set(ri_map.keys()) - - # Build mapping key -> default from --keyvalues-inputs specs - # These use KeyValues(name, {default}) but operator() type is still const TYPE& - kvi_map = {} # key -> default string + kvi_map = {} for s in (getattr(self.args, 'keyvalues_inputs', []) or []): parts = s.split(':', 1) k = parts[0] val = parts[1] if len(parts) > 1 else k kvi_map[k] = val kvi_keys = set(kvi_map.keys()) - if not self.args.inputs: in_block = '{}' else: @@ -307,47 +227,26 @@ def _build_constructor_k4fwcore(self) -> str: kv_list.append(f'KeyValues("{key}", {{"{key}"}})') else: kv_list.append(f'KeyValue("{key}", "{key}")') - # Single entry (KeyValue or KeyValues) stays bare; multiple use brace-list if len(kv_list) == 1: in_block = kv_list[0] else: indent = ' ' * 20 in_block = '{\n' + indent + (',\n' + indent).join(kv_list) + ',\n' + ' ' * 16 + '}' - - # ---- outputs block ---- if f_type in ('consumer', 'filter'): - # Consumer/filter have only inputs; pass bare KV or brace-list ctor_args = in_block - elif self._is_runtime(): - # Runtime: always KeyValues(...) in a brace-list, even if only one entry - # The property name is e.g. "OutputCollections"; the default value is - # the bare collection name without a trailing 's' added again. typ, key = self.parse_data_spec(self.args.runtime_outputs) key = key if key else self._default_key(typ) out_block = f'{{KeyValues("OutputCollections", {{"{key}"}})}}' ctor_args = f'{in_block}, {out_block}' - elif not self.args.outputs: ctor_args = f'{in_block}, {{}}' - elif len(self.args.outputs) > 1: - # Multiple fixed outputs: brace-list of KeyValues(...) - # Reference style: - # : Producer(name, svcLoc, {}, - # { - # KeyValues("A", {"a"}), - # KeyValues("B", {"b"})}) {} kv_items = [] for spec in self.args.outputs: typ, key = self.parse_data_spec(spec) key = key if key else self._default_key(typ) kv_items.append(f'KeyValues("{key}", {{"{key}"}})') - # Reference layout (17 spaces aligns all KeyValues entries): - # : Producer(name, svcLoc, {}, - # { - # KeyValues("A", {"a"}), - # KeyValues("B", {"b"})}) {} kv_ind = ' ' * 17 joined = (',\n' + kv_ind).join(kv_items) cls_ = self.args.class_name @@ -358,58 +257,41 @@ def _build_constructor_k4fwcore(self) -> str: f' {joined}}})' f' {{}}' ) - else: - # Single fixed output: bare KeyValue(...) typ, key = self.parse_data_spec(self.args.outputs[0]) key = key if key else self._default_key(typ) out_block = f'KeyValue("{key}", "{key}")' ctor_args = f'{in_block}, {out_block}' - return ( f' {cls}(const std::string& name, ISvcLocator* svcLoc)\n' f' : {base_short}(name, svcLoc, {ctor_args}) {{}}' ) def _build_constructor_gaudi(self) -> str: - """ - Native Gaudi Functional constructor. - Single KV -> : Producer(name, svcLoc, KeyValue{"Name", "Default"}) - Multiple -> : Producer(name, svcLoc, {KeyValue{"A","a"}, KeyValue{"B","b"}}) - """ cls = self.args.class_name base_short = FUNCTIONAL_TYPES[self.args.functional_type]['base'] - f_type = self.args.functional_type - def kv(spec: str) -> str: typ, key = self.parse_data_spec(spec) key = key if key else self._default_key(typ) return f'KeyValue{{"{ key }", "{ key }"}}' - in_kvs = [kv(s) for s in self.args.inputs] out_kvs = [kv(s) for s in self.args.outputs] all_kvs = in_kvs + out_kvs - if not all_kvs: ctor_args = '' elif len(all_kvs) == 1: ctor_args = all_kvs[0] else: ctor_args = '{' + ', '.join(all_kvs) + '}' - sep = ', ' if ctor_args else '' return ( f' {cls}(const std::string& name, ISvcLocator* svcLoc)\n' f' : {base_short}(name, svcLoc{sep}{ctor_args}) {{}}' ) - # ------------------------------------------------------------------ - # operator() signature - # ------------------------------------------------------------------ def generate_op_sig(self) -> str: f_type = self.args.functional_type ret = self._cpp_return_type() - runtime_in_keys = {self.parse_runtime_input_spec(s)[1] for s in getattr(self.args, 'runtime_inputs', []) or []} use_aliases = getattr(self.args, 'type_aliases', False) @@ -425,14 +307,12 @@ def generate_op_sig(self) -> str: params.append(f'const std::vector& {key}') else: params.append(f'const {disp}& {key}') - param_str = ', '.join(params) return f'{ret} operator()({param_str}) const override' def _default_return(self) -> str: f_type = self.args.functional_type out_types = [self.parse_data_spec(o)[0] for o in self.args.outputs] - if f_type == 'consumer': return '' if f_type == 'filter': @@ -442,7 +322,6 @@ def _default_return(self) -> str: return f'return std::vector<{elem}>{{}};' if len(out_types) == 1: return f'return {out_types[0]}{{}};' - # Multi-output: named locals + std::move (mirrors reference) lines = [] for i, t in enumerate(out_types, 1): lines.append(f' auto output{i} = {t}();') @@ -453,11 +332,7 @@ def _default_return(self) -> str: lines.append(f' return std::make_tuple({moves}); // NOLINT') return '\n'.join(lines) - # ------------------------------------------------------------------ - # Runtime operator() body hint - # ------------------------------------------------------------------ def _initialize_hint(self) -> List[str]: - """Emits an initialize() override skeleton when runtime inputs are present.""" ri = getattr(self.args, 'runtime_inputs', None) or [] if not ri: return [] @@ -468,17 +343,11 @@ def _initialize_hint(self) -> List[str]: for i, spec in enumerate(self.args.inputs): typ, key = self.parse_data_spec(spec) key = key if key else self._default_key(typ) - lines += [ - f' // inputLocations({i}) or inputLocations("{key}") -> names of {key}', - ] - lines += [ - " return StatusCode::SUCCESS;", - " }", - ] + lines += [f' // inputLocations({i}) or inputLocations("{key}") -> names of {key}'] + lines += [" return StatusCode::SUCCESS;", " }"] return lines def _consumer_body_hint(self) -> str: - """Skeleton body for Consumer showing Gaudi messaging and iteration idioms.""" lines = [] if getattr(self.args, 'event_context', False): lines.append(' info() << "Event number is " << ctx.evt() << endmsg;') @@ -497,7 +366,6 @@ def _consumer_body_hint(self) -> str: return '\n'.join(lines) def _runtime_body_hint(self) -> str: - """Emits a skeleton body showing outputLocations() usage.""" elem = self.parse_data_spec(self.args.runtime_outputs)[0] return ( f' const auto locs = outputLocations();\n' @@ -510,40 +378,14 @@ def _runtime_body_hint(self) -> str: f' return outputCollections;' ) - # ------------------------------------------------------------------ - # Private members (Gaudi only) - # ------------------------------------------------------------------ - def _build_data_handle_members(self) -> str: - lines = [] - for spec in self.args.inputs: - typ, key = self.parse_data_spec(spec) - key = key if key else self._default_key(typ) - lines.append( - f' DataObjectReadHandle<{typ}> m_input_{key}' - f'{{this, "{key}", "{key}"}};' - ) - for spec in self.args.outputs: - typ, key = self.parse_data_spec(spec) - key = key if key else self._default_key(typ) - lines.append( - f' DataObjectWriteHandle<{typ}> m_output_{key}' - f'{{this, "{key}", "{key}"}};' - ) - return '\n'.join(lines) - - # ------------------------------------------------------------------ - # Properties - # ------------------------------------------------------------------ def _build_properties(self) -> str: lines = [] for prop in self.args.properties: parts = prop.split(':', 3) - # Format: type:name:default:desc ptype = parts[0] pname = parts[1] if len(parts) > 1 else parts[0] pdefault = parts[2] if len(parts) > 2 else '0' pdesc = parts[3] if len(parts) > 3 else f'Example {pname} property' - # Avoid double m_ prefix if the user already included it member = pname if pname.startswith('m_') else f'm_{pname}' lines.append( f' Gaudi::Property<{ptype}> {member}' @@ -551,33 +393,28 @@ def _build_properties(self) -> str: ) return '\n'.join(lines) - # ------------------------------------------------------------------ - # Top-level code generator - # ------------------------------------------------------------------ def generate_code(self) -> str: base_short = FUNCTIONAL_TYPES[self.args.functional_type]['base'] + # k4FWCore uses MultiTransformer when there are multiple outputs + if self._is_k4() and self.args.functional_type == 'transformer' and len(self.args.outputs) > 1: + base_short = 'MultiTransformer' base_full = f"{'k4FWCore' if self._is_k4() else 'Gaudi::Functional'}::{base_short}" - kw = "struct" if not self.args.use_class else "class" access = "public:\n " if self.args.use_class else "" - ctor = ( self._build_constructor_k4fwcore() if self._is_k4() else self._build_constructor_gaudi() ) - op_sig = self.generate_op_sig() ns_open = f'namespace {self.args.namespace} {{\n' if self.args.namespace else '' ns_close = f'\n}} // namespace {self.args.namespace}\n' if self.args.namespace else '\n' - lines = [ "// Generated by Gaudi Functional C++ Class Generator", f"// Command: {self.command_line}", "", self.get_includes(), ] - if not self._is_k4(): lines += ["", self._base_class_alias()] if self._use_ret_type_alias(): @@ -589,7 +426,6 @@ def generate_code(self) -> str: lines.append("// Which type of collections we are reading") for alias, typ in aliases: lines.append(f"using {alias} = {typ};") - lines += [ "", f"{ns_open}{kw} {self.args.class_name} final" @@ -597,17 +433,13 @@ def generate_code(self) -> str: f" {access}// Constructor: KeyValues map to collection names, settable from Python", ctor, ] - - # initialize() skeleton for runtime-input consumers init_lines = self._initialize_hint() if init_lines: lines += init_lines - lines += [ f" // This is the function that will be called to produce the data", f" {op_sig} {{", ] - if self._is_runtime(): lines.append(' ' + self._runtime_body_hint().replace('\n', '\n ')) elif self.args.functional_type == 'consumer': @@ -619,17 +451,12 @@ def generate_code(self) -> str: else: ret_line = f'\n {default_ret}' if default_ret else '' lines.append(f' // TODO: Implement logic{ret_line}') - lines.append(" }") - - # Emit properties: under private: if --private-properties, else public if self.args.properties: if getattr(self.args, 'private_properties', False): lines += ["", "private:", self._build_properties()] else: lines += ["", self._build_properties()] - - # Event-context: add finalize() override and mutable threading members if getattr(self.args, 'event_context', False): lines += [ "", @@ -641,56 +468,42 @@ def generate_code(self) -> str: " mutable std::set m_eventNumbersSeen{};", " mutable std::mutex m_mutex{};", ] - lines += [ f"}};" + ns_close, f"DECLARE_COMPONENT({self.args.class_name})", ] - return '\n'.join(lines) - # ------------------------------------------------------------------ - # CMake generator - # ------------------------------------------------------------------ def generate_cmake(self) -> str: cls = self.args.class_name src = f'{cls}.cpp' - all_specs = list(self.args.inputs) + list(self.args.outputs) if self._is_runtime(): all_specs.append(self.args.runtime_outputs) has_edm4hep = any('edm4hep::' in self.parse_data_spec(s)[0] for s in all_specs) has_podio = any('podio::' in self.parse_data_spec(s)[0] for s in all_specs) - find_pkgs: List[str] = [] link_libs: List[str] = [] - if self._is_k4(): find_pkgs.append('find_package(k4FWCore REQUIRED)') link_libs.append('k4FWCore::k4FWCore') else: find_pkgs.append('find_package(Gaudi REQUIRED)') link_libs += ['Gaudi::GaudiAlgLib', 'Gaudi::GaudiKernel'] - if has_edm4hep: find_pkgs.append('find_package(EDM4HEP REQUIRED)') link_libs.append('EDM4HEP::edm4hep') if has_podio: find_pkgs.append('find_package(podio REQUIRED)') link_libs.append('podio::podio') - find_block = '\n'.join(find_pkgs) link_block = '\n '.join(link_libs) - return ( f"# Generated by Gaudi Gen\n" - f"# Command: {self.command_line}\n" - f"\n" + f"# Command: {self.command_line}\n\n" f"cmake_minimum_required(VERSION 3.15)\n" - f"project({cls}Plugin)\n" - f"\n" - f"{find_block}\n" - f"\n" + f"project({cls}Plugin)\n\n" + f"{find_block}\n\n" f"gaudi_add_module({cls}Plugin\n" f" SOURCES {src}\n" f" LINK\n" @@ -699,193 +512,6 @@ def generate_cmake(self) -> str: ) - -# ====================================================================== -# C++ formatter -# ====================================================================== - -def _split_top_level(s: str, delim: str = ',', openers: str = '<([{', closers: str = '>)]}') -> List[str]: - """Split string at top-level delimiter (not inside brackets).""" - parts, depth, buf = [], 0, '' - i = 0 - while i < len(s): - c = s[i] - if c in openers: depth += 1 - elif c in closers: depth -= 1 - if s[i:i+len(delim)] == delim and depth == 0: - parts.append(buf); buf = ''; i += len(delim); continue - buf += c; i += 1 - parts.append(buf) - return parts - - -def _find_matching(s: str, start: int, open_c: str, close_c: str) -> int: - """Return index of the closing char matching open_c at s[start].""" - depth = 0 - for i in range(start, len(s)): - if s[i] == open_c: depth += 1 - elif s[i] == close_c: - depth -= 1 - if depth == 0: return i - return -1 - - -def _wrap_angle_args(line: str, indent: str, col: int) -> str: - """ - Reformat prefixsuffix as - prefix< - A, - B, - C>suffix - where indent is the base indent of the continuation lines. - """ - lt = line.find('<') - if lt == -1: return line - gt = _find_matching(line, lt, '<', '>') - if gt == -1: return line - inner = line[lt+1:gt] - parts = [p.strip() for p in _split_top_level(inner)] - if len(parts) <= 1: return line - cont = indent + ' ' - joined = (',\n' + cont).join(parts) - return line[:lt+1] + '\n' + cont + joined + '\n' + indent + line[gt:] - - -def _wrap_paren_params(line: str, col: int) -> str: - """ - Wrap the LAST top-level (...) parameter list on the line. - Uses 6-space indent for continuation (matches reference style). - """ - # Find first ( that is not inside < > - depth_a, paren_s = 0, -1 - for i, c in enumerate(line): - if c == '<': depth_a += 1 - elif c == '>': depth_a -= 1 - elif c == '(' and depth_a == 0: paren_s = i; break - if paren_s == -1: return line - paren_e = _find_matching(line, paren_s, '(', ')') - if paren_e == -1: return line - inner = line[paren_s+1:paren_e] - parts = [p.strip() for p in _split_top_level(inner)] - if len(parts) <= 1: return line - suffix = line[paren_e+1:] - base = len(line) - len(line.lstrip()) - cont = ' ' * (base + 6) - joined = (',\n' + cont).join(parts) - return line[:paren_s+1] + '\n' + cont + joined + suffix - - -def format_cpp(code: str, col: int = 100) -> str: - """ - Wraps long lines in generated C++ to stay within `col` characters. - - Rules applied in order per line: - 1. struct Foo final : Base { -> split inheritance + wrap <> - 2. operator()(...) with long params -> wrap param list - 3. constructor / method with long params -> wrap param list - 4. Any line still over col with <> -> wrap angle args - """ - out = [] - for raw in code.splitlines(): - # NOLINT tag: emit as-is (strip the tag first) - if 'NOLINT' in raw: - out.append(raw.replace(' // NOLINT', '')); continue - if len(raw) <= col: - out.append(raw); continue - - line = raw - base_indent = ' ' * (len(line) - len(line.lstrip())) - - # ── Rule 1: struct Foo final : Base { ──────────────────── - m = re.match(r'^(struct \S+ final)\s*:\s*(.+)$', line) - if m: - struct_kw = m.group(1) - inheritance = m.group(2).rstrip() - cont = ' ' # 4-space indent before ':' - inh_line = cont + ': ' + inheritance - # Wrap angle args inside the inheritance line if needed - if len(inh_line) > col and '<' in inh_line: - lt = inh_line.find('<') - gt = _find_matching(inh_line, lt, '<', '>') - if gt != -1: - inner = inh_line[lt+1:gt] - # The sig looks like: RetType(param1, param2, ...)[, BaseClass_t] - # Split at top level to get [sig_part, BaseClass_t?] - top_parts = [p.strip() for p in _split_top_level(inner)] - # Expand the first part (the function signature) at its '(' - sig_part = top_parts[0] - paren_s = sig_part.find('(') - if paren_s != -1: - paren_e = _find_matching(sig_part, paren_s, '(', ')') - if paren_e != -1: - sig_inner = sig_part[paren_s+1:paren_e] - sig_params = [p.strip() for p in _split_top_level(sig_inner)] - if len(sig_params) > 1: - inner_indent = cont + ' ' # deep indent - sig_joined = (',\n' + inner_indent).join(sig_params) - sig_part = sig_part[:paren_s+1] + '\n' + inner_indent + sig_joined + sig_part[paren_e:] - top_parts[0] = sig_part - outer_indent = cont + ' ' - if len(top_parts) > 1: - joined = (',\n' + outer_indent).join(top_parts) - else: - joined = top_parts[0] - inh_line = inh_line[:lt+1] + '\n' + outer_indent + joined + inh_line[gt:] - for sub in (struct_kw + '\n' + inh_line).splitlines(): - out.append(sub) - continue - - # ── Rule 2: operator()(params) ──────────────────────────────── - # operator()() has two () pairs; we want the SECOND (the params) - if 'operator()' in line: - # Find the paren AFTER 'operator()' - op_pos = line.index('operator()') - search_from = op_pos + len('operator()') - paren_s = -1 - depth_a = 0 - for i in range(search_from, len(line)): - c = line[i] - if c == '<': depth_a += 1 - elif c == '>': depth_a -= 1 - elif c == '(' and depth_a == 0: paren_s = i; break - if paren_s != -1: - paren_e = _find_matching(line, paren_s, '(', ')') - if paren_e != -1: - inner = line[paren_s+1:paren_e] - parts = [p.strip() for p in _split_top_level(inner)] - if len(parts) > 1: - bi = len(line) - len(line.lstrip()) - cont = ' ' * (bi + 6) - joined = (',\n' + cont).join(parts) - line = line[:paren_s+1] + '\n' + cont + joined + line[paren_e:] - for sub in line.splitlines(): - out.append(sub) - continue - - # ── Rule 3: constructor / method param list ──────────────────── - is_fn = bool(re.match(r'^\s+\w[\w:<>*& ]+\(', line)) and '(' in line - if is_fn: - wrapped = _wrap_paren_params(line, col) - if wrapped != line: - for sub in wrapped.splitlines(): - out.append(sub) - continue - - # ── Rule 4: generic angle-bracket wrapping ───────────────────── - if '<' in line: - wrapped = _wrap_angle_args(line, base_indent, col) - if wrapped != line: - for sub in wrapped.splitlines(): - out.append(sub) - continue - - out.append(line) - return '\n'.join(out) - - -# ---------------------------------------------------------------------- -# CLI -# ---------------------------------------------------------------------- def main(): parser = argparse.ArgumentParser( description='Generate Gaudi Functional C++ algorithm boilerplate.', @@ -894,136 +520,34 @@ def main(): f' {k}: {v["desc"]}' for k, v in FUNCTIONAL_TYPES.items() ) ) - - parser.add_argument('class_name', help='Name of the C++ class to generate') - parser.add_argument( - 'functional_type', - choices=list(FUNCTIONAL_TYPES.keys()), - help='Functional algorithm type' - ) - parser.add_argument( - '-i', '--inputs', - nargs='*', default=[], - metavar='TYPE:KEY', - help='Input collection specs, e.g. edm4hep::MCParticleCollection:mcParticles' - ) - parser.add_argument( - '-o', '--outputs', - nargs='*', default=[], - metavar='TYPE:KEY', - help='Fixed output collection specs, e.g. edm4hep::MCParticleCollection:MCParticles' - ) - parser.add_argument( - '--runtime-outputs', - dest='runtime_outputs', - default=None, - metavar='TYPE', - help=( - 'Enable runtime (dynamic) output collections returning std::vector. ' - 'e.g. --runtime-outputs edm4hep::MCParticleCollection. ' - 'Mutually exclusive with --outputs.' - ) - ) - parser.add_argument( - '-p', '--properties', - nargs='*', default=[], - metavar='NAME:TYPE:DEFAULT[:DESC]', - help='Properties, e.g. threshold:float:0.5 or ExampleInt:int:3:My description' - ) - parser.add_argument( - '--namespace', default='', - help='Optional C++ namespace to wrap the class in' - ) - parser.add_argument( - '--framework', - choices=['gaudi', 'k4fwcore'], - default='k4fwcore', - help='Target framework (default: k4fwcore)' - ) - parser.add_argument( - '--use-class', - action='store_true', default=False, - help='Use "class" keyword instead of "struct"' - ) - parser.add_argument( - '--output-file', - dest='output_file', default=None, - help='Override the output .cpp filename (default: .cpp)' - ) - parser.add_argument( - '--type-aliases', - dest='type_aliases', action='store_true', default=False, - help=( - 'Emit "using AliasColl = FullType;" aliases before the struct and use ' - 'them in the template signature and operator() parameters. ' - 'Alias names are derived automatically: ' - 'edm4hep::MCParticleCollection -> ParticleColl, ' - 'podio::UserDataCollection -> FloatColl.' - ) - ) - parser.add_argument( - '--private-properties', - dest='private_properties', action='store_true', default=False, - help='Place Gaudi::Property members under a private: access label' - ) - parser.add_argument( - '--all-keyvalues', - dest='all_keyvalues', action='store_true', default=False, - help=( - 'Use KeyValues(name, {"default"}) for ALL inputs instead of KeyValue(name, "default"). ' - 'Applies to any input not already covered by --runtime-inputs or --keyvalues-inputs.' - ) - ) - parser.add_argument( - '--keyvalues-inputs', - dest='keyvalues_inputs', nargs='*', default=None, - metavar='KEY[:Default]', - help=( - 'Declare one or more inputs using KeyValues(..., {"Default"}) instead of ' - 'KeyValue(..., "Default"). The parameter type in operator() remains const TYPE&. ' - 'KEY must match an --inputs entry. Optionally override the default: KEY:MyDefault. ' - 'Example: --keyvalues-inputs InputCollection:MCParticles' - ) - ) - parser.add_argument( - '--runtime-inputs', - dest='runtime_inputs', nargs='*', default=None, - metavar='TYPE:KEY:Default0[,Default1,...]', - help=( - 'Mark one or more inputs as runtime (dynamic) collections received as ' - 'std::vector&. The KEY must match a --inputs entry. ' - 'Defaults are comma-separated, e.g. ' - 'edm4hep::MCParticleCollection:InputSeveralCollections:MCParticles0,MCParticles1' - ) - ) - parser.add_argument( - '--event-context', - dest='event_context', action='store_true', default=False, - help='Pass EventContext as first argument to operator() (consumer only)' - ) - parser.add_argument( - '--cmake', - action='store_true', default=False, - help='Also generate a CMakeLists.txt alongside the .cpp file' - ) - + parser.add_argument('class_name') + parser.add_argument('functional_type', choices=list(FUNCTIONAL_TYPES.keys())) + parser.add_argument('-i', '--inputs', nargs='*', default=[]) + parser.add_argument('-o', '--outputs', nargs='*', default=[]) + parser.add_argument('--runtime-outputs', dest='runtime_outputs', default=None) + parser.add_argument('-p', '--properties', nargs='*', default=[]) + parser.add_argument('-n', '--namespace', default='') + parser.add_argument('--framework', choices=['gaudi', 'k4fwcore'], default='k4fwcore') + parser.add_argument('--use-class', action='store_true', default=False) + parser.add_argument('--output-file', dest='output_file', default=None) + parser.add_argument('--type-aliases', dest='type_aliases', action='store_true', default=False) + parser.add_argument('--private-properties', dest='private_properties', action='store_true', default=False) + parser.add_argument('--all-keyvalues', dest='all_keyvalues', action='store_true', default=False) + parser.add_argument('--keyvalues-inputs', dest='keyvalues_inputs', nargs='*', default=None) + parser.add_argument('--runtime-inputs', dest='runtime_inputs', nargs='*', default=None) + parser.add_argument('--event-context', dest='event_context', action='store_true', default=False) + parser.add_argument('--cmake', action='store_true', default=False) args = parser.parse_args() - - # Validation if args.runtime_outputs and args.outputs: parser.error('--runtime-outputs and --outputs are mutually exclusive.') if args.runtime_outputs and args.framework != 'k4fwcore': parser.error('--runtime-outputs is only supported with --framework k4fwcore.') - gen = GaudiGen(args) code = gen.generate_code() - - code = format_cpp(code) cpp_file = args.output_file if args.output_file else f'{args.class_name}.cpp' with open(cpp_file, 'w') as f: f.write(code) print(f'Written to {cpp_file}', file=sys.stderr) - if args.cmake: cmake_file = 'CMakeLists.txt' with open(cmake_file, 'w') as f: @@ -1032,492 +556,4 @@ def main(): if __name__ == '__main__': - main()#!/usr/bin/env python3 -""" -Gaudi Functional C++ Class Generator - -A user-friendly script to generate Gaudi Functional C++ classes with proper -structure and boilerplate code. Supports both Gaudi::Functional and k4FWCore variants. -""" - -import argparse -import sys -import re -from typing import List, Tuple, Optional - -# Functional type definitions -FUNCTIONAL_TYPES = { - 'consumer': { - 'base': 'Consumer', - 'description': 'One or more inputs, no output', - 'example': 'EventTimeMonitor, ProcStatusAbortMoni' - }, - 'producer': { - 'base': 'Producer', - 'description': 'No input, one or more outputs', - 'example': 'ExampleFunctionalProducerMultiple, file IO, constant data generation' - }, - 'transformer': { - 'base': 'Transformer', - 'description': 'One or more inputs, one or more outputs', - 'example': 'Data transformation algorithms' - }, - 'filter': { - 'base': 'FilterPredicate', - 'description': 'One or more inputs, boolean output', - 'example': 'Event selection, filtering based on criteria' - } -} - - -def parse_arguments(): - """Parse command line arguments.""" - parser = argparse.ArgumentParser( - description='Generate Gaudi/k4FWCore Functional C++ classes', - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=_get_functional_types_help() - ) - - parser.add_argument('class_name', help='Name of the C++ class to generate') - parser.add_argument('functional_type', - choices=list(FUNCTIONAL_TYPES.keys()), - help='Type of functional to generate') - parser.add_argument('-i', '--inputs', nargs='*', default=[], - help='Input data specifications (format: "Type:Location" or just "Type")') - parser.add_argument('-o', '--outputs', nargs='*', default=[], - help='Output data specifications (format: "Type:Location" or just "Type")') - parser.add_argument('-n', '--namespace', default='', - help='Namespace for the class') - parser.add_argument('-f', '--output-file', - help='Output file name (default: .cpp)') - parser.add_argument('--framework', choices=['gaudi', 'k4fwcore'], default='k4fwcore', - help='Target framework (default: k4fwcore)') - parser.add_argument('--class', dest='use_class', action='store_true', - help='Generate as class instead of struct (struct is default for k4fwcore)') - parser.add_argument('-p', '--properties', nargs='*', default=[], - help='Gaudi properties (format: "Type:Name:Default:Description")') - - return parser.parse_args() - - -def _get_functional_types_help(): - """Generate help text for functional types.""" - help_text = "\nAvailable Functional Types:\n" - for key, info in FUNCTIONAL_TYPES.items(): - help_text += f"\n {key}:\n" - help_text += f" {info['description']}\n" - help_text += f" Example: {info['example']}\n" - - help_text += "\n\nExample Usage:\n" - help_text += " # k4FWCore producer with multiple outputs (generates struct by default)\n" - help_text += " python gaudi_gen.py MyProducer producer \\\n" - help_text += " -o 'edm4hep::MCParticleCollection:MCParticles' \\\n" - help_text += " 'edm4hep::TrackCollection:Tracks'\n\n" - help_text += " # Gaudi transformer (generates class by default)\n" - help_text += " python gaudi_gen.py MyTransformer transformer \\\n" - help_text += " -i 'InputType:InputLoc' \\\n" - help_text += " -o 'OutputType:OutputLoc' \\\n" - help_text += " --framework gaudi\n" - - return help_text - - -def parse_data_spec(spec: str) -> Tuple[str, str]: - """Parse data specification string into type and location. - Handles types with template parameters like podio::UserDataCollection - """ - # Find the last colon that's not inside angle brackets - depth = 0 - colon_pos = -1 - for i, char in enumerate(spec): - if char == '<': - depth += 1 - elif char == '>': - depth -= 1 - elif char == ':' and depth == 0: - colon_pos = i - - if colon_pos == -1: - return spec, '' - - return spec[:colon_pos], spec[colon_pos+1:] - - -def parse_property_spec(spec: str) -> Tuple[str, str, str, str]: - """Parse property specification into type, name, default, description.""" - parts = spec.split(':', 3) - typ = parts[0] if len(parts) > 0 else 'int' - name = parts[1] if len(parts) > 1 else 'Property' - default = parts[2] if len(parts) > 2 else '0' - desc = parts[3] if len(parts) > 3 else 'Property description' - return typ, name, default, desc - - -def generate_template_signature(functional_type: str, inputs: List[str], outputs: List[str], - framework: str) -> str: - """Generate the template signature for the functional.""" - in_types = [parse_data_spec(i)[0] for i in inputs] - out_types = [parse_data_spec(o)[0] for o in outputs] - - if functional_type == 'consumer': - in_sig = ', '.join([f"const {t}&" for t in in_types]) - return f"void({in_sig})" - elif functional_type == 'producer': - if len(out_types) == 0: - return "void()" - elif len(out_types) == 1: - return f"{out_types[0]}()" - else: - return f"std::tuple<{', '.join(out_types)}>()" - elif functional_type == 'transformer': - in_sig = ', '.join([f"const {t}&" for t in in_types]) if in_types else "" - if len(out_types) == 1: - return f"{out_types[0]}({in_sig})" - else: - out_sig = ', '.join(out_types) - return f"std::tuple<{out_sig}>({in_sig})" - elif functional_type == 'filter': - in_sig = ', '.join([f"const {t}&" for t in in_types]) - return f"bool({in_sig})" - - return "" - - -def generate_keyvalues_k4fwcore(data_specs: List[str], is_input: bool) -> str: - """Generate KeyValues initialization for k4FWCore.""" - if not data_specs: - return "{}" - - lines = [] - for spec in data_specs: - typ, loc = parse_data_spec(spec) - if not loc: - # Generate a default location name from type - # Remove Collection suffix and namespace - clean_name = typ.split('::')[-1].replace('Collection', '') - loc = clean_name - lines.append(f'KeyValues("{loc}", {{"{loc}"}})') - - if len(lines) == 1: - return lines[0] - else: - return "{\n " + ",\n ".join(lines) + "}" - - -def generate_keyvalue_gaudi(data_specs: List[str]) -> str: - """Generate KeyValue initialization for Gaudi.""" - if not data_specs: - return "" - - key_values = [] - for spec in data_specs: - typ, loc = parse_data_spec(spec) - if not loc: - loc = f"{typ.split('::')[-1]}Loc" - default_val = loc - key_values.append(f'KeyValue("{loc}", "{default_val}")') - - if len(key_values) == 1: - return key_values[0] - else: - return "{\n " + ",\n ".join(key_values) + " }" - - -def generate_constructor(class_name: str, functional_type: str, inputs: List[str], - outputs: List[str], framework: str, base_class_short: str) -> str: - """Generate the constructor.""" - if framework == 'k4fwcore': - input_kv = generate_keyvalues_k4fwcore(inputs, True) - output_kv = generate_keyvalues_k4fwcore(outputs, False) - - # For k4FWCore, use the short base class name (just "Producer", not "k4FWCore::Producer") - # because we're already inheriting from the fully qualified name - return f""" {class_name}(const std::string& name, ISvcLocator* svcLoc) - : {base_class_short}(name, svcLoc, {input_kv}, - {output_kv}) {{}}""" - else: # gaudi - input_kv = generate_keyvalue_gaudi(inputs) - output_kv = generate_keyvalue_gaudi(outputs) - - init_parts = [f"\n {base_class_short}(\n name,\n pSvc"] - if input_kv: - init_parts.append(f", {input_kv}") - if output_kv: - init_parts.append(f",\n {output_kv}") - init_parts.append(")") - - constructor_init = ''.join(init_parts) - - return f""" {class_name}(const std::string& name, ISvcLocator* pSvc) - :{constructor_init} {{}}""" - - -def generate_operator_signature(functional_type: str, inputs: List[str], outputs: List[str]) -> str: - """Generate the operator() signature.""" - in_types = [parse_data_spec(i)[0] for i in inputs] - out_types = [parse_data_spec(o)[0] for o in outputs] - - if functional_type == 'consumer': - params = ', '.join([f"const {t}& in{i+1}" for i, t in enumerate(in_types)]) - return f"void operator()({params}) const override" - elif functional_type == 'producer': - if len(out_types) == 0: - return "void operator()() const override" - elif len(out_types) == 1: - return f"{out_types[0]} operator()() const override" - else: - return f"std::tuple<{', '.join(out_types)}> operator()() const override" - elif functional_type == 'transformer': - params = ', '.join([f"const {t}& in{i+1}" for i, t in enumerate(in_types)]) - if len(out_types) == 1: - return f"{out_types[0]} operator()({params}) const override" - else: - return f"std::tuple<{', '.join(out_types)}> operator()({params}) const override" - elif functional_type == 'filter': - params = ', '.join([f"const {t}& in{i+1}" for i, t in enumerate(in_types)]) - return f"bool operator()({params}) const override" - - return "" - - -def generate_operator_body(functional_type: str, outputs: List[str]) -> str: - """Generate a template body for the operator().""" - out_types = [parse_data_spec(o)[0] for o in outputs] - - if functional_type == 'consumer': - return " // Process input data here\n" - elif functional_type == 'producer': - if len(out_types) == 0: - return " // Perform operations here\n" - elif len(out_types) == 1: - return f" // Generate and return output data\n auto output = {out_types[0]}();\n \n return output;\n" - else: - lines = [] - for i, typ in enumerate(out_types): - lines.append(f" auto output{i+1} = {typ}();") - lines.append("\n // TODO: Fill output collections\n") - lines.append(f" return std::make_tuple({', '.join([f'std::move(output{i+1})' for i in range(len(out_types))])});") - return '\n'.join(lines) + '\n' - elif functional_type == 'filter': - return " // Apply filter logic and return true/false\n return true;\n" - elif functional_type == 'transformer': - if len(out_types) == 1: - return f" // Transform input(s) to output\n auto output = {out_types[0]}();\n \n return output;\n" - else: - lines = [] - for i, typ in enumerate(out_types): - lines.append(f" auto output{i+1} = {typ}();") - lines.append("\n // TODO: Fill output collections\n") - lines.append(f" return std::make_tuple({', '.join([f'std::move(output{i+1})' for i in range(len(out_types))])});") - return '\n'.join(lines) + '\n' - - return "" - - -def extract_edm_includes(typ: str) -> List[str]: - """Extract EDM4hep include files from a type string.""" - includes = [] - - # Handle podio types - if 'podio::UserDataCollection' in typ: - includes.append('#include "podio/UserDataCollection.h"') - return includes - - # Handle edm4hep types - if 'edm4hep::' in typ: - # Extract all collection types (handle nested templates) - # Match patterns like edm4hep::MCParticleCollection - pattern = r'edm4hep::(\w+Collection)' - matches = re.findall(pattern, typ) - for match in matches: - base_type = match.replace('Collection', '') - includes.append(f'#include "edm4hep/{base_type}Collection.h"') - - return includes - - -def generate_includes(functional_type: str, inputs: List[str], outputs: List[str], - framework: str, properties: List[str]) -> str: - """Generate include statements.""" - includes = [] - - if framework == 'k4fwcore': - includes.append('#include "k4FWCore/Consumer.h"' if functional_type == 'consumer' else - '#include "k4FWCore/Producer.h"' if functional_type == 'producer' else - '#include "k4FWCore/Transformer.h"' if functional_type == 'transformer' else - '#include "k4FWCore/FilterPredicate.h"') - else: - includes.append('#include "GaudiAlg/Functional.h"') - includes.append('#include "GaudiKernel/KeyValue.h"') - - if properties: - includes.append('#include "Gaudi/Property.h"') - - includes.append('') - - # Collect unique type includes - all_type_strings = [] - for inp in inputs: - typ, _ = parse_data_spec(inp) - all_type_strings.append(typ) - for out in outputs: - typ, _ = parse_data_spec(out) - all_type_strings.append(typ) - - # Generate includes for EDM4hep types - type_includes = [] - for typ in all_type_strings: - type_includes.extend(extract_edm_includes(typ)) - - if type_includes: - includes.extend(sorted(set(type_includes))) - includes.append('') - - includes.append('#include ') - - # Add tuple if multiple outputs - out_types = [parse_data_spec(o)[0] for o in outputs] - if len(out_types) > 1: - includes.append('#include ') - - return '\n'.join(includes) - - -def generate_properties(properties: List[str]) -> str: - """Generate Gaudi property declarations.""" - if not properties: - return "" - - lines = ["\n\nprivate:"] - for prop_spec in properties: - typ, name, default, desc = parse_property_spec(prop_spec) - lines.append(f' Gaudi::Property<{typ}> m_{name}{{this, "{name}", {default}, "{desc}"}};') - - return '\n'.join(lines) - - -def generate_return_type_alias(outputs: List[str]) -> Optional[str]: - """Generate return type alias if needed (for complex return types).""" - out_types = [parse_data_spec(o)[0] for o in outputs] - if len(out_types) > 1: - types_str = ',\n '.join(out_types) - return f"using retType =\n std::tuple<{types_str}>;\n\n" - return None - - -def generate_class(class_name: str, functional_type: str, inputs: List[str], - outputs: List[str], namespace: str, framework: str, - use_class: bool, properties: List[str], command_line: str) -> str: - """Generate the complete C++ class code.""" - base_class_short = FUNCTIONAL_TYPES[functional_type]['base'] - - if framework == 'k4fwcore': - base_class_full = f"k4FWCore::{base_class_short}" - else: - base_class_full = f"Gaudi::Functional::{base_class_short}" - - template_sig = generate_template_signature(functional_type, inputs, outputs, framework) - # Pass just the short name for constructor initialization - constructor = generate_constructor(class_name, functional_type, inputs, outputs, framework, base_class_short) - operator_sig = generate_operator_signature(functional_type, inputs, outputs) - operator_body = generate_operator_body(functional_type, outputs) - includes = generate_includes(functional_type, inputs, outputs, framework, properties) - prop_declarations = generate_properties(properties) - return_type_alias = generate_return_type_alias(outputs) - - # k4FWCore uses struct by default, Gaudi uses class by default - if framework == 'k4fwcore': - class_keyword = "class" if use_class else "struct" - public_keyword = "public:\n" if use_class else "" - else: - class_keyword = "struct" if not use_class else "class" - public_keyword = "" if not use_class else "public:\n" - - code = f"""// Generated by Gaudi Functional C++ Class Generator -// Command: {command_line} -{includes} - -""" - - if namespace: - code += f"namespace {namespace} {{\n\n" - - # Use retType alias if it exists - if return_type_alias: - code += return_type_alias - template_for_class = "retType()" - else: - template_for_class = template_sig - - code += f"""{class_keyword} {class_name} final : {base_class_full}<{template_for_class}> {{ - -{public_keyword}{constructor} - - // This is the function that will be called to produce the data - {operator_sig} {{ -{operator_body} }}{prop_declarations} -}}; - -""" - - if namespace: - code += f"}} // namespace {namespace}\n\n" - - code += f"DECLARE_COMPONENT({class_name})\n" - - return code - - -def main(): - """Main entry point.""" - args = parse_arguments() - - # Validate inputs/outputs based on functional type - if args.functional_type == 'consumer' and not args.inputs: - print("Error: Consumer requires at least one input", file=sys.stderr) - return 1 - elif args.functional_type == 'producer' and not args.outputs: - print("Error: Producer requires at least one output", file=sys.stderr) - return 1 - elif args.functional_type in ['transformer', 'filter'] and not args.inputs: - print(f"Error: {args.functional_type} requires at least one input", file=sys.stderr) - return 1 - elif args.functional_type == 'transformer' and not args.outputs: - print(f"Error: {args.functional_type} requires at least one output", file=sys.stderr) - return 1 - - # Reconstruct the command line for documentation - import shlex - command_line = ' '.join(shlex.quote(arg) for arg in sys.argv) - - # Generate the class - code = generate_class( - args.class_name, - args.functional_type, - args.inputs, - args.outputs, - args.namespace, - args.framework, - args.use_class, - args.properties, - command_line - ) - - # Determine output file - output_file = args.output_file - if not output_file: - output_file = f"{args.class_name}.cpp" - - # Write to file or stdout - if output_file == '-': - print(code) - else: - with open(output_file, 'w') as f: - f.write(code) - print(f"Generated {output_file}") - - return 0 - - -if __name__ == '__main__': - sys.exit(main()) + main() From eae5bc1dbf815d8e72c1d02b3600c94fbc5923bb Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Tue, 31 Mar 2026 09:37:18 +0200 Subject: [PATCH 08/36] address reviewers comments --- k4FWCore/helpers/gaudi_gen.py | 1306 ++++++++++++++++++++------------- 1 file changed, 792 insertions(+), 514 deletions(-) diff --git a/k4FWCore/helpers/gaudi_gen.py b/k4FWCore/helpers/gaudi_gen.py index 20dd37343..0002948c7 100644 --- a/k4FWCore/helpers/gaudi_gen.py +++ b/k4FWCore/helpers/gaudi_gen.py @@ -1,559 +1,837 @@ #!/usr/bin/env python3 """ Gaudi Functional C++ Class Generator + +Generates boilerplate for Gaudi Functional algorithms in both the +k4FWCore and native Gaudi::Functional frameworks. + +Refactoring notes (addressing PR #372 review by tmadlener): + - All CLI strings are parsed up-front into typed dataclasses (DataSpec, + RuntimeInputSpec, PropertySpec, AlgorithmSpec). No re-parsing inside + generator methods. + - The functional type is inferred from the number of inputs / outputs when + not supplied explicitly on the command line. + - All C++ output is produced through Jinja2 templates, keeping Python logic + and string layout cleanly separated. """ +from __future__ import annotations + import argparse -import sys import re import shlex -from typing import List, Tuple - -# Configuration Constants -FUNCTIONAL_TYPES = { - 'consumer': {'base': 'Consumer', 'desc': 'In -> Void'}, - 'producer': {'base': 'Producer', 'desc': 'Void -> Out'}, - 'transformer': {'base': 'Transformer', 'desc': 'In -> Out'}, - 'filter': {'base': 'FilterPredicate', 'desc': 'In -> Bool'} +import sys +from dataclasses import dataclass, field +from typing import List, Optional + +from jinja2 import Environment, StrictUndefined + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +_FRAMEWORK_NS = { + "k4fwcore": "k4FWCore", + "gaudi": "Gaudi::Functional", +} + +_BASE_CLASS = { + "consumer": "Consumer", + "producer": "Producer", + "transformer": "Transformer", + "multitransformer": "MultiTransformer", + "filter": "FilterPredicate", } -class GaudiGen: - def __init__(self, args): - self.args = args - self.command_line = ' '.join(shlex.quote(arg) for arg in sys.argv) +# --------------------------------------------------------------------------- +# Data classes (parsed once at the CLI boundary, never re-parsed) +# --------------------------------------------------------------------------- - def parse_data_spec(self, spec: str) -> Tuple[str, str]: - depth, colon_pos = 0, -1 - for i, char in enumerate(spec): - if char == '<': +@dataclass +class DataSpec: + """One input or output collection: a C++ type and a collection-location key.""" + type_name: str + key: str # collection-location name used in KeyValue / KeyValues + is_vector: bool = False # True for std::vector variable-length inputs + + @staticmethod + def _split_at_separator(spec: str) -> tuple: + """ + Split 'TypeName:LocationKey' at the *last* bare colon (not inside + angle-brackets, not part of a C++ '::' token). + Returns (type_str, key_str); key_str is '' when no separator is found. + """ + depth, last_sep = 0, -1 + for i, ch in enumerate(spec): + if ch == "<": depth += 1 - elif char == '>': + elif ch == ">": depth -= 1 - elif char == ':' and depth == 0: - prev_colon = (i > 0 and spec[i - 1] == ':') - next_colon = (i + 1 < len(spec) and spec[i + 1] == ':') - if not prev_colon and not next_colon: - colon_pos = i - return (spec, '') if colon_pos == -1 else (spec[:colon_pos], spec[colon_pos + 1:]) - - def parse_runtime_input_spec(self, spec: str): - depth, sep_positions = 0, [] + elif ch == ":" and depth == 0: + if not (i > 0 and spec[i - 1] == ":") and \ + not (i + 1 < len(spec) and spec[i + 1] == ":"): + last_sep = i + return (spec, "") if last_sep == -1 else (spec[:last_sep], spec[last_sep + 1:]) + + @staticmethod + def _default_key(type_name: str) -> str: + """edm4hep::MCParticleCollection -> MCParticles""" + base = type_name.split("::")[-1] + base = re.sub(r"<.*>", "", base) # strip template params + base = re.sub(r"Collection$", "", base) + return base + "s" + + @classmethod + def parse(cls, spec: str, is_vector: bool = False) -> "DataSpec": + type_name, key = cls._split_at_separator(spec) + if not key: + key = cls._default_key(type_name) + return cls(type_name=type_name, key=key, is_vector=is_vector) + + # Derived properties used in templates ----------------------------------- + + @property + def edm4hep_header(self) -> Optional[str]: + """Return the edm4hep header filename for this type, or None.""" + m = re.search(r"edm4hep::(\w+Collection)", self.type_name) + if m: + return re.sub(r"Collection$", "", m.group(1)) + ".h" + return None + + @property + def needs_podio_header(self) -> bool: + return "podio::UserDataCollection" in self.type_name + + @property + def cpp_sig_type(self) -> str: + """C++ type as it appears in the template signature.""" + if self.is_vector: + return f"const std::vector&" + return f"const {self.type_name}&" + + +@dataclass +class RuntimeInputSpec: + """An input declared with KeyValues and received as std::vector&.""" + data: DataSpec + defaults: List[str] # initial default location names + + @classmethod + def parse(cls, spec: str) -> "RuntimeInputSpec": + """ + Format: TYPE:KEY:Default0[,Default1,...] + The KEY must match the key of a regular --inputs entry. + """ + depth, seps = 0, [] for i, ch in enumerate(spec): - if ch == '<': depth += 1 - elif ch == '>': depth -= 1 - elif ch == ':' and depth == 0: - prev = (i > 0 and spec[i-1] == ':') - nxt = (i+1 < len(spec) and spec[i+1] == ':') - if not prev and not nxt: - sep_positions.append(i) - if len(sep_positions) == 0: - return spec, self._default_key(spec), [self._default_key(spec)] - if len(sep_positions) == 1: - p = sep_positions[0] - typ, key = spec[:p], spec[p+1:] - return typ, key, [key] - p0, p1 = sep_positions[0], sep_positions[1] - typ = spec[:p0] - key = spec[p0+1:p1] - defaults = [d.strip() for d in spec[p1+1:].split(',')] - return typ, key, defaults - - def _default_key(self, typ: str) -> str: - base = typ.split('::')[-1] - base = re.sub(r'Collection$', '', base) - return base + 's' - - def _is_k4(self) -> bool: - return self.args.framework == 'k4fwcore' - - def _base_class_alias(self) -> str: - return "using BaseClass_t = Gaudi::Functional::Traits::BaseClass_t;" - - def _use_ret_type_alias(self) -> bool: - return self._is_k4() and not self.args.runtime_outputs and len(self.args.outputs) > 1 - - def _is_runtime(self) -> bool: - return bool(self.args.runtime_outputs) - - def _collect_type_aliases(self) -> List[Tuple[str, str]]: - seen_types: dict = {} - aliases: List[Tuple[str, str]] = [] - for spec in self.args.inputs: - typ, _ = self.parse_data_spec(spec) - if typ in seen_types: + if ch == "<": + depth += 1 + elif ch == ">": + depth -= 1 + elif ch == ":" and depth == 0: + if not (i > 0 and spec[i - 1] == ":") and \ + not (i + 1 < len(spec) and spec[i + 1] == ":"): + seps.append(i) + + if not seps: + key = DataSpec._default_key(spec) + return cls(data=DataSpec(spec, key, is_vector=True), defaults=[key]) + if len(seps) == 1: + p = seps[0] + type_name, key = spec[:p], spec[p + 1:] + return cls(data=DataSpec(type_name, key, is_vector=True), defaults=[key]) + + p0, p1 = seps[0], seps[1] + type_name = spec[:p0] + key = spec[p0 + 1:p1] + defaults = [d.strip() for d in spec[p1 + 1:].split(",")] + return cls(data=DataSpec(type_name, key, is_vector=True), defaults=defaults) + + +@dataclass +class PropertySpec: + """A Gaudi::Property member declaration.""" + type_name: str + name: str + default: str + description: str + + @classmethod + def parse(cls, spec: str) -> "PropertySpec": + """Format: type:name:default[:description]""" + parts = spec.split(":", 3) + return cls( + type_name = parts[0], + name = parts[1] if len(parts) > 1 else parts[0], + default = parts[2] if len(parts) > 2 else "0", + description = parts[3] if len(parts) > 3 else "", + ) + + @property + def member_name(self) -> str: + n = self.name + return n if n.startswith("m_") else f"m_{n}" + + +@dataclass +class AlgorithmSpec: + """ + Fully parsed, validated description of the algorithm to generate. + This is the single object threaded through all generator methods. + """ + class_name: str + functional_type: str # consumer | producer | transformer | multitransformer | filter + inputs: List[DataSpec] + outputs: List[DataSpec] + runtime_output: Optional[DataSpec] # set when --runtime-outputs is used + runtime_defaults: dict # key -> [default, ...] from --runtime-inputs + properties: List[PropertySpec] + namespace: Optional[str] + framework: str # k4fwcore | gaudi + use_class: bool + type_aliases: bool + private_props: bool + all_keyvalues: bool + event_context: bool + generate_cmake: bool + output_file: str + command_line: str + + # --- Derived helpers (used by templates) -------------------------------- + + @property + def is_k4(self) -> bool: + return self.framework == "k4fwcore" + + @property + def is_runtime(self) -> bool: + return self.runtime_output is not None + + @property + def framework_ns(self) -> str: + return _FRAMEWORK_NS[self.framework] + + @property + def base_short(self) -> str: + return _BASE_CLASS[self.functional_type] + + @property + def base_full(self) -> str: + return f"{self.framework_ns}::{self.base_short}" + + @property + def use_ret_type_alias(self) -> bool: + return self.is_k4 and not self.is_runtime and len(self.outputs) > 1 + + @property + def cpp_return_type(self) -> str: + if self.functional_type == "consumer": + return "void" + if self.functional_type == "filter": + return "bool" + if self.is_runtime: + return f"std::vector<{self.runtime_output.type_name}>" + if self.use_ret_type_alias: + return "retType" + if len(self.outputs) == 1: + return self.outputs[0].type_name + return "std::tuple<{}>".format(", ".join(o.type_name for o in self.outputs)) + + @property + def template_signature(self) -> str: + """ReturnType(const In1&, const In2&, ...)""" + in_parts = [inp.cpp_sig_type for inp in self.inputs] + if self.event_context: + in_parts = ["const EventContext&"] + in_parts + sig = "{}({})".format(self.cpp_return_type, ", ".join(in_parts)) + if not self.is_k4: + sig += ", BaseClass_t" + return sig + + @property + def type_alias_pairs(self) -> List[tuple]: + """[(alias_name, full_type), ...] for --type-aliases mode.""" + if not self.type_aliases: + return [] + seen: dict = {} + result = [] + used_aliases: set = set() + for inp in self.inputs: + t = inp.type_name + if t in seen: continue - inner = re.search(r'<([^>]+)>', typ) + inner = re.search(r"<([^>]+)>", t) if inner: - base = inner.group(1).strip().split('::')[-1].capitalize() + base = inner.group(1).strip().split("::")[-1].capitalize() else: - last = typ.split('::')[-1] - stem = re.sub(r'Collection$', '', last) - if stem.endswith('Link'): - base = 'Link' - elif re.search(r'Hit', stem): - base = re.sub(r'\d+[A-Z]?$', '', stem) or stem - elif stem.startswith('Reconstructed'): - base = 'Reco' + stem = re.sub(r"Collection$", "", t.split("::")[-1]) + if stem.endswith("Link"): + base = "Link" + elif "Hit" in stem: + base = re.sub(r"\d+[A-Z]?$", "", stem) or stem + elif stem.startswith("Reconstructed"): + base = "Reco" else: - words = re.findall(r'[A-Z][a-z0-9]*', stem) - base = words[-1] if words else stem - alias = base + 'Coll' - existing_aliases = {a for a, _ in aliases} + words = re.findall(r"[A-Z][a-z0-9]*", stem) + base = words[-1] if words else stem + alias = base + "Coll" suffix, candidate = 2, alias - while candidate in existing_aliases: - candidate = alias + str(suffix); suffix += 1 - alias = candidate - seen_types[typ] = alias - aliases.append((alias, typ)) - return aliases - - def _alias_for(self, typ: str, alias_map: dict) -> str: - return alias_map.get(typ, typ) - - def get_includes(self) -> str: - inc = [] - if self._is_k4(): - base = FUNCTIONAL_TYPES[self.args.functional_type]['base'] - inc.append(f'#include "k4FWCore/{base}.h"') - else: - base = FUNCTIONAL_TYPES[self.args.functional_type]['base'] - inc.append(f'#include "Gaudi/Functional/{base}.h"') - if self.args.properties: - inc.append('#include "Gaudi/Property.h"') - type_inc = set() - all_specs = list(self.args.inputs) + list(self.args.outputs) - if self._is_runtime(): - all_specs.append(self.args.runtime_outputs) - for spec in all_specs: - typ, _ = self.parse_data_spec(spec) - if 'podio::UserDataCollection' in typ: - type_inc.add('#include "podio/UserDataCollection.h"') - if 'edm4hep::' in typ: - for match in re.findall(r'edm4hep::(\w+Collection)', typ): - header = re.sub(r'Collection$', '', match) - type_inc.add(f'#include "edm4hep/{header}.h"') - inc.extend(sorted(type_inc)) - inc.append('#include ') - # sstream/stdexcept only if consumer body uses them (not added by default) - if getattr(self.args, 'event_context', False): - inc.extend(['#include ', '#include ', '#include ']) - if len(self.args.outputs) > 1: - inc.append('#include ') - if self._is_runtime() or getattr(self.args, 'runtime_inputs', None): - inc.append('#include ') - return '\n'.join(inc) - - def _ret_type_alias(self) -> str: - out_types = [self.parse_data_spec(o)[0] for o in self.args.outputs] - prefix = ' std::tuple<' - indent = ' ' * len(prefix) - joined = (',\n' + indent).join(out_types) - return f'using retType =\n{prefix}{joined}>;' - - def _cpp_return_type(self) -> str: - f_type = self.args.functional_type - out_types = [self.parse_data_spec(o)[0] for o in self.args.outputs] - if f_type == 'consumer': - return 'void' - if f_type == 'filter': - return 'bool' - if self._is_runtime(): - elem = self.parse_data_spec(self.args.runtime_outputs)[0] - return f'std::vector<{elem}>' - if self._use_ret_type_alias(): - return 'retType' - if len(out_types) == 1: - return out_types[0] - return f"std::tuple<{', '.join(out_types)}>" - - def generate_signature(self) -> str: - runtime_in_keys = {self.parse_runtime_input_spec(s)[1] - for s in getattr(self.args, 'runtime_inputs', []) or []} - use_aliases = getattr(self.args, 'type_aliases', False) - alias_map = {typ: alias for alias, typ in self._collect_type_aliases()} if use_aliases else {} - in_t = [] - for spec in self.args.inputs: - typ, key = self.parse_data_spec(spec) - key = key if key else self._default_key(typ) - disp = self._alias_for(typ, alias_map) - if key in runtime_in_keys: - in_t.append(f'const std::vector&') - else: - in_t.append(f'const {disp}&') - if getattr(self.args, 'event_context', False): - in_t = ['const EventContext&'] + in_t - ret = self._cpp_return_type() - sig = f"{ret}({', '.join(in_t)})" - if not self._is_k4(): - return f"{sig}, BaseClass_t" - return sig + while candidate in used_aliases: + candidate = f"{alias}{suffix}"; suffix += 1 + used_aliases.add(candidate) + seen[t] = candidate + result.append((candidate, t)) + return result - def _build_constructor_k4fwcore(self) -> str: - cls = self.args.class_name - base_short = FUNCTIONAL_TYPES[self.args.functional_type]['base'] - if self._is_k4() and self.args.functional_type == 'transformer' and len(self.args.outputs) > 1: - base_short = 'MultiTransformer' - f_type = self.args.functional_type - ri_map = {} - for s in (getattr(self.args, 'runtime_inputs', []) or []): - _, key, defaults = self.parse_runtime_input_spec(s) - ri_map[key] = defaults - ri_keys = set(ri_map.keys()) - kvi_map = {} - for s in (getattr(self.args, 'keyvalues_inputs', []) or []): - parts = s.split(':', 1) - k = parts[0] - val = parts[1] if len(parts) > 1 else k - kvi_map[k] = val - kvi_keys = set(kvi_map.keys()) - if not self.args.inputs: - in_block = '{}' - else: - kv_list = [] - has_runtime_input = False - for spec in self.args.inputs: - typ, key = self.parse_data_spec(spec) - key = key if key else self._default_key(typ) - if key in ri_keys: - defaults = ri_map.get(key, [key]) - defs_str = ', '.join(f'"{d}"' for d in defaults) - kv_list.append(f'KeyValues("{key}", {{{defs_str}}})') - has_runtime_input = True - elif key in kvi_keys: - default = kvi_map.get(key, key) - kv_list.append(f'KeyValues("{key}", {{"{default}"}})') - elif getattr(self.args, 'all_keyvalues', False): - kv_list.append(f'KeyValues("{key}", {{"{key}"}})') - else: - kv_list.append(f'KeyValue("{key}", "{key}")') - if len(kv_list) == 1: - in_block = kv_list[0] - else: - indent = ' ' * 20 - in_block = '{\n' + indent + (',\n' + indent).join(kv_list) + ',\n' + ' ' * 16 + '}' - if f_type in ('consumer', 'filter'): - ctor_args = in_block - elif self._is_runtime(): - typ, key = self.parse_data_spec(self.args.runtime_outputs) - key = key if key else self._default_key(typ) - out_block = f'{{KeyValues("OutputCollections", {{"{key}"}})}}' - ctor_args = f'{in_block}, {out_block}' - elif not self.args.outputs: - ctor_args = f'{in_block}, {{}}' - elif len(self.args.outputs) > 1: - kv_items = [] - for spec in self.args.outputs: - typ, key = self.parse_data_spec(spec) - key = key if key else self._default_key(typ) - kv_items.append(f'KeyValues("{key}", {{"{key}"}})') - kv_ind = ' ' * 17 - joined = (',\n' + kv_ind).join(kv_items) - cls_ = self.args.class_name - return ( - f' {cls_}(const std::string& name, ISvcLocator* svcLoc)\n' - f' : {base_short}(name, svcLoc, {in_block},\n' - f' {{\n' - f' {joined}}})' - f' {{}}' - ) + def display_type(self, data: DataSpec) -> str: + """Return alias name for a type if --type-aliases is active, else full type.""" + if not self.type_aliases: + return data.type_name + lookup = {t: a for a, t in self.type_alias_pairs} + return lookup.get(data.type_name, data.type_name) + + +# --------------------------------------------------------------------------- +# Parsing helpers +# --------------------------------------------------------------------------- + +def _infer_functional_type( + inputs: List[DataSpec], + outputs: List[DataSpec], + runtime_output: Optional[DataSpec], + explicit: Optional[str], +) -> str: + """ + Determine the functional type. When the caller supplies an explicit value + it is honoured (after auto-promoting transformer -> multitransformer). + Otherwise the type is inferred from the number of inputs and outputs. + """ + n_in = len(inputs) + n_out = len(outputs) + (1 if runtime_output else 0) + + if explicit: + ft = explicit.lower() + # Auto-promote: user wrote 'transformer' but gave multiple outputs + if ft == "transformer" and n_out > 1: + ft = "multitransformer" + return ft + + # Inference table + if n_in > 0 and n_out == 0: + return "consumer" + if n_in == 0 and n_out > 0: + return "producer" + if n_in > 0 and n_out == 1: + return "transformer" + if n_in > 0 and n_out > 1: + return "multitransformer" + raise ValueError( + "Cannot infer functional type: supply at least one --inputs or --outputs." + ) + + +def _build_spec(args: argparse.Namespace) -> AlgorithmSpec: + """ + Convert the raw argparse namespace into a fully validated AlgorithmSpec. + All string parsing happens here and nowhere else. + """ + # --- runtime-inputs: parse first so we know which keys are vector ------- + runtime_input_specs: List[RuntimeInputSpec] = [ + RuntimeInputSpec.parse(s) for s in (args.runtime_inputs or []) + ] + runtime_input_keys = {rs.data.key for rs in runtime_input_specs} + runtime_defaults = {rs.data.key: rs.defaults for rs in runtime_input_specs} + + # --- keyvalues-inputs overrides ----------------------------------------- + kvi_map: dict = {} + for s in (args.keyvalues_inputs or []): + parts = s.split(":", 1) + kvi_map[parts[0]] = parts[1] if len(parts) > 1 else parts[0] + + # --- inputs ------------------------------------------------------------- + inputs: List[DataSpec] = [] + for raw in (args.inputs or []): + ds = DataSpec.parse(raw) + if ds.key in runtime_input_keys: + # Promote to the RuntimeInputSpec's DataSpec (is_vector=True) + rs = next(r for r in runtime_input_specs if r.data.key == ds.key) + inputs.append(rs.data) + elif ds.key in kvi_map or getattr(args, "all_keyvalues", False): + inputs.append(DataSpec(ds.type_name, ds.key, is_vector=True)) else: - typ, key = self.parse_data_spec(self.args.outputs[0]) - key = key if key else self._default_key(typ) - out_block = f'KeyValue("{key}", "{key}")' - ctor_args = f'{in_block}, {out_block}' + inputs.append(ds) + + # --- outputs ------------------------------------------------------------ + outputs: List[DataSpec] = [DataSpec.parse(raw) for raw in (args.outputs or [])] + + # --- runtime output (dynamic vector return) ----------------------------- + runtime_output: Optional[DataSpec] = ( + DataSpec.parse(args.runtime_outputs) if args.runtime_outputs else None + ) + + # --- properties --------------------------------------------------------- + properties = [PropertySpec.parse(p) for p in (args.properties or [])] + + # --- functional type (inferred or explicit) ----------------------------- + functional_type = _infer_functional_type( + inputs, outputs, runtime_output, + explicit=getattr(args, "functional_type", None), + ) + + return AlgorithmSpec( + class_name = args.class_name, + functional_type = functional_type, + inputs = inputs, + outputs = outputs, + runtime_output = runtime_output, + runtime_defaults= runtime_defaults, + properties = properties, + namespace = args.namespace or None, + framework = args.framework, + use_class = args.use_class, + type_aliases = getattr(args, "type_aliases", False), + private_props = getattr(args, "private_properties", False), + all_keyvalues = getattr(args, "all_keyvalues", False), + event_context = getattr(args, "event_context", False), + generate_cmake = getattr(args, "cmake", False), + output_file = args.output_file or f"{args.class_name}.cpp", + command_line = " ".join(shlex.quote(a) for a in sys.argv), + ) + + +# --------------------------------------------------------------------------- +# Jinja2 templates +# --------------------------------------------------------------------------- + +_CPP_TEMPLATE = """\ +// Generated by Gaudi Functional C++ Class Generator +// Command: {{ spec.command_line }} + +{{ includes }} +{% if not spec.is_k4 %} +using BaseClass_t = Gaudi::Functional::Traits::BaseClass_t; +{% endif %} +{% if spec.use_ret_type_alias %} +// Which type of collections we are producing +using retType = std::tuple< +{% for out in spec.outputs %} + {{ out.type_name }}{{ "" if loop.last else "," }} +{% endfor %} +>; +{% endif %} +{% if spec.type_alias_pairs %} +// Which type of collections we are reading +{% for alias, typ in spec.type_alias_pairs %} +using {{ alias }} = {{ typ }}; +{% endfor %} +{% endif %} +{% if spec.namespace %} +namespace {{ spec.namespace }} { +{% endif %} +{{ class_kw }} {{ cls }} final : {{ spec.base_full }}<{{ spec.template_signature }}> { +{% if access_kw %}{{ access_kw }}{% endif %} + // Constructor: KeyValues map to collection names, settable from Python +{{ constructor }} +{% if spec.inputs | selectattr('is_vector') | list %} + + StatusCode initialize() override { + // Verify input locations are set from Python before the event loop +{% for inp in spec.inputs %} +{% if inp.is_vector %} + // inputLocations("{{ inp.key }}") -> current list of collection names +{% endif %} +{% endfor %} + return StatusCode::SUCCESS; + } +{% endif %} + + // This is the function that will be called to produce the data + {{ op_signature }} { +{{ op_body }} + } +{% if spec.properties %} +{% if spec.private_props %} +private: +{% endif %} +{% for prop in spec.properties %} + Gaudi::Property<{{ prop.type_name }}> {{ prop.member_name }}{ + this, "{{ prop.name }}", {{ prop.default }}{{ ', "' + prop.description + '"' if prop.description else '' }}}; +{% endfor %} +{% endif %} +{% if spec.event_context %} + + StatusCode finalize() override { + // TODO: finalise event-context state + return StatusCode::SUCCESS; + } + + mutable std::set m_eventNumbersSeen{}; + mutable std::mutex m_mutex{}; +{% endif %} +}; +{% if spec.namespace %} +} // namespace {{ spec.namespace }} +{% endif %} +DECLARE_COMPONENT({{ cls }}) +""" + +_CMAKE_TEMPLATE = """\ +# Generated by Gaudi Functional C++ Class Generator +# Command: {{ spec.command_line }} + +cmake_minimum_required(VERSION 3.15) +project({{ spec.class_name }}Plugin) + +{% for pkg in find_packages %}{{ pkg }} +{% endfor %} +gaudi_add_module({{ spec.class_name }}Plugin + SOURCES {{ spec.class_name }}.cpp + LINK +{% for lib in link_libs %} {{ lib }} +{% endfor %}) +""" + + +# --------------------------------------------------------------------------- +# Code generation (pure Python logic, no string surgery) +# --------------------------------------------------------------------------- + +def _build_includes(spec: AlgorithmSpec) -> str: + lines = [] + + # Framework header — MultiTransformer lives in Transformer.h for k4FWCore + header_base = "Transformer" if spec.functional_type == "multitransformer" and spec.is_k4 \ + else spec.base_short + if spec.is_k4: + lines.append(f'#include "k4FWCore/{header_base}.h"') + else: + lines.append(f'#include "Gaudi/Functional/{header_base}.h"') + + if spec.properties: + lines.append('#include "Gaudi/Property.h"') + + edm_headers: set = set() + podio_needed = False + all_ds = spec.inputs + spec.outputs + ([spec.runtime_output] if spec.runtime_output else []) + for ds in all_ds: + if ds.edm4hep_header: + edm_headers.add(ds.edm4hep_header) + if ds.needs_podio_header: + podio_needed = True + + for h in sorted(edm_headers): + lines.append(f'#include "edm4hep/{h}"') + if podio_needed: + lines.append('#include "podio/UserDataCollection.h"') + + lines.append("#include ") + if spec.event_context: + lines += ["#include ", "#include ", "#include "] + if len(spec.outputs) > 1 and not spec.use_ret_type_alias: + lines.append("#include ") + if spec.is_runtime or any(inp.is_vector for inp in spec.inputs): + lines.append("#include ") + + return "\n".join(lines) + + +def _build_constructor(spec: AlgorithmSpec) -> str: + """Return the full constructor definition (k4FWCore style).""" + cls = spec.class_name + base = spec.base_short + rd = spec.runtime_defaults + + def _kv(ds: DataSpec) -> str: + if ds.is_vector: + defs_str = ", ".join(f'"{d}"' for d in rd.get(ds.key, [ds.key])) + return f'KeyValues("{ds.key}", {{{defs_str}}})' + return f'KeyValue("{ds.key}", "{ds.key}")' + + # Input block + if not spec.inputs: + in_block = "{}" + elif len(spec.inputs) == 1: + in_block = _kv(spec.inputs[0]) + else: + ind = " " * 20 + items = (",\n" + ind).join(_kv(inp) for inp in spec.inputs) + in_block = "{\n" + ind + items + ",\n" + " " * 16 + "}" + + ft = spec.functional_type + + if ft in ("consumer", "filter"): return ( - f' {cls}(const std::string& name, ISvcLocator* svcLoc)\n' - f' : {base_short}(name, svcLoc, {ctor_args}) {{}}' + f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" + f" : {base}(name, svcLoc, {in_block}) {{}}" ) - def _build_constructor_gaudi(self) -> str: - cls = self.args.class_name - base_short = FUNCTIONAL_TYPES[self.args.functional_type]['base'] - def kv(spec: str) -> str: - typ, key = self.parse_data_spec(spec) - key = key if key else self._default_key(typ) - return f'KeyValue{{"{ key }", "{ key }"}}' - in_kvs = [kv(s) for s in self.args.inputs] - out_kvs = [kv(s) for s in self.args.outputs] - all_kvs = in_kvs + out_kvs - if not all_kvs: - ctor_args = '' - elif len(all_kvs) == 1: - ctor_args = all_kvs[0] - else: - ctor_args = '{' + ', '.join(all_kvs) + '}' - sep = ', ' if ctor_args else '' + if spec.is_runtime: + out_key = spec.runtime_output.key + out_block = f'{{KeyValues("OutputCollections", {{"{out_key}"}})}}' return ( - f' {cls}(const std::string& name, ISvcLocator* svcLoc)\n' - f' : {base_short}(name, svcLoc{sep}{ctor_args}) {{}}' + f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" + f" : {base}(name, svcLoc, {in_block}, {out_block}) {{}}" ) - def generate_op_sig(self) -> str: - f_type = self.args.functional_type - ret = self._cpp_return_type() - runtime_in_keys = {self.parse_runtime_input_spec(s)[1] - for s in getattr(self.args, 'runtime_inputs', []) or []} - use_aliases = getattr(self.args, 'type_aliases', False) - alias_map = {typ: alias for alias, typ in self._collect_type_aliases()} if use_aliases else {} - params = [] - if getattr(self.args, 'event_context', False): - params.append('const EventContext& ctx') - for spec in self.args.inputs: - typ, key = self.parse_data_spec(spec) - key = key if key else self._default_key(typ) - disp = self._alias_for(typ, alias_map) - if key in runtime_in_keys: - params.append(f'const std::vector& {key}') - else: - params.append(f'const {disp}& {key}') - param_str = ', '.join(params) - return f'{ret} operator()({param_str}) const override' - - def _default_return(self) -> str: - f_type = self.args.functional_type - out_types = [self.parse_data_spec(o)[0] for o in self.args.outputs] - if f_type == 'consumer': - return '' - if f_type == 'filter': - return 'return false;' - if self._is_runtime(): - elem = self.parse_data_spec(self.args.runtime_outputs)[0] - return f'return std::vector<{elem}>{{}};' - if len(out_types) == 1: - return f'return {out_types[0]}{{}};' - lines = [] - for i, t in enumerate(out_types, 1): - lines.append(f' auto output{i} = {t}();') - lines.append('') - lines.append(' // TODO: Fill output collections') - lines.append('') - moves = ', '.join(f'std::move(output{i})' for i in range(1, len(out_types)+1)) - lines.append(f' return std::make_tuple({moves}); // NOLINT') - return '\n'.join(lines) - - def _initialize_hint(self) -> List[str]: - ri = getattr(self.args, 'runtime_inputs', None) or [] - if not ri: - return [] - lines = [ - " StatusCode initialize() override {", - " // Verify input locations set from Python", - ] - for i, spec in enumerate(self.args.inputs): - typ, key = self.parse_data_spec(spec) - key = key if key else self._default_key(typ) - lines += [f' // inputLocations({i}) or inputLocations("{key}") -> names of {key}'] - lines += [" return StatusCode::SUCCESS;", " }"] - return lines - - def _consumer_body_hint(self) -> str: - lines = [] - if getattr(self.args, 'event_context', False): - lines.append(' info() << "Event number is " << ctx.evt() << endmsg;') - if not self.args.inputs: - lines.append(' // TODO: Implement consumer logic') - return '\n'.join(lines) - for spec in self.args.inputs: - typ, key = self.parse_data_spec(spec) - key = key if key else self._default_key(typ) - lines += [ - f' debug() << "Received {key} with " << {key}.size() << " elements" << endmsg;', - f' for (const auto& elem : {key}) {{', - f' // TODO: process elem', - f' }}', - ] - return '\n'.join(lines) - - def _runtime_body_hint(self) -> str: - elem = self.parse_data_spec(self.args.runtime_outputs)[0] + if not spec.outputs: return ( - f' const auto locs = outputLocations();\n' - f' std::vector<{elem}> outputCollections;\n' - f' for (size_t i = 0; i < locs.size(); ++i) {{\n' - f' auto coll = {elem}();\n' - f' // TODO: fill coll\n' - f' outputCollections.emplace_back(std::move(coll));\n' - f' }}\n' - f' return outputCollections;' + f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" + f" : {base}(name, svcLoc, {in_block}, {{}}) {{}}" ) - def _build_properties(self) -> str: - lines = [] - for prop in self.args.properties: - parts = prop.split(':', 3) - ptype = parts[0] - pname = parts[1] if len(parts) > 1 else parts[0] - pdefault = parts[2] if len(parts) > 2 else '0' - pdesc = parts[3] if len(parts) > 3 else f'Example {pname} property' - member = pname if pname.startswith('m_') else f'm_{pname}' - lines.append( - f' Gaudi::Property<{ptype}> {member}' - f'{{this, "{pname}", {pdefault}, "{pdesc}"}};' - ) - return '\n'.join(lines) - - def generate_code(self) -> str: - base_short = FUNCTIONAL_TYPES[self.args.functional_type]['base'] - # k4FWCore uses MultiTransformer when there are multiple outputs - if self._is_k4() and self.args.functional_type == 'transformer' and len(self.args.outputs) > 1: - base_short = 'MultiTransformer' - base_full = f"{'k4FWCore' if self._is_k4() else 'Gaudi::Functional'}::{base_short}" - kw = "struct" if not self.args.use_class else "class" - access = "public:\n " if self.args.use_class else "" - ctor = ( - self._build_constructor_k4fwcore() - if self._is_k4() - else self._build_constructor_gaudi() + if len(spec.outputs) == 1: + out_block = _kv(spec.outputs[0]) + return ( + f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" + f" : {base}(name, svcLoc, {in_block}, {out_block}) {{}}" ) - op_sig = self.generate_op_sig() - ns_open = f'namespace {self.args.namespace} {{\n' if self.args.namespace else '' - ns_close = f'\n}} // namespace {self.args.namespace}\n' if self.args.namespace else '\n' - lines = [ - "// Generated by Gaudi Functional C++ Class Generator", - f"// Command: {self.command_line}", - "", - self.get_includes(), - ] - if not self._is_k4(): - lines += ["", self._base_class_alias()] - if self._use_ret_type_alias(): - lines += ["", "// Which type of collections we are producing", self._ret_type_alias()] - if getattr(self.args, 'type_aliases', False): - aliases = self._collect_type_aliases() - if aliases: - lines.append("") - lines.append("// Which type of collections we are reading") - for alias, typ in aliases: - lines.append(f"using {alias} = {typ};") - lines += [ - "", - f"{ns_open}{kw} {self.args.class_name} final" - f" : {base_full}<{self.generate_signature()}> {{", - f" {access}// Constructor: KeyValues map to collection names, settable from Python", - ctor, - ] - init_lines = self._initialize_hint() - if init_lines: - lines += init_lines - lines += [ - f" // This is the function that will be called to produce the data", - f" {op_sig} {{", - ] - if self._is_runtime(): - lines.append(' ' + self._runtime_body_hint().replace('\n', '\n ')) - elif self.args.functional_type == 'consumer': - lines.append(self._consumer_body_hint()) + + # Multiple fixed outputs — brace-list of KeyValues + ind2 = " " * 17 + items = (",\n" + ind2).join(_kv(out) for out in spec.outputs) + return ( + f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" + f" : {base}(name, svcLoc, {in_block},\n" + f" {{\n" + f" {items}}}) {{}}" + ) + + +def _build_constructor_gaudi(spec: AlgorithmSpec) -> str: + cls = spec.class_name + base = spec.base_short + all_kvs = [f'KeyValue{{"{ds.key}", "{ds.key}"}}' for ds in spec.inputs + spec.outputs] + if not all_kvs: + sep, args_str = "", "" + elif len(all_kvs) == 1: + sep, args_str = ", ", all_kvs[0] + else: + sep, args_str = ", ", "{" + ", ".join(all_kvs) + "}" + return ( + f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" + f" : {base}(name, svcLoc{sep}{args_str}) {{}}" + ) + + +def _build_op_signature(spec: AlgorithmSpec) -> str: + params = [] + if spec.event_context: + params.append("const EventContext& ctx") + for inp in spec.inputs: + disp = spec.display_type(inp) + if inp.is_vector: + params.append(f"const std::vector& {inp.key}") else: - default_ret = self._default_return() - if '\n' in default_ret: - lines.append(default_ret) - else: - ret_line = f'\n {default_ret}' if default_ret else '' - lines.append(f' // TODO: Implement logic{ret_line}') - lines.append(" }") - if self.args.properties: - if getattr(self.args, 'private_properties', False): - lines += ["", "private:", self._build_properties()] - else: - lines += ["", self._build_properties()] - if getattr(self.args, 'event_context', False): + params.append(f"const {disp}& {inp.key}") + return f"{spec.cpp_return_type} operator()({', '.join(params)}) const override" + + +def _build_op_body(spec: AlgorithmSpec) -> str: + ft = spec.functional_type + + if spec.is_runtime: + elem = spec.runtime_output.type_name + return ( + f" const auto locs = outputLocations();\n" + f" std::vector<{elem}> outputCollections;\n" + f" for (size_t i = 0; i < locs.size(); ++i) {{\n" + f" auto coll = {elem}();\n" + f" // TODO: fill coll\n" + f" outputCollections.emplace_back(std::move(coll));\n" + f" }}\n" + f" return outputCollections;" + ) + + if ft == "consumer": + lines = [] + if spec.event_context: + lines.append(' info() << "Event number is " << ctx.evt() << endmsg;') + for inp in spec.inputs: lines += [ - "", - " StatusCode finalize() override {", - " // TODO: finalise event-context state", - " return StatusCode::SUCCESS;", - " }", - "", - " mutable std::set m_eventNumbersSeen{};", - " mutable std::mutex m_mutex{};", + f' debug() << "Received {inp.key} with " << {inp.key}.size() << " elements" << endmsg;', + f" for (const auto& elem : {inp.key}) {{", + f" // TODO: process elem", + f" }}", ] - lines += [ - f"}};" + ns_close, - f"DECLARE_COMPONENT({self.args.class_name})", - ] - return '\n'.join(lines) - - def generate_cmake(self) -> str: - cls = self.args.class_name - src = f'{cls}.cpp' - all_specs = list(self.args.inputs) + list(self.args.outputs) - if self._is_runtime(): - all_specs.append(self.args.runtime_outputs) - has_edm4hep = any('edm4hep::' in self.parse_data_spec(s)[0] for s in all_specs) - has_podio = any('podio::' in self.parse_data_spec(s)[0] for s in all_specs) - find_pkgs: List[str] = [] - link_libs: List[str] = [] - if self._is_k4(): - find_pkgs.append('find_package(k4FWCore REQUIRED)') - link_libs.append('k4FWCore::k4FWCore') - else: - find_pkgs.append('find_package(Gaudi REQUIRED)') - link_libs += ['Gaudi::GaudiAlgLib', 'Gaudi::GaudiKernel'] - if has_edm4hep: - find_pkgs.append('find_package(EDM4HEP REQUIRED)') - link_libs.append('EDM4HEP::edm4hep') - if has_podio: - find_pkgs.append('find_package(podio REQUIRED)') - link_libs.append('podio::podio') - find_block = '\n'.join(find_pkgs) - link_block = '\n '.join(link_libs) + return "\n".join(lines) if lines else " // TODO: implement" + + if ft == "filter": + return " // TODO: implement filter logic\n return false;" + + if len(spec.outputs) == 1: return ( - f"# Generated by Gaudi Gen\n" - f"# Command: {self.command_line}\n\n" - f"cmake_minimum_required(VERSION 3.15)\n" - f"project({cls}Plugin)\n\n" - f"{find_block}\n\n" - f"gaudi_add_module({cls}Plugin\n" - f" SOURCES {src}\n" - f" LINK\n" - f" {link_block}\n" - f")\n" + f" // TODO: implement\n" + f" return {spec.outputs[0].type_name}{{}};" ) + # Multiple outputs + lines = [] + for i, out in enumerate(spec.outputs, 1): + lines.append(f" auto output{i} = {out.type_name}();") + lines += [ + "", + " // TODO: fill output collections", + "", + ] + moves = ", ".join(f"std::move(output{i})" for i in range(1, len(spec.outputs) + 1)) + lines.append(f" return std::make_tuple({moves}); // NOLINT") + return "\n".join(lines) + + +def _build_cmake_context(spec: AlgorithmSpec) -> dict: + all_ds = spec.inputs + spec.outputs + ([spec.runtime_output] if spec.runtime_output else []) + has_edm4hep = any(ds.edm4hep_header for ds in all_ds) + has_podio = any(ds.needs_podio_header for ds in all_ds) + + find_packages, link_libs = [], [] + if spec.is_k4: + find_packages.append("find_package(k4FWCore REQUIRED)") + link_libs.append("k4FWCore::k4FWCore") + else: + find_packages.append("find_package(Gaudi REQUIRED)") + link_libs += ["Gaudi::GaudiAlgLib", "Gaudi::GaudiKernel"] + if has_edm4hep: + find_packages.append("find_package(EDM4HEP REQUIRED)") + link_libs.append("EDM4HEP::edm4hep") + if has_podio: + find_packages.append("find_package(podio REQUIRED)") + link_libs.append("podio::podio") + return {"find_packages": find_packages, "link_libs": link_libs} + + +# --------------------------------------------------------------------------- +# Top-level generate() +# --------------------------------------------------------------------------- + +def generate(spec: AlgorithmSpec) -> tuple: + """ + Render the C++ source (and optionally CMakeLists.txt) for *spec*. + Returns (cpp_source, cmake_source_or_None). + """ + env = Environment( + trim_blocks=True, + lstrip_blocks=True, + keep_trailing_newline=True, + undefined=StrictUndefined, + ) + + constructor = ( + _build_constructor(spec) if spec.is_k4 + else _build_constructor_gaudi(spec) + ) + + cpp_ctx = { + "spec": spec, + "cls": spec.class_name, + "includes": _build_includes(spec), + "constructor": constructor, + "op_signature": _build_op_signature(spec), + "op_body": _build_op_body(spec), + "class_kw": "struct" if not spec.use_class else "class", + "access_kw": "public:\n" if spec.use_class else "", + } + + cpp_source = env.from_string(_CPP_TEMPLATE).render(**cpp_ctx).lstrip("\n") + + cmake_source = None + if spec.generate_cmake: + cmake_ctx = {"spec": spec, **_build_cmake_context(spec)} + cmake_source = env.from_string(_CMAKE_TEMPLATE).render(**cmake_ctx) -def main(): + return cpp_source, cmake_source + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def _build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( - description='Generate Gaudi Functional C++ algorithm boilerplate.', + prog="gaudi_gen.py", + description="Generate Gaudi Functional C++ algorithm boilerplate.", formatter_class=argparse.RawDescriptionHelpFormatter, - epilog='Functional types:\n' + '\n'.join( - f' {k}: {v["desc"]}' for k, v in FUNCTIONAL_TYPES.items() - ) + epilog="""\ +Functional type is inferred from the number of inputs and outputs: + consumer inputs > 0, outputs == 0 + producer inputs == 0, outputs >= 1 + transformer inputs >= 1, outputs == 1 + multitransformer inputs >= 1, outputs > 1 + filter inputs >= 1, supply --type filter explicitly + +Examples: + # k4FWCore producer (type inferred) + gaudi_gen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' + + # k4FWCore multi-output producer with properties + gaudi_gen.py MyProducer \\ + -o 'edm4hep::MCParticleCollection:MCParticles' \\ + 'edm4hep::TrackCollection:Tracks' \\ + -p 'int:ExampleInt:3:An example integer property' + + # Gaudi transformer with namespace (type inferred) + gaudi_gen.py MySum -i 'Input1:Loc1' 'Input2:Loc2' -o 'Output:OutLoc' \\ + --framework gaudi -n MyNamespace + + # Variable-length inputs (k4FWCore only) + gaudi_gen.py MyVarConsumer \\ + -i 'edm4hep::MCParticleCollection:Inputs' \\ + --runtime-inputs 'edm4hep::MCParticleCollection:Inputs:MCParticles0,MCParticles1' +""", + ) + parser.add_argument("class_name", help="Name of the C++ class to generate") + parser.add_argument( + "functional_type", nargs="?", + choices=["consumer", "producer", "transformer", "filter"], + help="Functional type (inferred from I/O counts when omitted)", + ) + parser.add_argument("-i", "--inputs", nargs="*", default=[], metavar="TYPE:KEY") + parser.add_argument("-o", "--outputs", nargs="*", default=[], metavar="TYPE:KEY") + parser.add_argument( + "--runtime-outputs", dest="runtime_outputs", default=None, metavar="TYPE", + help="Enable dynamic output collections returning std::vector", ) - parser.add_argument('class_name') - parser.add_argument('functional_type', choices=list(FUNCTIONAL_TYPES.keys())) - parser.add_argument('-i', '--inputs', nargs='*', default=[]) - parser.add_argument('-o', '--outputs', nargs='*', default=[]) - parser.add_argument('--runtime-outputs', dest='runtime_outputs', default=None) - parser.add_argument('-p', '--properties', nargs='*', default=[]) - parser.add_argument('-n', '--namespace', default='') - parser.add_argument('--framework', choices=['gaudi', 'k4fwcore'], default='k4fwcore') - parser.add_argument('--use-class', action='store_true', default=False) - parser.add_argument('--output-file', dest='output_file', default=None) - parser.add_argument('--type-aliases', dest='type_aliases', action='store_true', default=False) - parser.add_argument('--private-properties', dest='private_properties', action='store_true', default=False) - parser.add_argument('--all-keyvalues', dest='all_keyvalues', action='store_true', default=False) - parser.add_argument('--keyvalues-inputs', dest='keyvalues_inputs', nargs='*', default=None) - parser.add_argument('--runtime-inputs', dest='runtime_inputs', nargs='*', default=None) - parser.add_argument('--event-context', dest='event_context', action='store_true', default=False) - parser.add_argument('--cmake', action='store_true', default=False) + parser.add_argument("-p", "--properties", nargs="*", default=[], metavar="TYPE:NAME:DEFAULT[:DESC]") + parser.add_argument("-n", "--namespace", default="") + parser.add_argument("--framework", choices=["gaudi", "k4fwcore"], default="k4fwcore") + parser.add_argument("--use-class", dest="use_class", action="store_true", default=False) + parser.add_argument("-f", "--output-file", dest="output_file", default=None) + parser.add_argument("--type-aliases", dest="type_aliases", action="store_true", default=False) + parser.add_argument("--private-properties", dest="private_properties", action="store_true", default=False) + parser.add_argument("--all-keyvalues", dest="all_keyvalues", action="store_true", default=False) + parser.add_argument("--keyvalues-inputs", dest="keyvalues_inputs", nargs="*", default=None) + parser.add_argument("--runtime-inputs", dest="runtime_inputs", nargs="*", default=None) + parser.add_argument("--event-context", dest="event_context", action="store_true", default=False) + parser.add_argument("--cmake", action="store_true", default=False) + return parser + + +def main() -> None: + parser = _build_parser() args = parser.parse_args() + if args.runtime_outputs and args.outputs: - parser.error('--runtime-outputs and --outputs are mutually exclusive.') - if args.runtime_outputs and args.framework != 'k4fwcore': - parser.error('--runtime-outputs is only supported with --framework k4fwcore.') - gen = GaudiGen(args) - code = gen.generate_code() - cpp_file = args.output_file if args.output_file else f'{args.class_name}.cpp' - with open(cpp_file, 'w') as f: - f.write(code) - print(f'Written to {cpp_file}', file=sys.stderr) - if args.cmake: - cmake_file = 'CMakeLists.txt' - with open(cmake_file, 'w') as f: - f.write(gen.generate_cmake()) - print(f'Written to {cmake_file}', file=sys.stderr) - - -if __name__ == '__main__': + parser.error("--runtime-outputs and --outputs are mutually exclusive.") + if args.runtime_outputs and args.framework != "k4fwcore": + parser.error("--runtime-outputs is only supported with --framework k4fwcore.") + + try: + spec = _build_spec(args) + except ValueError as exc: + parser.error(str(exc)) + return + + cpp_source, cmake_source = generate(spec) + + with open(spec.output_file, "w") as fh: + fh.write(cpp_source) + print(f"Written to {spec.output_file}", file=sys.stderr) + + if cmake_source is not None: + with open("CMakeLists.txt", "w") as fh: + fh.write(cmake_source) + print("Written to CMakeLists.txt", file=sys.stderr) + + +if __name__ == "__main__": main() From 0d5a1b28577f1de7c591857ac4c2385aa6ac2bbe Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Wed, 15 Apr 2026 23:05:13 +0200 Subject: [PATCH 09/36] Apply suggestion from @tmadlener Co-authored-by: Thomas Madlener --- k4FWCore/helpers/gaudi_gen.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/k4FWCore/helpers/gaudi_gen.py b/k4FWCore/helpers/gaudi_gen.py index 0002948c7..a1e518df5 100644 --- a/k4FWCore/helpers/gaudi_gen.py +++ b/k4FWCore/helpers/gaudi_gen.py @@ -5,14 +5,6 @@ Generates boilerplate for Gaudi Functional algorithms in both the k4FWCore and native Gaudi::Functional frameworks. -Refactoring notes (addressing PR #372 review by tmadlener): - - All CLI strings are parsed up-front into typed dataclasses (DataSpec, - RuntimeInputSpec, PropertySpec, AlgorithmSpec). No re-parsing inside - generator methods. - - The functional type is inferred from the number of inputs / outputs when - not supplied explicitly on the command line. - - All C++ output is produced through Jinja2 templates, keeping Python logic - and string layout cleanly separated. """ from __future__ import annotations From 65400040ffc2463600e0fa7cdb820d3a8d117a64 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Wed, 15 Apr 2026 23:05:24 +0200 Subject: [PATCH 10/36] Apply suggestion from @tmadlener Co-authored-by: Thomas Madlener --- k4FWCore/helpers/gaudi_gen.py | 1 - 1 file changed, 1 deletion(-) diff --git a/k4FWCore/helpers/gaudi_gen.py b/k4FWCore/helpers/gaudi_gen.py index a1e518df5..27cd3f785 100644 --- a/k4FWCore/helpers/gaudi_gen.py +++ b/k4FWCore/helpers/gaudi_gen.py @@ -7,7 +7,6 @@ """ -from __future__ import annotations import argparse import re From 5df9628b90953cf842f231a2fc8c45070093c466 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Wed, 29 Apr 2026 16:13:21 +0200 Subject: [PATCH 11/36] Add README for gaudi_gen.py script Add documentation for gaudi_gen.py script, including usage, requirements, arguments, and examples. --- k4FWCore/helpers/README.md | 230 +++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 k4FWCore/helpers/README.md diff --git a/k4FWCore/helpers/README.md b/k4FWCore/helpers/README.md new file mode 100644 index 000000000..e192a8ddb --- /dev/null +++ b/k4FWCore/helpers/README.md @@ -0,0 +1,230 @@ +# gaudi_gen.py — Gaudi Functional C++ Class Generator + +`gaudi_gen.py` writes the boilerplate for a Gaudi Functional algorithm: the +`#include`s, the constructor with `KeyValue` / `KeyValues` wiring, the +`operator()` signature, a placeholder body, optional `Gaudi::Property` +members, and (optionally) a matching `CMakeLists.txt`. It supports both the +**k4FWCore** flavour used by Key4hep / FCC and the native +**Gaudi::Functional** flavour. + +The script is opinionated: it parses every CLI argument once, builds a +single `AlgorithmSpec`, and renders Jinja2 templates from it. There is no +in-place string surgery on the output, so the generated code is consistent +across the matrix of options. + +--- + +## Requirements + +- Python 3.9+ +- `Jinja2` (`pip install jinja2`) + +--- + +## Quick start + +```bash +# k4FWCore producer (functional type inferred from --outputs) +python3 gaudi_gen.py MyProducer \ + -o 'edm4hep::MCParticleCollection:MCParticles' +``` + +That writes `MyProducer.cpp` in the current directory. Add `--cmake` to also +emit a `CMakeLists.txt`: + +```bash +python3 gaudi_gen.py MyProducer \ + -o 'edm4hep::MCParticleCollection:MCParticles' \ + --cmake +``` + +--- + +## File-overwrite policy + +`gaudi_gen.py` **never silently overwrites an existing file**. If the target +`.cpp` or `CMakeLists.txt` already exists, the script prints a diagnostic +and exits non-zero: + +``` +Refusing to overwrite existing CMakeLists.txt at 'CMakeLists.txt'. + Re-run with --force (or remove the file) if you really want to replace it. +``` + +Pass `--force` to allow overwriting. The check applies to both the source +file and the CMake file independently, so partial regeneration is fine +(e.g. delete just the `.cpp` and re-run without `--force`). + +--- + +## Arguments + +### Positional + +| Argument | Description | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| `class_name` | Name of the C++ class to generate (e.g. `MyProducer`). | +| `functional_type` | Optional. One of `consumer`, `producer`, `transformer`, `filter`. If omitted, the type is inferred from the number of inputs and outputs. | + +### Inputs / outputs + +| Flag | Format | Notes | +| ------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `-i`, `--inputs` | `TYPE:KEY` (one or more) | If `:KEY` is omitted, a default key is derived from the type name (e.g. `edm4hep::MCParticleCollection` → `MCParticles`). | +| `-o`, `--outputs` | `TYPE:KEY` (one or more) | Multiple outputs trigger `MultiTransformer` and a `std::tuple` return type. Mutually exclusive with `--runtime-outputs`. | +| `--runtime-outputs` | `TYPE` | Dynamic output collections; `operator()` returns `std::vector`. **k4FWCore-only.** | + +### Vector / runtime inputs (k4FWCore) + +| Flag | Format | Notes | +| --------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| `--runtime-inputs` | `TYPE:KEY:DEF1[,DEF2,...]` | Promotes the matching `--inputs` entry to a runtime `KeyValues` vector with the given default location names. | +| `--keyvalues-inputs` | `KEY[:LABEL]` | Per-input override: turn the named `--inputs` KEY(s) into vector inputs while leaving the others scalar. | +| `--all-keyvalues` | flag | Treat every `--inputs` entry as a `KeyValues` vector. | + +### Properties + +| Flag | Format | Notes | +| --------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------ | +| `-p`, `--properties` | `type:name:default[:description]` | Emits `Gaudi::Property m_{this, "", , ""}` for each entry. | +| `--private-properties`| flag | Place `Gaudi::Property` members under a `private:` access label. | + +### Class shape & framework + +| Flag | Notes | +| ---------------- | -------------------------------------------------------------------------------------------------- | +| `-n`, `--namespace` | Wrap the generated class in `namespace { ... }`. | +| `--framework` | `k4fwcore` (default) or `gaudi` (vanilla `Gaudi::Functional`). | +| `--use-class` | Generate `class ... { public: ... }` instead of the default `struct`. | +| `--type-aliases` | Emit `using XxxColl = ...;` aliases for input collection types and use them in the operator signature. | +| `--event-context`| Add `const EventContext&` as the first `operator()` argument and scaffold a `finalize()` override. | + +### Output + +| Flag | Notes | +| ------------------- | -------------------------------------------------------------------------------------- | +| `-f`, `--output-file` | Path for the generated `.cpp`. Default: `.cpp` in the current directory. | +| `--cmake` | Also emit `CMakeLists.txt` next to the source. | +| `--force` | Allow overwriting existing files. Without this flag, the script refuses to clobber. | + +--- + +## Functional-type inference + +When the positional `functional_type` is omitted, the script picks one from +the I/O counts: + +| inputs | outputs | inferred type | +| -----: | ------: | ------------------ | +| > 0 | == 0 | `consumer` | +| == 0 | >= 1 | `producer` | +| >= 1 | == 1 | `transformer` | +| >= 1 | > 1 | `multitransformer` | + +`filter` is never inferred — supply it explicitly. + +If you write `transformer` but pass multiple `--outputs`, the script +auto-promotes to `multitransformer`. + +--- + +## Examples + +### Producer with multiple outputs and a property + +```bash +python3 gaudi_gen.py MyProducer \ + -o 'edm4hep::MCParticleCollection:MCParticles' \ + 'edm4hep::TrackCollection:Tracks' \ + -p 'int:ExampleInt:3:An example integer property' +``` + +The output uses a `retType = std::tuple<...>` alias for readability. + +### Native Gaudi transformer with namespace + +```bash +python3 gaudi_gen.py MySum \ + -i 'Input1:Loc1' 'Input2:Loc2' \ + -o 'Output:OutLoc' \ + --framework gaudi \ + -n MyNamespace +``` + +This emits a `BaseClass_t = Gaudi::Functional::Traits::BaseClass_t` +typedef and uses `Gaudi::Functional::Transformer<...>` as the base. + +### Variable-length / runtime inputs (k4FWCore) + +```bash +python3 gaudi_gen.py MyVarConsumer \ + -i 'edm4hep::MCParticleCollection:Inputs' \ + --runtime-inputs 'edm4hep::MCParticleCollection:Inputs:MCParticles0,MCParticles1' +``` + +`Inputs` is wired as `KeyValues("Inputs", {"MCParticles0", "MCParticles1"})` +and `operator()` receives `const std::vector&`. + +### Dynamic output collections + +```bash +python3 gaudi_gen.py MyDynProducer \ + --runtime-outputs 'edm4hep::MCParticleCollection' +``` + +The constructor wires `KeyValues("OutputCollections", {"MCParticles"})` and +`operator()` returns `std::vector`. + +### Filter + +```bash +python3 gaudi_gen.py MyFilter filter \ + -i 'edm4hep::MCParticleCollection:MCParticles' +``` + +Returns `bool`. + +--- + +## What ends up in the generated `.cpp` + +- A header banner with the exact command line used to generate the file. +- Framework header (`k4FWCore/.h` or `Gaudi/Functional/.h`). + For k4FWCore, `MultiTransformer` is included from `Transformer.h`. +- Auto-detected `edm4hep/.h` headers and `podio/UserDataCollection.h` + when applicable. +- An optional `using BaseClass_t = ...;` for native Gaudi. +- Optional `using retType = std::tuple<...>;` (k4FWCore multi-output). +- Optional `using XxxColl = ...;` aliases (`--type-aliases`). +- The class itself: constructor, `initialize()` (when there are vector + inputs), `operator()`, properties, optional `finalize()` and bookkeeping + members (`--event-context`). +- `DECLARE_COMPONENT()` at the bottom. + +## What ends up in `CMakeLists.txt` + +- `find_package(k4FWCore REQUIRED)` or `find_package(Gaudi REQUIRED)`. +- `find_package(EDM4HEP REQUIRED)` if any collection type is from `edm4hep`. +- `find_package(podio REQUIRED)` if `podio::UserDataCollection` is used. +- `gaudi_add_module(Plugin SOURCES .cpp LINK ...)` + with the matching link libraries. + +--- + +## Exit codes + +| Code | Meaning | +| ---: | -------------------------------------------------------------------- | +| `0` | Generation succeeded. | +| `1` | A target file already existed and `--force` was not supplied. | +| `2` | argparse error (bad flag, mutually exclusive options, etc.). | + +## Common pitfalls + +- **`--runtime-outputs` + `--outputs`** — these are mutually exclusive. +- **`--runtime-outputs` + `--framework gaudi`** — k4FWCore-only. +- **Inferring `filter`** — the script will not infer this; pass `filter` + explicitly as the second positional argument. +- **Type keys** — keys like `MCParticles` are derived from the type name by + stripping `Collection` and adding `s`. Override with the explicit + `TYPE:KEY` form when that doesn't match your project's conventions. From 57e4b1ec090b5a8b24e16ffa51090f659a4d8856 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Wed, 29 Apr 2026 16:18:22 +0200 Subject: [PATCH 12/36] Refactor gaudi_gen.py for improved readability Refactor gaudi_gen.py to address reviewers comments, improve code organization and readability. Changes include removing unused imports, updating function signatures, and enhancing argument help descriptions. --- k4FWCore/helpers/gaudi_gen.py | 217 +++++++++++++++++++++++----------- 1 file changed, 151 insertions(+), 66 deletions(-) diff --git a/k4FWCore/helpers/gaudi_gen.py b/k4FWCore/helpers/gaudi_gen.py index 27cd3f785..a8415b8db 100644 --- a/k4FWCore/helpers/gaudi_gen.py +++ b/k4FWCore/helpers/gaudi_gen.py @@ -1,18 +1,15 @@ #!/usr/bin/env python3 """ Gaudi Functional C++ Class Generator - Generates boilerplate for Gaudi Functional algorithms in both the k4FWCore and native Gaudi::Functional frameworks. - """ - - import argparse +import os import re import shlex import sys -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import List, Optional from jinja2 import Environment, StrictUndefined @@ -20,12 +17,10 @@ # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- - _FRAMEWORK_NS = { "k4fwcore": "k4FWCore", "gaudi": "Gaudi::Functional", } - _BASE_CLASS = { "consumer": "Consumer", "producer": "Producer", @@ -38,7 +33,6 @@ # --------------------------------------------------------------------------- # Data classes (parsed once at the CLI boundary, never re-parsed) # --------------------------------------------------------------------------- - @dataclass class DataSpec: """One input or output collection: a C++ type and a collection-location key.""" @@ -81,7 +75,6 @@ def parse(cls, spec: str, is_vector: bool = False) -> "DataSpec": return cls(type_name=type_name, key=key, is_vector=is_vector) # Derived properties used in templates ----------------------------------- - @property def edm4hep_header(self) -> Optional[str]: """Return the edm4hep header filename for this type, or None.""" @@ -124,7 +117,6 @@ def parse(cls, spec: str) -> "RuntimeInputSpec": if not (i > 0 and spec[i - 1] == ":") and \ not (i + 1 < len(spec) and spec[i + 1] == ":"): seps.append(i) - if not seps: key = DataSpec._default_key(spec) return cls(data=DataSpec(spec, key, is_vector=True), defaults=[key]) @@ -132,7 +124,6 @@ def parse(cls, spec: str) -> "RuntimeInputSpec": p = seps[0] type_name, key = spec[:p], spec[p + 1:] return cls(data=DataSpec(type_name, key, is_vector=True), defaults=[key]) - p0, p1 = seps[0], seps[1] type_name = spec[:p0] key = spec[p0 + 1:p1] @@ -190,7 +181,6 @@ class AlgorithmSpec: command_line: str # --- Derived helpers (used by templates) -------------------------------- - @property def is_k4(self) -> bool: return self.framework == "k4fwcore" @@ -269,7 +259,8 @@ def type_alias_pairs(self) -> List[tuple]: alias = base + "Coll" suffix, candidate = 2, alias while candidate in used_aliases: - candidate = f"{alias}{suffix}"; suffix += 1 + candidate = f"{alias}{suffix}" + suffix += 1 used_aliases.add(candidate) seen[t] = candidate result.append((candidate, t)) @@ -286,7 +277,6 @@ def display_type(self, data: DataSpec) -> str: # --------------------------------------------------------------------------- # Parsing helpers # --------------------------------------------------------------------------- - def _infer_functional_type( inputs: List[DataSpec], outputs: List[DataSpec], @@ -394,12 +384,11 @@ def _build_spec(args: argparse.Namespace) -> AlgorithmSpec: # --------------------------------------------------------------------------- # Jinja2 templates # --------------------------------------------------------------------------- - _CPP_TEMPLATE = """\ // Generated by Gaudi Functional C++ Class Generator // Command: {{ spec.command_line }} - {{ includes }} + {% if not spec.is_k4 %} using BaseClass_t = Gaudi::Functional::Traits::BaseClass_t; {% endif %} @@ -417,6 +406,7 @@ def _build_spec(args: argparse.Namespace) -> AlgorithmSpec: using {{ alias }} = {{ typ }}; {% endfor %} {% endif %} + {% if spec.namespace %} namespace {{ spec.namespace }} { {% endif %} @@ -424,8 +414,8 @@ def _build_spec(args: argparse.Namespace) -> AlgorithmSpec: {% if access_kw %}{{ access_kw }}{% endif %} // Constructor: KeyValues map to collection names, settable from Python {{ constructor }} -{% if spec.inputs | selectattr('is_vector') | list %} +{% if spec.inputs | selectattr('is_vector') | list %} StatusCode initialize() override { // Verify input locations are set from Python before the event loop {% for inp in spec.inputs %} @@ -435,12 +425,13 @@ def _build_spec(args: argparse.Namespace) -> AlgorithmSpec: {% endfor %} return StatusCode::SUCCESS; } -{% endif %} +{% endif %} // This is the function that will be called to produce the data {{ op_signature }} { {{ op_body }} } + {% if spec.properties %} {% if spec.private_props %} private: @@ -451,12 +442,10 @@ def _build_spec(args: argparse.Namespace) -> AlgorithmSpec: {% endfor %} {% endif %} {% if spec.event_context %} - StatusCode finalize() override { // TODO: finalise event-context state return StatusCode::SUCCESS; } - mutable std::set m_eventNumbersSeen{}; mutable std::mutex m_mutex{}; {% endif %} @@ -464,18 +453,19 @@ def _build_spec(args: argparse.Namespace) -> AlgorithmSpec: {% if spec.namespace %} } // namespace {{ spec.namespace }} {% endif %} + DECLARE_COMPONENT({{ cls }}) """ _CMAKE_TEMPLATE = """\ # Generated by Gaudi Functional C++ Class Generator # Command: {{ spec.command_line }} - cmake_minimum_required(VERSION 3.15) project({{ spec.class_name }}Plugin) {% for pkg in find_packages %}{{ pkg }} {% endfor %} + gaudi_add_module({{ spec.class_name }}Plugin SOURCES {{ spec.class_name }}.cpp LINK @@ -487,10 +477,8 @@ def _build_spec(args: argparse.Namespace) -> AlgorithmSpec: # --------------------------------------------------------------------------- # Code generation (pure Python logic, no string surgery) # --------------------------------------------------------------------------- - def _build_includes(spec: AlgorithmSpec) -> str: lines = [] - # Framework header — MultiTransformer lives in Transformer.h for k4FWCore header_base = "Transformer" if spec.functional_type == "multitransformer" and spec.is_k4 \ else spec.base_short @@ -498,7 +486,6 @@ def _build_includes(spec: AlgorithmSpec) -> str: lines.append(f'#include "k4FWCore/{header_base}.h"') else: lines.append(f'#include "Gaudi/Functional/{header_base}.h"') - if spec.properties: lines.append('#include "Gaudi/Property.h"') @@ -510,12 +497,10 @@ def _build_includes(spec: AlgorithmSpec) -> str: edm_headers.add(ds.edm4hep_header) if ds.needs_podio_header: podio_needed = True - for h in sorted(edm_headers): lines.append(f'#include "edm4hep/{h}"') if podio_needed: lines.append('#include "podio/UserDataCollection.h"') - lines.append("#include ") if spec.event_context: lines += ["#include ", "#include ", "#include "] @@ -523,7 +508,6 @@ def _build_includes(spec: AlgorithmSpec) -> str: lines.append("#include ") if spec.is_runtime or any(inp.is_vector for inp in spec.inputs): lines.append("#include ") - return "\n".join(lines) @@ -550,13 +534,11 @@ def _kv(ds: DataSpec) -> str: in_block = "{\n" + ind + items + ",\n" + " " * 16 + "}" ft = spec.functional_type - if ft in ("consumer", "filter"): return ( f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" f" : {base}(name, svcLoc, {in_block}) {{}}" ) - if spec.is_runtime: out_key = spec.runtime_output.key out_block = f'{{KeyValues("OutputCollections", {{"{out_key}"}})}}' @@ -564,20 +546,17 @@ def _kv(ds: DataSpec) -> str: f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" f" : {base}(name, svcLoc, {in_block}, {out_block}) {{}}" ) - if not spec.outputs: return ( f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" f" : {base}(name, svcLoc, {in_block}, {{}}) {{}}" ) - if len(spec.outputs) == 1: out_block = _kv(spec.outputs[0]) return ( f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" f" : {base}(name, svcLoc, {in_block}, {out_block}) {{}}" ) - # Multiple fixed outputs — brace-list of KeyValues ind2 = " " * 17 items = (",\n" + ind2).join(_kv(out) for out in spec.outputs) @@ -620,7 +599,6 @@ def _build_op_signature(spec: AlgorithmSpec) -> str: def _build_op_body(spec: AlgorithmSpec) -> str: ft = spec.functional_type - if spec.is_runtime: elem = spec.runtime_output.type_name return ( @@ -633,7 +611,6 @@ def _build_op_body(spec: AlgorithmSpec) -> str: f" }}\n" f" return outputCollections;" ) - if ft == "consumer": lines = [] if spec.event_context: @@ -646,16 +623,13 @@ def _build_op_body(spec: AlgorithmSpec) -> str: f" }}", ] return "\n".join(lines) if lines else " // TODO: implement" - if ft == "filter": return " // TODO: implement filter logic\n return false;" - if len(spec.outputs) == 1: return ( f" // TODO: implement\n" f" return {spec.outputs[0].type_name}{{}};" ) - # Multiple outputs lines = [] for i, out in enumerate(spec.outputs, 1): @@ -694,7 +668,6 @@ def _build_cmake_context(spec: AlgorithmSpec) -> dict: # --------------------------------------------------------------------------- # Top-level generate() # --------------------------------------------------------------------------- - def generate(spec: AlgorithmSpec) -> tuple: """ Render the C++ source (and optionally CMakeLists.txt) for *spec*. @@ -706,12 +679,10 @@ def generate(spec: AlgorithmSpec) -> tuple: keep_trailing_newline=True, undefined=StrictUndefined, ) - constructor = ( _build_constructor(spec) if spec.is_k4 else _build_constructor_gaudi(spec) ) - cpp_ctx = { "spec": spec, "cls": spec.class_name, @@ -722,7 +693,6 @@ def generate(spec: AlgorithmSpec) -> tuple: "class_kw": "struct" if not spec.use_class else "class", "access_kw": "public:\n" if spec.use_class else "", } - cpp_source = env.from_string(_CPP_TEMPLATE).render(**cpp_ctx).lstrip("\n") cmake_source = None @@ -734,9 +704,29 @@ def generate(spec: AlgorithmSpec) -> tuple: # --------------------------------------------------------------------------- -# CLI +# Safe-write helper # --------------------------------------------------------------------------- +def _safe_write(path: str, content: str, force: bool, label: str) -> bool: + """ + Write *content* to *path*. If the file exists and *force* is False, refuse + to overwrite and tell the user how to override. Returns True on success. + """ + if os.path.exists(path) and not force: + print( + f"Refusing to overwrite existing {label} at {path!r}.\n" + f" Re-run with --force (or remove the file) if you really want to replace it.", + file=sys.stderr, + ) + return False + with open(path, "w") as fh: + fh.write(content) + print(f"Written to {path}", file=sys.stderr) + return True + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- def _build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="gaudi_gen.py", @@ -770,30 +760,126 @@ def _build_parser() -> argparse.ArgumentParser: --runtime-inputs 'edm4hep::MCParticleCollection:Inputs:MCParticles0,MCParticles1' """, ) - parser.add_argument("class_name", help="Name of the C++ class to generate") + parser.add_argument( + "class_name", + help="Name of the C++ class to generate (e.g. MyProducer).", + ) parser.add_argument( "functional_type", nargs="?", choices=["consumer", "producer", "transformer", "filter"], - help="Functional type (inferred from I/O counts when omitted)", + help=( + "Functional type. Omit to infer from --inputs / --outputs counts; " + "supply 'filter' explicitly when a FilterPredicate is wanted." + ), + ) + parser.add_argument( + "-i", "--inputs", nargs="*", default=[], metavar="TYPE:KEY", + help=( + "Input collections, one or more 'TYPE:KEY' specs. " + "Example: 'edm4hep::MCParticleCollection:MCParticles'. " + "If KEY is omitted, a default is derived from the type name." + ), + ) + parser.add_argument( + "-o", "--outputs", nargs="*", default=[], metavar="TYPE:KEY", + help=( + "Output collections, one or more 'TYPE:KEY' specs. " + "Multiple outputs trigger MultiTransformer/tuple return generation. " + "Mutually exclusive with --runtime-outputs." + ), ) - parser.add_argument("-i", "--inputs", nargs="*", default=[], metavar="TYPE:KEY") - parser.add_argument("-o", "--outputs", nargs="*", default=[], metavar="TYPE:KEY") parser.add_argument( "--runtime-outputs", dest="runtime_outputs", default=None, metavar="TYPE", - help="Enable dynamic output collections returning std::vector", + help=( + "Enable dynamic output collections returning std::vector. " + "k4FWCore-only. Mutually exclusive with --outputs." + ), + ) + parser.add_argument( + "-p", "--properties", nargs="*", default=[], + metavar="TYPE:NAME:DEFAULT[:DESC]", + help=( + "Gaudi::Property members to declare. Format " + "'type:name:default[:description]'. Example " + "'int:NumThreads:4:Worker thread count'." + ), + ) + parser.add_argument( + "-n", "--namespace", default="", + help="C++ namespace to wrap the generated class in. Empty = no namespace.", + ) + parser.add_argument( + "--framework", choices=["gaudi", "k4fwcore"], default="k4fwcore", + help=( + "Target framework: 'k4fwcore' (default, for Key4hep / FCC) or " + "'gaudi' (vanilla Gaudi::Functional)." + ), + ) + parser.add_argument( + "--use-class", dest="use_class", action="store_true", default=False, + help="Generate 'class ... { public: ... }' instead of the default 'struct'.", + ) + parser.add_argument( + "-f", "--output-file", dest="output_file", default=None, + help="Path for the generated .cpp file. Default: .cpp in the cwd.", + ) + parser.add_argument( + "--type-aliases", dest="type_aliases", action="store_true", default=False, + help=( + "Emit 'using XxxColl = ...;' aliases for input collection types " + "and use them in the operator() signature." + ), + ) + parser.add_argument( + "--private-properties", dest="private_properties", + action="store_true", default=False, + help="Place Gaudi::Property members under a 'private:' access label.", + ) + parser.add_argument( + "--all-keyvalues", dest="all_keyvalues", + action="store_true", default=False, + help=( + "Treat every --inputs entry as a runtime KeyValues vector " + "(std::vector&) instead of a single KeyValue." + ), + ) + parser.add_argument( + "--keyvalues-inputs", dest="keyvalues_inputs", nargs="*", default=None, + metavar="KEY[:LABEL]", + help=( + "Per-input override: turn the named --inputs KEY(s) into KeyValues " + "vector inputs while leaving the others as scalars." + ), + ) + parser.add_argument( + "--runtime-inputs", dest="runtime_inputs", nargs="*", default=None, + metavar="TYPE:KEY:DEF1[,DEF2,...]", + help=( + "Declare runtime (variable-length) inputs with default location " + "names. Example " + "'edm4hep::MCParticleCollection:Inputs:MCParticles0,MCParticles1'. " + "k4FWCore-only." + ), + ) + parser.add_argument( + "--event-context", dest="event_context", + action="store_true", default=False, + help=( + "Add 'const EventContext&' as the first operator() argument and " + "scaffold a finalize() override with mutable bookkeeping members." + ), + ) + parser.add_argument( + "--cmake", action="store_true", default=False, + help="Also emit a CMakeLists.txt next to the source (cwd).", + ) + parser.add_argument( + "--force", action="store_true", default=False, + help=( + "Allow overwriting existing files. Without this flag, the script " + "refuses to clobber an existing .cpp or CMakeLists.txt." + ), ) - parser.add_argument("-p", "--properties", nargs="*", default=[], metavar="TYPE:NAME:DEFAULT[:DESC]") - parser.add_argument("-n", "--namespace", default="") - parser.add_argument("--framework", choices=["gaudi", "k4fwcore"], default="k4fwcore") - parser.add_argument("--use-class", dest="use_class", action="store_true", default=False) - parser.add_argument("-f", "--output-file", dest="output_file", default=None) - parser.add_argument("--type-aliases", dest="type_aliases", action="store_true", default=False) - parser.add_argument("--private-properties", dest="private_properties", action="store_true", default=False) - parser.add_argument("--all-keyvalues", dest="all_keyvalues", action="store_true", default=False) - parser.add_argument("--keyvalues-inputs", dest="keyvalues_inputs", nargs="*", default=None) - parser.add_argument("--runtime-inputs", dest="runtime_inputs", nargs="*", default=None) - parser.add_argument("--event-context", dest="event_context", action="store_true", default=False) - parser.add_argument("--cmake", action="store_true", default=False) return parser @@ -814,14 +900,13 @@ def main() -> None: cpp_source, cmake_source = generate(spec) - with open(spec.output_file, "w") as fh: - fh.write(cpp_source) - print(f"Written to {spec.output_file}", file=sys.stderr) - + cpp_ok = _safe_write(spec.output_file, cpp_source, args.force, label="C++ source") + cmake_ok = True if cmake_source is not None: - with open("CMakeLists.txt", "w") as fh: - fh.write(cmake_source) - print("Written to CMakeLists.txt", file=sys.stderr) + cmake_ok = _safe_write("CMakeLists.txt", cmake_source, args.force, label="CMakeLists.txt") + + if not (cpp_ok and cmake_ok): + sys.exit(1) if __name__ == "__main__": From 1b893c9866e09a19cad5f5cc4ee93780ccfbbf12 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Wed, 29 Apr 2026 16:24:50 +0200 Subject: [PATCH 13/36] Refactor argument parsing for output options --- k4FWCore/helpers/gaudi_gen.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/k4FWCore/helpers/gaudi_gen.py b/k4FWCore/helpers/gaudi_gen.py index a8415b8db..961f33338 100644 --- a/k4FWCore/helpers/gaudi_gen.py +++ b/k4FWCore/helpers/gaudi_gen.py @@ -780,7 +780,10 @@ def _build_parser() -> argparse.ArgumentParser: "If KEY is omitted, a default is derived from the type name." ), ) - parser.add_argument( + # --outputs and --runtime-outputs are alternative ways to declare outputs; + # let argparse enforce that at parse time. + out_group = parser.add_mutually_exclusive_group() + out_group.add_argument( "-o", "--outputs", nargs="*", default=[], metavar="TYPE:KEY", help=( "Output collections, one or more 'TYPE:KEY' specs. " @@ -788,7 +791,7 @@ def _build_parser() -> argparse.ArgumentParser: "Mutually exclusive with --runtime-outputs." ), ) - parser.add_argument( + out_group.add_argument( "--runtime-outputs", dest="runtime_outputs", default=None, metavar="TYPE", help=( "Enable dynamic output collections returning std::vector. " @@ -887,8 +890,8 @@ def main() -> None: parser = _build_parser() args = parser.parse_args() - if args.runtime_outputs and args.outputs: - parser.error("--runtime-outputs and --outputs are mutually exclusive.") + # --outputs vs --runtime-outputs is enforced by the mutually-exclusive + # group on the parser; only the framework constraint remains here. if args.runtime_outputs and args.framework != "k4fwcore": parser.error("--runtime-outputs is only supported with --framework k4fwcore.") From b14d00c68ab5546d5ca5cbc57fc265a2806100e5 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Wed, 29 Apr 2026 16:30:37 +0200 Subject: [PATCH 14/36] Update comments and help text for namespace option --- k4FWCore/helpers/gaudi_gen.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/k4FWCore/helpers/gaudi_gen.py b/k4FWCore/helpers/gaudi_gen.py index 961f33338..53cc0723e 100644 --- a/k4FWCore/helpers/gaudi_gen.py +++ b/k4FWCore/helpers/gaudi_gen.py @@ -750,7 +750,7 @@ def _build_parser() -> argparse.ArgumentParser: 'edm4hep::TrackCollection:Tracks' \\ -p 'int:ExampleInt:3:An example integer property' - # Gaudi transformer with namespace (type inferred) + # Gaudi transformer wrapped in 'namespace MyNamespace { ... }' (type inferred) gaudi_gen.py MySum -i 'Input1:Loc1' 'Input2:Loc2' -o 'Output:OutLoc' \\ --framework gaudi -n MyNamespace @@ -808,8 +808,14 @@ def _build_parser() -> argparse.ArgumentParser: ), ) parser.add_argument( - "-n", "--namespace", default="", - help="C++ namespace to wrap the generated class in. Empty = no namespace.", + "-n", "--namespace", default="", metavar="NAME", + help=( + "Wrap the generated class in a C++ namespace, i.e. emit " + "'namespace NAME { ... } // namespace NAME' around the class " + "definition. Empty (default) leaves the class at global scope. " + "Note: this is the C++ namespace, not the Gaudi/k4FWCore framework " + "namespace and not the runtime algorithm instance name." + ), ) parser.add_argument( "--framework", choices=["gaudi", "k4fwcore"], default="k4fwcore", From 0671bae519d2846c558ef881a3a53a5e3749dd7d Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Wed, 29 Apr 2026 16:35:02 +0200 Subject: [PATCH 15/36] Remove unrechable code --- k4FWCore/helpers/gaudi_gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k4FWCore/helpers/gaudi_gen.py b/k4FWCore/helpers/gaudi_gen.py index 53cc0723e..266b82d17 100644 --- a/k4FWCore/helpers/gaudi_gen.py +++ b/k4FWCore/helpers/gaudi_gen.py @@ -904,8 +904,8 @@ def main() -> None: try: spec = _build_spec(args) except ValueError as exc: + # parser.error() prints usage and calls sys.exit(2); never returns. parser.error(str(exc)) - return cpp_source, cmake_source = generate(spec) From 73f2c34f0db91196a1360e4e5db9dc73da503b99 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Wed, 29 Apr 2026 16:43:05 +0200 Subject: [PATCH 16/36] Refactor help text formatting in gaudi_gen.py --- k4FWCore/helpers/gaudi_gen.py | 55 ++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/k4FWCore/helpers/gaudi_gen.py b/k4FWCore/helpers/gaudi_gen.py index 266b82d17..9fd505c44 100644 --- a/k4FWCore/helpers/gaudi_gen.py +++ b/k4FWCore/helpers/gaudi_gen.py @@ -9,6 +9,7 @@ import re import shlex import sys +import textwrap from dataclasses import dataclass from typing import List, Optional @@ -732,33 +733,33 @@ def _build_parser() -> argparse.ArgumentParser: prog="gaudi_gen.py", description="Generate Gaudi Functional C++ algorithm boilerplate.", formatter_class=argparse.RawDescriptionHelpFormatter, - epilog="""\ -Functional type is inferred from the number of inputs and outputs: - consumer inputs > 0, outputs == 0 - producer inputs == 0, outputs >= 1 - transformer inputs >= 1, outputs == 1 - multitransformer inputs >= 1, outputs > 1 - filter inputs >= 1, supply --type filter explicitly - -Examples: - # k4FWCore producer (type inferred) - gaudi_gen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' - - # k4FWCore multi-output producer with properties - gaudi_gen.py MyProducer \\ - -o 'edm4hep::MCParticleCollection:MCParticles' \\ - 'edm4hep::TrackCollection:Tracks' \\ - -p 'int:ExampleInt:3:An example integer property' - - # Gaudi transformer wrapped in 'namespace MyNamespace { ... }' (type inferred) - gaudi_gen.py MySum -i 'Input1:Loc1' 'Input2:Loc2' -o 'Output:OutLoc' \\ - --framework gaudi -n MyNamespace - - # Variable-length inputs (k4FWCore only) - gaudi_gen.py MyVarConsumer \\ - -i 'edm4hep::MCParticleCollection:Inputs' \\ - --runtime-inputs 'edm4hep::MCParticleCollection:Inputs:MCParticles0,MCParticles1' -""", + epilog=textwrap.dedent("""\ + Functional type is inferred from the number of inputs and outputs: + consumer inputs > 0, outputs == 0 + producer inputs == 0, outputs >= 1 + transformer inputs >= 1, outputs == 1 + multitransformer inputs >= 1, outputs > 1 + filter inputs >= 1, supply --type filter explicitly + + Examples: + # k4FWCore producer (type inferred) + gaudi_gen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' + + # k4FWCore multi-output producer with properties + gaudi_gen.py MyProducer \\ + -o 'edm4hep::MCParticleCollection:MCParticles' \\ + 'edm4hep::TrackCollection:Tracks' \\ + -p 'int:ExampleInt:3:An example integer property' + + # Gaudi transformer wrapped in 'namespace MyNamespace { ... }' (type inferred) + gaudi_gen.py MySum -i 'Input1:Loc1' 'Input2:Loc2' -o 'Output:OutLoc' \\ + --framework gaudi -n MyNamespace + + # Variable-length inputs (k4FWCore only) + gaudi_gen.py MyVarConsumer \\ + -i 'edm4hep::MCParticleCollection:Inputs' \\ + --runtime-inputs 'edm4hep::MCParticleCollection:Inputs:MCParticles0,MCParticles1' + """), ) parser.add_argument( "class_name", From 41f894f49e0f7ccc3f3033a75c897df3cf2fcbc0 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Wed, 29 Apr 2026 17:54:09 +0200 Subject: [PATCH 17/36] Modify shebang and add script metadata Updated shebang to use 'uv run' for script execution and added metadata for dependencies. --- k4FWCore/helpers/gaudi_gen.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/k4FWCore/helpers/gaudi_gen.py b/k4FWCore/helpers/gaudi_gen.py index 9fd505c44..2f4bac554 100644 --- a/k4FWCore/helpers/gaudi_gen.py +++ b/k4FWCore/helpers/gaudi_gen.py @@ -1,8 +1,19 @@ -#!/usr/bin/env python3 +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.9" +# dependencies = [ +# "jinja2>=3.0", +# ] +# /// """ Gaudi Functional C++ Class Generator Generates boilerplate for Gaudi Functional algorithms in both the k4FWCore and native Gaudi::Functional frameworks. + +Run with either: + uv run gaudi_gen.py [args...] # uv resolves deps from the PEP 723 block + ./gaudi_gen.py [args...] # uses the shebang (requires uv on PATH) + python3 gaudi_gen.py [args...] # plain Python; needs jinja2 installed """ import argparse import os From 833efe1f3db7c36fa321d0797d6c3cde1be32588 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Wed, 29 Apr 2026 17:55:36 +0200 Subject: [PATCH 18/36] Update README with clearer usage and requirements Clarified requirements and usage instructions for the script, specifying the need for Jinja2 only in certain contexts and providing detailed invocation methods. --- k4FWCore/helpers/README.md | 58 ++++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/k4FWCore/helpers/README.md b/k4FWCore/helpers/README.md index e192a8ddb..619a8bb66 100644 --- a/k4FWCore/helpers/README.md +++ b/k4FWCore/helpers/README.md @@ -17,7 +17,31 @@ across the matrix of options. ## Requirements - Python 3.9+ -- `Jinja2` (`pip install jinja2`) +- `Jinja2` (only needed for the plain-`python3` invocation path) + +The script ships a [PEP 723](https://peps.python.org/pep-0723/) inline +metadata block, so [`uv`](https://docs.astral.sh/uv/) can run it directly +without any manual environment setup. If you don't have `uv` installed, +`pipx install uv` or follow the install instructions on the uv site. + +## How to run it + +There are three equivalent ways to invoke the script: + +```bash +# 1. Recommended — uv resolves Python and Jinja2 from the PEP 723 block. +uv run gaudi_gen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' + +# 2. Direct execution via the shebang (requires uv on PATH). +chmod +x gaudi_gen.py +./gaudi_gen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' + +# 3. Plain Python (you must have jinja2 installed in the active env). +uv run gaudi_gen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' +``` + +The first two routes are self-contained: nothing needs to be installed in +the system or active Python environment beyond `uv` itself. --- @@ -25,7 +49,7 @@ across the matrix of options. ```bash # k4FWCore producer (functional type inferred from --outputs) -python3 gaudi_gen.py MyProducer \ +uv run gaudi_gen.py MyProducer \ -o 'edm4hep::MCParticleCollection:MCParticles' ``` @@ -33,7 +57,7 @@ That writes `MyProducer.cpp` in the current directory. Add `--cmake` to also emit a `CMakeLists.txt`: ```bash -python3 gaudi_gen.py MyProducer \ +uv run gaudi_gen.py MyProducer \ -o 'edm4hep::MCParticleCollection:MCParticles' \ --cmake ``` @@ -93,7 +117,7 @@ file and the CMake file independently, so partial regeneration is fine | Flag | Notes | | ---------------- | -------------------------------------------------------------------------------------------------- | -| `-n`, `--namespace` | Wrap the generated class in `namespace { ... }`. | +| `-n`, `--namespace` | Wrap the generated class in a **C++ namespace** — `namespace { ... } // namespace ` around the class definition. Empty (default) leaves the class at global scope. Not to be confused with the Gaudi/k4FWCore framework namespace or the runtime algorithm instance name. | | `--framework` | `k4fwcore` (default) or `gaudi` (vanilla `Gaudi::Functional`). | | `--use-class` | Generate `class ... { public: ... }` instead of the default `struct`. | | `--type-aliases` | Emit `using XxxColl = ...;` aliases for input collection types and use them in the operator signature. | @@ -133,7 +157,7 @@ auto-promotes to `multitransformer`. ### Producer with multiple outputs and a property ```bash -python3 gaudi_gen.py MyProducer \ +uv run gaudi_gen.py MyProducer \ -o 'edm4hep::MCParticleCollection:MCParticles' \ 'edm4hep::TrackCollection:Tracks' \ -p 'int:ExampleInt:3:An example integer property' @@ -141,23 +165,33 @@ python3 gaudi_gen.py MyProducer \ The output uses a `retType = std::tuple<...>` alias for readability. -### Native Gaudi transformer with namespace +### Native Gaudi transformer wrapped in a C++ namespace ```bash -python3 gaudi_gen.py MySum \ +uv run gaudi_gen.py MySum \ -i 'Input1:Loc1' 'Input2:Loc2' \ -o 'Output:OutLoc' \ --framework gaudi \ -n MyNamespace ``` -This emits a `BaseClass_t = Gaudi::Functional::Traits::BaseClass_t` -typedef and uses `Gaudi::Functional::Transformer<...>` as the base. +`-n MyNamespace` wraps the class definition in `namespace MyNamespace { ... }`. +The output also emits a +`BaseClass_t = Gaudi::Functional::Traits::BaseClass_t` +typedef and uses `Gaudi::Functional::Transformer<...>` as the base: + +```cpp +namespace MyNamespace { +struct MySum final : Gaudi::Functional::Transformer { + // ... +}; +} // namespace MyNamespace +``` ### Variable-length / runtime inputs (k4FWCore) ```bash -python3 gaudi_gen.py MyVarConsumer \ +uv run gaudi_gen.py MyVarConsumer \ -i 'edm4hep::MCParticleCollection:Inputs' \ --runtime-inputs 'edm4hep::MCParticleCollection:Inputs:MCParticles0,MCParticles1' ``` @@ -168,7 +202,7 @@ and `operator()` receives `const std::vector Date: Wed, 20 May 2026 21:07:46 +0200 Subject: [PATCH 19/36] Remove the legacy podio I/O components and services (#392) --- README.md | 16 +- cmake/k4FWCoreConfig.cmake.in | 3 +- doc/LegacyPodioInputOutput.md | 136 ---------- doc/PodioInputOutput.md | 246 ------------------ k4FWCore/CMakeLists.txt | 9 +- k4FWCore/components/FCCDataSvc.cpp | 30 --- k4FWCore/components/FCCDataSvc.h | 31 --- k4FWCore/components/PodioInput.cpp | 213 --------------- k4FWCore/components/PodioInput.h | 61 ----- k4FWCore/components/PodioOutput.cpp | 150 ----------- k4FWCore/components/PodioOutput.h | 65 ----- k4FWCore/components/k4DataSvc.cpp | 31 --- k4FWCore/components/k4DataSvc.h | 30 --- k4FWCore/include/k4FWCore/MetaDataHandle.h | 175 ------------- k4FWCore/include/k4FWCore/MetadataUtils.h | 27 -- k4FWCore/include/k4FWCore/PodioDataSvc.h | 131 ---------- k4FWCore/src/PodioDataSvc.cpp | 192 -------------- test/k4FWCoreTest/CMakeLists.txt | 16 +- .../options/TestAlgorithmWithTFile.py | 21 +- .../options/TestUniqueIDGenSvc.py | 6 +- test/k4FWCoreTest/options/TwoProducers.py | 18 +- .../options/checkExampleEventData.py | 17 +- .../k4FWCoreTest/options/createEventHeader.py | 24 +- .../options/createExampleEventData.py | 20 +- .../createExampleEventDataInDirectory.py | 18 +- .../options/createExampleEventData_cellID.py | 23 +- .../options/readExampleDataFromNthEvent.py | 26 +- .../options/readExampleEventData.py | 26 +- .../readLimitedSetOfCollectionsk4DataSvc.py | 38 --- test/k4FWCoreTest/options/runFunctionalMix.py | 52 +--- test/k4FWCoreTest/scripts/CheckOutputFiles.py | 15 +- ...stAlgorithmWithTFile_framework_nonempty.py | 27 -- ...TestAlgorithmWithTFile_myTFile_nonempty.py | 27 -- .../k4FWCoreTest_AlgorithmWithTFile.h | 2 +- 34 files changed, 105 insertions(+), 1817 deletions(-) delete mode 100644 doc/LegacyPodioInputOutput.md delete mode 100644 doc/PodioInputOutput.md delete mode 100644 k4FWCore/components/FCCDataSvc.cpp delete mode 100644 k4FWCore/components/FCCDataSvc.h delete mode 100644 k4FWCore/components/PodioInput.cpp delete mode 100644 k4FWCore/components/PodioInput.h delete mode 100644 k4FWCore/components/PodioOutput.cpp delete mode 100644 k4FWCore/components/PodioOutput.h delete mode 100644 k4FWCore/components/k4DataSvc.cpp delete mode 100644 k4FWCore/components/k4DataSvc.h delete mode 100644 k4FWCore/include/k4FWCore/MetaDataHandle.h delete mode 100644 k4FWCore/include/k4FWCore/PodioDataSvc.h delete mode 100644 k4FWCore/src/PodioDataSvc.cpp delete mode 100644 test/k4FWCoreTest/options/readLimitedSetOfCollectionsk4DataSvc.py delete mode 100644 test/k4FWCoreTest/scripts/check_TestAlgorithmWithTFile_framework_nonempty.py delete mode 100644 test/k4FWCoreTest/scripts/check_TestAlgorithmWithTFile_myTFile_nonempty.py diff --git a/README.md b/README.md index fa1577a90..786108890 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,12 @@ k4FWCore also provides the `k4run` script used to run Gaudi steering files. See ### Basic I/O -| Current | Legacy | Description | -|---------|--------|-| -| IOSvc | k4DataSvc | Service handling the PODIO types and collections | -| Reader | PodioInput | Algorithm to read data from input files on disk. | -| Writer | PodioOutput | Algorithm to write data to an output file on disk. | -| MetadataSvc | MetaDataHandle | Service/Handle handling user defined metadata | - -See the [documentation](doc/PodioInputOutput.md) for more information. - -### Auxiliary +| Name | Description | +|---------|-| +| IOSvc | Service handling the PODIO types and collections | +| Reader | Algorithm to read data from input files on disk. | +| Writer | Algorithm to write data to an output file on disk. | +| MetadataSvc | Service/Handle handling user defined metadata | ### Collection Merger diff --git a/cmake/k4FWCoreConfig.cmake.in b/cmake/k4FWCoreConfig.cmake.in index 02a13c497..01bb38853 100644 --- a/cmake/k4FWCoreConfig.cmake.in +++ b/cmake/k4FWCoreConfig.cmake.in @@ -2,8 +2,7 @@ # k4FWCore CMake Config # # Exported Targets -# - k4FWCore::k4FWCore The core library containing the PodioDataSvc -# and the KeepDropSwitch +# - k4FWCore::k4FWCore The core library # - k4FWCore::k4FWCorePlugins The plugin library for the core plugins # provided by k4FWCore. Includes all major # services for I/O and as well as some utility diff --git a/doc/LegacyPodioInputOutput.md b/doc/LegacyPodioInputOutput.md deleted file mode 100644 index 5c1d24d92..000000000 --- a/doc/LegacyPodioInputOutput.md +++ /dev/null @@ -1,136 +0,0 @@ - -# Legacy reading and writing EDM4hep files in Gaudi with the 4DataSvc - -:::{caution} -`k4DataSvc` is a legacy service previously used in K4FWCore for reading and writing data in EDM4hep or other data models based on PODIO. - -The currently used service is `IOSvc`, which offers improved streamlined functionality and better support for modern workflows. For detailed documentation on `IOSvc`, refer to [this documentation](PodioInputOutput.md). -::: - -This page will describe the usage of legacy [k4FWCore](https://github.com/key4hep/k4FWCore) -facilities to read and write EDM4hep. This page also assumes a certain -familiarity with Gaudi, i.e. most of the snippets just show a minimal -configuration part, and not a complete runnable example. - -## The `k4DataSvc` - -Whenever you want to work with EDM4hep in the Gaudi based framework of Key4hep, -you will need to use the `k4DataSvc` as *EventDataSvc*. You can instantiate and -configure this service like the following - -```python -from Gaudi.Configuration import * -from Configurables import k4DataSvc - -evtSvc = k4DataSvc("EventDataSvc") -``` - -**It is important that the name is `EventDataSvc` in this case, as otherwise -this is an assumption from Gaudi.** Once you have the `k4DataSvc` instantiated, -you still have to make the `ApplicationMgr` aware of it, by making sure that the -`evtSvc` is in the list of the *external services* (`ExtSvc`): - -```python -from Configurables import ApplicationMgr -ApplicationMgr( - # other args - ExtSvc = [evtSvc] -) -``` - -## Reading events - -To read events you will need to use the `PodioInput` algorithm in addition to -the [`k4DataSvc`](#the-k4datasvc). Currently, you will need to pass the input -file to the `k4DataSvc` via the `input` option but pass the collections that you -want to read to the `PodioInput`. We are working on making this (discussion -happens in this [issue](https://github.com/key4hep/k4FWCore/issues/105)). The -parts of your options file related to reading EDM4hep files will look something -like this - -```python -from Configurables import PodioInput, k4DataSvc - -evtSvc = k4DataSvc("EventDataSvc") -evtSvc.input = "/path/to/your/input-file.root" - -podioInput = PodioInput() -``` - -It is possible to change the input file from the command line via -```bash -k4run --EventDataSvc.input= -``` - -By default the `PodioInput` will read all collections that are available from -the input file. It is possible to limit the collections that should become -available via the `collections` option - -```python -podioInput.collections = [ - # List of collection names that should be made available -] -``` - -## Writing events - -To write events you will need to use the `PodioOutput` algorithm in addition to -the [`k4DataSvc`](#the-k4datasvc): - -```python -from Configurables import PodioOutput - -podioOutput = PodioOutput("PodioOutput", filename="my_output.root") -``` - -By default this will write the complete event contents to the output file. - -### Writing only a subset of collections - -Sometimes it is desirable to limit the collections to a subset of all available -collections from the EventStore. The `PodioOutput` allows to do this via the -`outputCommands` option that takes a list of `keep` or `drop` commands. Each -command must consist of the `keep`/`drop` command and a target. The target is a -collection name that may include the `?` or `*` wildcard patterns. This might -look like the following - -```python -podioOutput.outputCommands = ["keep *"] -``` - -which will keep everything (the default), while - -```python -podioOutput.outputCommands = ["drop *"] -``` - -will simply drop all collections and effectively write an empty file (apart from -some metadata). A common pattern is to `"drop *"` and then selectively adding -`keep` collections to keep, e.g. to only keep the highest level MC and reco -information: - -```python -podioOutput.outputCommands = [ - "drop *", - "keep MCParticlesSkimmed", - "keep PandoraPFOs", - "keep RecoMCTruthLink", -] -``` diff --git a/doc/PodioInputOutput.md b/doc/PodioInputOutput.md deleted file mode 100644 index c02594557..000000000 --- a/doc/PodioInputOutput.md +++ /dev/null @@ -1,246 +0,0 @@ - -# Reading and writing EDM4hep files in Gaudi - -The facilities to read and write EDM4hep (or in general event data models based on podio) are provided by [k4FWCore](https://github.com/key4hep/k4FWCore). This page will describe their usage, but not go into too much details of their internals. This page also assumes a certain familiarity with Gaudi, i.e. most of the snippets just show a minimal configuration part, and not a complete runnable example. - -## Accessing event data - -`IOSvc` is an external Gaudi service for reading and writing EDM4hep files. The service should be imported from `k4FWCore` and named "IOSvc" as other components may look for it under this name. - -```python -from k4FWCore import IOSvc - -io_svc = IOSvc("IOSvc") # or just IOSvc() as "IOSvc" name is used by default -``` - -After instantiation the service should be register as an external service in the `ApplicationMgr`. Similarly, it's important to import the `ApplicationMgr` from `k4FWCore`: - -```python -from k4FWCore import ApplicationMgr - -ApplicationMgr( - # other args - ExtSvc=[ - io_svc, - # other services - ] -) -``` - -### Reading events - -The `IOSvc` supports reading EDM4hep ROOT files. Both files written with the ROOT TTree or RNTuple backend are supported with the backend inferred automatically from the files themselves. - -The `Input` property can be used to specify the input. The `IOSvc` will not read any files unless the `Input` property is specified. - -::::{tab-set} -:::{tab-item} Python -```python -io_svc.Input = "input.root" -``` -::: -:::{tab-item} CLI -```sh -k4run --IOSvc.Input input.root -``` -::: -:::: - -:::{note} -The value assigned to the `Input` will be processed as is, in particular without regular expression or glob expansion. -::: - -A list of filenames can be given in order to specify multiple input files: - -::::{tab-set} -:::{tab-item} Python -```python -io_svc.Input = ["input.root", "another_input.root", ] -``` -::: -:::{tab-item} CLI -```sh -k4run --IOSvc.Input input.root another_input.root -``` -::: -:::: - - -During processing, for each event in the Gaudi event loop the `IOSvc` will read a frame from the input and populate the Gaudi Transient Event Store (TES) with the collections stored in that frame. - -The `FirstEventEntry` property of `IOSvc` can be used to start processing from a given frame instead of from the first frame in the input: - -::::{tab-set} -:::{tab-item} Python -```python -io_svc.FirstEventEntry = 7 # default 0 -``` -::: -:::{tab-item} CLI -```sh -k4run --IOSvc.FirstEventEntry 7 -``` -::: -:::: - -A list of collection names can be assigned to the `CollectionNames` property of `IOSvc` to limit the number of collections that will be populated. Without specifying the `CollectionNames` all present collections will be read and put into TES. - -::::{tab-set} -:::{tab-item} Python -```python -io_svc.CollectionNames = ["MCParticles", "SimTrackerHits"] -``` -::: -:::{tab-item} CLI -```sh -k4run --IOSvc.CollectionNames "MCParticles" "SimTrackerHits" -``` -::: -:::: - -### Writing events - -The `IOSvc` supports writing EDM4hep to the ROOT output. The `Output` property can be used to specify the output. The `IOSvc` will not write any files unless the `Output` property is specified. - -::::{tab-set} -:::{tab-item} Python -```python -io_svc.Output = "output.root" -``` -::: -:::{tab-item} CLI -```sh -k4run --IOSvc.Output output.root -``` -::: -:::: - -:::{note} -Unlike the `Input`, the `Output` property should be a single string even when writing multiple files is expected. When the size limit for an output file is reached, the system will automatically open a new file and start writing to it. -::: - -The writing backend can be specified with the `OutputType` property of `IOSvc`. The allowed values are `"ROOT"` for TTree-based output or `"RNTuple"` for RNTuple-based output. By default the `"ROOT"` backend is used. - -::::{tab-set} -:::{tab-item} Python -```python -io_svc.OutputType = "RNTuple" -``` -::: -:::{tab-item} CLI -```sh -k4run --IOSvc.OutputType "RNTuple" -``` -::: -:::: - -During processing, at the end of each event from the Gaudi event loop the `IOSvc` will write a frame with the collections present in TES. By default all the collections will be written. The `outputCommands` property of `IOSvc` can be used to specify commands to select which collections should be written. For example, the following commands will skip writing all the collections except for the collections named `MCParticles1`, `MCParticles2` and `SimTrackerHits`: - -::::{tab-set} -:::{tab-item} Python -```python -io_svc.outputCommands = [ - "drop *", - "keep MCParticles1", - "keep MCParticles2", - "keep SimTrackerHits", -] -``` -::: -:::{tab-item} CLI -```sh -k4run --IOSvc.outputCommands \ - "drop *" \ - "keep MCParticles1" \ - "keep MCParticles2" \ - "keep SimTrackerHits" -``` -::: -:::: - -It is also possible to specify entire datatypes for keeping or dropping via the `type` sub-command, e.g. to keep everything but any collection that is of type `edm4hep::SimTrackerHitCollection` one would do: - -```python -io_svc.outputCommands = [ - "drop type edm4hep::SimTrackerHitCollection", -] -``` - -The type name here is matched against the type obtained with `podio::CollectionBase::getTypeName()`, i.e. *Collection* has to be included. Only exact matches are considered for the application of the command. - -:::{note} -The commands are processed in order so the last relevant *keep* or *drop* for any given collection will decide. -::: - - -## Accessing metadata - -The k4FWCore provides the `MetadataSvc` that allows accessing user metadata in PODIO-based data-models. There is no need to instantiate the `MetadataSvc` explicitly when using `IOSvc` as `IOSvc` can instantiate it on its own if needed. - -When both the `Input` and `Output` properties of `IOSvc` are defined, all the metadata originally present in the input will be propagated to the output, possibly adding also any user metadata created during processing. - -Unlike event data, metadata is not exposed to users through the Gaudi TES and cannot be accessed directly by algorithms in the same way. Instead, handling metadata is encapsulated within the algorithm implementation itself. For more details on how this is managed, refer to the developer documentation. - - -## Migrating from the legacy `k4DataSvc` - -Migrating from the legacy `k4DataSvc` or `PodioDataSvc` is rather straightforward. On a steering file level the `PodioDataSvc` should be replaced with the `IOSvc`, while the `PodioInput` and `PodioOutput` algorithms should be removed. For example: - -```diff --from Configurables import k4DataSvc --from Configurables import PodioInput --from Configurables import PodioOutput -+from k4FWCore import IOSvc -from k4FWCore import ApplicationMgr -from Configurables import SelectorAlg - --podioevent = k4DataSvc("EventDataSvc") --podioevent.input = "example_input.root" -+io_svc = IOSvc("IOSvc") -+io_svc.Input= "example_output.root" - --inp = PodioInput() --inp.collections = ["MCParticles", "SimTrackerHits", "TrackerHits", "Tracks"] -+io_svc.CollectionNames = ["MCParticles", "SimTrackerHits"] - -alg = SelectorAlg( - "Selector", - InputParticles="MCParticles", - InputHits="SimTrackerHits", - Output="SelectedParticles", -) - --oup = PodioOutput() --oup.filename = "example_output.root" --oup.outputCommands = ["drop MCParticles"] -+io_svc.Output = "example_output.root" -+io_svc.outputCommands = ["drop MCParticles"] - - -ApplicationMgr( -- TopAlg=[inp, alg,oup], -+ TopAlg=[alg], - EvtSel="NONE", -- ExtSvc=[podioevent], -+ ExtSvc=[io_svc], -) -``` - -Both functional algorithms and classic algorithms are compatible with either `IOSvc` or `PodioDataSvc`. diff --git a/k4FWCore/CMakeLists.txt b/k4FWCore/CMakeLists.txt index 203ab57ef..d19c08e53 100644 --- a/k4FWCore/CMakeLists.txt +++ b/k4FWCore/CMakeLists.txt @@ -20,8 +20,7 @@ limitations under the License. gaudi_install(SCRIPTS) gaudi_add_library(k4FWCore - SOURCES src/PodioDataSvc.cpp - src/KeepDropSwitch.cpp + SOURCES src/KeepDropSwitch.cpp LINK Gaudi::GaudiKernel podio::podioIO ROOT::Core ROOT::RIO ROOT::Tree EDM4HEP::utils ) target_include_directories(k4FWCore PUBLIC @@ -34,17 +33,13 @@ gaudi_add_module(k4FWCorePlugins components/EfficiencyFilter.cpp components/EventCounter.cpp components/EventHeaderCreator.cpp - components/FCCDataSvc.cpp components/IOSvc.cpp components/MetadataSvc.cpp components/OverlayTiming.cpp - components/PodioInput.cpp - components/PodioOutput.cpp components/Reader.cpp components/UniqueIDGenSvc.cpp components/Writer.cpp - components/k4DataSvc.cpp - LINK Gaudi::GaudiKernel k4FWCore k4FWCore::k4Interface ROOT::Core ROOT::RIO ROOT::Tree ROOT::MathCore EDM4HEP::edm4hep podio::podioIO) + LINK Gaudi::GaudiKernel k4FWCore k4FWCore::k4Interface ROOT::Core ROOT::RIO ROOT::Tree EDM4HEP::edm4hep) target_include_directories(k4FWCorePlugins PUBLIC $ diff --git a/k4FWCore/components/FCCDataSvc.cpp b/k4FWCore/components/FCCDataSvc.cpp deleted file mode 100644 index 088bd07e7..000000000 --- a/k4FWCore/components/FCCDataSvc.cpp +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2014-2024 Key4hep-Project. - * - * This file is part of Key4hep. - * See https://key4hep.github.io/key4hep-doc/ for further info. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "FCCDataSvc.h" - -// Instantiation of a static factory class used by clients to create -// instances of this service -DECLARE_COMPONENT(FCCDataSvc) - -/// Standard Constructor -FCCDataSvc::FCCDataSvc(const std::string& name, ISvcLocator* svc) : PodioDataSvc(name, svc) { - declareProperty("inputs", m_filenames = {}, "Names of the files to read"); - declareProperty("input", m_filename = "", "Name of the file to read"); -} diff --git a/k4FWCore/components/FCCDataSvc.h b/k4FWCore/components/FCCDataSvc.h deleted file mode 100644 index e900a114e..000000000 --- a/k4FWCore/components/FCCDataSvc.h +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2014-2024 Key4hep-Project. - * - * This file is part of Key4hep. - * See https://key4hep.github.io/key4hep-doc/ for further info. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef K4FWCORE_FCCDATASVC_H -#define K4FWCORE_FCCDATASVC_H - -#include "k4FWCore/PodioDataSvc.h" - -/// an alias to k4DataSvc for backwards compatibility -class [[deprecated("Use the IOSvc instead")]] FCCDataSvc : public PodioDataSvc { -public: - /// Standard Constructor - FCCDataSvc(const std::string& name, ISvcLocator* svc); -}; -#endif diff --git a/k4FWCore/components/PodioInput.cpp b/k4FWCore/components/PodioInput.cpp deleted file mode 100644 index 74d208b42..000000000 --- a/k4FWCore/components/PodioInput.cpp +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright (c) 2014-2024 Key4hep-Project. - * - * This file is part of Key4hep. - * See https://key4hep.github.io/key4hep-doc/ for further info. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#include "PodioInput.h" -#include "Gaudi/Functional/Consumer.h" - -#include "k4FWCore/PodioDataSvc.h" - -#include "edm4hep/edm4hep.h" - -#include "podio/UserDataCollection.h" - -DECLARE_COMPONENT(PodioInput) - -template -inline void PodioInput::maybeRead(std::string_view collName) const { - if (m_podioDataSvc->readCollection(std::string(collName)).isFailure()) { - error() << "Failed to register collection " << collName << endmsg; - } -} - -void PodioInput::fillReaders() { - m_readers["edm4hep::MCParticleCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::SimTrackerHitCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::CaloHitContributionCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::SimCalorimeterHitCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::RawCalorimeterHitCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::CalorimeterHitCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::ParticleIDCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::ClusterCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::TrackerHit3DCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::TrackerHitCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::TrackerHitPlaneCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::RawTimeSeriesCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::TrackCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::VertexCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::ReconstructedParticleCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::RecoMCParticleLinkCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::CaloHitSimCaloHitLinkCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::TrackerHitSimTrackerHitLinkCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::CaloHitMCParticleLinkCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::ClusterMCParticleLinkCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::TrackMCParticleLinkCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::VertexRecoParticleLinkCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::TimeSeriesCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["edm4hep::RecDqdxCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; - m_readers["podio::UserDataCollection"] = [&](std::string_view collName) { - maybeRead>(collName); - }; - m_readers["podio::UserDataCollection"] = [&](std::string_view collName) { - maybeRead>(collName); - }; - m_readers["podio::UserDataCollection"] = [&](std::string_view collName) { - maybeRead>(collName); - }; - m_readers["podio::UserDataCollection"] = [&](std::string_view collName) { - maybeRead>(collName); - }; - m_readers["podio::UserDataCollection"] = [&](std::string_view collName) { - maybeRead>(collName); - }; - m_readers["podio::UserDataCollection"] = [&](std::string_view collName) { - maybeRead>(collName); - }; - m_readers["podio::UserDataCollection"] = [&](std::string_view collName) { - maybeRead>(collName); - }; - m_readers["podio::UserDataCollection"] = [&](std::string_view collName) { - maybeRead>(collName); - }; - m_readers["podio::UserDataCollection"] = [&](std::string_view collName) { - maybeRead>(collName); - }; - m_readers["podio::UserDataCollection"] = [&](std::string_view collName) { - maybeRead>(collName); - }; - m_readers["podio::UserDataCollection"] = [&](std::string_view collName) { - maybeRead>(collName); - }; - m_readers["edm4hep::EventHeaderCollection"] = [&](std::string_view collName) { - maybeRead(collName); - }; -} - -PodioInput::PodioInput(const std::string& name, ISvcLocator* svcLoc) : Consumer(name, svcLoc) { - // do not do anything during the genconf step - const std::string cmd = System::cmdLineArgs()[0]; - if (cmd.find("genconf") != std::string::npos) - return; - - // check whether we have the PodioEvtSvc active - m_podioDataSvc = dynamic_cast(evtSvc().get()); - if (!m_podioDataSvc) { - error() << "Could not get PodioDataSvc" << endmsg; - } - fillReaders(); -} - -StatusCode PodioInput::initialize() { - warning() << "PodioInput is deprecated and will be removed. Use the IOSvc instead" << endmsg; - // If someone uses the collections property from the command line and passes - // an empty string we assume they want all collections (as a simple way to - // override whatever is in the options file) - if (m_collectionNames.size() == 1 && m_collectionNames[0].empty()) { - m_collectionNames.clear(); - } - - debug() << "Setting collections to read to: " << m_collectionNames.value() << endmsg; - m_podioDataSvc->setCollsToRead(m_collectionNames); - - return StatusCode::SUCCESS; -} - -StatusCode PodioInput::finalize() { - warning() << "PodioInput is deprecated and will be removed. Use the IOSvc instead" << endmsg; - return StatusCode::SUCCESS; -} - -void PodioInput::operator()() const { - if (m_podioDataSvc->getEventFrame().get(edm4hep::labels::EventHeader)) { - m_readers[edm4hep::EventHeaderCollection::typeName](edm4hep::labels::EventHeader); - } else { - info() << "No EventHeader collection found in the event. Not reading it" << endmsg; - } - - const auto& collsToRead = [&]() { - if (m_collectionNames.empty()) { - return m_podioDataSvc->getEventFrame().getAvailableCollections(); - } else { - return m_collectionNames.value(); - } - }(); - - for (const auto& collName : collsToRead) { - debug() << "Registering collection to read " << collName << endmsg; - if (!m_podioDataSvc->getEventFrame().get(collName)) { - warning() << "Collection " << collName << " is not available from file." << endmsg; - continue; - } - auto type = m_podioDataSvc->getCollectionType(collName); - if (m_readers.find(type) != m_readers.end()) { - m_readers[type](collName); - } else { - maybeRead(collName); - } - } - - // Tell data service that we are done with requested collections - m_podioDataSvc->endOfRead(); -} diff --git a/k4FWCore/components/PodioInput.h b/k4FWCore/components/PodioInput.h deleted file mode 100644 index 81c88d3b7..000000000 --- a/k4FWCore/components/PodioInput.h +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2014-2024 Key4hep-Project. - * - * This file is part of Key4hep. - * See https://key4hep.github.io/key4hep-doc/ for further info. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#ifndef FWCORE_PODIOINPUT_H -#define FWCORE_PODIOINPUT_H -// Gaudi -#include "Gaudi/Functional/Consumer.h" -#include "Gaudi/Property.h" - -// STL -#include -#include - -class PodioDataSvc; - -/** @class PodioInput - * - * Class that allows to read ROOT files written with PodioOutput - * - * @author J. Lingemann - */ - -using BaseClass_t = Gaudi::Functional::Traits::BaseClass_t; - -class [[deprecated("Use the IOSvc instead")]] PodioInput final - : public Gaudi::Functional::Consumer { -public: - PodioInput(const std::string& name, ISvcLocator* svcLoc); - void operator()() const override; - - StatusCode initialize() final; - StatusCode finalize() final; - -private: - template - void maybeRead(std::string_view collName) const; - void fillReaders(); - // Name of collections to read. Set by option collections (this is temporary) - Gaudi::Property> m_collectionNames{ - this, "collections", {}, "Collections that should be read (default all)"}; - // Data service: needed to register objects and get collection IDs. Just an observing pointer. - PodioDataSvc* m_podioDataSvc; - mutable std::map> m_readers; -}; - -#endif diff --git a/k4FWCore/components/PodioOutput.cpp b/k4FWCore/components/PodioOutput.cpp deleted file mode 100644 index fcc3e200c..000000000 --- a/k4FWCore/components/PodioOutput.cpp +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (c) 2014-2024 Key4hep-Project. - * - * This file is part of Key4hep. - * See https://key4hep.github.io/key4hep-doc/ for further info. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#include -#include -#include - -#include "GaudiKernel/MsgStream.h" - -#include "PodioOutput.h" -#include "k4FWCore/PodioDataSvc.h" - -DECLARE_COMPONENT(PodioOutput) - -PodioOutput::PodioOutput(const std::string& name, ISvcLocator* svcLoc) - : Gaudi::Algorithm(name, svcLoc), m_firstEvent(true) {} - -StatusCode PodioOutput::initialize() { - warning() << "PodioOutput is deprecated and will be removed. Use the IOSvc instead" << endmsg; - if (Gaudi::Algorithm::initialize().isFailure()) - return StatusCode::FAILURE; - - // check whether we have the PodioEvtSvc active - m_podioDataSvc = dynamic_cast(evtSvc().get()); - if (nullptr == m_podioDataSvc) { - error() << "Could not get DataSvc!" << endmsg; - return StatusCode::FAILURE; - } - - // check whether output directory needs to be created and eventually create it - auto outDirPath = std::filesystem::path(m_filename.value()).parent_path(); - if (!outDirPath.empty() && !std::filesystem::is_directory(outDirPath)) { - std::error_code ec; - std::filesystem::create_directories(outDirPath, ec); - if (ec.value() != 0) { - error() << "Output directory \"" << outDirPath << "\" was not created!" << endmsg; - error() << "Error " << ec.value() << ": " << ec.message() << endmsg; - - return StatusCode::FAILURE; - } - debug() << "Created output directory: " << outDirPath << endmsg; - } - - m_framewriter = std::make_unique(m_filename); - try { - m_switch = k4FWCore::KeepDropSwitch(m_outputCommands); - } catch (const std::invalid_argument& ex) { - fatal() << ex.what() << endmsg; - return StatusCode::FAILURE; - } - - return StatusCode::SUCCESS; -} - -StatusCode PodioOutput::execute(const EventContext&) const { - auto& frame = m_podioDataSvc->getEventFrame(); - - // register for writing - if (m_firstEvent) { - auto collections = frame.getAvailableCollections(); - for (auto& collection_name : collections) { - if (m_switch.isOn(collection_name)) { - m_collection_names_to_write.push_back(collection_name); - } - } - m_framewriter->writeFrame(frame, "events", m_collection_names_to_write); - } else { - try { - m_framewriter->writeFrame(frame, "events", m_collection_names_to_write); - } catch (std::runtime_error&) { - // In this error message we are only interested in the ones that are - // missing, since only a missing collection can trigger the exception - // here. Additional collections that are present in the Frame are not - // necessarily an issue here, because we might just be configured to not - // write all of them - const auto& [missing, _] = m_framewriter->checkConsistency(frame.getAvailableCollections(), "events"); - error() << "Could not write event, because the following collections are not present: "; - std::string sep = ""; - for (const auto& name : missing) { - error() << sep << name; - sep = ", "; - } - error() << endmsg; - - return StatusCode::FAILURE; - } - } - m_firstEvent = false; - - return StatusCode::SUCCESS; -} - -/** PodioOutput::finalize - * has to happen after all algorithms that touch the data store finish. - * Here the job options are retrieved and stored to disk as a branch - * in the metadata tree. - * - */ -StatusCode PodioOutput::finalize() { - warning() << "PodioOutput is deprecated and will be removed. Use the IOSvc instead" << endmsg; - - if (Gaudi::Algorithm::finalize().isFailure()) - return StatusCode::FAILURE; - //// prepare job options metadata /////////////////////// - // retrieve the configuration of the job - // and write it to file as vector of strings - std::vector config_data; - const auto& jobOptionsSvc = Gaudi::svcLocator()->getOptsSvc(); - const auto& configured_properties = jobOptionsSvc.items(); - for (const auto& per_property : configured_properties) { - // sample output: - // HepMCToEDMConverter.genparticles = "GenParticles"; - // Note that quotes are added to all property values, - // which leads to problems with ints, lists, dicts and bools. - // For these types, the quotes must be removed in postprocessing. - config_data.emplace_back(std::get<0>(per_property) + " = \"" + std::get<1>(per_property) + "\"\n"); - } - - // Collect all the metadata - podio::Frame config_metadata_frame{}; - config_metadata_frame.putParameter("gaudiConfigOptions", config_data); - if (const char* env_key4hep_stack = std::getenv("KEY4HEP_STACK")) { - std::string s_env_key4hep_stack = env_key4hep_stack; - config_metadata_frame.putParameter("key4hepstack", s_env_key4hep_stack); - } - m_framewriter->writeFrame(config_metadata_frame, "configuration_metadata"); - - auto& metadata_frame = m_podioDataSvc->getMetaDataFrame(); - m_framewriter->writeFrame(metadata_frame, "metadata"); - - // write information into file - m_framewriter->finish(); - - return StatusCode::SUCCESS; -} diff --git a/k4FWCore/components/PodioOutput.h b/k4FWCore/components/PodioOutput.h deleted file mode 100644 index 66f572071..000000000 --- a/k4FWCore/components/PodioOutput.h +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2014-2024 Key4hep-Project. - * - * This file is part of Key4hep. - * See https://key4hep.github.io/key4hep-doc/ for further info. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#ifndef FWCORE_PODIOOUTPUT_H -#define FWCORE_PODIOOUTPUT_H - -#include "Gaudi/Algorithm.h" -#include "k4FWCore/KeepDropSwitch.h" -#include "podio/ROOTWriter.h" - -#include - -// forward declarations -class PodioDataSvc; - -class [[deprecated("Use the IOSvc instead")]] PodioOutput : public Gaudi::Algorithm { -public: - /// Constructor. - PodioOutput(const std::string& name, ISvcLocator* svcLoc); - - /// Initialization of PodioOutput. Acquires the data service, creates trees and root file. - StatusCode initialize() override; - /// Execute. For the first event creates branches for all collections known to PodioDataSvc and prepares them for - /// writing. For the following events it reconnects the branches with collections and prepares them for write. - StatusCode execute(const EventContext&) const override; - /// Finalize. Writes the meta data tree; writes file and cleans up all ROOT-pointers. - StatusCode finalize() override; - -private: - /// First event or not - mutable bool m_firstEvent; - /// Root file name the output is written to - Gaudi::Property m_filename{this, "filename", "output.root", "Name of the file to create"}; - /// Commands which output is to be kept - Gaudi::Property> m_outputCommands{ - this, "outputCommands", {"keep *"}, "A set of commands to declare which collections to keep or drop."}; - Gaudi::Property m_filenameRemote{this, "remoteFilename", "", - "An optional file path to copy the outputfile to."}; - /// Switch for keeping or dropping outputs - k4FWCore::KeepDropSwitch m_switch; - PodioDataSvc* m_podioDataSvc; - /// The actual ROOT frame writer - std::unique_ptr m_framewriter; - /// The stored collections - std::vector m_storedCollections; - /// The collections to write out - mutable std::vector m_collection_names_to_write; -}; - -#endif diff --git a/k4FWCore/components/k4DataSvc.cpp b/k4FWCore/components/k4DataSvc.cpp deleted file mode 100644 index d5d0e490e..000000000 --- a/k4FWCore/components/k4DataSvc.cpp +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2014-2024 Key4hep-Project. - * - * This file is part of Key4hep. - * See https://key4hep.github.io/key4hep-doc/ for further info. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "k4DataSvc.h" - -// Instantiation of a static factory class used by clients to create -// instances of this service -DECLARE_COMPONENT(k4DataSvc) - -/// Standard Constructor -k4DataSvc::k4DataSvc(const std::string& name, ISvcLocator* svc) : PodioDataSvc(name, svc) { - declareProperty("inputs", m_filenames = {}, "Names of the files to read"); - declareProperty("input", m_filename = "", "Name of the file to read"); - declareProperty("FirstEventEntry", m_1stEvtEntry = 0, "First event to read"); -} diff --git a/k4FWCore/components/k4DataSvc.h b/k4FWCore/components/k4DataSvc.h deleted file mode 100644 index 65c932401..000000000 --- a/k4FWCore/components/k4DataSvc.h +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2014-2024 Key4hep-Project. - * - * This file is part of Key4hep. - * See https://key4hep.github.io/key4hep-doc/ for further info. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef K4FWCORE_K4DATASVC_H -#define K4FWCORE_K4DATASVC_H - -#include "k4FWCore/PodioDataSvc.h" - -class [[deprecated("Use the IOSvc instead")]] k4DataSvc : public PodioDataSvc { -public: - /// Standard Constructor - k4DataSvc(const std::string& name, ISvcLocator* svc); -}; -#endif diff --git a/k4FWCore/include/k4FWCore/MetaDataHandle.h b/k4FWCore/include/k4FWCore/MetaDataHandle.h deleted file mode 100644 index 2769fa8d2..000000000 --- a/k4FWCore/include/k4FWCore/MetaDataHandle.h +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright (c) 2014-2024 Key4hep-Project. - * - * This file is part of Key4hep. - * See https://key4hep.github.io/key4hep-doc/ for further info. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#ifndef K4FWCORE_METADATAHANDLE_H -#define K4FWCORE_METADATAHANDLE_H - -#include - -#include "k4FWCore/MetadataUtils.h" -#include "k4FWCore/PodioDataSvc.h" - -namespace k4FWCore { - -template -class [[deprecated("Use k4FWCore::putParameter / k4FWCore::getParameter from MetadataUtils.h instead")]] -MetaDataHandle { -public: - MetaDataHandle(const std::string& descriptor, Gaudi::DataHandle::Mode a); - MetaDataHandle(const Gaudi::DataHandle& handle, const std::string& descriptor, Gaudi::DataHandle::Mode a); - - /// Get the value that is stored in this MetaDataHandle - /// - /// @returns The value for this MetaDataHandle - /// - /// @throws GaudiException in case the value is not (yet) available - const T get() const; - - /// Get the (optional) value that is stored in this MetaDataHandle - /// - /// @returns An optional that contains the value if it was available from the - /// data store and is not engaged otherwise - std::optional get_optional() const; - - /// Get the value that is stored in the MetaDataHandle or the provided default - /// value in case that is not available - /// - /// @returns The value stored in the Handle or the default value - const T get(const T& defaultValue) const; - - /// Set the value for this MetaDataHandle - /// - /// @note This can only be called during initialize and/or finalize but not - /// during execute for algorithms that use it - void put(T); - -private: - std::string fullDescriptor() const; - - void checkPodioDataSvc(); - -private: - ServiceHandle m_eds; - std::string m_descriptor; - PodioDataSvc* m_podio_data_service{nullptr}; - const Gaudi::DataHandle* m_dataHandle{nullptr}; // holds the identifier in case we do collection metadata - Gaudi::DataHandle::Mode m_mode; -}; - -//--------------------------------------------------------------------------- -template -MetaDataHandle::MetaDataHandle(const std::string& descriptor, Gaudi::DataHandle::Mode a) - : m_eds("EventDataSvc", "DataHandle"), m_descriptor(descriptor), m_mode(a) { - m_eds.retrieve().ignore(); - m_podio_data_service = dynamic_cast(m_eds.get()); - checkPodioDataSvc(); -} - -//--------------------------------------------------------------------------- -template -MetaDataHandle::MetaDataHandle(const Gaudi::DataHandle& handle, const std::string& descriptor, - Gaudi::DataHandle::Mode a) - : m_eds("EventDataSvc", "DataHandle"), m_descriptor(descriptor), m_dataHandle(&handle), m_mode(a) { - m_eds.retrieve().ignore(); - m_podio_data_service = dynamic_cast(m_eds.get()); - checkPodioDataSvc(); -} - -//--------------------------------------------------------------------------- -template -std::optional MetaDataHandle::get_optional() const { - if (m_podio_data_service) { - return m_podio_data_service->getMetaDataFrame().getParameter(fullDescriptor()); - } - return k4FWCore::getParameter(fullDescriptor()); -} - -//--------------------------------------------------------------------------- -template -const T MetaDataHandle::get() const { - auto optional_parameter = get_optional(); - if (!optional_parameter.has_value()) { - throw GaudiException("MetaDataHandle empty handle access", - "MetaDataHandle " + fullDescriptor() + " not (yet?) available", StatusCode::FAILURE); - } - return optional_parameter.value(); -} - -//--------------------------------------------------------------------------- -template -const T MetaDataHandle::get(const T& defaultValue) const { - return get_optional().value_or(defaultValue); -} - -//--------------------------------------------------------------------------- -template -void MetaDataHandle::put(T value) { - if (m_mode != Gaudi::DataHandle::Writer) - throw GaudiException("MetaDataHandle policy violation", "Put for non-writing MetaDataHandle not allowed", - StatusCode::FAILURE); - // check whether we are in the proper State - // put is only allowed in the initialization - - std::string full_descriptor = fullDescriptor(); - // DataHandle based algorithms - if (m_podio_data_service) { - if (m_podio_data_service->targetFSMState() == Gaudi::StateMachine::RUNNING) { - throw GaudiException("MetaDataHandle policy violation", "Put cannot be used during the event loop", - StatusCode::FAILURE); - } - podio::Frame& frame = m_podio_data_service->getMetaDataFrame(); - frame.putParameter(full_descriptor, value); - // Functional algorithms - } else { - k4FWCore::putParameter(full_descriptor, value); - } -} - -//--------------------------------------------------------------------------- -template -std::string MetaDataHandle::fullDescriptor() const { - if (nullptr != m_dataHandle) { - auto full_descriptor = podio::collMetadataParamName(m_dataHandle->objKey(), m_descriptor); - // remove the "/Event/" part of the collections' object key if in read mode - if (m_mode == Gaudi::DataHandle::Reader && full_descriptor.find("/Event/") == 0u) { - full_descriptor.erase(0, 7); - } - return full_descriptor; - } - - return m_descriptor; -} - -//--------------------------------------------------------------------------- -template -void MetaDataHandle::checkPodioDataSvc() { - // do not do this check during the genconf step - const std::string cmd = System::cmdLineArgs()[0]; - if (cmd.find("genconf") != std::string::npos) - return; - - if (!m_podio_data_service && !Gaudi::svcLocator()->service("MetadataSvc", false)) { - std::cout << "Warning: MetaDataHandles require the PodioDataSvc or for compatibility the MetadataSvc" << std::endl; - } -} -} // namespace k4FWCore - -template -using MetaDataHandle [[deprecated("Use k4FWCore::MetaDataHandle instead")]] = k4FWCore::MetaDataHandle; - -#endif diff --git a/k4FWCore/include/k4FWCore/MetadataUtils.h b/k4FWCore/include/k4FWCore/MetadataUtils.h index ca8cd8eb3..a671139ce 100644 --- a/k4FWCore/include/k4FWCore/MetadataUtils.h +++ b/k4FWCore/include/k4FWCore/MetadataUtils.h @@ -73,20 +73,6 @@ void putParameter(const std::string& name, const T& value, const GaudiComp* comp metadataSvc->template put(name, value); } -/// @brief Save a metadata parameter in the metadata frame. Overload for compatibility -/// with the MetadataHandle, don't use! -/// @deprecated Use the overload taking a Gaudi::Algorithm* instead -template -[[deprecated("Use the overload taking a Gaudi::Algorithm* as third argument instead")]] void -putParameter(const std::string& name, const T& value) { - auto metadataSvc = Gaudi::svcLocator()->service("MetadataSvc", false); - if (!metadataSvc) { - std::cout << "MetadataSvc not found" << std::endl; - return; - } - return metadataSvc->put(name, value); -} - /// @brief Get a metadata parameter from the metadata frame /// @param name The name of the parameter /// @param comp The Gaudi component (algorithm, tool) that is retrieving the @@ -105,19 +91,6 @@ std::optional getParameter(const std::string& name, const GaudiComp* comp) { return metadataSvc->template get(name); } -/// @brief Get a metadata parameter from the metadata frame. Overload for compatibility -/// with the MetadataHandle, don't use! -/// @deprecated Use the overload taking a Gaudi::Algorithm* instead -template -[[deprecated("Use the overload taking a Gaudi::Algorithm* as second argument instead")]] std::optional -getParameter(const std::string& name) { - auto metadataSvc = Gaudi::svcLocator()->service("MetadataSvc", false); - if (!metadataSvc) { - return std::nullopt; - } - return metadataSvc->get(name); -} - /// @brief Put a metadata parameter associated with a collection into the metadata /// /// Internally builds the correct parameter name from the collection name and diff --git a/k4FWCore/include/k4FWCore/PodioDataSvc.h b/k4FWCore/include/k4FWCore/PodioDataSvc.h deleted file mode 100644 index 0da2591e2..000000000 --- a/k4FWCore/include/k4FWCore/PodioDataSvc.h +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (c) 2014-2024 Key4hep-Project. - * - * This file is part of Key4hep. - * See https://key4hep.github.io/key4hep-doc/ for further info. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#ifndef FWCORE_PODIODATASVC_H -#define FWCORE_PODIODATASVC_H - -#include "GaudiKernel/DataSvc.h" -#include "GaudiKernel/IConversionSvc.h" -// PODIO -#include "podio/CollectionBase.h" -#include "podio/CollectionIDTable.h" -#include "podio/Frame.h" -#include "podio/ROOTReader.h" -#include -// Forward declarations -#include "k4FWCore/DataWrapper.h" -class DataWrapperBase; -class PodioOutput; -namespace k4FWCore { -template -class MetaDataHandle; -} - -/** @class PodioEvtSvc EvtDataSvc.h - * - * An EvtDataSvc for PODIO classes - * - * @author B. Hegner - */ -class [[deprecated("Use the IOSvc instead")]] PodioDataSvc : public DataSvc { - template - friend class k4FWCore::MetaDataHandle; - friend class PodioOutput; - friend class Lcio2EDM4hepTool; - -public: - typedef std::vector> CollRegistry; - - StatusCode initialize() final; - StatusCode reinitialize() final; - StatusCode finalize() final; - StatusCode clearStore() final; - StatusCode i_setRoot(std::string root_path, IOpaqueAddress* pRootAddr) final; - StatusCode i_setRoot(std::string root_path, DataObject* pRootObj) final; - - /// Standard Constructor - PodioDataSvc(const std::string& name, ISvcLocator* svc); - - // Use DataSvc functionality except where we override - using DataSvc::registerObject; - /// Overriding standard behaviour of evt service - /// Register object with the data store. - StatusCode registerObject(std::string_view parentPath, std::string_view fullPath, DataObject* pObject) final; - - const std::string_view getCollectionType(const std::string& collName); - - template - StatusCode readCollection(const std::string& collName) { - DataObject* objectPtr = nullptr; - if (DataSvc::findObject("/Event", "/" + collName, objectPtr)) { - debug() << "Collection " << collName << " already read, not reading it again" << endmsg; - return StatusCode::SUCCESS; - } - const T* collection(nullptr); - collection = static_cast(m_eventframe.get(collName)); - if (collection == nullptr) { - error() << "Collection " << collName << " does not exist." << endmsg; - } - auto wrapper = new DataWrapper; - wrapper->setData(collection); - m_podio_datawrappers.push_back(wrapper); - return DataSvc::registerObject("/Event", "/" + collName, wrapper); - } - - const podio::Frame& getEventFrame() const { return m_eventframe; } - - /// Resets caches of reader and event store, increases event counter - void endOfRead(); - - /// TODO: Make this private again after conversions have been properly solved - podio::Frame& getMetaDataFrame() { return m_metadataframe; } - - void setCollsToRead(const std::vector& collsToRead) { m_collsToRead = collsToRead; } - -private: - /// PODIO reader for ROOT files - podio::ROOTReader m_reader; - /// PODIO Frame, used to initialise collections - podio::Frame m_eventframe; - /// PODIO Frame, used to store metadata - podio::Frame m_metadataframe; - /// Counter of the event number - int m_eventNum{0}; - /// Number of events in the file / to process - int m_numAvailableEvents{-1}; - int m_requestedEventMax{-1}; - /// Whether reading from file at all - bool m_reading_from_file{false}; - - SmartIF m_cnvSvc; - - // Registry of data wrappers; needed for memory management - std::vector m_podio_datawrappers; - /// The names of the collections to read (set externally) - std::vector m_collsToRead{}; - -protected: - /// ROOT file name the input is read from. Set by option filename - std::vector m_filenames; - std::string m_filename; - /// Jump to nth events at the beginning. Set by option FirstEventEntry - /// This option is helpful when we want to debug an event in the middle of a file - unsigned m_1stEvtEntry{0}; - bool m_bounds_check_needed{true}; -}; -#endif // CORE_PODIODATASVC_H diff --git a/k4FWCore/src/PodioDataSvc.cpp b/k4FWCore/src/PodioDataSvc.cpp deleted file mode 100644 index c3990c4f6..000000000 --- a/k4FWCore/src/PodioDataSvc.cpp +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright (c) 2014-2024 Key4hep-Project. - * - * This file is part of Key4hep. - * See https://key4hep.github.io/key4hep-doc/ for further info. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#include "k4FWCore/PodioDataSvc.h" -#include "GaudiKernel/IEventProcessor.h" -#include "GaudiKernel/IProperty.h" -#include "GaudiKernel/ISvcLocator.h" -#include "k4FWCore/DataWrapper.h" -#include - -#include "podio/CollectionBase.h" -#include "podio/podioVersion.h" - -/// Service initialisation -StatusCode PodioDataSvc::initialize() { - warning() << "The PodioDataSvc is deprecated and will be removed. Use the IOSvc instead" << endmsg; - // Nothing to do: just call base class initialisation - StatusCode status = DataSvc::initialize(); - ISvcLocator* svc_loc = serviceLocator(); - - // Attach data loader facility - m_cnvSvc = svc_loc->service("EventPersistencySvc"); - status = setDataLoader(m_cnvSvc); - - if (!m_filename.empty()) { - m_filenames.push_back(m_filename); - } - - if (!m_filenames.empty()) { - if (!m_filenames[0].empty()) { - m_reading_from_file = true; - m_reader.openFiles(m_filenames); - m_numAvailableEvents = m_reader.getEntries("events"); - m_numAvailableEvents -= m_1stEvtEntry; - } - } - - if (m_reading_from_file) { - if (auto metadata = m_reader.readEntry("metadata", 0)) { - m_metadataframe = std::move(metadata); - } else { - warning() << "Reading file without a 'metadata' category." << endmsg; - m_metadataframe = podio::Frame(); - } - } else { - m_metadataframe = podio::Frame(); - } - - auto appMgr = service("ApplicationMgr", false); - if (!appMgr) { - throw std::runtime_error("Could not get ApplicationMgr"); - } - Gaudi::Property evtMax; - evtMax.assign(appMgr->getProperty("EvtMax")); - m_requestedEventMax = evtMax; - m_requestedEventMax -= m_1stEvtEntry; - - // if run with a fixed number of requested events and we have enough - // in the file we don't need to check if we run out of events - if (m_requestedEventMax > 0 && m_requestedEventMax <= m_numAvailableEvents) { - m_bounds_check_needed = false; - } - - return status; -} -/// Service reinitialisation -StatusCode PodioDataSvc::reinitialize() { - // Do nothing for this service - return StatusCode::SUCCESS; -} -/// Service finalization -StatusCode PodioDataSvc::finalize() { - warning() << "The PodioDataSvc is deprecated and will be removed. Use the IOSvc instead" << endmsg; - m_cnvSvc = nullptr; // release - DataSvc::finalize().ignore(); - return StatusCode::SUCCESS; -} - -StatusCode PodioDataSvc::clearStore() { - // as the frame takes care of the ownership of the podio::Collections, - // make sure the DataWrappers don't cause a double delete - for (auto wrapper : m_podio_datawrappers) { - wrapper->resetData(); - } - m_podio_datawrappers.clear(); - - DataSvc::clearStore().ignore(); - return StatusCode::SUCCESS; -} - -StatusCode PodioDataSvc::i_setRoot(std::string root_path, IOpaqueAddress* pRootAddr) { - // create a new frame - if (m_reading_from_file) { - debug() << "Reading event " << m_eventNum + m_1stEvtEntry << ", using collections: " << m_collsToRead << endmsg; -#if PODIO_BUILD_VERSION <= PODIO_VERSION(1, 2, 0) - if (!m_collsToRead.empty()) { - warning() << "Trying to limit collections that are read, but podio does only support this with version > 1.2" - << endmsg; - } - m_eventframe = podio::Frame(m_reader.readEntry("events", m_eventNum + m_1stEvtEntry)); -#else - m_eventframe = podio::Frame(m_reader.readEntry("events", m_eventNum + m_1stEvtEntry, m_collsToRead)); -#endif - } else { - m_eventframe = podio::Frame(); - } - return DataSvc::i_setRoot(root_path, pRootAddr); -} - -StatusCode PodioDataSvc::i_setRoot(std::string root_path, DataObject* pRootObj) { - // create a new frame - if (m_reading_from_file) { - debug() << "Reading event " << m_eventNum + m_1stEvtEntry << ", using collections: " << m_collsToRead << endmsg; -#if PODIO_BUILD_VERSION <= PODIO_VERSION(1, 2, 0) - if (!m_collsToRead.empty()) { - warning() << "Trying to limit collections that are read, but podio does only support this with version > 1.2" - << endmsg; - } - m_eventframe = podio::Frame(m_reader.readEntry("events", m_eventNum + m_1stEvtEntry)); -#else - m_eventframe = podio::Frame(m_reader.readEntry("events", m_eventNum + m_1stEvtEntry, m_collsToRead)); -#endif - } else { - m_eventframe = podio::Frame(); - } - return DataSvc::i_setRoot(root_path, pRootObj); -} - -void PodioDataSvc::endOfRead() { - m_eventNum++; - - if (!m_bounds_check_needed) { - return; - } - - StatusCode sc; - // m_eventNum already points to the next event here so check if it is available - if (m_eventNum >= m_numAvailableEvents) { - info() << "Reached end of file with event " << m_eventNum << " (" << m_requestedEventMax << " events requested)" - << endmsg; - auto eventProcessor = service("ApplicationMgr", false); - if (!eventProcessor) { - throw std::runtime_error("Could not retrieve ApplicationMgr to schedule a stop"); - } - sc = eventProcessor->stopRun(); - if (sc.isFailure()) { - throw std::runtime_error("Failed to stop the run"); - } - } -} - -/// Standard Constructor -PodioDataSvc::PodioDataSvc(const std::string& name, ISvcLocator* svc) : DataSvc(name, svc) {} - -const std::string_view PodioDataSvc::getCollectionType(const std::string& collName) { - const auto coll = m_eventframe.get(collName); - if (coll == nullptr) { - error() << "Collection " << collName << " does not exist." << endmsg; - return ""; - } - return coll->getTypeName(); -} - -StatusCode PodioDataSvc::registerObject(std::string_view parentPath, std::string_view fullPath, DataObject* pObject) { - auto* wrapper = dynamic_cast(pObject); - if (wrapper != nullptr) { - podio::CollectionBase* coll = wrapper->collectionBase(); - if (coll != nullptr) { - size_t pos = fullPath.find_last_of("/"); - std::string shortPath(fullPath.substr(pos + 1, fullPath.length())); - // Attention: this passes the ownership of the data to the frame - m_eventframe.put(std::unique_ptr(coll), shortPath); - m_podio_datawrappers.push_back(wrapper); - } - } - return DataSvc::registerObject(parentPath, fullPath, pObject); -} diff --git a/test/k4FWCoreTest/CMakeLists.txt b/test/k4FWCoreTest/CMakeLists.txt index 49c3b5daa..9f22acd57 100644 --- a/test/k4FWCoreTest/CMakeLists.txt +++ b/test/k4FWCoreTest/CMakeLists.txt @@ -140,22 +140,11 @@ add_test_fwcore(CheckExampleEventData_unbounded options/checkExampleEventData.py add_test_fwcore(ReadExampleEventData options/readExampleEventData.py) set_property(TEST ReadExampleEventData APPEND PROPERTY FIXTURES_REQUIRED ExampleEventDataFile) add_test_fwcore(ReadExampleDataFromNthEvent options/readExampleDataFromNthEvent.py PROPERTIES FIXTURES_REQUIRED ExampleEventDataFile) -add_test_fwcore(ReadLimitedInputsk4DataSvc options/readLimitedSetOfCollectionsk4DataSvc.py ADD_TO_CHECK_FILES PROPERTIES FIXTURES_REQUIRED ExampleEventDataFile) -add_test_fwcore(ReadLimitedInputsAllEventsk4DataSvc options/readLimitedSetOfCollectionsk4DataSvc.py -n -1 --PodioOutput.filename "output_k4test_exampledata_limited_allevents.root" ADD_TO_CHECK_FILES PROPERTIES FIXTURES_REQUIRED ExampleEventDataFile) -add_test_fwcore(AlgorithmWithTFile options/TestAlgorithmWithTFile.py PROPERTIES FIXTURES_SETUP AlgorithmWithTFileFixture) -set_property(TEST AlgorithmWithTFile PROPERTY WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}) -add_test(NAME AlgorithmWithTFileCheckFrameworkOutput - WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} - COMMAND python scripts/check_TestAlgorithmWithTFile_framework_nonempty.py) -set_property(TEST AlgorithmWithTFileCheckFrameworkOutput APPEND PROPERTY FIXTURES_REQUIRED AlgorithmWithTFileFixture) -add_test(NAME AlgorithmWithTFileCheckMyTFileOutput - WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} - COMMAND python scripts/check_TestAlgorithmWithTFile_myTFile_nonempty.py) -set_property(TEST AlgorithmWithTFileCheckMyTFileOutput APPEND PROPERTY FIXTURES_REQUIRED AlgorithmWithTFileFixture) +add_test_fwcore(AlgorithmWithTFile options/TestAlgorithmWithTFile.py ADD_TO_CHECK_FILES) add_test_fwcore(CreateExampleEventData_cellID options/createExampleEventData_cellID.py ADD_TO_CHECK_FILES) -add_test_fwcore(TwoProducers options/TwoProducers.py --filename output_k4fwcore_test_twoproducer.root +add_test_fwcore(TwoProducers options/TwoProducers.py --magicNumberOffset.Producer2 12345 --Producer1.magicNumberOffset 54321) add_test_fwcore(CheckCommandLineArguments options/createHelloWorld.py --HelloWorldAlg1.PerEventPrintMessage TwasBrilligAndTheSlithyToves PROPERTIES PASS_REGULAR_EXPRESSION "TwasBrilligAndTheSlithyToves" @@ -206,7 +195,6 @@ add_test_fwcore(FunctionalSeveralInputFiles options/ExampleFunctionalSeveralInpu add_test_fwcore(FunctionalMTFile options/ExampleFunctionalMTFile.py PROPERTIES FIXTURES_REQUIRED ProducerMultipleFile ADD_TO_CHECK_FILES) add_test_fwcore(FunctionalMultipleFile options/ExampleFunctionalFileMultiple.py PROPERTIES FIXTURES_REQUIRED ProducerMultipleFile ADD_TO_CHECK_FILES) add_test_fwcore(FunctionalMix options/runFunctionalMix.py PROPERTIES FIXTURES_REQUIRED ProducerMultipleFile ADD_TO_CHECK_FILES) -add_test_fwcore(FunctionalMixIOSvc options/runFunctionalMix.py --iosvc PROPERTIES FIXTURES_REQUIRED ProducerMultipleFile ADD_TO_CHECK_FILES) add_test_fwcore(FunctionalOutputCommands options/ExampleFunctionalOutputCommands.py PROPERTIES FIXTURES_REQUIRED ProducerMultipleFile ADD_TO_CHECK_FILES) add_test_fwcore(FunctionalConsumerRuntimeCollections options/ExampleFunctionalConsumerRuntimeCollections.py) add_test_fwcore(FunctionalConsumerRuntimeCollectionsMultiple options/ExampleFunctionalConsumerRuntimeCollectionsMultiple.py) diff --git a/test/k4FWCoreTest/options/TestAlgorithmWithTFile.py b/test/k4FWCoreTest/options/TestAlgorithmWithTFile.py index b1046bb59..55cdcb9b0 100644 --- a/test/k4FWCoreTest/options/TestAlgorithmWithTFile.py +++ b/test/k4FWCoreTest/options/TestAlgorithmWithTFile.py @@ -16,29 +16,24 @@ # See the License for the specific language governing permissions and # limitations under the License. # -from Gaudi.Configuration import * +from Gaudi.Configuration import INFO -from Configurables import k4DataSvc -from Configurables import PodioOutput -from k4FWCore import ApplicationMgr -from Configurables import k4FWCoreTest_AlgorithmWithTFile - -podioevent = k4DataSvc("EventDataSvc") +from k4FWCore import ApplicationMgr, IOSvc +from Configurables import k4FWCoreTest_AlgorithmWithTFile, EventDataSvc producer = k4FWCoreTest_AlgorithmWithTFile() - -out = PodioOutput("out") -out.filename = "output_TestAlgorithmWithTFile_framework.root" -out.outputCommands = ["keep *"] +iosvc = IOSvc() +iosvc.Output = "output_TestAlgorithmWithTFile_framework.root" +iosvc.outputCommands = ["keep *"] ApplicationMgr( - TopAlg=[producer, out], + TopAlg=[producer], EvtSel="NONE", EvtMax=100, - ExtSvc=[podioevent], + ExtSvc=[EventDataSvc("EventDataSvc")], OutputLevel=INFO, StopOnSignal=True, ) diff --git a/test/k4FWCoreTest/options/TestUniqueIDGenSvc.py b/test/k4FWCoreTest/options/TestUniqueIDGenSvc.py index d0b71ea0e..5396433f0 100644 --- a/test/k4FWCoreTest/options/TestUniqueIDGenSvc.py +++ b/test/k4FWCoreTest/options/TestUniqueIDGenSvc.py @@ -20,7 +20,7 @@ from Configurables import UniqueIDGenSvc from k4FWCore import ApplicationMgr -from Configurables import k4DataSvc +from Configurables import EventDataSvc from Configurables import TestUniqueIDGenSvc uid_svc = UniqueIDGenSvc(Seed=987, CheckDuplicates=True) @@ -30,8 +30,8 @@ ApplicationMgr().OutputLevel = INFO ApplicationMgr().StopOnSignal = True -podioevent = k4DataSvc("EventDataSvc") -ApplicationMgr().ExtSvc += [podioevent] +evtDataSvc = EventDataSvc("EventDataSvc") +ApplicationMgr().ExtSvc += [evtDataSvc] uniqueidtest = TestUniqueIDGenSvc() diff --git a/test/k4FWCoreTest/options/TwoProducers.py b/test/k4FWCoreTest/options/TwoProducers.py index 17e3507c7..001b46bb9 100644 --- a/test/k4FWCoreTest/options/TwoProducers.py +++ b/test/k4FWCoreTest/options/TwoProducers.py @@ -18,10 +18,13 @@ # from Gaudi.Configuration import INFO -from k4FWCore import ApplicationMgr -from Configurables import k4DataSvc +from k4FWCore import ApplicationMgr, IOSvc +from Configurables import EventDataSvc from Configurables import k4FWCoreTest_CreateExampleEventData -from Configurables import PodioOutput + +iosvc = IOSvc() +iosvc.Output = "output_k4test_exampledata_twoproducer.root" +iosvc.outputCommands = ["keep *"] ApplicationMgr( EvtSel="NONE", @@ -31,8 +34,7 @@ ) -podioevent = k4DataSvc("EventDataSvc") -ApplicationMgr().ExtSvc += [podioevent] +ApplicationMgr().ExtSvc += [EventDataSvc("EventDataSvc")] producer1 = k4FWCoreTest_CreateExampleEventData("Producer1") @@ -48,9 +50,3 @@ producer2.recoparticles.Path = "recoparticles2" producer2.links.Path = "links" ApplicationMgr().TopAlg += [producer2] - - -out = PodioOutput("out") -out.filename = "output_k4test_exampledata_twoproducer.root" -out.outputCommands = ["keep *"] -ApplicationMgr().TopAlg += [out] diff --git a/test/k4FWCoreTest/options/checkExampleEventData.py b/test/k4FWCoreTest/options/checkExampleEventData.py index 36572d3ee..fccc65578 100644 --- a/test/k4FWCoreTest/options/checkExampleEventData.py +++ b/test/k4FWCoreTest/options/checkExampleEventData.py @@ -19,14 +19,11 @@ # from Gaudi.Configuration import INFO -from Configurables import k4DataSvc -from Configurables import PodioInput +from Configurables import EventDataSvc from k4FWCore.parseArgs import parser from Configurables import k4FWCoreTest_CheckExampleEventData -from k4FWCore import ApplicationMgr +from k4FWCore import ApplicationMgr, IOSvc -podioevent = k4DataSvc("EventDataSvc") -podioevent.input = "output_k4test_exampledata.root" parser.add_argument( "--collections", @@ -37,17 +34,17 @@ ) my_args = parser.parse_known_args()[0] -inp = PodioInput() -inp.collections = my_args.collections - +iosvc = IOSvc() +iosvc.Input = "output_k4test_exampledata.root" +iosvc.CollectionNames = my_args.collections checker = k4FWCoreTest_CheckExampleEventData() ApplicationMgr( - TopAlg=[inp, checker], + TopAlg=[checker], EvtSel="NONE", EvtMax=100, - ExtSvc=[podioevent], + ExtSvc=[EventDataSvc()], OutputLevel=INFO, StopOnSignal=True, ) diff --git a/test/k4FWCoreTest/options/createEventHeader.py b/test/k4FWCoreTest/options/createEventHeader.py index 20993b978..d3d777546 100644 --- a/test/k4FWCoreTest/options/createEventHeader.py +++ b/test/k4FWCoreTest/options/createEventHeader.py @@ -16,13 +16,10 @@ # See the License for the specific language governing permissions and # limitations under the License. # -from Gaudi.Configuration import * +from Gaudi.Configuration import DEBUG -from Configurables import EventHeaderCreator -from Configurables import k4DataSvc -from Configurables import PodioOutput -from Configurables import ExampleRNGSeedingAlg -from k4FWCore import ApplicationMgr +from Configurables import EventHeaderCreator, ExampleRNGSeedingAlg, EventDataSvc +from k4FWCore import ApplicationMgr, IOSvc eventHeaderCreator = EventHeaderCreator( "eventHeaderCreator", runNumber=42, eventNumberOffset=42, OutputLevel=DEBUG @@ -31,21 +28,14 @@ # algorithm using the header to seed a prng rngAlg = ExampleRNGSeedingAlg("ExampleRNGSeedingAlg") -podioevent = k4DataSvc("EventDataSvc") - - -out = PodioOutput("out") -out.filename = "eventHeader.root" +iosvc = IOSvc() +iosvc.Output = "eventHeader.root" ApplicationMgr( - TopAlg=[ - eventHeaderCreator, - rngAlg, - out, - ], + TopAlg=[eventHeaderCreator, rngAlg], EvtSel="NONE", EvtMax=2, - ExtSvc=[podioevent], + ExtSvc=[EventDataSvc()], StopOnSignal=True, ) diff --git a/test/k4FWCoreTest/options/createExampleEventData.py b/test/k4FWCoreTest/options/createExampleEventData.py index fe4e10fa0..71d10d76f 100644 --- a/test/k4FWCoreTest/options/createExampleEventData.py +++ b/test/k4FWCoreTest/options/createExampleEventData.py @@ -18,27 +18,21 @@ # from Gaudi.Configuration import INFO -from Configurables import k4DataSvc +from Configurables import EventDataSvc from Configurables import k4FWCoreTest_CreateExampleEventData -from Configurables import PodioOutput -from k4FWCore import ApplicationMgr - -podioevent = k4DataSvc("EventDataSvc") +from k4FWCore import ApplicationMgr, IOSvc +iosvc = IOSvc() +iosvc.Output = "output_k4test_exampledata.root" +iosvc.outputCommands = ["keep *"] producer = k4FWCoreTest_CreateExampleEventData() - -out = PodioOutput("out") -out.filename = "output_k4test_exampledata.root" -out.outputCommands = ["keep *"] - - ApplicationMgr( - TopAlg=[producer, out], + TopAlg=[producer], EvtSel="NONE", EvtMax=100, - ExtSvc=[podioevent], + ExtSvc=[EventDataSvc()], OutputLevel=INFO, StopOnSignal=True, ) diff --git a/test/k4FWCoreTest/options/createExampleEventDataInDirectory.py b/test/k4FWCoreTest/options/createExampleEventDataInDirectory.py index 691f55fa1..c679be517 100644 --- a/test/k4FWCoreTest/options/createExampleEventDataInDirectory.py +++ b/test/k4FWCoreTest/options/createExampleEventDataInDirectory.py @@ -17,25 +17,21 @@ # limitations under the License. # from Gaudi.Configuration import INFO -from k4FWCore import ApplicationMgr -from Configurables import k4FWCoreTest_CreateExampleEventData -from Configurables import k4DataSvc -from Configurables import PodioOutput - -podioevent = k4DataSvc("EventDataSvc") +from k4FWCore import ApplicationMgr, IOSvc +from Configurables import k4FWCoreTest_CreateExampleEventData, EventDataSvc producer = k4FWCoreTest_CreateExampleEventData() -out = PodioOutput("out") -out.filename = "output/dir/output_k4test_exampledata.root" -out.outputCommands = ["keep *"] +iosvc = IOSvc() +iosvc.Output = "output/dir/output_k4test_exampledata.root" +iosvc.outputCommands = ["keep *"] ApplicationMgr( - TopAlg=[producer, out], + TopAlg=[producer], EvtSel="NONE", EvtMax=100, - ExtSvc=[podioevent], + ExtSvc=[EventDataSvc()], OutputLevel=INFO, StopOnSignal=True, ) diff --git a/test/k4FWCoreTest/options/createExampleEventData_cellID.py b/test/k4FWCoreTest/options/createExampleEventData_cellID.py index f8c78e047..1186bf0e7 100644 --- a/test/k4FWCoreTest/options/createExampleEventData_cellID.py +++ b/test/k4FWCoreTest/options/createExampleEventData_cellID.py @@ -16,18 +16,18 @@ # See the License for the specific language governing permissions and # limitations under the License. # -from Gaudi.Configuration import INFO +from Gaudi.Configuration import INFO, DEBUG -from Configurables import k4DataSvc from Configurables import ( k4FWCoreTest_cellID_writer, k4FWCoreTest_cellID_reader, MetadataSvc, + EventDataSvc, ) -from Configurables import PodioOutput -from k4FWCore import ApplicationMgr +from k4FWCore import ApplicationMgr, IOSvc -podioevent = k4DataSvc("EventDataSvc") +evtDataSvc = EventDataSvc("EventDataSvc") +evtDataSvc.OutputLevel = DEBUG producer = k4FWCoreTest_cellID_writer( @@ -40,20 +40,19 @@ vectorFloatProp2=[1.1, 2.2, 3.3, 4.4], vectorDoubleProp2=[1.1, 2.2, 3.3, 4.4], vectorStringProp2=["one", "two", "three", "four"], + OutputLevel=DEBUG, ) consumer = k4FWCoreTest_cellID_reader() - -out = PodioOutput("out") -out.filename = "output_k4test_exampledata_cellid.root" -out.outputCommands = ["keep *"] - +iosvc = IOSvc() +iosvc.Output = "output_k4test_exampledata_cellid.root" +iosvc.outputCommands = ["keep *"] ApplicationMgr( - TopAlg=[producer, consumer, out], + TopAlg=[producer, consumer], EvtSel="NONE", EvtMax=10, - ExtSvc=[podioevent, MetadataSvc()], + ExtSvc=[evtDataSvc, MetadataSvc()], OutputLevel=INFO, StopOnSignal=True, ) diff --git a/test/k4FWCoreTest/options/readExampleDataFromNthEvent.py b/test/k4FWCoreTest/options/readExampleDataFromNthEvent.py index 6fd415181..2d1a04411 100644 --- a/test/k4FWCoreTest/options/readExampleDataFromNthEvent.py +++ b/test/k4FWCoreTest/options/readExampleDataFromNthEvent.py @@ -18,28 +18,20 @@ # from Gaudi.Configuration import DEBUG -from Configurables import k4DataSvc -from Configurables import PodioInput -from Configurables import PodioOutput -from k4FWCore import ApplicationMgr +from Configurables import EventDataSvc +from k4FWCore import ApplicationMgr, IOSvc -podioevent = k4DataSvc("EventDataSvc") -podioevent.input = "output_k4test_exampledata.root" -podioevent.FirstEventEntry = 66 - - -inp = PodioInput() -inp.collections = ["MCParticles", "SimTrackerHits", "Tracks"] - - -oup = PodioOutput() -oup.filename = "output_k4test_exampledata_3.root" +iosvc = IOSvc() +iosvc.Input = "output_k4test_exampledata.root" +iosvc.FirstEventEntry = 66 +iosvc.CollectionNames = ["MCParticles", "SimTrackerHits", "Tracks"] +iosvc.Output = "output_k4test_exampledata_3.root" ApplicationMgr( - TopAlg=[inp, oup], + TopAlg=[], EvtSel="NONE", EvtMax=5, - ExtSvc=[podioevent], + ExtSvc=[EventDataSvc()], OutputLevel=DEBUG, ) diff --git a/test/k4FWCoreTest/options/readExampleEventData.py b/test/k4FWCoreTest/options/readExampleEventData.py index 09bdb6df7..21fa4cd7b 100644 --- a/test/k4FWCoreTest/options/readExampleEventData.py +++ b/test/k4FWCoreTest/options/readExampleEventData.py @@ -18,28 +18,20 @@ # from Gaudi.Configuration import DEBUG -from Configurables import k4DataSvc -from Configurables import PodioInput -from Configurables import PodioOutput -from k4FWCore import ApplicationMgr +from Configurables import EventDataSvc +from k4FWCore import ApplicationMgr, IOSvc -podioevent = k4DataSvc("EventDataSvc") -podioevent.input = "output_k4test_exampledata.root" - - -inp = PodioInput() -inp.collections = ["MCParticles", "SimTrackerHits", "TrackerHits", "Tracks"] - - -oup = PodioOutput() -oup.filename = "output_k4test_exampledata_2.root" -oup.outputCommands = ["drop MCParticles"] +iosvc = IOSvc +iosvc.Input = "output_k4test_exampledata.root" +iosvc.Output = "output_k4test_exampledata_2.root" +iosvc.CollectionNames = ["MCParticles", "SimTrackerHits", "TrackerHits", "Tracks"] +iosvc.outputCommands = ["drop MCParticles"] ApplicationMgr( - TopAlg=[inp, oup], + TopAlg=[], EvtSel="NONE", EvtMax=10, - ExtSvc=[podioevent], + ExtSvc=[EventDataSvc()], OutputLevel=DEBUG, ) diff --git a/test/k4FWCoreTest/options/readLimitedSetOfCollectionsk4DataSvc.py b/test/k4FWCoreTest/options/readLimitedSetOfCollectionsk4DataSvc.py deleted file mode 100644 index ea8e66615..000000000 --- a/test/k4FWCoreTest/options/readLimitedSetOfCollectionsk4DataSvc.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2014-2024 Key4hep-Project. -# -# This file is part of Key4hep. -# See https://key4hep.github.io/key4hep-doc/ for further info. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from Gaudi.Configuration import DEBUG -from Configurables import k4DataSvc -from Configurables import PodioInput -from Configurables import PodioOutput -from k4FWCore import ApplicationMgr - -podioevent = k4DataSvc("EventDataSvc") -podioevent.input = "output_k4test_exampledata.root" -podioevent.OutputLevel = DEBUG - -inp = PodioInput() -inp.collections = ["MCParticles", "Links"] - -output = PodioOutput() -output.filename = "output_k4test_exampledata_limited.root" - -ApplicationMgr( - TopAlg=[inp, output], EvtSel="NONE", EvtMax=5, ExtSvc=[podioevent], OutputLevel=DEBUG -) diff --git a/test/k4FWCoreTest/options/runFunctionalMix.py b/test/k4FWCoreTest/options/runFunctionalMix.py index 7e995c3e3..747c7ac77 100644 --- a/test/k4FWCoreTest/options/runFunctionalMix.py +++ b/test/k4FWCoreTest/options/runFunctionalMix.py @@ -30,47 +30,11 @@ k4FWCoreTest_CreateExampleEventData, ) from Configurables import k4FWCoreTest_CheckExampleEventData -from k4FWCore import ApplicationMgr -from Configurables import k4DataSvc -from Configurables import PodioInput, PodioOutput -from k4FWCore.parseArgs import parser - -parser.add_argument( - "--iosvc", - help="Use the IOSvc instead of PodioInput and PodioOutput", - action="store_true", - default=False, -) -args = parser.parse_known_args()[0] - -print(args.iosvc) - -if not args.iosvc: - podioevent = k4DataSvc("EventDataSvc") - podioevent.input = "functional_producer_multiple.root" - - inp = PodioInput() - inp.collections = [ - "VectorFloat", - "MCParticles1", - "MCParticles2", - "SimTrackerHits", - "TrackerHits", - "Tracks", - "RecoParticles", - "Links", - ] - - out = PodioOutput() - out.filename = "functional_mix.root" - out.outputCommands = ["keep *"] - -else: - from k4FWCore import IOSvc, ApplicationMgr +from k4FWCore import ApplicationMgr, IOSvc - iosvc = IOSvc("IOSvc") - iosvc.Input = "functional_producer_multiple.root" - iosvc.Output = "functional_mix_iosvc.root" +iosvc = IOSvc("IOSvc") +iosvc.Input = "functional_producer_multiple.root" +iosvc.Output = "functional_mix_iosvc.root" # Check input with functional and old algorithms @@ -153,8 +117,7 @@ ApplicationMgr( - TopAlg=([inp] if not args.iosvc else []) - + [ + TopAlg=[ # Check we can read input consumer_input_functional, consumer_input_algorithm, @@ -167,10 +130,9 @@ consumer_produceralg_functional, consumer_produceralg_algorithm, transformer_functional, - ] - + ([out] if not args.iosvc else []), + ], EvtSel="NONE", EvtMax=10, - ExtSvc=[iosvc if args.iosvc else podioevent], + ExtSvc=[iosvc], OutputLevel=INFO, ) diff --git a/test/k4FWCoreTest/scripts/CheckOutputFiles.py b/test/k4FWCoreTest/scripts/CheckOutputFiles.py index b719d7ca5..a24664169 100644 --- a/test/k4FWCoreTest/scripts/CheckOutputFiles.py +++ b/test/k4FWCoreTest/scripts/CheckOutputFiles.py @@ -125,10 +125,6 @@ def check_metadata(filename, expected_metadata): check_collections("functional_limited_input.root", ["MCParticles", "Links"]) check_collections("functional_limited_input_all_events.root", ["MCParticles", "Links"]) -if podio.version.build_version > podio.version.parse("1.2.0"): - check_collections("output_k4test_exampledata_limited.root", ["MCParticles", "Links"]) - check_collections("output_k4test_exampledata_limited_allevents.root", ["MCParticles", "Links"]) - mix_collections = [ # From file "VectorFloat", @@ -434,3 +430,14 @@ def check_metadata(filename, expected_metadata): "ToolFinalizeParam": 42, }, ) + +check_events("output_TestAlgorithmWithTFile_framework.root", 100) + +f_tfile = ROOT.TFile.Open("output_TestAlgorithmWithTFile_myTFile.root") +mytree = f_tfile.Get("mytree") +if mytree is None: + raise RuntimeError("output_TestAlgorithmWithTFile_myTFile.root has no TTree named mytree") +if mytree.GetEntries() == 0: + raise RuntimeError( + "output_TestAlgorithmWithTFile_myTFile.root contains TTree mytree with no entries" + ) diff --git a/test/k4FWCoreTest/scripts/check_TestAlgorithmWithTFile_framework_nonempty.py b/test/k4FWCoreTest/scripts/check_TestAlgorithmWithTFile_framework_nonempty.py deleted file mode 100644 index 0aa70dfb7..000000000 --- a/test/k4FWCoreTest/scripts/check_TestAlgorithmWithTFile_framework_nonempty.py +++ /dev/null @@ -1,27 +0,0 @@ -# -# Copyright (c) 2014-2024 Key4hep-Project. -# -# This file is part of Key4hep. -# See https://key4hep.github.io/key4hep-doc/ for further info. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -import ROOT - -f = ROOT.TFile.Open("output_TestAlgorithmWithTFile_framework.root") -t = f.Get("events") -print( - "File: output_TestAlgorithmWithTFile_framework.root contains TTree events with " - + str(t.GetEntries()) - + " entries." -) diff --git a/test/k4FWCoreTest/scripts/check_TestAlgorithmWithTFile_myTFile_nonempty.py b/test/k4FWCoreTest/scripts/check_TestAlgorithmWithTFile_myTFile_nonempty.py deleted file mode 100644 index 82b990338..000000000 --- a/test/k4FWCoreTest/scripts/check_TestAlgorithmWithTFile_myTFile_nonempty.py +++ /dev/null @@ -1,27 +0,0 @@ -# -# Copyright (c) 2014-2024 Key4hep-Project. -# -# This file is part of Key4hep. -# See https://key4hep.github.io/key4hep-doc/ for further info. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -import ROOT - -f = ROOT.TFile.Open("output_TestAlgorithmWithTFile_myTFile.root") -t = f.Get("mytree") -print( - "File: output_TestAlgorithmWithTFile_myTFile.root contains TTree mytree with " - + str(t.GetEntries()) - + " entries." -) diff --git a/test/k4FWCoreTest/src/components/k4FWCoreTest_AlgorithmWithTFile.h b/test/k4FWCoreTest/src/components/k4FWCoreTest_AlgorithmWithTFile.h index 49404ab4f..455ff866e 100644 --- a/test/k4FWCoreTest/src/components/k4FWCoreTest_AlgorithmWithTFile.h +++ b/test/k4FWCoreTest/src/components/k4FWCoreTest_AlgorithmWithTFile.h @@ -38,7 +38,7 @@ class SimCaloHit; /** @class k4FWCoreTest_AlgorithmWithTFile * Test producer to check that data can still be written to - * a user-declared TFile when using the PodioDataSvc + * a user-declared TFile * */ class k4FWCoreTest_AlgorithmWithTFile : public Gaudi::Algorithm { From 24cbdb385a14a6b4211d0f37bce46187e9c7d3ed Mon Sep 17 00:00:00 2001 From: Juan Miguel Carceller Date: Sun, 31 May 2026 22:26:43 +0200 Subject: [PATCH 20/36] Add Ubuntu 26 builds in CI --- .github/workflows/key4hep-build.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/key4hep-build.yaml b/.github/workflows/key4hep-build.yaml index be4aa20a7..110b39409 100644 --- a/.github/workflows/key4hep-build.yaml +++ b/.github/workflows/key4hep-build.yaml @@ -25,10 +25,12 @@ jobs: include: - build_type: nightly image: ubuntu24 + - build_type: nightly + image: ubuntu26 fail-fast: false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6 - uses: key4hep/key4hep-actions/cache-external-data@main - uses: key4hep/key4hep-actions/key4hep-build@main with: From fc6cee5519ada2b66f86e49829a270462926e124 Mon Sep 17 00:00:00 2001 From: Juan Miguel Carceller <22276694+jmcarcell@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:50:32 +0200 Subject: [PATCH 21/36] Update k4FWCore after DataHandleMixin has been deprecated (#410) --- k4FWCore/include/k4FWCore/Consumer.h | 13 ++++++++++--- k4FWCore/include/k4FWCore/Transformer.h | 19 +++++++++++++------ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/k4FWCore/include/k4FWCore/Consumer.h b/k4FWCore/include/k4FWCore/Consumer.h index c5c0a9381..c892fa306 100644 --- a/k4FWCore/include/k4FWCore/Consumer.h +++ b/k4FWCore/include/k4FWCore/Consumer.h @@ -19,6 +19,7 @@ #ifndef FWCORE_CONSUMER_H #define FWCORE_CONSUMER_H +#include "GAUDI_VERSION.h" #include "Gaudi/Functional/details.h" #include "Gaudi/Functional/utilities.h" #include "GaudiKernel/FunctionalFilterDecision.h" @@ -37,13 +38,19 @@ namespace k4FWCore { namespace details { +#if GAUDI_MAJOR_VERSION >= 41 + using EmptyTypeList = Gaudi::Functional::details::type_list<>; +#else + using EmptyTypeList = std::tuple<>; +#endif + template struct Consumer; template struct Consumer - : Gaudi::Functional::details::DataHandleMixin, std::tuple<>, Traits_> { - using Gaudi::Functional::details::DataHandleMixin, std::tuple<>, Traits_>::DataHandleMixin; + : Gaudi::Functional::details::DataHandleMixin { + using Gaudi::Functional::details::DataHandleMixin::DataHandleMixin; static_assert(((std::is_base_of_v || isVectorLike_v || std::is_same_v) && @@ -52,7 +59,7 @@ namespace details { static constexpr size_t N_in = filter_evtcontext::size; - using base_class = Gaudi::Functional::details::DataHandleMixin, std::tuple<>, Traits_>; + using base_class = Gaudi::Functional::details::DataHandleMixin; using KeyValue = base_class::KeyValue; using KeyValues = base_class::KeyValues; diff --git a/k4FWCore/include/k4FWCore/Transformer.h b/k4FWCore/include/k4FWCore/Transformer.h index bb31bcc29..1afe24e8f 100644 --- a/k4FWCore/include/k4FWCore/Transformer.h +++ b/k4FWCore/include/k4FWCore/Transformer.h @@ -19,6 +19,7 @@ #ifndef FWCORE_TRANSFORMER_H #define FWCORE_TRANSFORMER_H +#include "GAUDI_VERSION.h" #include "Gaudi/Functional/details.h" #include "Gaudi/Functional/utilities.h" @@ -36,13 +37,19 @@ namespace k4FWCore { namespace details { +#if GAUDI_MAJOR_VERSION >= 41 + using EmptyTypeList = Gaudi::Functional::details::type_list<>; +#else + using EmptyTypeList = std::tuple<>; +#endif + template struct Transformer; template struct Transformer - : Gaudi::Functional::details::DataHandleMixin, std::tuple<>, Traits_> { - using Gaudi::Functional::details::DataHandleMixin, std::tuple<>, Traits_>::DataHandleMixin; + : Gaudi::Functional::details::DataHandleMixin { + using Gaudi::Functional::details::DataHandleMixin::DataHandleMixin; static_assert(((std::is_base_of_v || isVectorLike_v || std::is_same_v) && @@ -55,7 +62,7 @@ namespace details { static constexpr std::size_t N_in = filter_evtcontext::size; static constexpr std::size_t N_out = 1; - using base_class = Gaudi::Functional::details::DataHandleMixin, std::tuple<>, Traits_>; + using base_class = Gaudi::Functional::details::DataHandleMixin; using KeyValue = base_class::KeyValue; using KeyValues = base_class::KeyValues; @@ -201,8 +208,8 @@ namespace details { template struct MultiTransformer(const In&...), Traits_> - : Gaudi::Functional::details::DataHandleMixin, std::tuple<>, Traits_> { - using Gaudi::Functional::details::DataHandleMixin, std::tuple<>, Traits_>::DataHandleMixin; + : Gaudi::Functional::details::DataHandleMixin { + using Gaudi::Functional::details::DataHandleMixin::DataHandleMixin; static_assert(((std::is_base_of_v || isVectorLike::value || std::is_same_v) && @@ -214,7 +221,7 @@ namespace details { static constexpr std::size_t N_in = filter_evtcontext::size; static constexpr std::size_t N_out = sizeof...(Out); - using base_class = Gaudi::Functional::details::DataHandleMixin, std::tuple<>, Traits_>; + using base_class = Gaudi::Functional::details::DataHandleMixin; using KeyValue = base_class::KeyValue; using KeyValues = base_class::KeyValues; From 26ea221e225111ed8abeb24830ed4338f442641b Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Fri, 5 Jun 2026 23:03:36 +0200 Subject: [PATCH 22/36] Update k4FWCore helpers and documentation --- k4FWCore/helpers/README.md | 8 +++++--- k4FWCore/helpers/gaudi_gen.py | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/k4FWCore/helpers/README.md b/k4FWCore/helpers/README.md index 619a8bb66..90ad7b572 100644 --- a/k4FWCore/helpers/README.md +++ b/k4FWCore/helpers/README.md @@ -37,7 +37,7 @@ chmod +x gaudi_gen.py ./gaudi_gen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' # 3. Plain Python (you must have jinja2 installed in the active env). -uv run gaudi_gen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' +python3 gaudi_gen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' ``` The first two routes are self-contained: nothing needs to be installed in @@ -226,7 +226,7 @@ Returns `bool`. - Framework header (`k4FWCore/.h` or `Gaudi/Functional/.h`). For k4FWCore, `MultiTransformer` is included from `Transformer.h`. - Auto-detected `edm4hep/.h` headers and `podio/UserDataCollection.h` - when applicable. + when applicable (podio headers are available transitively through `k4FWCore`). - An optional `using BaseClass_t = ...;` for native Gaudi. - Optional `using retType = std::tuple<...>;` (k4FWCore multi-output). - Optional `using XxxColl = ...;` aliases (`--type-aliases`). @@ -239,7 +239,9 @@ Returns `bool`. - `find_package(k4FWCore REQUIRED)` or `find_package(Gaudi REQUIRED)`. - `find_package(EDM4HEP REQUIRED)` if any collection type is from `edm4hep`. -- `find_package(podio REQUIRED)` if `podio::UserDataCollection` is used. +- `find_package(podio REQUIRED)` if `podio::UserDataCollection` is used with + `--framework gaudi` (for k4fwcore, podio is a transitive dependency of + `k4FWCore::k4FWCore` and no explicit find is needed). - `gaudi_add_module(Plugin SOURCES .cpp LINK ...)` with the matching link libraries. diff --git a/k4FWCore/helpers/gaudi_gen.py b/k4FWCore/helpers/gaudi_gen.py index 2f4bac554..a81a61d40 100644 --- a/k4FWCore/helpers/gaudi_gen.py +++ b/k4FWCore/helpers/gaudi_gen.py @@ -671,7 +671,9 @@ def _build_cmake_context(spec: AlgorithmSpec) -> dict: if has_edm4hep: find_packages.append("find_package(EDM4HEP REQUIRED)") link_libs.append("EDM4HEP::edm4hep") - if has_podio: + if has_podio and not spec.is_k4: + # For k4fwcore, podio is a transitive dependency of k4FWCore::k4FWCore + # (declared in k4FWCoreConfig.cmake.in), so no explicit find/link needed. find_packages.append("find_package(podio REQUIRED)") link_libs.append("podio::podio") return {"find_packages": find_packages, "link_libs": link_libs} From 91797bdf6ff653c75c632cc4c5e00ad46525c9d4 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Tue, 9 Jun 2026 13:53:40 +0200 Subject: [PATCH 23/36] final checks and updates --- README.md | 48 +++++++++ .../helpers/{gaudi_gen.py => gaudiGen.py} | 102 ++++++++++++------ 2 files changed, 116 insertions(+), 34 deletions(-) rename k4FWCore/helpers/{gaudi_gen.py => gaudiGen.py} (92%) diff --git a/README.md b/README.md index 786108890..f1c9eafff 100644 --- a/README.md +++ b/README.md @@ -118,3 +118,51 @@ each one of the above-mentioned algorithms. In addition, there are tests that have either multiple inputs and / or multiple outputs (like `ExampleFunctionalProducerMultiple`) that can be used as a template for the more typical case when working with multiple inputs or outputs. + +### Generating boilerplate with gaudiGen.py + +`k4FWCore/helpers/gaudiGen.py` is a code generator that produces the C++ +boilerplate for a new functional algorithm. The functional type (Consumer, +Producer, Transformer, MultiTransformer, FilterPredicate) is inferred +automatically from the number of inputs and outputs, or can be set explicitly. + +Requirements: Python ≥ 3.9 and [jinja2](https://pypi.org/project/Jinja2/). +With [uv](https://github.com/astral-sh/uv) installed, dependencies are +resolved automatically via the PEP 723 script block. + +```bash +# Producer with one output collection and one property +python3 k4FWCore/helpers/gaudiGen.py MyProducer \ + -o 'edm4hep::MCParticleCollection:OutputCollection' \ + -p 'int:ExampleInt:3:An example integer property' + +# Transformer (inferred from 1 input + 1 output) +python3 k4FWCore/helpers/gaudiGen.py MyTransformer \ + -i 'edm4hep::MCParticleCollection:InputCollection' \ + -o 'edm4hep::MCParticleCollection:OutputCollection' \ + --private-properties + +# MultiTransformer with type aliases +python3 k4FWCore/helpers/gaudiGen.py MyMulti \ + -i 'edm4hep::MCParticleCollection:Particles' \ + 'edm4hep::TrackCollection:Tracks' \ + -o 'edm4hep::MCParticleCollection:NewParticles' \ + 'podio::UserDataCollection:Counter' \ + --type-aliases + +# FilterPredicate (type must be specified explicitly) +python3 k4FWCore/helpers/gaudiGen.py MyFilter filter \ + -i 'edm4hep::MCParticleCollection:InputCollection' + +# Consumer with runtime (variable-length) input collections +python3 k4FWCore/helpers/gaudiGen.py MyConsumer \ + -i 'edm4hep::MCParticleCollection:Inputs' \ + --runtime-inputs 'edm4hep::MCParticleCollection:Inputs:MCParticles0,MCParticles1' + +# Also emit a CMakeLists.txt skeleton +python3 k4FWCore/helpers/gaudiGen.py MyProducer \ + -o 'edm4hep::MCParticleCollection:OutputCollection' \ + --cmake +``` + +Run `python3 k4FWCore/helpers/gaudiGen.py --help` for the full list of options. diff --git a/k4FWCore/helpers/gaudi_gen.py b/k4FWCore/helpers/gaudiGen.py similarity index 92% rename from k4FWCore/helpers/gaudi_gen.py rename to k4FWCore/helpers/gaudiGen.py index a81a61d40..5789c232f 100644 --- a/k4FWCore/helpers/gaudi_gen.py +++ b/k4FWCore/helpers/gaudiGen.py @@ -11,9 +11,9 @@ k4FWCore and native Gaudi::Functional frameworks. Run with either: - uv run gaudi_gen.py [args...] # uv resolves deps from the PEP 723 block - ./gaudi_gen.py [args...] # uses the shebang (requires uv on PATH) - python3 gaudi_gen.py [args...] # plain Python; needs jinja2 installed + uv run gaudiGen.py [args...] # uv resolves deps from the PEP 723 block + ./gaudiGen.py [args...] # uses the shebang (requires uv on PATH) + python3 gaudiGen.py [args...] # plain Python; needs jinja2 installed """ import argparse import os @@ -89,10 +89,14 @@ def parse(cls, spec: str, is_vector: bool = False) -> "DataSpec": # Derived properties used in templates ----------------------------------- @property def edm4hep_header(self) -> Optional[str]: - """Return the edm4hep header filename for this type, or None.""" + """Return the edm4hep header filename for this type, or None. + + Bug fix: keep 'Collection' in the filename. + edm4hep::MCParticleCollection -> edm4hep/MCParticleCollection.h + """ m = re.search(r"edm4hep::(\w+Collection)", self.type_name) if m: - return re.sub(r"Collection$", "", m.group(1)) + ".h" + return m.group(1) + ".h" return None @property @@ -164,8 +168,15 @@ def parse(cls, spec: str) -> "PropertySpec": @property def member_name(self) -> str: + """Return the C++ member variable name. + + Bug fix: lowercase the first character of the name after the 'm_' prefix + so that e.g. 'Offset' -> 'm_offset', not 'm_Offset'. + """ n = self.name - return n if n.startswith("m_") else f"m_{n}" + if n.startswith("m_"): + return n + return f"m_{n[0].lower()}{n[1:]}" @dataclass @@ -444,23 +455,26 @@ def _build_spec(args: argparse.Namespace) -> AlgorithmSpec: {{ op_body }} } -{% if spec.properties %} -{% if spec.private_props %} +{% if spec.event_context %} + StatusCode finalize() override { + // TODO: finalise event-context state + return StatusCode::SUCCESS; + } + +{% endif %} +{% if spec.properties or spec.event_context %} +{% if spec.private_props or spec.event_context %} private: {% endif %} {% for prop in spec.properties %} Gaudi::Property<{{ prop.type_name }}> {{ prop.member_name }}{ this, "{{ prop.name }}", {{ prop.default }}{{ ', "' + prop.description + '"' if prop.description else '' }}}; {% endfor %} -{% endif %} {% if spec.event_context %} - StatusCode finalize() override { - // TODO: finalise event-context state - return StatusCode::SUCCESS; - } mutable std::set m_eventNumbersSeen{}; mutable std::mutex m_mutex{}; {% endif %} +{% endif %} }; {% if spec.namespace %} } // namespace {{ spec.namespace }} @@ -528,6 +542,7 @@ def _build_constructor(spec: AlgorithmSpec) -> str: cls = spec.class_name base = spec.base_short rd = spec.runtime_defaults + ft = spec.functional_type def _kv(ds: DataSpec) -> str: if ds.is_vector: @@ -535,22 +550,42 @@ def _kv(ds: DataSpec) -> str: return f'KeyValues("{ds.key}", {{{defs_str}}})' return f'KeyValue("{ds.key}", "{ds.key}")' - # Input block - if not spec.inputs: - in_block = "{}" - elif len(spec.inputs) == 1: - in_block = _kv(spec.inputs[0]) - else: - ind = " " * 20 - items = (",\n" + ind).join(_kv(inp) for inp in spec.inputs) - in_block = "{\n" + ind + items + ",\n" + " " * 16 + "}" + def _brace_block(items: list, indent: int = 20, base_indent: int = 16) -> str: + """Build a brace-wrapped list of KeyValues, always including the braces. - ft = spec.functional_type + Bug fix: single-item blocks are now emitted as '{KeyValue(...)}' rather + than bare 'KeyValue(...)'. Consumer/filter pass through _bare_block + instead and are unaffected. + """ + if not items: + return "{}" + if len(items) == 1: + return "{" + _kv(items[0]) + "}" + ind = " " * indent + body = (",\n" + ind).join(_kv(it) for it in items) + return "{\n" + ind + body + ",\n" + " " * base_indent + "}" + + def _bare_block(items: list) -> str: + """For consumer/filter: single collection bare, multiple in braces.""" + if not items: + return "{}" + if len(items) == 1: + return _kv(items[0]) + ind = " " * 20 + body = (",\n" + ind).join(_kv(it) for it in items) + return "{\n" + ind + body + ",\n" + " " * 16 + "}" + + # consumer / filter: inputs are passed bare (no outer braces for single) if ft in ("consumer", "filter"): + in_block = _bare_block(spec.inputs) return ( f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" f" : {base}(name, svcLoc, {in_block}) {{}}" ) + + # producer / transformer / multitransformer: always use brace-wrapped blocks + in_block = _brace_block(spec.inputs) + if spec.is_runtime: out_key = spec.runtime_output.key out_block = f'{{KeyValues("OutputCollections", {{"{out_key}"}})}}' @@ -563,20 +598,19 @@ def _kv(ds: DataSpec) -> str: f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" f" : {base}(name, svcLoc, {in_block}, {{}}) {{}}" ) + + out_block = _brace_block(spec.outputs, indent=17, base_indent=17) + if len(spec.outputs) == 1: - out_block = _kv(spec.outputs[0]) return ( f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" f" : {base}(name, svcLoc, {in_block}, {out_block}) {{}}" ) - # Multiple fixed outputs — brace-list of KeyValues - ind2 = " " * 17 - items = (",\n" + ind2).join(_kv(out) for out in spec.outputs) + # Multiple fixed outputs return ( f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" f" : {base}(name, svcLoc, {in_block},\n" - f" {{\n" - f" {items}}}) {{}}" + f" {out_block}) {{}}" ) @@ -743,7 +777,7 @@ def _safe_write(path: str, content: str, force: bool, label: str) -> bool: # --------------------------------------------------------------------------- def _build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( - prog="gaudi_gen.py", + prog="gaudiGen.py", description="Generate Gaudi Functional C++ algorithm boilerplate.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=textwrap.dedent("""\ @@ -756,20 +790,20 @@ def _build_parser() -> argparse.ArgumentParser: Examples: # k4FWCore producer (type inferred) - gaudi_gen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' + gaudiGen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' # k4FWCore multi-output producer with properties - gaudi_gen.py MyProducer \\ + gaudiGen.py MyProducer \\ -o 'edm4hep::MCParticleCollection:MCParticles' \\ 'edm4hep::TrackCollection:Tracks' \\ -p 'int:ExampleInt:3:An example integer property' # Gaudi transformer wrapped in 'namespace MyNamespace { ... }' (type inferred) - gaudi_gen.py MySum -i 'Input1:Loc1' 'Input2:Loc2' -o 'Output:OutLoc' \\ + gaudiGen.py MySum -i 'Input1:Loc1' 'Input2:Loc2' -o 'Output:OutLoc' \\ --framework gaudi -n MyNamespace # Variable-length inputs (k4FWCore only) - gaudi_gen.py MyVarConsumer \\ + gaudiGen.py MyVarConsumer \\ -i 'edm4hep::MCParticleCollection:Inputs' \\ --runtime-inputs 'edm4hep::MCParticleCollection:Inputs:MCParticles0,MCParticles1' """), From bf2aa6d0631867dc33cfe19d3126269043c8a393 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Tue, 9 Jun 2026 14:06:05 +0200 Subject: [PATCH 24/36] update the script name --- k4FWCore/helpers/README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/k4FWCore/helpers/README.md b/k4FWCore/helpers/README.md index 90ad7b572..15f9b7508 100644 --- a/k4FWCore/helpers/README.md +++ b/k4FWCore/helpers/README.md @@ -1,6 +1,6 @@ -# gaudi_gen.py — Gaudi Functional C++ Class Generator +# gaudiGen.py — Gaudi Functional C++ Class Generator -`gaudi_gen.py` writes the boilerplate for a Gaudi Functional algorithm: the +`gaudiGen.py` writes the boilerplate for a Gaudi Functional algorithm: the `#include`s, the constructor with `KeyValue` / `KeyValues` wiring, the `operator()` signature, a placeholder body, optional `Gaudi::Property` members, and (optionally) a matching `CMakeLists.txt`. It supports both the @@ -30,14 +30,14 @@ There are three equivalent ways to invoke the script: ```bash # 1. Recommended — uv resolves Python and Jinja2 from the PEP 723 block. -uv run gaudi_gen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' +uv run gaudiGen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' # 2. Direct execution via the shebang (requires uv on PATH). -chmod +x gaudi_gen.py -./gaudi_gen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' +chmod +x gaudiGen.py +./gaudiGen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' # 3. Plain Python (you must have jinja2 installed in the active env). -python3 gaudi_gen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' +python3 gaudiGen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' ``` The first two routes are self-contained: nothing needs to be installed in @@ -49,7 +49,7 @@ the system or active Python environment beyond `uv` itself. ```bash # k4FWCore producer (functional type inferred from --outputs) -uv run gaudi_gen.py MyProducer \ +uv run gaudiGen.py MyProducer \ -o 'edm4hep::MCParticleCollection:MCParticles' ``` @@ -57,7 +57,7 @@ That writes `MyProducer.cpp` in the current directory. Add `--cmake` to also emit a `CMakeLists.txt`: ```bash -uv run gaudi_gen.py MyProducer \ +uv run gaudiGen.py MyProducer \ -o 'edm4hep::MCParticleCollection:MCParticles' \ --cmake ``` @@ -66,7 +66,7 @@ uv run gaudi_gen.py MyProducer \ ## File-overwrite policy -`gaudi_gen.py` **never silently overwrites an existing file**. If the target +`gaudiGen.py` **never silently overwrites an existing file**. If the target `.cpp` or `CMakeLists.txt` already exists, the script prints a diagnostic and exits non-zero: @@ -157,7 +157,7 @@ auto-promotes to `multitransformer`. ### Producer with multiple outputs and a property ```bash -uv run gaudi_gen.py MyProducer \ +uv run gaudiGen.py MyProducer \ -o 'edm4hep::MCParticleCollection:MCParticles' \ 'edm4hep::TrackCollection:Tracks' \ -p 'int:ExampleInt:3:An example integer property' @@ -168,7 +168,7 @@ The output uses a `retType = std::tuple<...>` alias for readability. ### Native Gaudi transformer wrapped in a C++ namespace ```bash -uv run gaudi_gen.py MySum \ +uv run gaudiGen.py MySum \ -i 'Input1:Loc1' 'Input2:Loc2' \ -o 'Output:OutLoc' \ --framework gaudi \ @@ -191,7 +191,7 @@ struct MySum final : Gaudi::Functional::Transformer.h` or `Gaudi/Functional/.h`). For k4FWCore, `MultiTransformer` is included from `Transformer.h`. -- Auto-detected `edm4hep/.h` headers and `podio/UserDataCollection.h` +- Auto-detected `edm4hep/.h` headers and `podio/UserDataCollection.h` when applicable (podio headers are available transitively through `k4FWCore`). - An optional `using BaseClass_t = ...;` for native Gaudi. - Optional `using retType = std::tuple<...>;` (k4FWCore multi-output). From a45ff021e4fed6346f3ecd898eff9166070556e7 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Tue, 9 Jun 2026 15:38:10 +0200 Subject: [PATCH 25/36] add tests --- k4FWCore/CMakeLists.txt | 2 + k4FWCore/helpers/tests/_test_common.sh | 40 +++++++++++++++++++ k4FWCore/helpers/tests/run_all_tests.sh | 39 ++++++++++++++++++ k4FWCore/helpers/tests/test_consumer.sh | 9 +++++ k4FWCore/helpers/tests/test_event_context.sh | 12 ++++++ k4FWCore/helpers/tests/test_filter.sh | 8 ++++ .../helpers/tests/test_gaudi_framework.sh | 12 ++++++ .../helpers/tests/test_multitransformer.sh | 13 ++++++ k4FWCore/helpers/tests/test_producer.sh | 9 +++++ .../helpers/tests/test_runtime_consumer.sh | 11 +++++ .../helpers/tests/test_runtime_transformer.sh | 12 ++++++ k4FWCore/helpers/tests/test_transformer.sh | 11 +++++ 12 files changed, 178 insertions(+) create mode 100644 k4FWCore/helpers/tests/_test_common.sh create mode 100644 k4FWCore/helpers/tests/run_all_tests.sh create mode 100644 k4FWCore/helpers/tests/test_consumer.sh create mode 100644 k4FWCore/helpers/tests/test_event_context.sh create mode 100644 k4FWCore/helpers/tests/test_filter.sh create mode 100644 k4FWCore/helpers/tests/test_gaudi_framework.sh create mode 100644 k4FWCore/helpers/tests/test_multitransformer.sh create mode 100644 k4FWCore/helpers/tests/test_producer.sh create mode 100644 k4FWCore/helpers/tests/test_runtime_consumer.sh create mode 100644 k4FWCore/helpers/tests/test_runtime_transformer.sh create mode 100644 k4FWCore/helpers/tests/test_transformer.sh diff --git a/k4FWCore/CMakeLists.txt b/k4FWCore/CMakeLists.txt index d19c08e53..faf0ce710 100644 --- a/k4FWCore/CMakeLists.txt +++ b/k4FWCore/CMakeLists.txt @@ -18,6 +18,8 @@ limitations under the License. ]] gaudi_install(SCRIPTS) +install(PROGRAMS helpers/gaudiGen.py + DESTINATION ${CMAKE_INSTALL_BINDIR}) gaudi_add_library(k4FWCore SOURCES src/KeepDropSwitch.cpp diff --git a/k4FWCore/helpers/tests/_test_common.sh b/k4FWCore/helpers/tests/_test_common.sh new file mode 100644 index 000000000..628b637f1 --- /dev/null +++ b/k4FWCore/helpers/tests/_test_common.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# _test_common.sh — sourced by every test_*.sh script. +# Provides: GENERATOR path, SANDBOX temp dir, and run_cmake_build(). +# +# Usage in a test script: +# source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" +# run_cmake_build ClassName [gaudiGen.py args...] + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Prefer the installed command; fall back to the source copy one level up. +if command -v gaudiGen.py &>/dev/null; then + GENERATOR="$(command -v gaudiGen.py)" +else + GENERATOR="${SCRIPT_DIR}/../gaudiGen.py" +fi + +if [[ ! -f "${GENERATOR}" ]]; then + echo "ERROR: gaudiGen.py not found (tried PATH and ${GENERATOR})" >&2 + exit 1 +fi + +SANDBOX="$(mktemp -d)" +trap 'rm -rf "${SANDBOX}"' EXIT + +# run_cmake_build [gaudiGen.py args...] +# 1. Generates .cpp + CMakeLists.txt via gaudiGen.py --cmake +# 2. Configures with cmake +# 3. Builds with cmake --build +run_cmake_build() { + local class="$1"; shift + ( + cd "${SANDBOX}" + python3 "${GENERATOR}" "${class}" "$@" --cmake --force + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release + cmake --build build + ) +} diff --git a/k4FWCore/helpers/tests/run_all_tests.sh b/k4FWCore/helpers/tests/run_all_tests.sh new file mode 100644 index 000000000..e48bda16e --- /dev/null +++ b/k4FWCore/helpers/tests/run_all_tests.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# run_all_tests.sh — run every test_*.sh script and report results. +# Requires a Key4hep / k4FWCore environment (k4FWCore, EDM4HEP, Gaudi on +# CMAKE_PREFIX_PATH). Source the Key4hep setup script before running: +# +# source /cvmfs/sw.hsf.org/key4hep/setup.sh +# bash k4FWCore/helpers/tests/run_all_tests.sh + +set -uo pipefail + +TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PASS=0 +FAIL=0 +FAILED_TESTS=() + +for test_script in "${TESTS_DIR}"/test_*.sh; do + name="$(basename "${test_script}" .sh)" + printf " %-35s" "${name} ..." + if output="$(bash "${test_script}" 2>&1)"; then + echo "PASS" + PASS=$((PASS + 1)) + else + echo "FAIL" + echo "${output}" | sed 's/^/ /' + FAIL=$((FAIL + 1)) + FAILED_TESTS+=("${name}") + fi +done + +echo "" +echo "Results: ${PASS} passed, ${FAIL} failed" + +if [[ ${FAIL} -gt 0 ]]; then + echo "Failed tests:" + for t in "${FAILED_TESTS[@]}"; do + echo " - ${t}" + done + exit 1 +fi diff --git a/k4FWCore/helpers/tests/test_consumer.sh b/k4FWCore/helpers/tests/test_consumer.sh new file mode 100644 index 000000000..9797f4c16 --- /dev/null +++ b/k4FWCore/helpers/tests/test_consumer.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# test_consumer.sh — build-test: k4FWCore Consumer (single input, one property) +source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" + +run_cmake_build MyConsumer \ + -i 'edm4hep::MCParticleCollection:InputCollection' \ + -p 'int:Offset:10:Integer to add to values' + +echo "PASS: consumer" diff --git a/k4FWCore/helpers/tests/test_event_context.sh b/k4FWCore/helpers/tests/test_event_context.sh new file mode 100644 index 000000000..e0d811c9e --- /dev/null +++ b/k4FWCore/helpers/tests/test_event_context.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# test_event_context.sh — build-test: k4FWCore Transformer with EventContext and finalize() +source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" + +run_cmake_build MyEventContextTransformer \ + -i 'edm4hep::MCParticleCollection:InputCollection' \ + -o 'edm4hep::MCParticleCollection:OutputCollection' \ + --event-context \ + --private-properties \ + -p 'int:Offset:10:Integer to add to values' + +echo "PASS: event_context" diff --git a/k4FWCore/helpers/tests/test_filter.sh b/k4FWCore/helpers/tests/test_filter.sh new file mode 100644 index 000000000..b854d3ccb --- /dev/null +++ b/k4FWCore/helpers/tests/test_filter.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# test_filter.sh — build-test: k4FWCore FilterPredicate +source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" + +run_cmake_build MyFilter filter \ + -i 'edm4hep::MCParticleCollection:InputCollection' + +echo "PASS: filter" diff --git a/k4FWCore/helpers/tests/test_gaudi_framework.sh b/k4FWCore/helpers/tests/test_gaudi_framework.sh new file mode 100644 index 000000000..d8dd1d4f2 --- /dev/null +++ b/k4FWCore/helpers/tests/test_gaudi_framework.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# test_gaudi_framework.sh — build-test: native Gaudi::Functional Transformer with namespace +source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" + +run_cmake_build MyGaudiTransformer \ + -i 'edm4hep::MCParticleCollection:InputCollection' \ + -o 'edm4hep::MCParticleCollection:OutputCollection' \ + --framework gaudi \ + --namespace MyNamespace \ + -p 'int:Offset:10:Integer to add to values' + +echo "PASS: gaudi_framework" diff --git a/k4FWCore/helpers/tests/test_multitransformer.sh b/k4FWCore/helpers/tests/test_multitransformer.sh new file mode 100644 index 000000000..a674eb616 --- /dev/null +++ b/k4FWCore/helpers/tests/test_multitransformer.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# test_multitransformer.sh — build-test: k4FWCore MultiTransformer (multiple outputs, type aliases) +source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" + +run_cmake_build MyMultiTransformer \ + -i 'edm4hep::MCParticleCollection:InputParticles' \ + 'edm4hep::SimTrackerHitCollection:InputHits' \ + -o 'edm4hep::MCParticleCollection:OutputParticles' \ + 'podio::UserDataCollection:Counter' \ + --type-aliases \ + -p 'int:Offset:10:Integer to add to values' + +echo "PASS: multitransformer" diff --git a/k4FWCore/helpers/tests/test_producer.sh b/k4FWCore/helpers/tests/test_producer.sh new file mode 100644 index 000000000..49586805b --- /dev/null +++ b/k4FWCore/helpers/tests/test_producer.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# test_producer.sh — build-test: k4FWCore Producer (single output, one property) +source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" + +run_cmake_build MyProducer \ + -o 'edm4hep::MCParticleCollection:OutputCollection' \ + -p 'int:ExampleInt:3:An example integer property' + +echo "PASS: producer" diff --git a/k4FWCore/helpers/tests/test_runtime_consumer.sh b/k4FWCore/helpers/tests/test_runtime_consumer.sh new file mode 100644 index 000000000..c41e163a7 --- /dev/null +++ b/k4FWCore/helpers/tests/test_runtime_consumer.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# test_runtime_consumer.sh — build-test: k4FWCore Consumer with runtime (variable-length) inputs +source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" + +run_cmake_build MyRuntimeConsumer \ + -i 'edm4hep::MCParticleCollection:InputCollections' \ + --runtime-inputs 'edm4hep::MCParticleCollection:InputCollections:MCParticles0,MCParticles1' \ + --private-properties \ + -p 'int:Offset:10:Integer to add to values' + +echo "PASS: runtime_consumer" diff --git a/k4FWCore/helpers/tests/test_runtime_transformer.sh b/k4FWCore/helpers/tests/test_runtime_transformer.sh new file mode 100644 index 000000000..1e4090ef5 --- /dev/null +++ b/k4FWCore/helpers/tests/test_runtime_transformer.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# test_runtime_transformer.sh — build-test: k4FWCore Transformer with runtime outputs +source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" + +run_cmake_build MyRuntimeTransformer \ + -i 'edm4hep::MCParticleCollection:InputCollections' \ + --runtime-inputs 'edm4hep::MCParticleCollection:InputCollections:MCParticles' \ + --runtime-outputs 'edm4hep::MCParticleCollection' \ + --private-properties \ + -p 'int:NumCollections:3:Number of output collections' + +echo "PASS: runtime_transformer" diff --git a/k4FWCore/helpers/tests/test_transformer.sh b/k4FWCore/helpers/tests/test_transformer.sh new file mode 100644 index 000000000..324cb680b --- /dev/null +++ b/k4FWCore/helpers/tests/test_transformer.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# test_transformer.sh — build-test: k4FWCore Transformer (single in/out, private property) +source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" + +run_cmake_build MyTransformer \ + -i 'edm4hep::MCParticleCollection:InputCollection' \ + -o 'edm4hep::MCParticleCollection:OutputCollection' \ + --private-properties \ + -p 'int:Offset:10:Integer to add to values' + +echo "PASS: transformer" From 1eee94e04d8f7ab2036eb0ac806017da8ef93f1d Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Tue, 9 Jun 2026 15:57:50 +0200 Subject: [PATCH 26/36] context for AI agents --- k4FWCore/helpers/AGENT.md | 159 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 k4FWCore/helpers/AGENT.md diff --git a/k4FWCore/helpers/AGENT.md b/k4FWCore/helpers/AGENT.md new file mode 100644 index 000000000..b2093dd46 --- /dev/null +++ b/k4FWCore/helpers/AGENT.md @@ -0,0 +1,159 @@ +# AGENT.md — k4FWCore/helpers + +Context for AI agents working on `gaudiGen.py` and its test suite. + +--- + +## What this directory contains + +| File/Dir | Purpose | +|---|---| +| `gaudiGen.py` | Code generator: produces Gaudi Functional C++ boilerplate from CLI arguments | +| `README.md` | Full user-facing reference (arguments, examples, exit codes) | +| `tests/` | Bash build-tests: generate → cmake → build for each algorithm type | +| `tests/_test_common.sh` | Shared helpers sourced by every `test_*.sh` | +| `tests/run_all_tests.sh` | Runs all `test_*.sh` and reports a pass/fail summary | + +--- + +## Architecture of gaudiGen.py + +The script is intentionally structured so that all string parsing happens once at the CLI boundary and never again: + +``` +CLI args + └─ _build_spec() Parses strings → AlgorithmSpec dataclass + └─ generate() AlgorithmSpec → (cpp_source, cmake_source) + ├─ _build_includes() + ├─ _build_constructor() k4FWCore style (with brace rules below) + ├─ _build_constructor_gaudi() + ├─ _build_op_signature() + ├─ _build_op_body() + └─ Jinja2 template (_CPP_TEMPLATE, _CMAKE_TEMPLATE) +``` + +### Key data classes + +- **`DataSpec`** — one input or output collection: `type_name`, `key`, `is_vector`. + - `edm4hep_header` property returns `TypeCollection.h` (keep "Collection" in filename — a past bug stripped it). + - `_default_key()` derives a key from the type name by stripping namespace and `Collection`, e.g. `edm4hep::MCParticleCollection` → `MCParticles`. + +- **`RuntimeInputSpec`** — a `DataSpec` with additional default location names for `KeyValues`. + +- **`PropertySpec`** — a `Gaudi::Property` member. + - `member_name` lowercases the first character after `m_`: `Offset` → `m_offset`, not `m_Offset`. + +- **`AlgorithmSpec`** — the single object passed through all generation functions. Contains all inputs, outputs, options, and derived properties used by templates. + +### Constructor brace rules + +This is the trickiest part of code generation. The k4FWCore constructors follow different conventions per type: + +| Type | Inputs | Outputs | +|---|---|---| +| `Consumer` / `FilterPredicate` | bare for single: `KeyValue(...)` | — | +| `Consumer` / `FilterPredicate` | braced for multiple: `{KeyValue(...), ...}` | — | +| `Producer` | `{}` (always empty) | bare for single: `KeyValue(...)` | +| `Producer` | `{}` | braced for multiple: `{KeyValues(...), ...}` | +| `Transformer` / `MultiTransformer` | **always braced**: `{KeyValue(...)}` | **always braced**: `{KeyValue(...)}` | + +In `_build_constructor`, `_brace_block()` always wraps in `{}` (used for transformer/producer), while `_bare_block()` leaves a single item unwrapped (used for consumer/filter). + +### Template structure (_CPP_TEMPLATE) + +Order of sections in the generated `.cpp`: + +1. `// Generated by ...` header comment with full command line +2. `#include` directives +3. Optional `using BaseClass_t = ...` (Gaudi framework only) +4. Optional `using retType = std::tuple<...>` (k4FWCore multi-output) +5. Optional `using XxxColl = ...` type aliases (`--type-aliases`) +6. Optional `namespace X {` +7. Class definition: + - Constructor + - `StatusCode initialize()` (only when vector inputs are present) + - `operator()` + - `StatusCode finalize()` (only with `--event-context`) — **must be before `private:`** + - Optional `private:` label (when `--private-properties` or `--event-context`) + - Properties + - `mutable std::set m_eventNumbersSeen{}` and `m_mutex` (only with `--event-context`) +8. `DECLARE_COMPONENT(ClassName)` + +### Functional type inference + +``` +inputs > 0, outputs == 0 → consumer +inputs == 0, outputs > 0 → producer +inputs > 0, outputs == 1 → transformer +inputs > 0, outputs > 1 → multitransformer +filter → never inferred; must be explicit +``` + +`transformer` auto-promotes to `multitransformer` when multiple `--outputs` are given. + +--- + +## Known remaining gaps vs. test examples + +These are design limitations, not bugs: + +1. **`KeyValue` default location = key name.** The script emits `KeyValue("OutputCollection", "OutputCollection")` but test examples have `KeyValue("OutputCollection", "MCParticles")`. There is no CLI argument for a separate default location value. + +2. **Include order.** Script: `k4FWCore/` first, then `Gaudi/Property.h`, then `edm4hep/`. Test examples: `Gaudi/Property.h` first, then `edm4hep/`, then `k4FWCore/`. + +3. **Multi-transformer output aliases.** Test examples define individual `using Counter = ...; using Particle = ...;` aliases for each output type. The script emits a single `using retType = std::tuple<...>` with raw types. + +4. **No license header.** The script emits `// Generated by ...`; test examples carry the Apache 2.0 block. + +--- + +## Test scripts + +Each script in `tests/` covers one feature axis: + +| Script | Feature | +|---|---| +| `test_producer.sh` | Single output, property | +| `test_consumer.sh` | Single input, property | +| `test_transformer.sh` | Single in/out, `--private-properties` | +| `test_multitransformer.sh` | Multiple in/out, `--type-aliases`, `podio::UserDataCollection` | +| `test_filter.sh` | `FilterPredicate` | +| `test_runtime_consumer.sh` | `--runtime-inputs` / `KeyValues` vector input | +| `test_runtime_transformer.sh` | `--runtime-outputs` / `std::vector` return | +| `test_event_context.sh` | `--event-context`, `finalize()` placement | +| `test_gaudi_framework.sh` | `--framework gaudi`, `--namespace` | + +Each script sources `_test_common.sh` which: +- Finds `gaudiGen.py` (installed on `PATH` first, then `../gaudiGen.py` fallback) +- Creates a `mktemp -d` sandbox, cleaned up on `EXIT` +- Provides `run_cmake_build [args...]` that runs generate → cmake configure → cmake build + +Tests require a Key4hep environment (`k4FWCore`, `EDM4HEP`, `Gaudi` on `CMAKE_PREFIX_PATH`). Source the Key4hep setup before running: + +```bash +source /cvmfs/sw.hsf.org/key4hep/setup.sh +bash k4FWCore/helpers/tests/run_all_tests.sh +``` + +--- + +## Installation + +`gaudiGen.py` is installed to `CMAKE_INSTALL_BINDIR` via `k4FWCore/CMakeLists.txt`: + +```cmake +install(PROGRAMS helpers/gaudiGen.py + DESTINATION ${CMAKE_INSTALL_BINDIR}) +``` + +After `cmake --install`, `gaudiGen.py` is on `PATH` in the Key4hep environment. + +--- + +## Common mistakes to avoid + +- **Do not strip `Collection` from edm4hep header filenames.** `edm4hep::MCParticleCollection` → `edm4hep/MCParticleCollection.h`, not `edm4hep/MCParticle.h`. See `DataSpec.edm4hep_header`. +- **Do not wrap `Consumer`/`FilterPredicate` single inputs in braces.** Only `Transformer`/`Producer` use `_brace_block()`. +- **`finalize()` must be emitted before `private:`.** The Jinja2 template places `finalize()` in its own block before the `{% if spec.private_props or spec.event_context %}private:{% endif %}` block. +- **Property member names must be lowercase after `m_`.** `PropertySpec.member_name` lowercases `n[0]`; do not change this or generated names diverge from k4FWCore conventions. +- **`--runtime-outputs` is k4FWCore-only.** The parser enforces this, but the cmake template only adds podio explicitly for `--framework gaudi`; for k4fwcore it is a transitive dependency of `k4FWCore::k4FWCore`. From 7641e8790f5cbc963d3b221e4c479b55871068dd Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Tue, 9 Jun 2026 16:06:38 +0200 Subject: [PATCH 27/36] rename gaudiGen.py to generateFunctional --- README.md | 18 ++++++------ k4FWCore/CMakeLists.txt | 2 +- k4FWCore/helpers/AGENT.md | 14 +++++----- k4FWCore/helpers/README.md | 28 +++++++++---------- .../{gaudiGen.py => generateFunctional} | 16 +++++------ k4FWCore/helpers/tests/_test_common.sh | 14 +++++----- 6 files changed, 46 insertions(+), 46 deletions(-) rename k4FWCore/helpers/{gaudiGen.py => generateFunctional} (98%) diff --git a/README.md b/README.md index f1c9eafff..9b220acc5 100644 --- a/README.md +++ b/README.md @@ -119,9 +119,9 @@ have either multiple inputs and / or multiple outputs (like `ExampleFunctionalProducerMultiple`) that can be used as a template for the more typical case when working with multiple inputs or outputs. -### Generating boilerplate with gaudiGen.py +### Generating boilerplate with generateFunctional -`k4FWCore/helpers/gaudiGen.py` is a code generator that produces the C++ +`k4FWCore/helpers/generateFunctional` is a code generator that produces the C++ boilerplate for a new functional algorithm. The functional type (Consumer, Producer, Transformer, MultiTransformer, FilterPredicate) is inferred automatically from the number of inputs and outputs, or can be set explicitly. @@ -132,18 +132,18 @@ resolved automatically via the PEP 723 script block. ```bash # Producer with one output collection and one property -python3 k4FWCore/helpers/gaudiGen.py MyProducer \ +python3 k4FWCore/helpers/generateFunctional MyProducer \ -o 'edm4hep::MCParticleCollection:OutputCollection' \ -p 'int:ExampleInt:3:An example integer property' # Transformer (inferred from 1 input + 1 output) -python3 k4FWCore/helpers/gaudiGen.py MyTransformer \ +python3 k4FWCore/helpers/generateFunctional MyTransformer \ -i 'edm4hep::MCParticleCollection:InputCollection' \ -o 'edm4hep::MCParticleCollection:OutputCollection' \ --private-properties # MultiTransformer with type aliases -python3 k4FWCore/helpers/gaudiGen.py MyMulti \ +python3 k4FWCore/helpers/generateFunctional MyMulti \ -i 'edm4hep::MCParticleCollection:Particles' \ 'edm4hep::TrackCollection:Tracks' \ -o 'edm4hep::MCParticleCollection:NewParticles' \ @@ -151,18 +151,18 @@ python3 k4FWCore/helpers/gaudiGen.py MyMulti \ --type-aliases # FilterPredicate (type must be specified explicitly) -python3 k4FWCore/helpers/gaudiGen.py MyFilter filter \ +python3 k4FWCore/helpers/generateFunctional MyFilter filter \ -i 'edm4hep::MCParticleCollection:InputCollection' # Consumer with runtime (variable-length) input collections -python3 k4FWCore/helpers/gaudiGen.py MyConsumer \ +python3 k4FWCore/helpers/generateFunctional MyConsumer \ -i 'edm4hep::MCParticleCollection:Inputs' \ --runtime-inputs 'edm4hep::MCParticleCollection:Inputs:MCParticles0,MCParticles1' # Also emit a CMakeLists.txt skeleton -python3 k4FWCore/helpers/gaudiGen.py MyProducer \ +python3 k4FWCore/helpers/generateFunctional MyProducer \ -o 'edm4hep::MCParticleCollection:OutputCollection' \ --cmake ``` -Run `python3 k4FWCore/helpers/gaudiGen.py --help` for the full list of options. +Run `python3 k4FWCore/helpers/generateFunctional --help` for the full list of options. diff --git a/k4FWCore/CMakeLists.txt b/k4FWCore/CMakeLists.txt index faf0ce710..cdf893dac 100644 --- a/k4FWCore/CMakeLists.txt +++ b/k4FWCore/CMakeLists.txt @@ -18,7 +18,7 @@ limitations under the License. ]] gaudi_install(SCRIPTS) -install(PROGRAMS helpers/gaudiGen.py +install(PROGRAMS helpers/generateFunctional DESTINATION ${CMAKE_INSTALL_BINDIR}) gaudi_add_library(k4FWCore diff --git a/k4FWCore/helpers/AGENT.md b/k4FWCore/helpers/AGENT.md index b2093dd46..96849fd48 100644 --- a/k4FWCore/helpers/AGENT.md +++ b/k4FWCore/helpers/AGENT.md @@ -1,6 +1,6 @@ # AGENT.md — k4FWCore/helpers -Context for AI agents working on `gaudiGen.py` and its test suite. +Context for AI agents working on `generateFunctional` and its test suite. --- @@ -8,7 +8,7 @@ Context for AI agents working on `gaudiGen.py` and its test suite. | File/Dir | Purpose | |---|---| -| `gaudiGen.py` | Code generator: produces Gaudi Functional C++ boilerplate from CLI arguments | +| `generateFunctional` | Code generator: produces Gaudi Functional C++ boilerplate from CLI arguments | | `README.md` | Full user-facing reference (arguments, examples, exit codes) | | `tests/` | Bash build-tests: generate → cmake → build for each algorithm type | | `tests/_test_common.sh` | Shared helpers sourced by every `test_*.sh` | @@ -16,7 +16,7 @@ Context for AI agents working on `gaudiGen.py` and its test suite. --- -## Architecture of gaudiGen.py +## Architecture of generateFunctional The script is intentionally structured so that all string parsing happens once at the CLI boundary and never again: @@ -124,7 +124,7 @@ Each script in `tests/` covers one feature axis: | `test_gaudi_framework.sh` | `--framework gaudi`, `--namespace` | Each script sources `_test_common.sh` which: -- Finds `gaudiGen.py` (installed on `PATH` first, then `../gaudiGen.py` fallback) +- Finds `generateFunctional` (installed on `PATH` first, then `../generateFunctional` fallback) - Creates a `mktemp -d` sandbox, cleaned up on `EXIT` - Provides `run_cmake_build [args...]` that runs generate → cmake configure → cmake build @@ -139,14 +139,14 @@ bash k4FWCore/helpers/tests/run_all_tests.sh ## Installation -`gaudiGen.py` is installed to `CMAKE_INSTALL_BINDIR` via `k4FWCore/CMakeLists.txt`: +`generateFunctional` is installed to `CMAKE_INSTALL_BINDIR` via `k4FWCore/CMakeLists.txt`: ```cmake -install(PROGRAMS helpers/gaudiGen.py +install(PROGRAMS helpers/generateFunctional DESTINATION ${CMAKE_INSTALL_BINDIR}) ``` -After `cmake --install`, `gaudiGen.py` is on `PATH` in the Key4hep environment. +After `cmake --install`, `generateFunctional` is on `PATH` in the Key4hep environment. --- diff --git a/k4FWCore/helpers/README.md b/k4FWCore/helpers/README.md index 15f9b7508..b4c42c155 100644 --- a/k4FWCore/helpers/README.md +++ b/k4FWCore/helpers/README.md @@ -1,6 +1,6 @@ -# gaudiGen.py — Gaudi Functional C++ Class Generator +# generateFunctional — Gaudi Functional C++ Class Generator -`gaudiGen.py` writes the boilerplate for a Gaudi Functional algorithm: the +`generateFunctional` writes the boilerplate for a Gaudi Functional algorithm: the `#include`s, the constructor with `KeyValue` / `KeyValues` wiring, the `operator()` signature, a placeholder body, optional `Gaudi::Property` members, and (optionally) a matching `CMakeLists.txt`. It supports both the @@ -30,14 +30,14 @@ There are three equivalent ways to invoke the script: ```bash # 1. Recommended — uv resolves Python and Jinja2 from the PEP 723 block. -uv run gaudiGen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' +uv run generateFunctional MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' # 2. Direct execution via the shebang (requires uv on PATH). -chmod +x gaudiGen.py -./gaudiGen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' +chmod +x generateFunctional +./generateFunctional MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' # 3. Plain Python (you must have jinja2 installed in the active env). -python3 gaudiGen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' +python3 generateFunctional MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' ``` The first two routes are self-contained: nothing needs to be installed in @@ -49,7 +49,7 @@ the system or active Python environment beyond `uv` itself. ```bash # k4FWCore producer (functional type inferred from --outputs) -uv run gaudiGen.py MyProducer \ +uv run generateFunctional MyProducer \ -o 'edm4hep::MCParticleCollection:MCParticles' ``` @@ -57,7 +57,7 @@ That writes `MyProducer.cpp` in the current directory. Add `--cmake` to also emit a `CMakeLists.txt`: ```bash -uv run gaudiGen.py MyProducer \ +uv run generateFunctional MyProducer \ -o 'edm4hep::MCParticleCollection:MCParticles' \ --cmake ``` @@ -66,7 +66,7 @@ uv run gaudiGen.py MyProducer \ ## File-overwrite policy -`gaudiGen.py` **never silently overwrites an existing file**. If the target +`generateFunctional` **never silently overwrites an existing file**. If the target `.cpp` or `CMakeLists.txt` already exists, the script prints a diagnostic and exits non-zero: @@ -157,7 +157,7 @@ auto-promotes to `multitransformer`. ### Producer with multiple outputs and a property ```bash -uv run gaudiGen.py MyProducer \ +uv run generateFunctional MyProducer \ -o 'edm4hep::MCParticleCollection:MCParticles' \ 'edm4hep::TrackCollection:Tracks' \ -p 'int:ExampleInt:3:An example integer property' @@ -168,7 +168,7 @@ The output uses a `retType = std::tuple<...>` alias for readability. ### Native Gaudi transformer wrapped in a C++ namespace ```bash -uv run gaudiGen.py MySum \ +uv run generateFunctional MySum \ -i 'Input1:Loc1' 'Input2:Loc2' \ -o 'Output:OutLoc' \ --framework gaudi \ @@ -191,7 +191,7 @@ struct MySum final : Gaudi::Functional::Transformer bool: # --------------------------------------------------------------------------- def _build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( - prog="gaudiGen.py", + prog="generateFunctional", description="Generate Gaudi Functional C++ algorithm boilerplate.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=textwrap.dedent("""\ @@ -790,20 +790,20 @@ def _build_parser() -> argparse.ArgumentParser: Examples: # k4FWCore producer (type inferred) - gaudiGen.py MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' + generateFunctional MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' # k4FWCore multi-output producer with properties - gaudiGen.py MyProducer \\ + generateFunctional MyProducer \\ -o 'edm4hep::MCParticleCollection:MCParticles' \\ 'edm4hep::TrackCollection:Tracks' \\ -p 'int:ExampleInt:3:An example integer property' # Gaudi transformer wrapped in 'namespace MyNamespace { ... }' (type inferred) - gaudiGen.py MySum -i 'Input1:Loc1' 'Input2:Loc2' -o 'Output:OutLoc' \\ + generateFunctional MySum -i 'Input1:Loc1' 'Input2:Loc2' -o 'Output:OutLoc' \\ --framework gaudi -n MyNamespace # Variable-length inputs (k4FWCore only) - gaudiGen.py MyVarConsumer \\ + generateFunctional MyVarConsumer \\ -i 'edm4hep::MCParticleCollection:Inputs' \\ --runtime-inputs 'edm4hep::MCParticleCollection:Inputs:MCParticles0,MCParticles1' """), diff --git a/k4FWCore/helpers/tests/_test_common.sh b/k4FWCore/helpers/tests/_test_common.sh index 628b637f1..8885da8ba 100644 --- a/k4FWCore/helpers/tests/_test_common.sh +++ b/k4FWCore/helpers/tests/_test_common.sh @@ -4,29 +4,29 @@ # # Usage in a test script: # source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" -# run_cmake_build ClassName [gaudiGen.py args...] +# run_cmake_build ClassName [generateFunctional args...] set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Prefer the installed command; fall back to the source copy one level up. -if command -v gaudiGen.py &>/dev/null; then - GENERATOR="$(command -v gaudiGen.py)" +if command -v generateFunctional &>/dev/null; then + GENERATOR="$(command -v generateFunctional)" else - GENERATOR="${SCRIPT_DIR}/../gaudiGen.py" + GENERATOR="${SCRIPT_DIR}/../generateFunctional" fi if [[ ! -f "${GENERATOR}" ]]; then - echo "ERROR: gaudiGen.py not found (tried PATH and ${GENERATOR})" >&2 + echo "ERROR: generateFunctional not found (tried PATH and ${GENERATOR})" >&2 exit 1 fi SANDBOX="$(mktemp -d)" trap 'rm -rf "${SANDBOX}"' EXIT -# run_cmake_build [gaudiGen.py args...] -# 1. Generates .cpp + CMakeLists.txt via gaudiGen.py --cmake +# run_cmake_build [generateFunctional args...] +# 1. Generates .cpp + CMakeLists.txt via generateFunctional --cmake # 2. Configures with cmake # 3. Builds with cmake --build run_cmake_build() { From 24af1d062f888aceeb21250f062841afd2681d79 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Tue, 9 Jun 2026 16:17:14 +0200 Subject: [PATCH 28/36] add agent usage tips --- k4FWCore/helpers/AGENT_USAGE.md | 107 ++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 k4FWCore/helpers/AGENT_USAGE.md diff --git a/k4FWCore/helpers/AGENT_USAGE.md b/k4FWCore/helpers/AGENT_USAGE.md new file mode 100644 index 000000000..db39cddc2 --- /dev/null +++ b/k4FWCore/helpers/AGENT_USAGE.md @@ -0,0 +1,107 @@ +# Using an AI Agent with generateFunctional + +You can ask an AI agent (such as Claude in Cowork or via the API) to run +`generateFunctional` for you. Instead of memorising flags, describe your +algorithm in plain language and the agent handles the rest. + +--- + +## How it works + +1. You describe the algorithm you need. +2. The agent translates your description into a `generateFunctional` command. +3. The agent runs the command and shows you the generated `.cpp` (and + optionally `CMakeLists.txt`). +4. You ask for changes; the agent re-runs with updated flags. + +--- + +## What to tell the agent + +The more detail you provide, the closer the first attempt will be to what you +want. Cover these points: + +| What | Example | +|---|---| +| **Algorithm name** | `MyParticleSelector` | +| **Inputs** — type and key name | `edm4hep::MCParticleCollection` named `InputParticles` | +| **Outputs** — type and key name | `edm4hep::MCParticleCollection` named `SelectedParticles` | +| **Properties** — C++ type, name, default, description | `float` named `MinPt`, default `0.5`, "Minimum transverse momentum" | +| **Private properties?** | Yes / No | +| **EventContext needed?** | Yes / No | +| **Type aliases?** | Yes / No | +| **CMake file too?** | Yes / No | +| **Framework** | `k4fwcore` (default) or `gaudi` | +| **C++ namespace** | e.g. `MyExperiment` | + +You do not need to know any flags — just describe what you want. + +--- + +## Example prompts + +### Minimal — let the agent fill in the gaps + +> Generate a transformer called `TrackFilter` that reads +> `edm4hep::TrackCollection` and writes a filtered +> `edm4hep::TrackCollection`. + +### With properties + +> Generate a transformer `EnergyThresholdFilter` that takes +> `edm4hep::MCParticleCollection:InputParticles` as input and returns +> `edm4hep::MCParticleCollection:OutputParticles`. Add a float property +> `MinEnergy` with default `1.0` and description "Minimum particle energy in +> GeV". Put properties under `private:`. Also emit a `CMakeLists.txt`. + +### Multiple inputs and outputs + +> I need a MultiTransformer `JetBuilder` with two inputs — +> `edm4hep::MCParticleCollection:Particles` and +> `edm4hep::TrackCollection:Tracks` — and two outputs — +> `edm4hep::ReconstructedParticleCollection:Jets` and +> `podio::UserDataCollection:JetPt`. Use type aliases. + +### Runtime (variable-length) inputs + +> Generate a consumer `MultiCollectionReader` that reads a variable number +> of `edm4hep::MCParticleCollection` inputs at runtime, with default names +> `MCParticles0` and `MCParticles1`. + +### From an existing example + +> Look at `ExampleFunctionalTransformerRuntimeCollections.cpp` in the test +> folder and generate something similar for `edm4hep::TrackCollection`. + +### Refinement after seeing the output + +> That looks good. Can you add an `int` property `MaxParticles` with default +> `100`, and regenerate with `--force`? + +--- + +## What the agent can do automatically + +- Infer the functional type (`Consumer`, `Producer`, `Transformer`, + `MultiTransformer`) from your inputs and outputs. +- Derive default key names from collection types when you don't specify them + (e.g. `edm4hep::MCParticleCollection` → key `MCParticles`). +- Add the correct `#include` directives for all edm4hep and podio types. +- Emit `DECLARE_COMPONENT()` and a ready-to-build `CMakeLists.txt`. +- Re-run with `--force` to overwrite after you request changes. + +--- + +## Tips + +- **`filter` must be explicit.** The agent cannot infer `FilterPredicate` from + I/O counts alone — say "FilterPredicate" or "filter type" in your prompt. +- **Key names matter.** If your steering file already names the collections, + tell the agent the exact keys so the generated `KeyValue` strings match. +- **Iterate freely.** Generated code is cheap to redo. Ask the agent to tweak + property types, add an `EventContext`, switch to `--use-class`, or change + the namespace — it will re-run the generator rather than hand-editing the + output. +- **Review before committing.** Check the generated constructor argument order + and `operator()` signature against your project's conventions before adding + the file to git. From 23fe3b0807db505816c02a385e1af5724b7248bd Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Tue, 9 Jun 2026 16:43:12 +0200 Subject: [PATCH 29/36] fix gaudi test --- k4FWCore/helpers/AGENT.md | 1 + k4FWCore/helpers/generateFunctional | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/k4FWCore/helpers/AGENT.md b/k4FWCore/helpers/AGENT.md index 96849fd48..dfe51f1c8 100644 --- a/k4FWCore/helpers/AGENT.md +++ b/k4FWCore/helpers/AGENT.md @@ -157,3 +157,4 @@ After `cmake --install`, `generateFunctional` is on `PATH` in the Key4hep enviro - **`finalize()` must be emitted before `private:`.** The Jinja2 template places `finalize()` in its own block before the `{% if spec.private_props or spec.event_context %}private:{% endif %}` block. - **Property member names must be lowercase after `m_`.** `PropertySpec.member_name` lowercases `n[0]`; do not change this or generated names diverge from k4FWCore conventions. - **`--runtime-outputs` is k4FWCore-only.** The parser enforces this, but the cmake template only adds podio explicitly for `--framework gaudi`; for k4fwcore it is a transitive dependency of `k4FWCore::k4FWCore`. +- **Do not link `Gaudi::GaudiAlgLib` for `--framework gaudi`.** This target was removed in Gaudi 40.x. The cmake template links only `Gaudi::GaudiKernel`. diff --git a/k4FWCore/helpers/generateFunctional b/k4FWCore/helpers/generateFunctional index b05925ce3..2372b21e3 100644 --- a/k4FWCore/helpers/generateFunctional +++ b/k4FWCore/helpers/generateFunctional @@ -701,7 +701,7 @@ def _build_cmake_context(spec: AlgorithmSpec) -> dict: link_libs.append("k4FWCore::k4FWCore") else: find_packages.append("find_package(Gaudi REQUIRED)") - link_libs += ["Gaudi::GaudiAlgLib", "Gaudi::GaudiKernel"] + link_libs += ["Gaudi::GaudiKernel"] if has_edm4hep: find_packages.append("find_package(EDM4HEP REQUIRED)") link_libs.append("EDM4HEP::edm4hep") From 721b470f44701b359402897cbe87cd32ee6cf98c Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Tue, 9 Jun 2026 16:53:50 +0200 Subject: [PATCH 30/36] fix gaudi test --- k4FWCore/helpers/AGENT.md | 2 + k4FWCore/helpers/generateFunctional | 33 ++++++--- k4FWCore/helpers/tests/README.md | 101 ++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 k4FWCore/helpers/tests/README.md diff --git a/k4FWCore/helpers/AGENT.md b/k4FWCore/helpers/AGENT.md index dfe51f1c8..b12e5c3c0 100644 --- a/k4FWCore/helpers/AGENT.md +++ b/k4FWCore/helpers/AGENT.md @@ -158,3 +158,5 @@ After `cmake --install`, `generateFunctional` is on `PATH` in the Key4hep enviro - **Property member names must be lowercase after `m_`.** `PropertySpec.member_name` lowercases `n[0]`; do not change this or generated names diverge from k4FWCore conventions. - **`--runtime-outputs` is k4FWCore-only.** The parser enforces this, but the cmake template only adds podio explicitly for `--framework gaudi`; for k4fwcore it is a transitive dependency of `k4FWCore::k4FWCore`. - **Do not link `Gaudi::GaudiAlgLib` for `--framework gaudi`.** This target was removed in Gaudi 40.x. The cmake template links only `Gaudi::GaudiKernel`. +- **Native Gaudi constructor takes separate input and output arguments, not a single merged list.** `_build_constructor_gaudi` passes `_arg(in_kvs), _arg(out_kvs)` as separate arguments. A single KV is bare; multiple KVs are `{kv1, kv2, ...}`. +- **`DECLARE_COMPONENT` must use the fully qualified name when `--namespace` is set.** The template emits `DECLARE_COMPONENT(Ns::ClassName)` outside the namespace block. diff --git a/k4FWCore/helpers/generateFunctional b/k4FWCore/helpers/generateFunctional index 2372b21e3..febe71e0e 100644 --- a/k4FWCore/helpers/generateFunctional +++ b/k4FWCore/helpers/generateFunctional @@ -480,7 +480,7 @@ private: } // namespace {{ spec.namespace }} {% endif %} -DECLARE_COMPONENT({{ cls }}) +DECLARE_COMPONENT({% if spec.namespace %}{{ spec.namespace }}::{% endif %}{{ cls }}) """ _CMAKE_TEMPLATE = """\ @@ -617,13 +617,30 @@ def _build_constructor(spec: AlgorithmSpec) -> str: def _build_constructor_gaudi(spec: AlgorithmSpec) -> str: cls = spec.class_name base = spec.base_short - all_kvs = [f'KeyValue{{"{ds.key}", "{ds.key}"}}' for ds in spec.inputs + spec.outputs] - if not all_kvs: - sep, args_str = "", "" - elif len(all_kvs) == 1: - sep, args_str = ", ", all_kvs[0] - else: - sep, args_str = ", ", "{" + ", ".join(all_kvs) + "}" + + def _kv(ds: DataSpec) -> str: + return f'KeyValue{{"{ds.key}", "{ds.key}"}}' + + in_kvs = [_kv(ds) for ds in spec.inputs] + out_kvs = [_kv(ds) for ds in spec.outputs] + + # Native Gaudi Transformer constructor variants (details.h): + # (name, svc, KeyValue_in, KeyValue_out) — 1 in, 1 out + # (name, svc, KeyValue_in, RepeatValues__out) — 1 in, N out + # (name, svc, RepeatValues__in, KeyValue_out) — N in, 1 out + # (name, svc, RepeatValues__in, RepeatValues__out) — N in, M out + # RepeatValues_ is a tuple of pairs, passed as a braced list {kv1, kv2, ...}. + # Single KeyValues must NOT be wrapped in braces. + def _arg(kvs: list[str]) -> str: + if len(kvs) == 0: + return "" + if len(kvs) == 1: + return kvs[0] + return "{" + ", ".join(kvs) + "}" + + parts = [p for p in [_arg(in_kvs), _arg(out_kvs)] if p] + sep = ", " if parts else "" + args_str = ", ".join(parts) return ( f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" f" : {base}(name, svcLoc{sep}{args_str}) {{}}" diff --git a/k4FWCore/helpers/tests/README.md b/k4FWCore/helpers/tests/README.md new file mode 100644 index 000000000..af8946950 --- /dev/null +++ b/k4FWCore/helpers/tests/README.md @@ -0,0 +1,101 @@ +# Running the generateFunctional tests + +The tests run entirely in bash — no CTest or Python test framework needed. +Each script generates a C++ algorithm, runs `cmake`, and compiles it. + +--- + +## Prerequisites + +A Key4hep environment and the installed `generateFunctional` script. +`uv` is not required — the tests invoke `python3` directly. + +--- + +## Setup + +```bash +# 1. Clone and check out the branch +git clone https://github.com/key4hep/k4FWCore.git +cd k4FWCore +git remote add ianna https://github.com/ianna/k4FWCore.git +git fetch ianna +git checkout ianna/gaudi_functional_generator + +# 2. Source the Key4hep environment (use zsh, not bash/tcsh) +zsh +source /cvmfs/sw.hsf.org/key4hep/setup.sh + +# 3. Build and install (skip the test suite — it has an unrelated build error) +mkdir build && cd build +cmake .. -DCMAKE_INSTALL_PREFIX=../install -DBUILD_TESTING=OFF +make -j$(nproc) install + +# 4. Put generateFunctional on PATH +export PATH="${PWD}/../install/bin:${PATH}" + +# 5. Since uv is not available on lxplus, alias the script to use python3 +alias generateFunctional="python3 $(which generateFunctional)" + +# 6. Go back to the repo root +cd .. +``` + +--- + +## Run all tests + +```bash +bash k4FWCore/helpers/tests/run_all_tests.sh +``` + +Expected output: + +``` + test_consumer ... PASS + test_event_context ... PASS + test_filter ... PASS + test_gaudi_framework ... PASS + test_multitransformer ... PASS + test_producer ... PASS + test_runtime_consumer ... PASS + test_runtime_transformer ... PASS + test_transformer ... PASS + +9 passed, 0 failed +``` + +Each test takes ~1–3 minutes on lxplus (cmake configure + compile per test). + +--- + +## Run a single test + +```bash +bash k4FWCore/helpers/tests/test_producer.sh +``` + +--- + +## What each test covers + +| Script | Feature tested | +|---|---| +| `test_producer.sh` | Single output, property | +| `test_consumer.sh` | Single input, property | +| `test_transformer.sh` | Single in/out, `--private-properties` | +| `test_multitransformer.sh` | Multiple in/out, `--type-aliases`, `podio::UserDataCollection` | +| `test_filter.sh` | `FilterPredicate` | +| `test_runtime_consumer.sh` | `--runtime-inputs` / `KeyValues` vector input | +| `test_runtime_transformer.sh` | `--runtime-outputs` / `std::vector` return | +| `test_event_context.sh` | `--event-context`, `finalize()` placement | +| `test_gaudi_framework.sh` | `--framework gaudi`, `--namespace` | + +--- + +## Notes + +- Each test creates an isolated `mktemp -d` sandbox, cleaned up automatically on exit. +- `_test_common.sh` must not be run directly — it is sourced by the other scripts. +- If `generateFunctional` is not on `PATH`, the scripts fall back to `../generateFunctional` (one level above `tests/`). +- Tests require the full Key4hep environment on `CMAKE_PREFIX_PATH`. Without it, cmake will fail to find `k4FWCore` or `Gaudi`. From ed4b739d0e84241632d1c4ac4069d9c66b3a3405 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Mon, 6 Jul 2026 21:24:20 +0200 Subject: [PATCH 31/36] address Juan's comments --- k4FWCore/CMakeLists.txt | 4 + k4FWCore/helpers/tests/README.md | 107 ++++++++---------------- k4FWCore/helpers/tests/_test_common.sh | 9 +- k4FWCore/helpers/tests/run_all_tests.sh | 39 --------- 4 files changed, 48 insertions(+), 111 deletions(-) delete mode 100644 k4FWCore/helpers/tests/run_all_tests.sh diff --git a/k4FWCore/CMakeLists.txt b/k4FWCore/CMakeLists.txt index cdf893dac..f57807d47 100644 --- a/k4FWCore/CMakeLists.txt +++ b/k4FWCore/CMakeLists.txt @@ -21,6 +21,10 @@ gaudi_install(SCRIPTS) install(PROGRAMS helpers/generateFunctional DESTINATION ${CMAKE_INSTALL_BINDIR}) +if(BUILD_TESTING) + add_subdirectory(helpers/tests) +endif() + gaudi_add_library(k4FWCore SOURCES src/KeepDropSwitch.cpp LINK Gaudi::GaudiKernel podio::podioIO ROOT::Core ROOT::RIO ROOT::Tree EDM4HEP::utils diff --git a/k4FWCore/helpers/tests/README.md b/k4FWCore/helpers/tests/README.md index af8946950..bd8c19699 100644 --- a/k4FWCore/helpers/tests/README.md +++ b/k4FWCore/helpers/tests/README.md @@ -1,101 +1,68 @@ -# Running the generateFunctional tests +# generateFunctional tests -The tests run entirely in bash — no CTest or Python test framework needed. -Each script generates a C++ algorithm, runs `cmake`, and compiles it. +Each test generates a C++ algorithm with `generateFunctional`, configures it +with `cmake`, and compiles it. The tests are registered with CTest and run as +part of the standard k4FWCore test suite. --- -## Prerequisites - -A Key4hep environment and the installed `generateFunctional` script. -`uv` is not required — the tests invoke `python3` directly. - ---- - -## Setup +## Running via CTest (standard) ```bash -# 1. Clone and check out the branch -git clone https://github.com/key4hep/k4FWCore.git -cd k4FWCore -git remote add ianna https://github.com/ianna/k4FWCore.git -git fetch ianna -git checkout ianna/gaudi_functional_generator - -# 2. Source the Key4hep environment (use zsh, not bash/tcsh) -zsh +# Source the Key4hep environment source /cvmfs/sw.hsf.org/key4hep/setup.sh -# 3. Build and install (skip the test suite — it has an unrelated build error) +# Build (tests are enabled by default) mkdir build && cd build -cmake .. -DCMAKE_INSTALL_PREFIX=../install -DBUILD_TESTING=OFF -make -j$(nproc) install - -# 4. Put generateFunctional on PATH -export PATH="${PWD}/../install/bin:${PATH}" +cmake .. -DCMAKE_BUILD_TYPE=Release +make -j$(nproc) -# 5. Since uv is not available on lxplus, alias the script to use python3 -alias generateFunctional="python3 $(which generateFunctional)" - -# 6. Go back to the repo root -cd .. -``` - ---- - -## Run all tests - -```bash -bash k4FWCore/helpers/tests/run_all_tests.sh -``` - -Expected output: - -``` - test_consumer ... PASS - test_event_context ... PASS - test_filter ... PASS - test_gaudi_framework ... PASS - test_multitransformer ... PASS - test_producer ... PASS - test_runtime_consumer ... PASS - test_runtime_transformer ... PASS - test_transformer ... PASS - -9 passed, 0 failed +# Run only the generateFunctional tests +ctest -R GenerateFunctional --output-on-failure ``` Each test takes ~1–3 minutes on lxplus (cmake configure + compile per test). +Run them in parallel with `ctest -j9 -R GenerateFunctional`. --- -## Run a single test +## Running a single test manually + +The bash scripts can also be run directly without building or installing, +as long as the Key4hep environment is sourced: ```bash +source /cvmfs/sw.hsf.org/key4hep/setup.sh bash k4FWCore/helpers/tests/test_producer.sh ``` +The script finds `generateFunctional` via (in order): +1. `GENERATEFUNCTIONAL` env var (set automatically by CTest) +2. Installed `generateFunctional` on `PATH` +3. `../generateFunctional` relative to the `tests/` directory + --- ## What each test covers -| Script | Feature tested | -|---|---| -| `test_producer.sh` | Single output, property | -| `test_consumer.sh` | Single input, property | -| `test_transformer.sh` | Single in/out, `--private-properties` | -| `test_multitransformer.sh` | Multiple in/out, `--type-aliases`, `podio::UserDataCollection` | -| `test_filter.sh` | `FilterPredicate` | -| `test_runtime_consumer.sh` | `--runtime-inputs` / `KeyValues` vector input | -| `test_runtime_transformer.sh` | `--runtime-outputs` / `std::vector` return | -| `test_event_context.sh` | `--event-context`, `finalize()` placement | -| `test_gaudi_framework.sh` | `--framework gaudi`, `--namespace` | +| Script | CTest name | Feature | +|---|---|---| +| `test_producer.sh` | `GenerateFunctional_producer` | Single output, property | +| `test_consumer.sh` | `GenerateFunctional_consumer` | Single input, property | +| `test_transformer.sh` | `GenerateFunctional_transformer` | Single in/out, `--private-properties` | +| `test_multitransformer.sh` | `GenerateFunctional_multitransformer` | Multiple in/out, `--type-aliases`, `podio::UserDataCollection` | +| `test_filter.sh` | `GenerateFunctional_filter` | `FilterPredicate` | +| `test_runtime_consumer.sh` | `GenerateFunctional_runtime_consumer` | `--runtime-inputs` / `KeyValues` vector input | +| `test_runtime_transformer.sh` | `GenerateFunctional_runtime_transformer` | `--runtime-outputs` / `std::vector` return | +| `test_event_context.sh` | `GenerateFunctional_event_context` | `--event-context`, `finalize()` placement | +| `test_gaudi_framework.sh` | `GenerateFunctional_gaudi_framework` | `--framework gaudi`, `--namespace` | --- ## Notes - Each test creates an isolated `mktemp -d` sandbox, cleaned up automatically on exit. -- `_test_common.sh` must not be run directly — it is sourced by the other scripts. -- If `generateFunctional` is not on `PATH`, the scripts fall back to `../generateFunctional` (one level above `tests/`). -- Tests require the full Key4hep environment on `CMAKE_PREFIX_PATH`. Without it, cmake will fail to find `k4FWCore` or `Gaudi`. +- `_test_common.sh` is sourced by all test scripts — do not run it directly. +- Tests require the Key4hep environment on `CMAKE_PREFIX_PATH`. Source + `setup.sh` before building or running tests manually. +- `uv` is not required — the tests invoke `python3` directly. diff --git a/k4FWCore/helpers/tests/_test_common.sh b/k4FWCore/helpers/tests/_test_common.sh index 8885da8ba..ff181e845 100644 --- a/k4FWCore/helpers/tests/_test_common.sh +++ b/k4FWCore/helpers/tests/_test_common.sh @@ -10,8 +10,13 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# Prefer the installed command; fall back to the source copy one level up. -if command -v generateFunctional &>/dev/null; then +# Resolve the generator: +# 1. GENERATEFUNCTIONAL env var (set by CTest via CMakeLists.txt) +# 2. Installed command on PATH +# 3. Source copy one level above this directory +if [[ -n "${GENERATEFUNCTIONAL:-}" ]]; then + GENERATOR="${GENERATEFUNCTIONAL}" +elif command -v generateFunctional &>/dev/null; then GENERATOR="$(command -v generateFunctional)" else GENERATOR="${SCRIPT_DIR}/../generateFunctional" diff --git a/k4FWCore/helpers/tests/run_all_tests.sh b/k4FWCore/helpers/tests/run_all_tests.sh deleted file mode 100644 index e48bda16e..000000000 --- a/k4FWCore/helpers/tests/run_all_tests.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash -# run_all_tests.sh — run every test_*.sh script and report results. -# Requires a Key4hep / k4FWCore environment (k4FWCore, EDM4HEP, Gaudi on -# CMAKE_PREFIX_PATH). Source the Key4hep setup script before running: -# -# source /cvmfs/sw.hsf.org/key4hep/setup.sh -# bash k4FWCore/helpers/tests/run_all_tests.sh - -set -uo pipefail - -TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PASS=0 -FAIL=0 -FAILED_TESTS=() - -for test_script in "${TESTS_DIR}"/test_*.sh; do - name="$(basename "${test_script}" .sh)" - printf " %-35s" "${name} ..." - if output="$(bash "${test_script}" 2>&1)"; then - echo "PASS" - PASS=$((PASS + 1)) - else - echo "FAIL" - echo "${output}" | sed 's/^/ /' - FAIL=$((FAIL + 1)) - FAILED_TESTS+=("${name}") - fi -done - -echo "" -echo "Results: ${PASS} passed, ${FAIL} failed" - -if [[ ${FAIL} -gt 0 ]]; then - echo "Failed tests:" - for t in "${FAILED_TESTS[@]}"; do - echo " - ${t}" - done - exit 1 -fi From d5123c40332040302306434e1d3412bfa2d5443d Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Mon, 6 Jul 2026 21:36:00 +0200 Subject: [PATCH 32/36] add cmake --- k4FWCore/helpers/tests/CMakeLists.txt | 39 +++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 k4FWCore/helpers/tests/CMakeLists.txt diff --git a/k4FWCore/helpers/tests/CMakeLists.txt b/k4FWCore/helpers/tests/CMakeLists.txt new file mode 100644 index 000000000..bc0b5f5bf --- /dev/null +++ b/k4FWCore/helpers/tests/CMakeLists.txt @@ -0,0 +1,39 @@ +#[[ +Copyright (c) 2014-2024 Key4hep-Project. + +This file is part of Key4hep. +See https://key4hep.github.io/key4hep-doc/ for further info. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +]] + +set(GENERATE_FUNCTIONAL "${PROJECT_SOURCE_DIR}/k4FWCore/helpers/generateFunctional") +set(GF_TESTS_DIR "${CMAKE_CURRENT_LIST_DIR}") + +foreach(test + producer + consumer + transformer + multitransformer + filter + runtime_consumer + runtime_transformer + event_context + gaudi_framework) + add_test(NAME GenerateFunctional_${test} + COMMAND bash "${GF_TESTS_DIR}/test_${test}.sh" + ) + set_tests_properties(GenerateFunctional_${test} PROPERTIES + ENVIRONMENT "GENERATEFUNCTIONAL=${GENERATE_FUNCTIONAL}" + ) +endforeach() From f4da327ab089065b55bb4e3f1a58be0c048e5c1f Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Tue, 21 Jul 2026 09:49:22 +0200 Subject: [PATCH 33/36] Add copyright and license information to test_transformer.sh Added copyright information and licensing details to the script. --- k4FWCore/helpers/tests/test_transformer.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/k4FWCore/helpers/tests/test_transformer.sh b/k4FWCore/helpers/tests/test_transformer.sh index 324cb680b..79bbcebc8 100644 --- a/k4FWCore/helpers/tests/test_transformer.sh +++ b/k4FWCore/helpers/tests/test_transformer.sh @@ -1,4 +1,23 @@ #!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + # test_transformer.sh — build-test: k4FWCore Transformer (single in/out, private property) source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" From 86f00bfe85df406bfc328ee042028d8f6c891b3a Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Tue, 21 Jul 2026 18:33:24 +0200 Subject: [PATCH 34/36] fix: forward generator and CXX compiler to GenerateFunctional tests --- k4FWCore/helpers/generateFunctional | 2 +- k4FWCore/helpers/tests/CMakeLists.txt | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/k4FWCore/helpers/generateFunctional b/k4FWCore/helpers/generateFunctional index febe71e0e..93ac11331 100644 --- a/k4FWCore/helpers/generateFunctional +++ b/k4FWCore/helpers/generateFunctional @@ -487,7 +487,7 @@ _CMAKE_TEMPLATE = """\ # Generated by Gaudi Functional C++ Class Generator # Command: {{ spec.command_line }} cmake_minimum_required(VERSION 3.15) -project({{ spec.class_name }}Plugin) +project({{ spec.class_name }}Plugin LANGUAGES CXX) {% for pkg in find_packages %}{{ pkg }} {% endfor %} diff --git a/k4FWCore/helpers/tests/CMakeLists.txt b/k4FWCore/helpers/tests/CMakeLists.txt index bc0b5f5bf..b6345f5fe 100644 --- a/k4FWCore/helpers/tests/CMakeLists.txt +++ b/k4FWCore/helpers/tests/CMakeLists.txt @@ -33,7 +33,10 @@ foreach(test add_test(NAME GenerateFunctional_${test} COMMAND bash "${GF_TESTS_DIR}/test_${test}.sh" ) + # Forward the parent build's generator and C++ compiler so the inner + # cmake invocation in the test scripts doesn't fall back to "Unix Makefiles" + # (which fails in CI where only Ninja is available and make is not on PATH). set_tests_properties(GenerateFunctional_${test} PROPERTIES - ENVIRONMENT "GENERATEFUNCTIONAL=${GENERATE_FUNCTIONAL}" + ENVIRONMENT "GENERATEFUNCTIONAL=${GENERATE_FUNCTIONAL};CMAKE_GENERATOR=${CMAKE_GENERATOR};CXX=${CMAKE_CXX_COMPILER}" ) endforeach() From 2890b5a6d387d326071ef1b7b789d8f13a261fa3 Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Tue, 21 Jul 2026 20:47:03 +0200 Subject: [PATCH 35/36] Add license headers to helpers docs and test scripts --- k4FWCore/helpers/AGENT.md | 18 ++++++++++++++++++ k4FWCore/helpers/AGENT_USAGE.md | 18 ++++++++++++++++++ k4FWCore/helpers/README.md | 18 ++++++++++++++++++ k4FWCore/helpers/tests/README.md | 18 ++++++++++++++++++ k4FWCore/helpers/tests/_test_common.sh | 19 +++++++++++++++++++ k4FWCore/helpers/tests/test_consumer.sh | 19 +++++++++++++++++++ k4FWCore/helpers/tests/test_event_context.sh | 19 +++++++++++++++++++ k4FWCore/helpers/tests/test_filter.sh | 19 +++++++++++++++++++ .../helpers/tests/test_gaudi_framework.sh | 19 +++++++++++++++++++ .../helpers/tests/test_multitransformer.sh | 19 +++++++++++++++++++ k4FWCore/helpers/tests/test_producer.sh | 19 +++++++++++++++++++ .../helpers/tests/test_runtime_consumer.sh | 19 +++++++++++++++++++ .../helpers/tests/test_runtime_transformer.sh | 19 +++++++++++++++++++ 13 files changed, 243 insertions(+) diff --git a/k4FWCore/helpers/AGENT.md b/k4FWCore/helpers/AGENT.md index b12e5c3c0..decc19c52 100644 --- a/k4FWCore/helpers/AGENT.md +++ b/k4FWCore/helpers/AGENT.md @@ -1,3 +1,21 @@ + # AGENT.md — k4FWCore/helpers Context for AI agents working on `generateFunctional` and its test suite. diff --git a/k4FWCore/helpers/AGENT_USAGE.md b/k4FWCore/helpers/AGENT_USAGE.md index db39cddc2..88b20b662 100644 --- a/k4FWCore/helpers/AGENT_USAGE.md +++ b/k4FWCore/helpers/AGENT_USAGE.md @@ -1,3 +1,21 @@ + # Using an AI Agent with generateFunctional You can ask an AI agent (such as Claude in Cowork or via the API) to run diff --git a/k4FWCore/helpers/README.md b/k4FWCore/helpers/README.md index b4c42c155..1bf0ca582 100644 --- a/k4FWCore/helpers/README.md +++ b/k4FWCore/helpers/README.md @@ -1,3 +1,21 @@ + # generateFunctional — Gaudi Functional C++ Class Generator `generateFunctional` writes the boilerplate for a Gaudi Functional algorithm: the diff --git a/k4FWCore/helpers/tests/README.md b/k4FWCore/helpers/tests/README.md index bd8c19699..59905cf4e 100644 --- a/k4FWCore/helpers/tests/README.md +++ b/k4FWCore/helpers/tests/README.md @@ -1,3 +1,21 @@ + # generateFunctional tests Each test generates a C++ algorithm with `generateFunctional`, configures it diff --git a/k4FWCore/helpers/tests/_test_common.sh b/k4FWCore/helpers/tests/_test_common.sh index ff181e845..bd32c0f37 100644 --- a/k4FWCore/helpers/tests/_test_common.sh +++ b/k4FWCore/helpers/tests/_test_common.sh @@ -1,4 +1,23 @@ #!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + # _test_common.sh — sourced by every test_*.sh script. # Provides: GENERATOR path, SANDBOX temp dir, and run_cmake_build(). # diff --git a/k4FWCore/helpers/tests/test_consumer.sh b/k4FWCore/helpers/tests/test_consumer.sh index 9797f4c16..5cf02596b 100644 --- a/k4FWCore/helpers/tests/test_consumer.sh +++ b/k4FWCore/helpers/tests/test_consumer.sh @@ -1,4 +1,23 @@ #!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + # test_consumer.sh — build-test: k4FWCore Consumer (single input, one property) source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" diff --git a/k4FWCore/helpers/tests/test_event_context.sh b/k4FWCore/helpers/tests/test_event_context.sh index e0d811c9e..d725bfc76 100644 --- a/k4FWCore/helpers/tests/test_event_context.sh +++ b/k4FWCore/helpers/tests/test_event_context.sh @@ -1,4 +1,23 @@ #!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + # test_event_context.sh — build-test: k4FWCore Transformer with EventContext and finalize() source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" diff --git a/k4FWCore/helpers/tests/test_filter.sh b/k4FWCore/helpers/tests/test_filter.sh index b854d3ccb..da1814408 100644 --- a/k4FWCore/helpers/tests/test_filter.sh +++ b/k4FWCore/helpers/tests/test_filter.sh @@ -1,4 +1,23 @@ #!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + # test_filter.sh — build-test: k4FWCore FilterPredicate source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" diff --git a/k4FWCore/helpers/tests/test_gaudi_framework.sh b/k4FWCore/helpers/tests/test_gaudi_framework.sh index d8dd1d4f2..586ea592c 100644 --- a/k4FWCore/helpers/tests/test_gaudi_framework.sh +++ b/k4FWCore/helpers/tests/test_gaudi_framework.sh @@ -1,4 +1,23 @@ #!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + # test_gaudi_framework.sh — build-test: native Gaudi::Functional Transformer with namespace source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" diff --git a/k4FWCore/helpers/tests/test_multitransformer.sh b/k4FWCore/helpers/tests/test_multitransformer.sh index a674eb616..0990f9f4f 100644 --- a/k4FWCore/helpers/tests/test_multitransformer.sh +++ b/k4FWCore/helpers/tests/test_multitransformer.sh @@ -1,4 +1,23 @@ #!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + # test_multitransformer.sh — build-test: k4FWCore MultiTransformer (multiple outputs, type aliases) source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" diff --git a/k4FWCore/helpers/tests/test_producer.sh b/k4FWCore/helpers/tests/test_producer.sh index 49586805b..02259052d 100644 --- a/k4FWCore/helpers/tests/test_producer.sh +++ b/k4FWCore/helpers/tests/test_producer.sh @@ -1,4 +1,23 @@ #!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + # test_producer.sh — build-test: k4FWCore Producer (single output, one property) source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" diff --git a/k4FWCore/helpers/tests/test_runtime_consumer.sh b/k4FWCore/helpers/tests/test_runtime_consumer.sh index c41e163a7..ad11c54db 100644 --- a/k4FWCore/helpers/tests/test_runtime_consumer.sh +++ b/k4FWCore/helpers/tests/test_runtime_consumer.sh @@ -1,4 +1,23 @@ #!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + # test_runtime_consumer.sh — build-test: k4FWCore Consumer with runtime (variable-length) inputs source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" diff --git a/k4FWCore/helpers/tests/test_runtime_transformer.sh b/k4FWCore/helpers/tests/test_runtime_transformer.sh index 1e4090ef5..bdf4617e6 100644 --- a/k4FWCore/helpers/tests/test_runtime_transformer.sh +++ b/k4FWCore/helpers/tests/test_runtime_transformer.sh @@ -1,4 +1,23 @@ #!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + # test_runtime_transformer.sh — build-test: k4FWCore Transformer with runtime outputs source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" From 6cead9cecefab8347edb4cc8c1e55a7d2191060a Mon Sep 17 00:00:00 2001 From: Ianna Osborne Date: Tue, 21 Jul 2026 21:24:55 +0200 Subject: [PATCH 36/36] Fix generated project name clashing with module target (.components collision) --- k4FWCore/helpers/generateFunctional | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k4FWCore/helpers/generateFunctional b/k4FWCore/helpers/generateFunctional index 93ac11331..de01daeb8 100644 --- a/k4FWCore/helpers/generateFunctional +++ b/k4FWCore/helpers/generateFunctional @@ -487,7 +487,7 @@ _CMAKE_TEMPLATE = """\ # Generated by Gaudi Functional C++ Class Generator # Command: {{ spec.command_line }} cmake_minimum_required(VERSION 3.15) -project({{ spec.class_name }}Plugin LANGUAGES CXX) +project({{ spec.class_name }} LANGUAGES CXX) {% for pkg in find_packages %}{{ pkg }} {% endfor %}