Merge lp:~mpontillo/maas/remove-iscpy-1.8 into lp:maas/1.8
- remove-iscpy-1.8
- Merge into 1.8
Status: | Merged |
---|---|
Approved by: | Mike Pontillo |
Approved revision: | no longer in the source branch. |
Merged at revision: | 4014 |
Proposed branch: | lp:~mpontillo/maas/remove-iscpy-1.8 |
Merge into: | lp:maas/1.8 |
Diff against target: |
546 lines (+470/-10) 5 files modified
buildout.cfg (+0/-1) src/maasserver/management/commands/edit_named_options.py (+8/-8) src/maasserver/utils/isc.py (+283/-0) src/maasserver/utils/tests/test_isc.py (+179/-0) versions.cfg (+0/-1) |
To merge this branch: | bzr merge lp:~mpontillo/maas/remove-iscpy-1.8 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Mike Pontillo (community) | Approve | ||
Review via email: mp+263552@code.launchpad.net |
Commit message
Remove dependency on iscpy. Add MAAS utility (based on iscpy) for parsing named.options. Add tests for iscpy-derived code.
Description of the change
Mike Pontillo (mpontillo) wrote : | # |
Mike Pontillo (mpontillo) wrote : | # |
Self-approving. 1.7 and trunk have already landed. I don't want 1.8 to feel left out. =)
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~mpontillo/maas/remove-iscpy-1.8 into lp:maas/1.8 failed. Below is the output from the failed tests.
Ign http://
Get:1 http://
Get:2 http://
Ign http://
Ign http://
Hit http://
Get:3 http://
Hit http://
Get:4 http://
Get:5 http://
Get:6 http://
Hit http://
Get:7 http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Get:8 http://
Get:9 http://
Hit http://
Hit http://
Get:10 http://
Get:11 http://
Get:12 http://
Hit http://
Hit http://
Ign http://
Ign http://
Fetched 1,848 kB in 3s (600 kB/s)
Reading package lists...
sudo DEBIAN_
--
Preview Diff
1 | === modified file 'buildout.cfg' |
2 | --- buildout.cfg 2015-05-29 12:30:52 +0000 |
3 | +++ buildout.cfg 2015-07-04 09:40:25 +0000 |
4 | @@ -71,7 +71,6 @@ |
5 | djorm-ext-pgarray |
6 | docutils |
7 | crochet |
8 | - iscpy |
9 | entry-points = |
10 | maas-region-admin=maasserver:execute_from_command_line |
11 | twistd.region=twisted.scripts.twistd:run |
12 | |
13 | === modified file 'src/maasserver/management/commands/edit_named_options.py' |
14 | --- src/maasserver/management/commands/edit_named_options.py 2015-05-07 18:14:38 +0000 |
15 | +++ src/maasserver/management/commands/edit_named_options.py 2015-07-04 09:40:25 +0000 |
16 | @@ -28,9 +28,9 @@ |
17 | BaseCommand, |
18 | CommandError, |
19 | ) |
20 | -from iscpy import ( |
21 | - MakeISC, |
22 | - ParseISCString, |
23 | +from maasserver.utils.isc import ( |
24 | + make_isc_string, |
25 | + parse_isc_string, |
26 | ) |
27 | from provisioningserver.dns.config import MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME |
28 | |
29 | @@ -61,12 +61,12 @@ |
30 | return options_file |
31 | |
32 | def parse_file(self, config_path, options_file): |
33 | - """Read the named.conf.options file and parse it with iscpy. |
34 | + """Read the named.conf.options file and parse it. |
35 | |
36 | - We also use iscpy to insert the include statement that we need. |
37 | + Then insert the include statement that we need. |
38 | """ |
39 | try: |
40 | - config_dict = ParseISCString(options_file) |
41 | + config_dict = parse_isc_string(options_file) |
42 | except Exception as e: |
43 | # Yes, it throws bare exceptions :( |
44 | raise CommandError("Failed to parse %s: %s" % ( |
45 | @@ -81,7 +81,7 @@ |
46 | return config_dict |
47 | |
48 | def set_up_include_statement(self, options_block, config_path): |
49 | - """Insert the 'include' directive into the iscpy-parsed options.""" |
50 | + """Insert the 'include' directive into the parsed options.""" |
51 | dir = os.path.join(os.path.dirname(config_path), "maas") |
52 | options_block['include'] = '"%s%s%s"' % ( |
53 | dir, os.path.sep, MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME) |
54 | @@ -128,7 +128,7 @@ |
55 | self.set_up_include_statement(options_block, config_path) |
56 | self.remove_forwarders(options_block) |
57 | self.remove_dnssec_validation(options_block) |
58 | - new_content = MakeISC(config_dict) |
59 | + new_content = make_isc_string(config_dict) |
60 | |
61 | # Back up and write new file. |
62 | self.back_up_existing_file(config_path) |
63 | |
64 | === added file 'src/maasserver/utils/isc.py' |
65 | --- src/maasserver/utils/isc.py 1970-01-01 00:00:00 +0000 |
66 | +++ src/maasserver/utils/isc.py 2015-07-04 09:40:25 +0000 |
67 | @@ -0,0 +1,283 @@ |
68 | +# Copyright (c) 2009, Purdue University |
69 | +# Copyright (c) 2015, Canonical Ltd. |
70 | +# All rights reserved. |
71 | +# |
72 | +# Redistribution and use in source and binary forms, with or without |
73 | +# modification, are permitted provided that the following conditions are met: |
74 | +# |
75 | +# Redistributions of source code must retain the above copyright notice, this |
76 | +# list of conditions and the following disclaimer. |
77 | +# |
78 | +# Redistributions in binary form must reproduce the above copyright notice, |
79 | +# this list of conditions and the following disclaimer in the documentation |
80 | +# and/or other materials provided with the distribution. |
81 | +# |
82 | +# Neither the name of the Purdue University nor the names of its contributors |
83 | +# may be used to endorse or promote products derived from this software without |
84 | +# specific prior written permission. |
85 | +# |
86 | +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
87 | +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
88 | +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
89 | +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE |
90 | +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
91 | +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
92 | +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
93 | +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
94 | +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
95 | +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
96 | +# POSSIBILITY OF SUCH DAMAGE. |
97 | + |
98 | +from __future__ import ( |
99 | + absolute_import, |
100 | + print_function, |
101 | + unicode_literals, |
102 | +) |
103 | + |
104 | +str = None |
105 | + |
106 | +__metaclass__ = type |
107 | +__all__ = [ |
108 | + 'make_isc_string', |
109 | + 'parse_isc_string', |
110 | +] |
111 | + |
112 | +import copy |
113 | + |
114 | + |
115 | +def _clip(char_list): |
116 | + """Clips char_list to individual stanza. |
117 | + |
118 | + Inputs: |
119 | + char_list: partial of char_list from _parse_tokens |
120 | + |
121 | + Outputs: |
122 | + tuple: (int: skip to char list index, list: shortened char_list) |
123 | + """ |
124 | + assert char_list[0] == '{' |
125 | + char_list.pop(0) |
126 | + skip = 0 |
127 | + for index, item in enumerate(char_list): |
128 | + if item == '{': |
129 | + skip += 1 |
130 | + elif item == '}' and skip == 0: |
131 | + return index, char_list[:index] |
132 | + elif item == '}': |
133 | + skip -= 1 |
134 | + raise Exception("Invalid brackets.") |
135 | + |
136 | + |
137 | +def _parse_tokens(char_list): |
138 | + """Parses exploded isc named.conf portions. |
139 | + |
140 | + Inputs: |
141 | + char_list: List of isc file parts |
142 | + |
143 | + Outputs: |
144 | + dict: fragment or full isc file dict |
145 | + Recursive dictionary of isc file, dict values can be of 3 types, |
146 | + dict, string and bool. Boolean values are always true. Booleans are false |
147 | + if key is absent. Booleans represent situations in isc files such as: |
148 | + acl "registered" { 10.1.0/32; 10.1.1:/32;}} |
149 | + |
150 | + Example: |
151 | + |
152 | + {'stanza1 "new"': 'test_info', 'stanza1 "embedded"': {'acl "registered"': |
153 | + {'10.1.0/32': True, '10.1.1/32': True}}} |
154 | + """ |
155 | + index = 0 |
156 | + dictionary_fragment = {} |
157 | + new_char_list = copy.deepcopy(char_list) |
158 | + if type(new_char_list) == str: |
159 | + return new_char_list |
160 | + if type(new_char_list) == dict: |
161 | + return new_char_list |
162 | + last_open = None |
163 | + continuous_line = False |
164 | + temp_list = [] |
165 | + |
166 | + # Prevent "may be referenced before assignment" error |
167 | + key = None |
168 | + |
169 | + while index < len(new_char_list): |
170 | + if new_char_list[index] == '{': |
171 | + last_open = index |
172 | + if new_char_list[index] == ';' and continuous_line: |
173 | + dictionary_fragment = temp_list |
174 | + temp_list = [] |
175 | + continuous_line = False |
176 | + if new_char_list[index] == ';': |
177 | + continuous_line = False |
178 | + if (len(new_char_list) > index + 1 and |
179 | + new_char_list[index] == '}' and |
180 | + new_char_list[index + 1] != ';'): |
181 | + skip, value = _clip(new_char_list[last_open:]) |
182 | + temp_list.append({key: copy.deepcopy(_parse_tokens(value))}) |
183 | + continuous_line = True |
184 | + if len(new_char_list) > index + 1 and new_char_list[index + 1] == '{': |
185 | + # assert key is not None |
186 | + key = new_char_list.pop(index) |
187 | + skip, dict_value = _clip(new_char_list[index:]) |
188 | + if continuous_line: |
189 | + temp_list.append( |
190 | + {key: copy.deepcopy(_parse_tokens(dict_value))}) |
191 | + else: |
192 | + dictionary_fragment[key] = copy.deepcopy( |
193 | + _parse_tokens(dict_value)) |
194 | + index += skip |
195 | + else: |
196 | + if len(new_char_list[ |
197 | + index].split()) == 1 and '{' not in new_char_list: |
198 | + for item in new_char_list: |
199 | + if item in [';']: |
200 | + continue |
201 | + dictionary_fragment[item] = True |
202 | + |
203 | + # If there are more than 1 'keywords' at new_char_list[index] |
204 | + # ex - "recursion no;" |
205 | + elif len(new_char_list[index].split()) >= 2: |
206 | + dictionary_fragment[ |
207 | + new_char_list[index].split()[0]] = ( |
208 | + ' '.join(new_char_list[index].split()[1:])) |
209 | + index += 1 |
210 | + |
211 | + # If there is just 1 'keyword' at new_char_list[index] |
212 | + # ex "recursion;" (not a valid option, but for example's sake it's |
213 | + # fine) |
214 | + elif new_char_list[index] not in ['{', ';', '}']: |
215 | + key = new_char_list[index] |
216 | + dictionary_fragment[key] = '' |
217 | + index += 1 |
218 | + index += 1 |
219 | + |
220 | + return dictionary_fragment |
221 | + |
222 | + |
223 | +def _scrub_comments(isc_string): |
224 | + """Clears comments from an isc file |
225 | + |
226 | + Inputs: |
227 | + isc_string: string of isc file |
228 | + Outputs: |
229 | + string: string of scrubbed isc file |
230 | + """ |
231 | + isc_list = [] |
232 | + if isc_string is None: |
233 | + return '' |
234 | + expanded_comment = False |
235 | + for line in isc_string.split('\n'): |
236 | + no_comment_line = "" |
237 | + # Vet out any inline comments |
238 | + if '/*' in line.strip(): |
239 | + try: |
240 | + striped_line = line.strip() |
241 | + chars = enumerate(striped_line) |
242 | + while True: |
243 | + i, c = chars.next() |
244 | + try: |
245 | + if c == '/' and striped_line[i + 1] == '*': |
246 | + expanded_comment = True |
247 | + chars.next() # Skip '*' |
248 | + continue |
249 | + elif c == '*' and striped_line[i + 1] == '/': |
250 | + expanded_comment = False |
251 | + chars.next() # Skip '/' |
252 | + continue |
253 | + except IndexError: |
254 | + continue # We are at the end of the line |
255 | + if expanded_comment: |
256 | + continue |
257 | + else: |
258 | + no_comment_line += c |
259 | + except StopIteration: |
260 | + if no_comment_line: |
261 | + isc_list.append(no_comment_line) |
262 | + continue |
263 | + |
264 | + if expanded_comment: |
265 | + if '*/' in line.strip(): |
266 | + expanded_comment = False |
267 | + isc_list.append(line.split('*/')[-1]) |
268 | + continue |
269 | + else: |
270 | + continue |
271 | + if line.strip().startswith(('#', '//')): |
272 | + continue |
273 | + else: |
274 | + isc_list.append(line.split('#')[0].split('//')[0].strip()) |
275 | + return '\n'.join(isc_list) |
276 | + |
277 | + |
278 | +def _explode(isc_string): |
279 | + """Explodes isc file into relevant tokens. |
280 | + |
281 | + Inputs: |
282 | + isc_string: String of isc file |
283 | + |
284 | + Outputs: |
285 | + list: list of isc file tokens delimited by brackets and semicolons |
286 | + ['stanza1 "new"', '{', 'test_info', ';', '}'] |
287 | + """ |
288 | + str_array = [] |
289 | + temp_string = [] |
290 | + for char in isc_string: |
291 | + if char in ['\n']: |
292 | + continue |
293 | + if char in ['{', '}', ';']: |
294 | + if ''.join(temp_string).strip() == '': |
295 | + str_array.append(char) |
296 | + else: |
297 | + str_array.append(''.join(temp_string).strip()) |
298 | + str_array.append(char) |
299 | + temp_string = [] |
300 | + else: |
301 | + temp_string.append(char) |
302 | + return str_array |
303 | + |
304 | + |
305 | +def parse_isc_string(isc_string): |
306 | + """Makes a dictionary from an ISC file string |
307 | + |
308 | + Inputs: |
309 | + isc_string: string of isc file |
310 | + |
311 | + Outputs: |
312 | + dict: dictionary of ISC file representation |
313 | + """ |
314 | + return _parse_tokens(_explode(_scrub_comments(isc_string))) |
315 | + |
316 | + |
317 | +def make_isc_string(isc_dict, terminate=True): |
318 | + """Outputs an isc formatted file string from a dict |
319 | + |
320 | + Inputs: |
321 | + isc_dict: a recursive dictionary to be turned into an isc file |
322 | + (from ParseTokens) |
323 | + |
324 | + Outputs: |
325 | + str: string of isc file without indentation |
326 | + """ |
327 | + if terminate: |
328 | + terminator = ';' |
329 | + else: |
330 | + terminator = '' |
331 | + if type(isc_dict) == str: |
332 | + return isc_dict |
333 | + isc_list = [] |
334 | + for option in isc_dict: |
335 | + if type(isc_dict[option]) == bool: |
336 | + isc_list.append('%s%s' % (option, terminator)) |
337 | + elif (type(isc_dict[option]) == str or |
338 | + type(isc_dict[option]) == unicode): |
339 | + isc_list.append('%s %s%s' % (option, isc_dict[option], terminator)) |
340 | + elif type(isc_dict[option]) == list: |
341 | + new_list = [] |
342 | + for item in isc_dict[option]: |
343 | + new_list.append(make_isc_string(item, terminate=False)) |
344 | + new_list[-1] = '%s%s' % (new_list[-1], terminator) |
345 | + isc_list.append( |
346 | + '%s { %s }%s' % (option, ' '.join(new_list), terminator)) |
347 | + elif type(isc_dict[option]) == dict: |
348 | + isc_list.append('%s { %s }%s' % ( |
349 | + option, make_isc_string(isc_dict[option]), terminator)) |
350 | + return '\n'.join(isc_list) |
351 | |
352 | === added file 'src/maasserver/utils/tests/test_isc.py' |
353 | --- src/maasserver/utils/tests/test_isc.py 1970-01-01 00:00:00 +0000 |
354 | +++ src/maasserver/utils/tests/test_isc.py 2015-07-04 09:40:25 +0000 |
355 | @@ -0,0 +1,179 @@ |
356 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
357 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
358 | + |
359 | +"""Test ISC configuration file parser/generator.""" |
360 | + |
361 | +from __future__ import ( |
362 | + absolute_import, |
363 | + print_function, |
364 | + unicode_literals, |
365 | +) |
366 | + |
367 | +str = None |
368 | + |
369 | +__metaclass__ = type |
370 | +__all__ = [] |
371 | + |
372 | + |
373 | +from textwrap import dedent |
374 | + |
375 | +from maasserver.utils.isc import ( |
376 | + make_isc_string, |
377 | + parse_isc_string, |
378 | +) |
379 | +from maastesting.testcase import MAASTestCase |
380 | + |
381 | + |
382 | +class TestParseISCString(MAASTestCase): |
383 | + |
384 | + def test_parses_simple_bind_options(self): |
385 | + testdata = dedent("""\ |
386 | + options { |
387 | + directory "/var/cache/bind"; |
388 | + |
389 | + dnssec-validation auto; |
390 | + |
391 | + auth-nxdomain no; # conform to RFC1035 |
392 | + listen-on-v6 { any; }; |
393 | + }; |
394 | + """) |
395 | + options = parse_isc_string(testdata) |
396 | + self.assertEqual( |
397 | + {u'options': {u'auth-nxdomain': u'no', |
398 | + u'directory': u'"/var/cache/bind"', |
399 | + u'dnssec-validation': u'auto', |
400 | + u'listen-on-v6': {u'any': True}}}, options) |
401 | + |
402 | + def test_parses_bind_acl(self): |
403 | + testdata = dedent("""\ |
404 | + acl goodclients { |
405 | + 192.0.2.0/24; |
406 | + localhost; |
407 | + localnets; |
408 | + }; |
409 | + """) |
410 | + acl = parse_isc_string(testdata) |
411 | + self.assertEqual( |
412 | + {u'acl goodclients': {u'192.0.2.0/24': True, |
413 | + u'localhost': True, |
414 | + u'localnets': True}}, acl) |
415 | + |
416 | + def test_parses_multiple_forwarders(self): |
417 | + testdata = dedent("""\ |
418 | + forwarders { |
419 | + 91.189.94.2; |
420 | + 91.189.94.3; |
421 | + 91.189.94.4; |
422 | + 91.189.94.5; |
423 | + 91.189.94.6; |
424 | + }; |
425 | + """) |
426 | + forwarders = parse_isc_string(testdata) |
427 | + self.assertEqual( |
428 | + {u'forwarders': {u'91.189.94.2': True, |
429 | + u'91.189.94.3': True, |
430 | + u'91.189.94.4': True, |
431 | + u'91.189.94.5': True, |
432 | + u'91.189.94.6': True}}, forwarders) |
433 | + |
434 | + def test_parses_bug_1413388_config(self): |
435 | + testdata = dedent("""\ |
436 | + acl canonical-int-ns { 91.189.90.151; 91.189.89.192; }; |
437 | + |
438 | + options { |
439 | + directory "/var/cache/bind"; |
440 | + |
441 | + forwarders { |
442 | + 91.189.94.2; |
443 | + 91.189.94.2; |
444 | + }; |
445 | + |
446 | + dnssec-validation auto; |
447 | + |
448 | + auth-nxdomain no; # conform to RFC1035 |
449 | + listen-on-v6 { any; }; |
450 | + |
451 | + allow-query { any; }; |
452 | + allow-transfer { 10.222.64.1; canonical-int-ns; }; |
453 | + |
454 | + notify explicit; |
455 | + also-notify { 91.189.90.151; 91.189.89.192; }; |
456 | + |
457 | + allow-query-cache { 10.222.64.0/18; }; |
458 | + recursion yes; |
459 | + }; |
460 | + |
461 | + zone "." { type master; file "/etc/bind/db.special"; }; |
462 | + """) |
463 | + config = parse_isc_string(testdata) |
464 | + self.assertEqual( |
465 | + {u'acl canonical-int-ns': |
466 | + {u'91.189.89.192': True, u'91.189.90.151': True}, |
467 | + u'options': {u'allow-query': {u'any': True}, |
468 | + u'allow-query-cache': {u'10.222.64.0/18': True}, |
469 | + u'allow-transfer': {u'10.222.64.1': True, |
470 | + u'canonical-int-ns': True}, |
471 | + u'also-notify': {u'91.189.89.192': True, |
472 | + u'91.189.90.151': True}, |
473 | + u'auth-nxdomain': u'no', |
474 | + u'directory': u'"/var/cache/bind"', |
475 | + u'dnssec-validation': u'auto', |
476 | + u'forwarders': {u'91.189.94.2': True}, |
477 | + u'listen-on-v6': {u'any': True}, |
478 | + u'notify': u'explicit', |
479 | + u'recursion': u'yes'}, |
480 | + u'zone "."': |
481 | + {u'file': u'"/etc/bind/db.special"', u'type': u'master'}}, |
482 | + config) |
483 | + |
484 | + def test_parse_then_make_then_parse_generates_identical_config(self): |
485 | + testdata = dedent("""\ |
486 | + acl canonical-int-ns { 91.189.90.151; 91.189.89.192; }; |
487 | + |
488 | + options { |
489 | + directory "/var/cache/bind"; |
490 | + |
491 | + forwarders { |
492 | + 91.189.94.2; |
493 | + 91.189.94.2; |
494 | + }; |
495 | + |
496 | + dnssec-validation auto; |
497 | + |
498 | + auth-nxdomain no; # conform to RFC1035 |
499 | + listen-on-v6 { any; }; |
500 | + |
501 | + allow-query { any; }; |
502 | + allow-transfer { 10.222.64.1; canonical-int-ns; }; |
503 | + |
504 | + notify explicit; |
505 | + also-notify { 91.189.90.151; 91.189.89.192; }; |
506 | + |
507 | + allow-query-cache { 10.222.64.0/18; }; |
508 | + recursion yes; |
509 | + }; |
510 | + |
511 | + zone "." { type master; file "/etc/bind/db.special"; }; |
512 | + """) |
513 | + config = parse_isc_string(testdata) |
514 | + config_string = make_isc_string(config) |
515 | + config = parse_isc_string(config_string) |
516 | + self.assertEqual( |
517 | + {u'acl canonical-int-ns': |
518 | + {u'91.189.89.192': True, u'91.189.90.151': True}, |
519 | + u'options': {u'allow-query': {u'any': True}, |
520 | + u'allow-query-cache': {u'10.222.64.0/18': True}, |
521 | + u'allow-transfer': {u'10.222.64.1': True, |
522 | + u'canonical-int-ns': True}, |
523 | + u'also-notify': {u'91.189.89.192': True, |
524 | + u'91.189.90.151': True}, |
525 | + u'auth-nxdomain': u'no', |
526 | + u'directory': u'"/var/cache/bind"', |
527 | + u'dnssec-validation': u'auto', |
528 | + u'forwarders': {u'91.189.94.2': True}, |
529 | + u'listen-on-v6': {u'any': True}, |
530 | + u'notify': u'explicit', |
531 | + u'recursion': u'yes'}, |
532 | + u'zone "."': |
533 | + {u'file': u'"/etc/bind/db.special"', u'type': u'master'}}, |
534 | + config) |
535 | |
536 | === modified file 'versions.cfg' |
537 | --- versions.cfg 2015-05-06 09:51:19 +0000 |
538 | +++ versions.cfg 2015-07-04 09:40:25 +0000 |
539 | @@ -35,7 +35,6 @@ |
540 | extras = 0.0.3 |
541 | fixtures = 0.3.14 |
542 | httplib2 = 0.8 |
543 | -iscpy = 1.05 |
544 | iso8601 = 0.1.4 |
545 | junitxml = 0.6 |
546 | nose = 1.3.1 |
This is the same code that was approved for 1.7 and trunk, and landed on trunk. (Self-review might be appropriate in this circumstance?)