import collections import os.path import re from . import utils class Changelog(list): _top_rules = r""" ^ (?P \w[-+0-9a-z.]+ ) \ \( (?P [^\(\)\ \t]+ ) \) \s+ (?P [-+0-9a-zA-Z.]+ ) \;\s+urgency= (?P \w+ ) \n """ _top_re = re.compile(_top_rules, re.X) _bottom_rules = r""" ^ \ --\ (?P \S(?:\ ?\S)* ) \ \ (?P (.*) ) \n """ _bottom_re = re.compile(_bottom_rules, re.X) _ignore_re = re.compile(r'^(?: |\s*\n)') class Entry(object): __slot__ = 'distribution', 'source', 'version', 'urgency', 'maintainer', 'date' def __init__(self, **kwargs): for key, value in kwargs.items(): setattr(self, key, value) def __init__(self, dir='', version=None, file=None): if version is None: version = Version if file: self._parse(version, file) else: with open(os.path.join(dir, "debian/changelog"), encoding="UTF-8") as f: self._parse(version, f) def _parse(self, version, f): top_match = None line_no = 0 for line in f: line_no += 1 if self._ignore_re.match(line): pass elif top_match is None: top_match = self._top_re.match(line) if not top_match: raise Exception('invalid top line %d in changelog' % line_no) try: v = version(top_match.group('version')) except Exception: if not len(self): raise v = Version(top_match.group('version')) else: bottom_match = self._bottom_re.match(line) if not bottom_match: raise Exception('invalid bottom line %d in changelog' % line_no) self.append(self.Entry(distribution=top_match.group('distribution'), source=top_match.group('source'), version=v, urgency=top_match.group('urgency'), maintainer=bottom_match.group('maintainer'), date=bottom_match.group('date'))) top_match = bottom_match = None class Version(object): _version_rules = r""" ^ (?: (?P \d+ ) : )? (?P .+? ) (?: - (?P[^-]+) )? $ """ _version_re = re.compile(_version_rules, re.X) def __init__(self, version): match = self._version_re.match(version) if match is None: raise RuntimeError(u"Invalid debian version") self.epoch = None if match.group("epoch") is not None: self.epoch = int(match.group("epoch")) self.upstream = match.group("upstream") self.revision = match.group("revision") def __str__(self): return self.complete @property def complete(self): if self.epoch is not None: return u"%d:%s" % (self.epoch, self.complete_noepoch) return self.complete_noepoch @property def complete_noepoch(self): if self.revision is not None: return u"%s-%s" % (self.upstream, self.revision) return self.upstream @property def debian(self): from warnings import warn warn(u"debian argument was replaced by revision", DeprecationWarning, stacklevel=2) return self.revision class VersionLinux(Version): _version_linux_rules = r""" ^ (?P \d+\.\d+ ) (?P (?:\.\d+)? (?:-[a-z]+\d+)? ) (?: ~ (?P .+? ) )? (?: \.dfsg\. (?P \d+ ) )? - \d+ (\.\d+)? (?: (?P ~exp\d+ ) | (?P [~+]deb\d+u\d+ )? (?P ~bpo\d+\+\d+ )? | (?P [^-]+ ) ) $ """ _version_linux_re = re.compile(_version_linux_rules, re.X) def __init__(self, version): super(VersionLinux, self).__init__(version) match = self._version_linux_re.match(version) if match is None: raise RuntimeError(u"Invalid debian linux version") d = match.groupdict() self.linux_modifier = d['modifier'] self.linux_version = d['version'] if d['modifier'] is not None: assert not d['update'] self.linux_upstream = '-'.join((d['version'], d['modifier'])) else: self.linux_upstream = d['version'] self.linux_upstream_full = self.linux_upstream + d['update'] self.linux_dfsg = d['dfsg'] self.linux_revision_experimental = match.group('revision_experimental') and True self.linux_revision_security = match.group('revision_security') and True self.linux_revision_backports = match.group('revision_backports') and True self.linux_revision_other = match.group('revision_other') and True class PackageArchitecture(collections.MutableSet): __slots__ = '_data' def __init__(self, value=None): self._data = set() if value: self.extend(value) def __contains__(self, value): return self._data.__contains__(value) def __iter__(self): return self._data.__iter__() def __len__(self): return self._data.__len__() def __str__(self): return ' '.join(sorted(self)) def add(self, value): self._data.add(value) def discard(self, value): self._data.discard(value) def extend(self, value): if isinstance(value, str): for i in re.split('\s', value.strip()): self.add(i) else: raise RuntimeError class PackageDescription(object): __slots__ = "short", "long" def __init__(self, value=None): self.short = [] self.long = [] if value is not None: desc_split = value.split("\n", 1) self.append_short(desc_split[0]) if len(desc_split) == 2: self.append(desc_split[1]) def __str__(self): wrap = utils.TextWrapper(width=74, fix_sentence_endings=True).wrap short = ', '.join(self.short) long_pars = [] for i in self.long: long_pars.append(wrap(i)) long = '\n .\n '.join(['\n '.join(i) for i in long_pars]) return short + '\n ' + long if long else short def append(self, str): str = str.strip() if str: self.long.extend(str.split(u"\n.\n")) def append_short(self, str): for i in [i.strip() for i in str.split(u",")]: if i: self.short.append(i) def extend(self, desc): if isinstance(desc, PackageDescription): self.short.extend(desc.short) self.long.extend(desc.long) else: raise TypeError class PackageRelation(list): def __init__(self, value=None, override_arches=None): if value: self.extend(value, override_arches) def __str__(self): return ', '.join(str(i) for i in self) def _search_value(self, value): for i in self: if i._search_value(value): return i return None def append(self, value, override_arches=None): if isinstance(value, str): value = PackageRelationGroup(value, override_arches) elif not isinstance(value, PackageRelationGroup): raise ValueError(u"got %s" % type(value)) j = self._search_value(value) if j: j._update_arches(value) else: super(PackageRelation, self).append(value) def extend(self, value, override_arches=None): if isinstance(value, str): value = (j.strip() for j in re.split(',', value.strip())) for i in value: self.append(i, override_arches) class PackageRelationGroup(list): def __init__(self, value=None, override_arches=None): if value: self.extend(value, override_arches) def __str__(self): return ' | '.join(str(i) for i in self) def _search_value(self, value): for i, j in zip(self, value): if i.name != j.name or i.operator != j.operator or \ i.version != j.version or i.restrictions != j.restrictions: return None return self def _update_arches(self, value): for i, j in zip(self, value): if i.arches: for arch in j.arches: if arch not in i.arches: i.arches.append(arch) def append(self, value, override_arches=None): if isinstance(value, str): value = PackageRelationEntry(value, override_arches) elif not isinstance(value, PackageRelationEntry): raise ValueError super(PackageRelationGroup, self).append(value) def extend(self, value, override_arches=None): if isinstance(value, str): value = (j.strip() for j in re.split('\|', value.strip())) for i in value: self.append(i, override_arches) class PackageRelationEntry(object): __slots__ = "name", "operator", "version", "arches", "restrictions" _re = re.compile(r'^(\S+)(?: \((<<|<=|=|!=|>=|>>)\s*([^)]+)\))?(?: \[([^]]+)\])?(?: <([^>]+)>)?$') class _operator(object): OP_LT = 1 OP_LE = 2 OP_EQ = 3 OP_NE = 4 OP_GE = 5 OP_GT = 6 operators = { '<<': OP_LT, '<=': OP_LE, '=': OP_EQ, '!=': OP_NE, '>=': OP_GE, '>>': OP_GT, } operators_neg = { OP_LT: OP_GE, OP_LE: OP_GT, OP_EQ: OP_NE, OP_NE: OP_EQ, OP_GE: OP_LT, OP_GT: OP_LE, } operators_text = dict((b, a) for a, b in operators.items()) __slots__ = '_op', def __init__(self, value): self._op = self.operators[value] def __neg__(self): return self.__class__(self.operators_text[self.operators_neg[self._op]]) def __str__(self): return self.operators_text[self._op] def __eq__(self, other): return type(other) == type(self) and self._op == other._op def __init__(self, value=None, override_arches=None): if not isinstance(value, str): raise ValueError self.parse(value) if override_arches: self.arches = list(override_arches) def __str__(self): ret = [self.name] if self.operator is not None and self.version is not None: ret.extend((' (', str(self.operator), ' ', self.version, ')')) if self.arches: ret.extend((' [', ' '.join(self.arches), ']')) if self.restrictions: ret.extend((' <', ' '.join(self.restrictions), '>')) return ''.join(ret) def parse(self, value): match = self._re.match(value) if match is None: raise RuntimeError(u"Can't parse dependency %s" % value) match = match.groups() self.name = match[0] if match[1] is not None: self.operator = self._operator(match[1]) else: self.operator = None self.version = match[2] if match[3] is not None: self.arches = re.split('\s+', match[3]) else: self.arches = [] if match[4] is not None: self.restrictions = re.split('\s+', match[4]) else: self.restrictions = [] class _ControlFileDict(dict): def __setitem__(self, key, value): try: cls = self._fields[key] if not isinstance(value, cls): value = cls(value) except KeyError: pass super(_ControlFileDict, self).__setitem__(key, value) def keys(self): keys = set(super(_ControlFileDict, self).keys()) for i in self._fields.keys(): if i in self: keys.remove(i) yield i for i in sorted(list(keys)): yield i def items(self): for i in self.keys(): yield (i, self[i]) def values(self): for i in self.keys(): yield self[i] class Package(_ControlFileDict): _fields = collections.OrderedDict(( ('Package', str), ('Source', str), ('Architecture', PackageArchitecture), ('Section', str), ('Priority', str), ('Maintainer', str), ('Uploaders', str), ('Standards-Version', str), ('Build-Depends', PackageRelation), ('Build-Depends-Arch', PackageRelation), ('Build-Depends-Indep', PackageRelation), ('Provides', PackageRelation), ('Pre-Depends', PackageRelation), ('Depends', PackageRelation), ('Recommends', PackageRelation), ('Suggests', PackageRelation), ('Replaces', PackageRelation), ('Breaks', PackageRelation), ('Conflicts', PackageRelation), ('Description', PackageDescription), )) class TestsControl(_ControlFileDict): _fields = collections.OrderedDict(( ('Tests', str), ('Test-Command', str), ('Restrictions', str), ('Features', str), ('Depends', PackageRelation), ('Tests-Directory', str), ('Classes', str), ))