From 3e8f634226a4615425c6486c8d54d8fdbc340a32 Mon Sep 17 00:00:00 2001 From: Thomas Madlener Date: Thu, 19 Sep 2024 10:35:51 +0200 Subject: [PATCH] Add version module that allows to compare versions in python (#678) * Add version module that allows to compare versions in python * Make comparison operators from c++ available in python * Make it an import error if the header cannot be found --- podioVersion.in.h | 8 ++---- python/podio/test_version.py | 54 ++++++++++++++++++++++++++++++++++++ python/podio/version.py | 45 ++++++++++++++++++++---------- tools/podio-dump | 5 ++-- 4 files changed, 89 insertions(+), 23 deletions(-) create mode 100644 python/podio/test_version.py diff --git a/podioVersion.in.h b/podioVersion.in.h index 3348e9d5e..4a26f4f9b 100644 --- a/podioVersion.in.h +++ b/podioVersion.in.h @@ -41,10 +41,9 @@ struct Version { uint16_t minor{0}; uint16_t patch{0}; -#if __cplusplus >= 202002L - auto operator<=>(const Version&) const = default; -#else - // No spaceship yet in c++17 + // We explicitly define these here instead of using the default spaceship + // operator because cppyy does not recognize that yet and we want the + // comparisons to work in python as well #define DEFINE_COMP_OPERATOR(OP) \ constexpr bool operator OP(const Version& o) const noexcept { \ return std::tie(major, minor, patch) OP std::tie(o.major, o.minor, o.patch); \ @@ -58,7 +57,6 @@ struct Version { DEFINE_COMP_OPERATOR(!=) #undef DEFINE_COMP_OPERATOR -#endif explicit operator std::string() const { std::stringstream ss; diff --git a/python/podio/test_version.py b/python/podio/test_version.py new file mode 100644 index 000000000..21234a0e2 --- /dev/null +++ b/python/podio/test_version.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Unittests for python version module""" + +import unittest + +from podio import version +from podio import __version__ + + +class TestVersion(unittest.TestCase): + """General unittests for the python bindings of Version""" + + def test_parse(self): + """Make sure that parse works as expected""" + vv = version.parse(1, 2, 3) + self.assertEqual(vv.major, 1) + self.assertEqual(vv.minor, 2) + self.assertEqual(vv.patch, 3) + + vv = version.parse(0, 2) + self.assertEqual(vv.major, 0) + self.assertEqual(vv.minor, 2) + self.assertEqual(vv.patch, 0) + + vv = version.parse("42.0") + self.assertEqual(vv.major, 42) + self.assertEqual(vv.minor, 0) + self.assertEqual(vv.patch, 0) + + vv = version.Version() + self.assertEqual(vv.major, 0) + self.assertEqual(vv.minor, 0) + self.assertEqual(vv.patch, 0) + + def test_build_version(self): + """Make sure that the build version is set consistently (i.e. configured + correctly)""" + self.assertEqual(version.build_version, version.parse(__version__)) + + def test_version_comparison(self): + """Make sure that version comparisons work""" + v1 = version.parse(1, 2, 3) + v2 = version.parse("0.4.2") + self.assertTrue(v1 > v2) + + v3 = version.parse("1.2.3") + self.assertEqual(v3, v1) + + def test_string_representation(self): + """Make sure the string representation is OK""" + self.assertEqual(f"{version.build_version}", __version__) + + vv = version.parse(42) + self.assertEqual(f"{vv}", "42.0.0") diff --git a/python/podio/version.py b/python/podio/version.py index 9707e2df1..3d708c2ec 100644 --- a/python/podio/version.py +++ b/python/podio/version.py @@ -4,23 +4,38 @@ import ROOT # NOTE: It is necessary that this can be found on the ROOT_INCLUDE_PATH -# -# We check whether we can actually load the header to not break python bindings -# in environments with *ancient* podio versions -if ROOT.gInterpreter.LoadFile("podio/podioVersion.h") == 0: # noqa: E402 - from ROOT import podio # noqa: E402 # pylint: disable=wrong-import-position +if ROOT.gInterpreter.LoadFile("podio/podioVersion.h") != 0: # noqa: E402 + raise ImportError("Cannot find the podio/podioVersion.h header") -build_version = podio.version.build_version +from ROOT import podio # noqa: E402 # pylint: disable=wrong-import-position + +Version = podio.version.Version + + +def _str_dunder(self): + """Shim to get a more reasonable string representation""" + return f"{self.major}.{self.minor}.{self.patch}" -def version_as_str(ver): - """Stringify the version into the usual format +Version.__str__ = _str_dunder - Args: - ver (podio.version.Version): A podio version - Returns: - str: A stringified version of the version, in the format - MAJOR.MINOR.PATCH - """ - return f"{ver.major}.{ver.minor}.{ver.patch}" +def parse(*args): + """Construct a version from either a list of integers or a version string""" + if len(args) == 1: + if isinstance(args[0], podio.version.Version): + return args[0] + if isinstance(args[0], str): + ver_tuple = tuple(int(v) for v in args[0].split(".")) + else: + ver_tuple = (int(args[0]),) + else: + ver_tuple = tuple(args) + ver = Version() + for mem, val in zip(("major", "minor", "patch"), ver_tuple): + setattr(ver, mem, val) + return ver + + +# The version with which podio has been built. Same as __version__ +build_version = podio.version.build_version diff --git a/tools/podio-dump b/tools/podio-dump index f85387711..ea8143762 100755 --- a/tools/podio-dump +++ b/tools/podio-dump @@ -8,7 +8,6 @@ import yaml from tabulate import tabulate from podio_version import __version__ -from podio.version import version_as_str def print_general_info(reader, filename): @@ -25,14 +24,14 @@ def print_general_info(reader, filename): print( f"input file: {filename}{legacy_text}\n" " (written with podio version: " - f"{version_as_str(reader.current_file_version())})\n" + f"{reader.current_file_version()})\n" ) print("datamodel model definitions stored in this file: ") for edm_name in reader.datamodel_definitions: try: edm_version = reader.current_file_version(edm_name) - print(f" - {edm_name} ({version_as_str(edm_version)})") + print(f" - {edm_name} ({edm_version})") except KeyError: print(f" - {edm_name}")