From 5d60791a400a424d48f8d3f0e6e55d8b2b1f4abd Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Thu, 12 Feb 2026 09:45:07 +0100 Subject: [PATCH 01/19] Add compatibility with fmtlib formatting for collections As a first step using the ostream_formatter adaptor to re-use the existing operator<<. --- CMakeLists.txt | 4 ++-- cmake/podioConfig.cmake.in | 1 + include/podio/UserDataCollection.h | 5 +++++ include/podio/detail/LinkCollectionImpl.h | 5 +++++ python/templates/Collection.h.jinja2 | 5 +++++ src/CMakeLists.txt | 1 + tests/unittests/links.cpp | 15 +++++++++++++++ tests/unittests/unittest.cpp | 18 ++++++++++++++++++ 8 files changed, 52 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 10ac32a97..d694a2f7d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -172,6 +172,8 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/python/__version__.py.in install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR}) +find_package(fmt 9 REQUIRED) + #--- project specific subdirectories ------------------------------------------- add_subdirectory(src) @@ -188,8 +190,6 @@ if(BUILD_TESTING) add_subdirectory(tests) endif() -find_package(fmt 9 REQUIRED) - add_subdirectory(tools) add_subdirectory(python) diff --git a/cmake/podioConfig.cmake.in b/cmake/podioConfig.cmake.in index 70605d6bf..b60aba21e 100644 --- a/cmake/podioConfig.cmake.in +++ b/cmake/podioConfig.cmake.in @@ -30,6 +30,7 @@ if(NOT "@REQUIRE_PYTHON_VERSION@" STREQUAL "") else() find_dependency(Python3 COMPONENTS Interpreter Development) endif() +find_dependency(fmt @fmt_VERSION@) SET(ENABLE_SIO @ENABLE_SIO@) if(ENABLE_SIO) diff --git a/include/podio/UserDataCollection.h b/include/podio/UserDataCollection.h index 361bb0034..b65909457 100644 --- a/include/podio/UserDataCollection.h +++ b/include/podio/UserDataCollection.h @@ -8,6 +8,8 @@ #include "podio/detail/Pythonizations.h" #include "podio/utilities/TypeHelpers.h" +#include + #define PODIO_ADD_USER_TYPE(type) \ template <> \ consteval const char* userDataTypeName() { \ @@ -354,4 +356,7 @@ constexpr std::string_view UserDataCollection::dataTypeName; } // namespace podio +template +struct fmt::formatter> : fmt::ostream_formatter {}; + #endif diff --git a/include/podio/detail/LinkCollectionImpl.h b/include/podio/detail/LinkCollectionImpl.h index f6b83140c..8fe7ff0eb 100644 --- a/include/podio/detail/LinkCollectionImpl.h +++ b/include/podio/detail/LinkCollectionImpl.h @@ -28,6 +28,8 @@ #include "nlohmann/json.hpp" #endif +#include + #include #include #include @@ -460,4 +462,7 @@ void to_json(nlohmann::json& j, const podio::LinkCollection& collect } // namespace podio +template +struct fmt::formatter> : fmt::ostream_formatter {}; + #endif // PODIO_DETAIL_LINKCOLLECTIONIMPL_H diff --git a/python/templates/Collection.h.jinja2 b/python/templates/Collection.h.jinja2 index c74a50d5c..433788cdb 100644 --- a/python/templates/Collection.h.jinja2 +++ b/python/templates/Collection.h.jinja2 @@ -30,6 +30,8 @@ #include #include +#include + namespace podio { struct RelationNames; } @@ -280,6 +282,9 @@ void to_json(nlohmann::json& j, const {{ class.bare_type }}Collection& collectio {{ utils.namespace_close(class.namespace) }} +template <> +struct fmt::formatter<{% if class.namespace %}{{ class.namespace }}::{% endif %}{{ class.bare_type }}Collection> : fmt::ostream_formatter {}; + {{ workarounds.ld_library_path(class, "Collection", ["valueTypeName", "dataTypeName"]) }} #endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e5fecaa38..3fd7fc61b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -78,6 +78,7 @@ SET(core_headers PODIO_ADD_LIB_AND_DICT(podio "${core_headers}" "${core_sources}" selection.xml) target_compile_options(podio PRIVATE -pthread) target_link_libraries(podio PRIVATE Python3::Python) +target_link_libraries(podio PUBLIC fmt::fmt) # For Frame.h if (ROOT_VERSION VERSION_LESS 6.36) target_compile_definitions(podio PUBLIC PODIO_ROOT_OLDER_6_36=1) diff --git a/tests/unittests/links.cpp b/tests/unittests/links.cpp index 187390d26..58a56868b 100644 --- a/tests/unittests/links.cpp +++ b/tests/unittests/links.cpp @@ -15,6 +15,8 @@ #include "nlohmann/json.hpp" #endif +#include + #include #include #include @@ -443,6 +445,19 @@ TEST_CASE("LinkCollection basics", "[links]") { } } +TEST_CASE("LinkCollection formatting", "[links][formatting]") { + podio::LinkCollection links; + + auto formatted = fmt::format("{}", links); + REQUIRE_FALSE(formatted.empty()); + + links.create(); + links.create(); + + auto formatted2 = fmt::format("{}", links); + REQUIRE(formatted2.size() > formatted.size()); +} + auto createLinkCollections(const size_t nElements = 3u) { auto colls = std::make_tuple(TestLColl(), ExampleHitCollection(), ExampleClusterCollection()); diff --git a/tests/unittests/unittest.cpp b/tests/unittests/unittest.cpp index 6538b9588..f092ffeff 100644 --- a/tests/unittests/unittest.cpp +++ b/tests/unittests/unittest.cpp @@ -66,6 +66,8 @@ #include "podio/UserDataCollection.h" +#include + TEST_CASE("AutoDelete", "[basics][memory-management]") { auto coll = EventInfoCollection(); auto hit1 = MutableEventInfo(); @@ -413,6 +415,9 @@ TEST_CASE("UserDataCollection print", "[basics]") { coll.print(sstr); REQUIRE(sstr.str() == "[1, 2, 3]"); + + auto formatted = fmt::format("{}", coll); + REQUIRE_FALSE(formatted.empty()); } TEST_CASE("UserDataCollection access", "[basics]") { @@ -637,6 +642,19 @@ TEST_CASE("Equality", "[basics]") { REQUIRE(clu != cluster); } +TEST_CASE("Collection formatting", "[basics]") { + ExampleClusterCollection clusters; + auto cluster = clusters.create(); + cluster.energy(42.5f); + auto formatted = fmt::format("{}", clusters); + REQUIRE_FALSE(formatted.empty()); + + ExampleWithComponentCollection components; + auto comp = components.create(); + formatted = fmt::format("{}", components); + REQUIRE_FALSE(formatted.empty()); +} + TEST_CASE("UserInitialization", "[basics][code-gen]") { ExampleWithUserInitCollection coll; // Default initialization values should work even through the create factory From 52fbb9d7b055e8f8537ad24478e7c4f7d3bc1b26 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Thu, 12 Feb 2026 10:13:48 +0100 Subject: [PATCH 02/19] Add fmt compatibility for ObjectID --- include/podio/ObjectID.h | 5 +++++ tests/unittests/unittest.cpp | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/include/podio/ObjectID.h b/include/podio/ObjectID.h index 8efe9de7d..62965eae5 100644 --- a/include/podio/ObjectID.h +++ b/include/podio/ObjectID.h @@ -1,6 +1,8 @@ #ifndef PODIO_OBJECTID_H #define PODIO_OBJECTID_H +#include + #include #include #include @@ -60,4 +62,7 @@ struct std::hash { } }; +template <> +struct fmt::formatter : fmt::ostream_formatter {}; + #endif diff --git a/tests/unittests/unittest.cpp b/tests/unittests/unittest.cpp index f092ffeff..5b37a151e 100644 --- a/tests/unittests/unittest.cpp +++ b/tests/unittests/unittest.cpp @@ -20,6 +20,7 @@ // podio specific includes #include "podio/Frame.h" #include "podio/GenericParameters.h" +#include "podio/ObjectID.h" #include "podio/ROOTLegacyReader.h" #include "podio/ROOTReader.h" #include "podio/ROOTWriter.h" @@ -68,6 +69,17 @@ #include +TEST_CASE("ObjectID formatting", "[basics][formatting]") { + auto objId = podio::ObjectID{}; + auto formatted = fmt::format("{}", objId); + REQUIRE(formatted == "ffffffff|-1"); + + objId.collectionID = 42; + objId.index = 123; + formatted = fmt::format("{}", objId); + REQUIRE(formatted == fmt::format("{:8x}|123", 42)); +} + TEST_CASE("AutoDelete", "[basics][memory-management]") { auto coll = EventInfoCollection(); auto hit1 = MutableEventInfo(); From 2a2bc9bb2933bdcc2df3bf116fb9e19790a425fa Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Thu, 12 Feb 2026 10:14:04 +0100 Subject: [PATCH 03/19] Add fmtlib compatibility for Links --- include/podio/detail/Link.h | 9 +++++++-- tests/unittests/links.cpp | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/include/podio/detail/Link.h b/include/podio/detail/Link.h index 811a079b8..ec6424cc1 100644 --- a/include/podio/detail/Link.h +++ b/include/podio/detail/Link.h @@ -12,6 +12,8 @@ #include "nlohmann/json.hpp" #endif +#include + #include #include #include @@ -348,8 +350,8 @@ class LinkT { podio::utils::MaybeSharedPtr m_obj{nullptr}; }; -template -std::ostream& operator<<(std::ostream& os, const Link& link) { +template +std::ostream& operator<<(std::ostream& os, const LinkT& link) { if (!link.isAvailable()) { return os << "[not available]"; } @@ -382,4 +384,7 @@ struct std::hash> { } }; +template +struct fmt::formatter> : fmt::ostream_formatter {}; + #endif // PODIO_DETAIL_LINK_H diff --git a/tests/unittests/links.cpp b/tests/unittests/links.cpp index 58a56868b..739a86019 100644 --- a/tests/unittests/links.cpp +++ b/tests/unittests/links.cpp @@ -301,6 +301,22 @@ TEST_CASE("Links templated accessors", "[links]") { } } // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) + +TEST_CASE("Link formatting", "[links]") { + TestL link; + auto formatted = fmt::format("{}", link); + REQUIRE_FALSE(formatted.empty()); + REQUIRE(formatted != "[not available]"); + + auto emptyLink = TestL::makeEmpty(); + auto emptyFmt = fmt::format("{}", emptyLink); + REQUIRE(emptyFmt == "[not available]"); + + TestMutL mutLink; + formatted = fmt::format("{}", mutLink); + REQUIRE(formatted != "[not avialable]"); +} + TEST_CASE("LinkCollection collection concept", "[links][concepts]") { STATIC_REQUIRE(podio::CollectionType); STATIC_REQUIRE(std::is_same_v, TestL>); From 698a74e47ac97dd35d8281b72c505b6af368b113 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Thu, 12 Feb 2026 10:26:40 +0100 Subject: [PATCH 04/19] Add fmt compatibility for generated objects --- python/templates/MutableObject.h.jinja2 | 3 +++ python/templates/Object.h.jinja2 | 4 ++++ python/templates/macros/declarations.jinja2 | 6 ++++++ tests/unittests/unittest.cpp | 16 ++++++++++++++++ 4 files changed, 29 insertions(+) diff --git a/python/templates/MutableObject.h.jinja2 b/python/templates/MutableObject.h.jinja2 index ecb9ca5af..87b61dbfb 100644 --- a/python/templates/MutableObject.h.jinja2 +++ b/python/templates/MutableObject.h.jinja2 @@ -62,4 +62,7 @@ private: {{ macros.std_hash(class, prefix='Mutable') }} +{{ macros.ostream_formatter(class, prefix='Mutable') }} + + #endif diff --git a/python/templates/Object.h.jinja2 b/python/templates/Object.h.jinja2 index 9c77afd4d..af6e0cb36 100644 --- a/python/templates/Object.h.jinja2 +++ b/python/templates/Object.h.jinja2 @@ -15,6 +15,8 @@ #include "podio/utilities/MaybeSharedPtr.h" #include "podio/detail/OrderKey.h" +#include + #include #include @@ -79,6 +81,8 @@ std::ostream& operator<<(std::ostream& o, const {{ class.bare_type }}& value); {{ macros.std_hash(class) }} +{{ macros.ostream_formatter(class) }} + {{ workarounds.ld_library_path(class) }} #endif diff --git a/python/templates/macros/declarations.jinja2 b/python/templates/macros/declarations.jinja2 index 83ed49ddc..346db8662 100644 --- a/python/templates/macros/declarations.jinja2 +++ b/python/templates/macros/declarations.jinja2 @@ -158,3 +158,9 @@ struct std::hash<{{ namespace }}{{ prefix }}{{ class.bare_type }}> { } }; {% endmacro %} + +{% macro ostream_formatter(class, prefix='') %} +{% set namespace = class.namespace + '::' if class.namespace else '' %} +template <> +struct fmt::formatter<{{ namespace }}{{ prefix }}{{ class.bare_type }}> : fmt::ostream_formatter {}; +{% endmacro %} diff --git a/tests/unittests/unittest.cpp b/tests/unittests/unittest.cpp index 5b37a151e..9ac5a8794 100644 --- a/tests/unittests/unittest.cpp +++ b/tests/unittests/unittest.cpp @@ -152,6 +152,22 @@ TEST_CASE("makeEmpty", "[basics]") { REQUIRE(hit.energy() == 0); } +TEST_CASE("Object formatting", "[basics][formatting]") { + ExampleCluster cluster; + auto formatted = fmt::format("{}", cluster); + REQUIRE_FALSE(formatted.empty()); + REQUIRE(formatted != "[not avaialble]"); + + cluster = ExampleCluster::makeEmpty(); + formatted = fmt::format("{}", cluster); + REQUIRE(formatted == "[not available]"); + + auto mutCluster = MutableExampleCluster{}; + formatted = fmt::format("{}", mutCluster); + REQUIRE_FALSE(formatted.empty()); + REQUIRE(formatted != "[not available]"); +} + TEST_CASE("Cyclic", "[basics][relations][memory-management]") { auto coll1 = ExampleForCyclicDependency1Collection(); auto start = coll1.create(); From 44dd932bf97e2e24144671f75cb64e7922bedd02 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Thu, 12 Feb 2026 19:49:17 +0100 Subject: [PATCH 05/19] Add fmt compatibility for generated components --- python/templates/Component.h.jinja2 | 7 +++++++ tests/unittests/unittest.cpp | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/python/templates/Component.h.jinja2 b/python/templates/Component.h.jinja2 index 19a9e7b8b..660bf6c31 100644 --- a/python/templates/Component.h.jinja2 +++ b/python/templates/Component.h.jinja2 @@ -11,6 +11,8 @@ {% if generate_current_version %} #include +#include + #if defined(PODIO_JSON_OUTPUT) && !defined(__CLING__) #include "nlohmann/json_fwd.hpp" #endif @@ -58,4 +60,9 @@ public: {{ utils.namespace_close(class.namespace) }} +{% if generate_current_version %} +template <> +struct fmt::formatter<{{ class.full_type }}> : fmt::ostream_formatter {}; +{% endif %} + #endif diff --git a/tests/unittests/unittest.cpp b/tests/unittests/unittest.cpp index 9ac5a8794..14aa8092a 100644 --- a/tests/unittests/unittest.cpp +++ b/tests/unittests/unittest.cpp @@ -61,6 +61,7 @@ #include "datamodel/MutableExampleWithArray.h" #include "datamodel/MutableExampleWithComponent.h" #include "datamodel/MutableExampleWithExternalExtraCode.h" +#include "datamodel/NamespaceInNamespaceStruct.h" #include "datamodel/StructWithExtraCode.h" #include "datamodel/datamodel.h" #include "extension_model/extension_model.h" @@ -166,6 +167,14 @@ TEST_CASE("Object formatting", "[basics][formatting]") { formatted = fmt::format("{}", mutCluster); REQUIRE_FALSE(formatted.empty()); REQUIRE(formatted != "[not available]"); + + auto typeWithComponent = ExampleWithArrayComponent{}; + formatted = fmt::format("{}", typeWithComponent); + REQUIRE_FALSE(formatted.empty()); + + auto nspComp = ex2::NamespaceInNamespaceStruct{}; + formatted = fmt::format("{}", nspComp); + REQUIRE_FALSE(formatted.empty()); } TEST_CASE("Cyclic", "[basics][relations][memory-management]") { From a6d084b99071255a1d4d9b02770d47b1f04dc08c Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Fri, 13 Feb 2026 09:40:24 +0100 Subject: [PATCH 06/19] Add fmt compatibility for interface types --- python/templates/Interface.h.jinja2 | 5 +++++ tests/unittests/interface_types.cpp | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/python/templates/Interface.h.jinja2 b/python/templates/Interface.h.jinja2 index 569f13125..6874852e0 100644 --- a/python/templates/Interface.h.jinja2 +++ b/python/templates/Interface.h.jinja2 @@ -14,6 +14,8 @@ #include "podio/utilities/TypeHelpers.h" #include "podio/detail/OrderKey.h" +#include + #include #include #include @@ -196,4 +198,7 @@ struct std::hash<{{ class.full_type }}> { } }; +template <> +struct fmt::formatter<{{ class.full_type }}> : fmt::ostream_formatter {}; + #endif diff --git a/tests/unittests/interface_types.cpp b/tests/unittests/interface_types.cpp index 6996cc746..d0c4a2a07 100644 --- a/tests/unittests/interface_types.cpp +++ b/tests/unittests/interface_types.cpp @@ -195,3 +195,14 @@ TEST_CASE("InterfaceType extension model", "[interface-types][extension]") { REQUIRE(wrapper.isA()); REQUIRE(wrapper.as().energy() == 4.2f); } + +TEST_CASE("InterfaceType formatting", "[interface-types][basics][formatting]") { + auto iface = iextension::EnergyInterface::makeEmpty(); + auto formatted = fmt::format("{}", iface); + REQUIRE(formatted == "[not available]"); + + iface = ExampleCluster{}; + formatted = fmt::format("{}", iface); + REQUIRE_FALSE(formatted.empty()); + REQUIRE(formatted != "[not available]"); +} From ac81cc23074448a0e3e3907eaf2672177ed256d2 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Thu, 12 Feb 2026 13:42:51 +0100 Subject: [PATCH 07/19] Add fmt support for ObjectId Implement the existing operator<< in terms of format_to --- include/podio/ObjectID.h | 32 ++++++++++++++++++++++---------- tests/unittests/unittest.cpp | 6 ++++++ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/include/podio/ObjectID.h b/include/podio/ObjectID.h index 62965eae5..5263a28c4 100644 --- a/include/podio/ObjectID.h +++ b/include/podio/ObjectID.h @@ -1,11 +1,11 @@ #ifndef PODIO_OBJECTID_H #define PODIO_OBJECTID_H -#include +#include #include #include -#include +#include #include #if defined(PODIO_JSON_OUTPUT) && !defined(__CLING__) @@ -37,13 +37,6 @@ class ObjectID { } }; -inline std::ostream& operator<<(std::ostream& os, const podio::ObjectID& id) { - const auto oldFlags = os.flags(); - os << std::hex << std::setw(8) << id.collectionID; - os.flags(oldFlags); - return os << "|" << id.index; -} - #if defined(PODIO_JSON_OUTPUT) && !defined(__CLING__) inline void to_json(nlohmann::json& j, const podio::ObjectID& id) { j = nlohmann::json{{"collectionID", id.collectionID}, {"index", id.index}}; @@ -63,6 +56,25 @@ struct std::hash { }; template <> -struct fmt::formatter : fmt::ostream_formatter {}; +struct fmt::formatter { + constexpr auto parse(fmt::format_parse_context& ctx) { + auto it = ctx.begin(); + if (it != ctx.end() && *it != '}') { + fmt::throw_format_error("Invalid format. ObjectId does not support specifiers"); + } + return it; + } + + auto format(const podio::ObjectID& obj, fmt::format_context& ctx) const { + return fmt::format_to(ctx.out(), "{:8x}|{}", obj.collectionID, obj.index); + } +}; + +namespace podio { +inline std::ostream& operator<<(std::ostream& os, const podio::ObjectID& id) { + fmt::format_to(std::ostreambuf_iterator(os), "{}", id); + return os; +} +} // namespace podio #endif diff --git a/tests/unittests/unittest.cpp b/tests/unittests/unittest.cpp index 14aa8092a..ef13c4705 100644 --- a/tests/unittests/unittest.cpp +++ b/tests/unittests/unittest.cpp @@ -70,6 +70,8 @@ #include +#include + TEST_CASE("ObjectID formatting", "[basics][formatting]") { auto objId = podio::ObjectID{}; auto formatted = fmt::format("{}", objId); @@ -79,6 +81,10 @@ TEST_CASE("ObjectID formatting", "[basics][formatting]") { objId.index = 123; formatted = fmt::format("{}", objId); REQUIRE(formatted == fmt::format("{:8x}|123", 42)); + + std::stringstream sstr; + sstr << objId; + REQUIRE(sstr.str() == fmt::format("{:8x}|123", 42)); } TEST_CASE("AutoDelete", "[basics][memory-management]") { From 9fb2833eaf4938ba8da1f6cf74d95aa052841ea2 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Thu, 12 Feb 2026 14:23:44 +0100 Subject: [PATCH 08/19] Add fmt support for Links --- include/podio/detail/Link.h | 40 ++++++++++++++++++++++++------------- tests/unittests/links.cpp | 6 ++++++ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/include/podio/detail/Link.h b/include/podio/detail/Link.h index ec6424cc1..89f0cfa56 100644 --- a/include/podio/detail/Link.h +++ b/include/podio/detail/Link.h @@ -12,7 +12,7 @@ #include "nlohmann/json.hpp" #endif -#include +#include #include #include @@ -350,18 +350,6 @@ class LinkT { podio::utils::MaybeSharedPtr m_obj{nullptr}; }; -template -std::ostream& operator<<(std::ostream& os, const LinkT& link) { - if (!link.isAvailable()) { - return os << "[not available]"; - } - - return os << " id: " << link.id() << '\n' - << " weight: " << link.getWeight() << '\n' - << " from: " << link.getFrom().id() << '\n' - << " to: " << link.getTo().id() << '\n'; -} - #if defined(PODIO_JSON_OUTPUT) && !defined(__CLING__) template void to_json(nlohmann::json& j, const podio::LinkT& link) { @@ -385,6 +373,30 @@ struct std::hash> { }; template -struct fmt::formatter> : fmt::ostream_formatter {}; +struct fmt::formatter> { + constexpr auto parse(fmt::format_parse_context& ctx) { + auto it = ctx.begin(); + if (it != ctx.end() && *it != '}') { + fmt::throw_format_error("Invalid format. Links do not support specifiers"); + } + return it; + } + + auto format(const podio::LinkT& link, fmt::format_context& ctx) const { + if (!link.isAvailable()) { + return fmt::format_to(ctx.out(), "[not available]"); + } + return fmt::format_to(ctx.out(), " id: {}\n weight: {}\n from: {}\n to: {}\n", link.id(), link.getWeight(), + link.getFrom().id(), link.getTo().id()); + } +}; + +namespace podio { +template +std::ostream& operator<<(std::ostream& os, const LinkT& link) { + fmt::format_to(std::ostreambuf_iterator(os), "{}", link); + return os; +} +} // namespace podio #endif // PODIO_DETAIL_LINK_H diff --git a/tests/unittests/links.cpp b/tests/unittests/links.cpp index 739a86019..2894f6f37 100644 --- a/tests/unittests/links.cpp +++ b/tests/unittests/links.cpp @@ -307,6 +307,12 @@ TEST_CASE("Link formatting", "[links]") { auto formatted = fmt::format("{}", link); REQUIRE_FALSE(formatted.empty()); REQUIRE(formatted != "[not available]"); + std::stringstream manual; + manual << " id: " << link.id() << '\n' + << " weight: " << link.getWeight() << '\n' + << " from: " << link.getFrom().id() << '\n' + << " to: " << link.getTo().id() << '\n'; + REQUIRE(formatted == manual.str()); auto emptyLink = TestL::makeEmpty(); auto emptyFmt = fmt::format("{}", emptyLink); From 0cf8ce17f818ee7a31982bbd6fdb579ca96c7111 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Thu, 12 Feb 2026 16:04:43 +0100 Subject: [PATCH 09/19] Add fmt support for LinkCollections --- include/podio/detail/LinkCollectionImpl.h | 48 ++++++++++++++--------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/include/podio/detail/LinkCollectionImpl.h b/include/podio/detail/LinkCollectionImpl.h index 8fe7ff0eb..182bd8570 100644 --- a/include/podio/detail/LinkCollectionImpl.h +++ b/include/podio/detail/LinkCollectionImpl.h @@ -377,24 +377,6 @@ class LinkCollection : public podio::CollectionBase { mutable CollectionDataT m_storage{}; }; -template -std::ostream& operator<<(std::ostream& o, const LinkCollection& v) { - const auto old_flags = o.flags(); - o << " id: weight:" << '\n'; - for (const auto&& el : v) { - o << std::scientific << std::showpos << std::setw(12) << el.id() << " " << std::setw(12) << " " << el.getWeight() - << '\n'; - - o << " from : "; - o << el.getFrom().id() << std::endl; - o << " to : "; - o << el.getTo().id() << std::endl; - } - - o.flags(old_flags); - return o; -} - namespace detail { template podio::CollectionReadBuffers createLinkBuffers(bool subsetColl) { @@ -463,6 +445,34 @@ void to_json(nlohmann::json& j, const podio::LinkCollection& collect } // namespace podio template -struct fmt::formatter> : fmt::ostream_formatter {}; +struct fmt::formatter> { + constexpr auto parse(fmt::format_parse_context& ctx) { + auto it = ctx.begin(); + if (it != ctx.end() && *it != '}') { + fmt::throw_format_error("Invalid format. LinkCollections do not support specifiers"); + } + return it; + } + + auto format(const podio::LinkCollection& coll, fmt::format_context& ctx) const { + auto out = ctx.out(); + + out = fmt::format_to(out, " id: weight:\n"); + for (const auto&& elem : coll) { + out = fmt::format_to(out, "{} {:+12e}\n", elem.id(), elem.getWeight()); + out = fmt::format_to(out, " from : {}\n to : {}\n", elem.getFrom().id(), elem.getTo().id()); + } + + return out; + } +}; + +namespace podio { +template +std::ostream& operator<<(std::ostream& o, const LinkCollection& v) { + fmt::format_to(std::ostreambuf_iterator(o), "{}", v); + return o; +} +} // namespace podio #endif // PODIO_DETAIL_LINKCOLLECTIONIMPL_H From 8b3b3b4eba62893b8c54a3e08f73bac6399c2609 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Thu, 12 Feb 2026 16:18:05 +0100 Subject: [PATCH 10/19] Add fmt support for Version --- podioVersion.in.h | 41 +++++++++++++++++++++++++---------- tools/src/podio-dump-tool.cpp | 3 --- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/podioVersion.in.h b/podioVersion.in.h index d46404d2f..576fde28e 100644 --- a/podioVersion.in.h +++ b/podioVersion.in.h @@ -1,9 +1,11 @@ #ifndef PODIO_PODIOVERSION_H #define PODIO_PODIOVERSION_H +#include + #include #include -#include +#include #include // Some preprocessor constants and macros for the use cases where they might be @@ -55,19 +57,9 @@ struct Version { #undef DEFINE_COMP_OPERATOR - explicit operator std::string() const { - std::stringstream ss; - ss << *this; - return ss.str(); - } - - friend std::ostream& operator<<(std::ostream&, const Version& v); + explicit operator std::string() const; }; -inline std::ostream& operator<<(std::ostream& os, const Version& v) { - return os << v.major << "." << v.minor << "." << v.patch; -} - /// The current build version static constexpr Version build_version{podio_VERSION_MAJOR, podio_VERSION_MINOR, podio_VERSION_PATCH}; @@ -79,4 +71,29 @@ static constexpr Version decode_version(unsigned long version) noexcept { } } // namespace podio::version +template <> +struct fmt::formatter { + constexpr auto parse(fmt::format_parse_context& ctx) { + auto it = ctx.begin(); + if (it != ctx.end() && *it != '}') { + fmt::throw_format_error("Invalid format. Version does not support specifiers"); + } + return it; + } + + auto format(const podio::version::Version& version, fmt::format_context& ctx) { + return fmt::format_to(ctx.out(), "{}.{}.{}", version.major, version.minor, version.patch); + } +}; + +namespace podio::version { +inline std::ostream& operator<<(std::ostream& os, const Version& v) { + fmt::format_to(std::ostreambuf_iterator(os), "{}", v); + return os; +} + +inline Version::operator std::string() const { + return fmt::format("{}", *this); +} +} // namespace podio::version #endif diff --git a/tools/src/podio-dump-tool.cpp b/tools/src/podio-dump-tool.cpp index 988a6c2fe..530a8e581 100644 --- a/tools/src/podio-dump-tool.cpp +++ b/tools/src/podio-dump-tool.cpp @@ -18,9 +18,6 @@ #include #include -template <> -struct fmt::formatter : ostream_formatter {}; - struct ParsedArgs { std::string inputFile{}; std::string category{"events"}; From 5b8078de45ad4d97ab91364e32d078522e528e3b Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Fri, 13 Feb 2026 14:16:30 +0100 Subject: [PATCH 11/19] Make UserDataCollection formattable as range --- include/podio/UserDataCollection.h | 8 ++++---- tests/unittests/unittest.cpp | 8 +++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/include/podio/UserDataCollection.h b/include/podio/UserDataCollection.h index b65909457..63840991e 100644 --- a/include/podio/UserDataCollection.h +++ b/include/podio/UserDataCollection.h @@ -9,6 +9,9 @@ #include "podio/utilities/TypeHelpers.h" #include +#include + +#include #define PODIO_ADD_USER_TYPE(type) \ template <> \ @@ -323,7 +326,7 @@ using UserDataCollectionTypes = decltype(std::apply( template std::ostream& operator<<(std::ostream& o, const podio::UserDataCollection& coll) { - coll.print(o); + fmt::format_to(std::ostreambuf_iterator(o), "{}", coll); return o; } @@ -356,7 +359,4 @@ constexpr std::string_view UserDataCollection::dataTypeName; } // namespace podio -template -struct fmt::formatter> : fmt::ostream_formatter {}; - #endif diff --git a/tests/unittests/unittest.cpp b/tests/unittests/unittest.cpp index ef13c4705..729ac9bc2 100644 --- a/tests/unittests/unittest.cpp +++ b/tests/unittests/unittest.cpp @@ -460,7 +460,11 @@ TEST_CASE("UserDataCollection print", "[basics]") { REQUIRE(sstr.str() == "[1, 2, 3]"); auto formatted = fmt::format("{}", coll); - REQUIRE_FALSE(formatted.empty()); + REQUIRE(formatted == "[1, 2, 3]"); + + std::stringstream sstr2; + sstr2 << coll; + REQUIRE(sstr2.str() == formatted); } TEST_CASE("UserDataCollection access", "[basics]") { @@ -696,6 +700,8 @@ TEST_CASE("Collection formatting", "[basics]") { auto comp = components.create(); formatted = fmt::format("{}", components); REQUIRE_FALSE(formatted.empty()); + + formatted = fmt::format("{}", cluster.Hits()); } TEST_CASE("UserInitialization", "[basics][code-gen]") { From 58f3ab1b93a7f6510d3d1c97ca153629980002a0 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Fri, 13 Feb 2026 15:40:30 +0100 Subject: [PATCH 12/19] Ensure Links and LinkCollections remain formattable Necessary to ensure that things still compile if fmt/ranges.h is included to avoid ambiguities that would otherwise arise because LinkT behaves tuple-like enough to trigger tuple formatting. Collections are ranges so we have to explicitly opt-out. --- include/podio/detail/Link.h | 7 +++++++ include/podio/detail/LinkCollectionImpl.h | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/include/podio/detail/Link.h b/include/podio/detail/Link.h index 89f0cfa56..7d5e4d96e 100644 --- a/include/podio/detail/Link.h +++ b/include/podio/detail/Link.h @@ -13,6 +13,7 @@ #endif #include +#include #include #include @@ -391,6 +392,12 @@ struct fmt::formatter> { } }; +// Disable fmt's tuple formatter for LinkT to avoid ambiguity with the custom +// formatter above. This is necessary because opting tuple_size and +// tuple_element makes LinkT behave like a tuple to the compiler +template +struct fmt::is_tuple_formattable, Char> : std::false_type {}; + namespace podio { template std::ostream& operator<<(std::ostream& os, const LinkT& link) { diff --git a/include/podio/detail/LinkCollectionImpl.h b/include/podio/detail/LinkCollectionImpl.h index 182bd8570..aefddb7a8 100644 --- a/include/podio/detail/LinkCollectionImpl.h +++ b/include/podio/detail/LinkCollectionImpl.h @@ -28,7 +28,8 @@ #include "nlohmann/json.hpp" #endif -#include +#include +#include #include #include @@ -467,6 +468,11 @@ struct fmt::formatter> { } }; +// Disable fmt's range formatter for LinkCollection to avoid ambiguity with the +// custom formatter above +template +struct fmt::is_range, char> : std::false_type {}; + namespace podio { template std::ostream& operator<<(std::ostream& o, const LinkCollection& v) { From 848f148407db3f8ec1448c75270e91e23e06f176 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Fri, 13 Feb 2026 16:07:27 +0100 Subject: [PATCH 13/19] Add proper fmt support for user facing handles --- python/templates/MutableObject.cc.jinja2 | 2 ++ python/templates/MutableObject.h.jinja2 | 6 ++-- python/templates/Object.cc.jinja2 | 11 +++++-- python/templates/Object.h.jinja2 | 4 +-- python/templates/macros/declarations.jinja2 | 15 +++++++-- .../templates/macros/implementations.jinja2 | 33 ++++++++----------- tests/unittests/unittest.cpp | 4 +++ 7 files changed, 46 insertions(+), 29 deletions(-) diff --git a/python/templates/MutableObject.cc.jinja2 b/python/templates/MutableObject.cc.jinja2 index 329a0f830..cdc9e3dd1 100644 --- a/python/templates/MutableObject.cc.jinja2 +++ b/python/templates/MutableObject.cc.jinja2 @@ -8,6 +8,8 @@ {{ include }} {% endfor %} +#include + #if defined(PODIO_JSON_OUTPUT) && !defined(__CLING__) #include "nlohmann/json.hpp" #endif diff --git a/python/templates/MutableObject.h.jinja2 b/python/templates/MutableObject.h.jinja2 index 87b61dbfb..7d27bf033 100644 --- a/python/templates/MutableObject.h.jinja2 +++ b/python/templates/MutableObject.h.jinja2 @@ -15,6 +15,8 @@ #include "podio/utilities/MaybeSharedPtr.h" +#include + #include #if defined(PODIO_JSON_OUTPUT) && !defined(__CLING__) @@ -62,7 +64,7 @@ private: {{ macros.std_hash(class, prefix='Mutable') }} -{{ macros.ostream_formatter(class, prefix='Mutable') }} - +template <> +struct fmt::formatter<{{ class.namespace }}::Mutable{{ class.bare_type }}> : fmt::formatter<{{ class.full_type }}>{}; #endif diff --git a/python/templates/Object.cc.jinja2 b/python/templates/Object.cc.jinja2 index 3e4fb95a1..c9f1d77d6 100644 --- a/python/templates/Object.cc.jinja2 +++ b/python/templates/Object.cc.jinja2 @@ -8,6 +8,8 @@ {{ include }} {% endfor %} +#include + #if defined(PODIO_JSON_OUTPUT) && !defined(__CLING__) #include "nlohmann/json.hpp" #endif @@ -34,9 +36,10 @@ {{ macros.common_object_funcs(class) }} -{{ macros.ostream_operator(class.bare_type, Members, - OneToOneRelations, OneToManyRelations + VectorMembers, - use_get_syntax) }} +std::ostream& operator<<(std::ostream& o, const {{ class.bare_type }}& value) { + fmt::format_to(std::ostreambuf_iterator(o), "{}", value); + return o; +} {{ macros.json_output(class, Members, OneToOneRelations, OneToManyRelations, @@ -47,3 +50,5 @@ podio::detail::OrderKey podio::detail::getOrderKey(const {{ class.namespace }}::{{ class.bare_type }}& obj) { return podio::detail::OrderKey{obj.m_obj.get()}; } + +{{ macros.formatter(class, Members, OneToOneRelations, OneToManyRelations + VectorMembers, use_get_syntax) }} diff --git a/python/templates/Object.h.jinja2 b/python/templates/Object.h.jinja2 index af6e0cb36..f35dd363e 100644 --- a/python/templates/Object.h.jinja2 +++ b/python/templates/Object.h.jinja2 @@ -15,7 +15,7 @@ #include "podio/utilities/MaybeSharedPtr.h" #include "podio/detail/OrderKey.h" -#include +#include #include #include @@ -81,7 +81,7 @@ std::ostream& operator<<(std::ostream& o, const {{ class.bare_type }}& value); {{ macros.std_hash(class) }} -{{ macros.ostream_formatter(class) }} +{{ macros.formatter(class) }} {{ workarounds.ld_library_path(class) }} diff --git a/python/templates/macros/declarations.jinja2 b/python/templates/macros/declarations.jinja2 index 346db8662..0db7ab81c 100644 --- a/python/templates/macros/declarations.jinja2 +++ b/python/templates/macros/declarations.jinja2 @@ -159,8 +159,19 @@ struct std::hash<{{ namespace }}{{ prefix }}{{ class.bare_type }}> { }; {% endmacro %} -{% macro ostream_formatter(class, prefix='') %} +{% macro formatter(class, prefix='') %} {% set namespace = class.namespace + '::' if class.namespace else '' %} template <> -struct fmt::formatter<{{ namespace }}{{ prefix }}{{ class.bare_type }}> : fmt::ostream_formatter {}; +struct fmt::formatter<{{ namespace }}{{ prefix }}{{ class.bare_type }}> { + constexpr auto parse(fmt::format_parse_context& ctx) { + auto it = ctx.begin(); + if (it != ctx.end() && *it != '}') { + fmt::throw_format_error("Invalid format. {{ class.bare_type }} does not support specifiers"); + } + return it; + } + + fmt::format_context::iterator format(const {{ namespace }}{{ prefix }}{{ class.bare_type }}& value, fmt::format_context& ctx) const; +}; + {% endmacro %} diff --git a/python/templates/macros/implementations.jinja2 b/python/templates/macros/implementations.jinja2 index 6fca1698b..3c119773a 100644 --- a/python/templates/macros/implementations.jinja2 +++ b/python/templates/macros/implementations.jinja2 @@ -191,42 +191,35 @@ bool {{ full_type }}::operator==(const {{ inverse_type }}& other) const { } {%- endmacro %} - -{% macro ostream_operator(type, members, single_relations, multi_relations, get_syntax) %} -std::ostream& operator<<(std::ostream& o, const {{ type }}& value) { +{% macro formatter(class, members, single_relations, multi_relations, get_syntax, prefix='') %} +{% set namespace = class.namespace + '::' if class.namespace else '' %} +fmt::format_context::iterator fmt::formatter<{{ namespace }}{{ prefix }}{{ class.bare_type }}>::format(const {{ namespace }}{{ prefix }}{{ class.bare_type }}& value, fmt::format_context& ctx) const { if (!value.isAvailable()) { - return o << "[not available]"; + return fmt::format_to(ctx.out(), "[not available]"); } - o << " id: " << value.id() << '\n'; + auto out = ctx.out(); + out = fmt::format_to(out, " id: {} \n", value.id()); {% for member in members %} {% if member.is_array %} - o << " {{ member.name }} : "; - for (size_t i = 0; i < {{ member.array_size }}; ++i) { - o << value.{{ member.getter_name(get_syntax) }}()[i] << "|"; - } - o << '\n'; + out = fmt::format_to(out, " {{ member.name }} : {}\n", fmt::join(value.{{ member.getter_name(get_syntax) }}(), "|")); {% else %} - o << " {{ member.name }} : " << value.{{ member.getter_name(get_syntax) }}() << '\n'; + out = fmt::format_to(out, " {{member.name }} : {}\n", value.{{ member.getter_name(get_syntax) }}()); {% endif %} {% endfor %} {% for relation in single_relations %} - o << " {{ relation.name }} : " << value.{{ relation.getter_name(get_syntax) }}().id() << '\n'; + out = fmt::format_to(out, " {{ relation.name }} : {}\n", value.{{ relation.getter_name(get_syntax) }}().id()); {% endfor %} {% for relation in multi_relations %} - o << " {{ relation.name }} : "; - for (unsigned i = 0; i < value.{{ relation.name }}_size(); ++i) { -{% if type == relation.bare_type %} - o << value.{{ relation.getter_name(get_syntax) }}(i).id() << " "; +{% if class.bare_type == relation.bare_type %} + out = fmt::format_to(out, " {{ relation.name }} : {}\n", fmt::join(value.{{ relation.getter_name(get_syntax) }}() | std::views::transform(&{{ relation.full_type }}::id), " ")); {% else %} - o << value.{{ relation.getter_name(get_syntax) }}(i) << " "; + out = fmt::format_to(out, " {{ relation.name }} : {}\n", fmt::join(value.{{ relation.getter_name(get_syntax) }}(), " ")); {% endif %} - } - o << '\n'; {% endfor %} - return o; + return out; } {%- endmacro %} diff --git a/tests/unittests/unittest.cpp b/tests/unittests/unittest.cpp index 729ac9bc2..80765194c 100644 --- a/tests/unittests/unittest.cpp +++ b/tests/unittests/unittest.cpp @@ -173,6 +173,10 @@ TEST_CASE("Object formatting", "[basics][formatting]") { formatted = fmt::format("{}", mutCluster); REQUIRE_FALSE(formatted.empty()); REQUIRE(formatted != "[not available]"); + // Ensure operator<< is still working + std::stringstream sstr; + sstr << mutCluster; + REQUIRE(sstr.str() == formatted); auto typeWithComponent = ExampleWithArrayComponent{}; formatted = fmt::format("{}", typeWithComponent); From 76a7f68c52a52790902fb732dfa7e710a3dad15e Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Fri, 13 Feb 2026 17:10:49 +0100 Subject: [PATCH 14/19] Add fmt support for collections --- python/templates/Collection.cc.jinja2 | 16 ++++++- python/templates/Collection.h.jinja2 | 14 +++++- python/templates/macros/collections.jinja2 | 52 ++++++++-------------- 3 files changed, 45 insertions(+), 37 deletions(-) diff --git a/python/templates/Collection.cc.jinja2 b/python/templates/Collection.cc.jinja2 index a70741dcd..1070c70a3 100644 --- a/python/templates/Collection.cc.jinja2 +++ b/python/templates/Collection.cc.jinja2 @@ -21,6 +21,8 @@ #include "nlohmann/json.hpp" #endif +#include + // standard includes #include #include @@ -251,6 +253,18 @@ void to_json(nlohmann::json& j, const {{ collection_type }}& collection) { {{ iterator_definitions(class, prefix='Mutable' ) }} -{{ macros.ostream_operator(class, Members, OneToOneRelations, OneToManyRelations, VectorMembers, use_get_syntax, ostream_collection_settings) }} +std::ostream& operator<<(std::ostream& o, const {{ class.bare_type }}Collection& coll) { + fmt::format_to(std::ostreambuf_iterator(o), "{}", coll); + return o; +} + +void {{ class.bare_type }}Collection::print(std::ostream& os, bool flush) const { + os << *this; + if (flush) { + os.flush(); + } +} {{ utils.namespace_close(class.namespace) }} + +{{ macros.formatter(class, Members, OneToOneRelations, OneToManyRelations, VectorMembers, use_get_syntax, ostream_collection_settings) }} diff --git a/python/templates/Collection.h.jinja2 b/python/templates/Collection.h.jinja2 index 433788cdb..2441af5d2 100644 --- a/python/templates/Collection.h.jinja2 +++ b/python/templates/Collection.h.jinja2 @@ -30,7 +30,7 @@ #include #include -#include +#include namespace podio { struct RelationNames; @@ -283,7 +283,17 @@ void to_json(nlohmann::json& j, const {{ class.bare_type }}Collection& collectio {{ utils.namespace_close(class.namespace) }} template <> -struct fmt::formatter<{% if class.namespace %}{{ class.namespace }}::{% endif %}{{ class.bare_type }}Collection> : fmt::ostream_formatter {}; +struct fmt::formatter<{% if class.namespace %}{{ class.namespace }}::{% endif %}{{ class.bare_type }}Collection> { + constexpr auto parse(fmt::format_parse_context& ctx) { + auto it = ctx.begin(); + if (it != ctx.end() && *it != '}') { + fmt::throw_format_error("Invalid format. {{ class.bare_type }}Collections do not support specifiers"); + } + return it; + } + + fmt::format_context::iterator format(const {{ class.full_type }}Collection& coll, fmt::format_context& ctx) const; +}; {{ workarounds.ld_library_path(class, "Collection", ["valueTypeName", "dataTypeName"]) }} diff --git a/python/templates/macros/collections.jinja2 b/python/templates/macros/collections.jinja2 index fb9f7eb03..8fce035c6 100644 --- a/python/templates/macros/collections.jinja2 +++ b/python/templates/macros/collections.jinja2 @@ -97,57 +97,41 @@ std::vector<{{ member.full_type }}> {{ class.bare_type }}Collection::{{ member.n {% endmacro %} -{% macro ostream_operator(class, members, single_relations, multi_relations, vector_members, get_syntax, settings) %} -std::ostream& operator<<(std::ostream& o, const {{ class.bare_type }}Collection& v) { -{% set col_width = 12 %} - const auto old_flags = o.flags(); - o << "{{ 'id' | ostream_collection_header(col_width=col_width) }}: -{%- for header in settings.header_contents -%} - {{ header | ostream_collection_header(col_width=col_width) }}: -{%- endfor -%}" << '\n'; - - for (const auto&& el : v) { - o << std::scientific << std::showpos << std::setw({{ col_width }}) << el.id() << " " +{% macro formatter(class, members, single_relations, multi_relations, vector_members, get_syntax, settings) %} +fmt::format_context::iterator fmt::formatter<{{ class.full_type }}Collection>::format(const {{ class.full_type }}Collection& coll, fmt::format_context& ctx) const { + auto out = ctx.out(); + {% set cw = 12 %} + out = fmt::format_to(out, "{:>{{ cw }}}:", "id"); +{% for header in settings.header_contents %} + out = fmt::format_to(out, "{}", "{{ header | ostream_collection_header(col_width=cw) }}"); +{% endfor %} + out = fmt::format_to(out, "\n"); + + for (const auto& el : coll) { + out = fmt::format_to(out, "{} ", el.id()); {% for member in members %} {% if not member.is_array %} - << std::setw({{ col_width }}) << el.{{ member.getter_name(get_syntax) }}() << " " + out = fmt::format_to(out, "{:^{{ cw }}} ", el.{{ member.getter_name(get_syntax) }}()); {% endif %} {% endfor %} - << std::endl; + out = fmt::format_to(out, "\n"); {% for relation in multi_relations %} - o << " {{ relation.name }} : "; - for (unsigned j = 0, N = el.{{ relation.name }}_size(); j < N; ++j) { - o << el.{{ relation.getter_name(get_syntax) }}(j).id() << " "; - } - o << std::endl; + out = fmt::format_to(out, " {{ relation.name }} : {}\n", fmt::join(el.{{ relation.getter_name(get_syntax) }}() | std::views::transform(&{{ relation.full_type }}::id), " ")); {% endfor %} {% for relation in single_relations %} - o << " {{ relation.name }} : "; - o << el.{{ relation.getter_name(get_syntax) }}().id() << std::endl; + out = fmt::format_to(out, " {{ relation.name }} : {}\n", el.{{ relation.getter_name(get_syntax) }}().id()); {% endfor %} {% for member in vector_members %} - o << " {{ member.name }} : "; - for (unsigned j = 0, N = el.{{ member.name }}_size(); j < N; ++j) { - o << el.{{ member.getter_name(get_syntax) }}(j) << " "; - } - o << std::endl; + out = fmt::format_to(out, " {{ member.name }} : {}\n", fmt::join(el.{{ member.getter_name(get_syntax) }}(), " ")); {% endfor %} - } - o.flags(old_flags); - return o; + return out; } -void {{ class.bare_type }}Collection::print(std::ostream& os, bool flush) const { - os << *this; - if (flush) { - os.flush(); - } -} {% endmacro %} {% macro create_buffers(class, package_name, collection_type, OneToManyRelations, OneToOneRelations, VectorMembers, schemaVersion) %} From e8d4be8163c6be48845db75a0cdd8a9c03c27430 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Fri, 13 Feb 2026 19:49:33 +0100 Subject: [PATCH 15/19] Implement CollectionBase::print in terms of format --- include/podio/UserDataCollection.h | 9 +-------- include/podio/detail/LinkCollectionImpl.h | 3 +-- python/templates/Collection.cc.jinja2 | 2 +- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/include/podio/UserDataCollection.h b/include/podio/UserDataCollection.h index 63840991e..d10d8da25 100644 --- a/include/podio/UserDataCollection.h +++ b/include/podio/UserDataCollection.h @@ -224,14 +224,7 @@ class UserDataCollection : public CollectionBase { /// Print this collection to the passed stream void print(std::ostream& os = std::cout, bool flush = true) const override { - os << "["; - if (!_vec.empty()) { - os << _vec[0]; - for (size_t i = 1; i < _vec.size(); ++i) { - os << ", " << _vec[i]; - } - } - os << "]"; + os << fmt::format("{}", _vec); if (flush) { os.flush(); // Necessary for python diff --git a/include/podio/detail/LinkCollectionImpl.h b/include/podio/detail/LinkCollectionImpl.h index aefddb7a8..e2c51329e 100644 --- a/include/podio/detail/LinkCollectionImpl.h +++ b/include/podio/detail/LinkCollectionImpl.h @@ -31,7 +31,6 @@ #include #include -#include #include #include #include @@ -213,7 +212,7 @@ class LinkCollection : public podio::CollectionBase { } void print(std::ostream& os = std::cout, bool flush = true) const override { - os << *this; + os << fmt::format("{}", *this); if (flush) { os.flush(); } diff --git a/python/templates/Collection.cc.jinja2 b/python/templates/Collection.cc.jinja2 index 1070c70a3..67604e45b 100644 --- a/python/templates/Collection.cc.jinja2 +++ b/python/templates/Collection.cc.jinja2 @@ -259,7 +259,7 @@ std::ostream& operator<<(std::ostream& o, const {{ class.bare_type }}Collection& } void {{ class.bare_type }}Collection::print(std::ostream& os, bool flush) const { - os << *this; + os << fmt::format("{}", *this); if (flush) { os.flush(); } From f315e891f44707a66cdc21766dc97abd6a34bdcc Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Fri, 13 Feb 2026 20:02:20 +0100 Subject: [PATCH 16/19] Implement GenericParamters::print in terms of format Also get rid of some hideous string concatenation while we are at it --- src/GenericParameters.cc | 41 ++++++++++++---------------------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/src/GenericParameters.cc b/src/GenericParameters.cc index 6befeb206..c0895dca9 100644 --- a/src/GenericParameters.cc +++ b/src/GenericParameters.cc @@ -1,6 +1,7 @@ #include "podio/GenericParameters.h" -#include +#include +#include namespace podio { @@ -18,41 +19,23 @@ GenericParameters::GenericParameters(const GenericParameters& other) { _doubleMap = other._doubleMap; } -template -std::ostream& operator<<(std::ostream& os, const std::vector& values) { - os << "["; - if (!values.empty()) { - os << values[0]; - for (size_t i = 1; i < values.size(); ++i) { - os << ", " << values[i]; - } - } - - return os << "]"; -} - template -void printMap(const MapType& map, std::ostream& os) { - const auto osflags = os.flags(); - os << std::left << std::setw(30) << "Key " - << "Value " << '\n'; - os << "--------------------------------------------------------------------------------\n"; +void formatMap(const MapType& map, std::ostream& os) { + fmt::format_to(std::ostreambuf_iterator(os), "{:<30}{}\n{:-<80}", "Key", "Value", ""); for (const auto& [key, value] : map) { - os << std::left << std::setw(30) << key << value << '\n'; + fmt::format_to(std::ostreambuf_iterator(os), "{:<30}{}\n", key, value); } - - os.flags(osflags); } void GenericParameters::print(std::ostream& os, bool flush) const { os << "int parameters\n\n"; - printMap(getMap(), os); - os << "\nfloat parameters\n"; - printMap(getMap(), os); - os << "\ndouble parameters\n"; - printMap(getMap(), os); - os << "\nstd::string parameters\n"; - printMap(getMap(), os); + formatMap(getMap(), os); + os << "float parameters\n\n"; + formatMap(getMap(), os); + os << "double parameters\n\n"; + formatMap(getMap(), os); + os << "string parameters\n\n"; + formatMap(getMap(), os); if (flush) { os.flush(); From ce8bee83e01cace78d3ea16b8c9df67e69114a33 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Fri, 13 Feb 2026 20:04:55 +0100 Subject: [PATCH 17/19] Remove unnecessary template parameter specification --- include/podio/ObjectID.h | 2 +- include/podio/UserDataCollection.h | 2 +- include/podio/detail/Link.h | 2 +- include/podio/detail/LinkCollectionImpl.h | 2 +- podioVersion.in.h | 2 +- python/templates/Collection.cc.jinja2 | 2 +- python/templates/Object.cc.jinja2 | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/include/podio/ObjectID.h b/include/podio/ObjectID.h index 5263a28c4..d3be53537 100644 --- a/include/podio/ObjectID.h +++ b/include/podio/ObjectID.h @@ -72,7 +72,7 @@ struct fmt::formatter { namespace podio { inline std::ostream& operator<<(std::ostream& os, const podio::ObjectID& id) { - fmt::format_to(std::ostreambuf_iterator(os), "{}", id); + fmt::format_to(std::ostreambuf_iterator(os), "{}", id); return os; } } // namespace podio diff --git a/include/podio/UserDataCollection.h b/include/podio/UserDataCollection.h index d10d8da25..1880efe87 100644 --- a/include/podio/UserDataCollection.h +++ b/include/podio/UserDataCollection.h @@ -319,7 +319,7 @@ using UserDataCollectionTypes = decltype(std::apply( template std::ostream& operator<<(std::ostream& o, const podio::UserDataCollection& coll) { - fmt::format_to(std::ostreambuf_iterator(o), "{}", coll); + fmt::format_to(std::ostreambuf_iterator(o), "{}", coll); return o; } diff --git a/include/podio/detail/Link.h b/include/podio/detail/Link.h index 7d5e4d96e..81137b925 100644 --- a/include/podio/detail/Link.h +++ b/include/podio/detail/Link.h @@ -401,7 +401,7 @@ struct fmt::is_tuple_formattable, Char> : std: namespace podio { template std::ostream& operator<<(std::ostream& os, const LinkT& link) { - fmt::format_to(std::ostreambuf_iterator(os), "{}", link); + fmt::format_to(std::ostreambuf_iterator(os), "{}", link); return os; } } // namespace podio diff --git a/include/podio/detail/LinkCollectionImpl.h b/include/podio/detail/LinkCollectionImpl.h index e2c51329e..2feecb187 100644 --- a/include/podio/detail/LinkCollectionImpl.h +++ b/include/podio/detail/LinkCollectionImpl.h @@ -475,7 +475,7 @@ struct fmt::is_range, char> : std::false_type namespace podio { template std::ostream& operator<<(std::ostream& o, const LinkCollection& v) { - fmt::format_to(std::ostreambuf_iterator(o), "{}", v); + fmt::format_to(std::ostreambuf_iterator(o), "{}", v); return o; } } // namespace podio diff --git a/podioVersion.in.h b/podioVersion.in.h index 576fde28e..db5ebc2e8 100644 --- a/podioVersion.in.h +++ b/podioVersion.in.h @@ -88,7 +88,7 @@ struct fmt::formatter { namespace podio::version { inline std::ostream& operator<<(std::ostream& os, const Version& v) { - fmt::format_to(std::ostreambuf_iterator(os), "{}", v); + fmt::format_to(std::ostreambuf_iterator(os), "{}", v); return os; } diff --git a/python/templates/Collection.cc.jinja2 b/python/templates/Collection.cc.jinja2 index 67604e45b..396c535b6 100644 --- a/python/templates/Collection.cc.jinja2 +++ b/python/templates/Collection.cc.jinja2 @@ -254,7 +254,7 @@ void to_json(nlohmann::json& j, const {{ collection_type }}& collection) { {{ iterator_definitions(class, prefix='Mutable' ) }} std::ostream& operator<<(std::ostream& o, const {{ class.bare_type }}Collection& coll) { - fmt::format_to(std::ostreambuf_iterator(o), "{}", coll); + fmt::format_to(std::ostreambuf_iterator(o), "{}", coll); return o; } diff --git a/python/templates/Object.cc.jinja2 b/python/templates/Object.cc.jinja2 index c9f1d77d6..f66c499ca 100644 --- a/python/templates/Object.cc.jinja2 +++ b/python/templates/Object.cc.jinja2 @@ -37,7 +37,7 @@ {{ macros.common_object_funcs(class) }} std::ostream& operator<<(std::ostream& o, const {{ class.bare_type }}& value) { - fmt::format_to(std::ostreambuf_iterator(o), "{}", value); + fmt::format_to(std::ostreambuf_iterator(o), "{}", value); return o; } From 87571a8c64cf11d41d48c0d3e086be7c1024cb62 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Fri, 13 Feb 2026 20:20:58 +0100 Subject: [PATCH 18/19] Add format support for GenericParameters --- include/podio/GenericParameters.h | 16 ++++++++++++ src/GenericParameters.cc | 41 ++++++++++++++++++------------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/include/podio/GenericParameters.h b/include/podio/GenericParameters.h index f21e15d99..71e6bf04e 100644 --- a/include/podio/GenericParameters.h +++ b/include/podio/GenericParameters.h @@ -275,4 +275,20 @@ void GenericParameters::loadFrom(VecLike keys, VecLike + +template <> +struct fmt::formatter { + constexpr auto parse(fmt::format_parse_context& ctx) { + auto it = ctx.begin(); + if (it != ctx.end() && *it != '}') { + fmt::throw_format_error("Invalid format. GenericParameters does not support specifiers"); + } + return it; + } + + fmt::format_context::iterator format(const podio::GenericParameters& params, fmt::format_context& ctx) const; +}; + #endif diff --git a/src/GenericParameters.cc b/src/GenericParameters.cc index c0895dca9..1aa6b38d1 100644 --- a/src/GenericParameters.cc +++ b/src/GenericParameters.cc @@ -19,27 +19,34 @@ GenericParameters::GenericParameters(const GenericParameters& other) { _doubleMap = other._doubleMap; } -template -void formatMap(const MapType& map, std::ostream& os) { - fmt::format_to(std::ostreambuf_iterator(os), "{:<30}{}\n{:-<80}", "Key", "Value", ""); - for (const auto& [key, value] : map) { - fmt::format_to(std::ostreambuf_iterator(os), "{:<30}{}\n", key, value); - } -} - void GenericParameters::print(std::ostream& os, bool flush) const { - os << "int parameters\n\n"; - formatMap(getMap(), os); - os << "float parameters\n\n"; - formatMap(getMap(), os); - os << "double parameters\n\n"; - formatMap(getMap(), os); - os << "string parameters\n\n"; - formatMap(getMap(), os); - + fmt::format_to(std::ostreambuf_iterator(os), "{}", *this); if (flush) { os.flush(); } } } // namespace podio + +fmt::format_context::iterator fmt::formatter::format(const podio::GenericParameters& params, + fmt::format_context& ctx) const { + auto out = ctx.out(); + + auto formatMap = [&out](const auto& map) { + out = fmt::format_to(out, "{:<30}{}\n{:-<80}\n", "Key", "Value", ""); + for (const auto& [key, value] : map) { + out = fmt::format_to(out, "{:<30}{}\n", key, value); + } + }; + + out = fmt::format_to(out, "int parameters\n\n"); + formatMap(params.getMap()); + out = fmt::format_to(out, "float parameters\n\n"); + formatMap(params.getMap()); + out = fmt::format_to(out, "double parameters\n\n"); + formatMap(params.getMap()); + out = fmt::format_to(out, "string parameters\n\n"); + formatMap(params.getMap()); + + return out; +} From 163cead2e53c58c4810f612de986eb3dd517a85b Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Fri, 13 Feb 2026 20:28:37 +0100 Subject: [PATCH 19/19] Make sure fmt headers can be found by ROOT interactively --- cmake/podioTest.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/podioTest.cmake b/cmake/podioTest.cmake index 61aac90e8..5aba01ae3 100644 --- a/cmake/podioTest.cmake +++ b/cmake/podioTest.cmake @@ -8,7 +8,7 @@ function(PODIO_SET_TEST_ENV test) LD_LIBRARY_PATH=${PROJECT_BINARY_DIR}/src:$:$<$:$>:$ENV{LD_LIBRARY_PATH} PYTHONPATH=${PROJECT_SOURCE_DIR}/python:$ENV{PYTHONPATH} PODIO_SIOBLOCK_PATH=${PROJECT_BINARY_DIR}/tests - ROOT_INCLUDE_PATH=${PROJECT_SOURCE_DIR}/tests:${PROJECT_SOURCE_DIR}/include:$ENV{ROOT_INCLUDE_PATH} + ROOT_INCLUDE_PATH=${PROJECT_SOURCE_DIR}/tests:${PROJECT_SOURCE_DIR}/include:$ENV{ROOT_INCLUDE_PATH}:$/../include SKIP_SIO_TESTS=$> IO_HANDLERS=${IO_HANDLERS} PODIO_USE_CLANG_FORMAT=${PODIO_USE_CLANG_FORMAT}