From 52f420b7ff985919a3f94984c87d980b7fcba769 Mon Sep 17 00:00:00 2001 From: Rohan McGovern Date: Mon, 9 Aug 2021 13:44:38 +1000 Subject: [PATCH] Tolerate absence of rpm at import time Some of the code in the staged backend depends on kobo.rpmlib, which in turn depends on python bindings to rpm. Previously, it was a hard dependency that these bindings must be available even for "import pushsource" to succeed. Let's make it a bit friendlier and avoid a hard dependency at import-time: we will only raise if we reach the relevant piece of code at runtime. Motivations for doing this now include: - Python bindings to rpm aren't easy to install in all contexts. Although there's an "rpm-py-installer" package on pypi which can help, the way it works is unusual and insecure, and I don't think it's appropriate to declare a dependency on that package within install_requires. - Recently, pubtools-1.1.0 was released, which now attempts to eagerly import all installed pubtools-* projects during task startup to ensure that all installed hooks are registered. This means that pushsource library is now being imported in some cases where it previously wasn't, which means that some environments would now require rpm bindings where they previously didn't. It'd be nice to avoid that. --- CHANGELOG.md | 2 ++ src/pushsource/_impl/backend/broken_rpmlib.py | 17 +++++++++++++++++ .../_impl/backend/staged/staged_rpm.py | 14 +++++++++++--- tests/staged/test_broken_rpmlib.py | 15 +++++++++++++++ 4 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 src/pushsource/_impl/backend/broken_rpmlib.py create mode 100644 tests/staged/test_broken_rpmlib.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 710e4c46..090f06e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Internally created executors are now named for improved metrics and debuggability. +- Library no longer has a hard dependency on Python bindings to `rpm`, if RPM-related + functionality is not used. ## [2.9.0] - 2021-07-05 diff --git a/src/pushsource/_impl/backend/broken_rpmlib.py b/src/pushsource/_impl/backend/broken_rpmlib.py new file mode 100644 index 00000000..6fd6b705 --- /dev/null +++ b/src/pushsource/_impl/backend/broken_rpmlib.py @@ -0,0 +1,17 @@ +import six + +# A replacement for kobo.rpmlib to give a decent error message +# when rpm bindings are not available. + +# importer is expected to put a more specific cause here. +CAUSE = RuntimeError("unknown error") + +MESSAGE = "kobo.rpmlib is not available (consider 'pip install rpm-py-installer')" + + +def not_available(*_args, **_kwargs): + six.raise_from(RuntimeError(MESSAGE), CAUSE) + + +get_rpm_header = not_available +get_keys_from_header = not_available diff --git a/src/pushsource/_impl/backend/staged/staged_rpm.py b/src/pushsource/_impl/backend/staged/staged_rpm.py index eb7f0c74..955cbfe5 100644 --- a/src/pushsource/_impl/backend/staged/staged_rpm.py +++ b/src/pushsource/_impl/backend/staged/staged_rpm.py @@ -1,6 +1,14 @@ import logging -import kobo.rpmlib +try: + from kobo import rpmlib +except Exception as ex: # pragma: no cover, pylint: disable=broad-except + # If kobo.rpmlib is unavailable, let's not immediately crash. + # We will hold this exception and re-raise it only if there's an + # attempt to use the related functionality. + from .. import broken_rpmlib as rpmlib + + rpmlib.CAUSE = ex from ...model import RpmPushItem from .staged_base import StagedBaseMixin, handles_type @@ -19,8 +27,8 @@ def __push_item(self, leafdir, _, entry): LOG.warning("Unexpected non-RPM %s (ignored)", entry.path) return None - header = kobo.rpmlib.get_rpm_header(entry.path) - key_id = kobo.rpmlib.get_keys_from_header(header) + header = rpmlib.get_rpm_header(entry.path) + key_id = rpmlib.get_keys_from_header(header) return RpmPushItem( name=entry.name, diff --git a/tests/staged/test_broken_rpmlib.py b/tests/staged/test_broken_rpmlib.py new file mode 100644 index 00000000..9aba47a2 --- /dev/null +++ b/tests/staged/test_broken_rpmlib.py @@ -0,0 +1,15 @@ +import pytest + +from pushsource._impl.backend import broken_rpmlib + + +@pytest.mark.parametrize( + "fn", [broken_rpmlib.get_keys_from_header, broken_rpmlib.get_rpm_header] +) +def test_functions_raise(fn): + """Expected functions are present in broken_rpmlib and raise on use.""" + + with pytest.raises(RuntimeError) as excinfo: + fn("a", "b", "c") + + assert "kobo.rpmlib is not available" in str(excinfo)