Merge lp:~allenap/launchpad/dd-initseries-bug-727105-derivation-vocab into lp:launchpad

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 12705
Proposed branch: lp:~allenap/launchpad/dd-initseries-bug-727105-derivation-vocab
Merge into: lp:launchpad
Diff against target: 516 lines (+369/-12)
7 files modified
lib/lp/registry/adapters.py (+1/-2)
lib/lp/registry/tests/test_adapters.py (+36/-0)
lib/lp/registry/tests/test_distroseries_vocabularies.py (+183/-0)
lib/lp/registry/vocabularies.py (+120/-5)
lib/lp/registry/vocabularies.zcml (+12/-0)
lib/lp/testing/factory.py (+9/-5)
lib/lp/testing/tests/test_factory.py (+8/-0)
To merge this branch: bzr merge lp:~allenap/launchpad/dd-initseries-bug-727105-derivation-vocab
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+52471@code.launchpad.net

Description of the change

This branch adds a named vocabulary ("DistroSeriesDerivation") that
contains the set of distroseries that the context distroseries or
distribution can derive from.

It also fixes adapters for IProductSeries -> IProduct and
IDistroSeries -> IDistribution. The code already exists in the tree
but was not registered or tested.

Support for dir() has also been added to LaunchpadObjectFactory to
help out interactive shenanigans.

This branch will not be committed; it is part of a larger change.

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

I don't know why you want to adapt a ProductSeries to a Product or a DistroSeries to a Distribution. It seems weird to me. I don't find any explanation of that in your cover letter. It would be helpful if you used our standard review template-- I'd expect to find this in the "implementation details" section.

With the adapters you've registered, wouldn't IDistribution.providedBy(distro_series) return True? If so, I think the tests are not testing whether the distroseries has been converted to a distribution. Instead of IDistribution.providedBy(distribution), you could do assertEqual(distribution, distro_series.distribution).

I think the comment on LaunchpadObjectFactory.__dir__ is too narrow. While it is helpful to interactive users, it can be used in other ways.

I thought distroseries were generally ordered by version or codename. Why is DistroSeriesDerivationVocabularyFactory using creation date instead?

Getting a list of distroseries by doing set(distribution) is also pretty wacky. I would have expected set(distribution) to fail, because distributions don't seem like they should be iterable.

review: Needs Information
Revision history for this message
Gavin Panella (allenap) wrote :

Hi Aaron,

Thanks for looking at this. In answering your questions below I
discovered a situation I had not tested. I've remedied that, and also
managed to make the code a bit tighter, requiring fewer queries. It
seems I wasn't thinking very well while coding this branch.

> I don't know why you want to adapt a ProductSeries to a Product or a
> DistroSeries to a Distribution. It seems weird to me. I don't find any
> explanation of that in your cover letter. It would be helpful if you used our
> standard review template-- I'd expect to find this in the "implementation
> details" section.

It's a convenient way of getting a IDistribution context from anything
that wants to register for it. Having said that, I think this
vocabulary is only ever needed in an IDistroSeries context, so I
/could/ remove the adaption in the vocabulary and thus the adapters.

Also, note I didn't actually write the adapters; the code already
existed and I just registered it. Not sure what it was doing there to
be honest, but it suited my needs (and I did productseries_to_product
for completeness).

Something that factors into my reasoning is my (mild) dislike of:

    if IOneInterface.providedBy(an_argument):
        do_something()
    elif IOtherInterface.providedBy(an_argument):
        do_something_else()
    ...

when we have a component system all ready and running, although I do
admit that it can be expedient to just do things this way.

> With the adapters you've registered, wouldn't
> IDistribution.providedBy(distro_series) return True? If so, I think the tests
> are not testing whether the distroseries has been converted to a distribution.
> Instead of IDistribution.providedBy(distribution), you could do
> assertEqual(distribution, distro_series.distribution).

IDistribution.providedBy(distro_series) is not true. At least, it
shouldn't be if I understand correctly!

I've added the tests you suggest, but maybe I'll end up killing the
adapters altogether.

> I think the comment on LaunchpadObjectFactory.__dir__ is too narrow. While it
> is helpful to interactive users, it can be used in other ways.

I've changed it to:

        """Enumerate the attributes and methods of the wrapped object factory.

        This is especially useful for interactive users."""

Is that the kind of thing you were thinking of?

> I thought distroseries were generally ordered by version or codename. Why is
> DistroSeriesDerivationVocabularyFactory using creation date instead?

I've ordered it the same as DistroSeriesVocabulary, which seems to be
used quite widely.

> Getting a list of distroseries by doing set(distribution) is also pretty
> wacky. I would have expected set(distribution) to fail, because distributions
> don't seem like they should be iterable.

Yeah, it does read a bit oddly. I'm not the author (I think...) of
Distribution.__iter__() so I can't say why that was decided. I've
spent far too long on this branch as it is to revisit that decision,
so do you mind if I leave it as it is?

Revision history for this message
Aaron Bentley (abentley) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 11-03-11 12:57 PM, Gavin Panella wrote:
>> I don't know why you want to adapt a ProductSeries to a Product or a
>> DistroSeries to a Distribution. It seems weird to me. I don't find any
>> explanation of that in your cover letter. It would be helpful if you used our
>> standard review template-- I'd expect to find this in the "implementation
>> details" section.
>
> It's a convenient way of getting a IDistribution context from anything
> that wants to register for it.

That's a definition of an adapter, but it doesn't explain the motivation
for providing this adaptation.

> Having said that, I think this
> vocabulary is only ever needed in an IDistroSeries context, so I
> /could/ remove the adaption in the vocabulary and thus the adapters.

That would seem like a more straightforward way of doing things.

> Also, note I didn't actually write the adapters; the code already
> existed and I just registered it.

Instead of going "We haven't used it" and deleting it? :-)

> Not sure what it was doing there to
> be honest, but it suited my needs (and I did productseries_to_product
> for completeness).
>
> Something that factors into my reasoning is my (mild) dislike of:
>
> if IOneInterface.providedBy(an_argument):
> do_something()
> elif IOtherInterface.providedBy(an_argument):
> do_something_else()

I share that dislike, but I don't see how that applies to this context.

This is a situation where you're passing in objects of the wrong type
and adapting them to objects of the correct type. Why not just pass in
objects of the correct type?

> IDistribution.providedBy(distro_series) is not true. At least, it
> shouldn't be if I understand correctly!

I have checked, by downloading your branch and tweaking the test, and
you're right, it doesn't.

> I've changed it to:
>
> """Enumerate the attributes and methods of the wrapped object factory.
>
> This is especially useful for interactive users."""
>
> Is that the kind of thing you were thinking of?

Sure, that's good.

>> Getting a list of distroseries by doing set(distribution) is also pretty
>> wacky. I would have expected set(distribution) to fail, because distributions
>> don't seem like they should be iterable.
>
> Yeah, it does read a bit oddly. I'm not the author (I think...) of
> Distribution.__iter__() so I can't say why that was decided.

Distribution.__iter__ is implemented in terms of Distribution.series, so
I recommend using that instead.

> I've
> spent far too long on this branch as it is to revisit that decision,
> so do you mind if I leave it as it is?

I think switching your client code to use Distribution.series is a very
small hardship, so I'd prefer if you changed it. I didn't mean that you
should remove Distribution.__iter__.

Aaron
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.10 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAk16d5IACgkQ0F+nu1YWqI2rXACfX367xjl2QOscPS0Iv+8DKaFU
sBkAnAzeL6esmFNZd/FxI1VqO3rAp9c6
=PCpH
-----END PGP SIGNATURE-----

Revision history for this message
Gavin Panella (allenap) wrote :

> > It's a convenient way of getting a IDistribution context from anything
> > that wants to register for it.
>
> That's a definition of an adapter, but it doesn't explain the motivation
> for providing this adaptation.

I don't see the harm in doing it, but that's perhaps because I've not
really experienced a situation where this kind of thing has led to
problems later on.

>
> > Having said that, I think this vocabulary is only ever needed in
> > an IDistroSeries context, so I /could/ remove the adaption in the
> > vocabulary and thus the adapters.
>
> That would seem like a more straightforward way of doing things.

All the vocabulary needs is a distribution to function, yet it will
currently only be used in a distroseries context. It seems strange to
require a distroseries when I could just say "give me anything that
can be adapted to a distribution".

Anyway, I've made it work only for distroseries now.

>
> > Also, note I didn't actually write the adapters; the code already
> > existed and I just registered it.
>
> Instead of going "We haven't used it" and deleting it? :-)

I was wrong; that code is actually used as the basis for other
adapters.

>
> > Not sure what it was doing there to
> > be honest, but it suited my needs (and I did productseries_to_product
> > for completeness).
> >
> > Something that factors into my reasoning is my (mild) dislike of:
> >
> > if IOneInterface.providedBy(an_argument):
> > do_something()
> > elif IOtherInterface.providedBy(an_argument):
> > do_something_else()
>
> I share that dislike, but I don't see how that applies to this context.

If I don't use adapters but want to keep the ability to work in a
distribution or distroseries context I can't really see a way around
the IFoo.providedBy(...) stuff.

Well, I could getattr(context, "distribution", context) but I suspect that
might an order of magnitude worse again :)

>
> This is a situation where you're passing in objects of the wrong type
> and adapting them to objects of the correct type. Why not just pass in
> objects of the correct type?

Because it's a named vocabulary. You get the context in which it is
used. I don't think there is an opportunity to select what gets
passed.

> Distribution.__iter__ is implemented in terms of Distribution.series, so
> I recommend using that instead.

Done.

Thanks again!

1=== modified file 'lib/lp/registry/tests/test_distroseries_vocabularies.py'
2--- lib/lp/registry/tests/test_distroseries_vocabularies.py 2011-03-11 17:29:36 +0000
3+++ lib/lp/registry/tests/test_distroseries_vocabularies.py 2011-03-14 14:58:34 +0000
4@@ -51,28 +51,20 @@
5 def test_interfaces(self):
6 # DistroSeriesDerivationVocabularyFactory instances provide
7 # IVocabulary and IVocabularyTokenized.
8- distribution = self.factory.makeDistribution()
9- vocabulary = self.vocabulary_factory(distribution)
10+ distroseries = self.factory.makeDistroSeries()
11+ vocabulary = self.vocabulary_factory(distroseries)
12 self.assertProvides(vocabulary, IVocabulary)
13 self.assertProvides(vocabulary, IVocabularyTokenized)
14
15- def test_distribution_without_series(self):
16- # Given a distribution without any series, the vocabulary factory
17- # returns a vocabulary for all distroseries in all distributions.
18- distribution = self.factory.makeDistribution()
19- vocabulary = self.vocabulary_factory(distribution)
20- expected_distroseries = set(self.all_distroseries)
21- observed_distroseries = set(term.value for term in vocabulary)
22- self.assertEqual(expected_distroseries, observed_distroseries)
23-
24 def test_distribution_with_non_derived_series(self):
25 # Given a distribution with series, none of which are derived, the
26 # vocabulary factory returns a vocabulary for all distroseries in all
27 # distributions *except* the given distribution.
28 distroseries = self.factory.makeDistroSeries()
29- vocabulary = self.vocabulary_factory(distroseries.distribution)
30+ vocabulary = self.vocabulary_factory(distroseries)
31 expected_distroseries = (
32- set(self.all_distroseries) - set(distroseries.distribution))
33+ set(self.all_distroseries).difference(
34+ distroseries.distribution.series))
35 observed_distroseries = set(term.value for term in vocabulary)
36 self.assertEqual(expected_distroseries, observed_distroseries)
37
38@@ -86,8 +78,8 @@
39 distribution=parent_distroseries.distribution)
40 distroseries = self.factory.makeDistroSeries(
41 parent_series=parent_distroseries)
42- vocabulary = self.vocabulary_factory(distroseries.distribution)
43- expected_distroseries = set(parent_distroseries.distribution)
44+ vocabulary = self.vocabulary_factory(distroseries)
45+ expected_distroseries = set(parent_distroseries.distribution.series)
46 observed_distroseries = set(term.value for term in vocabulary)
47 self.assertEqual(expected_distroseries, observed_distroseries)
48
49@@ -100,9 +92,10 @@
50 distroseries = self.factory.makeDistroSeries(
51 distribution=parent_distroseries.distribution,
52 parent_series=parent_distroseries)
53- vocabulary = self.vocabulary_factory(distroseries.distribution)
54+ vocabulary = self.vocabulary_factory(distroseries)
55 expected_distroseries = (
56- set(self.all_distroseries) - set(distroseries.distribution))
57+ set(self.all_distroseries).difference(
58+ distroseries.distribution.series))
59 observed_distroseries = set(term.value for term in vocabulary)
60 self.assertEqual(expected_distroseries, observed_distroseries)
61
62@@ -112,7 +105,8 @@
63 distroseries = self.factory.makeDistroSeries()
64 vocabulary = self.vocabulary_factory(distroseries)
65 expected_distroseries = (
66- set(self.all_distroseries) - set(distroseries.distribution))
67+ set(self.all_distroseries).difference(
68+ distroseries.distribution.series))
69 observed_distroseries = set(term.value for term in vocabulary)
70 self.assertEqual(expected_distroseries, observed_distroseries)
71
72@@ -140,8 +134,9 @@
73 removeSecurityProxy(bbb_series_newer).date_created = two_days_ago
74
75 ccc = self.factory.makeDistribution(displayname="ccc")
76+ ccc_series = self.factory.makeDistroSeries(distribution=ccc)
77
78- vocabulary = self.vocabulary_factory(ccc)
79+ vocabulary = self.vocabulary_factory(ccc_series)
80 expected_distroseries = [
81 aaa_series_newer, aaa_series_older,
82 bbb_series_newer, bbb_series_older]
83@@ -154,27 +149,20 @@
84 if series in expected_distroseries]
85 self.assertEqual(expected_distroseries, observed_distroseries)
86
87- def test_queries_for_distribution_without_series(self):
88- for index in range(10):
89- self.factory.makeDistroSeries()
90- distribution = self.factory.makeDistribution()
91- flush_database_caches()
92- # Getting terms issues two queries: one for the distribution's
93- # serieses and a second for all serieses.
94- with StormStatementRecorder() as recorder:
95- self.vocabulary_factory(distribution).terms
96- self.assertThat(recorder, HasQueryCount(Equals(2)))
97-
98 def test_queries_for_distribution_with_non_derived_series(self):
99 for index in range(10):
100 self.factory.makeDistroSeries()
101 distribution = self.factory.makeDistribution()
102- self.factory.makeDistroSeries(distribution=distribution)
103+ distroseries = self.factory.makeDistroSeries(
104+ distribution=distribution)
105 flush_database_caches()
106+ # Reload distroseries and distribution; these will reasonably already
107+ # be loaded before using the vocabulary.
108+ distroseries.distribution
109 # Getting terms issues two queries: one to search for parent serieses
110 # (of which there are none) and a second for all serieses.
111 with StormStatementRecorder() as recorder:
112- self.vocabulary_factory(distribution).terms
113+ self.vocabulary_factory(distroseries).terms
114 self.assertThat(recorder, HasQueryCount(Equals(2)))
115
116 def test_queries_for_distribution_with_derived_series(self):
117@@ -182,11 +170,14 @@
118 self.factory.makeDistroSeries()
119 distribution = self.factory.makeDistribution()
120 parent_distroseries = self.factory.makeDistroSeries()
121- self.factory.makeDistroSeries(
122+ distroseries = self.factory.makeDistroSeries(
123 parent_series=parent_distroseries,
124 distribution=distribution)
125 flush_database_caches()
126+ # Reload distroseries and distribution; these will reasonably already
127+ # be loaded before using the vocabulary.
128+ distroseries.distribution
129 # Getting terms issues one query to find parent serieses.
130 with StormStatementRecorder() as recorder:
131- self.vocabulary_factory(distribution).terms
132+ self.vocabulary_factory(distroseries).terms
133 self.assertThat(recorder, HasQueryCount(Equals(1)))
134
135=== modified file 'lib/lp/registry/vocabularies.py'
136--- lib/lp/registry/vocabularies.py 2011-03-11 17:29:36 +0000
137+++ lib/lp/registry/vocabularies.py 2011-03-14 14:36:36 +0000
138@@ -1500,7 +1500,8 @@
139
140 def __init__(self, context):
141 """See `IVocabularyFactory.__call__`."""
142- self.context = context
143+ assert IDistroSeries.providedBy(context)
144+ self.distribution = context.distribution
145
146 def find_terms(self, *where):
147 """Return a `tuple` of terms matching the given criteria.
148@@ -1525,19 +1526,18 @@
149
150 The order is the same as for `DistroSeriesVocabulary`.
151 """
152- distribution = IDistribution(self.context)
153 parent = ClassAlias(DistroSeries, "parent")
154 child = ClassAlias(DistroSeries, "child")
155 parent_distributions = Select(
156 parent.distributionID, And(
157- parent.distributionID != distribution.id,
158- child.distributionID == distribution.id,
159+ parent.distributionID != self.distribution.id,
160+ child.distributionID == self.distribution.id,
161 child.parent_seriesID == parent.id))
162 terms = self.find_terms(
163 DistroSeries.distributionID.is_in(parent_distributions))
164 if len(terms) == 0:
165 terms = self.find_terms(
166- DistroSeries.distribution != distribution)
167+ DistroSeries.distribution != self.distribution)
168 return terms
169
170 @cachedproperty
Revision history for this message
Robert Collins (lifeless) wrote :

On Tue, Mar 15, 2011 at 4:12 AM, Gavin Panella
<email address hidden> wrote:
>> >     if IOneInterface.providedBy(an_argument):
>> >         do_something()
>> >     elif IOtherInterface.providedBy(an_argument):
>> >         do_something_else()
>>
>> I share that dislike, but I don't see how that applies to this context.
>
> If I don't use adapters but want to keep the ability to work in a
> distribution or distroseries context I can't really see a way around
> the IFoo.providedBy(...) stuff.
>
> Well, I could getattr(context, "distribution", context) but I suspect that
> might an order of magnitude worse again :)

There are a few general patterns that can be used here:
 - delegation:
   an_argument.do_something() -> trust it will dtrt.
   an_argument can have a range of techniques to have do_something
implemented appropropriately.
 - multiple dispatch: register (in advance) how you want to dispatch
things, and then dispatch off a tuple of types rather than just the
type of an_argument:
   do_something(an_argument, self) -> will handle the type consideration for you
 - adaption - a primitive form of multiple dispatch - cast or acquire
into a known type and then call a method on it:
 IPillar(an_argument).do_something
 - case statements: the thing we all dislike :)

adaption makes me nervous unless there is /clearly/ and /forevermore/
One Sensible Answer : and distroseries -> distribution violates the
second condition: for a derived distribution, there are multiple
reasonable answers to 'I need a distribution for this distroseries' -
there is .distribution, and there is .parent_series.distribution.

Generally speaking, I reach for delegation as the first tool to hand
in these sorts of situations, though it may not be the right tool for
this specific situation.

HTH,
Rob

Revision history for this message
Gavin Panella (allenap) wrote :

> adaption makes me nervous unless there is /clearly/ and
> /forevermore/ One Sensible Answer

I like this rule; I'll try and keep it in mind. Thanks.

Revision history for this message
Aaron Bentley (abentley) wrote :

Sorry for the delay. I thought Graham had approved it, but that was something else.

I like the change as far as it goes, but it seems you've retained the adaptation even though now it's no longer needed by your vocabulary. If that was an oversight, consider this approved. Otherwise, I'd like to understand why.

Revision history for this message
Gavin Panella (allenap) wrote :

The adaption has gone, but I left in a couple of trivial-ish tests for distroseries_to_distribution() and productseries_to_product(). That code no longer has anything else to do with this branch, but they are still valid tests of code that isn't otherwise tested directly.

Revision history for this message
Aaron Bentley (abentley) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/adapters.py'
2--- lib/lp/registry/adapters.py 2011-01-04 16:08:57 +0000
3+++ lib/lp/registry/adapters.py 2011-03-30 16:10:58 +0000
4@@ -6,8 +6,7 @@
5 __metaclass__ = type
6
7 __all__ = [
8- 'distroseries_to_launchpadusage',
9- 'distroseries_to_serviceusage',
10+ 'distroseries_to_distribution',
11 'PollSubset',
12 'productseries_to_product',
13 ]
14
15=== added file 'lib/lp/registry/tests/test_adapters.py'
16--- lib/lp/registry/tests/test_adapters.py 1970-01-01 00:00:00 +0000
17+++ lib/lp/registry/tests/test_adapters.py 2011-03-30 16:10:58 +0000
18@@ -0,0 +1,36 @@
19+# Copyright 2011 Canonical Ltd. This software is licensed under the
20+# GNU Affero General Public License version 3 (see the file LICENSE).
21+
22+"""Tests for adapters."""
23+
24+__metaclass__ = type
25+
26+from canonical.testing.layers import DatabaseFunctionalLayer
27+from lp.registry.adapters import (
28+ distroseries_to_distribution,
29+ productseries_to_product,
30+ )
31+from lp.registry.interfaces.distribution import IDistribution
32+from lp.registry.interfaces.product import IProduct
33+from lp.testing import TestCaseWithFactory
34+
35+
36+class TestAdapters(TestCaseWithFactory):
37+
38+ layer = DatabaseFunctionalLayer
39+
40+ def test_distroseries_to_distribution(self):
41+ # distroseries_to_distribution() returns an IDistribution given an
42+ # IDistroSeries.
43+ distro_series = self.factory.makeDistroSeries()
44+ distribution = distroseries_to_distribution(distro_series)
45+ self.assertTrue(IDistribution.providedBy(distribution))
46+ self.assertEqual(distribution, distro_series.distribution)
47+
48+ def test_productseries_to_product(self):
49+ # productseries_to_product() returns an IProduct given an
50+ # IProductSeries.
51+ product_series = self.factory.makeProductSeries()
52+ product = productseries_to_product(product_series)
53+ self.assertTrue(IProduct.providedBy(product))
54+ self.assertEqual(product, product_series.product)
55
56=== added file 'lib/lp/registry/tests/test_distroseries_vocabularies.py'
57--- lib/lp/registry/tests/test_distroseries_vocabularies.py 1970-01-01 00:00:00 +0000
58+++ lib/lp/registry/tests/test_distroseries_vocabularies.py 2011-03-30 16:10:58 +0000
59@@ -0,0 +1,183 @@
60+# Copyright 2011 Canonical Ltd. This software is licensed under the
61+# GNU Affero General Public License version 3 (see the file LICENSE).
62+
63+"""Tests for distroseries vocabularies in `lp.registry.vocabularies`."""
64+
65+__metaclass__ = type
66+
67+from datetime import (
68+ datetime,
69+ timedelta,
70+ )
71+
72+from pytz import utc
73+from testtools.matchers import Equals
74+from zope.component import getUtility
75+from zope.schema.interfaces import (
76+ IVocabulary,
77+ IVocabularyFactory,
78+ IVocabularyTokenized,
79+ )
80+from zope.security.proxy import removeSecurityProxy
81+
82+from canonical.database.sqlbase import flush_database_caches
83+from canonical.testing.layers import DatabaseFunctionalLayer
84+from lp.registry.interfaces.distroseries import IDistroSeriesSet
85+from lp.registry.vocabularies import DistroSeriesDerivationVocabularyFactory
86+from lp.testing import (
87+ StormStatementRecorder,
88+ TestCaseWithFactory,
89+ )
90+from lp.testing.matchers import HasQueryCount
91+
92+
93+class TestDistroSeriesDerivationVocabularyFactory(TestCaseWithFactory):
94+ """Tests for `DistroSeriesDerivationVocabularyFactory`."""
95+
96+ layer = DatabaseFunctionalLayer
97+
98+ def setUp(self):
99+ super(TestDistroSeriesDerivationVocabularyFactory, self).setUp()
100+ self.vocabulary_factory = DistroSeriesDerivationVocabularyFactory
101+ self.all_distroseries = getUtility(IDistroSeriesSet).search()
102+
103+ def test_registration(self):
104+ # DistroSeriesDerivationVocabularyFactory is registered as a named
105+ # utility for IVocabularyFactory.
106+ self.assertEqual(
107+ getUtility(IVocabularyFactory, name="DistroSeriesDerivation"),
108+ DistroSeriesDerivationVocabularyFactory)
109+
110+ def test_interfaces(self):
111+ # DistroSeriesDerivationVocabularyFactory instances provide
112+ # IVocabulary and IVocabularyTokenized.
113+ distroseries = self.factory.makeDistroSeries()
114+ vocabulary = self.vocabulary_factory(distroseries)
115+ self.assertProvides(vocabulary, IVocabulary)
116+ self.assertProvides(vocabulary, IVocabularyTokenized)
117+
118+ def test_distribution_with_non_derived_series(self):
119+ # Given a distribution with series, none of which are derived, the
120+ # vocabulary factory returns a vocabulary for all distroseries in all
121+ # distributions *except* the given distribution.
122+ distroseries = self.factory.makeDistroSeries()
123+ vocabulary = self.vocabulary_factory(distroseries)
124+ expected_distroseries = (
125+ set(self.all_distroseries).difference(
126+ distroseries.distribution.series))
127+ observed_distroseries = set(term.value for term in vocabulary)
128+ self.assertEqual(expected_distroseries, observed_distroseries)
129+
130+ def test_distribution_with_derived_series(self):
131+ # Given a distribution with series, one or more of which are derived,
132+ # the vocabulary factory returns a vocabulary for all distroseries of
133+ # the distribution from which the derived series have been derived.
134+ parent_distroseries = self.factory.makeDistroSeries()
135+ # Create a sibling to the parent.
136+ self.factory.makeDistroSeries(
137+ distribution=parent_distroseries.distribution)
138+ distroseries = self.factory.makeDistroSeries(
139+ parent_series=parent_distroseries)
140+ vocabulary = self.vocabulary_factory(distroseries)
141+ expected_distroseries = set(parent_distroseries.distribution.series)
142+ observed_distroseries = set(term.value for term in vocabulary)
143+ self.assertEqual(expected_distroseries, observed_distroseries)
144+
145+ def test_distribution_with_derived_series_of_self(self):
146+ # Given a distribution with series derived from other of its series
147+ # (which shouldn't happen), the vocabulary factory returns a
148+ # vocabulary for all distroseries in all distributions *except* the
149+ # given distribution.
150+ parent_distroseries = self.factory.makeDistroSeries()
151+ distroseries = self.factory.makeDistroSeries(
152+ distribution=parent_distroseries.distribution,
153+ parent_series=parent_distroseries)
154+ vocabulary = self.vocabulary_factory(distroseries)
155+ expected_distroseries = (
156+ set(self.all_distroseries).difference(
157+ distroseries.distribution.series))
158+ observed_distroseries = set(term.value for term in vocabulary)
159+ self.assertEqual(expected_distroseries, observed_distroseries)
160+
161+ def test_distroseries(self):
162+ # Given a distroseries, the vocabulary factory returns the vocabulary
163+ # the same as for its distribution.
164+ distroseries = self.factory.makeDistroSeries()
165+ vocabulary = self.vocabulary_factory(distroseries)
166+ expected_distroseries = (
167+ set(self.all_distroseries).difference(
168+ distroseries.distribution.series))
169+ observed_distroseries = set(term.value for term in vocabulary)
170+ self.assertEqual(expected_distroseries, observed_distroseries)
171+
172+ def test_ordering(self):
173+ # The vocabulary is sorted by distribution display name then by the
174+ # date the distroseries was created, newest first.
175+ now = datetime.now(utc)
176+ two_days_ago = now - timedelta(2)
177+ six_days_ago = now - timedelta(7)
178+
179+ aaa = self.factory.makeDistribution(displayname="aaa")
180+ aaa_series_older = self.factory.makeDistroSeries(
181+ name="aaa-series-older", distribution=aaa)
182+ removeSecurityProxy(aaa_series_older).date_created = six_days_ago
183+ aaa_series_newer = self.factory.makeDistroSeries(
184+ name="aaa-series-newer", distribution=aaa)
185+ removeSecurityProxy(aaa_series_newer).date_created = two_days_ago
186+
187+ bbb = self.factory.makeDistribution(displayname="bbb")
188+ bbb_series_older = self.factory.makeDistroSeries(
189+ name="bbb-series-older", distribution=bbb)
190+ removeSecurityProxy(bbb_series_older).date_created = six_days_ago
191+ bbb_series_newer = self.factory.makeDistroSeries(
192+ name="bbb-series-newer", distribution=bbb)
193+ removeSecurityProxy(bbb_series_newer).date_created = two_days_ago
194+
195+ ccc = self.factory.makeDistribution(displayname="ccc")
196+ ccc_series = self.factory.makeDistroSeries(distribution=ccc)
197+
198+ vocabulary = self.vocabulary_factory(ccc_series)
199+ expected_distroseries = [
200+ aaa_series_newer, aaa_series_older,
201+ bbb_series_newer, bbb_series_older]
202+ observed_distroseries = list(term.value for term in vocabulary)
203+ # observed_distroseries will contain distroseries from the sample
204+ # data, so we must only look at the set of distroseries we have
205+ # created.
206+ observed_distroseries = [
207+ series for series in observed_distroseries
208+ if series in expected_distroseries]
209+ self.assertEqual(expected_distroseries, observed_distroseries)
210+
211+ def test_queries_for_distribution_with_non_derived_series(self):
212+ for index in range(10):
213+ self.factory.makeDistroSeries()
214+ distribution = self.factory.makeDistribution()
215+ distroseries = self.factory.makeDistroSeries(
216+ distribution=distribution)
217+ flush_database_caches()
218+ # Reload distroseries and distribution; these will reasonably already
219+ # be loaded before using the vocabulary.
220+ distroseries.distribution
221+ # Getting terms issues two queries: one to search for parent serieses
222+ # (of which there are none) and a second for all serieses.
223+ with StormStatementRecorder() as recorder:
224+ self.vocabulary_factory(distroseries).terms
225+ self.assertThat(recorder, HasQueryCount(Equals(2)))
226+
227+ def test_queries_for_distribution_with_derived_series(self):
228+ for index in range(10):
229+ self.factory.makeDistroSeries()
230+ distribution = self.factory.makeDistribution()
231+ parent_distroseries = self.factory.makeDistroSeries()
232+ distroseries = self.factory.makeDistroSeries(
233+ parent_series=parent_distroseries,
234+ distribution=distribution)
235+ flush_database_caches()
236+ # Reload distroseries and distribution; these will reasonably already
237+ # be loaded before using the vocabulary.
238+ distroseries.distribution
239+ # Getting terms issues one query to find parent serieses.
240+ with StormStatementRecorder() as recorder:
241+ self.vocabulary_factory(distroseries).terms
242+ self.assertThat(recorder, HasQueryCount(Equals(1)))
243
244=== modified file 'lib/lp/registry/vocabularies.py'
245--- lib/lp/registry/vocabularies.py 2011-03-07 02:32:57 +0000
246+++ lib/lp/registry/vocabularies.py 2011-03-30 16:10:58 +0000
247@@ -31,6 +31,7 @@
248 'DistributionOrProductOrProjectGroupVocabulary',
249 'DistributionOrProductVocabulary',
250 'DistributionVocabulary',
251+ 'DistroSeriesDerivationVocabularyFactory',
252 'DistroSeriesVocabulary',
253 'FeaturedProjectVocabulary',
254 'FilteredDistroSeriesVocabulary',
255@@ -38,23 +39,23 @@
256 'KarmaCategoryVocabulary',
257 'MilestoneVocabulary',
258 'NonMergedPeopleAndTeamsVocabulary',
259+ 'person_team_participations_vocabulary_factory',
260 'PersonAccountToMergeVocabulary',
261 'PersonActiveMembershipVocabulary',
262 'ProductReleaseVocabulary',
263 'ProductSeriesVocabulary',
264 'ProductVocabulary',
265+ 'project_products_vocabulary_factory',
266 'ProjectGroupVocabulary',
267 'SourcePackageNameVocabulary',
268+ 'UserTeamsParticipationPlusSelfVocabulary',
269 'UserTeamsParticipationVocabulary',
270- 'UserTeamsParticipationPlusSelfVocabulary',
271 'ValidPersonOrClosedTeamVocabulary',
272 'ValidPersonOrTeamVocabulary',
273 'ValidPersonVocabulary',
274 'ValidTeamMemberVocabulary',
275 'ValidTeamOwnerVocabulary',
276 'ValidTeamVocabulary',
277- 'person_team_participations_vocabulary_factory',
278- 'project_products_vocabulary_factory',
279 ]
280
281
282@@ -73,11 +74,16 @@
283 LeftJoin,
284 Not,
285 Or,
286+ Select,
287 SQL,
288 )
289+from storm.info import ClassAlias
290 from zope.component import getUtility
291 from zope.interface import implements
292-from zope.schema.interfaces import IVocabularyTokenized
293+from zope.schema.interfaces import (
294+ IVocabulary,
295+ IVocabularyTokenized,
296+ )
297 from zope.schema.vocabulary import (
298 SimpleTerm,
299 SimpleVocabulary,
300@@ -1432,7 +1438,8 @@
301 for series in sorted(series, key=attrgetter('sortkey')):
302 yield self.toTerm(series)
303
304- def toTerm(self, obj):
305+ @staticmethod
306+ def toTerm(obj):
307 """See `IVocabulary`."""
308 # NB: We use '/' as the separator because '-' is valid in
309 # a distribution.name
310@@ -1474,6 +1481,114 @@
311 return objs
312
313
314+class DistroSeriesDerivationVocabularyFactory:
315+ """A vocabulary source for series to derive from.
316+
317+ Once a distribution has a series that has derived from a series in another
318+ distribution, all other derived series must also derive from a series in
319+ the same distribution.
320+
321+ A distribution can have non-derived series. Any of these can be changed to
322+ derived at a later date, but as soon as this happens, the above rule
323+ applies.
324+
325+ It is permissible for a distribution to have both derived and non-derived
326+ series at the same time.
327+ """
328+
329+ implements(IVocabulary, IVocabularyTokenized)
330+
331+ def __init__(self, context):
332+ """See `IVocabularyFactory.__call__`."""
333+ assert IDistroSeries.providedBy(context)
334+ self.distribution = context.distribution
335+
336+ def find_terms(self, *where):
337+ """Return a `tuple` of terms matching the given criteria.
338+
339+ The terms are returned in order. The `Distribution`s related to those
340+ terms are preloaded at the same time.
341+ """
342+ query = IStore(DistroSeries).find(
343+ (DistroSeries, Distribution),
344+ DistroSeries.distribution == Distribution.id,
345+ *where)
346+ query = query.order_by(
347+ Distribution.displayname,
348+ Desc(DistroSeries.date_created))
349+ return tuple(
350+ DistroSeriesVocabulary.toTerm(series)
351+ for (series, distribution) in query)
352+
353+ @cachedproperty
354+ def terms(self):
355+ """Terms for the series the context can derive from, in order.
356+
357+ The order is the same as for `DistroSeriesVocabulary`.
358+ """
359+ parent = ClassAlias(DistroSeries, "parent")
360+ child = ClassAlias(DistroSeries, "child")
361+ parent_distributions = Select(
362+ parent.distributionID, And(
363+ parent.distributionID != self.distribution.id,
364+ child.distributionID == self.distribution.id,
365+ child.parent_seriesID == parent.id))
366+ terms = self.find_terms(
367+ DistroSeries.distributionID.is_in(parent_distributions))
368+ if len(terms) == 0:
369+ terms = self.find_terms(
370+ DistroSeries.distribution != self.distribution)
371+ return terms
372+
373+ @cachedproperty
374+ def terms_by_value(self):
375+ """Mapping of terms by value."""
376+ return dict((term.value, term) for term in self.terms)
377+
378+ @cachedproperty
379+ def terms_by_token(self):
380+ """Mapping of terms by token."""
381+ return dict((term.token, term) for term in self.terms)
382+
383+ def __iter__(self):
384+ """Returns an iterator over the terms in the vocabulary.
385+
386+ See `IIterableVocabulary`.
387+ """
388+ return iter(self.terms)
389+
390+ def __len__(self):
391+ """The number of terms.
392+
393+ See `IIterableVocabulary`.
394+ """
395+ return len(self.terms)
396+
397+ def getTerm(self, value):
398+ """Return the `ITerm` object for the term 'value'.
399+
400+ See `IBaseVocabulary`.
401+ """
402+ try:
403+ return self.terms_by_value[value]
404+ except KeyError:
405+ raise LookupError(value)
406+
407+ def __contains__(self, value):
408+ """Return whether the value is available in this source.
409+
410+ See `ISource`.
411+ """
412+ return (value in self.terms_by_value)
413+
414+ def getTermByToken(self, token):
415+ """See `IVocabularyTokenized`."""
416+ try:
417+ return self.terms_by_token[token]
418+ except KeyError:
419+ raise LookupError(token)
420+
421+
422 class PillarVocabularyBase(NamedSQLObjectHugeVocabulary):
423 """Active `IPillar` objects vocabulary."""
424 displayname = 'Needs to be overridden'
425
426=== modified file 'lib/lp/registry/vocabularies.zcml'
427--- lib/lp/registry/vocabularies.zcml 2010-11-30 22:55:58 +0000
428+++ lib/lp/registry/vocabularies.zcml 2011-03-30 16:10:58 +0000
429@@ -30,6 +30,18 @@
430
431
432 <securedutility
433+ name="DistroSeriesDerivation"
434+ component=".vocabularies.DistroSeriesDerivationVocabularyFactory"
435+ provides="zope.schema.interfaces.IVocabularyFactory">
436+ <allow interface="zope.schema.interfaces.IVocabularyFactory"/>
437+ </securedutility>
438+
439+ <class class=".vocabularies.DistroSeriesDerivationVocabularyFactory">
440+ <allow interface="zope.schema.interfaces.IVocabularyTokenized"/>
441+ </class>
442+
443+
444+ <securedutility
445 name="FeaturedProject"
446 component="lp.registry.vocabularies.FeaturedProjectVocabulary"
447 provides="zope.schema.interfaces.IVocabularyFactory"
448
449=== modified file 'lib/lp/testing/factory.py'
450--- lib/lp/testing/factory.py 2011-03-28 20:54:50 +0000
451+++ lib/lp/testing/factory.py 2011-03-30 16:10:58 +0000
452@@ -36,19 +36,19 @@
453 isSequenceType,
454 )
455 import os
456-from pytz import UTC
457 from random import randint
458 from StringIO import StringIO
459 from textwrap import dedent
460 from threading import local
461-import transaction
462 from types import InstanceType
463 import warnings
464
465 from bzrlib.merge_directive import MergeDirective2
466 from bzrlib.plugins.builder.recipe import BaseRecipeBranch
467 import pytz
468+from pytz import UTC
469 import simplejson
470+import transaction
471 from twisted.python.util import mergeFunctionMetadata
472 from zope.component import (
473 ComponentLookupError,
474@@ -279,10 +279,8 @@
475 time_counter,
476 )
477 from lp.translations.enums import RosettaImportStatus
478-from lp.translations.interfaces.side import (
479- TranslationSide,
480- )
481 from lp.translations.interfaces.potemplate import IPOTemplateSet
482+from lp.translations.interfaces.side import TranslationSide
483 from lp.translations.interfaces.translationfileformat import (
484 TranslationFileFormat,
485 )
486@@ -3992,6 +3990,12 @@
487 else:
488 return attr
489
490+ def __dir__(self):
491+ """Enumerate the attributes and methods of the wrapped object factory.
492+
493+ This is especially useful for interactive users."""
494+ return dir(self._factory)
495+
496
497 def remove_security_proxy_and_shout_at_engineer(obj):
498 """Remove an object's security proxy and print a warning.
499
500=== modified file 'lib/lp/testing/tests/test_factory.py'
501--- lib/lp/testing/tests/test_factory.py 2010-12-02 16:13:51 +0000
502+++ lib/lp/testing/tests/test_factory.py 2011-03-30 16:10:58 +0000
503@@ -611,6 +611,14 @@
504 sequence='2000-1234', cvestate=CveStatus.DEPRECATED)
505 self.assertEqual(CveStatus.DEPRECATED, cve.status)
506
507+ # dir() support.
508+ def test_dir(self):
509+ # LaunchpadObjectFactory supports dir() even though all of its
510+ # attributes are pseudo-attributes.
511+ self.assertEqual(
512+ dir(self.factory._factory),
513+ dir(self.factory))
514+
515
516 class TestFactoryWithLibrarian(TestCaseWithFactory):
517