← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~bac/launchpad/bug-776437 into lp:launchpad

 

Brad Crittenden has proposed merging lp:~bac/launchpad/bug-776437 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #776437 in Launchpad itself: "Enable ARM builders for PPA via API"
  https://bugs.launchpad.net/launchpad/+bug/776437

For more details, see:
https://code.launchpad.net/~bac/launchpad/bug-776437/+merge/65692

= Summary =

Expose a way to set enabled restricted families for OEM's use.

== Proposed fix ==

Expose attributes for IProcessor, IProcessorFamily, and IProcessorFamilySet.

== Pre-implementation notes ==

Many chats with Francis, Gary, Benji, and Curtis.

== Implementation details ==

I used to <3 the API.  Now not so much.

== Tests ==

bin/test -vvm lp.soyuz -t TestProcessorFamilies

== Demo and Q/A ==

Fire up a server and hit it with launchpadlib.  Items of interest are
archive.enabled_restricted_families and archive.enableRestrictedFamily.
 For the latter you must be logged in as a member of the commercial
team.  bac@xxxxxxxxxxxxx/test works.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/soyuz/stories/webservice/xx-archive.txt
  lib/lp/soyuz/interfaces/processor.py
  lib/lp/soyuz/interfaces/archive.py
  lib/lp/app/browser/launchpad.py
  lib/lp/soyuz/configure.zcml
  lib/lp/testing/__init__.py
  lib/lp/soyuz/interfaces/webservice.py
  lib/lp/soyuz/browser/configure.zcml
  lib/lp/soyuz/browser/tests/test_archive_webservice.py
  lib/lp/soyuz/model/archive.py
  lib/lp/soyuz/browser/processor.py
  lib/canonical/launchpad/interfaces/_schema_circular_imports.py

./lib/lp/soyuz/stories/webservice/xx-archive.txt
      37: source exceeds 78 characters.
      40: want exceeds 78 characters.
      44: want exceeds 78 characters.
     165: want exceeds 78 characters.
     181: want exceeds 78 characters.
     197: want exceeds 78 characters.
     213: want exceeds 78 characters.
     301: want exceeds 78 characters.
     362: want exceeds 78 characters.
     493: want exceeds 78 characters.
./lib/lp/soyuz/model/archive.py
     348: redefinition of function 'private' from line 344
-- 
https://code.launchpad.net/~bac/launchpad/bug-776437/+merge/65692
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~bac/launchpad/bug-776437 into lp:launchpad.
=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py	2011-06-11 00:49:33 +0000
+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py	2011-06-23 16:21:19 +0000
@@ -200,6 +200,10 @@
     IPackageset,
     IPackagesetSet,
     )
+from lp.soyuz.interfaces.processor import (
+    IProcessor,
+    IProcessorFamily,
+    )
 from lp.soyuz.interfaces.publishing import (
     IBinaryPackagePublishingHistory,
     ISourcePackagePublishingHistory,
@@ -377,6 +381,8 @@
 # IArchive apocalypse.
 patch_reference_property(IArchive, 'distribution', IDistribution)
 patch_collection_property(IArchive, 'dependencies', IArchiveDependency)
+patch_collection_property(
+    IArchive, 'enabled_restricted_families', IProcessorFamily)
 patch_collection_return_type(
     IArchive, 'getPermissionsForPerson', IArchivePermission)
 patch_collection_return_type(
@@ -442,6 +448,8 @@
     IArchive, '_addArchiveDependency', 'pocket', PackagePublishingPocket)
 patch_entry_return_type(
     IArchive, '_addArchiveDependency', IArchiveDependency)
+patch_plain_parameter_type(
+    IArchive, 'enableRestrictedFamily', 'family', IProcessorFamily)
 
 
 # IBuildFarmJob
@@ -540,6 +548,10 @@
 patch_reference_property(IPackageUpload, 'distroseries', IDistroSeries)
 patch_reference_property(IPackageUpload, 'archive', IArchive)
 
+# IProcessor
+patch_reference_property(
+    IProcessor, 'family', IProcessorFamily)
+
 # IStructuralSubscription
 patch_collection_property(
     IStructuralSubscription, 'bug_filters', IBugSubscriptionFilter)

=== modified file 'lib/lp/app/browser/launchpad.py'
--- lib/lp/app/browser/launchpad.py	2011-05-29 01:59:15 +0000
+++ lib/lp/app/browser/launchpad.py	2011-06-23 16:21:19 +0000
@@ -147,6 +147,7 @@
 from lp.services.worlddata.interfaces.language import ILanguageSet
 from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet
 from lp.soyuz.interfaces.packageset import IPackagesetSet
+from lp.soyuz.interfaces.processor import IProcessorFamilySet
 from lp.testopenid.interfaces.server import ITestOpenIDApplication
 from lp.translations.interfaces.translationgroup import ITranslationGroupSet
 from lp.translations.interfaces.translationimportqueue import (
@@ -617,6 +618,7 @@
         'package-sets': IPackagesetSet,
         'people': IPersonSet,
         'pillars': IPillarNameSet,
+        '+processor-families': IProcessorFamilySet,
         'projects': IProductSet,
         'projectgroups': IProjectGroupSet,
         'sourcepackagenames': ISourcePackageNameSet,

=== modified file 'lib/lp/soyuz/browser/configure.zcml'
--- lib/lp/soyuz/browser/configure.zcml	2011-05-29 21:18:09 +0000
+++ lib/lp/soyuz/browser/configure.zcml	2011-06-23 16:21:19 +0000
@@ -32,6 +32,18 @@
             path_expression="string:+binarypub"
             attribute_to_parent="archive"
             urldata="lp.soyuz.browser.publishing.BinaryPublicationURL"/>
+        <browser:url
+	    for="lp.soyuz.interfaces.processor.IProcessorFamilySet"
+            path_expression="string:+processor-families"
+            parent_utility="canonical.launchpad.webapp.interfaces.ILaunchpadRoot"/>
+        <browser:url
+	    for="lp.soyuz.interfaces.processor.IProcessorFamily"
+            path_expression="string:${name}"
+            parent_utility="lp.soyuz.interfaces.processor.IProcessorFamilySet" />
+        <browser:url
+	    for="lp.soyuz.interfaces.processor.IProcessor"
+            path_expression="string:${id}"
+            attribute_to_parent="family" />
     </facet>
     <browser:navigation
         module="lp.soyuz.browser.binarypackagerelease"
@@ -217,8 +229,11 @@
         name="+index"/>
     <browser:navigation
         module="lp.soyuz.browser.archive"
-        classes="
-            ArchiveNavigation"/>
+        classes="ArchiveNavigation" />
+    <browser:navigation
+        module="lp.soyuz.browser.processor"
+	classes="
+	    ProcessorFamilySetNavigation ProcessorFamilyNavigation"/>
     <browser:url
         for="lp.soyuz.interfaces.archive.IPPA"
         path_expression="string:+archive"

=== added file 'lib/lp/soyuz/browser/processor.py'
--- lib/lp/soyuz/browser/processor.py	1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/browser/processor.py	2011-06-23 16:21:19 +0000
@@ -0,0 +1,46 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Navigation views for processors."""
+
+
+__metaclass__ = type
+
+__all__ = [
+    'ProcessorFamilySetNavigation',
+    'ProcessorFamilyNavigation',
+    ]
+
+
+from canonical.launchpad.webapp import Navigation
+from lp.app.errors import NotFoundError
+from lp.soyuz.interfaces.processor import (
+    IProcessorFamily,
+    IProcessorFamilySet,
+    )
+
+
+class ProcessorFamilySetNavigation(Navigation):
+    """IProcessorFamilySet navigation."""
+    usedfor = IProcessorFamilySet
+
+    def traverse(self, name):
+        family = self.context.getByName(name)
+        # Raise NotFoundError on invalid processor family name.
+        if family is None:
+            raise NotFoundError(name)
+        return family
+
+
+class ProcessorFamilyNavigation(Navigation):
+    """IProcessorFamily navigation."""
+
+    usedfor = IProcessorFamily
+
+    def traverse(self, id_):
+        id_ = int(id_)
+        processors = self.processors
+        for p in processors:
+            if p.id == id_:
+                return p
+        raise NotFoundError(id_)

=== modified file 'lib/lp/soyuz/browser/tests/test_archive_webservice.py'
--- lib/lp/soyuz/browser/tests/test_archive_webservice.py	2011-06-20 17:48:12 +0000
+++ lib/lp/soyuz/browser/tests/test_archive_webservice.py	2011-06-23 16:21:19 +0000
@@ -20,6 +20,7 @@
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.soyuz.enums import ArchivePurpose
+from lp.soyuz.interfaces.processor import IProcessorFamilySet
 from lp.testing import (
     celebrity_logged_in,
     launchpadlib_for,
@@ -200,5 +201,113 @@
         self.assertContentEqual([], ws_archive.dependencies)
 
 
+class TestProcessorFamilies(WebServiceTestCase):
+    """Test the enabled_restricted_families property and methods."""
+
+    def test_erfNotAvailableInBeta(self):
+        """The enabled_restricted_families property is not in beta."""
+        self.ws_version = 'beta'
+        archive = self.factory.makeArchive()
+        commercial = getUtility(ILaunchpadCelebrities).commercial_admin
+        commercial_admin = self.factory.makePerson(member_of=[commercial])
+        transaction.commit()
+        ws_archive = self.wsObject(archive, user=commercial_admin)
+        expected_re = (
+            "(.|\n)*'Entry' object has no attribute "
+            "'enabled_restricted_families'(.|\n)*")
+        with ExpectedException(AttributeError, expected_re):
+            ws_archive.enabled_restricted_families
+
+    def test_erfAvailableInDevel(self):
+        """The enabled_restricted_families property is in devel."""
+        self.ws_version = 'devel'
+        archive = self.factory.makeArchive()
+        commercial = getUtility(ILaunchpadCelebrities).commercial_admin
+        commercial_admin = self.factory.makePerson(member_of=[commercial])
+        transaction.commit()
+        ws_archive = self.wsObject(archive, user=commercial_admin)
+        self.assertContentEqual([], ws_archive.enabled_restricted_families)
+
+    def test_getByName(self):
+        """The getByName method returns a processor family."""
+        self.ws_version = 'devel'
+        transaction.commit()
+        arm = self.service.processor_families.getByName(name='arm')
+        self.assertEqual(u'arm', arm.name)
+        self.assertEqual(u'ARM Processors', arm.title)
+        self.assertEqual(
+            u'The ARM and compatible processors', arm.description)
+        self.assertEqual(True, arm.restricted)
+
+    def test_processors(self):
+        """Attributes about processors are available."""
+        self.ws_version = 'devel'
+        product_family_set = getUtility(IProcessorFamilySet)
+        ws_arm = self.service.processor_families.getByName(name='arm')
+        self.assertContentEqual([], ws_arm.processors)
+        product_family_set = getUtility(IProcessorFamilySet)
+        arm = product_family_set.getByName('arm')
+        arm.addProcessor('new-arm', 'New ARM Title', 'New ARM Description')
+        transaction.commit()
+        ws_proc = ws_arm.processors[0]
+        self.assertEqual('new-arm', ws_proc.name)
+        self.assertEqual('New ARM Title', ws_proc.title)
+        self.assertEqual('New ARM Description', ws_proc.description)
+
+    def test_enableRestrictedFamily(self):
+        """A new family can be added to the enabled restricted set."""
+        self.ws_version = 'devel'
+        archive = self.factory.makeArchive()
+        commercial = getUtility(ILaunchpadCelebrities).commercial_admin
+        commercial_admin = self.factory.makePerson(member_of=[commercial])
+        transaction.commit()
+        ws_arm = self.service.processor_families.getByName(name='arm')
+        ws_archive = self.wsObject(archive, user=commercial_admin)
+        self.assertContentEqual([], ws_archive.enabled_restricted_families)
+        ws_archive.enableRestrictedFamily(family=ws_arm)
+        self.assertContentEqual(
+            [ws_arm], ws_archive.enabled_restricted_families)
+
+    def test_enableRestrictedFamily_owner(self):
+        """A new family can be added to the enabled restricted set.
+
+        An unauthorized user, even the archive owner, is not allowed.
+        """
+        self.ws_version = 'devel'
+        archive = self.factory.makeArchive()
+        transaction.commit()
+        ws_arm = self.service.processor_families.getByName(name='arm')
+        ws_archive = self.wsObject(archive, user=archive.owner)
+        self.assertContentEqual([], ws_archive.enabled_restricted_families)
+        expected_re = (
+            "(.|\n)*'launchpad\.Commercial'(.|\n)*")
+        with ExpectedException(LRUnauthorized, expected_re):
+            ws_archive.enableRestrictedFamily(family=ws_arm)
+
+    def test_enableRestrictedFamily_nonPrivUser(self):
+        """A new family can be added to the enabled restricted set.
+
+        An unauthorized user, some regular user, is not allowed.
+        """
+        self.ws_version = 'devel'
+        archive = self.factory.makeArchive()
+        just_some_guy = self.factory.makePerson()
+        transaction.commit()
+        ws_arm = self.service.processor_families.getByName(name='arm')
+        ws_archive = self.wsObject(archive, user=just_some_guy)
+        self.assertContentEqual([], ws_archive.enabled_restricted_families)
+        expected_re = (
+            "(.|\n)*'launchpad\.Commercial'(.|\n)*")
+        with ExpectedException(LRUnauthorized, expected_re):
+            ws_archive.enableRestrictedFamily(family=ws_arm)
+
+    def test_defaultCollection(self):
+        """getRestricted will return all of the restricted families."""
+        self.ws_version = 'devel'
+        ws_arm = self.service.processor_families.getByName(name='arm')
+        self.assertContentEqual(
+            [ws_arm], self.service.processor_families)
+
+
 def test_suite():
     return unittest.TestLoader().loadTestsFromName(__name__)

=== modified file 'lib/lp/soyuz/configure.zcml'
--- lib/lp/soyuz/configure.zcml	2011-06-15 02:41:34 +0000
+++ lib/lp/soyuz/configure.zcml	2011-06-23 16:21:19 +0000
@@ -399,6 +399,7 @@
             set_attributes="description displayname publish status"/>
         <require
             permission="launchpad.Commercial"
+	    interface="lp.soyuz.interfaces.archive.IArchiveCommercial"
             set_attributes="authorized_size build_debug_symbols buildd_secret
                             commercial enabled_restricted_families
                             external_dependencies private

=== modified file 'lib/lp/soyuz/interfaces/archive.py'
--- lib/lp/soyuz/interfaces/archive.py	2011-06-16 15:13:51 +0000
+++ lib/lp/soyuz/interfaces/archive.py	2011-06-23 16:21:19 +0000
@@ -23,6 +23,7 @@
     'FULL_COMPONENT_SUPPORT',
     'IArchive',
     'IArchiveAppend',
+    'IArchiveCommercial',
     'IArchiveEdit',
     'IArchiveView',
     'IArchiveEditDependenciesForm',
@@ -100,7 +101,6 @@
 from lp.soyuz.enums import ArchivePurpose
 from lp.soyuz.interfaces.buildrecords import IHasBuildRecords
 from lp.soyuz.interfaces.component import IComponent
-from lp.soyuz.interfaces.processor import IProcessorFamily
 
 
 @error_status(httplib.BAD_REQUEST)
@@ -433,13 +433,16 @@
             "context build.\n"
             "NOTE: This is for migration of OEM PPAs only!")))
 
-    enabled_restricted_families = CollectionField(
+    enabled_restricted_families = exported(
+        CollectionField(
             title=_("Enabled restricted families"),
             description=_(
                 "The restricted architecture families on which the archive "
                 "can build."),
-            value_type=Reference(schema=IProcessorFamily),
-            readonly=False)
+            value_type=Reference(schema=Interface),
+            # Really IProcessorFamily.
+            readonly=True),
+        as_of='devel')
 
     commercial = exported(
         Bool(
@@ -1506,7 +1509,24 @@
         """
 
 
-class IArchive(IArchivePublic, IArchiveAppend, IArchiveEdit, IArchiveView):
+class IArchiveCommercial(Interface):
+    """Archive interface for operations restricted by commercial."""
+
+    @operation_parameters(
+        family=Reference(schema=Interface, required=True),
+        # Really IProcessorFamily.
+    )
+    @export_write_operation()
+    @operation_for_version('devel')
+    def enableRestrictedFamily(family):
+        """Add the processor family to the set of enabled restricted families.
+
+        :param family: is an `IProcessorFamily` object.
+        """
+
+
+class IArchive(IArchivePublic, IArchiveAppend, IArchiveEdit, IArchiveView,
+               IArchiveCommercial):
     """Main Archive interface."""
     export_as_webservice_entry()
 

=== modified file 'lib/lp/soyuz/interfaces/processor.py'
--- lib/lp/soyuz/interfaces/processor.py	2010-08-20 20:31:18 +0000
+++ lib/lp/soyuz/interfaces/processor.py	2011-06-23 16:21:19 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=E0211,E0213
@@ -17,27 +17,101 @@
     Attribute,
     Interface,
     )
-from zope.schema import Bool
+from zope.schema import (
+    Bool,
+    Text,
+    TextLine,
+    )
 
 from canonical.launchpad import _
+from lazr.restful.declarations import (
+    collection_default_content,
+    export_as_webservice_collection,
+    export_as_webservice_entry,
+    export_read_operation,
+    exported,
+    operation_for_version,
+    operation_parameters,
+    operation_returns_entry,
+    )
+from lazr.restful.fields import (
+    CollectionField,
+    Reference,
+    )
 
 
 class IProcessor(Interface):
     """The SQLObject Processor Interface"""
+
+    # XXX: BradCrittenden 2011-06-20 bug=760849: The following use of 'beta'
+    # is a work-around to allow the WADL to be generated.  It is a bald-faced
+    # lie, though.  The class is being exported in 'devel' but in order to get
+    # the WADL generation work it must be back-dated to the earliest version.
+    # Note that individual attributes and methods can and must truthfully set
+    # 'devel' as their version.
+    export_as_webservice_entry(publish_web_link=True, as_of='beta')
     id = Attribute("The Processor ID")
-    family = Attribute("The Processor Family Reference")
-    name = Attribute("The Processor Name")
-    title = Attribute("The Processor Title")
-    description = Attribute("The Processor Description")
+    family = exported(
+        Reference(
+            schema=Interface,
+            # Really IProcessorFamily.
+            required=True, readonly=True,
+            title=_("Processor Family"),
+            description=_("The Processor Family Reference")),
+        as_of='devel', readonly=True)
+    name = exported(
+        TextLine(title=_("Name"),
+                 description=_("The Processor Name")),
+        as_of='devel', readonly=True)
+    title = exported(
+        TextLine(title=_("Title"),
+                 description=_("The Processor Title")),
+        as_of='devel', readonly=True)
+    description = exported(
+        Text(title=_("Description"),
+             description=_("The Processor Description")),
+        as_of='devel', readonly=True)
+
 
 class IProcessorFamily(Interface):
     """The SQLObject ProcessorFamily Interface"""
+
+    # XXX: BradCrittenden 2011-06-20 bug=760849: The following use of 'beta'
+    # is a work-around to allow the WADL to be generated.  It is a bald-faced
+    # lie, though.  The class is being exported in 'devel' but in order to get
+    # the WADL generation work it must be back-dated to the earliest version.
+    # Note that individual attributes and methods can and must truthfully set
+    # 'devel' as their version.
+    export_as_webservice_entry(
+        plural_name='processor_families',
+        publish_web_link=True,
+        as_of='beta')
+
     id = Attribute("The ProcessorFamily ID")
-    name = Attribute("The Processor Family Name")
-    title = Attribute("The Processor Family Title")
-    description = Attribute("The Processor Name Description")
-    processors = Attribute("The Processors in this family.")
-    restricted = Bool(title=_("Whether this family is restricted."))
+    name = exported(
+        TextLine(
+            title=_("Name"),
+            description=_("The Processor Family Name")),
+        as_of='devel', readonly=True)
+    title = exported(
+        TextLine(
+            title=_("Title"),
+            description=_("The Processor Family Title")),
+        as_of='devel', readonly=True)
+    description = exported(
+        Text(
+            title=_("Description"),
+            description=_("The Processor Name Description")),
+        as_of='devel', readonly=True)
+    processors = exported(
+        CollectionField(
+            title=_("Processors"),
+            description=_("The Processors in this family."),
+            value_type=Reference(IProcessor)),
+        as_of='devel', readonly=True)
+    restricted = exported(
+        Bool(title=_("Whether this family is restricted.")),
+        as_of='devel', readonly=True)
 
     def addProcessor(name, title, description):
         """Add a new processor to this family.
@@ -48,9 +122,17 @@
         :return: A `IProcessor`
         """
 
+
 class IProcessorFamilySet(Interface):
     """Operations related to ProcessorFamily instances."""
 
+    export_as_webservice_collection(Interface)
+
+    @operation_parameters(
+        name=TextLine(required=True))
+    @operation_returns_entry(Interface)
+    @export_read_operation()
+    @operation_for_version('devel')
     def getByName(name):
         """Return the ProcessorFamily instance with the matching name.
 
@@ -59,6 +141,7 @@
         :return: A `IProcessorFamily` instance if found, None otherwise.
         """
 
+    @collection_default_content()
     def getRestricted():
         """Return a sequence of all restricted architectures.
 

=== modified file 'lib/lp/soyuz/interfaces/webservice.py'
--- lib/lp/soyuz/interfaces/webservice.py	2011-06-16 14:56:55 +0000
+++ lib/lp/soyuz/interfaces/webservice.py	2011-06-23 16:21:19 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """All the interfaces that are exposed through the webservice.
@@ -32,6 +32,9 @@
     'IPackageUpload',
     'IPackageset',
     'IPackagesetSet',
+    'IProcessor',
+    'IProcessorFamily',
+    'IProcessorFamilySet',
     'ISourcePackagePublishingHistory',
     'IncompatibleArguments',
     'InsufficientUploadRights',
@@ -89,12 +92,28 @@
     IPackagesetSet,
     NoSuchPackageSet,
     )
+from lp.soyuz.interfaces.processor import (
+    IProcessor,
+    IProcessorFamily,
+    IProcessorFamilySet,
+    )
 from lp.soyuz.interfaces.publishing import (
     IBinaryPackagePublishingHistory,
     ISourcePackagePublishingHistory,
     )
 from lp.soyuz.interfaces.queue import IPackageUpload
+
+from canonical.launchpad.components.apihelpers import (
+    patch_entry_return_type,
+    )
+
 # XXX: JonathanLange 2010-11-09 bug=673083: Legacy work-around for circular
 # import bugs.  Break this up into a per-package thing.
 from canonical.launchpad.interfaces import _schema_circular_imports
 _schema_circular_imports
+
+from lazr.restful.declarations import LAZR_WEBSERVICE_EXPORTED
+IProcessorFamilySet.queryTaggedValue(
+    LAZR_WEBSERVICE_EXPORTED)['collection_entry_schema'] = IProcessorFamily
+
+patch_entry_return_type(IProcessorFamilySet, 'getByName', IProcessorFamily)

=== modified file 'lib/lp/soyuz/model/archive.py'
--- lib/lp/soyuz/model/archive.py	2011-06-21 17:22:38 +0000
+++ lib/lp/soyuz/model/archive.py	2011-06-23 16:21:19 +0000
@@ -1828,6 +1828,12 @@
     enabled_restricted_families = property(_getEnabledRestrictedFamilies,
                                            _setEnabledRestrictedFamilies)
 
+    def enableRestrictedFamily(self, family):
+        """See `IArchive`."""
+        restricted = set(self.enabled_restricted_families)
+        restricted.add(family)
+        self.enabled_restricted_families = restricted
+
     @classmethod
     def validatePPA(self, person, proposed_name):
         ubuntu = getUtility(ILaunchpadCelebrities).ubuntu

=== modified file 'lib/lp/soyuz/stories/webservice/xx-archive.txt'
--- lib/lp/soyuz/stories/webservice/xx-archive.txt	2011-06-16 20:12:00 +0000
+++ lib/lp/soyuz/stories/webservice/xx-archive.txt	2011-06-23 16:21:19 +0000
@@ -32,6 +32,26 @@
     signing_key_fingerprint: None
     web_link: u'http://launchpad.../~cprov/+archive/ppa'
 
+For "devel" additional attributes are available.
+
+    >>> cprov_archive_devel = webservice.get("/~cprov/+archive/ppa", api_version='devel').jsonBody()
+    >>> pprint_entry(cprov_archive_devel)
+    commercial: False
+    dependencies_collection_link: u'http://.../~cprov/+archive/ppa/dependencies'
+    description: u'packages to help my friends.'
+    displayname: u'PPA for Celso Providelo'
+    distribution_link: u'http://.../ubuntu'
+    enabled_restricted_families_collection_link: u'http://.../~cprov/+archive/ppa/enabled_restricted_families'
+    external_dependencies: None
+    name: u'ppa'
+    owner_link: u'http://.../~cprov'
+    private: False
+    require_virtualized: True
+    resource_type_link: u'http://.../#archive'
+    self_link: u'http://.../~cprov/+archive/ppa'
+    signing_key_fingerprint: None
+    web_link: u'http://launchpad.../~cprov/+archive/ppa'
+
 While the Archive signing key is being generated its
 'signing_key_fingerprint' attribute is None.
 

=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py	2011-06-21 14:04:50 +0000
+++ lib/lp/testing/__init__.py	2011-06-23 16:21:19 +0000
@@ -894,7 +894,7 @@
 
         :param obj: The object to find the launchpadlib equivalent of.
         :param user: The user to use for accessing the object over
-            lauchpadlib.  Defaults to an arbitrary logged-in user.
+            launchpadlib.  Defaults to an arbitrary logged-in user.
         """
         if user is not None:
             service = self.factory.makeLaunchpadService(