From 2cea163f57dcbf385225b473186f42dcb8ee8d39 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Thu, 12 Feb 2026 09:45:07 +0100 Subject: [PATCH 1/6] 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 19885916fb7d1207deebb3abb5e73babb4d567b0 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Thu, 12 Feb 2026 10:13:48 +0100 Subject: [PATCH 2/6] 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 41a454c6af128efa11606bc8e50ce12261a467a2 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Thu, 12 Feb 2026 10:14:04 +0100 Subject: [PATCH 3/6] 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 9271146a1414fa5d5463cf7a3988713526b88933 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Thu, 12 Feb 2026 10:26:40 +0100 Subject: [PATCH 4/6] 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 32e0056f486fd293f27bafa73bb49630994e8406 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Thu, 12 Feb 2026 19:49:17 +0100 Subject: [PATCH 5/6] 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 5db02eb3c4ed7ce88555c9eba3ad6a531ac7faa3 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Fri, 13 Feb 2026 09:40:24 +0100 Subject: [PATCH 6/6] 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]"); +}