Commit 9781b0ba authored by Karan Sheth's avatar Karan Sheth Committed by Cyril Roelandt
Browse files

Add support for recursive packaging.

parent 9e25dabe
......@@ -6,6 +6,7 @@ Based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- A man page is now available
- New option: -r/--recursive, to recursively package all requirements
## [0.9] - 2019-06-19
### Added
......
......@@ -65,6 +65,10 @@ Or, using shorter options:
$ upt package -f pypi -b guix requests
You may also want to recursively package all of its requirements:
$ upt package -f pypi -b guix -r requests
The package definition will be written to the standard output.
......@@ -316,6 +320,19 @@ class MyBackend(upt.Backend):
# "upt --list-backends".
name = 'mybackend'
# Should you wish to use the "--recursive" flag, in order to recursively
# package all dependencies of your package, you must define the following
# method, which returns a list of all versions of a given package currently
# packaged in your backend.
def package_versions(self, package_name):
"""Return a list of available versions of PACKAGE_NAME"""
raise NotImplementedError
# Alternatively, you could redefine the following method to have complete
# control over the decision to package (or not package) the given
# requirement:
# def needs_requirement(self, req, phase):
def create_package(self, upt_pkg, output=None):
# Parameters:
# - upt_pkg: an instance of upt_pkg. See the "Adding a frontend"
......
......@@ -21,6 +21,7 @@ classifiers =
[options]
packages = find:
install_requires =
packaging
spdx_lookup
[options.entry_points]
......
......@@ -129,6 +129,11 @@ class TestParser(unittest.TestCase):
args = 'package -f pypi requests'.split()
parser.parse_args(args)
def test_package_recursive(self):
parser = upt.upt.create_parser(['pypi'], ['guix'])
args = 'package -r requests'.split()
parser.parse_args(args)
def test_package_missing_frontend(self):
parser = upt.upt.create_parser(['cpan', 'pypi'], ['guix', 'nix'])
args = 'package -b guix requests'.split()
......@@ -326,3 +331,70 @@ class TestArchive(unittest.TestCase):
self.assertEqual(archive.sha256, 'sha256')
self.assertEqual(archive.sha256_base64, 'sha256_base64')
compute_checksum_fn.assert_not_called()
class TestRecursion(unittest.TestCase):
def setUp(self):
self.package = upt.Package('foo', '42')
self.frontend = mock.Mock()
self.frontend.parse.return_value = self.package
self.backend = mock.Mock()
def test_no_dependencies(self):
upt.upt.package('foo', self.frontend, self.backend, None, True, [])
self.backend.create_package.assert_called()
self.backend.needs_requirement.assert_not_called()
def test_dependencies(self):
self.package.requirements = {
'run': [upt.PackageRequirement('bar')]
}
upt.upt.package('foo', self.frontend, self.backend, None, True, [])
self.backend.create_package.assert_called()
self.backend.needs_requirement.assert_called()
def test_circular_dependency(self):
self.package.requirements = {
'run': [upt.PackageRequirement('foo')]
}
upt.upt.package('foo', self.frontend, self.backend, None, True, [])
self.backend.create_package.assert_called_once()
self.backend.needs_requirement.assert_not_called()
def test_recursion_disabled(self):
self.package.requirements = {
'run': [upt.PackageRequirement('bar')]
}
upt.upt.package('foo', self.frontend, self.backend, None, False, [])
self.backend.create_package.assert_called_once()
self.backend.needs_requirement.assert_not_called()
class TestBackend(unittest.TestCase):
def setUp(self):
self.backend = upt.upt.Backend()
@mock.patch.object(upt.upt.Backend, 'package_versions', return_value=[])
def test_needs_requirement_not_packaged(self, mock_pkg):
requirement = upt.PackageRequirement('foo')
self.assertTrue(self.backend.needs_requirement(requirement, None))
@mock.patch.object(upt.upt.Backend, 'package_versions', return_value=['1'])
def test_needs_requirement_incompatible_version(self, mock_pkg):
requirement = upt.PackageRequirement('foo', '>2')
self.assertTrue(self.backend.needs_requirement(requirement, None))
@mock.patch.object(upt.upt.Backend, 'package_versions', return_value=['1'])
def test_needs_requirement_compatible_version(self, mock_pkg):
requirement = upt.PackageRequirement('foo', '>0.5')
self.assertFalse(self.backend.needs_requirement(requirement, None))
@mock.patch.object(upt.upt.Backend, 'package_versions', return_value=['1'])
def test_needs_requirement_no_specifier(self, mock_pkg):
requirement = upt.PackageRequirement('foo')
self.assertFalse(self.backend.needs_requirement(requirement, None))
def test_package_versions_not_overriden(self):
requirement = upt.PackageRequirement('foo')
with self.assertRaises(NotImplementedError):
self.backend.needs_requirement(requirement, None)
......@@ -9,6 +9,7 @@ import urllib.request
import sys
import tempfile
from packaging.specifiers import SpecifierSet
import pkg_resources
import upt.checksum
......@@ -17,7 +18,33 @@ import upt.log
class Backend(object):
pass
def package_versions(self, package_name):
"""Return a list of available versions of PACKAGE_NAME"""
raise NotImplementedError
def needs_requirement(self, req, phase):
logger = logging.getLogger('upt')
versions = self.package_versions(req.name)
if not versions:
logger.info(f'Dependency {req}: currently not packaged. '
f'Packaging it.')
return True
if not req.specifier:
logger.info(f'Dependency {req}: currently packaged, no specific '
f'version is required. Not packaging it.')
return False
s = SpecifierSet(req.specifier)
compatible_versions = list(s.filter(versions))
if compatible_versions:
logger.info(f'Dependency {req}: at least one compatible version '
f'found in {compatible_versions}. Not packaging it.')
return False
else:
logger.info(f'Dependency {req}: no compatible version found '
f'in {versions}. Packaging it.')
return True
class Frontend(object):
......@@ -230,6 +257,8 @@ def create_parser(frontends, backends):
logger_group.add_argument('-q', '--quiet', action='store_const',
const=logging.CRITICAL+1, dest='log_level',
help='Suppress all logging output')
parser_package.add_argument('-r', '--recursive', action='store_true',
help='Recursively package requirements'),
parser_package.add_argument('package', help='Name of the package')
return parser
......@@ -252,6 +281,39 @@ def _get_installed_backends():
return _get_installed_plugins('upt.backends')
def package(pkg_name, frontend, backend, output, recursive, packaged=[]):
logger = logging.getLogger('upt')
try:
upt_pkg = None
upt.log.logger_set_formatter(logger, 'Frontend')
upt_pkg = frontend.parse(pkg_name)
upt_pkg.frontend = frontend.name
upt.log.logger_set_formatter(logger, 'Backend')
backend.create_package(upt_pkg, output=output)
packaged.append(pkg_name)
if recursive:
for phase, requirements in upt_pkg.requirements.items():
for requirement in requirements:
# Let's not repackage what we have already packaged.
if requirement.name in packaged:
continue
if backend.needs_requirement(requirement, phase):
package(requirement.name, frontend, backend,
output, recursive, packaged)
except upt.exceptions.UnhandledFrontendError as e:
print(e, file=sys.stderr)
sys.exit(1)
except upt.exceptions.InvalidPackageNameError as e:
print(e, file=sys.stderr)
sys.exit(1)
finally:
if upt_pkg is not None:
# We always want to clean up after ourselves.
upt_pkg._clean()
def main():
frontends = _get_installed_frontends()
backends = _get_installed_backends()
......@@ -291,22 +353,5 @@ def main():
# values for us.
frontend = frontends[args.frontend]()
backend = backends[args.backend]()
logger = upt.log.create_logger(args.log_level)
try:
upt_pkg = None
upt.log.logger_set_formatter(logger, 'Frontend')
upt_pkg = frontend.parse(args.package)
upt_pkg.frontend = args.frontend
upt.log.logger_set_formatter(logger, 'Backend')
backend.create_package(upt_pkg, output=args.output)
except upt.exceptions.UnhandledFrontendError as e:
print(e, file=sys.stderr)
sys.exit(1)
except upt.exceptions.InvalidPackageNameError as e:
print(e, file=sys.stderr)
sys.exit(1)
finally:
if upt_pkg is not None:
# We always want to clean up after ourselves.
upt_pkg._clean()
upt.log.create_logger(args.log_level)
package(args.package, 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