[Blends-commit] [Git][blends-team/blends][experimental] Create and use python3-blends package

Ole Streicher gitlab at salsa.debian.org
Tue Apr 10 13:12:24 UTC 2018


Ole Streicher pushed to branch experimental at Debian Blends Team / blends


Commits:
fb15ffe0 by Ole Streicher at 2018-04-10T15:10:50+02:00
Create and use python3-blends package

- - - - -


10 changed files:

- + blends.py
- debian/control
- + debian/python3-blends.doc-base
- + debian/python3-blends.docs
- + debian/python3-blends.pyinstall
- debian/rules
- devtools/blend-gen-control
- + sphinxdoc/Makefile
- + sphinxdoc/conf.py
- + sphinxdoc/index.rst


Changes:

=====================================
blends.py
=====================================
--- /dev/null
+++ b/blends.py
@@ -0,0 +1,793 @@
+'''A module to handle Debian Pure Blends tasks, modelled after apt.package.
+
+The examples use the following sample tasks file:
+
+>>> sample_task = """Format: https://blends.debian.org/blends/1.1
+... Task: Education
+... Install: true
+... Description: Educational astronomy applications
+...  Various applications that can be used to teach astronomy.
+...  .
+...  This is however incomplete.
+...
+... Recommends: celestia-gnome | celestia-glut, starplot
+...
+... Recommends: gravit
+... WNPP: 743379
+... Homepage: http://gravit.slowchop.com/
+... Pkg-Description: Visually stunning gravity simulator
+...  Gravit is a free, visually stunning gravity simulator.
+...  .
+...  You can spend endless time experimenting with various
+...  configurations of simulated universes.
+... Why: Useful package
+... Remark: Entered Debian in 2014
+...
+... Suggests: sunclock, xtide
+... """
+>>> with open('education', 'w') as fp:
+...     nbytes = fp.write(sample_task)
+'''
+import io
+import os
+import itertools
+import re
+import shutil
+from debian.deb822 import Deb822
+
+
+class Blend:
+    '''Representation of a Debian Pure Blend.
+    '''
+    def __init__(self, basedir='.'):
+        with open(os.path.join(basedir, 'debian', 'control.stub'),
+                  encoding="UTF-8") as fp:
+            self.control_stub = Deb822List(Deb822.iter_paragraphs(fp))
+
+        self.name = self.control_stub[0]['Source']
+        '''Full (package) name of the blend (``debian-astro``)'''
+
+        self.short_name = self.name.split('-', 1)[-1]
+        '''Short name of the blend (``astro``)'''
+
+        self.title = 'Debian ' + self.short_name.capitalize()
+        '''Blends title (``Debian Astro``)'''
+
+        base_deps = ["${misc:Depends}"]
+
+        self.prefix = self.short_name
+        '''Prefix for tasks (``astro``)'''
+
+        for pkg in self.control_stub[1:]:
+            p = pkg['Package'].split('-', 1)
+            if len(p) > 1 and p[1] == 'tasks':
+                self.prefix = p[0]
+                base_deps.append("{Package} (= ${{source:Version}})"
+                                 .format(**pkg))
+                break
+
+        try:
+            with open(os.path.join(basedir, 'config', 'control'),
+                      encoding="UTF-8") as fp:
+                self.control_stub.append(Deb822(fp))
+                base_deps.append("{}-config (= ${{source:Version}})"
+                                 .format(self.prefix))
+        except IOError:
+            pass
+
+        self.tasks = []
+        '''``Task`` list'''
+        for name in sorted(filter(lambda n: n[-1] != '~',
+                                  os.listdir(os.path.join(basedir, 'tasks')))):
+            with open(os.path.join(basedir, 'tasks', name),
+                      encoding="UTF-8") as fp:
+                task = Task(self, name, fp, base_deps=base_deps)
+            self.tasks.append(task)
+
+    def update(self, cache):
+        '''Update from cache
+
+        :param cache: ``apt.Cache`` like object
+
+        This adds the available versions to all dependencies. It
+        updates descriptions, summaries etc. available to all
+        BaseDependencies in all tasks.
+
+        Instead of using ``update()``, also the ``+=`` operator can be used.
+
+        '''
+        for task in self.tasks:
+            task += cache
+
+    def __iadd__(self, cache):
+        self.update(cache)
+        return self
+
+    @property
+    def all(self):
+        '''All Base Dependencies of this task
+        '''
+        return list(itertools.chain(*(t.all for t in self.tasks)))
+
+    def fix_dependencies(self):
+        '''Fix the dependencies according to available packages
+
+        This lowers all unavailable ``recommended`` dependencies to
+        ``suggested``.
+        '''
+        missing = []
+        for task in self.tasks:
+            missing += task.fix_dependencies()
+        return missing
+
+    def gen_control(self):
+        '''Return the task as list of ``Deb822`` objects suitable for
+        ``debian/control``
+        '''
+        tasks = list(filter(lambda task: task.is_metapackage, self.tasks))
+
+        # Create the special 'all' task recommending all tasks that
+        # shall be installed by default
+        all_task = Task(
+            self, "all",
+            '''Description: Default selection of tasks for {task.title}
+ This package is part of the {task.title} Pure Blend and installs all
+ tasks for a default installation of this blend.'''.format(task=self),
+            base_deps=['${misc:Depends}'])
+        for task in tasks:
+            if task.install:
+                all_task.recommends.append(Dependency("Recommends",
+                                                      task.package_name))
+            else:
+                all_task.suggests.append(Dependency("Suggests",
+                                                    task.package_name))
+        if len(all_task.recommends) > 0:
+            tasks.insert(0, all_task)
+
+        return Deb822List(self.control_stub
+                          + [task.gen_control() for task in tasks])
+
+    def gen_task_desc(self, udeb=False):
+        '''Return the task as list of ``Deb822`` objects suitable for
+        ``blends-task.desc``
+        '''
+        tasks = list(filter(lambda task: task.is_metapackage and task.is_leaf,
+                            self.tasks))
+
+        header = [Deb822({
+            'Task': self.name,
+            'Relevance':  '7',
+            'Section': self.name,
+            'Description': '{} Pure Blend\n .'.format(self.title),
+        })] if not udeb else []
+        return Deb822List(header + [task.gen_task_desc(udeb)
+                                    for task in tasks])
+
+
+class Task:
+    '''Representation of a Blends task. Modelled after apt.package.Version.
+
+    The Version class contains all information related to a
+    specific package version of a blends task.
+
+    :param blend: ``Blend`` object, or Blend name
+
+    :param name: Name of the task
+
+    :param sequence: ``str`` or ``file`` containing the ``Deb822``
+                     description of the task
+
+    :param base_deps: List of dependencies to add to the task (``str``)
+
+    When the header does not contain a line
+
+    ``Format: https://blends.debian.org/blends/1.1``
+
+    then the ``Depends`` priorities will be lowered to ``Recommends``
+    when read.
+
+    Example:
+
+    >>> with open('education') as fp:
+    ...     task = Task('debian-astro', 'education', fp)
+    >>> print(task.name)
+    education
+    >>> print(task.package_name)
+    astro-education
+    >>> print(task.description)
+    Various applications that can be used to teach astronomy.
+    <BLANKLINE>
+    This is however incomplete.
+    >>> print(task.summary)
+    Educational astronomy applications
+    >>> print(task.section)
+    metapackages
+    >>> print(task.architecture)
+    all
+    >>> for p in task.all:
+    ...     print(p.name)
+    celestia-gnome
+    celestia-glut
+    starplot
+    gravit
+    sunclock
+    xtide
+
+    '''
+    def __init__(self, blend, name, sequence, base_deps=None):
+        if isinstance(blend, str):
+            self.blend = blend
+            '''Blend name'''
+
+            self.prefix = blend[len('debian-'):] \
+                if blend.startswith('debian-') else blend
+            '''Metapackage prefix'''
+        else:
+            self.blend = blend.name
+            self.prefix = blend.prefix
+
+        self.name = name
+        '''Task name'''
+
+        self.content = Deb822List(Deb822.iter_paragraphs(sequence))
+        '''Deb822List content of the task'''
+
+        self.header = self.content[0]
+        '''Deb822 header'''
+
+        self.base_deps = base_deps or []
+        '''Base dependencies'''
+
+        # Check for the format version, and upgrade if not actual
+        self.format_upgraded = False
+        '''``True`` if the format was upgraded from an older version'''
+
+        if 'Format' in self.header:
+            self.format_version = self.header['Format'].strip() \
+                .rsplit('/', 1)[-1]
+        else:
+            self.format_version = '1'
+        if self.format_version.split('.') < ['1', '1']:
+            self.content = Task.upgrade_from_1_0(self.content)
+            self.format_upgraded = True
+
+        # Create package dependencies
+        dep_types = ["Depends", "Recommends", "Suggests"]
+        dep_attrs = ["dependencies", "recommends", "suggests"]
+        for dep_type, dep_attr in zip(dep_types, dep_attrs):
+            setattr(self, dep_attr, list(itertools.chain(
+                *(list(Dependency(dep_type, s.strip(), par)
+                       for s in par.get(dep_type, '').split(",") if s)
+                  for par in self.content[1:]))))
+        self.enhances = [
+            Dependency('Enhances', s.strip(), self.header)
+            for s in self.header.get('Enhances', '').split(",") if s
+        ]
+
+    @property
+    def install(self):
+        '''``True`` if the task is installed as a default package
+        '''
+        return self.header.get("Install") == "true"
+
+    @property
+    def index(self):
+        '''``True`` if the task shall appear in the tasks index in the
+        web senitel
+        '''
+        return self.header.get("index", "true") == "true"
+
+    @property
+    def is_leaf(self):
+        return self.header.get("leaf", "true") == "true"
+
+    @property
+    def is_metapackage(self):
+        '''``True`` if the tasks has a Debian metapackage
+        '''
+        return self.header.get("metapackage", "true") == "true"
+
+    @property
+    def package_name(self):
+        return '{task.prefix}-{task.name}'.format(task=self)
+
+    @property
+    def description(self):
+        '''Return the formatted long description.
+        '''
+        desc = self.header.get("Pkg-Description",
+                               self.header.get("Description"))
+        if not desc:
+            return None
+        else:
+            return "\n".join(line[1:] if line != ' .' else ''
+                             for line in desc.split("\n")[1:])
+
+    @property
+    def summary(self):
+        '''Return the short description (one line summary).
+        '''
+        desc = self.header.get("Pkg-Description",
+                               self.header.get("Description"))
+        return desc.split('\n')[0] if desc else None
+
+    @property
+    def section(self):
+        '''Return the section of the package.
+        '''
+        return 'metapackages'
+
+    @property
+    def architecture(self):
+        '''Return the architecture of the package version.
+        '''
+        return self.header.get('Architecture', 'all')
+
+    @property
+    def tests(self):
+        '''Return all tests for this task when included in tasksel
+        '''
+        tests = dict((key.split('-', 1)[1], value)
+                     for key, value in self.header.items()
+                     if key.startswith('Test-'))
+        if self.install:
+            tests['new-install'] = 'mark show'
+        return tests
+
+    @property
+    def all(self):
+        '''All Base Dependencies of this task
+        '''
+        return list(itertools.chain(
+            *itertools.chain(self.dependencies,
+                             self.recommends,
+                             self.suggests)))
+
+    def gen_control(self):
+        '''Return the task as ``Deb822`` object suitable for ``debian/control``
+
+        >>> with open('education') as fp:
+        ...     task = Task('debian-astro', 'education', fp)
+        >>> print(task.gen_control().dump())
+        Package: astro-education
+        Section: metapackages
+        Architecture: all
+        Recommends: celestia-gnome | celestia-glut,
+                    gravit,
+                    starplot
+        Suggests: sunclock,
+                  xtide
+        Description: Educational astronomy applications
+         Various applications that can be used to teach astronomy.
+         .
+         This is however incomplete.
+        <BLANKLINE>
+        '''
+        d = Deb822()
+        d['Package'] = self.package_name
+        d['Section'] = self.section
+        d['Architecture'] = self.architecture
+        if self.dependencies or self.base_deps:
+            d['Depends'] = ",\n         ".join(sorted(
+                self.base_deps
+                + list(set(d.rawstr for d in self.dependencies))
+            ))
+        if self.recommends:
+            d['Recommends'] = ",\n            ".join(sorted(
+                set(d.rawstr for d in self.recommends)
+            ))
+        if self.suggests:
+            d['Suggests'] = ",\n          ".join(sorted(
+                set(d.rawstr for d in self.suggests)
+            ))
+        d['Description'] = self.summary + '\n ' + \
+            "\n ".join(self.description.replace("\n\n", "\n.\n").split("\n"))
+        return d
+
+    def gen_task_desc(self, udeb=False):
+        '''Return the task as ``Deb822`` object suitable for ``blends-task.desc``.
+
+        :parameter udeb: if ``True``, generate ```blends-task.desc``
+                         suitable for udebs
+
+        >>> with open('education') as fp:
+        ...     task = Task('debian-astro', 'education', fp)
+        >>> print(task.gen_task_desc().dump())
+        Task: astro-education
+        Parent: debian-astro
+        Section: debian-astro
+        Description: Educational astronomy applications
+         Various applications that can be used to teach astronomy.
+         .
+         This is however incomplete.
+        Test-new-install: mark show
+        Key:
+         astro-education
+        <BLANKLINE>
+        >>> print(task.gen_task_desc(udeb=True).dump())
+        Task: astro-education
+        Section: debian-astro
+        Description: Educational astronomy applications
+         Various applications that can be used to teach astronomy.
+         .
+         This is however incomplete.
+        Relevance: 10
+        Test-new-install: mark show
+        Key:
+         astro-education
+        Packages: list
+         celestia-glut
+         celestia-gnome
+         gravit
+         starplot
+        <BLANKLINE>
+
+        '''
+        d = Deb822()
+        d['Task'] = self.package_name
+        if not udeb:
+            d['Parent'] = self.blend
+        d['Section'] = self.blend
+        d['Description'] = self.summary + '\n ' + \
+            "\n ".join(self.description.replace("\n\n", "\n.\n").split("\n"))
+        if udeb:
+            d['Relevance'] = '10'
+        if self.enhances:
+            d['Enhances'] = ', '.join(sorted(d.name for d in itertools.chain(
+                *self.enhances)))
+        for key, value in self.tests.items():
+            d['Test-' + key] = value
+        d['Key'] = '\n {}'.format(self.package_name)
+        if udeb:
+            d['Packages'] = 'list\n ' + \
+                          '\n '.join(sorted(d.name for d in itertools.chain(
+                              *(self.recommends + self.dependencies))))
+        return d
+
+    def update(self, cache):
+        '''Update from cache
+
+        This adds the available versions to all dependencies. It updates
+        descriptions, summaries etc. available to all BaseDependencies.
+
+        :param cache: ``apt.Cache`` like object
+
+        Instead of using ``update()``, also the ``+=`` operator can be used:
+
+        >>> import apt
+        >>> with open('education') as fp:
+        ...     task = Task('debian-astro', 'education', fp)
+        >>> dep = task.recommends[1][0]
+        >>> print(dep.name + ": ", dep.summary)
+        starplot:  None
+        >>> task += apt.Cache()
+        >>> print(dep.name + ": ", dep.summary)
+        starplot:  3-dimensional perspective star map viewer
+        '''
+        for dep in self.all:
+            pkg = cache.get(dep.name)
+            if pkg is not None:
+                dep.target_versions += pkg.versions
+            if hasattr(cache, 'get_providing_packages'):
+                for pkg in cache.get_providing_packages(dep.name):
+                    dep.target_versions += pkg.versions
+
+    def __iadd__(self, cache):
+        self.update(cache)
+        return self
+
+    def fix_dependencies(self):
+        '''Fix the dependencies according to available packages
+
+        This lowers all unavailable ``recommended`` dependencies to
+        ``suggested``.
+
+        >>> import apt
+        >>> with open('education') as fp:
+        ...     task = Task('debian-astro', 'education', fp)
+        >>> for dep in task.recommends:
+        ...     print(dep.rawstr)
+        celestia-gnome | celestia-glut
+        starplot
+        gravit
+        >>> for dep in task.suggests:
+        ...     print(dep.rawstr)
+        sunclock
+        xtide
+        >>> task += apt.Cache()
+        >>> missing = task.fix_dependencies()
+        >>> for dep in task.recommends:
+        ...     print(dep.rawstr)
+        starplot
+        gravit
+        >>> for dep in task.suggests:
+        ...     print(dep.rawstr)
+        sunclock
+        xtide
+        celestia-gnome | celestia-glut
+        '''
+        missing = list()
+        for recommended in self.recommends[:]:
+            suggested = Dependency("Suggests")
+            for dep in recommended[:]:
+                if len(dep.target_versions) == 0:
+                    recommended.remove(dep)
+                    suggested.append(dep)
+                    missing.append(dep)
+            if len(recommended) == 0:
+                self.recommends.remove(recommended)
+            if len(suggested) > 0:
+                self.suggests.append(suggested)
+        return missing
+
+    @staticmethod
+    def upgrade_from_1_0(content):
+        header = [("Format", "https://blends.debian.org/blends/1.1")]
+        header += list(filter(lambda x: x[0] != "Format", content[0].items()))
+        res = [dict(header)]
+        for p in content[1:]:
+            q = []
+            for key, value in p.items():
+                if key == 'Depends' and 'Recommends' not in p:
+                    key = 'Recommends'
+                # Remove backslashes, which are not DEB822 compliant
+                value = re.sub(r'\s*\\', '', value)
+                q.append((key, value))
+            res.append(dict(q))
+        return Deb822List(res)
+
+
+class Dependency(list):
+    '''Represent an Or-group of dependencies.
+
+    Example:
+
+    >>> with open('education') as fp:
+    ...     task = Task('debian-astro', 'education', fp)
+    >>> dep = task.recommends[0]
+    >>> print(dep.rawstr)
+    celestia-gnome | celestia-glut
+    '''
+
+    def __init__(self, rawtype, s=None, content=None):
+        super(Dependency, self).__init__(BaseDependency(bs.strip(), content)
+                                         for bs in (s.split("|") if s else []))
+        self.rawtype = rawtype
+        '''The type of the dependencies in the Or-group'''
+
+    @property
+    def or_dependencies(self):
+        return self
+
+    @property
+    def rawstr(self):
+        '''String represenation of the Or-group of dependencies.
+
+        Returns the string representation of the Or-group of
+        dependencies as it would be written in the ``debian/control``
+        file.  The string representation does not include the type of
+        the Or-group of dependencies.
+        '''
+        return ' | '.join(bd.rawstr for bd in self)
+
+    @property
+    def target_versions(self):
+        '''A list of all Version objects which satisfy this Or-group of deps.
+        '''
+        return list(itertools.chain(bd.target_versions for bd in self))
+
+
+class BaseDependency:
+    '''A single dependency.
+
+    Example:
+
+    >>> with open('education') as fp:
+    ...     task = Task('debian-astro', 'education', fp)
+    >>> dep = task.recommends[2][0]
+    >>> print(dep.rawstr)
+    gravit
+    >>> print(dep.wnpp)
+    743379
+    >>> print(dep.homepage)
+    http://gravit.slowchop.com/
+    >>> print(dep.description)
+    Gravit is a free, visually stunning gravity simulator.
+    <BLANKLINE>
+    You can spend endless time experimenting with various
+    configurations of simulated universes.
+    >>> print(dep.summary)
+    Visually stunning gravity simulator
+    '''
+
+    def __init__(self, s, content=None):
+        r = re.compile(r'([a-z0-9][a-z0-9+-\.]+)')
+        m = r.match(s)
+        if m is None or m.string != s:
+            raise ValueError('"{}" is not a valid package name'.format(s))
+        self.name = s
+        self.content = content or dict()
+        self.target_versions = []
+
+    def _get_from_target_versions(self, key):
+        for v in self.target_versions:
+            if v.package.name == self.name:
+                return getattr(v, key)
+
+    @property
+    def rawstr(self):
+        '''String represenation of the dependency.
+
+        Returns the string representation of the dependency as it
+        would be written in the ``debian/control`` file.  The string
+        representation does not include the type of the dependency.
+        '''
+        return self.name
+
+    @property
+    def wnpp(self):
+        '''The WNPP bug number, if available, or None
+        '''
+        return self.content.get("WNPP")
+
+    @property
+    def homepage(self):
+        '''Return the homepage for the package.
+        '''
+        return self._get_from_target_versions("homepage") or \
+            self.content.get("Homepage")
+
+    @property
+    def description(self):
+        '''Return the formatted long description.
+        '''
+        desc = self._get_from_target_versions("description")
+        if desc is not None:
+            return desc
+        desc = self.content.get("Pkg-Description",
+                                self.content.get("Description"))
+        if desc:
+            return "\n".join(line[1:] if line != ' .' else ''
+                             for line in desc.split("\n")[1:])
+
+    @property
+    def summary(self):
+        '''Return the short description (one line summary).
+        '''
+        summary = self._get_from_target_versions("summary")
+        if summary:
+            return summary
+
+        desc = self.content.get("Pkg-Description",
+                                self.content.get("Description"))
+        if desc:
+            return desc.split('\n')[0]
+
+    @property
+    def why(self):
+        return self.content.get("Why")
+
+    @property
+    def remark(self):
+        return self.content.get("Remark")
+
+
+class Deb822List(list):
+    '''A list of ``Deb822`` paragraphs
+    '''
+    def __init__(self, paragraphs):
+        list.__init__(self, (p if isinstance(p, Deb822) else Deb822(p)
+                             for p in paragraphs))
+
+    def dump(self, fd=None, encoding=None, text_mode=False):
+        '''Dump the the contents in the original format
+
+        If ``fd`` is ``None``, returns a ``str`` object. Otherwise,
+        ``fd`` is assumed to be a ``file``-like object, and this
+        method will write the data to it instead of returning an
+        ``str`` object.
+
+        If ``fd`` is not ``None`` and ``text_mode`` is ``False``, the
+        data will be encoded to a byte string before writing to the
+        file.  The encoding used is chosen via the encoding parameter;
+        None means to use the encoding the object was initialized with
+        (utf-8 by default).  This will raise ``UnicodeEncodeError`` if
+        the encoding can't support all the characters in the
+        ``Deb822Dict`` values.
+
+        '''
+        if fd is None:
+            fd = io.StringIO()
+            return_string = True
+        else:
+            return_string = False
+
+        for p in self:
+            p.dump(fd, encoding, text_mode)
+            fd.write("\n")
+
+        if return_string:
+            return fd.getvalue()
+
+
+def aptcache(release=None, srcdirs=['/etc/blends']):
+    '''Open and update a (temporary) apt cache for the specified distribution.
+
+    :param release: Distribution name
+
+    :param srcdirs: List of directories to search for
+        ``sources.list.<<release>>``
+
+    If the distribution is not given, use the system's cache without update.
+    '''
+    import tempfile
+    import apt
+
+    if release is None:
+        return apt.Cache()
+    rootdir = tempfile.mkdtemp()
+    try:
+        os.makedirs(os.path.join(rootdir, 'etc', 'apt'))
+        shutil.copytree('/etc/apt/trusted.gpg.d',
+                        os.path.join(rootdir, 'etc', 'apt', 'trusted.gpg.d'))
+        for src_dir in srcdirs:
+            sources_list = os.path.join(src_dir,
+                                        'sources.list.{}'.format(release))
+            if os.path.exists(sources_list):
+                shutil.copy(sources_list,
+                            os.path.join(rootdir, 'etc/apt/sources.list'))
+                break
+        else:
+            raise OSError("sources.list not found in " + str(srcdirs))
+        cache = apt.Cache(rootdir=rootdir, memonly=True)
+        cache.update()
+        cache.open()
+    finally:
+        shutil.rmtree(rootdir)
+    return cache
+
+
+def uddcache(release, packages, **db_args):
+    '''Create a ``dict`` from UDD that is roughly modelled after ``apt.Cache``.
+
+    The ``dict`` just resolves the version number and archs for the packages.
+    For performance reasons, an initial package list needs to be given.
+
+    :param release: Distribution name
+    :param packages: Initial package list
+    :param db_args: UDD connection parameters
+
+    ``Provided`` dependencies are integrated in the returned ``dict``.
+    '''
+    import collections
+    import psycopg2
+
+    pkgtuple = tuple(set(p.name for p in packages))
+
+    Package = collections.namedtuple('Package',
+                                     ('name', 'versions',))
+    Version = collections.namedtuple('Version',
+                                     ('package', 'architecture', 'version'))
+
+    stmt = '''SELECT packages.package,
+                     packages.provides,
+                     packages.architecture,
+                     packages.version
+              FROM packages, releases
+              WHERE packages.release=releases.release
+                    AND (releases.release=%s  OR releases.role=%s)
+                    AND (packages.package IN %s OR packages.provides in %s);'''
+
+    with psycopg2.connect(**db_args) as conn:
+        cursor = conn.cursor()
+        cursor.execute(stmt, (release, release, pkgtuple, pkgtuple))
+
+        cache = dict()
+        for package, provides, arch, version in cursor:
+            p = cache.setdefault(package, Package(package, []))
+            p.versions.append(Version(p, arch, version))
+            if provides:
+                pp = cache.setdefault(provides, Package(package, []))
+                pp.versions.append(Version(p, arch, version))
+
+        return cache


=====================================
debian/control
=====================================
--- a/debian/control
+++ b/debian/control
@@ -8,11 +8,15 @@ Uploaders: Petter Reinholdtsen <pere at debian.org>,
 Section: devel
 Priority: optional
 Build-Depends: debhelper (>= 10)
-Build-Depends-Indep: xmlto,
-                     dblatex,
-                     w3m,
+Build-Depends-Indep: dblatex,
                      dh-python,
-                     python3
+                     python3-all,
+                     python3-apt,
+                     python3-debian,
+                     python3-pytest,
+                     python3-sphinx,
+                     w3m,
+                     xmlto
 Standards-Version: 4.1.3
 Vcs-Browser: https://salsa.debian.org/blends-team/blends
 Vcs-Git: https://salsa.debian.org/blends-team/blends.git
@@ -20,10 +24,12 @@ Vcs-Git: https://salsa.debian.org/blends-team/blends.git
 Package: blends-dev
 Architecture: all
 Depends: debconf,
+         debhelper (>= 9),
          make | build-essential,
-         debhelper (>= 9),python3,
-         ${misc:Depends},
-         ${python3:Depends}
+         python3,
+         python3-apt,
+         python3-blends,
+         ${misc:Depends}
 Suggests: blends-doc
 Recommends: python3-psycopg2
 Description: Debian Pure Blends common files for developing metapackages
@@ -36,8 +42,8 @@ Package: blends-common
 Architecture: all
 Section: misc
 Depends: adduser,
-         menu,
          debconf,
+         menu,
          ${misc:Depends}
 Suggests: blends-doc
 Description: Debian Pure Blends common package
@@ -52,8 +58,8 @@ Package: blends-doc
 Architecture: all
 Section: doc
 Depends: ${misc:Depends}
-Suggests: www-browser,
-          postscript-viewer
+Suggests: postscript-viewer,
+          www-browser
 Description: Debian Pure Blends documentation
  This paper is intended to people who are interested in the philosophy
  of Debian Pure Blends and the technique which is used to
@@ -70,8 +76,8 @@ Package: blends-tasks
 Architecture: all
 Section: misc
 Priority: important
-Depends: ${misc:Depends},
-         tasksel
+Depends: tasksel,
+         ${misc:Depends}
 Description: Debian Pure Blends tasks for new installations
  This package installs a choice of a default installation for each
  Debian Pure Blend when run from the Debian installer. The
@@ -80,3 +86,18 @@ Description: Debian Pure Blends tasks for new installations
  .
  The package is intended to be installed in the base system. Later
  (un)installation is harmless, but has no effect.
+
+Package: python3-blends
+Architecture: all
+Section: python
+Depends: python3-debian,
+         ${misc:Depends},
+         ${python3:Depends},
+         ${sphinxdoc:Depends}
+Suggests: python3-psycopg2,
+          python3-apt
+Description: Python 3 module for Debian Pure Blends support
+ This package installs a module to handle Debian Pure Blends tasks.
+ It reads the tasks description from unpacked Blend Metapackages
+ sources. It is directly possible to create the debian/control file
+ and the tasks definition files from it.


=====================================
debian/python3-blends.doc-base
=====================================
--- /dev/null
+++ b/debian/python3-blends.doc-base
@@ -0,0 +1,10 @@
+Document: python-blends
+Title: Debian Pure Blends Python package
+Author: Ole Streicher
+Abstract: This document describes the Debian Pure Blends
+ Python package API.
+Section: Debian
+
+Format: HTML
+Index: /usr/share/doc/python3-blends/index.html
+Files: /usr/share/doc/python3-blends/*.html


=====================================
debian/python3-blends.docs
=====================================
--- /dev/null
+++ b/debian/python3-blends.docs
@@ -0,0 +1 @@
+sphinxdoc/_build/html/*


=====================================
debian/python3-blends.pyinstall
=====================================
--- /dev/null
+++ b/debian/python3-blends.pyinstall
@@ -0,0 +1 @@
+blends.py


=====================================
debian/rules
=====================================
--- a/debian/rules
+++ b/debian/rules
@@ -8,12 +8,19 @@ include /usr/share/dpkg/default.mk
 DISTDIR := $(DEB_SOURCE)-$(DEB_VERSION)
 
 %:
-	dh $@ --with python3
+	dh $@ --with python3,sphinxdoc
 
 override_dh_auto_build:
-	cd doc; $(MAKE) html; $(MAKE) txt; $(MAKE) pdf
+	$(MAKE) -C doc html txt pdf
+	$(MAKE) -C sphinxdoc html
 	dh_auto_build
 
+override_dh_auto_test:
+ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
+	pytest-3 --doctest-modules
+	rm -f education # clean up test remnants
+endif
+
 override_dh_installchangelogs:
 	for pkgnews in $(DEB_SOURCE)-common $(DEB_SOURCE)-dev ; do \
 	    cp -a debian/$$pkgnews.NEWS.Debian debian/$$pkgnews/usr/share/doc/$$pkgnews/NEWS.Debian ; \
@@ -21,7 +28,8 @@ override_dh_installchangelogs:
 	dh_installchangelogs
 
 override_dh_auto_clean:
-	cd doc; $(MAKE) clean
+	$(MAKE) -C doc clean
+	$(MAKE) -C sphinxdoc clean
 	dh_auto_clean
 
 override_dh_compress:


=====================================
devtools/blend-gen-control
=====================================
--- a/devtools/blend-gen-control
+++ b/devtools/blend-gen-control
@@ -1,915 +1,132 @@
 #!/usr/bin/python3
 
-'''A module to handle Debian Pure Blends tasks, modelled after apt.package.
-
-The examples use the following sample tasks file:
-
->>> sample_task = """Format: https://blends.debian.org/blends/1.1
-... Task: Education
-... Install: true
-... Description: Educational astronomy applications
-...  Various applications that can be used to teach astronomy.
-...  .
-...  This is however incomplete.
-...
-... Recommends: celestia-gnome | celestia-glut, starplot
-...
-... Recommends: gravit
-... WNPP: 743379
-... Homepage: http://gravit.slowchop.com/
-... Pkg-Description: Visually stunning gravity simulator
-...  Gravit is a free, visually stunning gravity simulator.
-...  .
-...  You can spend endless time experimenting with various
-...  configurations of simulated universes.
-... Why: Useful package
-... Remark: Entered Debian in 2014
-...
-... Suggests: sunclock, xtide
-... """
->>> with open('education', 'w') as fp:
-...     nbytes = fp.write(sample_task)
-'''
-import io
 import os
+import sys
+import argparse
 import itertools
-import re
-import shutil
-from debian.deb822 import Deb822
-
-class Blend:
-    '''Representation of a Debian Pure Blend.
-    '''
-    def __init__(self, basedir = '.'):
-        with open(os.path.join(basedir, 'debian', 'control.stub'), encoding="UTF-8") as fp:
-            self.control_stub = Deb822List(Deb822.iter_paragraphs(fp))
-
-        self.name = self.control_stub[0]['Source']
-        '''Full (package) name of the blend (``debian-astro``)'''
-
-        self.short_name = self.name.split('-', 1)[-1]
-        '''Short name of the blend (``astro``)'''
-
-        self.title = 'Debian ' + self.short_name.capitalize()
-        '''Blends title (``Debian Astro``)'''
-
-        base_deps = [ "${misc:Depends}", ]
-
-        self.prefix = self.short_name
-        '''Prefix for tasks (``astro``)'''
-
-        for pkg in self.control_stub[1:]:
-            p = pkg['Package'].split('-', 1)
-            if len(p) > 1 and p[1] == 'tasks':
-                self.prefix = p[0]
-                base_deps.append("{Package} (= ${{source:Version}})".format(**pkg))
-                break
-
-        try:
-            with open(os.path.join(basedir, 'config', 'control'), encoding="UTF-8") as fp:
-                self.control_stub.append(Deb822(fp))
-                base_deps.append("{}-config (= ${{source:Version}})"
-                                 .format(self.prefix))
-        except IOError:
-            pass
-
-        self.tasks = []
-        '''``Task`` list'''
-        for name in sorted(filter(lambda n: n[-1] != '~',
-                                  os.listdir(os.path.join(basedir, 'tasks')))):
-            with open(os.path.join(basedir, 'tasks', name), encoding="UTF-8") as fp:
-                task = Task(self,  name, fp, base_deps = base_deps)
-            self.tasks.append(task)
-
-    def update(self, cache):
-        '''Update from cache
-
-        :param cache: ``apt.Cache`` like object
-
-        This adds the available versions to all dependencies. It
-        updates descriptions, summaries etc. available to all
-        BaseDependencies in all tasks.
-
-        Instead of using ``update()``, also the ``+=`` operator can be used.
-
-        '''
-        for task in self.tasks:
-            task += cache
-
-    def __iadd__(self, cache):
-        self.update(cache)
-        return self
-
-    @property
-    def all(self):
-        '''All Base Dependencies of this task
-        '''
-        return list(itertools.chain(*(t.all for t in self.tasks)))
-
-    def fix_dependencies(self):
-        '''Fix the dependencies according to available packages
-
-        This lowers all unavailable ``recommended`` dependencies to
-        ``suggested``.
-        '''
-        missing = []
-        for task in self.tasks:
-            missing += task.fix_dependencies()
-        return missing
-
-    def gen_control(self):
-        '''Return the task as list of ``Deb822`` objects suitable for
-        ``debian/control``
-        '''
-        tasks = list(filter(lambda task: task.is_metapackage, self.tasks))
-
-        # Create the special 'all' task recommending all tasks that
-        # shall be installed by default
-        all_task = Task(
-            self, "all",
-            '''Description: Default selection of tasks for {task.title}
- This package is part of the {task.title} Pure Blend and installs all
- tasks for a default installation of this blend.'''.format(task=self),
-            base_deps = ['${misc:Depends}'])
-        for task in tasks:
-            if task.install:
-                all_task.recommends.append(Dependency("Recommends",
-                                                      task.package_name))
-            else:
-                all_task.suggests.append(Dependency("Suggests",
-                                                    task.package_name))
-        if len(all_task.recommends) > 0:
-            tasks.insert(0, all_task)
-
-        return Deb822List(self.control_stub
-                          + [ task.gen_control() for task in tasks ])
-
-    def gen_task_desc(self, udeb = False):
-        '''Return the task as list of ``Deb822`` objects suitable for
-        ``blends-task.desc``
-        '''
-        tasks = list(filter(lambda task: task.is_metapackage and task.is_leaf,
-                            self.tasks))
-
-        header = [ Deb822({
-            'Task': self.name,
-            'Relevance':  '7',
-            'Section': self.name,
-            'Description': '{} Pure Blend\n .'.format(self.title),
-        }) ] if not udeb else []
-        return Deb822List(header + [ task.gen_task_desc(udeb) for task in tasks ])
-
-
-class Task:
-    '''Representation of a Blends task. Modelled after apt.package.Version.
-
-    The Version class contains all information related to a
-    specific package version of a blends task.
-
-    :param blend: ``Blend`` object, or Blend name
-
-    :param name: Name of the task
-
-    :param sequence: ``str`` or ``file`` containing the ``Deb822``
-                     description of the task
-
-    :param base_deps: List of dependencies to add to the task (``str``)
-
-    When the header does not contain a line
-
-    ``Format: https://blends.debian.org/blends/1.1``
-
-    then the ``Depends`` priorities will be lowered to ``Recommends``
-    when read.
-
-    Example:
-
-    >>> with open('education') as fp:
-    ...     task = Task('debian-astro', 'education', fp)
-    >>> print(task.name)
-    education
-    >>> print(task.package_name)
-    astro-education
-    >>> print(task.description)
-    Various applications that can be used to teach astronomy.
-    <BLANKLINE>
-    This is however incomplete.
-    >>> print(task.summary)
-    Educational astronomy applications
-    >>> print(task.section)
-    metapackages
-    >>> print(task.architecture)
-    all
-    >>> for p in task.all:
-    ...     print(p.name)
-    celestia-gnome
-    celestia-glut
-    starplot
-    gravit
-    sunclock
-    xtide
-
-    '''
-    def __init__(self, blend, name, sequence, base_deps = None):
-        if isinstance(blend, str):
-            self.blend = blend
-            '''Blend name'''
-
-            self.prefix = blend[len('debian-'):] if blend.startswith('debian-') else blend
-            '''Metapackage prefix'''
-        else:
-            self.blend = blend.name
-            self.prefix = blend.prefix
-
-        self.name = name
-        '''Task name'''
-
-        self.content = Deb822List(Deb822.iter_paragraphs(sequence))
-        '''Deb822List content of the task'''
-
-        self.header = self.content[0]
-        '''Deb822 header'''
-
-        self.base_deps = base_deps or []
-        '''Base dependencies'''
-
-        # Check for the format version, and upgrade if not actual
-        self.format_upgraded = False
-        '''``True`` if the format was upgraded from an older version'''
-
-        if 'Format' in self.header:
-            self.format_version = self.header['Format'].strip().rsplit('/', 1)[-1]
-        else:
-            self.format_version = '1'
-        if self.format_version.split('.') < ['1', '1']:
-            self.content = Task.upgrade_from_1_0(self.content)
-            self.format_upgraded = True
-
-        # Create package dependencies
-        dep_types = ["Depends", "Recommends", "Suggests"]
-        dep_attrs = [ "dependencies", "recommends", "suggests" ]
-        for dep_type, dep_attr in zip(dep_types, dep_attrs):
-            setattr(self, dep_attr, list(itertools.chain(
-                *(list(Dependency(dep_type, s.strip(), par)
-                       for s in par.get(dep_type, '').split(",") if s)
-                  for par in self.content[1:]))))
-        self.enhances = [
-            Dependency('Enhances', s.strip(), self.header)
-            for s in self.header.get('Enhances', '').split(",") if s
-        ]
-
-    @property
-    def install(self):
-        '''``True`` if the task is installed as a default package
-        '''
-        return self.header.get("Install") == "true"
-
-    @property
-    def index(self):
-        '''``True`` if the task shall appear in the tasks index in the
-        web senitel
-        '''
-        return self.header.get("index", "true") == "true"
-
-    @property
-    def is_leaf(self):
-        return self.header.get("leaf", "true") == "true"
-
-    @property
-    def is_metapackage(self):
-        '''``True`` if the tasks has a Debian metapackage
-        '''
-        return self.header.get("metapackage", "true") == "true"
-
-    @property
-    def package_name(self):
-        return '{task.prefix}-{task.name}'.format(task=self)
-
-    @property
-    def description(self):
-        '''Return the formatted long description.
-        '''
-        desc = self.header.get("Pkg-Description",
-                                self.header.get("Description"))
-        if not desc:
-            return None
-        else:
-            return "\n".join(l[1:] if l != ' .' else ''
-                             for l in desc.split("\n")[1:])
-
-    @property
-    def summary(self):
-        '''Return the short description (one line summary).
-        '''
-        desc = self.header.get("Pkg-Description",
-                                self.header.get("Description"))
-        return desc.split('\n')[0] if desc else None
-
-    @property
-    def section(self):
-        '''Return the section of the package.
-        '''
-        return 'metapackages'
-
-    @property
-    def architecture(self):
-        '''Return the architecture of the package version.
-        '''
-        return self.header.get('Architecture', 'all')
-
-    @property
-    def tests(self):
-        '''Return all tests for this task when included in tasksel
-        '''
-        tests = dict((key.split('-', 1)[1], value)
-                     for key, value in self.header.items()
-                     if key.startswith('Test-'))
-        if self.install:
-            tests['new-install'] = 'mark show'
-        return tests
-
-    @property
-    def all(self):
-        '''All Base Dependencies of this task
-        '''
-        return list(itertools.chain(
-            *itertools.chain(self.dependencies,
-                             self.recommends,
-                             self.suggests)))
-
-    def gen_control(self):
-        '''Return the task as ``Deb822`` object suitable for ``debian/control``
-
-        >>> with open('education') as fp:
-        ...     task = Task('debian-astro', 'education', fp)
-        >>> print(task.gen_control().dump())
-        Package: astro-education
-        Section: metapackages
-        Architecture: all
-        Recommends: celestia-gnome | celestia-glut,
-                    gravit,
-                    starplot
-        Suggests: sunclock,
-                  xtide
-        Description: Educational astronomy applications
-         Various applications that can be used to teach astronomy.
-         .
-         This is however incomplete.
-        <BLANKLINE>
-        '''
-        d = Deb822()
-        d['Package'] = self.package_name
-        d['Section'] = self.section
-        d['Architecture'] = self.architecture
-        if self.dependencies or self.base_deps:
-            d['Depends'] = ",\n         ".join(sorted(
-                self.base_deps
-                + list(set(d.rawstr for d in self.dependencies))
-            ))
-        if self.recommends:
-            d['Recommends'] = ",\n            ".join(sorted(
-                set(d.rawstr for d in self.recommends)
-            ))
-        if self.suggests:
-            d['Suggests'] = ",\n          ".join(sorted(
-                set(d.rawstr for d in self.suggests)
-            ))
-        d['Description'] = self.summary + '\n ' + \
-            "\n ".join(self.description.replace("\n\n", "\n.\n").split("\n"))
-        return d
-
-    def gen_task_desc(self, udeb = False):
-        '''Return the task as ``Deb822`` object suitable for ``blends-task.desc``.
-
-        :parameter udeb: if ``True``, generate ```blends-task.desc``
-                         suitable for udebs
-
-        >>> with open('education') as fp:
-        ...     task = Task('debian-astro', 'education', fp)
-        >>> print(task.gen_task_desc().dump())
-        Task: astro-education
-        Parent: debian-astro
-        Section: debian-astro
-        Description: Educational astronomy applications
-         Various applications that can be used to teach astronomy.
-         .
-         This is however incomplete.
-        Test-new-install: mark show
-        Key:
-         astro-education
-        <BLANKLINE>
-        >>> print(task.gen_task_desc(udeb=True).dump())
-        Task: astro-education
-        Section: debian-astro
-        Description: Educational astronomy applications
-         Various applications that can be used to teach astronomy.
-         .
-         This is however incomplete.
-        Relevance: 10
-        Test-new-install: mark show
-        Key:
-         astro-education
-        Packages: list
-         celestia-glut
-         celestia-gnome
-         gravit
-         starplot
-        <BLANKLINE>
 
-        '''
-        d = Deb822()
-        d['Task'] = self.package_name
-        if not udeb:
-            d['Parent'] = self.blend
-        d['Section'] = self.blend
-        d['Description'] = self.summary + '\n ' + \
-          "\n ".join(self.description.replace("\n\n", "\n.\n").split("\n"))
-        if udeb:
-            d['Relevance'] = '10'
-        if self.enhances:
-            d['Enhances'] = ', '.join(sorted(d.name for d in itertools.chain(
-                *self.enhances)))
-        for key, value in self.tests.items():
-            d['Test-' + key] = value
-        d['Key'] = '\n {}'.format(self.package_name)
-        if udeb:
-            d['Packages'] = 'list\n ' + \
-                          '\n '.join(sorted(d.name for d in itertools.chain(
-                              *(self.recommends + self.dependencies))))
-        return d
+from blends import Blend, aptcache, uddcache
 
-    def update(self, cache):
-        '''Update from cache
+default_release = "testing"
 
-        This adds the available versions to all dependencies. It updates
-        descriptions, summaries etc. available to all BaseDependencies.
+parser = argparse.ArgumentParser()
 
-        :param cache: ``apt.Cache`` like object
+parser.add_argument("-d", "--dir", dest="dir", type=str,
+                    default=".",
+                    help="Base directory of the tasks source package"
+                    + " (default: current directory)")
 
-        Instead of using ``update()``, also the ``+=`` operator can be used:
+parser.add_argument("-r", "--release", dest="release", type=str,
+                    default=default_release,
+                    help="Target release, eg: stable, testing etc."
+                    + " (default: {})".format(default_release))
 
-        >>> import apt
-        >>> with open('education') as fp:
-        ...     task = Task('debian-astro', 'education', fp)
-        >>> dep = task.recommends[1][0]
-        >>> print(dep.name + ": ", dep.summary)
-        starplot:  None
-        >>> task += apt.Cache()
-        >>> print(dep.name + ": ", dep.summary)
-        starplot:  3-dimensional perspective star map viewer
-        '''
-        for dep in self.all:
-            pkg = cache.get(dep.name)
-            if pkg is not None:
-                dep.target_versions += pkg.versions
-            if hasattr(cache, 'get_providing_packages'):
-                for pkg in cache.get_providing_packages(dep.name):
-                    dep.target_versions += pkg.versions
+parser.add_argument("-S", '--supress-empty', dest="suppressempty",
+                    action="store_true", default=False,
+                    help="suppress tasks without any recommended package")
 
-    def __iadd__(self, cache):
-        self.update(cache)
-        return self
-    
-    def fix_dependencies(self):
-        '''Fix the dependencies according to available packages
+parser.add_argument("-a", '--wanted', dest="wanted",
+                    action="store_true", default=False,
+                    help="print all wanted packages")
 
-        This lowers all unavailable ``recommended`` dependencies to
-        ``suggested``.
+parser.add_argument("-m", '--missing', dest="missing",
+                    action="store_true", default=False,
+                    help="print missing packages")
 
-        >>> import apt
-        >>> with open('education') as fp:
-        ...     task = Task('debian-astro', 'education', fp)
-        >>> for dep in task.recommends:
-        ...     print(dep.rawstr)
-        celestia-gnome | celestia-glut
-        starplot
-        gravit
-        >>> for dep in task.suggests:
-        ...     print(dep.rawstr)
-        sunclock
-        xtide
-        >>> task += apt.Cache()
-        >>> missing = task.fix_dependencies()
-        >>> for dep in task.recommends:
-        ...     print(dep.rawstr)
-        starplot
-        gravit
-        >>> for dep in task.suggests:
-        ...     print(dep.rawstr)
-        sunclock
-        xtide
-        celestia-gnome | celestia-glut
-        '''
-        missing = list()
-        for recommended in self.recommends[:]:
-            suggested = Dependency("Suggests")
-            for dep in recommended[:]:
-                if len(dep.target_versions) == 0:
-                    recommended.remove(dep)
-                    suggested.append(dep)
-                    missing.append(dep)
-            if len(recommended) == 0:
-                self.recommends.remove(recommended)
-            if len(suggested) > 0:
-                self.suggests.append(suggested)
-        return missing
+parser.add_argument("-c", '--control', dest="gencontrol",
+                    action="store_true", default=False,
+                    help="Create new debian/control file.")
 
-    @staticmethod
-    def upgrade_from_1_0(content):
-        header = [ ("Format", "https://blends.debian.org/blends/1.1") ]
-        header += list(filter(lambda x: x[0] != "Format", content[0].items()))
-        res = [ dict(header) ]
-        for p in content[1:]:
-            q = []
-            for key, value in p.items():
-                if key == 'Depends' and 'Recommends' not in p:
-                    key = 'Recommends'
-                # Remove backslashes, which are not DEB822 compliant
-                value = re.sub(r'\s*\\', '', value)
-                q.append((key, value))
-            res.append(dict(q))
-        return Deb822List(res)
-            
+parser.add_argument("-t", '--taskdesc', dest="taskdesc",
+                    action="store_true", default=False,
+                    help="Generate task descriptions and package list for task")
 
-class Dependency(list):
-    '''Represent an Or-group of dependencies. 
+parser.add_argument("-u", '--udebs', dest="udebs",
+                    action="store_true", default=False,
+                    help="Modify tasks desc file in case the blend uses udebs")
 
-    Example:
+parser.add_argument("-F", '--upgrade-task-format', dest="upgrade_tasks",
+                    action="store_true", default=False,
+                    help="Upgrade tasks files to the latest format version")
 
-    >>> with open('education') as fp:
-    ...     task = Task('debian-astro', 'education', fp)
-    >>> dep = task.recommends[0]
-    >>> print(dep.rawstr)
-    celestia-gnome | celestia-glut
-    '''
+parser.add_argument("-U", "--udd", dest="udd",
+                    action="store_true", default=False,
+                    help="Query UDD instead of apt (needs python3-psycopg2)")
 
-    def __init__(self, rawtype, s = None, content = None):
-        super(Dependency, self).__init__(BaseDependency(bs.strip(), content)
-                                         for bs in (s.split("|") if s else []))
-        self.rawtype = rawtype
-        '''The type of the dependencies in the Or-group'''
-        
-    @property
-    def or_dependencies(self):
-        return self
+parser.add_argument("--udd-host", dest="udd_host",
+                    default="udd-mirror.debian.net",
+                    help="UDD host name (default: udd-mirror.debian.net)")
 
-    @property
-    def rawstr(self):
-        '''String represenation of the Or-group of dependencies.
+parser.add_argument("--udd-user", dest="udd_user",
+                    default="udd-mirror",
+                    help="UDD user (default: udd-mirror)")
 
-        Returns the string representation of the Or-group of
-        dependencies as it would be written in the ``debian/control``
-        file.  The string representation does not include the type of
-        the Or-group of dependencies.
-        '''
-        return ' | '.join(bd.rawstr for bd in self)
+parser.add_argument("--udd-password", dest="udd_password",
+                    default="udd-mirror",
+                    help="UDD password (default: udd-mirror)")
 
-    @property
-    def target_versions(self):
-        '''A list of all Version objects which satisfy this Or-group of deps.
-        '''
-        return list(itertools.chain(bd.target_versions for bd in self))
+parser.add_argument("--udd-database", dest="udd_database",
+                    default="udd",
+                    help="UDD database name (default: udd)")
 
+args = parser.parse_args()
 
-class BaseDependency:
-    '''A single dependency.
+if args.release == "current":
+    args.release = None
 
-    Example:
+blend = Blend(basedir=args.dir)
 
-    >>> with open('education') as fp:
-    ...     task = Task('debian-astro', 'education', fp)
-    >>> dep = task.recommends[2][0]
-    >>> print(dep.rawstr)
-    gravit
-    >>> print(dep.wnpp)
-    743379
-    >>> print(dep.homepage)
-    http://gravit.slowchop.com/
-    >>> print(dep.description)
-    Gravit is a free, visually stunning gravity simulator.
-    <BLANKLINE>
-    You can spend endless time experimenting with various
-    configurations of simulated universes.
-    >>> print(dep.summary)
-    Visually stunning gravity simulator
-    '''
+# For better performance, remove all tasks that will not create metapackages
+for task in blend.tasks[:]:
+    if not task.is_metapackage:
+        blend.tasks.remove(task)
 
-    def __init__(self, s, content = None):
-        r = re.compile(r'([a-z0-9][a-z0-9+-\.]+)')
-        m = r.match(s)
-        if m is None or m.string != s:
-            raise ValueError('"{}" is not a valid package name'.format(s))
-        self.name = s
-        self.content = content or dict()
-        self.target_versions = []
+if args.udd:
+    blend += uddcache(args.release, blend.all, host=args.udd_host,
+                      user=args.udd_user, password=args.udd_password,
+                      database=args.udd_database)
+else:
+    blend += aptcache(args.release, [args.dir, '/etc/blends'])
 
-    def _get_from_target_versions(self, key):
-        for v in self.target_versions:
-            if v.package.name == self.name:
-                return getattr(v, key)
+missing = blend.fix_dependencies()
 
-    @property
-    def rawstr(self):
-        '''String represenation of the dependency.
-
-        Returns the string representation of the dependency as it
-        would be written in the ``debian/control`` file.  The string
-        representation does not include the type of the dependency.
-        '''
-        return self.name
-
-    @property
-    def wnpp(self):
-        '''The WNPP bug number, if available, or None
-        '''
-        return self.content.get("WNPP")
-
-    @property
-    def homepage(self):
-        '''Return the homepage for the package.
-        '''
-        return self._get_from_target_versions("homepage") or \
-            self.content.get("Homepage")
-
-    @property
-    def description(self):
-        '''Return the formatted long description.
-        '''
-        desc = self._get_from_target_versions("description")
-        if desc is not None:
-            return desc
-        desc = self.content.get("Pkg-Description",
-                                self.content.get("Description"))
-        if desc:
-            return "\n".join(l[1:] if l != ' .' else ''
-                             for l in desc.split("\n")[1:])
-
-    @property
-    def summary(self):
-        '''Return the short description (one line summary).
-        '''
-        summary = self._get_from_target_versions("summary")
-        if summary:
-            return summary
-
-        desc = self.content.get("Pkg-Description",
-                                self.content.get("Description"))
-        if desc:
-            return desc.split('\n')[0]
-
-    @property
-    def why(self):
-        return self.content.get("Why")
-
-    @property
-    def remark(self):
-        return self.content.get("Remark")
-
-
-class Deb822List(list):
-    '''A list of ``Deb822`` paragraphs
-    '''
-    def __init__(self, paragraphs):
-        list.__init__(self, (p if isinstance(p, Deb822) else Deb822(p)
-                             for p in paragraphs))
-
-    def dump(self, fd=None, encoding=None, text_mode=False):
-        '''Dump the the contents in the original format
-
-        If ``fd`` is ``None``, returns a ``str`` object. Otherwise,
-        ``fd`` is assumed to be a ``file``-like object, and this
-        method will write the data to it instead of returning an
-        ``str`` object.
-
-        If ``fd`` is not ``None`` and ``text_mode`` is ``False``, the
-        data will be encoded to a byte string before writing to the
-        file.  The encoding used is chosen via the encoding parameter;
-        None means to use the encoding the object was initialized with
-        (utf-8 by default).  This will raise ``UnicodeEncodeError`` if
-        the encoding can't support all the characters in the
-        ``Deb822Dict`` values.
-
-        '''
-        if fd is None:
-            fd = io.StringIO()
-            return_string = True
-        else:
-            return_string = False
-
-        for p in self:
-            p.dump(fd, encoding, text_mode)
-            fd.write("\n")
-
-        if return_string:
-            return fd.getvalue()
-
-
-def aptcache(release = None, srcdirs = [ '/etc/blends' ]):
-    '''Open and update a (temporary) apt cache for the specified distribution.
-
-    :param release: Distribution name
-    :param srcdirs: List of directories to search for ``sources.list.<<release>>``
-
-    If the distribution is not given, use the system's cache without update.
-    '''
-    import tempfile
-    import apt
-
-    if release is None:
-        return apt.Cache()
-    rootdir = tempfile.mkdtemp()
-    try:
-        os.makedirs(os.path.join(rootdir, 'etc', 'apt'))
-        shutil.copytree('/etc/apt/trusted.gpg.d',
-                        os.path.join(rootdir, 'etc', 'apt', 'trusted.gpg.d'))
-        for src_dir in srcdirs:
-            sources_list = os.path.join(src_dir,
-                                        'sources.list.{}'.format(release))
-            if os.path.exists(sources_list):
-                shutil.copy(sources_list,
-                            os.path.join(rootdir, 'etc', 'apt', 'sources.list'))
-                break
-        else:
-            raise OSError("sources.list not found in " + str(srcdirs))
-        cache = apt.Cache(rootdir=rootdir, memonly=True)
-        cache.update()
-        cache.open()
-    finally:
-        shutil.rmtree(rootdir)
-    return cache
-
-
-def uddcache(release, packages, **db_args):
-    '''Create a ``dict`` from UDD that is roughly modelled after ``apt.Cache``.
-
-    The ``dict`` just resolves the version number and archs for the packages.
-    For performance reasons, an initial package list needs to be given.
-
-    :param release: Distribution name
-    :param packages: Initial package list
-    :param db_args: UDD connection parameters
-
-    ``Provided`` dependencies are integrated in the returned ``dict``.
-    '''
-    import collections
-    import psycopg2
-
-    pkgtuple = tuple(set(p.name for p in packages))
-
-    Package = collections.namedtuple('Package',
-                                     ('name', 'versions',))
-    Version = collections.namedtuple('Version',
-                                     ('package', 'architecture', 'version'))
-
-    stmt = '''SELECT packages.package,
-                     packages.provides,
-                     packages.architecture,
-                     packages.version
-              FROM packages, releases
-              WHERE packages.release=releases.release
-                    AND (releases.release=%s  OR releases.role=%s)
-                    AND (packages.package IN %s OR packages.provides in %s);'''
-
-    with psycopg2.connect(**db_args) as conn:
-        cursor = conn.cursor()
-        cursor.execute(stmt, (release, release, pkgtuple, pkgtuple))
-
-        cache = dict()
-        for package, provides, arch, version in cursor:
-            p = cache.setdefault(package, Package(package, []))
-            p.versions.append(Version(p, arch, version))
-            if provides:
-                pp = cache.setdefault(provides, Package(package, []))
-                pp.versions.append(Version(p, arch, version))
-
-        return cache
-
-
-if __name__ == '__main__':
-    import sys
-    import argparse
-
-    default_release = "testing"
-
-    parser = argparse.ArgumentParser()
-
-    parser.add_argument("-d", "--dir", dest="dir", type=str,
-                        default=".",
-                        help="Base directory of the tasks source package"
-                        + " (default: current directory)")
-
-    parser.add_argument("-r", "--release", dest="release", type=str,
-                        default=default_release,
-                        help="Target release, eg: stable, testing etc."
-                        + " (default: {})".format(default_release))
-
-    parser.add_argument("-S", '--supress-empty', dest="suppressempty",
-                        action="store_true", default=False,
-                        help="suppress tasks without any recommended package")
-
-    parser.add_argument("-a", '--wanted', dest="wanted",
-                        action="store_true", default=False,
-                        help="print all wanted packages")
-
-    parser.add_argument("-m", '--missing', dest="missing",
-                        action="store_true", default=False,
-                        help="print missing packages")
-
-    parser.add_argument("-c", '--control', dest="gencontrol",
-                        action="store_true", default=False,
-                        help="Create new debian/control file.")
-
-    parser.add_argument("-t", '--taskdesc', dest="taskdesc",
-                        action="store_true", default=False,
-                        help="Generate task descriptions and package list for task")
-
-    parser.add_argument("-u", '--udebs', dest="udebs",
-                        action="store_true", default=False,
-                        help="Modify tasks desc file in case the blend uses udebs")
-
-    parser.add_argument("-F", '--upgrade-task-format', dest="upgrade_tasks",
-                        action="store_true", default=False,
-                        help="Upgrade tasks files to the latest format version")
-
-    parser.add_argument("-U", "--udd", dest="udd",
-                        action="store_true", default=False,
-                        help="Query UDD instead of apt (needs python3-psycopg2)")
-
-    parser.add_argument("--udd-host", dest="udd_host",
-                        default="udd-mirror.debian.net",
-                        help="UDD host name (default: udd-mirror.debian.net)")
-
-    parser.add_argument("--udd-user", dest="udd_user",
-                        default="udd-mirror",
-                        help="UDD user (default: udd-mirror)")
-
-    parser.add_argument("--udd-password", dest="udd_password",
-                        default="udd-mirror",
-                        help="UDD password (default: udd-mirror)")
-
-    parser.add_argument("--udd-database", dest="udd_database",
-                        default="udd",
-                        help="UDD database name (default: udd)")
-
-    args = parser.parse_args()
-
-    if args.release == "current":
-        args.release = None
-
-    blend = Blend(basedir=args.dir)
-
-    # For better performance, remove all tasks that will not create metapackages
+if args.suppressempty:
     for task in blend.tasks[:]:
-        if not task.is_metapackage:
+        if len(task.recommends) == 0 and len(task.dependencies) > 0:
             blend.tasks.remove(task)
 
-    if args.udd:
-        blend += uddcache(args.release, blend.all, host=args.udd_host,
-                          user=args.udd_user, password=args.udd_password,
-                          database=args.udd_database)
+if missing and args.missing:
+    missing = sorted(set(d.name for d in missing))
+    print('Missing {} packages downgraded to `suggests`:\n  '
+          .format(len(missing))
+          + '\n  '.join(missing))
+
+if args.wanted:
+    wanted = sorted(set(d.name for d in itertools.chain(*(
+        itertools.chain(*(t.recommends + t.dependencies))
+        for t in blend.tasks))))
+    print("Total {} packages in Depends or Recommend:\n  "
+          .format(len(wanted))
+          + '\n  '.join(wanted))
+
+if args.gencontrol:
+    with open(os.path.join(args.dir, 'debian', 'control'), 'w') as fp:
+        fp.write('# This file is autogenerated. Do not edit!\n')
+        blend.gen_control().dump(fp, text_mode=True)
+
+if args.taskdesc:
+    with open(os.path.join(args.dir, '{}-tasks.desc'.format(blend.name)), 'w') as fp:
+        blend.gen_task_desc(udeb=args.udebs).dump(fp, text_mode=True)
+
+upgraded_tasks = list(filter(lambda task: task.format_upgraded, blend.tasks))
+if upgraded_tasks:
+    if args.upgrade_tasks:
+        print('Upgrading {} tasks from format version {}'
+              .format(len(upgraded_tasks), upgraded_tasks[0].format_version))
+        for task in upgraded_tasks:
+            with open(os.path.join(args.dir, 'tasks', task.name), 'w') as fp:
+                task.content.dump(fp, text_mode=True)
     else:
-        blend += aptcache(args.release, [ args.dir, '/etc/blends' ])
-
-    missing = blend.fix_dependencies()
-
-    if args.suppressempty:
-        for task in blend.tasks[:]:
-            if len(task.recommends) == 0 and len(task.dependencies) > 0:
-                blend.tasks.remove(task)
-
-    if missing and args.missing:
-        missing = sorted(set(d.name for d in missing))
-        print('Missing {} packages downgraded to `suggests`:\n  '
-              .format(len(missing))
-              + '\n  '.join(missing))
-
-    if args.wanted:
-        wanted = sorted(set(d.name for d in itertools.chain(*(
-            itertools.chain(*(t.recommends + t.dependencies))
-            for t in blend.tasks))))
-        print("Total {} packages in Depends or Recommend:\n  "
-              .format(len(wanted))
-              + '\n  '.join(wanted))
-
-    if args.gencontrol:
-        with open(os.path.join(args.dir,'debian', 'control'), 'w') as fp:
-            fp.write('# This file is autogenerated. Do not edit!\n')
-            blend.gen_control().dump(fp, text_mode=True)
-
-    if args.taskdesc:
-        with open(os.path.join(args.dir,'{}-tasks.desc'.format(blend.name)), 'w') as fp:
-            blend.gen_task_desc(udeb = args.udebs).dump(fp, text_mode=True)
-
-    upgraded_tasks = list(filter(lambda task: task.format_upgraded, blend.tasks))
-    if upgraded_tasks:
-        if args.upgrade_tasks:
-            print('Upgrading {} tasks from format version {}'
-                  .format(len(upgraded_tasks), upgraded_tasks[0].format_version))
-            for task in upgraded_tasks:
-                with open(os.path.join(args.dir, 'tasks', task.name), 'w') as fp:
-                    task.content.dump(fp, text_mode=True)
-        else:
-            print('Warning: {} tasks use the old format {}'
-                  .format(len(upgraded_tasks), upgraded_tasks[0].format_version))
-            print('Please consider upgrading the task files with the `-U` flag.')
-
+        print('Warning: {} tasks use the old format {}'
+              .format(len(upgraded_tasks), upgraded_tasks[0].format_version))
+        print('Please consider upgrading the task files with the `-U` flag.')


=====================================
sphinxdoc/Makefile
=====================================
--- /dev/null
+++ b/sphinxdoc/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+SPHINXPROJ    = blends
+SOURCEDIR     = .
+BUILDDIR      = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
\ No newline at end of file


=====================================
sphinxdoc/conf.py
=====================================
--- /dev/null
+++ b/sphinxdoc/conf.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+#
+# blends documentation build configuration file, created by
+# sphinx-quickstart on Tue Mar 20 22:23:34 2018.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+import os
+import sys
+sys.path.insert(0, os.path.abspath('..'))
+import blends
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = ['sphinx.ext.autodoc',
+    'sphinx.ext.doctest',
+    'sphinx.ext.intersphinx']
+
+autodoc_member_order = 'bysource'
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'blends'
+copyright = u'2018, Debian Blends Team'
+author = u'Debian Blends Team'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = u'0.7'
+# The full version, including alpha/beta/rc tags.
+release = u'0.7'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This patterns also effect to html_static_path and html_extra_path
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'alabaster'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Custom sidebar templates, must be a dictionary that maps document names
+# to template names.
+#
+# This is required for the alabaster theme
+# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
+html_sidebars = {
+    '**': [
+        'relations.html',  # needs 'show_related': True theme option to display
+        'searchbox.html',
+    ]
+}
+
+
+# -- Options for HTMLHelp output ------------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'blendsdoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+    # The paper size ('letterpaper' or 'a4paper').
+    #
+    # 'papersize': 'letterpaper',
+
+    # The font size ('10pt', '11pt' or '12pt').
+    #
+    # 'pointsize': '10pt',
+
+    # Additional stuff for the LaTeX preamble.
+    #
+    # 'preamble': '',
+
+    # Latex figure (float) alignment
+    #
+    # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+#  author, documentclass [howto, manual, or own class]).
+latex_documents = [
+    (master_doc, 'blends.tex', u'blends Documentation',
+     u'Debian Blends Team', 'manual'),
+]
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    (master_doc, 'blends', u'blends Documentation',
+     [author], 1)
+]
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+    (master_doc, 'blends', u'blends Documentation',
+     author, 'blends', 'One line description of project.',
+     'Miscellaneous'),
+]
+
+
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {'https://docs.python.org/': None}


=====================================
sphinxdoc/index.rst
=====================================
--- /dev/null
+++ b/sphinxdoc/index.rst
@@ -0,0 +1,9 @@
+Python module debian.blends
+===========================
+
+.. automodule:: blends
+   :members:
+
+.. toctree::
+   :maxdepth: 2
+   :caption: Contents:



View it on GitLab: https://salsa.debian.org/blends-team/blends/commit/fb15ffe0e7c17fbdcafa9173b9562b4c6d1d0bce

---
View it on GitLab: https://salsa.debian.org/blends-team/blends/commit/fb15ffe0e7c17fbdcafa9173b9562b4c6d1d0bce
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.alioth.debian.org/pipermail/blends-commit/attachments/20180410/5c69dc86/attachment-0001.html>


More information about the Blends-commit mailing list