util: add new module for reading and writing toml

The toml module situation in Python is a bit of a mess.  Different
distro versions have different modules packaged or built-in, sometimes
with different capabilities (no writing).  Since we need to support
reading and writing toml files both on the host (osbuild internals,
sources, inputs) and in the build root (stages), let's centralise the
import decision making in an internal utility module that covers all
cases.

Two of the modules we might import (tomli and tomllib) don't support
writing, so we need to either import a separate module (tomli_w) or
raise an exception when dump() is called without a write-capable module.

The tomli and tomllib modules require files be opened in binary mode
(not text) while the others require text mode.  So we can't wrap the
toml.load() and toml.dump() functions directly; the caller doesn't know
which module it will be using.  Let's keep track of the mode based on
which import succeeded and have our functions open the files as needed.

The wrapper functions are named load_from_file() and dump_to_file() to
avoid confusion with the load() and dump() functions that take a file
object.

See also #1847
This commit is contained in:
Achilleas Koutsou 2024-08-15 14:06:49 +02:00 committed by Michael Vogt
parent 347c0dec4a
commit 94cdcecafb

62
osbuild/util/toml.py Normal file
View file

@ -0,0 +1,62 @@
"""
Utility functions for reading and writing toml files.
Handles module imports for all supported versions (in a build root or on a host).
"""
import importlib
# Different modules require different file mode (text vs binary)
toml_modules = {
"tomllib": "rb", # stdlib since 3.11 (read-only)
"tomli": "rb", # EL9+
"toml": "r", # older unmaintained lib, needed for backwards compatibility with existing EL9 and Fedora manifests
"pytoml": "r", # deprecated, needed for backwards compatibility (EL8 manifests)
}
toml = None
rmode: str = None
for module, mode in toml_modules.items():
try:
toml = importlib.import_module(module)
rmode = mode
break
except ModuleNotFoundError:
pass
else:
raise ModuleNotFoundError("No toml module found: " + ", ".join(toml_modules))
# Different modules require different file mode (text vs binary)
tomlw_modules = {
"tomli_w": "wb", # EL9+
"toml": "w", # older unmaintained lib, needed for backwards compatibility with existing EL9 and Fedora manifests
"pytoml": "w", # deprecated, needed for backwards compatibility (EL8 manifests)
}
tomlw = None
wmode: str = None
for module, mode in tomlw_modules.items():
try:
tomlw = importlib.import_module(module)
wmode = mode
break
except ModuleNotFoundError:
# allow importing without write support
pass
def load_from_file(path):
with open(path, rmode) as tomlfile:
return toml.load(tomlfile)
def dump_to_file(data, path, pre=None):
if tomlw is None:
raise RuntimeError("no toml module available with write support")
with open(path, wmode) as tomlfile:
if pre:
tomlfile.write(pre)
tomlw.dump(data, tomlfile)