Hi!

I've been contemplating deb822's support for build profiles some more and in 
particular looked the recent changes to the build profiles / build restrictions 
specification. The attached patches are my current thinking on the matter.

The main changes are:

* there now can be multiple sets of < … > restrictions specified for each 
package

* as a result, we need to return a list of restrictions not a single 
restriction

* I propose flattening the representation of each restriction from (enabled, 
(namespace, label)) to (enabled, namespace, label)

* Following John's suggestion, I'm looking to use namedtuples to provide 
backwards compatibility for the architecture restrictions while also providing 
a nicer syntax for accessors to both arch and build restrictions objects.


As you will see in the patches, that means that 

      "texlive <!profile.cross>"   

becomes

                    [[(false, 'profile', 'cross')]]

and

    'debhelper (>> 5.0.0)  <!profile.stage1> <!profile.cross !profile.stage2>'

becomes

        [
                        [(False, 'profile', 'stage1')],
                        [(False, 'profile', 'stage2'),
                         (False, 'profile', 'cross')]
        ]

The innermost tuple is also accessible as a namedtuple (enabled, namespace, 
label).

Comments and suggestions welcome!

cheers
Stuart

-- 
Stuart Prescott    http://www.nanonanonano.net/   [email protected]
Debian Developer   http://www.debian.org/         [email protected]
GPG fingerprint    90E2 D2C1 AD14 6A1B 7EBB 891D BBC1 7EBB 1396 F2F7
From 990e668abb13808928df64eb092c9851969adcc2 Mon Sep 17 00:00:00 2001
From: Stuart Prescott <[email protected]>
Date: Sun, 31 Aug 2014 20:14:20 +1000
Subject: [PATCH 1/2] Use namedtuple for architecture restrictions

---
 lib/debian/deb822.py | 21 ++++++++++++---------
 1 file changed, 12 insertions(+), 9 deletions(-)

diff --git a/lib/debian/deb822.py b/lib/debian/deb822.py
index b40fd38..25a204f 100644
--- a/lib/debian/deb822.py
+++ b/lib/debian/deb822.py
@@ -36,6 +36,7 @@ except (ImportError, AttributeError):
     _have_apt_pkg = False
 
 import chardet
+import collections
 import os
 import re
 import subprocess
@@ -898,6 +899,9 @@ class PkgRelation(object):
             r'(?P<namespace>[^.]+)\.'
             r'(?P<name>[^\s]+)')
 
+    ArchRestriction = collections.namedtuple('ArchRestriction',
+            ['enabled', 'arch'])
+
     @classmethod
     def parse_relations(cls, raw):
         """Parse a package relationship string (i.e. the value of a field like
@@ -907,10 +911,10 @@ class PkgRelation(object):
             # assumption: no space between '!' and architecture name
             archs = []
             for arch in cls.__blank_sep_RE.split(raw.strip()):
-                if len(arch) and arch[0] == '!':
-                    archs.append((False, arch[1:]))
-                else:
-                    archs.append((True, arch))
+                disabled = arch[0] == '!'
+                if disabled:
+                    arch = arch[1:]
+                archs.append(cls.ArchRestriction(not disabled, arch))
             return archs
 
         def parse_profiles(raw):
@@ -972,11 +976,10 @@ class PkgRelation(object):
         suitable to be written in a package stanza.
         """
         def pp_arch(arch_spec):
-            (excl, arch) = arch_spec
-            if excl:
-                return arch
-            else:
-                return '!' + arch
+            return '%s%s' % (
+                    '' if arch_spec.enabled else '!',
+                    arch_spec.arch,
+                )
 
         def pp_atomic_dep(dep):
             s = dep['name']
-- 
1.9.1

From b8c2dbde7989c97d84054a528a8739f85bf61fb1 Mon Sep 17 00:00:00 2001
From: Stuart Prescott <[email protected]>
Date: Sun, 31 Aug 2014 20:22:06 +1000
Subject: [PATCH 2/2] Update build restrictions parser for new syntax
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Support multiple sets of <…> expressions
* Correctly call it "namespace.label" rather than "namespace.name".
* Use namedtuples for BuildRestrictions in symmetry with ArchRestrictions
* Add support for turning build restrictions back into strings
* Update documentation for _PkgRelationMixin to include build restrictions
* Update tests with examples of multiple sets of <…>
---
 lib/debian/deb822.py         | 71 ++++++++++++++++++++++++++++++++------------
 tests/test_Sources           |  2 +-
 tests/test_Sources.iso8859-1 |  2 +-
 tests/test_deb822.py         | 46 ++++++++++++++++++++++++----
 4 files changed, 95 insertions(+), 26 deletions(-)

diff --git a/lib/debian/deb822.py b/lib/debian/deb822.py
index 25a204f..43413a4 100644
--- a/lib/debian/deb822.py
+++ b/lib/debian/deb822.py
@@ -889,18 +889,21 @@ class PkgRelation(object):
             r'(\s*\(\s*(?P<relop>[>=<]+)\s*'
             r'(?P<version>[0-9a-zA-Z:\-+~.]+)\s*\))?'
             r'(\s*\[(?P<archs>[\s!\w\-]+)\])?\s*'
-            r'(<(?P<restrictions>.+)>)?\s*'
+            r'((?P<restrictions><.+>))?\s*'
             r'$')
     __comma_sep_RE = re.compile(r'\s*,\s*')
     __pipe_sep_RE = re.compile(r'\s*\|\s*')
     __blank_sep_RE = re.compile(r'\s*')
+    __restriction_sep_RE = re.compile(r'>\s*<')
     __restriction_RE = re.compile(
             r'(?P<enabled>\!)?'
             r'(?P<namespace>[^.]+)\.'
-            r'(?P<name>[^\s]+)')
+            r'(?P<label>[^\s]+)')
 
     ArchRestriction = collections.namedtuple('ArchRestriction',
             ['enabled', 'arch'])
+    BuildRestriction = collections.namedtuple('BuildRestriction',
+            ['enabled', 'namespace', 'label'])
 
     @classmethod
     def parse_relations(cls, raw):
@@ -917,27 +920,31 @@ class PkgRelation(object):
                 archs.append(cls.ArchRestriction(not disabled, arch))
             return archs
 
-        def parse_profiles(raw):
+        def parse_restrictions(raw):
             """ split a string of restrictions into a list of restrictions
 
             Each restriction is a tuple of form:
 
-                (active, (namespace, name))
+                (active, namespace, label)
 
             where
                 active: boolean: whether the restriction is positive or negative
                 namespace: the namespace of the restriction e.g. 'profile'
-                name: the name of the restriction e.g. 'stage1'
+                label: the name of the restriction e.g. 'stage1'
             """
             restrictions = []
-            for restriction in cls.__blank_sep_RE.split(raw.lower().strip()):
-                match = cls.__restriction_RE.match(restriction)
-                if match:
-                    parts = match.groupdict()
-                    restrictions.append((
-                                    parts['enabled'] != '!',
-                                    (parts['namespace'], parts['name']),
-                                ))
+            for rgrp in cls.__restriction_sep_RE.split(raw.lower().strip('<> ')):
+                group = []
+                for restriction in cls.__blank_sep_RE.split(rgrp):
+                    match = cls.__restriction_RE.match(restriction)
+                    if match:
+                        parts = match.groupdict()
+                        group.append(cls.BuildRestriction(
+                                        parts['enabled'] != '!',
+                                        parts['namespace'],
+                                        parts['label'],
+                                    ))
+                restrictions.append(group)
             return restrictions
 
 
@@ -957,7 +964,7 @@ class PkgRelation(object):
                 if parts['archs']:
                     d['arch'] = parse_archs(parts['archs'])
                 if parts['restrictions']:
-                    d['restrictions'] = parse_profiles(parts['restrictions'])
+                    d['restrictions'] = parse_restrictions(parts['restrictions'])
                 return d
             else:
                 warnings.warn('cannot parse package' \
@@ -981,12 +988,25 @@ class PkgRelation(object):
                     arch_spec.arch,
                 )
 
+        def pp_restrictions(restrictions):
+            s = []
+            for r in restrictions:
+                s.append('%s%s.%s' % (
+                            '' if r.enabled else '!',
+                            r.namespace,
+                            r.label
+                        )
+                    )
+            return '<%s>' % ' '.join(s)
+
         def pp_atomic_dep(dep):
             s = dep['name']
             if dep.get('version') is not None:
                 s += ' (%s %s)' % dep['version']
             if dep.get('arch') is not None:
                 s += ' [%s]' % ' '.join(map(pp_arch, dep['arch']))
+            if dep.get('restrictions') is not None:
+                s += ' %s' % ' '.join(map(pp_restrictions, dep['restrictions']))
             return s
 
         pp_or_dep = lambda deps: ' | '.join(map(pp_atomic_dep, deps))
@@ -1053,7 +1073,7 @@ class _PkgRelationMixin(object):
 
         The encoding of package relationships is as follows:
         - the top-level lists corresponds to the comma-separated list of
-          Deb822, their components form a conjuction, i.e. they have to be
+          Deb822, their components form a conjunction, i.e. they have to be
           AND-ed together
         - the inner lists corresponds to the pipe-separated list of Deb822,
           their components form a disjunction, i.e. they have to be OR-ed
@@ -1064,11 +1084,20 @@ class _PkgRelationMixin(object):
                         versioned, None otherwise. operator is one of "<<",
                         "<=", "=", ">=", ">>"; version is the given version as
                         a string.
-          - arch:       A list of pairs <polarity, architecture> if the
+          - arch:       A list of pairs <enabled, arch> if the
                         relationship is architecture specific, None otherwise.
-                        Polarity is a boolean (false if the architecture is
-                        negated with "!", true otherwise), architecture the
-                        Debian archtiecture name as a string.
+                        Enabled is a boolean (false if the architecture is
+                        negated with "!", true otherwise), arch the
+                        Debian architecture name as a string.
+          - restrictions: A list of lists of tuples <enabled, namespace, label>
+                        if there are restrictions defined, None otherwise.
+                        Enabled is a boolean (false if the restriction is
+                        negated with "!", true otherwise), namespace and label
+                        are as defined for the build restrictions/profiles.
+
+          The arch and restrictions tuples are available as named tuples so
+          elements are available as some_restriction[0] or alternatively as
+          some_restriction.enabled (and so forth).
 
         Examples:
 
@@ -1080,6 +1109,10 @@ class _PkgRelationMixin(object):
           "tcl8.4-dev, procps [!hurd-i386]"                 becomes
           [ [ {'name': 'tcl8.4-dev'} ],
             [ {'name': 'procps', 'arch': (false, 'hurd-i386')} ] ]
+
+          "texlive <!profile.cross>"                       becomes
+          [ [ {'name': 'texlive',
+                    'restriction': [[(false, 'profile', 'cross')]]} ] ]
         """
         if not self.__parsed_relations:
             lazy_rels = filter(lambda n: self.__relations[n] is None,
diff --git a/tests/test_Sources b/tests/test_Sources
index 1ffab0d..4357219 100644
--- a/tests/test_Sources
+++ b/tests/test_Sources
@@ -26,7 +26,7 @@ Version: 2.18.1~cvs20080103-6
 Priority: optional
 Section: devel
 Maintainer: James Troup <[email protected]>
-Build-Depends: dpkg-dev (>= 1.13.9), autoconf (>= 2.13), bash, bison:amd64, flex, gettext:any, texinfo <!profile.stage1 !profile.cross>, expect-tcl8.3 (>= 5.32.2) [hppa] <!profile.stage1>, dejagnu (>= 1.4.2-1.1), dpatch, file, bzip2:native, lsb-release
+Build-Depends: dpkg-dev (>= 1.13.9), autoconf (>= 2.13), bash, bison:amd64, flex, gettext:any, texinfo <!profile.stage1> <!profile.stage2 !profile.cross>, expect-tcl8.3 (>= 5.32.2) [hppa] <!profile.stage1>, dejagnu (>= 1.4.2-1.1), dpatch, file, bzip2:native, lsb-release
 Architecture: any
 Standards-Version: 3.7.3
 Format: 1.0
diff --git a/tests/test_Sources.iso8859-1 b/tests/test_Sources.iso8859-1
index 948b190..ff2d999 100644
--- a/tests/test_Sources.iso8859-1
+++ b/tests/test_Sources.iso8859-1
@@ -26,7 +26,7 @@ Version: 2.18.1~cvs20080103-6
 Priority: optional
 Section: devel
 Maintainer: James Troup <[email protected]>
-Build-Depends: dpkg-dev (>= 1.13.9), autoconf (>= 2.13), bash, bison:amd64, flex, gettext:any, texinfo <!profile.stage1 !profile.cross>, expect-tcl8.3 (>= 5.32.2) [hppa] <!profile.stage1>, dejagnu (>= 1.4.2-1.1), dpatch, file, bzip2:native, lsb-release
+Build-Depends: dpkg-dev (>= 1.13.9), autoconf (>= 2.13), bash, bison:amd64, flex, gettext:any, texinfo <!profile.stage1> <!profile.stage2 !profile.cross>, expect-tcl8.3 (>= 5.32.2) [hppa] <!profile.stage1>, dejagnu (>= 1.4.2-1.1), dpatch, file, bzip2:native, lsb-release
 Architecture: any
 Standards-Version: 3.7.3
 Format: 1.0
diff --git a/tests/test_deb822.py b/tests/test_deb822.py
index f12dbfd..abf7fa1 100755
--- a/tests/test_deb822.py
+++ b/tests/test_deb822.py
@@ -1050,9 +1050,11 @@ class TestPkgRelations(unittest.TestCase):
         f.close()
 
         bin_rels = ['file, libc6 (>= 2.7-1), libpaper1, psutils']
-        src_rels = ['apache2-src (>= 2.2.9), libaprutil1-dev, ' \
-                'libcap-dev [!kfreebsd-i386 !kfreebsd-amd64 !hurd-i386], ' \
-                'autoconf, debhelper (>> 5.0.0)']
+        src_rels = ['apache2-src (>= 2.2.9), libaprutil1-dev, '
+                'libcap-dev [!kfreebsd-i386 !kfreebsd-amd64 !hurd-i386], '
+                'autoconf <!profile.cross>, '
+                'debhelper (>> 5.0.0) '
+                '<!profile.stage1> <!profile.cross !profile.stage2>']
         for bin_rel in bin_rels:
             self.assertEqual(bin_rel,
                     deb822.PkgRelation.str(deb822.PkgRelation.parse_relations(
@@ -1098,10 +1100,14 @@ class TestPkgRelations(unittest.TestCase):
                         [rel({'name': 'flex'})],
                         [rel({'name': 'gettext', 'archqual': 'any'})],
                         [rel({'name': 'texinfo',
-                            'restrictions': [(False, ('profile', 'stage1')), (False, ('profile', 'cross'))]})],
+                            'restrictions': [
+                                [(False, 'profile', 'stage1')],
+                                [(False, 'profile', 'stage2'),
+                                 (False, 'profile', 'cross')]
+                            ]})],
                         [rel({'arch': [(True, 'hppa')], 'name': 'expect-tcl8.3',
                             'version': ('>=', '5.32.2'),
-                            'restrictions': [(False, ('profile', 'stage1'))]})],
+                            'restrictions': [[(False, 'profile', 'stage1')]]})],
                         [rel({'name': 'dejagnu', 'version': ('>=', '1.4.2-1.1'), 'arch': None})],
                         [rel({'name': 'dpatch'})],
                         [rel({'name': 'file'})],
@@ -1122,6 +1128,36 @@ class TestPkgRelations(unittest.TestCase):
         self.assertPkgDictEqual(rel2, pkg2.relations)
         f.close()
 
+    def test_restrictions_parse(self):
+        r = "foo <profile.cross>"
+        # relation 0, alternative 0, restrictions set 0, condition 0
+        rel = deb822.PkgRelation.parse_relations(r)[0][0]['restrictions'][0][0]
+        self.assertEqual(rel.enabled, True)
+        self.assertEqual(rel[0], True)
+        self.assertEqual(rel.namespace, 'profile')
+        self.assertEqual(rel[1], 'profile')
+        self.assertEqual(rel.label, 'cross')
+        self.assertEqual(rel[2], 'cross')
+
+        r = "foo <!profile.stage1> <!profile.stage2 !profile.cross>"
+        # relation 0, alternative 0, restrictions set 1, condition 0
+        rel = deb822.PkgRelation.parse_relations(r)[0][0]['restrictions'][1][0]
+        self.assertEqual(rel.enabled, False)
+        self.assertEqual(rel[0], False)
+        self.assertEqual(rel.namespace, 'profile')
+        self.assertEqual(rel[1], 'profile')
+        self.assertEqual(rel.label, 'stage2')
+        self.assertEqual(rel[2], 'stage2')
+
+        # relation 0, alternative 0, restrictions set 1, condition 1
+        rel = deb822.PkgRelation.parse_relations(r)[0][0]['restrictions'][1][1]
+        self.assertEqual(rel.enabled, False)
+        self.assertEqual(rel[0], False)
+        self.assertEqual(rel.namespace, 'profile')
+        self.assertEqual(rel[1], 'profile')
+        self.assertEqual(rel.label, 'cross')
+        self.assertEqual(rel[2], 'cross')
+
 
 class TestGpgInfo(unittest.TestCase):
 
-- 
1.9.1

-- 
http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/pkg-python-debian-maint

Reply via email to