diff --git a/CMakeLists.txt b/CMakeLists.txt index d37aae8a4..1b4bbde03 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -206,6 +206,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) @@ -222,8 +224,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 674602faa..b2d20c9e4 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(PODIO_ENABLE_SIO @ENABLE_SIO@) if(PODIO_ENABLE_SIO) diff --git a/cmake/podioTest.cmake b/cmake/podioTest.cmake index 8dd1fafb2..3b74eb51c 100644 --- a/cmake/podioTest.cmake +++ b/cmake/podioTest.cmake @@ -46,7 +46,7 @@ function(PODIO_SET_TEST_ENV test) PYTHONPATH=${PROJECT_SOURCE_DIR}/python:$ENV{PYTHONPATH} PODIO_SIOBLOCK_PATH=${PROJECT_BINARY_DIR}/tests PODIO_ARROW_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/doc/advanced_topics.md b/doc/advanced_topics.md index 0fd5743f7..8dc3eb09b 100644 --- a/doc/advanced_topics.md +++ b/doc/advanced_topics.md @@ -391,3 +391,130 @@ their objects. For more information or to follow future developments, see [podio issue #655](https://github.com/AIDASoft/podio/issues/655). +(formatting)= +## Formatting Objects and Collections + +All generated datatypes and collections come with built-in `fmt::formatter` +specializations that allow them to be used directly with `fmt::format` . The +formatters support several format specifiers and a customization point that lets +users define their own formatting for any generated type. + +### Format Specifiers + +Generated objects and collections support the following format specifiers: + +| Specifier | Description | +|-----------|-------------| +| `d` (default or detailed) | Detailed format showing all data members and relations | +| `u` (user defined) | User-defined format via the `customPodioFormat` ADL customization point | + +When no specifier is given, `d` is used. + +```cpp +#include + +edm::Hit hit = hits.create(1.0, 2.0, 3.0, 42.0); + +// Detailed format (default) - shows all members, relations, etc. +fmt::format("{}", hit); +fmt::format("{:d}", hit); // equivalent + +// Collections work the same way, with a tabular default format +fmt::format("{}", hits); +``` + +The detailed format for individual objects prints each data member, single +relation and multi relation on its own line. For collections it produces a +tabular layout with one row per element. + +Objects that are not available (e.g. created via `makeEmpty()`) format as +`[not available]`. + +### Custom Formatting with `customPodioFormat` + +The `u` format specifier invokes a user-defined `customPodioFormat` function that is +found via [Argument-Dependent +Lookup](https://en.cppreference.com/w/cpp/language/adl) (ADL). This follows the +same pattern as `std::swap`: you define a free function named `customPodioFormat` in +the **same namespace as your type**, and the formatter will find it +automatically. + +#### Function signature + +The `customPodioFormat` function must have the following signature: + +```cpp +fmt::format_context::iterator customPodioFormat(const YourType& value, fmt::format_context& ctx); +``` + +It receives the object to format and an `fmt::format_context`, and must return +the output iterator (typically by returning the result of `fmt::format_to`). + +#### Example + +For a generated datatype `edm::Cluster` and its collection: + +```cpp +// These overloads MUST be in the same namespace as the type (here: edm) +// so that ADL can find them. +namespace edm { + +fmt::format_context::iterator customPodioFormat(const Cluster& cluster, + fmt::format_context& ctx) { + return fmt::format_to(ctx.out(), "Cluster(e={:.2f})", cluster.energy()); +} + +fmt::format_context::iterator customPodioFormat(const ClusterCollection& coll, + fmt::format_context& ctx) { + return fmt::format_to(ctx.out(), "Clusters(n={})", coll.size()); +} + +} // namespace edm +``` + +These overloads are then used when formatting with `:u`: + +```cpp +fmt::format("{:u}", cluster); // "Cluster(e=42.50)" +fmt::format("{:u}", clusters); // "Clusters(n=3)" +``` + +#### Mutable objects + +The `fmt::formatter` for `MutableT` inherits from the `fmt::formatter` for the +corresponding immutable type `T`. This means that when formatting a mutable +object with `:u`, the mutable object is implicitly converted to its immutable +counterpart, and the `customPodioFormat` overload for the immutable type is called. +You do not need to provide separate overloads for mutable types. + +```cpp +MutableCluster mut{}; +mut.energy(42.5f); +fmt::format("{:u}", mut); // calls customPodioFormat(const Cluster&, ...) +``` + +#### Error handling + +If you use the `u` specifier on a type that has no `customPodioFormat` overload +defined, you will get a **compile-time error** when using compile-time format +strings (the default for `fmt::format`): + +```cpp +fmt::format("{:u}", someHit); // compile error if no customPodioFormat for Hit exists +``` + +The check uses `fmt::throw_format_error` inside the `constexpr` `parse()` +method. Since `fmt::format` validates format strings at compile time, an +unsupported `u` specifier is caught before the program runs. For runtime format +strings (via `fmt::runtime()`), the error manifests as a runtime +`fmt::format_error` exception instead. + +### `operator<<` support + +All formatted types also provide an `operator<<` that delegates to +`fmt::format`, so using objects and collections with output streams produces the +same result as the default format: + +```cpp +std::cout << hit << std::endl; // equivalent to fmt::print("{}\n", hit); +``` diff --git a/doc/links.md b/doc/links.md index 802316ad5..731e73135 100644 --- a/doc/links.md +++ b/doc/links.md @@ -175,6 +175,37 @@ for (const auto& [reco, weight] : linkedRecs) { Alternatively, you can access the object via the `o` member and the weight via the `weight` member. +## Formatting `Link`s and `LinkCollection`s + +`Link`s and `LinkCollection`s support the same `fmt::format` integration as +generated datatypes (see [formatting](advanced_topics.md#formatting) for full details). In +addition to the `d` (detailed, default) and `u` (user-defined) specifiers, they +also support a `b` (brief) specifier for compact output: + +```cpp +using TestLink = podio::Link; +TestLink link = /* ... */; + +fmt::format("{}", link); // detailed: one member per line +fmt::format("{:b}", link); // brief: "id | from.id to.id weight" (single line) +fmt::format("{:u}", link); // user-defined: calls customPodioFormat via ADL +``` + +For `LinkCollection`s, the brief format shows the collection type name, ID, and +size on a single line, while the detailed format lists each element in a table. + +Since `Link` types live in the `podio` namespace, `customPodioFormat` overloads +for the `u` specifier must also be placed in the `podio` namespace: + +```cpp +namespace podio { +fmt::format_context::iterator customPodioFormat(const TestLink& link, + fmt::format_context& ctx) { + return fmt::format_to(ctx.out(), "link(w={})", link.getWeight()); +} +} // namespace podio +``` + (implementation-details)= ## Implementation details diff --git a/include/podio/GenericParameters.h b/include/podio/GenericParameters.h index f21e15d99..9e765b011 100644 --- a/include/podio/GenericParameters.h +++ b/include/podio/GenericParameters.h @@ -275,4 +275,22 @@ 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 != '}') { + podio::detail::reportFormatError("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 02efc48df..0274f3244 100644 --- a/include/podio/ObjectID.h +++ b/include/podio/ObjectID.h @@ -1,10 +1,14 @@ #ifndef PODIO_OBJECTID_H #define PODIO_OBJECTID_H +#include "podio/utilities/FormatCompat.h" + +#include + #include #include #include -#include +#include #include #if defined(PODIO_JSON_OUTPUT) && !defined(__CLING__) @@ -39,13 +43,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}}; @@ -66,4 +63,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 != '}') { + podio::detail::reportFormatError("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 9193f80c1..954db3e16 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,37 @@ struct std::hash> { } }; +template +struct fmt::formatter> + : podio::ADLFormatter, fmt::formatter>, 'b'> { + + fmt::format_context::iterator formatImpl(const podio::LinkT& link, + fmt::format_context& ctx) const { + if (!link.isAvailable()) { + return fmt::format_to(ctx.out(), "[not available]"); + } + if (this->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 cf498c31d..a9b289de6 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) { @@ -463,4 +447,38 @@ void to_json(nlohmann::json& j, const podio::LinkCollection& collect } // namespace podio +template +struct fmt::formatter> + : podio::ADLFormatter, fmt::formatter>, 'b'> { + + fmt::format_context::iterator formatImpl(const podio::LinkCollection& coll, + fmt::format_context& ctx) const { + auto out = ctx.out(); + + if (this->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/include/podio/utilities/FormatCompat.h b/include/podio/utilities/FormatCompat.h new file mode 100644 index 000000000..dff939e35 --- /dev/null +++ b/include/podio/utilities/FormatCompat.h @@ -0,0 +1,32 @@ +#ifndef PODIO_UTILITIES_FORMATCOMPAT_H +#define PODIO_UTILITIES_FORMATCOMPAT_H + +// fmt/core.h is enough for all the error reporting functionality below and it +// also provides FMT_VERSION +#include + +namespace podio::detail { + +/// Report a format error at compile time or, via a fmt::format_error exception, +/// at runtime. +/// +/// The function that fmt provides for this purpose has changed over the +/// different versions: +/// - fmt 9 and 10: fmt::detail::throw_format_error (fmt 10 additionally exposes +/// it as fmt::throw_format_error) +/// - fmt 11 and newer: fmt::report_error (fmt::throw_format_error is deprecated +/// in fmt 11.0 and removed afterwards) +/// +/// This is intentionally not constexpr to give a compile time error when a +/// format string is checked at compile time. +[[noreturn]] inline void reportFormatError(const char* message) { +#if FMT_VERSION >= 110000 + fmt::report_error(message); +#else + fmt::detail::throw_format_error(message); +#endif +} + +} // namespace podio::detail + +#endif // PODIO_UTILITIES_FORMATCOMPAT_H diff --git a/include/podio/utilities/FormatHelpers.h b/include/podio/utilities/FormatHelpers.h new file mode 100644 index 000000000..f0ca3b81c --- /dev/null +++ b/include/podio/utilities/FormatHelpers.h @@ -0,0 +1,144 @@ +#ifndef PODIO_UTILITIES_FORMATHELPERS_H +#define PODIO_UTILITIES_FORMATHELPERS_H + +#include "podio/utilities/FormatCompat.h" + +#include + +#include +#include +#include + +namespace podio { + +namespace detail { + /// Concept to detect if customPodioFormat is defined for type T. Uses + /// unqualified lookup so that ADL can find overloads defined in the same + /// namespace as the type. Users should define their customPodioFormat + /// overloads in the same namespace as their type (following the same pattern + /// as std::swap). + template + concept HasCustomFormat = requires(const T& val, fmt::format_context& ctx) { + { customPodioFormat(val, ctx) } -> std::same_as; + }; + + /// Dispatch helper: calls customPodioFormat if available, otherwise throws a format + /// error. + template + fmt::format_context::iterator dispatchCustomFormat(const T& val, fmt::format_context& ctx) { + if constexpr (HasCustomFormat) { + return customPodioFormat(val, ctx); + } else { + podio::detail::reportFormatError("Format specifier 'u' requires a customPodioFormat for this type"); + return ctx.out(); // unreachable, silences warnings + } + } + + /// Build a compile-time error message listing supported format specifiers. + /// Produces a message like: "Invalid format specifier. Supported: 'b', 'd', 'u'" + template + consteval auto buildSpecErrorMessage() { + constexpr std::size_t N = sizeof...(Specs); + constexpr std::string_view prefix = "Invalid format specifier. Supported: "; + constexpr std::string_view delim = ", "; + + // result buffer size = prefix + 3 chars per spec and appropriate number of + // delimiters and null terminator + constexpr auto bufLen = prefix.size() + (N * 3) + (N > 0 ? (N - 1) * delim.size() : 0) + 1; + std::array result{}; + auto out = result.begin(); + out = std::ranges::copy(prefix, out).out; + + if constexpr (N == 0) { + return result; + } + + // Sort specifiers alphabetically for consistent display + std::array specs{Specs...}; + std::ranges::sort(specs); + + auto format_spec = [&](char c) { + const std::array wrapped = {'\'', c, '\''}; + out = std::ranges::copy(wrapped, out).out; + }; + // First spec without leading delimiter + format_spec(specs[0]); + // The rest with leading delimieter + std::ranges::for_each(std::ranges::subrange(specs.begin() + 1, specs.end()), [&](char c) { + out = std::ranges::copy(delim, out).out; + format_spec(c); + }); + + return result; + } + + template + inline constexpr auto specErrorMsg = buildSpecErrorMessage(); + +} // namespace detail + +/// CRTP base for fmt::formatters that support ADL-based custom formatting. +/// +/// Provides the common parse/format logic shared by all podio formatters that +/// support the 'u' (user-defined via ADL) format specifier. The default format +/// specifier is 'd' (default - tries user-defined, falls back to code-generated). +/// The 'g' specifier always uses code-generated formatting. Additional specifiers +/// (e.g. 'b' for brief) can be added via the ExtraSpecifiers template parameter pack. +/// +/// @tparam T The type being formatted +/// @tparam Derived The concrete fmt::formatter specialization (CRTP) +/// @tparam ExtraSpecifiers Additional single-char format specifiers beyond 'd', 'g', and 'u' +/// +/// Derived classes must implement: +/// fmt::format_context::iterator formatImpl(const T& value, fmt::format_context& ctx) const; +template +struct ADLFormatter { + 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++; + bool valid = (it == end) || (*it == '}'); + // First check if we have a custom format available and can use it + if (presentation == 'u') { + if (detail::HasCustomFormat && valid) { + return it; + } + podio::detail::reportFormatError("Format specifier 'u' requires an overload of defineCustomPodioFormat for this type"); + } + + // Now check the rest and emit a corresponding error message depending on + // whether 'u' is available or not + if (valid && presentation != 'd' && presentation != 'g' && ((presentation != ExtraSpecifiers) && ...)) { + if constexpr (detail::HasCustomFormat) { + podio::detail::reportFormatError(detail::specErrorMsg<'d', 'g', 'u', ExtraSpecifiers...>.data()); + } else { + podio::detail::reportFormatError(detail::specErrorMsg<'d', 'g', ExtraSpecifiers...>.data()); + } + } + } + return it; + } + + fmt::format_context::iterator format(const T& value, fmt::format_context& ctx) const { + if (presentation == 'u') { + return detail::dispatchCustomFormat(value, ctx); + } + if (presentation == 'd') { + // For 'd', try user-defined formatting if available, otherwise fall back to code-generated + if constexpr (detail::HasCustomFormat) { + return detail::dispatchCustomFormat(value, ctx); + } else { + return static_cast(*this).formatImpl(value, ctx); + } + } + // For 'g' and any ExtraSpecifiers, always use code-generated formatting + return static_cast(*this).formatImpl(value, ctx); + } +}; + +} // namespace podio + +#endif // PODIO_UTILITIES_FORMATHELPERS_H diff --git a/podioVersion.in.h b/podioVersion.in.h index 83ac51cb8..f8483fbd2 100644 --- a/podioVersion.in.h +++ b/podioVersion.in.h @@ -1,11 +1,15 @@ #ifndef PODIO_PODIOVERSION_H #define PODIO_PODIOVERSION_H +#include "podio/utilities/FormatCompat.h" + +#include + #include #include #include -#include #include +#include #include // Some preprocessor constants and macros for the use cases where they might be @@ -57,29 +61,9 @@ struct Version { #undef DEFINE_COMP_OPERATOR - explicit operator std::string() const { - std::stringstream ss; - ss << *this; - return ss.str(); - } - - static std::optional fromString(const std::string& versionStr) { - uint16_t major = 0, minor = 0, patch = 0; - char dot1, dot2; - std::stringstream ss(versionStr); - if (ss >> major >> dot1 >> minor >> dot2 >> patch && dot1 == '.' && dot2 == '.') { - return Version{major, minor, patch}; - } - return std::nullopt; - } - - 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}; @@ -91,4 +75,29 @@ static consteval 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 != '}') { + podio::detail::reportFormatError("Invalid format. Version does not support specifiers"); + } + return it; + } + + auto format(const podio::version::Version& version, fmt::format_context& ctx) const { + 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 0acc2e736..ad0a31801 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 @@ -260,6 +262,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..3c002630c 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,12 @@ void to_json(nlohmann::json& j, const {{ class.bare_type }}Collection& collectio {{ utils.namespace_close(class.namespace) }} +template <> +struct fmt::formatter<{{ class.full_type }}Collection> + : podio::ADLFormatter<{{ class.full_type }}Collection, fmt::formatter<{{ class.full_type }}Collection>> { + fmt::format_context::iterator formatImpl(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 b23fb1408..8757f8ffc 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 @@ -190,4 +192,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 87a72dc1b..80e582ce5 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>::formatImpl(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 75058f5bc..6e0900481 100644 --- a/python/templates/macros/declarations.jinja2 +++ b/python/templates/macros/declarations.jinja2 @@ -153,3 +153,14 @@ struct std::hash<{{ namespace }}{{ prefix }}{{ class.bare_type }}> { } }; {% endmacro %} + +{% macro formatter(class, prefix='') %} +{% set namespace = class.namespace + '::' if class.namespace else '' %} +{% set fulltype = namespace + prefix + class.bare_type %} +template <> +struct fmt::formatter<{{ fulltype }}> + : podio::ADLFormatter<{{ fulltype }}, fmt::formatter<{{ fulltype }}>> { + fmt::format_context::iterator formatImpl(const {{ fulltype }}& value, fmt::format_context& ctx) const; +}; + +{% endmacro %} diff --git a/python/templates/macros/implementations.jinja2 b/python/templates/macros/implementations.jinja2 index 9653c4f48..d55a7e946 100644 --- a/python/templates/macros/implementations.jinja2 +++ b/python/templates/macros/implementations.jinja2 @@ -188,42 +188,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 }}>::formatImpl(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 3a52679c4..966c623be 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -79,6 +79,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 235456b1e..c756ddff7 100644 --- a/tests/unittests/interface_types.cpp +++ b/tests/unittests/interface_types.cpp @@ -219,3 +219,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 fd76eaed6..8a45a0f43 100644 --- a/tests/unittests/links.cpp +++ b/tests/unittests/links.cpp @@ -15,6 +15,8 @@ #include "nlohmann/json.hpp" #endif +#include + #include #include #include @@ -29,6 +31,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 customPodioFormat(const TestL& link, fmt::format_context& ctx) { + return fmt::format_to(ctx.out(), "custom-link(w={})", link.getWeight()); +} + +fmt::format_context::iterator customPodioFormat(const TestMutL& link, fmt::format_context& ctx) { + return fmt::format_to(ctx.out(), "custom-mut-link(w={})", link.getWeight()); +} + +fmt::format_context::iterator customPodioFormat(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>); @@ -305,6 +322,69 @@ TEST_CASE("Links templated accessors", "[links]") { } } // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) + +TEST_CASE("Link formatting", "[links]") { + TestL link; + + SECTION("Code-generated format (detailed)") { + // TestL has a custom format defined, so use {:g} to test code-generated format + auto formatted = fmt::format("{:g}", 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()); + } + + SECTION("Default format uses custom if available") { + // Default format ({} or {:d}) should use custom format when available + auto formatted_default = fmt::format("{}", link); + auto formatted_d = fmt::format("{:d}", link); + REQUIRE(formatted_default == "custom-link(w=1)"); + REQUIRE(formatted_d == "custom-link(w=1)"); + } + + 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("{:g}", 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("{:g}", 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>); @@ -449,6 +529,81 @@ 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") { + // TestLColl has a custom format defined, so use {:g} for code-generated format + auto formatted = fmt::format("{:g}", 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 code-generated format (detailed) + auto formatted_codegen = fmt::format("{:g}", links); + REQUIRE_FALSE(formatted_codegen.empty()); + REQUIRE(formatted_codegen.find("id:") != std::string::npos); + REQUIRE(formatted_codegen.find("weight:") != std::string::npos); + REQUIRE(formatted_codegen.find("from") != std::string::npos); + REQUIRE(formatted_codegen.find("to") != std::string::npos); + + // Test default format (should use custom) + auto formatted_default = fmt::format("{}", links); + auto formatted_d = fmt::format("{:d}", links); + REQUIRE(formatted_default == "custom-link-coll(n=2)"); + REQUIRE(formatted_d == "custom-link-coll(n=2)"); + + // 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_codegen.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 92fea37f3..e91c087b3 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,44 @@ #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 "podio/utilities/FormatHelpers.h" + +#include + +#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 customPodioFormat(const ExampleCluster& cluster, fmt::format_context& ctx) { + return fmt::format_to(ctx.out(), "custom-cluster(e={})", cluster.energy()); +} + +fmt::format_context::iterator customPodioFormat(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 +171,55 @@ TEST_CASE("makeEmpty", "[basics]") { REQUIRE(hit.energy() == 0); } +TEST_CASE("Object formatting", "[basics][formatting]") { + // ExampleCluster has a custom format defined, so use {:g} to test code-generated format + ExampleCluster cluster; + auto formatted = fmt::format("{:g}", cluster); + REQUIRE_FALSE(formatted.empty()); + REQUIRE(formatted != "[not avaialble]"); + + cluster = ExampleCluster::makeEmpty(); + formatted = fmt::format("{:g}", cluster); + REQUIRE(formatted == "[not available]"); + + auto mutCluster = MutableExampleCluster{}; + formatted = fmt::format("{:g}", mutCluster); + REQUIRE_FALSE(formatted.empty()); + REQUIRE(formatted != "[not available]"); + // Ensure operator<< is still working (uses default format, which uses custom if available) + std::stringstream sstr; + sstr << mutCluster; + auto formatted_default = fmt::format("{}", mutCluster); + REQUIRE(sstr.str() == formatted_default); + + 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 fails for types without a customPodioFormat overload. + // With compile-time format strings this would be a compile error; use + // fmt::runtime to verify the runtime error path. + auto hitForFmt = ExampleHit{}; + REQUIRE_THROWS_AS(fmt::format(fmt::runtime("{:u}"), hitForFmt), fmt::format_error); +} + TEST_CASE("Cyclic dependencies", "[LEAK-FAIL][basics][relations][memory-management]") { SECTION("with collections") { auto coll1 = ExampleForCyclicDependency1Collection(); @@ -418,6 +500,13 @@ TEST_CASE("UserDataCollection basics", "[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); } SECTION("access") { @@ -647,6 +736,30 @@ 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 fails for collections without a customPodioFormat overload. + // With compile-time format strings this would be a compile error; use + // fmt::runtime to verify the runtime error path. + REQUIRE_THROWS_AS(fmt::format(fmt::runtime("{: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 3349c08a5..a57e74743 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"};