Commit 3f63b877 authored by Cyril Roelandt's avatar Cyril Roelandt
Browse files

Add support for package update.

parent 97083164
......@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
Based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- New option: --update/-u to update a package.
### Changed
- colorlog is no longer a required dependency. It can be installed by using the
"color" extra.
......
......@@ -75,6 +75,15 @@ You may also want to package a version other than the latest one:
The package definition will be written to the standard output.
You may update an existing package to the latest version:
$ upt package -f pypi -b guix -u requests
You may update an existing package to a specific version:
$ upt package -f pypi -b guix -u requests@2.22.0
## Under the hood
There are two kinds of places from where packages may be installed:
......
......@@ -14,7 +14,7 @@ upt list-backends
upt list-frontends
upt package [-f FRONTEND] [-b BACKEND] [-o OUTPUT] [\--debug] [\--quiet] PACKAGE[@VERSION]
upt package [-f FRONTEND] [-b BACKEND] [-o OUTPUT] [-u/\--update] [\--debug] [\--quiet] PACKAGE[@VERSION]
# DESCRIPTION
Create a package for a distribution (such as OpenBSD, Fedora, etc.) from a
......@@ -81,6 +81,10 @@ on the backend: it should do the most natural thing.
: Recursively package requirements.
-u, \--update
: Update a package.
# BACKENDS
**upt-fedora**
......@@ -138,6 +142,14 @@ on the backend: it should do the most natural thing.
: upt package requests@2.16.0
**Update an existing package**
: upt package -f pypi -b macports -u requests
**Update an existing package to a specific version**
: upt package -f pypi -b macports -u requests@2.22.0
# BUGS
Bugs can be reported to upt@framalistes.org. A web interface is also available
......
......@@ -9,6 +9,7 @@ from .upt import Backend
from .upt import Frontend
from .upt import Package
from .upt import PackageRequirement
from .upt import PackageDiff
from .exceptions import InvalidPackageNameError
from .exceptions import InvalidPackageVersionError
from .exceptions import UnhandledFrontendError
......
......@@ -82,6 +82,66 @@ class TestPackageRequirement(unittest.TestCase):
self.assertNotEqual(pkg_req_a, pkg_req_b)
class TestPackageDiff(unittest.TestCase):
def test_new_version(self):
oldpkg = upt.Package('foo', '1.0')
newpkg = upt.Package('foo', '1.1')
diff = upt.PackageDiff(oldpkg, newpkg)
self.assertEqual(diff.new_version, newpkg.version)
def test_old_version(self):
oldpkg = upt.Package('foo', '1.0')
newpkg = upt.Package('foo', '1.1')
diff = upt.PackageDiff(oldpkg, newpkg)
self.assertEqual(diff.old_version, oldpkg.version)
def test_new_requirements(self):
oldreqs = {
'run': [upt.PackageRequirement('bar')]
}
oldpkg = upt.Package('foo', '1.1', requirements=oldreqs)
newreqs = {
'run': [
upt.PackageRequirement('bar'),
upt.PackageRequirement('baz')
]
}
newpkg = upt.Package('foo', '1.1', requirements=newreqs)
diff = upt.PackageDiff(oldpkg, newpkg)
self.assertEqual(diff.new_requirements('build'), [])
self.assertEqual(diff.new_requirements('run'), newreqs['run'][1:])
self.assertEqual(diff.new_requirements('test'), [])
def test_updated_requirements(self):
oldreqs = {
'run': [upt.PackageRequirement('bar', '>=1.0')]
}
oldpkg = upt.Package('foo', '1.0', requirements=oldreqs)
newreqs = {
'run': [upt.PackageRequirement('bar', '>=1.1')]
}
newpkg = upt.Package('foo', '1.0', requirements=newreqs)
diff = upt.PackageDiff(oldpkg, newpkg)
self.assertEqual(diff.updated_requirements('build'), [])
self.assertEqual(diff.updated_requirements('run'),
[upt.PackageRequirement('bar', '>=1.1')])
self.assertEqual(diff.updated_requirements('test'), [])
def test_deleted_requirements(self):
oldreqs = {
'run': [upt.PackageRequirement('bar')]
}
oldpkg = upt.Package('foo', '1.0', requirements=oldreqs)
newreqs = {
'run': []
}
newpkg = upt.Package('foo', '1.0', requirements=newreqs)
diff = upt.PackageDiff(oldpkg, newpkg)
self.assertEqual(diff.deleted_requirements('build'), [])
self.assertEqual(diff.deleted_requirements('run'), oldreqs['run'])
self.assertEqual(diff.deleted_requirements('test'), [])
class TestUtils(unittest.TestCase):
@mock.patch('pkg_resources.iter_entry_points')
def test_get_installed_plugins(self, m_iter_entry_points):
......@@ -147,6 +207,11 @@ class TestParser(unittest.TestCase):
args = 'package -r requests'.split()
parser.parse_args(args)
def test_package_update(self):
parser = upt.upt.create_parser(['pypi'], ['guix'])
args = 'package -u requests'.split()
parser.parse_args(args)
def test_package_specific_version(self):
parser = upt.upt.create_parser(['pypi'], ['guix'])
args = 'package requests@2.16.0'.split()
......@@ -330,6 +395,87 @@ class TestCommandLine(unittest.TestCase):
upt.upt.main()
self.assertEqual(exit.exception.code, 1)
@mock.patch('upt.upt._get_installed_frontends',
return_value={'valid-frontend': mock.Mock()})
@mock.patch('upt.upt._get_installed_backends')
def test_package_update_not_supported(self, m_backends, m_frontends):
cmdline = 'package -f valid-frontend -b valid-backend -u pkgname'
sys.argv.extend(cmdline.split())
def backend_fail(*args, **kwargs):
raise NotImplementedError
fake_backend = mock.Mock()
fake_backend().update_package.side_effect = backend_fail
m_backends.return_value = {
'valid-backend': fake_backend,
}
with self.assertRaises(SystemExit) as exit:
upt.upt.main()
self.assertEqual(exit.exception.code, 1)
@mock.patch('upt.upt._get_installed_frontends',
return_value={'valid-frontend': mock.Mock()})
@mock.patch('upt.upt._get_installed_backends')
@mock.patch('sys.stdout', new_callable=StringIO)
def test_package_update_already_up_to_date(self, m_stdout,
m_backends, m_frontends):
cmdline = 'package -f valid-frontend -b valid-backend -u pkgname'
sys.argv.extend(cmdline.split())
def frontend_parse(*args, **kwargs):
return upt.upt.Package('foo', '42.0')
def backend_update(*args, **kwargs):
raise upt.upt.PackageUpToDateException('pkgname', '42.0')
fake_frontend = mock.Mock()
fake_frontend().parse.side_effect = frontend_parse
m_frontends.return_value = {
'valid-frontend': fake_frontend
}
fake_backend = mock.Mock()
fake_backend().update_package.side_effect = backend_update
m_backends.return_value = {
'valid-backend': fake_backend,
}
upt.upt.main()
self.assertIn(str(upt.upt.PackageUpToDateException('pkgname', '42.0')),
m_stdout.getvalue().split('\n')[-2])
@mock.patch('upt.upt._get_installed_frontends',
return_value={'valid-frontend': mock.Mock()})
@mock.patch('upt.upt._get_installed_backends')
@mock.patch('sys.stderr', new_callable=StringIO)
def test_package_update_unavailable(self, m_stderr, m_backends,
m_frontends):
cmdline = 'package -f valid-frontend -b valid-backend -u pkgname'
sys.argv.extend(cmdline.split())
def frontend_parse(*args, **kwargs):
raise upt.exceptions.InvalidPackageNameError('valid-frontend',
'pkgname')
def backend_update(*args, **kwargs):
raise upt.upt.PackageUpToDateException('pkgname', '42.0')
fake_frontend = mock.Mock()
fake_frontend().parse.side_effect = frontend_parse
m_frontends.return_value = {
'valid-frontend': fake_frontend
}
fake_backend = mock.Mock()
fake_backend().update_package.side_effect = backend_update
m_backends.return_value = {
'valid-backend': fake_backend,
}
with self.assertRaises(SystemExit):
upt.upt.main()
expected = upt.exceptions.InvalidPackageNameError('valid-frontend',
'pkgname')
self.assertIn(str(expected), m_stderr.getvalue().split('\n')[-2])
class TestArchive(unittest.TestCase):
def setUp(self):
......@@ -426,6 +572,16 @@ class TestBackend(unittest.TestCase):
def setUp(self):
self.backend = upt.upt.Backend()
def test_current_version(self):
with mock.patch('builtins.input', return_value='1.2'):
frontend = mock.Mock()
self.assertEqual(self.backend.current_version(frontend, 'pkg'),
'1.2')
def test_update_package(self):
with self.assertRaises(NotImplementedError):
self.backend.update_package(mock.Mock())
def test_needs_requirement_not_packaged(self):
self.backend.package_versions = lambda x: []
requirement = upt.PackageRequirement('foo')
......@@ -481,3 +637,34 @@ class TestBackend(unittest.TestCase):
with mock.patch('upt.upt.Backend.needs_requirement_interactive'):
self.backend.needs_requirement(requirement, None)
self.backend.needs_requirement_interactive.assert_called()
class TestUpdate(unittest.TestCase):
def test_update_already_up_to_date(self):
frontend = mock.Mock()
frontend.parse = mock.Mock(return_value=upt.Package('foo', '1.0'))
# The package is already up to date
backend = mock.Mock()
backend.current_version = mock.Mock(return_value='1.0')
with self.assertRaises(upt.upt.PackageUpToDateException) as e:
upt.upt.update('foo', '1.0', frontend, backend, None)
self.assertEqual(e.exception.pkgname, 'foo')
self.assertEqual(e.exception.version, '1.0')
self.assertEqual(str(e.exception), "foo is already up-to-date (1.0)")
# The package is not up to date
backend = mock.Mock()
backend.current_version = mock.Mock(return_value='0.9')
try:
upt.upt.update('foo', '1.0', frontend, backend, None)
except upt.exceptions.PackageUpToDateException as e:
self.fail(f'{e.__class__.__name__} should not have been raised')
def test_update_not_implemented(self):
frontend = mock.Mock()
frontend.parse = mock.Mock(return_value=upt.Package('foo', '1.0'))
backend = mock.Mock()
backend.current_version = mock.Mock(return_value='0.9')
backend.update_package = mock.Mock(side_effect=NotImplementedError)
with self.assertRaises(SystemExit):
upt.upt.update('foo', '1.0', frontend, backend, None)
......@@ -18,6 +18,18 @@ import upt.log
class Backend(object):
def current_version(self, frontend, pkgname, output=None):
"""Return the currently packaged version of a package.
This method must return the currently packaged version of PKGNAME,
packaged using the given FRONTEND.
TODO: output
"""
return input(f'Current version of {pkgname} ({frontend.name})?')
def update_package(self, pdiff, output=None):
raise NotImplementedError()
def needs_requirement_interactive(self, req, phase):
while True:
ret = input(f'Should we package {req} (phase {phase})? [y/n] ')
......@@ -61,6 +73,15 @@ class Frontend(object):
pass
class PackageUpToDateException(Exception):
def __init__(self, pkgname, version):
self.pkgname = pkgname
self.version = version
def __str__(self):
return f'{self.pkgname} is already up-to-date ({self.version})'
class ArchiveType(Enum):
SOURCE_TARGZ = 1
RUBYGEM = 2
......@@ -239,6 +260,47 @@ class Package(object):
os.remove(archive._filepath)
class PackageDiff(object):
def __init__(self, old, new):
self.old = old
self.new = new
@property
def old_version(self):
return self.old.version
@property
def new_version(self):
return self.new.version
def new_requirements(self, phase):
old_names = [req.name for req in self.old.requirements.get(phase, [])]
new_names = [req.name for req in self.new.requirements.get(phase, [])]
added_names = set(new_names) - set(old_names)
return [
req for req in self.new.requirements.get(phase, [])
if req.name in added_names
]
def updated_requirements(self, phase):
old_names = [req.name for req in self.old.requirements.get(phase, [])]
new_names = [req.name for req in self.new.requirements.get(phase, [])]
updated_names = set(old_names) & set(new_names)
return [
req for req in self.new.requirements.get(phase, [])
if req.name in updated_names
]
def deleted_requirements(self, phase):
old_names = [req.name for req in self.old.requirements.get(phase, [])]
new_names = [req.name for req in self.new.requirements.get(phase, [])]
deleted_names = set(old_names) - set(new_names)
return [
req for req in self.old.requirements.get(phase, [])
if req.name in deleted_names
]
def create_parser(frontends, backends):
parser = argparse.ArgumentParser(prog='upt')
......@@ -281,6 +343,8 @@ def create_parser(frontends, backends):
help='Recursively package requirements'),
parser_package.add_argument('-c', '--color', action='store_true',
help='Show colored logging output'),
parser_package.add_argument('-u', '--update', action='store_true',
help='Update package'),
parser_package.add_argument('package', help='Name of the package. '
'Use <package>@<version> to '
'package a specific version.')
......@@ -348,6 +412,34 @@ def _extract_name_version_from_package(package):
return name, version or None
def update(pkg_name, version, frontend, backend, output):
logger = logging.getLogger('upt')
upt.log.logger_set_formatter(logger, 'Backend')
old_version = backend.current_version(frontend, pkg_name, output)
# Parse both the old and the new version of the package in order to create
# a "diff".
upt.log.logger_set_formatter(logger, 'Frontend')
old_pkg = frontend.parse(pkg_name, old_version)
old_pkg.frontend = frontend.name
new_pkg = frontend.parse(pkg_name, version)
new_pkg.frontend = frontend.name
diff = PackageDiff(old_pkg, new_pkg)
if diff.new_version == old_version:
raise PackageUpToDateException(pkg_name, old_version)
# Let the backend do its magic.
upt.log.logger_set_formatter(logger, 'Backend')
logger.info(f'Updating {pkg_name} from {old_version} to {new_pkg.version}')
try:
backend.update_package(diff, output=output)
except NotImplementedError:
logger.error(f'The "{backend.name}" backend does not implement the '
'update feature yet.')
sys.exit(1)
def main():
frontends = _get_installed_frontends()
backends = _get_installed_backends()
......@@ -389,4 +481,18 @@ def main():
backend = backends[args.backend]()
name, version = _extract_name_version_from_package(args.package)
upt.log.create_logger(args.log_level, args.color)
package(name, version, frontend, backend, args.output, args.recursive)
if args.update:
try:
update(name, version, frontend, backend, args.output)
except PackageUpToDateException as e:
logger = logging.getLogger('upt')
logger.info(e)
except (upt.exceptions.InvalidPackageNameError,
upt.exceptions.InvalidPackageVersionError) as e:
logger = logging.getLogger('upt')
upt.log.logger_set_formatter(logger, 'Frontend')
logger.error(e)
sys.exit(1)
else:
package(name, version, frontend, backend, args.output,
args.recursive)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment