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/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} 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/include/podio/ObjectID.h b/include/podio/ObjectID.h index 8efe9de7d..d3be53537 100644 --- a/include/podio/ObjectID.h +++ b/include/podio/ObjectID.h @@ -1,9 +1,11 @@ #ifndef PODIO_OBJECTID_H #define PODIO_OBJECTID_H +#include + #include #include -#include +#include #include #if defined(PODIO_JSON_OUTPUT) && !defined(__CLING__) @@ -35,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}}; @@ -60,4 +55,26 @@ struct std::hash { } }; +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. 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/include/podio/UserDataCollection.h b/include/podio/UserDataCollection.h index 361bb0034..1880efe87 100644 --- a/include/podio/UserDataCollection.h +++ b/include/podio/UserDataCollection.h @@ -8,6 +8,11 @@ #include "podio/detail/Pythonizations.h" #include "podio/utilities/TypeHelpers.h" +#include +#include + +#include + #define PODIO_ADD_USER_TYPE(type) \ template <> \ consteval const char* userDataTypeName() { \ @@ -219,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 @@ -321,7 +319,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; } diff --git a/include/podio/detail/Link.h b/include/podio/detail/Link.h index 811a079b8..cea9543c3 100644 --- a/include/podio/detail/Link.h +++ b/include/podio/detail/Link.h @@ -12,6 +12,11 @@ #include "nlohmann/json.hpp" #endif +#include "podio/utilities/FormatHelpers.h" + +#include +#include + #include #include #include @@ -348,18 +353,6 @@ class LinkT { podio::utils::MaybeSharedPtr m_obj{nullptr}; }; -template -std::ostream& operator<<(std::ostream& os, const Link& 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) { @@ -382,4 +375,57 @@ 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(); + auto end = ctx.end(); + + if (it != end && *it != '}') { + presentation = *it++; + 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"); + } + } + + if (it != end && *it != '}') { + fmt::throw_format_error("Invalid format specifier for Link"); + } + + return it; + } + + 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]"); + } + 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()); + } +}; + +// 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) { + fmt::format_to(std::ostreambuf_iterator(os), "{}", link); + return os; +} +} // namespace podio + #endif // PODIO_DETAIL_LINK_H diff --git a/include/podio/detail/LinkCollectionImpl.h b/include/podio/detail/LinkCollectionImpl.h index f6b83140c..718f0b236 100644 --- a/include/podio/detail/LinkCollectionImpl.h +++ b/include/podio/detail/LinkCollectionImpl.h @@ -28,7 +28,9 @@ #include "nlohmann/json.hpp" #endif -#include +#include +#include + #include #include #include @@ -210,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(); } @@ -375,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) { @@ -460,4 +444,59 @@ void to_json(nlohmann::json& j, const podio::LinkCollection& collect } // namespace podio +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(); + auto end = ctx.end(); + + if (it != end && *it != '}') { + presentation = *it++; + if (presentation != 'b' && presentation != 'd' && presentation != 'u') { + fmt::throw_format_error( + "Unsupported format specifier for LinkCollection. Use 'b' for brief, 'd' for detailed, or 'u' for user-defined"); + } + } + + 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 { + if (presentation == 'u') { + return podio::detail::dispatchCustomFormat(coll, ctx); + } + 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; + } +}; + +// 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) { + fmt::format_to(std::ostreambuf_iterator(o), "{}", v); + return o; +} +} // namespace podio + #endif // PODIO_DETAIL_LINKCOLLECTIONIMPL_H diff --git a/podioVersion.in.h b/podioVersion.in.h index d46404d2f..db5ebc2e8 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/python/templates/Collection.cc.jinja2 b/python/templates/Collection.cc.jinja2 index a70741dcd..396c535b6 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 << fmt::format("{}", *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 c74a50d5c..1dc4d00d7 100644 --- a/python/templates/Collection.h.jinja2 +++ b/python/templates/Collection.h.jinja2 @@ -30,6 +30,9 @@ #include #include +#include +#include "podio/utilities/FormatHelpers.h" + namespace podio { struct RelationNames; } @@ -280,6 +283,36 @@ 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> { + char presentation = 'd'; + + constexpr auto parse(fmt::format_parse_context& ctx) { + auto it = ctx.begin(); + 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; + } + + 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"]) }} #endif 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/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/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 ecb9ca5af..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,4 +64,7 @@ private: {{ macros.std_hash(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..f66c499ca 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 9c77afd4d..80ec82f15 100644 --- a/python/templates/Object.h.jinja2 +++ b/python/templates/Object.h.jinja2 @@ -15,6 +15,9 @@ #include "podio/utilities/MaybeSharedPtr.h" #include "podio/detail/OrderKey.h" +#include +#include "podio/utilities/FormatHelpers.h" + #include #include @@ -79,6 +82,8 @@ std::ostream& operator<<(std::ostream& o, const {{ class.bare_type }}& value); {{ macros.std_hash(class) }} +{{ macros.formatter(class) }} + {{ workarounds.ld_library_path(class) }} #endif diff --git a/python/templates/macros/collections.jinja2 b/python/templates/macros/collections.jinja2 index fb9f7eb03..e46c9c5cf 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>::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"); +{% 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) %} diff --git a/python/templates/macros/declarations.jinja2 b/python/templates/macros/declarations.jinja2 index 83ed49ddc..0ed135a9f 100644 --- a/python/templates/macros/declarations.jinja2 +++ b/python/templates/macros/declarations.jinja2 @@ -158,3 +158,37 @@ struct std::hash<{{ namespace }}{{ prefix }}{{ class.bare_type }}> { } }; {% endmacro %} + +{% macro formatter(class, prefix='') %} +{% 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(); + 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; + } + + 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 6fca1698b..14c71d4e9 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 }}>::formatDefault(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/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/src/GenericParameters.cc b/src/GenericParameters.cc index 6befeb206..1aa6b38d1 100644 --- a/src/GenericParameters.cc +++ b/src/GenericParameters.cc @@ -1,6 +1,7 @@ #include "podio/GenericParameters.h" -#include +#include +#include namespace podio { @@ -18,45 +19,34 @@ 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"; - for (const auto& [key, value] : map) { - os << std::left << std::setw(30) << key << value << '\n'; - } - - 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); - + 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; +} 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]"); +} diff --git a/tests/unittests/links.cpp b/tests/unittests/links.cpp index 187390d26..09a09067e 100644 --- a/tests/unittests/links.cpp +++ b/tests/unittests/links.cpp @@ -15,6 +15,9 @@ #include "nlohmann/json.hpp" #endif +#include +#include "podio/utilities/FormatHelpers.h" + #include #include #include @@ -29,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>); @@ -299,6 +317,64 @@ TEST_CASE("Links templated accessors", "[links]") { } } // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) + +TEST_CASE("Link formatting", "[links]") { + TestL link; + + 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()); + } + + 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]") { STATIC_REQUIRE(podio::CollectionType); STATIC_REQUIRE(std::is_same_v, TestL>); @@ -443,6 +519,78 @@ 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); + + SECTION("Empty collection") { + auto formatted = fmt::format("{}", links); + REQUIRE_FALSE(formatted.empty()); + + 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()); + + // 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); + } + + 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) { auto colls = std::make_tuple(TestLColl(), ExampleHitCollection(), ExampleClusterCollection()); diff --git a/tests/unittests/unittest.cpp b/tests/unittests/unittest.cpp index 6538b9588..870b3c26e 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" @@ -60,12 +61,43 @@ #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" #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); + REQUIRE(formatted == "ffffffff|-1"); + + objId.collectionID = 42; + 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]") { auto coll = EventInfoCollection(); auto hit1 = MutableEventInfo(); @@ -138,6 +170,51 @@ 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]"); + // Ensure operator<< is still working + std::stringstream sstr; + sstr << mutCluster; + REQUIRE(sstr.str() == formatted); + + auto typeWithComponent = ExampleWithArrayComponent{}; + formatted = fmt::format("{}", typeWithComponent); + REQUIRE_FALSE(formatted.empty()); + + 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]") { auto coll1 = ExampleForCyclicDependency1Collection(); auto start = coll1.create(); @@ -413,6 +490,13 @@ TEST_CASE("UserDataCollection print", "[basics]") { coll.print(sstr); REQUIRE(sstr.str() == "[1, 2, 3]"); + + auto formatted = fmt::format("{}", coll); + REQUIRE(formatted == "[1, 2, 3]"); + + std::stringstream sstr2; + sstr2 << coll; + REQUIRE(sstr2.str() == formatted); } TEST_CASE("UserDataCollection access", "[basics]") { @@ -637,6 +721,28 @@ 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()); + + 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]") { ExampleWithUserInitCollection coll; // Default initialization values should work even through the create factory 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"};