From 7334e41998fb99dfd10dcf9fd977967ceb0f79f2 Mon Sep 17 00:00:00 2001 From: Mykyta Holubakha Date: Mon, 19 Jun 2017 04:24:06 +0300 Subject: Numerous improvements and fixes Documented most of the functions and classes. Added an option to fetch a package into a specified directory. Added a merge_into method to the Package class, which would merge it into a directory, and refactored repo::merge_package to use that. Extended the package class to store category, version and slot metadata. Added tests of the portage package source module. --- pomu/cli.py | 15 ++++++++++++--- pomu/package.py | 48 +++++++++++++++++++++++++++++++++++++++--------- pomu/repo/repo.py | 7 ++++--- pomu/source/manager.py | 18 ++++++++++++++++++ pomu/source/portage.py | 9 ++++++++- pomu/util/cache.py | 3 +++ pomu/util/fs.py | 1 + pomu/util/str.py | 6 ++++++ tests/test_dispatch.py | 7 ++++++- 9 files changed, 97 insertions(+), 17 deletions(-) diff --git a/pomu/cli.py b/pomu/cli.py index c17840e..4b09000 100644 --- a/pomu/cli.py +++ b/pomu/cli.py @@ -1,6 +1,8 @@ """pomu command line interface""" import click +from os import path + from pomu.repo.init import init_plain_repo, init_portage_repo from pomu.repo.repo import portage_repo_path, portage_repos, pomu_active_repo from pomu.source import dispatcher @@ -90,10 +92,17 @@ def uninstall(uri, package): @main.command() @click.argument('package', required=True) -@needs_repo -def fetch(package): +@click.option('--into', default=None, + help='Specify fetch destination') +def fetch(package, into): pkg = dispatcher.get_package(package).expect() - print('Fetched package', pkg.name, 'at', pkg.root) + print('Fetched package', pkg, 'at', pkg.root) + print('Contents:') + for f in pkg.files: + print(' ', path.join(*f)) + if into: + pkg.merge_into(into).expect() + print('Copied to', into, 'successfully') def main_(): try: diff --git a/pomu/package.py b/pomu/package.py index 470d76b..7b7ec55 100644 --- a/pomu/package.py +++ b/pomu/package.py @@ -4,12 +4,14 @@ A package can be installed into a repository. A package is supposed to be created by a package source from a set of files. """ -from os import path, walk +from os import path, walk, makedirs +from shutil import copy2 from pomu.util.fs import strip_prefix +from pomu.util.result import Result class Package(): - def __init__(self, name, root, d_path=None, files=None): + def __init__(self, name, root, category=None, version=None, slot='0', d_path=None, files=None): """ Parameters: name - name of the package @@ -17,9 +19,13 @@ class Package(): d_path - a subdirectory of the root path, which would be sourced recursively. could be a relative or an absolute path files - a set of files to build a package from + category, version, slot - self-descriptive """ self.name = name self.root = root + self.category = category + self.version = version + self.slot = slot self.files = [] if d_path is None and files is None: self.d_path = None @@ -29,20 +35,44 @@ class Package(): self.read_path(path.join(self.root, self.d_path)) elif d_path is None: for f in files: - self.files.append(self.strip_root(f)) + self.files.append(path.split(self.strip_root(f))) else: raise ValueError('You should specify either d_path, or files') def strip_root(self, d_path): - # the path should be either relative, or a child of root - if d_path.startswith('/'): - if path.commonprefix(d_path, self.root) != self.root: - raise ValueError('Path should be a subdirectory of root') - return strip_prefix(strip_prefix(d_path, self.root), '/') - return d_path + """Strip the root component of a path""" + # the path should be either relative, or a child of root + if d_path.startswith('/'): + if path.commonprefix(d_path, self.root) != self.root: + raise ValueError('Path should be a subdirectory of root') + return strip_prefix(strip_prefix(d_path, self.root), '/') + return d_path def read_path(self, d_path): + """Recursively add files from a subtree""" for wd, dirs, files in walk(d_path): wd = self.strip_root(wd) self.files.extend([(wd, f) for f in files]) + def merge_into(self, dst): + """Merges contents of the package into a specified directory""" + for wd, f in self.files: + dest = path.join(dst, wd) + try: + makedirs(dest, exist_ok=True) + copy2(path.join(self.root, wd, f), dest) + except PermissionError: + return Result.Err('You do not have enough permissions') + return Result.Ok() + + + def __str__(self): + s = '' + if self.category: + s = self.category + '/' + s += self.name + if self.version: + s += '-' + self.version + if self.slot != '0': + s += self.slot + return s diff --git a/pomu/repo/repo.py b/pomu/repo/repo.py index 076b2b7..6434ed3 100644 --- a/pomu/repo/repo.py +++ b/pomu/repo/repo.py @@ -25,11 +25,10 @@ class Repository(): return path.join(self.root, 'metadata/pomu') def merge(self, package): + """Merge a package into the repository""" r = self.repo + package.merge(self.root).expect('Failed to merge package') for wd, f in package.files: - dst = path.join(self.root, wd) - makedirs(dst) - copy2(path.join(package.root, wd, f), dst) r.index.add(path.join(dst, f)) with open(path.join(self.pomu_dir, package.name), 'w') as f: f.write('{}/{}'.format(wd, f)) @@ -38,6 +37,7 @@ class Repository(): return Result.Ok('Merged package ' + package.name + ' successfully') def unmerge(self, package): + """Remove a package (by contents) from the repository""" r = self.repo for wd, f in package.files: dst = path.join(self.root, wd) @@ -52,6 +52,7 @@ class Repository(): return Result.Ok('Removed package ' + package.name + ' successfully') def remove_package(self, name): + """Remove a package (by name) from the repository""" r = self.repo pf = path.join(self.pomu_dir, name) if not path.isfile(pf): diff --git a/pomu/source/manager.py b/pomu/source/manager.py index f36eb90..9d3e2de 100644 --- a/pomu/source/manager.py +++ b/pomu/source/manager.py @@ -37,12 +37,22 @@ class PackageDispatcher(): self.handlers = [] def source(self, cls): + """ + A decorator to mark package source modules + It would register all the methods of the class marked by @handler + with the dispatcher. + """ for m, obj in inspect.getmembers(cls): if isinstance(obj, self.handler._handler): self.register_package_handler(cls, obj.handler, obj.priority) return cls class handler(): + """ + A decorator to denote package source module handler, which + should attempt to parse a package descriptor. If it succeeds, + the result would be passed to the module for further processing. + """ class _handler(): def __init__(self, handler): self.handler = handler @@ -58,6 +68,10 @@ class PackageDispatcher(): return x def register_package_handler(self, source, handler, priority): + """ + Register a package handler for a specified source. + Handlers with lower priority get called first. + """ i = 0 for i in range(len(self.handlers)): if self.handlers[0][0] > priority: @@ -65,12 +79,14 @@ class PackageDispatcher(): self.handlers.insert(i, (priority, source, handler)) def get_package_source(self, uri): + """Get a source which accepts the package""" for priority, source, handler in self.handlers: if handler(uri).is_ok(): return source return None def get_package(self, uri): + """Fetch a package specified by the descriptor""" for priority, source, handler in self.handlers: res = handler(uri) if res.is_ok(): @@ -78,9 +94,11 @@ class PackageDispatcher(): return Result.Err('No handler found for package ' + uri) def install_package(self, uri): + """Install a package specified by the descriptor""" pkg = self.get_package(uri).unwrap() return pomu_active_repo().merge(pkg) def uninstall_package(self, uri): + """Uninstall a package specified by the descriptor""" pkg = self.get_package(uri).unwrap() return pomu_active_repo().unmerge(pkg) diff --git a/pomu/source/portage.py b/pomu/source/portage.py index aaf5c1c..6e58873 100644 --- a/pomu/source/portage.py +++ b/pomu/source/portage.py @@ -26,6 +26,7 @@ class PortagePackage(): def fetch(self): return Package(self.name, portage_repo_path(self.repo), + category=self.category, version=self.version, slot=self.slot, files=[path.join(self.category, self.name, 'metadata.xml'), path.join(self.category, self.name, self.name + '-' + self.version + '.ebuild')]) @@ -38,6 +39,7 @@ misc_dirs = ['profiles', 'licenses', 'eclass', 'metadata', 'distfiles', 'package @dispatcher.source class PortageSource(): + """The source module responsible for fetching portage packages""" @dispatcher.handler(priority=5) def parse_spec(uri, repo=None): # dev-libs/openssl-0.9.8z_p8-r100:0.9.8::gentoo @@ -77,6 +79,10 @@ class PortageSource(): def sanity_check(repo, category, name, vernum, suff, rev, slot): + """ + Checks whether a package descriptor is valid and corresponds + to a package in a configured portage repository + """ if not name: return False if repo and repo not in list(portage_repos()): @@ -95,10 +101,11 @@ def sanity_check(repo, category, name, vernum, suff, rev, slot): def ver_str(vernum, suff, rev): + """Gets the string representation of the version""" return vernum + (suff if suff else '') + (rev if rev else '') def best_ver(repo, category, name, ver=None): - """Gets the best (newest) version of a package in the repo""" + """Gets the best (newest) version of a package in the repo""" ebuilds = [category + '/' + name + x[len(name):-7] for x in os.listdir(path.join(portage_repo_path(repo), category, name)) if x.endswith('.ebuild')] diff --git a/pomu/util/cache.py b/pomu/util/cache.py index b1d09b2..93502c5 100644 --- a/pomu/util/cache.py +++ b/pomu/util/cache.py @@ -2,6 +2,9 @@ Caches the return value of a function -> Result, regardless of input params """ class cached(): + """ + A decorator to make the function cache its return value, regardless of input + """ def __init__(self, fun): self.fun = fun def __call__(self, *args): diff --git a/pomu/util/fs.py b/pomu/util/fs.py index 3e69e2e..5d25ec5 100644 --- a/pomu/util/fs.py +++ b/pomu/util/fs.py @@ -11,5 +11,6 @@ def strip_prefix(string, prefix): return string def remove_file(repo, dst): + """Removes a file from a repository""" repo.index.remove(dst) os.remove(dst) diff --git a/pomu/util/str.py b/pomu/util/str.py index 419425b..96a7c81 100644 --- a/pomu/util/str.py +++ b/pomu/util/str.py @@ -1,4 +1,10 @@ +"""String processing utilities""" def pivot(string, idx, keep_pivot=True): + """ + A function to split a string in two, pivoting at string[idx]. + If keep_pivot is set, the pivot character is included in the second string. + Alternatively, it is omitted. + """ if keep_pivot: return (string[:idx], string[idx:]) else: diff --git a/tests/test_dispatch.py b/tests/test_dispatch.py index 7ccf830..5ccba6d 100644 --- a/tests/test_dispatch.py +++ b/tests/test_dispatch.py @@ -12,7 +12,7 @@ from pomu.util.result import Result @dispatcher.source class DummySource(): - @dispatcher.handler + @dispatcher.handler(priority=3) @classmethod def parse(cls, uri): if uri.startswith('/'): @@ -33,6 +33,7 @@ class DispatcherTests(unittest.TestCase): def testDispatch(self): self.assertEqual(dispatcher.get_package_source('/test').unwrap(), 'test') self.assertTrue(dispatcher.get_package_source('test').is_err()) + self.assertTrue(dispatcher.get_package('sys-apps/portage').is_ok()) def testFetch(self): pkg = dispatcher.get_package('/test').unwrap() @@ -64,6 +65,10 @@ class InstallTests(unittest.TestCase): pkg = Package('test', self.source_path) self.repo.merge(pkg).expect() + def testPortagePkg(self): + pkg = dispatcher.get_package('sys-apps/portage').expect() + self.repo.merge(pkg).expect() + def testPkgUnmerge(self): pkg = Package('test', self.source_path) self.repo.merge(pkg).expect() -- cgit v1.2.3-65-gdbad