package: Add package expressions

- Package represents a package to potentially be installed.

- PackageOr allows an alternate package, in case the first one is not
  available.

Signed-off-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
James Valleroy 2022-03-05 11:19:01 -05:00
parent 8d8a7c9837
commit 31a457029c
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
3 changed files with 113 additions and 3 deletions

View File

@ -25,3 +25,11 @@ class DomainNotRegisteredError(PlinthError):
FreedomBox doesn't have a registered domain
"""
pass
class MissingPackageError(PlinthError):
"""Package is not available to be installed at this time."""
def __init__(self, name):
self.name = name
super().__init__(self.name)

View File

@ -17,12 +17,78 @@ from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy
from plinth import actions, app
from plinth.errors import ActionError
from plinth.errors import ActionError, MissingPackageError
from plinth.utils import format_lazy
logger = logging.getLogger(__name__)
class PackageExpression:
def possible(self) -> list[str]:
"""Return the list of possible packages before resolving."""
raise NotImplementedError
def actual(self) -> str:
"""Return the resolved list of packages to install.
TODO: Also return version and suite to install from.
"""
raise NotImplementedError
class Package(PackageExpression):
def __init__(
self,
name,
optional: bool = False,
version: Optional[str] = None, # ">=1.0,<2.0"
distribution: Optional[str] = None, # Debian, Ubuntu
suite: Optional[str] = None, # stable, testing
codename: Optional[str] = None, # bullseye-backports
architecture: Optional[str] = None): # arm64
self.name = name
self.optional = optional
self.version = version
self.distribution = distribution
self.suite = suite
self.codename = codename
self.architecture = architecture
def __or__(self, other):
return PackageOr(self, other)
def possible(self) -> list[str]:
return [self.name]
def actual(self) -> str:
cache = apt.Cache()
if self.name in cache:
# TODO: Also return version and suite to install from
return self.name
raise MissingPackageError(self.name)
class PackageOr(PackageExpression):
"""Specify that one of the two packages will be installed."""
def __init__(self, package1: PackageExpression,
package2: PackageExpression):
self.package1 = package1
self.package2 = package2
def possible(self) -> list[str]:
return self.package1.possible() + self.package2.possible()
def actual(self) -> str:
try:
return self.package1.actual()
except MissingPackageError:
return self.package2.actual()
class Packages(app.FollowerComponent):
"""Component to manage the packages of an app.

View File

@ -3,17 +3,53 @@
Test module for package module.
"""
import unittest
from unittest.mock import Mock, call, patch
import pytest
from plinth.app import App
from plinth.errors import ActionError
from plinth.package import Packages, packages_installed, remove
from plinth.errors import ActionError, MissingPackageError
from plinth.package import Package, Packages, packages_installed, remove
setup_helper = Mock()
class TestPackageExpressions(unittest.TestCase):
def test_package(self):
"""Test resolving package names."""
package = Package('python3')
assert package.possible() == ['python3']
assert package.actual() == 'python3'
package = Package('unknown-package')
assert package.possible() == ['unknown-package']
self.assertRaises(MissingPackageError, package.actual)
def test_package_or_expression(self):
"""Test resolving package OR expressions."""
expression = Package('python3') | Package('unknown-package')
assert expression.possible() == ['python3', 'unknown-package']
assert expression.actual() == 'python3'
expression = Package('unknown-package') | Package('python3')
assert expression.possible() == ['unknown-package', 'python3']
assert expression.actual() == 'python3'
# When both packages are available, prefer the first.
expression = Package('bash') | Package('dash')
assert expression.possible() == ['bash', 'dash']
assert expression.actual() == 'bash'
expression = Package('unknown-package') | Package(
'another-unknown-package')
assert expression.possible() == [
'unknown-package', 'another-unknown-package'
]
self.assertRaises(MissingPackageError, expression.actual)
def test_packages_init():
"""Test initialization of packages component."""
component = Packages('test-component', ['foo', 'bar'])