From 5d60791a400a424d48f8d3f0e6e55d8b2b1f4abd Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Thu, 12 Feb 2026 09:45:07 +0100 Subject: [PATCH 01/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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} From 6833c2c963fa024ce1b4b80e43a80ebea9cd988d Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Fri, 13 Feb 2026 20:57:19 +0100 Subject: [PATCH 20/23] Add brief format specifier to Link for more concise formatting --- include/podio/detail/Link.h | 21 ++++++++++++-- tests/unittests/links.cpp | 58 ++++++++++++++++++++++++++----------- 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/include/podio/detail/Link.h b/include/podio/detail/Link.h index 81137b925..24f47d461 100644 --- a/include/podio/detail/Link.h +++ b/include/podio/detail/Link.h @@ -375,11 +375,23 @@ struct std::hash> { template struct fmt::formatter> { + char presentation = 'd'; // 'd' for default/detailed, 'b' for brief + 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"); + auto end = ctx.end(); + + if (it != end && *it != '}') { + presentation = *it++; + if (presentation != 'b' && presentation != 'd') { + fmt::throw_format_error("Invalid format specifier for Link. Use 'b' for brief or 'd' for detailed"); + } + } + + if (it != end && *it != '}') { + fmt::throw_format_error("Invalid format specifier for Link"); } + return it; } @@ -387,6 +399,11 @@ struct fmt::formatter> { if (!link.isAvailable()) { return fmt::format_to(ctx.out(), "[not available]"); } + if (presentation == 'b') { + return fmt::format_to(ctx.out(), "{} | {} {} {}", link.id(), link.getFrom().id(), link.getTo().id(), + link.getWeight()); + } + return fmt::format_to(ctx.out(), " id: {}\n weight: {}\n from: {}\n to: {}\n", link.id(), link.getWeight(), link.getFrom().id(), link.getTo().id()); } diff --git a/tests/unittests/links.cpp b/tests/unittests/links.cpp index 2894f6f37..c9d6edf78 100644 --- a/tests/unittests/links.cpp +++ b/tests/unittests/links.cpp @@ -304,23 +304,47 @@ TEST_CASE("Links templated accessors", "[links]") { TEST_CASE("Link formatting", "[links]") { TestL link; - 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); - REQUIRE(emptyFmt == "[not available]"); - - TestMutL mutLink; - formatted = fmt::format("{}", mutLink); - REQUIRE(formatted != "[not avialable]"); + + SECTION("Default format (detailed)") { + 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()); + + // Explicit detailed format should be the same + auto formatted_detailed = fmt::format("{:d}", link); + REQUIRE(formatted_detailed == formatted); + } + + SECTION("Brief format") { + auto formatted_basic = fmt::format("{:b}", link); + REQUIRE_FALSE(formatted_basic.empty()); + REQUIRE(formatted_basic == "ffffffff|-1 | ffffffff|-1 ffffffff|-1 1"); + } + + SECTION("Empty link") { + auto emptyLink = TestL::makeEmpty(); + auto emptyFmt = fmt::format("{}", emptyLink); + REQUIRE(emptyFmt == "[not available]"); + + // Basic format should also show [not available] for empty link + auto emptyFmtBasic = fmt::format("{:b}", emptyLink); + REQUIRE(emptyFmtBasic == "[not available]"); + } + + SECTION("Mutable link") { + TestMutL mutLink; + auto formatted = fmt::format("{}", mutLink); + REQUIRE(formatted != "[not avialable]"); + + auto formatted_basic = fmt::format("{:b}", mutLink); + REQUIRE_FALSE(formatted_basic.empty()); + } } TEST_CASE("LinkCollection collection concept", "[links][concepts]") { From a8db92e7b7d2b31e81fceedf6870d7127b467464 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Fri, 13 Feb 2026 21:02:59 +0100 Subject: [PATCH 21/23] Add brief format specifier to LinkCollection --- include/podio/detail/LinkCollectionImpl.h | 22 +++++++-- tests/unittests/links.cpp | 59 ++++++++++++++++++++--- 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/include/podio/detail/LinkCollectionImpl.h b/include/podio/detail/LinkCollectionImpl.h index 2feecb187..ecd5b1a4a 100644 --- a/include/podio/detail/LinkCollectionImpl.h +++ b/include/podio/detail/LinkCollectionImpl.h @@ -446,23 +446,39 @@ void to_json(nlohmann::json& j, const podio::LinkCollection& collect template struct fmt::formatter> { + char presentation = 'd'; // 'd' for default/detailed, 'b' for brief + 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"); + auto end = ctx.end(); + + if (it != end && *it != '}') { + presentation = *it++; + if (presentation != 'b' && presentation != 'd') { + fmt::throw_format_error( + "Unsupported format specifier for LinkCollection. Use 'b' for brief or 'd' for detailed"); + } + } + + if (it != end && *it != '}') { + fmt::throw_format_error("Invalid format specifier for LinkCollection"); } + return it; } auto format(const podio::LinkCollection& coll, fmt::format_context& ctx) const { auto out = ctx.out(); + if (presentation == 'b') { + return fmt::format_to(out, "{} (id: {:8x}, size: {})", coll.getTypeName(), coll.getID(), coll.size()); + } + 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; } }; diff --git a/tests/unittests/links.cpp b/tests/unittests/links.cpp index c9d6edf78..0858cbd3d 100644 --- a/tests/unittests/links.cpp +++ b/tests/unittests/links.cpp @@ -492,16 +492,63 @@ TEST_CASE("LinkCollection basics", "[links]") { } TEST_CASE("LinkCollection formatting", "[links][formatting]") { + ExampleHitCollection hits; + ExampleClusterCollection clusters; + auto hit1 = hits.create(); + auto hit2 = hits.create(); + auto cluster1 = clusters.create(); + auto cluster2 = clusters.create(); + podio::LinkCollection links; + links.setID(42); + const auto idHex = fmt::format("{:8x}", 42); - auto formatted = fmt::format("{}", links); - REQUIRE_FALSE(formatted.empty()); + SECTION("Empty collection") { + auto formatted = fmt::format("{}", links); + REQUIRE_FALSE(formatted.empty()); - links.create(); - links.create(); + auto formatted_basic = fmt::format("{:b}", links); + REQUIRE_FALSE(formatted_basic.empty()); + REQUIRE(formatted_basic.find(idHex) != std::string::npos); // Should contain collection ID + REQUIRE(formatted_basic.find("0") != std::string::npos); // Should contain size = 0 + } + + SECTION("Non-empty collection") { + auto link1 = links.create(); + link1.setFrom(hit1); + link1.setTo(cluster1); + link1.setWeight(1.5f); + + auto link2 = links.create(); + link2.setFrom(hit2); + link2.setTo(cluster2); + link2.setWeight(2.5f); + + // Test default format (detailed) + auto formatted_default = fmt::format("{}", links); + REQUIRE_FALSE(formatted_default.empty()); + REQUIRE(formatted_default.find("id:") != std::string::npos); + REQUIRE(formatted_default.find("weight:") != std::string::npos); + REQUIRE(formatted_default.find("from") != std::string::npos); + REQUIRE(formatted_default.find("to") != std::string::npos); + + // Test explicit detailed format + auto formatted_detailed = fmt::format("{:d}", links); + REQUIRE(formatted_detailed == formatted_default); + + // Test basic format + auto formatted_basic = fmt::format("{:b}", links); + REQUIRE_FALSE(formatted_basic.empty()); + REQUIRE(formatted_basic.find(idHex) != std::string::npos); // Should contain collection ID + REQUIRE(formatted_basic.find("2") != std::string::npos); // Should contain size = 2 + REQUIRE(formatted_basic.find("podio::LinkCollection") != std::string::npos); // Should contain type name + // Basic format should be much shorter than detailed + REQUIRE(formatted_basic.size() < formatted_default.size()); - auto formatted2 = fmt::format("{}", links); - REQUIRE(formatted2.size() > formatted.size()); + // Test that basic format doesn't contain detailed information + REQUIRE(formatted_basic.find("from") == std::string::npos); + REQUIRE(formatted_basic.find("to") == std::string::npos); + } } auto createLinkCollections(const size_t nElements = 3u) { From 062a60ff67778ca7803b6093754c6d963c157124 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Fri, 13 Feb 2026 21:40:14 +0100 Subject: [PATCH 22/23] First version of customization hook Claude generated. --- include/podio/detail/Link.h | 9 +++++-- include/podio/detail/LinkCollectionImpl.h | 7 ++++-- python/templates/Collection.h.jinja2 | 24 ++++++++++++++++--- python/templates/Object.h.jinja2 | 1 + python/templates/macros/collections.jinja2 | 2 +- python/templates/macros/declarations.jinja2 | 23 +++++++++++++++--- .../templates/macros/implementations.jinja2 | 2 +- 7 files changed, 56 insertions(+), 12 deletions(-) diff --git a/include/podio/detail/Link.h b/include/podio/detail/Link.h index 24f47d461..cea9543c3 100644 --- a/include/podio/detail/Link.h +++ b/include/podio/detail/Link.h @@ -12,6 +12,8 @@ #include "nlohmann/json.hpp" #endif +#include "podio/utilities/FormatHelpers.h" + #include #include @@ -383,8 +385,8 @@ struct fmt::formatter> { if (it != end && *it != '}') { presentation = *it++; - if (presentation != 'b' && presentation != 'd') { - fmt::throw_format_error("Invalid format specifier for Link. Use 'b' for brief or 'd' for detailed"); + if (presentation != 'b' && presentation != 'd' && presentation != 'u') { + fmt::throw_format_error("Invalid format specifier for Link. Use 'b' for brief, 'd' for detailed, or 'u' for user-defined"); } } @@ -396,6 +398,9 @@ struct fmt::formatter> { } auto format(const podio::LinkT& link, fmt::format_context& ctx) const { + if (presentation == 'u') { + return podio::detail::dispatchCustomFormat(link, ctx); + } if (!link.isAvailable()) { return fmt::format_to(ctx.out(), "[not available]"); } diff --git a/include/podio/detail/LinkCollectionImpl.h b/include/podio/detail/LinkCollectionImpl.h index ecd5b1a4a..718f0b236 100644 --- a/include/podio/detail/LinkCollectionImpl.h +++ b/include/podio/detail/LinkCollectionImpl.h @@ -454,9 +454,9 @@ struct fmt::formatter> { if (it != end && *it != '}') { presentation = *it++; - if (presentation != 'b' && presentation != 'd') { + if (presentation != 'b' && presentation != 'd' && presentation != 'u') { fmt::throw_format_error( - "Unsupported format specifier for LinkCollection. Use 'b' for brief or 'd' for detailed"); + "Unsupported format specifier for LinkCollection. Use 'b' for brief, 'd' for detailed, or 'u' for user-defined"); } } @@ -468,6 +468,9 @@ struct fmt::formatter> { } auto format(const podio::LinkCollection& coll, fmt::format_context& ctx) const { + if (presentation == 'u') { + return podio::detail::dispatchCustomFormat(coll, ctx); + } auto out = ctx.out(); if (presentation == 'b') { diff --git a/python/templates/Collection.h.jinja2 b/python/templates/Collection.h.jinja2 index 2441af5d2..1dc4d00d7 100644 --- a/python/templates/Collection.h.jinja2 +++ b/python/templates/Collection.h.jinja2 @@ -31,6 +31,7 @@ #include #include +#include "podio/utilities/FormatHelpers.h" namespace podio { struct RelationNames; @@ -284,15 +285,32 @@ void to_json(nlohmann::json& j, const {{ class.bare_type }}Collection& collectio template <> struct fmt::formatter<{% if class.namespace %}{{ class.namespace }}::{% endif %}{{ class.bare_type }}Collection> { + char presentation = 'd'; + 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"); + auto end = ctx.end(); + if (it != end && *it != '}') { + presentation = *it++; + if (presentation != 'd' && presentation != 'u') { + fmt::throw_format_error("Invalid format specifier for {{ class.bare_type }}Collection. Use 'u' for user-defined or 'd' for detailed"); + } + } + if (it != end && *it != '}') { + fmt::throw_format_error("Invalid format specifier for {{ class.bare_type }}Collection"); } return it; } - fmt::format_context::iterator format(const {{ class.full_type }}Collection& coll, fmt::format_context& ctx) const; + template + fmt::format_context::iterator format(const {{ class.full_type }}Collection& coll, fmt::format_context& ctx) const { + if (presentation == 'u') { + return podio::detail::dispatchCustomFormat(coll, ctx); + } + return formatDefault(coll, ctx); + } + + fmt::format_context::iterator formatDefault(const {{ class.full_type }}Collection& coll, fmt::format_context& ctx) const; }; {{ workarounds.ld_library_path(class, "Collection", ["valueTypeName", "dataTypeName"]) }} diff --git a/python/templates/Object.h.jinja2 b/python/templates/Object.h.jinja2 index f35dd363e..80ec82f15 100644 --- a/python/templates/Object.h.jinja2 +++ b/python/templates/Object.h.jinja2 @@ -16,6 +16,7 @@ #include "podio/detail/OrderKey.h" #include +#include "podio/utilities/FormatHelpers.h" #include #include diff --git a/python/templates/macros/collections.jinja2 b/python/templates/macros/collections.jinja2 index 8fce035c6..e46c9c5cf 100644 --- a/python/templates/macros/collections.jinja2 +++ b/python/templates/macros/collections.jinja2 @@ -98,7 +98,7 @@ std::vector<{{ member.full_type }}> {{ class.bare_type }}Collection::{{ member.n {% 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 { +fmt::format_context::iterator fmt::formatter<{{ class.full_type }}Collection>::formatDefault(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"); diff --git a/python/templates/macros/declarations.jinja2 b/python/templates/macros/declarations.jinja2 index 0db7ab81c..0ed135a9f 100644 --- a/python/templates/macros/declarations.jinja2 +++ b/python/templates/macros/declarations.jinja2 @@ -163,15 +163,32 @@ struct std::hash<{{ namespace }}{{ prefix }}{{ class.bare_type }}> { {% set namespace = class.namespace + '::' if class.namespace else '' %} template <> struct fmt::formatter<{{ namespace }}{{ prefix }}{{ class.bare_type }}> { + char presentation = 'd'; + 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"); + auto end = ctx.end(); + if (it != end && *it != '}') { + presentation = *it++; + if (presentation != 'd' && presentation != 'u') { + fmt::throw_format_error("Invalid format specifier for {{ class.bare_type }}. Use 'u' for user-defined or 'd' for detailed"); + } + } + if (it != end && *it != '}') { + fmt::throw_format_error("Invalid format specifier for {{ class.bare_type }}"); } return it; } - fmt::format_context::iterator format(const {{ namespace }}{{ prefix }}{{ class.bare_type }}& value, fmt::format_context& ctx) const; + template + fmt::format_context::iterator format(const {{ namespace }}{{ prefix }}{{ class.bare_type }}& value, fmt::format_context& ctx) const { + if (presentation == 'u') { + return podio::detail::dispatchCustomFormat(value, ctx); + } + return formatDefault(value, ctx); + } + + fmt::format_context::iterator formatDefault(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 3c119773a..14c71d4e9 100644 --- a/python/templates/macros/implementations.jinja2 +++ b/python/templates/macros/implementations.jinja2 @@ -193,7 +193,7 @@ bool {{ full_type }}::operator==(const {{ inverse_type }}& other) const { {% 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 { +fmt::format_context::iterator fmt::formatter<{{ namespace }}{{ prefix }}{{ class.bare_type }}>::formatDefault(const {{ namespace }}{{ prefix }}{{ class.bare_type }}& value, fmt::format_context& ctx) const { if (!value.isAvailable()) { return fmt::format_to(ctx.out(), "[not available]"); } From ea428e70c1fee6240d1f60602433b18b14cedcc5 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Fri, 13 Feb 2026 21:56:40 +0100 Subject: [PATCH 23/23] First version of tests that use custom formatting hooks --- tests/unittests/links.cpp | 40 ++++++++++++++++++++++++++++++++++++ tests/unittests/unittest.cpp | 35 +++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/tests/unittests/links.cpp b/tests/unittests/links.cpp index 0858cbd3d..09a09067e 100644 --- a/tests/unittests/links.cpp +++ b/tests/unittests/links.cpp @@ -16,6 +16,7 @@ #endif #include +#include "podio/utilities/FormatHelpers.h" #include #include @@ -31,6 +32,21 @@ using TestLColl = podio::LinkCollection; using TestLIter = podio::LinkCollectionIterator; using TestLMutIter = podio::LinkMutableCollectionIterator; +// Custom format overloads for testing the 'u' format specifier +namespace podio { +fmt::format_context::iterator customFormat(const TestL& link, fmt::format_context& ctx) { + return fmt::format_to(ctx.out(), "custom-link(w={})", link.getWeight()); +} + +fmt::format_context::iterator customFormat(const TestMutL& link, fmt::format_context& ctx) { + return fmt::format_to(ctx.out(), "custom-mut-link(w={})", link.getWeight()); +} + +fmt::format_context::iterator customFormat(const TestLColl& coll, fmt::format_context& ctx) { + return fmt::format_to(ctx.out(), "custom-link-coll(n={})", coll.size()); +} +} // namespace podio + TEST_CASE("Link constness", "[links][static-checks]") { STATIC_REQUIRE(std::is_same_v().getFrom()), const ExampleHit>); STATIC_REQUIRE(std::is_same_v().getTo()), const ExampleCluster>); @@ -345,6 +361,18 @@ TEST_CASE("Link formatting", "[links]") { auto formatted_basic = fmt::format("{:b}", mutLink); REQUIRE_FALSE(formatted_basic.empty()); } + + SECTION("User-defined format") { + auto formatted = fmt::format("{:u}", link); + REQUIRE(formatted == "custom-link(w=1)"); + } + + SECTION("User-defined format for mutable link") { + TestMutL mutLink; + mutLink.setWeight(3.5f); + auto formatted = fmt::format("{:u}", mutLink); + REQUIRE(formatted == "custom-mut-link(w=3.5)"); + } } TEST_CASE("LinkCollection collection concept", "[links][concepts]") { @@ -549,6 +577,18 @@ TEST_CASE("LinkCollection formatting", "[links][formatting]") { REQUIRE(formatted_basic.find("from") == std::string::npos); REQUIRE(formatted_basic.find("to") == std::string::npos); } + + SECTION("User-defined format") { + auto link1 = links.create(); + link1.setFrom(hit1); + link1.setTo(cluster1); + auto link2 = links.create(); + link2.setFrom(hit2); + link2.setTo(cluster2); + + auto formatted = fmt::format("{:u}", links); + REQUIRE(formatted == "custom-link-coll(n=2)"); + } } auto createLinkCollections(const size_t nElements = 3u) { diff --git a/tests/unittests/unittest.cpp b/tests/unittests/unittest.cpp index 80765194c..870b3c26e 100644 --- a/tests/unittests/unittest.cpp +++ b/tests/unittests/unittest.cpp @@ -69,9 +69,20 @@ #include "podio/UserDataCollection.h" #include +#include "podio/utilities/FormatHelpers.h" #include +// Custom format overloads for testing the 'u' format specifier. +// These must be in the same namespace as the type for ADL to find them. +fmt::format_context::iterator customFormat(const ExampleCluster& cluster, fmt::format_context& ctx) { + return fmt::format_to(ctx.out(), "custom-cluster(e={})", cluster.energy()); +} + +fmt::format_context::iterator customFormat(const ExampleClusterCollection& coll, fmt::format_context& ctx) { + return fmt::format_to(ctx.out(), "custom-cluster-coll(n={})", coll.size()); +} + TEST_CASE("ObjectID formatting", "[basics][formatting]") { auto objId = podio::ObjectID{}; auto formatted = fmt::format("{}", objId); @@ -185,6 +196,23 @@ TEST_CASE("Object formatting", "[basics][formatting]") { auto nspComp = ex2::NamespaceInNamespaceStruct{}; formatted = fmt::format("{}", nspComp); REQUIRE_FALSE(formatted.empty()); + + // User-defined format for object + auto customCluster = MutableExampleCluster{}; + customCluster.energy(42.5f); + // MutableT's formatter inherits from T's formatter, so conversion to + // immutable type happens and the ExampleCluster overload is called + formatted = fmt::format("{:u}", customCluster); + REQUIRE(formatted == "custom-cluster(e=42.5)"); + + // User-defined format via immutable type + ExampleCluster immutableCluster = customCluster; + formatted = fmt::format("{:u}", immutableCluster); + REQUIRE(formatted == "custom-cluster(e=42.5)"); + + // User-defined format throws for types without a customFormat overload + auto hitForFmt = ExampleHit{}; + REQUIRE_THROWS_AS(fmt::format("{:u}", hitForFmt), fmt::format_error); } TEST_CASE("Cyclic", "[basics][relations][memory-management]") { @@ -706,6 +734,13 @@ TEST_CASE("Collection formatting", "[basics]") { REQUIRE_FALSE(formatted.empty()); formatted = fmt::format("{}", cluster.Hits()); + + // User-defined format for collection + formatted = fmt::format("{:u}", clusters); + REQUIRE(formatted == "custom-cluster-coll(n=1)"); + + // User-defined format throws for collections without a customFormat overload + REQUIRE_THROWS_AS(fmt::format("{:u}", components), fmt::format_error); } TEST_CASE("UserInitialization", "[basics][code-gen]") {