diff --git a/britney2/console.py b/britney2/console.py index d916038..abafb1d 100644 --- a/britney2/console.py +++ b/britney2/console.py @@ -1,19 +1,111 @@ import code +import collections class SubInterpreterExit(SystemExit): pass +def fuzzy_match(string, collection, thing_being_matched): + if len(collection) < 1: + raise ValueError("No valid candidates to match!?") + matches = collection + if string: + if string in collection: + return string + matches = [x for x in collection if string in x] + if not matches: + raise ValueError("No matches for %s (%s), possible valid values are: %s" % ( + string, thing_being_matched, sorted(collection))) + if string is not None and len(matches) > 1: + matches = [x for x in collection if x.starts(string)] + if len(matches) > 1: + if string is None: + raise ValueError("More than one item and no search criteria for %s, possible valid values are: %s" % ( + thing_being_matched, sorted(matches))) + raise ValueError("Too many matches for %s (%s), matching candidates are: %s" % ( + string, thing_being_matched, sorted(matches))) + return next(iter(matches)) + + +class ConsoleUtils(object): + + def __init__(self, britney): + self._britney = britney + self._pkgid_cache = collections.defaultdict(list) + self._build_cache() + + def _build_cache(self): + for pkg_id in self._britney.all_binaries: + self._pkgid_cache[pkg_id.package_name].append(pkg_id) + + def pkg_id(self, package_name, package_version=None, package_architecture=None, *, + fuzzy_match_version=True, fuzzy_match_arch=True): + """Look up BinaryPackageID + + Example: + pkg_id("lintian", "2.5", "amd64") -> BinaryPackageID("lintian", "2.5.10", "amd64") + (Note that difference in version is intentional and a part of the fuzzy matching) + + :param package_name: Name of the package (e.g. "lintian") + :param package_version: Version of the package (e.g. "2.5") + :param package_architecture: Architecture of the package (note arch:all packages are always split + in to a per-architecture package) + :param fuzzy_match_version: If true, any version string is accepted as long as it uniquely identified + the package. + :param fuzzy_match_arch: If true, any architecture string is accepted as long as it uniquely identified + the package. + :return: Exactly one BinaryPackageId + """ + if package_name not in self._pkgid_cache: + raise ValueError("Unknown package name: %s" % package_name) + + package_candidates = self._pkgid_cache[package_name] + unique_versions = {x.version for x in package_candidates} + + real_version = package_version + if package_version is None or package_version not in unique_versions: + if not fuzzy_match_version: + if unique_versions is None: + raise ValueError("unique_versions cannot be None when fuzzy_match_version is False") + raise ValueError("Unknown version %s, valid options are: %s" % ( + package_version, sorted(unique_versions))) + real_version = fuzzy_match(package_architecture, unique_versions, 'package_version') + + unique_architectures = {x.architecture for x in package_candidates if x.version == real_version} + + real_architecture = package_architecture + if package_architecture is None or package_architecture not in unique_architectures: + if not fuzzy_match_arch: + if package_architecture is None: + raise ValueError("package_architecture cannot be None when fuzzy_match_arch is False") + raise ValueError("Unknown architecture %s" % package_architecture) + real_architecture = fuzzy_match(package_architecture, unique_architectures, 'package_architecture') + + match = [x for x in package_candidates if x.version == real_version and x.architecture == real_architecture] + if len(match) != 1: + if not match: + raise ValueError("Package %s, version %s (%s) is not available on architecture %s (%s)" % + package_name, package_version, real_version, package_architecture, real_architecture) + raise ValueError("The terms %s, %s (%s) and %s (%s) did not result in a unique package!? All matches: %s" % + package_name, package_version, real_version, package_architecture, real_architecture, + sorted(match)) + + return match[0] + + def console_quit(): raise SubInterpreterExit() def run_python_console(britney_obj): + console_utils = ConsoleUtils(britney_obj) console_locals = { 'britney': britney_obj, '__name__': '__console__', '__doc__': None, + 'all_bin_pkg_ids': britney_obj.all_binaries.keys(), + 'pkg_id': console_utils.pkg_id, 'quit': console_quit, 'exit': console_quit, } @@ -23,6 +115,8 @@ Interactive python (REPL) shell in britney. Locals available * britney: Instance of the Britney object. + * all_bin_pkg_ids: Set of all BinaryPackageIDs + * pkg_id: Lookup a BinaryPackageID * quit()/exit(): leave this REPL console. """ try: