Merge lp:~allenap/launchpad/sub-filters-via-api-bug-672619 into lp:launchpad
- sub-filters-via-api-bug-672619
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Gavin Panella |
Approved revision: | no longer in the source branch. |
Merged at revision: | 12132 |
Proposed branch: | lp:~allenap/launchpad/sub-filters-via-api-bug-672619 |
Merge into: | lp:launchpad |
Diff against target: |
727 lines (+373/-51) 12 files modified
lib/canonical/launchpad/interfaces/_schema_circular_imports.py (+2/-0) lib/lp/bugs/browser/configure.zcml (+9/-0) lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py (+210/-0) lib/lp/bugs/interfaces/bugsubscriptionfilter.py (+44/-28) lib/lp/bugs/interfaces/webservice.py (+6/-1) lib/lp/registry/browser/configure.zcml (+3/-0) lib/lp/registry/browser/structuralsubscription.py (+19/-3) lib/lp/registry/browser/tests/test_structuralsubscription.py (+53/-1) lib/lp/registry/interfaces/structuralsubscription.py (+3/-2) lib/lp/registry/model/structuralsubscription.py (+2/-0) lib/lp/registry/stories/webservice/xx-structuralsubscription.txt (+3/-0) utilities/format-imports (+19/-16) |
To merge this branch: | bzr merge lp:~allenap/launchpad/sub-filters-via-api-bug-672619 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Deryck Hodge (community) | code | Approve | |
Review via email:
|
Commit message
[r=deryck]
Description of the change
This exposes bug subscription filters to the API.
There are a few parts to making this happen:
- Providing URL information for bug subscription filters:
lib/lp/
- Providing navigation for bug subscription filters:
lib/lp/
lib/lp/
for TestBugSubscrip
lib/lp/
for StructuralSubsc
- Exporting IStructuralSubs
lib/canonical
lib/lp/
lib/lp/
- Exporting IBugSubscriptio
lib/lp/
for TestBugSubscrip
for TestBugSubscrip
lib/lp/
lib/lp/
- Fix a weird issue where navigation was attempted on a non-flushed
object. Haven't investigated this, but a flush prevents issues.
lib/lp/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gavin Panella (allenap) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Deryck Hodge (deryck) wrote : | # |
Looks fine to me, per our discussions on IRC.
Preview Diff
1 | === modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py' | |||
2 | --- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-12-01 19:12:00 +0000 | |||
3 | +++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-12-21 23:23:17 +0000 | |||
4 | @@ -420,6 +420,8 @@ | |||
5 | 420 | # IStructuralSubscription | 420 | # IStructuralSubscription |
6 | 421 | patch_collection_property( | 421 | patch_collection_property( |
7 | 422 | IStructuralSubscription, 'bug_filters', IBugSubscriptionFilter) | 422 | IStructuralSubscription, 'bug_filters', IBugSubscriptionFilter) |
8 | 423 | patch_entry_return_type( | ||
9 | 424 | IStructuralSubscription, "newBugFilter", IBugSubscriptionFilter) | ||
10 | 423 | patch_reference_property( | 425 | patch_reference_property( |
11 | 424 | IStructuralSubscription, 'target', IStructuralSubscriptionTarget) | 426 | IStructuralSubscription, 'target', IStructuralSubscriptionTarget) |
12 | 425 | 427 | ||
13 | 426 | 428 | ||
14 | === modified file 'lib/lp/bugs/browser/configure.zcml' | |||
15 | --- lib/lp/bugs/browser/configure.zcml 2010-10-28 09:11:36 +0000 | |||
16 | +++ lib/lp/bugs/browser/configure.zcml 2010-12-21 23:23:17 +0000 | |||
17 | @@ -1176,4 +1176,13 @@ | |||
18 | 1176 | BugWatchSetNavigation"/> | 1176 | BugWatchSetNavigation"/> |
19 | 1177 | </facet> | 1177 | </facet> |
20 | 1178 | 1178 | ||
21 | 1179 | <!-- Bug Subscription Filters --> | ||
22 | 1180 | <facet facet="bugs"> | ||
23 | 1181 | <browser:url | ||
24 | 1182 | for="lp.bugs.interfaces.bugsubscriptionfilter.IBugSubscriptionFilter" | ||
25 | 1183 | path_expression="string:+filter/${id}" | ||
26 | 1184 | attribute_to_parent="structural_subscription" | ||
27 | 1185 | rootsite="bugs" /> | ||
28 | 1186 | </facet> | ||
29 | 1187 | |||
30 | 1179 | </configure> | 1188 | </configure> |
31 | 1180 | 1189 | ||
32 | === added file 'lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py' | |||
33 | --- lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py 1970-01-01 00:00:00 +0000 | |||
34 | +++ lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py 2010-12-21 23:23:17 +0000 | |||
35 | @@ -0,0 +1,210 @@ | |||
36 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | ||
37 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
38 | 3 | |||
39 | 4 | """Tests for bug subscription filter browser code.""" | ||
40 | 5 | |||
41 | 6 | __metaclass__ = type | ||
42 | 7 | |||
43 | 8 | from functools import partial | ||
44 | 9 | from urlparse import urlparse | ||
45 | 10 | |||
46 | 11 | from lazr.restfulclient.errors import BadRequest | ||
47 | 12 | from storm.exceptions import LostObjectError | ||
48 | 13 | from testtools.matchers import StartsWith | ||
49 | 14 | import transaction | ||
50 | 15 | |||
51 | 16 | from canonical.launchpad.webapp.publisher import canonical_url | ||
52 | 17 | from canonical.launchpad.webapp.servers import LaunchpadTestRequest | ||
53 | 18 | from canonical.testing.layers import ( | ||
54 | 19 | AppServerLayer, | ||
55 | 20 | LaunchpadFunctionalLayer, | ||
56 | 21 | ) | ||
57 | 22 | from lp.bugs.interfaces.bugtask import ( | ||
58 | 23 | BugTaskImportance, | ||
59 | 24 | BugTaskStatus, | ||
60 | 25 | ) | ||
61 | 26 | from lp.registry.browser.structuralsubscription import ( | ||
62 | 27 | StructuralSubscriptionNavigation, | ||
63 | 28 | ) | ||
64 | 29 | from lp.testing import ( | ||
65 | 30 | person_logged_in, | ||
66 | 31 | TestCaseWithFactory, | ||
67 | 32 | ws_object, | ||
68 | 33 | ) | ||
69 | 34 | |||
70 | 35 | |||
71 | 36 | class TestBugSubscriptionFilterBase: | ||
72 | 37 | |||
73 | 38 | def setUp(self): | ||
74 | 39 | super(TestBugSubscriptionFilterBase, self).setUp() | ||
75 | 40 | self.owner = self.factory.makePerson(name=u"foo") | ||
76 | 41 | self.structure = self.factory.makeProduct( | ||
77 | 42 | owner=self.owner, name=u"bar") | ||
78 | 43 | with person_logged_in(self.owner): | ||
79 | 44 | self.subscription = self.structure.addBugSubscription( | ||
80 | 45 | self.owner, self.owner) | ||
81 | 46 | self.subscription_filter = self.subscription.newBugFilter() | ||
82 | 47 | |||
83 | 48 | |||
84 | 49 | class TestBugSubscriptionFilterNavigation( | ||
85 | 50 | TestBugSubscriptionFilterBase, TestCaseWithFactory): | ||
86 | 51 | |||
87 | 52 | layer = LaunchpadFunctionalLayer | ||
88 | 53 | |||
89 | 54 | def test_canonical_url(self): | ||
90 | 55 | url = urlparse(canonical_url(self.subscription_filter)) | ||
91 | 56 | self.assertThat(url.hostname, StartsWith("bugs.")) | ||
92 | 57 | self.assertEqual( | ||
93 | 58 | "/bar/+subscription/foo/+filter/%d" % ( | ||
94 | 59 | self.subscription_filter.id), | ||
95 | 60 | url.path) | ||
96 | 61 | |||
97 | 62 | def test_navigation(self): | ||
98 | 63 | request = LaunchpadTestRequest() | ||
99 | 64 | request.setTraversalStack([unicode(self.subscription_filter.id)]) | ||
100 | 65 | navigation = StructuralSubscriptionNavigation( | ||
101 | 66 | self.subscription, request) | ||
102 | 67 | view = navigation.publishTraverse(request, '+filter') | ||
103 | 68 | self.assertIsNot(None, view) | ||
104 | 69 | |||
105 | 70 | |||
106 | 71 | class TestBugSubscriptionFilterAPI( | ||
107 | 72 | TestBugSubscriptionFilterBase, TestCaseWithFactory): | ||
108 | 73 | |||
109 | 74 | layer = AppServerLayer | ||
110 | 75 | |||
111 | 76 | def test_visible_attributes(self): | ||
112 | 77 | # Bug subscription filters are not private objects. All attributes are | ||
113 | 78 | # visible to everyone. | ||
114 | 79 | transaction.commit() | ||
115 | 80 | # Create a service for a new person. | ||
116 | 81 | service = self.factory.makeLaunchpadService() | ||
117 | 82 | get_ws_object = partial(ws_object, service) | ||
118 | 83 | ws_subscription = get_ws_object(self.subscription) | ||
119 | 84 | ws_subscription_filter = get_ws_object(self.subscription_filter) | ||
120 | 85 | self.assertEqual( | ||
121 | 86 | ws_subscription.self_link, | ||
122 | 87 | ws_subscription_filter.structural_subscription_link) | ||
123 | 88 | self.assertEqual( | ||
124 | 89 | self.subscription_filter.find_all_tags, | ||
125 | 90 | ws_subscription_filter.find_all_tags) | ||
126 | 91 | self.assertEqual( | ||
127 | 92 | self.subscription_filter.description, | ||
128 | 93 | ws_subscription_filter.description) | ||
129 | 94 | self.assertEqual( | ||
130 | 95 | list(self.subscription_filter.statuses), | ||
131 | 96 | ws_subscription_filter.statuses) | ||
132 | 97 | self.assertEqual( | ||
133 | 98 | list(self.subscription_filter.importances), | ||
134 | 99 | ws_subscription_filter.importances) | ||
135 | 100 | self.assertEqual( | ||
136 | 101 | list(self.subscription_filter.tags), | ||
137 | 102 | ws_subscription_filter.tags) | ||
138 | 103 | |||
139 | 104 | def test_structural_subscription_cannot_be_modified(self): | ||
140 | 105 | # Bug filters cannot be moved from one structural subscription to | ||
141 | 106 | # another. In other words, the structural_subscription field is | ||
142 | 107 | # read-only. | ||
143 | 108 | user = self.factory.makePerson(name=u"baz") | ||
144 | 109 | with person_logged_in(self.owner): | ||
145 | 110 | user_subscription = self.structure.addBugSubscription(user, user) | ||
146 | 111 | transaction.commit() | ||
147 | 112 | # Create a service for the structure owner. | ||
148 | 113 | service = self.factory.makeLaunchpadService(self.owner) | ||
149 | 114 | get_ws_object = partial(ws_object, service) | ||
150 | 115 | ws_user_subscription = get_ws_object(user_subscription) | ||
151 | 116 | ws_subscription_filter = get_ws_object(self.subscription_filter) | ||
152 | 117 | ws_subscription_filter.structural_subscription = ws_user_subscription | ||
153 | 118 | error = self.assertRaises(BadRequest, ws_subscription_filter.lp_save) | ||
154 | 119 | self.assertEqual(400, error.response.status) | ||
155 | 120 | self.assertEqual( | ||
156 | 121 | self.subscription, | ||
157 | 122 | self.subscription_filter.structural_subscription) | ||
158 | 123 | |||
159 | 124 | |||
160 | 125 | class TestBugSubscriptionFilterAPIModifications( | ||
161 | 126 | TestBugSubscriptionFilterBase, TestCaseWithFactory): | ||
162 | 127 | |||
163 | 128 | layer = AppServerLayer | ||
164 | 129 | |||
165 | 130 | def setUp(self): | ||
166 | 131 | super(TestBugSubscriptionFilterAPIModifications, self).setUp() | ||
167 | 132 | transaction.commit() | ||
168 | 133 | self.service = self.factory.makeLaunchpadService(self.owner) | ||
169 | 134 | self.ws_subscription_filter = ws_object( | ||
170 | 135 | self.service, self.subscription_filter) | ||
171 | 136 | |||
172 | 137 | def test_modify_tags_fields(self): | ||
173 | 138 | # Two tags-related fields - find_all_tags and tags - can be | ||
174 | 139 | # modified. The other two tags-related fields - include_any_tags and | ||
175 | 140 | # exclude_any_tags - are not exported because the tags field provides | ||
176 | 141 | # a more intuitive way to update them (from the perspective of an API | ||
177 | 142 | # consumer). | ||
178 | 143 | self.assertFalse(self.subscription_filter.find_all_tags) | ||
179 | 144 | self.assertFalse(self.subscription_filter.include_any_tags) | ||
180 | 145 | self.assertFalse(self.subscription_filter.exclude_any_tags) | ||
181 | 146 | self.assertEqual(set(), self.subscription_filter.tags) | ||
182 | 147 | |||
183 | 148 | # Modify, save, and start a new transaction. | ||
184 | 149 | self.ws_subscription_filter.find_all_tags = True | ||
185 | 150 | self.ws_subscription_filter.tags = ["foo", "-bar", "*", "-*"] | ||
186 | 151 | self.ws_subscription_filter.lp_save() | ||
187 | 152 | transaction.begin() | ||
188 | 153 | |||
189 | 154 | # Updated state. | ||
190 | 155 | self.assertTrue(self.subscription_filter.find_all_tags) | ||
191 | 156 | self.assertTrue(self.subscription_filter.include_any_tags) | ||
192 | 157 | self.assertTrue(self.subscription_filter.exclude_any_tags) | ||
193 | 158 | self.assertEqual( | ||
194 | 159 | set(["*", "-*", "foo", "-bar"]), | ||
195 | 160 | self.subscription_filter.tags) | ||
196 | 161 | |||
197 | 162 | def test_modify_description(self): | ||
198 | 163 | # The description can be modified. | ||
199 | 164 | self.assertEqual( | ||
200 | 165 | None, self.subscription_filter.description) | ||
201 | 166 | |||
202 | 167 | # Modify, save, and start a new transaction. | ||
203 | 168 | self.ws_subscription_filter.description = u"It's late." | ||
204 | 169 | self.ws_subscription_filter.lp_save() | ||
205 | 170 | transaction.begin() | ||
206 | 171 | |||
207 | 172 | # Updated state. | ||
208 | 173 | self.assertEqual( | ||
209 | 174 | u"It's late.", self.subscription_filter.description) | ||
210 | 175 | |||
211 | 176 | def test_modify_statuses(self): | ||
212 | 177 | # The statuses field can be modified. | ||
213 | 178 | self.assertEqual(set(), self.subscription_filter.statuses) | ||
214 | 179 | |||
215 | 180 | # Modify, save, and start a new transaction. | ||
216 | 181 | self.ws_subscription_filter.statuses = ["New", "Triaged"] | ||
217 | 182 | self.ws_subscription_filter.lp_save() | ||
218 | 183 | transaction.begin() | ||
219 | 184 | |||
220 | 185 | # Updated state. | ||
221 | 186 | self.assertEqual( | ||
222 | 187 | set([BugTaskStatus.NEW, BugTaskStatus.TRIAGED]), | ||
223 | 188 | self.subscription_filter.statuses) | ||
224 | 189 | |||
225 | 190 | def test_modify_importances(self): | ||
226 | 191 | # The importances field can be modified. | ||
227 | 192 | self.assertEqual(set(), self.subscription_filter.importances) | ||
228 | 193 | |||
229 | 194 | # Modify, save, and start a new transaction. | ||
230 | 195 | self.ws_subscription_filter.importances = ["Low", "High"] | ||
231 | 196 | self.ws_subscription_filter.lp_save() | ||
232 | 197 | transaction.begin() | ||
233 | 198 | |||
234 | 199 | # Updated state. | ||
235 | 200 | self.assertEqual( | ||
236 | 201 | set([BugTaskImportance.LOW, BugTaskImportance.HIGH]), | ||
237 | 202 | self.subscription_filter.importances) | ||
238 | 203 | |||
239 | 204 | def test_delete(self): | ||
240 | 205 | # Subscription filters can be deleted. | ||
241 | 206 | self.ws_subscription_filter.lp_delete() | ||
242 | 207 | transaction.begin() | ||
243 | 208 | self.assertRaises( | ||
244 | 209 | LostObjectError, getattr, self.subscription_filter, | ||
245 | 210 | "find_all_tags") | ||
246 | 0 | 211 | ||
247 | === modified file 'lib/lp/bugs/interfaces/bugsubscriptionfilter.py' | |||
248 | --- lib/lp/bugs/interfaces/bugsubscriptionfilter.py 2010-10-04 13:24:54 +0000 | |||
249 | +++ lib/lp/bugs/interfaces/bugsubscriptionfilter.py 2010-12-21 23:23:17 +0000 | |||
250 | @@ -9,12 +9,18 @@ | |||
251 | 9 | ] | 9 | ] |
252 | 10 | 10 | ||
253 | 11 | 11 | ||
254 | 12 | from lazr.restful.declarations import ( | ||
255 | 13 | export_as_webservice_entry, | ||
256 | 14 | export_destructor_operation, | ||
257 | 15 | exported, | ||
258 | 16 | ) | ||
259 | 12 | from lazr.restful.fields import Reference | 17 | from lazr.restful.fields import Reference |
260 | 13 | from zope.interface import Interface | 18 | from zope.interface import Interface |
261 | 14 | from zope.schema import ( | 19 | from zope.schema import ( |
262 | 15 | Bool, | 20 | Bool, |
263 | 16 | Choice, | 21 | Choice, |
264 | 17 | FrozenSet, | 22 | FrozenSet, |
265 | 23 | Int, | ||
266 | 18 | Text, | 24 | Text, |
267 | 19 | ) | 25 | ) |
268 | 20 | 26 | ||
269 | @@ -32,14 +38,18 @@ | |||
270 | 32 | class IBugSubscriptionFilterAttributes(Interface): | 38 | class IBugSubscriptionFilterAttributes(Interface): |
271 | 33 | """Attributes of `IBugSubscriptionFilter`.""" | 39 | """Attributes of `IBugSubscriptionFilter`.""" |
272 | 34 | 40 | ||
281 | 35 | structural_subscription = Reference( | 41 | id = Int(required=True, readonly=True) |
282 | 36 | IStructuralSubscription, | 42 | |
283 | 37 | title=_("Structural subscription"), | 43 | structural_subscription = exported( |
284 | 38 | required=True, readonly=True) | 44 | Reference( |
285 | 39 | 45 | IStructuralSubscription, | |
286 | 40 | find_all_tags = Bool( | 46 | title=_("Structural subscription"), |
287 | 41 | title=_("All given tags must be found, or any."), | 47 | required=True, readonly=True)) |
288 | 42 | required=True, default=False) | 48 | |
289 | 49 | find_all_tags = exported( | ||
290 | 50 | Bool( | ||
291 | 51 | title=_("All given tags must be found, or any."), | ||
292 | 52 | required=True, default=False)) | ||
293 | 43 | include_any_tags = Bool( | 53 | include_any_tags = Bool( |
294 | 44 | title=_("Include any tags."), | 54 | title=_("Include any tags."), |
295 | 45 | required=True, default=False) | 55 | required=True, default=False) |
296 | @@ -47,31 +57,36 @@ | |||
297 | 47 | title=_("Exclude all tags."), | 57 | title=_("Exclude all tags."), |
298 | 48 | required=True, default=False) | 58 | required=True, default=False) |
299 | 49 | 59 | ||
320 | 50 | description = Text( | 60 | description = exported( |
321 | 51 | title=_("Description of this filter."), | 61 | Text( |
322 | 52 | required=False) | 62 | title=_("Description of this filter."), |
323 | 53 | 63 | required=False)) | |
324 | 54 | statuses = FrozenSet( | 64 | |
325 | 55 | title=_("The statuses to filter on."), | 65 | statuses = exported( |
326 | 56 | required=True, default=frozenset(), | 66 | FrozenSet( |
327 | 57 | value_type=Choice( | 67 | title=_("The statuses to filter on."), |
328 | 58 | title=_('Status'), vocabulary=BugTaskStatus)) | 68 | required=True, default=frozenset(), |
329 | 59 | 69 | value_type=Choice( | |
330 | 60 | importances = FrozenSet( | 70 | title=_('Status'), vocabulary=BugTaskStatus))) |
331 | 61 | title=_("The importances to filter on."), | 71 | |
332 | 62 | required=True, default=frozenset(), | 72 | importances = exported( |
333 | 63 | value_type=Choice( | 73 | FrozenSet( |
334 | 64 | title=_('Importance'), vocabulary=BugTaskImportance)) | 74 | title=_("The importances to filter on."), |
335 | 65 | 75 | required=True, default=frozenset(), | |
336 | 66 | tags = FrozenSet( | 76 | value_type=Choice( |
337 | 67 | title=_("The tags to filter on."), | 77 | title=_('Importance'), vocabulary=BugTaskImportance))) |
338 | 68 | required=True, default=frozenset(), | 78 | |
339 | 69 | value_type=SearchTag()) | 79 | tags = exported( |
340 | 80 | FrozenSet( | ||
341 | 81 | title=_("The tags to filter on."), | ||
342 | 82 | required=True, default=frozenset(), | ||
343 | 83 | value_type=SearchTag())) | ||
344 | 70 | 84 | ||
345 | 71 | 85 | ||
346 | 72 | class IBugSubscriptionFilterMethods(Interface): | 86 | class IBugSubscriptionFilterMethods(Interface): |
347 | 73 | """Methods of `IBugSubscriptionFilter`.""" | 87 | """Methods of `IBugSubscriptionFilter`.""" |
348 | 74 | 88 | ||
349 | 89 | @export_destructor_operation() | ||
350 | 75 | def delete(): | 90 | def delete(): |
351 | 76 | """Delete this bug subscription filter.""" | 91 | """Delete this bug subscription filter.""" |
352 | 77 | 92 | ||
353 | @@ -79,3 +94,4 @@ | |||
354 | 79 | class IBugSubscriptionFilter( | 94 | class IBugSubscriptionFilter( |
355 | 80 | IBugSubscriptionFilterAttributes, IBugSubscriptionFilterMethods): | 95 | IBugSubscriptionFilterAttributes, IBugSubscriptionFilterMethods): |
356 | 81 | """A bug subscription filter.""" | 96 | """A bug subscription filter.""" |
357 | 97 | export_as_webservice_entry() | ||
358 | 82 | 98 | ||
359 | === modified file 'lib/lp/bugs/interfaces/webservice.py' | |||
360 | --- lib/lp/bugs/interfaces/webservice.py 2010-12-01 22:18:07 +0000 | |||
361 | +++ lib/lp/bugs/interfaces/webservice.py 2010-12-21 23:23:17 +0000 | |||
362 | @@ -50,7 +50,6 @@ | |||
363 | 50 | from lp.bugs.interfaces.bugattachment import IBugAttachment | 50 | from lp.bugs.interfaces.bugattachment import IBugAttachment |
364 | 51 | from lp.bugs.interfaces.bugbranch import IBugBranch | 51 | from lp.bugs.interfaces.bugbranch import IBugBranch |
365 | 52 | from lp.bugs.interfaces.buglink import IBugLinkTarget | 52 | from lp.bugs.interfaces.buglink import IBugLinkTarget |
366 | 53 | from lp.bugs.interfaces.malone import IMaloneApplication | ||
367 | 54 | from lp.bugs.interfaces.bugnomination import ( | 53 | from lp.bugs.interfaces.bugnomination import ( |
368 | 55 | BugNominationStatusError, | 54 | BugNominationStatusError, |
369 | 56 | IBugNomination, | 55 | IBugNomination, |
370 | @@ -58,6 +57,7 @@ | |||
371 | 58 | NominationSeriesObsoleteError, | 57 | NominationSeriesObsoleteError, |
372 | 59 | ) | 58 | ) |
373 | 60 | from lp.bugs.interfaces.bugsubscription import IBugSubscription | 59 | from lp.bugs.interfaces.bugsubscription import IBugSubscription |
374 | 60 | from lp.bugs.interfaces.bugsubscriptionfilter import IBugSubscriptionFilter | ||
375 | 61 | from lp.bugs.interfaces.bugtarget import ( | 61 | from lp.bugs.interfaces.bugtarget import ( |
376 | 62 | IBugTarget, | 62 | IBugTarget, |
377 | 63 | IHasBugs, | 63 | IHasBugs, |
378 | @@ -82,6 +82,11 @@ | |||
379 | 82 | ICve, | 82 | ICve, |
380 | 83 | ICveSet, | 83 | ICveSet, |
381 | 84 | ) | 84 | ) |
382 | 85 | from lp.bugs.interfaces.malone import IMaloneApplication | ||
383 | 86 | |||
384 | 87 | |||
385 | 88 | |||
386 | 85 | 89 | ||
387 | 90 | |||
388 | 86 | # XXX: JonathanLange 2010-11-09 bug=673083: Legacy work-around for circular | 91 | # XXX: JonathanLange 2010-11-09 bug=673083: Legacy work-around for circular |
389 | 87 | # import bugs. Break this up into a per-package thing. | 92 | # import bugs. Break this up into a per-package thing. |
390 | 88 | from canonical.launchpad.interfaces import _schema_circular_imports | 93 | from canonical.launchpad.interfaces import _schema_circular_imports |
391 | 89 | 94 | ||
392 | === modified file 'lib/lp/registry/browser/configure.zcml' | |||
393 | --- lib/lp/registry/browser/configure.zcml 2010-12-15 22:05:43 +0000 | |||
394 | +++ lib/lp/registry/browser/configure.zcml 2010-12-21 23:23:17 +0000 | |||
395 | @@ -2225,6 +2225,9 @@ | |||
396 | 2225 | for="lp.registry.interfaces.structuralsubscription.IStructuralSubscription" | 2225 | for="lp.registry.interfaces.structuralsubscription.IStructuralSubscription" |
397 | 2226 | path_expression="string:+subscription/${subscriber/name}" | 2226 | path_expression="string:+subscription/${subscriber/name}" |
398 | 2227 | attribute_to_parent="target"/> | 2227 | attribute_to_parent="target"/> |
399 | 2228 | <browser:navigation | ||
400 | 2229 | module="lp.registry.browser.structuralsubscription" | ||
401 | 2230 | classes="StructuralSubscriptionNavigation"/> | ||
402 | 2228 | 2231 | ||
403 | 2229 | <browser:url | 2232 | <browser:url |
404 | 2230 | for="lp.registry.interfaces.personproduct.IPersonProduct" | 2233 | for="lp.registry.interfaces.personproduct.IPersonProduct" |
405 | 2231 | 2234 | ||
406 | === modified file 'lib/lp/registry/browser/structuralsubscription.py' | |||
407 | --- lib/lp/registry/browser/structuralsubscription.py 2010-11-29 12:25:43 +0000 | |||
408 | +++ lib/lp/registry/browser/structuralsubscription.py 2010-12-21 23:23:17 +0000 | |||
409 | @@ -23,13 +23,14 @@ | |||
410 | 23 | SimpleVocabulary, | 23 | SimpleVocabulary, |
411 | 24 | ) | 24 | ) |
412 | 25 | 25 | ||
414 | 26 | from canonical.launchpad.webapp import ( | 26 | from canonical.launchpad.webapp.authorization import check_permission |
415 | 27 | from canonical.launchpad.webapp.menu import Link | ||
416 | 28 | from canonical.launchpad.webapp.publisher import ( | ||
417 | 27 | canonical_url, | 29 | canonical_url, |
418 | 28 | LaunchpadView, | 30 | LaunchpadView, |
419 | 31 | Navigation, | ||
420 | 29 | stepthrough, | 32 | stepthrough, |
421 | 30 | ) | 33 | ) |
422 | 31 | from canonical.launchpad.webapp.authorization import check_permission | ||
423 | 32 | from canonical.launchpad.webapp.menu import Link | ||
424 | 33 | from canonical.widgets import LabeledMultiCheckBoxWidget | 34 | from canonical.widgets import LabeledMultiCheckBoxWidget |
425 | 34 | from lp.app.browser.launchpadform import ( | 35 | from lp.app.browser.launchpadform import ( |
426 | 35 | action, | 36 | action, |
427 | @@ -44,14 +45,29 @@ | |||
428 | 44 | from lp.registry.interfaces.milestone import IProjectGroupMilestone | 45 | from lp.registry.interfaces.milestone import IProjectGroupMilestone |
429 | 45 | from lp.registry.interfaces.person import IPersonSet | 46 | from lp.registry.interfaces.person import IPersonSet |
430 | 46 | from lp.registry.interfaces.structuralsubscription import ( | 47 | from lp.registry.interfaces.structuralsubscription import ( |
431 | 48 | IStructuralSubscription, | ||
432 | 47 | IStructuralSubscriptionForm, | 49 | IStructuralSubscriptionForm, |
433 | 48 | IStructuralSubscriptionTarget, | 50 | IStructuralSubscriptionTarget, |
434 | 49 | ) | 51 | ) |
435 | 50 | from lp.services.propertycache import cachedproperty | 52 | from lp.services.propertycache import cachedproperty |
436 | 51 | 53 | ||
437 | 52 | 54 | ||
438 | 55 | class StructuralSubscriptionNavigation(Navigation): | ||
439 | 56 | |||
440 | 57 | usedfor = IStructuralSubscription | ||
441 | 58 | |||
442 | 59 | @stepthrough("+filter") | ||
443 | 60 | def bug_filter(self, filter_id): | ||
444 | 61 | bug_filter_id = int(filter_id) | ||
445 | 62 | for bug_filter in self.context.bug_filters: | ||
446 | 63 | if bug_filter.id == bug_filter_id: | ||
447 | 64 | return bug_filter | ||
448 | 65 | return None | ||
449 | 66 | |||
450 | 67 | |||
451 | 53 | class StructuralSubscriptionView(LaunchpadFormView, | 68 | class StructuralSubscriptionView(LaunchpadFormView, |
452 | 54 | AdvancedSubscriptionMixin): | 69 | AdvancedSubscriptionMixin): |
453 | 70 | |||
454 | 55 | """View class for structural subscriptions.""" | 71 | """View class for structural subscriptions.""" |
455 | 56 | 72 | ||
456 | 57 | schema = IStructuralSubscriptionForm | 73 | schema = IStructuralSubscriptionForm |
457 | 58 | 74 | ||
458 | === modified file 'lib/lp/registry/browser/tests/test_structuralsubscription.py' | |||
459 | --- lib/lp/registry/browser/tests/test_structuralsubscription.py 2010-11-30 11:38:12 +0000 | |||
460 | +++ lib/lp/registry/browser/tests/test_structuralsubscription.py 2010-12-21 23:23:17 +0000 | |||
461 | @@ -3,7 +3,10 @@ | |||
462 | 3 | 3 | ||
463 | 4 | """Tests for structural subscription traversal.""" | 4 | """Tests for structural subscription traversal.""" |
464 | 5 | 5 | ||
465 | 6 | from urlparse import urlparse | ||
466 | 7 | |||
467 | 6 | from lazr.restful.testing.webservice import FakeRequest | 8 | from lazr.restful.testing.webservice import FakeRequest |
468 | 9 | import transaction | ||
469 | 7 | from zope.publisher.interfaces import NotFound | 10 | from zope.publisher.interfaces import NotFound |
470 | 8 | 11 | ||
471 | 9 | from canonical.launchpad.ftests import ( | 12 | from canonical.launchpad.ftests import ( |
472 | @@ -14,6 +17,7 @@ | |||
473 | 14 | from canonical.launchpad.webapp.publisher import canonical_url | 17 | from canonical.launchpad.webapp.publisher import canonical_url |
474 | 15 | from canonical.launchpad.webapp.servers import StepsToGo | 18 | from canonical.launchpad.webapp.servers import StepsToGo |
475 | 16 | from canonical.testing.layers import ( | 19 | from canonical.testing.layers import ( |
476 | 20 | AppServerLayer, | ||
477 | 17 | DatabaseFunctionalLayer, | 21 | DatabaseFunctionalLayer, |
478 | 18 | LaunchpadFunctionalLayer, | 22 | LaunchpadFunctionalLayer, |
479 | 19 | ) | 23 | ) |
480 | @@ -27,13 +31,15 @@ | |||
481 | 27 | from lp.registry.browser.productseries import ProductSeriesNavigation | 31 | from lp.registry.browser.productseries import ProductSeriesNavigation |
482 | 28 | from lp.registry.browser.project import ProjectNavigation | 32 | from lp.registry.browser.project import ProjectNavigation |
483 | 29 | from lp.registry.browser.structuralsubscription import ( | 33 | from lp.registry.browser.structuralsubscription import ( |
485 | 30 | StructuralSubscriptionView) | 34 | StructuralSubscriptionView, |
486 | 35 | ) | ||
487 | 31 | from lp.registry.enum import BugNotificationLevel | 36 | from lp.registry.enum import BugNotificationLevel |
488 | 32 | from lp.testing import ( | 37 | from lp.testing import ( |
489 | 33 | feature_flags, | 38 | feature_flags, |
490 | 34 | person_logged_in, | 39 | person_logged_in, |
491 | 35 | set_feature_flag, | 40 | set_feature_flag, |
492 | 36 | TestCaseWithFactory, | 41 | TestCaseWithFactory, |
493 | 42 | ws_object, | ||
494 | 37 | ) | 43 | ) |
495 | 38 | from lp.testing.views import create_initialized_view | 44 | from lp.testing.views import create_initialized_view |
496 | 39 | 45 | ||
497 | @@ -356,3 +362,49 @@ | |||
498 | 356 | self.assertEqual( | 362 | self.assertEqual( |
499 | 357 | "To all bugs in %s" % self.target.displayname, | 363 | "To all bugs in %s" % self.target.displayname, |
500 | 358 | self.view.target_label) | 364 | self.view.target_label) |
501 | 365 | |||
502 | 366 | |||
503 | 367 | class TestStructuralSubscriptionAPI(TestCaseWithFactory): | ||
504 | 368 | |||
505 | 369 | layer = AppServerLayer | ||
506 | 370 | |||
507 | 371 | def setUp(self): | ||
508 | 372 | super(TestStructuralSubscriptionAPI, self).setUp() | ||
509 | 373 | self.owner = self.factory.makePerson(name=u"foo") | ||
510 | 374 | self.structure = self.factory.makeProduct( | ||
511 | 375 | owner=self.owner, name=u"bar") | ||
512 | 376 | with person_logged_in(self.owner): | ||
513 | 377 | self.subscription = self.structure.addBugSubscription( | ||
514 | 378 | self.owner, self.owner) | ||
515 | 379 | transaction.commit() | ||
516 | 380 | self.service = self.factory.makeLaunchpadService(self.owner) | ||
517 | 381 | self.ws_subscription = ws_object(self.service, self.subscription) | ||
518 | 382 | |||
519 | 383 | def test_newBugFilter(self): | ||
520 | 384 | # New bug subscription filters can be created with newBugFilter(). | ||
521 | 385 | ws_subscription_filter = self.ws_subscription.newBugFilter() | ||
522 | 386 | self.assertEqual( | ||
523 | 387 | "bug_subscription_filter", | ||
524 | 388 | urlparse(ws_subscription_filter.resource_type_link).fragment) | ||
525 | 389 | self.assertEqual( | ||
526 | 390 | ws_subscription_filter.structural_subscription.self_link, | ||
527 | 391 | self.ws_subscription.self_link) | ||
528 | 392 | |||
529 | 393 | def test_bug_filters(self): | ||
530 | 394 | # The bug_filters property is a collection of IBugSubscriptionFilter | ||
531 | 395 | # instances previously created by newBugFilter(). | ||
532 | 396 | bug_filter_links = lambda: set( | ||
533 | 397 | bug_filter.self_link for bug_filter in ( | ||
534 | 398 | self.ws_subscription.bug_filters)) | ||
535 | 399 | self.assertEqual(set(), bug_filter_links()) | ||
536 | 400 | # A new filter appears in the bug_filters collection. | ||
537 | 401 | ws_subscription_filter1 = self.ws_subscription.newBugFilter() | ||
538 | 402 | self.assertEqual( | ||
539 | 403 | set([ws_subscription_filter1.self_link]), | ||
540 | 404 | bug_filter_links()) | ||
541 | 405 | # A second new filter also appears in the bug_filters collection. | ||
542 | 406 | ws_subscription_filter2 = self.ws_subscription.newBugFilter() | ||
543 | 407 | self.assertEqual( | ||
544 | 408 | set([ws_subscription_filter1.self_link, | ||
545 | 409 | ws_subscription_filter2.self_link]), | ||
546 | 410 | bug_filter_links()) | ||
547 | 359 | 411 | ||
548 | === modified file 'lib/lp/registry/interfaces/structuralsubscription.py' | |||
549 | --- lib/lp/registry/interfaces/structuralsubscription.py 2010-11-09 11:00:55 +0000 | |||
550 | +++ lib/lp/registry/interfaces/structuralsubscription.py 2010-12-21 23:23:17 +0000 | |||
551 | @@ -129,11 +129,12 @@ | |||
552 | 129 | required=True, readonly=True, | 129 | required=True, readonly=True, |
553 | 130 | title=_("The structure to which this subscription belongs."))) | 130 | title=_("The structure to which this subscription belongs."))) |
554 | 131 | 131 | ||
556 | 132 | bug_filters = CollectionField( | 132 | bug_filters = exported(CollectionField( |
557 | 133 | title=_('List of bug filters that narrow this subscription.'), | 133 | title=_('List of bug filters that narrow this subscription.'), |
558 | 134 | readonly=True, required=False, | 134 | readonly=True, required=False, |
560 | 135 | value_type=Reference(schema=Interface)) | 135 | value_type=Reference(schema=Interface))) |
561 | 136 | 136 | ||
562 | 137 | @export_factory_operation(Interface, []) | ||
563 | 137 | def newBugFilter(): | 138 | def newBugFilter(): |
564 | 138 | """Returns a new `BugSubscriptionFilter` for this subscription.""" | 139 | """Returns a new `BugSubscriptionFilter` for this subscription.""" |
565 | 139 | 140 | ||
566 | 140 | 141 | ||
567 | === modified file 'lib/lp/registry/model/structuralsubscription.py' | |||
568 | --- lib/lp/registry/model/structuralsubscription.py 2010-11-26 17:08:03 +0000 | |||
569 | +++ lib/lp/registry/model/structuralsubscription.py 2010-12-21 23:23:17 +0000 | |||
570 | @@ -160,6 +160,8 @@ | |||
571 | 160 | """See `IStructuralSubscription`.""" | 160 | """See `IStructuralSubscription`.""" |
572 | 161 | bug_filter = BugSubscriptionFilter() | 161 | bug_filter = BugSubscriptionFilter() |
573 | 162 | bug_filter.structural_subscription = self | 162 | bug_filter.structural_subscription = self |
574 | 163 | # This flush is needed for the web service API. | ||
575 | 164 | IStore(StructuralSubscription).flush() | ||
576 | 163 | return bug_filter | 165 | return bug_filter |
577 | 164 | 166 | ||
578 | 165 | 167 | ||
579 | 166 | 168 | ||
580 | === modified file 'lib/lp/registry/stories/webservice/xx-structuralsubscription.txt' | |||
581 | --- lib/lp/registry/stories/webservice/xx-structuralsubscription.txt 2010-08-05 20:49:08 +0000 | |||
582 | +++ lib/lp/registry/stories/webservice/xx-structuralsubscription.txt 2010-12-21 23:23:17 +0000 | |||
583 | @@ -46,6 +46,7 @@ | |||
584 | 46 | start: 0 | 46 | start: 0 |
585 | 47 | total_size: 1 | 47 | total_size: 1 |
586 | 48 | --- | 48 | --- |
587 | 49 | bug_filters_collection_link: u'.../fooix/+subscription/eric/bug_filters' | ||
588 | 49 | date_created: u'...' | 50 | date_created: u'...' |
589 | 50 | date_last_updated: u'...' | 51 | date_last_updated: u'...' |
590 | 51 | resource_type_link: u'http://.../#structural_subscription' | 52 | resource_type_link: u'http://.../#structural_subscription' |
591 | @@ -60,6 +61,7 @@ | |||
592 | 60 | >>> pprint_entry(eric_webservice.named_get( | 61 | >>> pprint_entry(eric_webservice.named_get( |
593 | 61 | ... '/fooix', 'getSubscription', | 62 | ... '/fooix', 'getSubscription', |
594 | 62 | ... person=webservice.getAbsoluteUrl('/~eric')).jsonBody()) | 63 | ... person=webservice.getAbsoluteUrl('/~eric')).jsonBody()) |
595 | 64 | bug_filters_collection_link: u'.../fooix/+subscription/eric/bug_filters' | ||
596 | 63 | date_created: u'...' | 65 | date_created: u'...' |
597 | 64 | date_last_updated: u'...' | 66 | date_last_updated: u'...' |
598 | 65 | resource_type_link: u'http://.../#structural_subscription' | 67 | resource_type_link: u'http://.../#structural_subscription' |
599 | @@ -117,6 +119,7 @@ | |||
600 | 117 | start: 0 | 119 | start: 0 |
601 | 118 | total_size: 1 | 120 | total_size: 1 |
602 | 119 | --- | 121 | --- |
603 | 122 | bug_filters_collection_link: u'.../fooix/+subscription/pythons/bug_filters' | ||
604 | 120 | date_created: u'...' | 123 | date_created: u'...' |
605 | 121 | date_last_updated: u'...' | 124 | date_last_updated: u'...' |
606 | 122 | resource_type_link: u'http://.../#structural_subscription' | 125 | resource_type_link: u'http://.../#structural_subscription' |
607 | 123 | 126 | ||
608 | === modified file 'utilities/format-imports' | |||
609 | --- utilities/format-imports 2010-08-27 20:17:23 +0000 | |||
610 | +++ utilities/format-imports 2010-12-21 23:23:17 +0000 | |||
611 | @@ -24,7 +24,7 @@ | |||
612 | 24 | that start with "import" or "from" or are indented with at least one space or | 24 | that start with "import" or "from" or are indented with at least one space or |
613 | 25 | are blank lines. Comment lines are also included if they are followed by an | 25 | are blank lines. Comment lines are also included if they are followed by an |
614 | 26 | import statement. An inital __future__ import and a module docstring are | 26 | import statement. An inital __future__ import and a module docstring are |
616 | 27 | explicitly skipped. | 27 | explicitly skipped. |
617 | 28 | 28 | ||
618 | 29 | The import section is rewritten as three subsections, each separated by a | 29 | The import section is rewritten as three subsections, each separated by a |
619 | 30 | blank line. Any of the sections may be empty. | 30 | blank line. Any of the sections may be empty. |
620 | @@ -123,7 +123,7 @@ | |||
621 | 123 | from lp.app.verylongnames.orverlydeep.modulestructure.leavenoroom \ | 123 | from lp.app.verylongnames.orverlydeep.modulestructure.leavenoroom \ |
622 | 124 | import object | 124 | import object |
623 | 125 | }}} | 125 | }}} |
625 | 126 | """ | 126 | """ |
626 | 127 | 127 | ||
627 | 128 | __metaclass__ = type | 128 | __metaclass__ = type |
628 | 129 | 129 | ||
629 | @@ -145,7 +145,8 @@ | |||
630 | 145 | "(?P<objects>[*]|[a-zA-Z0-9_, ]+)" | 145 | "(?P<objects>[*]|[a-zA-Z0-9_, ]+)" |
631 | 146 | "(?P<comment>#.*)?$", re.M) | 146 | "(?P<comment>#.*)?$", re.M) |
632 | 147 | from_import_multi_regex = re.compile( | 147 | from_import_multi_regex = re.compile( |
634 | 148 | "^from +(?P<module>.+) +import *[(](?P<objects>[a-zA-Z0-9_, \n]+)[)]$", re.M) | 148 | "^from +(?P<module>.+) +import *[(](?P<objects>[a-zA-Z0-9_, \n]+)[)]$", |
635 | 149 | re.M) | ||
636 | 149 | comment_regex = re.compile( | 150 | comment_regex = re.compile( |
637 | 150 | "(?P<comment>(^#.+\n)+)(^import|^from) +(?P<module>[a-zA-Z0-9_.]+)", re.M) | 151 | "(?P<comment>(^#.+\n)+)(^import|^from) +(?P<module>[a-zA-Z0-9_.]+)", re.M) |
638 | 151 | split_regex = re.compile(",\s*") | 152 | split_regex = re.compile(",\s*") |
639 | @@ -153,14 +154,16 @@ | |||
640 | 153 | # Module docstrings are multiline (""") strings that are not indented and are | 154 | # Module docstrings are multiline (""") strings that are not indented and are |
641 | 154 | # followed at some point by an import . | 155 | # followed at some point by an import . |
642 | 155 | module_docstring_regex = re.compile( | 156 | module_docstring_regex = re.compile( |
644 | 156 | '(?P<docstring>^["]{3}[^"]+["]{3}\n).*^(import |from .+ import)', re.M | re.S) | 157 | '(?P<docstring>^["]{3}[^"]+["]{3}\n).*^(import |from .+ import)', |
645 | 158 | re.M | re.S) | ||
646 | 157 | # The imports section starts with an import state that is not a __future__ | 159 | # The imports section starts with an import state that is not a __future__ |
647 | 158 | # import and consists of import lines, indented lines, empty lines and | 160 | # import and consists of import lines, indented lines, empty lines and |
648 | 159 | # comments which are followed by an import line. Sometimes we even find | 161 | # comments which are followed by an import line. Sometimes we even find |
649 | 160 | # lines that contain a single ")"... :-( | 162 | # lines that contain a single ")"... :-( |
650 | 161 | imports_section_regex = re.compile( | 163 | imports_section_regex = re.compile( |
651 | 162 | "(^#.+\n)*^(import|(from ((?!__future__)\S+) import)).*\n" | 164 | "(^#.+\n)*^(import|(from ((?!__future__)\S+) import)).*\n" |
653 | 163 | "(^import .+\n|^from .+\n|^[\t ]+.+\n|(^#.+\n)+((^import|^from) .+\n)|^\n|^[)]\n)*", | 165 | "(^import .+\n|^from .+\n|^[\t ]+.+\n|(^#.+\n)+((^import|^from) " |
654 | 166 | ".+\n)|^\n|^[)]\n)*", | ||
655 | 164 | re.M) | 167 | re.M) |
656 | 165 | 168 | ||
657 | 166 | 169 | ||
658 | @@ -227,17 +230,17 @@ | |||
659 | 227 | imported as a sorted list of strings.""" | 230 | imported as a sorted list of strings.""" |
660 | 228 | imports = {} | 231 | imports = {} |
661 | 229 | # Search for escaped newlines and remove them. | 232 | # Search for escaped newlines and remove them. |
663 | 230 | searchpos = 0 | 233 | searchpos = 0 |
664 | 231 | while True: | 234 | while True: |
665 | 232 | match = escaped_nl_regex.search(import_section, searchpos) | 235 | match = escaped_nl_regex.search(import_section, searchpos) |
666 | 233 | if match is None: | 236 | if match is None: |
667 | 234 | break | 237 | break |
668 | 235 | start = match.start() | 238 | start = match.start() |
669 | 236 | end = match.end() | 239 | end = match.end() |
671 | 237 | import_section = import_section[:start]+import_section[end:] | 240 | import_section = import_section[:start] + import_section[end:] |
672 | 238 | searchpos = start | 241 | searchpos = start |
673 | 239 | # Search for simple one-line import statements. | 242 | # Search for simple one-line import statements. |
675 | 240 | searchpos = 0 | 243 | searchpos = 0 |
676 | 241 | while True: | 244 | while True: |
677 | 242 | match = import_regex.search(import_section, searchpos) | 245 | match = import_regex.search(import_section, searchpos) |
678 | 243 | if match is None: | 246 | if match is None: |
679 | @@ -299,7 +302,7 @@ | |||
680 | 299 | standard_section[module] = statement | 302 | standard_section[module] = statement |
681 | 300 | else: | 303 | else: |
682 | 301 | thirdparty_section[module] = statement | 304 | thirdparty_section[module] = statement |
684 | 302 | 305 | ||
685 | 303 | all_import_lines = [] | 306 | all_import_lines = [] |
686 | 304 | # Sort within each section and generate statement strings. | 307 | # Sort within each section and generate statement strings. |
687 | 305 | sections = ( | 308 | sections = ( |
688 | @@ -321,7 +324,7 @@ | |||
689 | 321 | if len(import_lines) > 0: | 324 | if len(import_lines) > 0: |
690 | 322 | all_import_lines.append('\n'.join(import_lines)) | 325 | all_import_lines.append('\n'.join(import_lines)) |
691 | 323 | # Sections are separated by two blank lines. | 326 | # Sections are separated by two blank lines. |
693 | 324 | return '\n\n'.join(all_import_lines) | 327 | return '\n\n'.join(all_import_lines) |
694 | 325 | 328 | ||
695 | 326 | 329 | ||
696 | 327 | def reformat_importsection(filename): | 330 | def reformat_importsection(filename): |
697 | @@ -334,17 +337,17 @@ | |||
698 | 334 | imports_section = pyfile[import_start:import_end] | 337 | imports_section = pyfile[import_start:import_end] |
699 | 335 | imports = parse_import_statements(imports_section) | 338 | imports = parse_import_statements(imports_section) |
700 | 336 | 339 | ||
702 | 337 | if pyfile[import_end:import_end+1] != '#': | 340 | if pyfile[import_end:import_end + 1] != '#': |
703 | 338 | # Two newlines before anything but comments. | 341 | # Two newlines before anything but comments. |
704 | 339 | number_of_newlines = 3 | 342 | number_of_newlines = 3 |
705 | 340 | else: | 343 | else: |
706 | 341 | number_of_newlines = 2 | 344 | number_of_newlines = 2 |
707 | 342 | 345 | ||
709 | 343 | new_imports = format_imports(imports)+"\n"*number_of_newlines | 346 | new_imports = format_imports(imports) + ("\n" * number_of_newlines) |
710 | 344 | if new_imports == imports_section: | 347 | if new_imports == imports_section: |
714 | 345 | # No change, no need to write a new file. | 348 | # No change, no need to write a new file. |
715 | 346 | return False | 349 | return False |
716 | 347 | 350 | ||
717 | 348 | new_file = open(filename, "w") | 351 | new_file = open(filename, "w") |
718 | 349 | new_file.write(pyfile[:import_start]) | 352 | new_file.write(pyfile[:import_start]) |
719 | 350 | new_file.write(new_imports) | 353 | new_file.write(new_imports) |
720 | @@ -372,7 +375,7 @@ | |||
721 | 372 | if len(sys.argv) == 1 or sys.argv[1] in ("-h", "-?", "--help"): | 375 | if len(sys.argv) == 1 or sys.argv[1] in ("-h", "-?", "--help"): |
722 | 373 | sys.stderr.write(dedent("""\ | 376 | sys.stderr.write(dedent("""\ |
723 | 374 | usage: format-imports <file or directory> ... | 377 | usage: format-imports <file or directory> ... |
725 | 375 | 378 | ||
726 | 376 | Type "format-imports --docstring | less" to see the documentation. | 379 | Type "format-imports --docstring | less" to see the documentation. |
727 | 377 | """)) | 380 | """)) |
728 | 378 | sys.exit(1) | 381 | sys.exit(1) |
I also fixed some lint in utilities/ format- imports.