Merge lp:~allenap/launchpad/dd-initseries-bug-727105-derivation-vocab into lp:launchpad
- dd-initseries-bug-727105-derivation-vocab
- Merge into devel
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Aaron Bentley (community) | Approve | ||
Review via email: mp+52471@code.launchpad.net |
Commit message
Description of the change
This branch adds a named vocabulary ("DistroSeriesD
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 LaunchpadObject
help out interactive shenanigans.
This branch will not be committed; it is part of a larger change.
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_
for completeness).
Something that factors into my reasoning is my (mild) dislike of:
if IOneInterface.
elif IOtherInterface
...
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.
> are not testing whether the distroseries has been converted to a distribution.
> Instead of IDistribution.
> assertEqual(
IDistribution.
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 LaunchpadObject
> is helpful to interactive users, it can be used in other ways.
I've changed it to:
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
> DistroSeriesDer
I've ordered it the same as DistroSeriesVoc
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.
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?
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_
> for completeness).
>
> Something that factors into my reasoning is my (mild) dislike of:
>
> if IOneInterface.
> do_something()
> elif IOtherInterface
> 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.
> 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.
Distribution.
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.
Aaron
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.10 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAk1
sBkAnAzeL6esmFN
=PCpH
-----END PGP SIGNATURE-----
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_
> > for completeness).
> >
> > Something that factors into my reasoning is my (mild) dislike of:
> >
> > if IOneInterface.
> > do_something()
> > elif IOtherInterface
> > 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
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.
> 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 |
Robert Collins (lifeless) wrote : | # |
On Tue, Mar 15, 2011 at 4:12 AM, Gavin Panella
<email address hidden> wrote:
>> > Â Â if IOneInterface.
>> > Â Â Â Â do_something()
>> > Â Â elif IOtherInterface
>> > Â Â Â Â 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
>
> 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.
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
- adaption - a primitive form of multiple dispatch - cast or acquire
into a known type and then call a method on it:
IPillar(
- 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_
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
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.
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.
Gavin Panella (allenap) wrote : | # |
The adaption has gone, but I left in a couple of trivial-ish tests for distroseries_
Aaron Bentley (abentley) : | # |
Preview Diff
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 |
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 LaunchpadObject Factory. __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 DistroSeriesDer ivationVocabula ryFactory 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.