Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b156bbc
Add compatibility with fmtlib formatting for collections
tmadlener Feb 12, 2026
ce57da5
Add fmt compatibility for ObjectID
tmadlener Feb 12, 2026
ea46c52
Add fmtlib compatibility for Links
tmadlener Feb 12, 2026
18b6fff
Add fmt compatibility for generated objects
tmadlener Feb 12, 2026
ae9b92e
Add fmt compatibility for generated components
tmadlener Feb 12, 2026
612ad36
Add fmt compatibility for interface types
tmadlener Feb 13, 2026
8114b41
Add fmt support for ObjectId
tmadlener Feb 12, 2026
c85dd3f
Add fmt support for Links
tmadlener Feb 12, 2026
e89f2c8
Add fmt support for LinkCollections
tmadlener Feb 12, 2026
0663a5f
Add fmt support for Version
tmadlener Feb 12, 2026
3e91adf
Make UserDataCollection formattable as range
tmadlener Feb 13, 2026
fa9e980
Ensure Links and LinkCollections remain formattable
tmadlener Feb 13, 2026
913f754
Add proper fmt support for user facing handles
tmadlener Feb 13, 2026
f070d1e
Add fmt support for collections
tmadlener Feb 13, 2026
71c852d
Implement CollectionBase::print in terms of format
tmadlener Feb 13, 2026
ff52574
Implement GenericParamters::print in terms of format
tmadlener Feb 13, 2026
ba63709
Remove unnecessary template parameter specification
tmadlener Feb 13, 2026
d2bf4a5
Add format support for GenericParameters
tmadlener Feb 13, 2026
bdddddf
Make sure fmt headers can be found by ROOT interactively
tmadlener Feb 13, 2026
38cb20b
Add brief format specifier to Link for more concise formatting
tmadlener Feb 13, 2026
8bdaade
Add brief format specifier to LinkCollection
tmadlener Feb 13, 2026
807b4d9
First version of customization hook
tmadlener Feb 13, 2026
684d3c7
First version of tests that use custom formatting hooks
tmadlener Feb 13, 2026
e890364
Add documentation focused on usage of the formatting
tmadlener Feb 16, 2026
cf9ab37
[WIP] Make custom formatter a compile time decision
tmadlener Feb 16, 2026
c4bac1e
[wip] First ADLFormatter base class implementation
tmadlener Feb 16, 2026
02797de
[wip] Better compile time error messages
tmadlener Feb 16, 2026
5c87738
[wip] Rename the customization point to customPodioFormat
tmadlener Feb 16, 2026
babaee3
[wip] Introduce explicit specifier for generated format
tmadlener Feb 16, 2026
5829013
[wip] fix build with fmt12
tmadlener Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -222,8 +224,6 @@ if(BUILD_TESTING)
add_subdirectory(tests)
endif()

find_package(fmt 9 REQUIRED)

add_subdirectory(tools)
add_subdirectory(python)

Expand Down
1 change: 1 addition & 0 deletions cmake/podioConfig.cmake.in
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion cmake/podioTest.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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}:$<TARGET_FILE_DIR:fmt::fmt>/../include
SKIP_SIO_TESTS=$<NOT:$<BOOL:${ENABLE_SIO}>>
IO_HANDLERS=${IO_HANDLERS}
PODIO_USE_CLANG_FORMAT=${PODIO_USE_CLANG_FORMAT}
Expand Down
127 changes: 127 additions & 0 deletions doc/advanced_topics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <fmt/core.h>

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);
```
31 changes: 31 additions & 0 deletions doc/links.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExampleHit, ExampleCluster>;
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

Expand Down
18 changes: 18 additions & 0 deletions include/podio/GenericParameters.h
Original file line number Diff line number Diff line change
Expand Up @@ -275,4 +275,22 @@ void GenericParameters::loadFrom(VecLike<std::string> keys, VecLike<std::vector<
}

} // namespace podio

#include "podio/utilities/FormatCompat.h"

#include <fmt/core.h>

template <>
struct fmt::formatter<podio::GenericParameters> {
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
35 changes: 27 additions & 8 deletions include/podio/ObjectID.h
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
#ifndef PODIO_OBJECTID_H
#define PODIO_OBJECTID_H

#include "podio/utilities/FormatCompat.h"

#include <fmt/core.h>

#include <compare>
#include <cstdint>
#include <functional>
#include <iomanip>
#include <iterator>
#include <ostream>

#if defined(PODIO_JSON_OUTPUT) && !defined(__CLING__)
Expand Down Expand Up @@ -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}};
Expand All @@ -66,4 +63,26 @@ struct std::hash<podio::ObjectID> {
}
};

template <>
struct fmt::formatter<podio::ObjectID> {
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
16 changes: 7 additions & 9 deletions include/podio/UserDataCollection.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
#include "podio/detail/Pythonizations.h"
#include "podio/utilities/TypeHelpers.h"

#include <fmt/ostream.h>
#include <fmt/ranges.h>

#include <iterator>

#define PODIO_ADD_USER_TYPE(type) \
template <> \
consteval const char* userDataTypeName<type>() { \
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -321,7 +319,7 @@ using UserDataCollectionTypes = decltype(std::apply(

template <SupportedUserDataType BasicType>
std::ostream& operator<<(std::ostream& o, const podio::UserDataCollection<BasicType>& coll) {
coll.print(o);
fmt::format_to(std::ostreambuf_iterator(o), "{}", coll);
return o;
}

Expand Down
50 changes: 38 additions & 12 deletions include/podio/detail/Link.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
#include "nlohmann/json.hpp"
#endif

#include "podio/utilities/FormatHelpers.h"

#include <fmt/core.h>
#include <fmt/ranges.h>

#include <functional>
#include <ostream>
#include <type_traits>
Expand Down Expand Up @@ -348,18 +353,6 @@ class LinkT {
podio::utils::MaybeSharedPtr<LinkObjT> m_obj{nullptr};
};

template <typename FromT, typename ToT>
std::ostream& operator<<(std::ostream& os, const Link<FromT, ToT>& 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 <typename FromT, typename ToT>
void to_json(nlohmann::json& j, const podio::LinkT<FromT, ToT, false>& link) {
Expand All @@ -382,4 +375,37 @@ struct std::hash<podio::LinkT<FromT, ToT, Mutable>> {
}
};

template <typename FromT, typename ToT, bool Mutable>
struct fmt::formatter<podio::LinkT<FromT, ToT, Mutable>>
: podio::ADLFormatter<podio::LinkT<FromT, ToT, Mutable>, fmt::formatter<podio::LinkT<FromT, ToT, Mutable>>, 'b'> {

fmt::format_context::iterator formatImpl(const podio::LinkT<FromT, ToT, Mutable>& 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 <typename FromT, typename ToT, bool Mutable, typename Char>
struct fmt::is_tuple_formattable<podio::LinkT<FromT, ToT, Mutable>, Char> : std::false_type {};

namespace podio {
template <typename FromT, typename ToT, bool Mutable>
std::ostream& operator<<(std::ostream& os, const LinkT<FromT, ToT, Mutable>& link) {
fmt::format_to(std::ostreambuf_iterator(os), "{}", link);
return os;
}
} // namespace podio

#endif // PODIO_DETAIL_LINK_H
Loading
Loading