Merge lp:~free.ekanayaka/landscape-client/drop-configobjd-and-amp into lp:~landscape/landscape-client/trunk
- drop-configobjd-and-amp
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Free Ekanayaka |
Approved revision: | 646 |
Merged at revision: | 650 |
Proposed branch: | lp:~free.ekanayaka/landscape-client/drop-configobjd-and-amp |
Merge into: | lp:~landscape/landscape-client/trunk |
Diff against target: |
6351 lines (+171/-6035) 7 files modified
landscape/broker/registration.py (+6/-6) landscape/broker/store.py (+102/-19) landscape/lib/amp.py (+2/-6) landscape/lib/configobj.py (+0/-3728) landscape/lib/persist.py (+60/-46) landscape/lib/tests/test_persist.py (+1/-7) landscape/lib/twisted_amp.py (+0/-2223) |
To merge this branch: | bzr merge lp:~free.ekanayaka/landscape-client/drop-configobjd-and-amp |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Björn Tillenius (community) | Approve | ||
Alberto Donato (community) | Approve | ||
Review via email: mp+157421@code.launchpad.net |
Commit message
This branch:
- drops landscape.
as they are not used
- drops landscape.
- improves the module docstring of landscape.
in detail the logic behind "sequence" and "pending offset" when dealing
with message exchanges
- improves docstrings in landscape.
Description of the change
This branch is quite large but only have trivial changes:
- drops landscape.
- drops landscape.
- improves the module docstring of landscape.
- improves docstrings in landscape.
Preview Diff
1 | === modified file 'landscape/broker/registration.py' |
2 | --- landscape/broker/registration.py 2013-04-03 14:40:57 +0000 |
3 | +++ landscape/broker/registration.py 2013-04-05 16:00:29 +0000 |
4 | @@ -110,14 +110,14 @@ |
5 | |
6 | def should_register(self): |
7 | id = self._identity |
8 | - # boolean logic is hard, I'm gonna use an if |
9 | + if id.secure_id: |
10 | + # We already have a secure ID, no need to register |
11 | + return False |
12 | if self._config.cloud: |
13 | - return bool(not id.secure_id |
14 | - and self._message_store.accepts("register-cloud-vm")) |
15 | + return self._message_store.accepts("register-cloud-vm") |
16 | elif self._config.provisioning_otp: |
17 | - return (not id.secure_id) and\ |
18 | - self._message_store.accepts("register-provisioned-machine") |
19 | - return bool(not id.secure_id and id.computer_title and id.account_name |
20 | + return self._message_store.accepts("register-provisioned-machine") |
21 | + return bool(id.computer_title and id.account_name |
22 | and self._message_store.accepts("register")) |
23 | |
24 | def register(self): |
25 | |
26 | === modified file 'landscape/broker/store.py' |
27 | --- landscape/broker/store.py 2013-03-31 10:09:42 +0000 |
28 | +++ landscape/broker/store.py 2013-04-05 16:00:29 +0000 |
29 | @@ -1,4 +1,96 @@ |
30 | -"""Message storage.""" |
31 | +"""Message storage. |
32 | + |
33 | +The sequencing system we use in the message store may be quite confusing |
34 | +if you haven't looked at it in the last 10 minutes. For that reason, let's |
35 | +review the mechanics here. |
36 | + |
37 | +Our goal is to implement a reasonably robust system for delivering messages |
38 | +from us to our peer. The system should be smart enough to recover if the peer |
39 | +happens to lose messages that we have already sent, provided that these |
40 | +messages are not too old (we'll see below what 'too old' means). |
41 | + |
42 | +Messages added to the store are identified by increasing natural numbers, the |
43 | +first message added is identified by 0, the second by 1, and so on. We call |
44 | +"sequence" the number identifying the next message that we want to send. For |
45 | +example, if the store has been added ten messages (that we represent with |
46 | +uppercase letters) and we want start sending the first of them, our store |
47 | +would like like:: |
48 | + |
49 | + sequence: 0 |
50 | + messages: A, B, C, D, E, F, G, H, I, J |
51 | + ^ |
52 | + |
53 | +The "^" marker is what we call "pending offset" and is the displacement of the |
54 | +message we want to send next from the first message we have in the store. |
55 | + |
56 | +Let's say we now send to our peer a batch of 3 sequential messages. In the |
57 | +payload we include the body of the messages being sent and the sequence, which |
58 | +identifies the first message of the batch. In this case the payload would look |
59 | +like (pseudo-code):: |
60 | + |
61 | + (sequence: 0, messages: A, B, C) |
62 | + |
63 | +If everything works fine on the other end, our peer replies with a payload that |
64 | +would like:: |
65 | + |
66 | + (next-expected-sequence: 4) |
67 | + |
68 | +meaning that the peer as received all the three messages that we sent, an so |
69 | +the next message it expects to receive is the one identified by the number 4. |
70 | +At this point we update both our pending offset and our sequence values, and |
71 | +the store now looks like:: |
72 | + |
73 | + sequence: 4 |
74 | + messages: A, B, C, D, E, F, G, H, I, J |
75 | + ^ |
76 | + |
77 | +Great, now let's pretend that we send another batch, this time with five |
78 | +messages:: |
79 | + |
80 | + (sequence: 4, messages: D, E, F, G, H) |
81 | + |
82 | +Our peer receives them fine responding with a payload looking like:: |
83 | + |
84 | + (next-expected-sequence: 9) |
85 | + |
86 | +meaning that it received all the eight messages we sent so far and it's waiting |
87 | +for the ninth. This is the second successful batch that we send in a row, so we |
88 | +can be reasonably confident that at least the messages in the first batch are |
89 | +not really needed anymore. We delete them and we update our sequence and |
90 | +pending offset accordingly:: |
91 | + |
92 | + sequence: 9 |
93 | + messages: D, E, F, G, H, I, J |
94 | + ^ |
95 | + |
96 | +Note that we still want to keep around the messages we sent in the very last |
97 | +batch, just in case. Indeed we now try to send a third batch with the last two |
98 | +messages that we have, but our peer surprisingly replies us with this payload:: |
99 | + |
100 | + (next-expected-sequence: 6) |
101 | + |
102 | +Ouch! This means that something bad happened and our peer has somehow lost not |
103 | +only the two messages that we sent in the last batch, but also the last three |
104 | +messages of the former batch :( |
105 | + |
106 | +Luckly we've kept enough old messages around that we can try to send them |
107 | +again, we update our sequence and pending offset and the store looks like:: |
108 | + |
109 | + sequence: 6 |
110 | + messages: D, E, F, G, H, I, J |
111 | + ^ |
112 | + |
113 | +We can now start again sending messages using the same strategy. |
114 | + |
115 | +Note however that in the worst case scenario we could receive from our peer |
116 | +a next-expected-sequence value which is so old to be outside our buffer |
117 | +of already-sent messages. In that case there is now way we can recover the |
118 | +lost messages, and we'll just send the oldest one that we have. |
119 | + |
120 | +See L{MessageStore} for details about how messages are stored on the file |
121 | +system and L{landscape.lib.message.got_next_expected} to check how the |
122 | +strategy for updating the pending offset and the sequence is implemented. |
123 | +""" |
124 | |
125 | import time |
126 | import itertools |
127 | @@ -16,24 +108,15 @@ |
128 | class MessageStore(object): |
129 | """A message store which stores its messages in a file system hierarchy. |
130 | |
131 | - The sequencing system we use in the message store may be quite |
132 | - confusing if you haven't looked at it in the last 10 minutes. For |
133 | - that reason, let's review the terminology here. |
134 | - |
135 | - Assume we have 10 messages in the store, which we label by |
136 | - the following uppercase letters:: |
137 | - |
138 | - A, B, C, D, E, F, G, H, I, J |
139 | - ^ |
140 | - |
141 | - Let's say that the next message we should send to the server is D. |
142 | - What we call "pending offset" is the displacement from the first |
143 | - message, which in our example above would be 3. What we call |
144 | - "sequence" is the number that the server expects us to label message |
145 | - D as. It could be pretty much any natural number, depending on the |
146 | - history of our exchanges with the server. What we call "server |
147 | - sequence", is the next message number expected by the *client* itself, |
148 | - and is entirely unrelated to the stored messages. |
149 | + Beside the "sequence" and the "pending offset" values described in the |
150 | + module docstring above, the L{MessageStore} also stores what we call |
151 | + "server sequence", which is the next message number expected by the |
152 | + *client* itself (because we are in turn the peer of a specular message |
153 | + system running in the server, which tries to deliver messages to us). |
154 | + |
155 | + The server sequence is entirely unrelated to the stored messages, but is |
156 | + incremented when successfully receiving messages from the server, in the |
157 | + very same way described above but with the roles inverted. |
158 | |
159 | @param persist: a L{Persist} used to save state parameters like the |
160 | accepted message types, sequence, server uuid etc. |
161 | |
162 | === modified file 'landscape/lib/amp.py' |
163 | --- landscape/lib/amp.py 2013-03-27 22:58:20 +0000 |
164 | +++ landscape/lib/amp.py 2013-04-05 16:00:29 +0000 |
165 | @@ -4,12 +4,8 @@ |
166 | from twisted.internet.protocol import ReconnectingClientFactory |
167 | from twisted.python.failure import Failure |
168 | |
169 | -try: |
170 | - from twisted.protocols.amp import ( |
171 | - Argument, String, Integer, Command, AMP, MAX_VALUE_LENGTH) |
172 | -except ImportError: |
173 | - from landscape.lib.twisted_amp import ( |
174 | - Argument, String, Integer, Command, AMP, MAX_VALUE_LENGTH) |
175 | +from twisted.protocols.amp import ( |
176 | + Argument, String, Integer, Command, AMP, MAX_VALUE_LENGTH) |
177 | |
178 | from landscape.lib.bpickle import loads, dumps, dumps_table |
179 | |
180 | |
181 | === removed file 'landscape/lib/configobj.py' |
182 | --- landscape/lib/configobj.py 2010-04-28 13:07:53 +0000 |
183 | +++ landscape/lib/configobj.py 1970-01-01 00:00:00 +0000 |
184 | @@ -1,3728 +0,0 @@ |
185 | -# configobj.py |
186 | -# A config file reader/writer that supports nested sections in config files. |
187 | -# Copyright (C) 2005-2006 Michael Foord, Nicola Larosa |
188 | -# E-mail: fuzzyman AT voidspace DOT org DOT uk |
189 | -# nico AT tekNico DOT net |
190 | - |
191 | -# ConfigObj 4 |
192 | -# http://www.voidspace.org.uk/python/configobj.html |
193 | - |
194 | -# Released subject to the BSD License |
195 | -# Please see http://www.voidspace.org.uk/python/license.shtml |
196 | - |
197 | -# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml |
198 | -# For information about bugfixes, updates and support, please join the |
199 | -# ConfigObj mailing list: |
200 | -# http://lists.sourceforge.net/lists/listinfo/configobj-develop |
201 | -# Comments, suggestions and bug reports welcome. |
202 | - |
203 | -from __future__ import generators |
204 | - |
205 | -""" |
206 | - >>> z = ConfigObj() |
207 | - >>> z['a'] = 'a' |
208 | - >>> z['sect'] = { |
209 | - ... 'subsect': { |
210 | - ... 'a': 'fish', |
211 | - ... 'b': 'wobble', |
212 | - ... }, |
213 | - ... 'member': 'value', |
214 | - ... } |
215 | - >>> x = ConfigObj(z.write()) |
216 | - >>> z == x |
217 | - 1 |
218 | -""" |
219 | - |
220 | -import sys |
221 | -INTP_VER = sys.version_info[:2] |
222 | -if INTP_VER < (2, 2): |
223 | - raise RuntimeError("Python v.2.2 or later needed") |
224 | - |
225 | -import os |
226 | -import re |
227 | -import compiler |
228 | -from types import StringTypes |
229 | -from warnings import warn |
230 | -from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE |
231 | - |
232 | -# A dictionary mapping BOM to |
233 | -# the encoding to decode with, and what to set the |
234 | -# encoding attribute to. |
235 | -BOMS = { |
236 | - BOM_UTF8: ('utf_8', None), |
237 | - BOM_UTF16_BE: ('utf16_be', 'utf_16'), |
238 | - BOM_UTF16_LE: ('utf16_le', 'utf_16'), |
239 | - BOM_UTF16: ('utf_16', 'utf_16'), |
240 | - } |
241 | -# All legal variants of the BOM codecs. |
242 | -# TODO: the list of aliases is not meant to be exhaustive, is there a |
243 | -# better way ? |
244 | -BOM_LIST = { |
245 | - 'utf_16': 'utf_16', |
246 | - 'u16': 'utf_16', |
247 | - 'utf16': 'utf_16', |
248 | - 'utf-16': 'utf_16', |
249 | - 'utf16_be': 'utf16_be', |
250 | - 'utf_16_be': 'utf16_be', |
251 | - 'utf-16be': 'utf16_be', |
252 | - 'utf16_le': 'utf16_le', |
253 | - 'utf_16_le': 'utf16_le', |
254 | - 'utf-16le': 'utf16_le', |
255 | - 'utf_8': 'utf_8', |
256 | - 'u8': 'utf_8', |
257 | - 'utf': 'utf_8', |
258 | - 'utf8': 'utf_8', |
259 | - 'utf-8': 'utf_8', |
260 | - } |
261 | - |
262 | -# Map of encodings to the BOM to write. |
263 | -BOM_SET = { |
264 | - 'utf_8': BOM_UTF8, |
265 | - 'utf_16': BOM_UTF16, |
266 | - 'utf16_be': BOM_UTF16_BE, |
267 | - 'utf16_le': BOM_UTF16_LE, |
268 | - None: BOM_UTF8} |
269 | - |
270 | -try: |
271 | - from validate import VdtMissingValue |
272 | -except ImportError: |
273 | - VdtMissingValue = None |
274 | - |
275 | -try: |
276 | - enumerate |
277 | -except NameError: |
278 | - |
279 | - def enumerate(obj): |
280 | - """enumerate for Python 2.2.""" |
281 | - i = -1 |
282 | - for item in obj: |
283 | - i += 1 |
284 | - yield i, item |
285 | - |
286 | -try: |
287 | - True, False |
288 | -except NameError: |
289 | - True, False = 1, 0 |
290 | - |
291 | - |
292 | -__version__ = '4.3.0alpha2' |
293 | - |
294 | -__revision__ = '$Id: configobj.py 156 2006-01-31 14:57:08Z fuzzyman $' |
295 | - |
296 | -__docformat__ = "restructuredtext en" |
297 | - |
298 | -# NOTE: Does it make sense to have the following in __all__ ? |
299 | -# NOTE: DEFAULT_INDENT_TYPE, NUM_INDENT_SPACES, MAX_INTERPOL_DEPTH |
300 | -# NOTE: If used via ``from configobj import...`` |
301 | -# NOTE: They are effectively read only |
302 | -__all__ = ( |
303 | - '__version__', |
304 | - 'DEFAULT_INDENT_TYPE', |
305 | - 'NUM_INDENT_SPACES', |
306 | - 'MAX_INTERPOL_DEPTH', |
307 | - 'ConfigObjError', |
308 | - 'NestingError', |
309 | - 'ParseError', |
310 | - 'DuplicateError', |
311 | - 'ConfigspecError', |
312 | - 'ConfigObj', |
313 | - 'SimpleVal', |
314 | - 'InterpolationError', |
315 | - 'InterpolationDepthError', |
316 | - 'MissingInterpolationOption', |
317 | - 'RepeatSectionError', |
318 | - '__docformat__', |
319 | - 'flatten_errors', |
320 | -) |
321 | - |
322 | -DEFAULT_INDENT_TYPE = ' ' |
323 | -NUM_INDENT_SPACES = 4 |
324 | -MAX_INTERPOL_DEPTH = 10 |
325 | - |
326 | -OPTION_DEFAULTS = { |
327 | - 'interpolation': True, |
328 | - 'raise_errors': False, |
329 | - 'list_values': True, |
330 | - 'create_empty': False, |
331 | - 'file_error': False, |
332 | - 'configspec': None, |
333 | - 'stringify': True, |
334 | - # option may be set to one of ('', ' ', '\t') |
335 | - 'indent_type': None, |
336 | - 'encoding': None, |
337 | - 'default_encoding': None, |
338 | - 'unrepr': False, |
339 | - 'write_empty_values': False, |
340 | -} |
341 | - |
342 | - |
343 | -def getObj(s): |
344 | - s = "a=" + s |
345 | - p = compiler.parse(s) |
346 | - return p.getChildren()[1].getChildren()[0].getChildren()[1] |
347 | - |
348 | - |
349 | -class UnknownType(Exception): |
350 | - pass |
351 | - |
352 | - |
353 | -class Builder: |
354 | - |
355 | - def build(self, o): |
356 | - m = getattr(self, 'build_' + o.__class__.__name__, None) |
357 | - if m is None: |
358 | - raise UnknownType(o.__class__.__name__) |
359 | - return m(o) |
360 | - |
361 | - def build_List(self, o): |
362 | - return map(self.build, o.getChildren()) |
363 | - |
364 | - def build_Const(self, o): |
365 | - return o.value |
366 | - |
367 | - def build_Dict(self, o): |
368 | - d = {} |
369 | - i = iter(map(self.build, o.getChildren())) |
370 | - for el in i: |
371 | - d[el] = i.next() |
372 | - return d |
373 | - |
374 | - def build_Tuple(self, o): |
375 | - return tuple(self.build_List(o)) |
376 | - |
377 | - def build_Name(self, o): |
378 | - if o.name == 'None': |
379 | - return None |
380 | - if o.name == 'True': |
381 | - return True |
382 | - if o.name == 'False': |
383 | - return False |
384 | - |
385 | - # An undefinted Name |
386 | - raise UnknownType('Undefined Name') |
387 | - |
388 | - def build_Add(self, o): |
389 | - real, imag = map(self.build_Const, o.getChildren()) |
390 | - try: |
391 | - real = float(real) |
392 | - except TypeError: |
393 | - raise UnknownType('Add') |
394 | - if not isinstance(imag, complex) or imag.real != 0.0: |
395 | - raise UnknownType('Add') |
396 | - return real + imag |
397 | - |
398 | - def build_Getattr(self, o): |
399 | - parent = self.build(o.expr) |
400 | - return getattr(parent, o.attrname) |
401 | - |
402 | - |
403 | -def unrepr(s): |
404 | - if not s: |
405 | - return s |
406 | - return Builder().build(getObj(s)) |
407 | - |
408 | - |
409 | -class ConfigObjError(SyntaxError): |
410 | - """ |
411 | - This is the base class for all errors that ConfigObj raises. |
412 | - It is a subclass of SyntaxError. |
413 | - |
414 | - >>> raise ConfigObjError |
415 | - Traceback (most recent call last): |
416 | - ConfigObjError |
417 | - """ |
418 | - |
419 | - def __init__(self, message='', line_number=None, line=''): |
420 | - self.line = line |
421 | - self.line_number = line_number |
422 | - self.message = message |
423 | - SyntaxError.__init__(self, message) |
424 | - |
425 | - |
426 | -class NestingError(ConfigObjError): |
427 | - """ |
428 | - This error indicates a level of nesting that doesn't match. |
429 | - |
430 | - >>> raise NestingError |
431 | - Traceback (most recent call last): |
432 | - NestingError |
433 | - """ |
434 | - |
435 | - |
436 | -class ParseError(ConfigObjError): |
437 | - """ |
438 | - This error indicates that a line is badly written. |
439 | - It is neither a valid ``key = value`` line, |
440 | - nor a valid section marker line. |
441 | - |
442 | - >>> raise ParseError |
443 | - Traceback (most recent call last): |
444 | - ParseError |
445 | - """ |
446 | - |
447 | - |
448 | -class DuplicateError(ConfigObjError): |
449 | - """ |
450 | - The keyword or section specified already exists. |
451 | - |
452 | - >>> raise DuplicateError |
453 | - Traceback (most recent call last): |
454 | - DuplicateError |
455 | - """ |
456 | - |
457 | - |
458 | -class ConfigspecError(ConfigObjError): |
459 | - """ |
460 | - An error occured whilst parsing a configspec. |
461 | - |
462 | - >>> raise ConfigspecError |
463 | - Traceback (most recent call last): |
464 | - ConfigspecError |
465 | - """ |
466 | - |
467 | - |
468 | -class InterpolationError(ConfigObjError): |
469 | - """Base class for the two interpolation errors.""" |
470 | - |
471 | - |
472 | -class InterpolationDepthError(InterpolationError): |
473 | - """Maximum interpolation depth exceeded in string interpolation.""" |
474 | - |
475 | - def __init__(self, option): |
476 | - """ |
477 | - >>> raise InterpolationDepthError('yoda') |
478 | - Traceback (most recent call last): |
479 | - InterpolationDepthError: max interpolation depth exceeded in value "yoda". |
480 | - """ |
481 | - InterpolationError.__init__( |
482 | - self, |
483 | - 'max interpolation depth exceeded in value "%s".' % option) |
484 | - |
485 | - |
486 | -class RepeatSectionError(ConfigObjError): |
487 | - """ |
488 | - This error indicates additional sections in a section with a |
489 | - ``__many__`` (repeated) section. |
490 | - |
491 | - >>> raise RepeatSectionError |
492 | - Traceback (most recent call last): |
493 | - RepeatSectionError |
494 | - """ |
495 | - |
496 | - |
497 | -class MissingInterpolationOption(InterpolationError): |
498 | - """A value specified for interpolation was missing.""" |
499 | - |
500 | - def __init__(self, option): |
501 | - """ |
502 | - >>> raise MissingInterpolationOption('yoda') |
503 | - Traceback (most recent call last): |
504 | - MissingInterpolationOption: missing option "yoda" in interpolation. |
505 | - """ |
506 | - InterpolationError.__init__( |
507 | - self, |
508 | - 'missing option "%s" in interpolation.' % option) |
509 | - |
510 | - |
511 | -class Section(dict): |
512 | - """ |
513 | - A dictionary-like object that represents a section in a config file. |
514 | - |
515 | - It does string interpolation if the 'interpolate' attribute |
516 | - of the 'main' object is set to True. |
517 | - |
518 | - Interpolation is tried first from the 'DEFAULT' section of this object, |
519 | - next from the 'DEFAULT' section of the parent, lastly the main object. |
520 | - |
521 | - A Section will behave like an ordered dictionary - following the |
522 | - order of the ``scalars`` and ``sections`` attributes. |
523 | - You can use this to change the order of members. |
524 | - |
525 | - Iteration follows the order: scalars, then sections. |
526 | - """ |
527 | - |
528 | - _KEYCRE = re.compile(r"%\(([^)]*)\)s|.") |
529 | - |
530 | - def __init__(self, parent, depth, main, indict=None, name=None): |
531 | - """ |
532 | - * parent is the section above |
533 | - * depth is the depth level of this section |
534 | - * main is the main ConfigObj |
535 | - * indict is a dictionary to initialise the section with |
536 | - """ |
537 | - if indict is None: |
538 | - indict = {} |
539 | - dict.__init__(self) |
540 | - # used for nesting level *and* interpolation |
541 | - self.parent = parent |
542 | - # used for the interpolation attribute |
543 | - self.main = main |
544 | - # level of nesting depth of this Section |
545 | - self.depth = depth |
546 | - # the sequence of scalar values in this Section |
547 | - self.scalars = [] |
548 | - # the sequence of sections in this Section |
549 | - self.sections = [] |
550 | - # purely for information |
551 | - self.name = name |
552 | - # for comments :-) |
553 | - self.comments = {} |
554 | - self.inline_comments = {} |
555 | - # for the configspec |
556 | - self.configspec = {} |
557 | - self._order = [] |
558 | - self._configspec_comments = {} |
559 | - self._configspec_inline_comments = {} |
560 | - self._cs_section_comments = {} |
561 | - self._cs_section_inline_comments = {} |
562 | - # for defaults |
563 | - self.defaults = [] |
564 | - # |
565 | - # we do this explicitly so that __setitem__ is used properly |
566 | - # (rather than just passing to ``dict.__init__``) |
567 | - for entry in indict: |
568 | - self[entry] = indict[entry] |
569 | - |
570 | - def _interpolate(self, value): |
571 | - """Nicked from ConfigParser.""" |
572 | - depth = MAX_INTERPOL_DEPTH |
573 | - # loop through this until it's done |
574 | - while depth: |
575 | - depth -= 1 |
576 | - if value.find("%(") != -1: |
577 | - value = self._KEYCRE.sub(self._interpolation_replace, value) |
578 | - else: |
579 | - break |
580 | - else: |
581 | - raise InterpolationDepthError(value) |
582 | - return value |
583 | - |
584 | - def _interpolation_replace(self, match): |
585 | - """ """ |
586 | - s = match.group(1) |
587 | - if s is None: |
588 | - return match.group() |
589 | - else: |
590 | - # switch off interpolation before we try and fetch anything ! |
591 | - self.main.interpolation = False |
592 | - # try the 'DEFAULT' member of *this section* first |
593 | - val = self.get('DEFAULT', {}).get(s) |
594 | - # try the 'DEFAULT' member of the *parent section* next |
595 | - if val is None: |
596 | - val = self.parent.get('DEFAULT', {}).get(s) |
597 | - # last, try the 'DEFAULT' member of the *main section* |
598 | - if val is None: |
599 | - val = self.main.get('DEFAULT', {}).get(s) |
600 | - self.main.interpolation = True |
601 | - if val is None: |
602 | - raise MissingInterpolationOption(s) |
603 | - return val |
604 | - |
605 | - def __getitem__(self, key): |
606 | - """Fetch the item and do string interpolation.""" |
607 | - val = dict.__getitem__(self, key) |
608 | - if self.main.interpolation and isinstance(val, StringTypes): |
609 | - return self._interpolate(val) |
610 | - return val |
611 | - |
612 | - def __setitem__(self, key, value, unrepr=False): |
613 | - """ |
614 | - Correctly set a value. |
615 | - |
616 | - Making dictionary values Section instances. |
617 | - (We have to special case 'Section' instances - which are also dicts) |
618 | - |
619 | - Keys must be strings. |
620 | - Values need only be strings (or lists of strings) if |
621 | - ``main.stringify`` is set. |
622 | - |
623 | - `unrepr`` must be set when setting a value to a dictionary, without |
624 | - creating a new sub-section. |
625 | - """ |
626 | - if not isinstance(key, StringTypes): |
627 | - raise ValueError, 'The key "%s" is not a string.' % key |
628 | - # add the comment |
629 | - if not key in self.comments: |
630 | - self.comments[key] = [] |
631 | - self.inline_comments[key] = '' |
632 | - # remove the entry from defaults |
633 | - if key in self.defaults: |
634 | - self.defaults.remove(key) |
635 | - # |
636 | - if isinstance(value, Section): |
637 | - if not self.has_key(key): |
638 | - self.sections.append(key) |
639 | - dict.__setitem__(self, key, value) |
640 | - elif isinstance(value, dict)and not unrepr: |
641 | - # First create the new depth level, |
642 | - # then create the section |
643 | - if not self.has_key(key): |
644 | - self.sections.append(key) |
645 | - new_depth = self.depth + 1 |
646 | - dict.__setitem__( |
647 | - self, |
648 | - key, |
649 | - Section( |
650 | - self, |
651 | - new_depth, |
652 | - self.main, |
653 | - indict=value, |
654 | - name=key)) |
655 | - else: |
656 | - if not self.has_key(key): |
657 | - self.scalars.append(key) |
658 | - if not self.main.stringify: |
659 | - if isinstance(value, StringTypes): |
660 | - pass |
661 | - elif isinstance(value, (list, tuple)): |
662 | - for entry in value: |
663 | - if not isinstance(entry, StringTypes): |
664 | - raise TypeError, ( |
665 | - 'Value is not a string "%s".' % entry) |
666 | - else: |
667 | - raise TypeError, 'Value is not a string "%s".' % value |
668 | - dict.__setitem__(self, key, value) |
669 | - |
670 | - def __delitem__(self, key): |
671 | - """Remove items from the sequence when deleting.""" |
672 | - dict. __delitem__(self, key) |
673 | - if key in self.scalars: |
674 | - self.scalars.remove(key) |
675 | - else: |
676 | - self.sections.remove(key) |
677 | - del self.comments[key] |
678 | - del self.inline_comments[key] |
679 | - |
680 | - def get(self, key, default=None): |
681 | - """A version of ``get`` that doesn't bypass string interpolation.""" |
682 | - try: |
683 | - return self[key] |
684 | - except KeyError: |
685 | - return default |
686 | - |
687 | - def update(self, indict): |
688 | - """ |
689 | - A version of update that uses our ``__setitem__``. |
690 | - """ |
691 | - for entry in indict: |
692 | - self[entry] = indict[entry] |
693 | - |
694 | - def pop(self, key, *args): |
695 | - """ """ |
696 | - val = dict.pop(self, key, *args) |
697 | - if key in self.scalars: |
698 | - del self.comments[key] |
699 | - del self.inline_comments[key] |
700 | - self.scalars.remove(key) |
701 | - elif key in self.sections: |
702 | - del self.comments[key] |
703 | - del self.inline_comments[key] |
704 | - self.sections.remove(key) |
705 | - if self.main.interpolation and isinstance(val, StringTypes): |
706 | - return self._interpolate(val) |
707 | - return val |
708 | - |
709 | - def popitem(self): |
710 | - """Pops the first (key,val)""" |
711 | - sequence = (self.scalars + self.sections) |
712 | - if not sequence: |
713 | - raise KeyError, ": 'popitem(): dictionary is empty'" |
714 | - key = sequence[0] |
715 | - val = self[key] |
716 | - del self[key] |
717 | - return key, val |
718 | - |
719 | - def clear(self): |
720 | - """ |
721 | - A version of clear that also affects scalars/sections |
722 | - Also clears comments and configspec. |
723 | - |
724 | - Leaves other attributes alone : |
725 | - depth/main/parent are not affected |
726 | - """ |
727 | - dict.clear(self) |
728 | - self.scalars = [] |
729 | - self.sections = [] |
730 | - self.comments = {} |
731 | - self.inline_comments = {} |
732 | - self.configspec = {} |
733 | - |
734 | - def setdefault(self, key, default=None): |
735 | - """A version of setdefault that sets sequence if appropriate.""" |
736 | - try: |
737 | - return self[key] |
738 | - except KeyError: |
739 | - self[key] = default |
740 | - return self[key] |
741 | - |
742 | - def items(self): |
743 | - """ """ |
744 | - return zip((self.scalars + self.sections), self.values()) |
745 | - |
746 | - def keys(self): |
747 | - """ """ |
748 | - return (self.scalars + self.sections) |
749 | - |
750 | - def values(self): |
751 | - """ """ |
752 | - return [self[key] for key in (self.scalars + self.sections)] |
753 | - |
754 | - def iteritems(self): |
755 | - """ """ |
756 | - return iter(self.items()) |
757 | - |
758 | - def iterkeys(self): |
759 | - """ """ |
760 | - return iter((self.scalars + self.sections)) |
761 | - |
762 | - __iter__ = iterkeys |
763 | - |
764 | - def itervalues(self): |
765 | - """ """ |
766 | - return iter(self.values()) |
767 | - |
768 | - def __repr__(self): |
769 | - return '{%s}' % ', '.join([('%s: %s' % (repr(key), repr(self[key]))) |
770 | - for key in (self.scalars + self.sections)]) |
771 | - |
772 | - __str__ = __repr__ |
773 | - |
774 | - # Extra methods - not in a normal dictionary |
775 | - |
776 | - def dict(self): |
777 | - """ |
778 | - Return a deepcopy of self as a dictionary. |
779 | - |
780 | - All members that are ``Section`` instances are recursively turned to |
781 | - ordinary dictionaries - by calling their ``dict`` method. |
782 | - |
783 | - >>> n = a.dict() |
784 | - >>> n == a |
785 | - 1 |
786 | - >>> n is a |
787 | - 0 |
788 | - """ |
789 | - newdict = {} |
790 | - for entry in self: |
791 | - this_entry = self[entry] |
792 | - if isinstance(this_entry, Section): |
793 | - this_entry = this_entry.dict() |
794 | - # XXX Modified to return tuples as tuples. -- niemeyer, 2006-04-24 |
795 | - elif isinstance(this_entry, list): |
796 | - this_entry = list(this_entry) |
797 | - elif isinstance(this_entry, tuple): |
798 | - this_entry = tuple(this_entry) |
799 | - newdict[entry] = this_entry |
800 | - return newdict |
801 | - |
802 | - def merge(self, indict): |
803 | - """ |
804 | - A recursive update - useful for merging config files. |
805 | - |
806 | - >>> a = '''[section1] |
807 | - ... option1 = True |
808 | - ... [[subsection]] |
809 | - ... more_options = False |
810 | - ... # end of file'''.splitlines() |
811 | - >>> b = '''# File is user.ini |
812 | - ... [section1] |
813 | - ... option1 = False |
814 | - ... # end of file'''.splitlines() |
815 | - >>> c1 = ConfigObj(b) |
816 | - >>> c2 = ConfigObj(a) |
817 | - >>> c2.merge(c1) |
818 | - >>> c2 |
819 | - {'section1': {'option1': 'False', 'subsection': {'more_options': 'False'}}} |
820 | - """ |
821 | - for key, val in indict.items(): |
822 | - if (key in self and isinstance(self[key], dict) and |
823 | - isinstance(val, dict)): |
824 | - self[key].merge(val) |
825 | - else: |
826 | - self[key] = val |
827 | - |
828 | - def rename(self, oldkey, newkey): |
829 | - """ |
830 | - Change a keyname to another, without changing position in sequence. |
831 | - |
832 | - Implemented so that transformations can be made on keys, |
833 | - as well as on values. (used by encode and decode) |
834 | - |
835 | - Also renames comments. |
836 | - """ |
837 | - if oldkey in self.scalars: |
838 | - the_list = self.scalars |
839 | - elif oldkey in self.sections: |
840 | - the_list = self.sections |
841 | - else: |
842 | - raise KeyError, 'Key "%s" not found.' % oldkey |
843 | - pos = the_list.index(oldkey) |
844 | - # |
845 | - val = self[oldkey] |
846 | - dict.__delitem__(self, oldkey) |
847 | - dict.__setitem__(self, newkey, val) |
848 | - the_list.remove(oldkey) |
849 | - the_list.insert(pos, newkey) |
850 | - comm = self.comments[oldkey] |
851 | - inline_comment = self.inline_comments[oldkey] |
852 | - del self.comments[oldkey] |
853 | - del self.inline_comments[oldkey] |
854 | - self.comments[newkey] = comm |
855 | - self.inline_comments[newkey] = inline_comment |
856 | - |
857 | - def walk(self, function, raise_errors=True, |
858 | - call_on_sections=False, **keywargs): |
859 | - """ |
860 | - Walk every member and call a function on the keyword and value. |
861 | - |
862 | - Return a dictionary of the return values |
863 | - |
864 | - If the function raises an exception, raise the errror |
865 | - unless ``raise_errors=False``, in which case set the return value to |
866 | - ``False``. |
867 | - |
868 | - Any unrecognised keyword arguments you pass to walk, will be pased on |
869 | - to the function you pass in. |
870 | - |
871 | - Note: if ``call_on_sections`` is ``True`` then - on encountering a |
872 | - subsection, *first* the function is called for the *whole* subsection, |
873 | - and then recurses into it's members. This means your function must be |
874 | - able to handle strings, dictionaries and lists. This allows you |
875 | - to change the key of subsections as well as for ordinary members. The |
876 | - return value when called on the whole subsection has to be discarded. |
877 | - |
878 | - See the encode and decode methods for examples, including functions. |
879 | - |
880 | - .. caution:: |
881 | - |
882 | - You can use ``walk`` to transform the names of members of a section |
883 | - but you mustn't add or delete members. |
884 | - |
885 | - >>> config = '''[XXXXsection] |
886 | - ... XXXXkey = XXXXvalue'''.splitlines() |
887 | - >>> cfg = ConfigObj(config) |
888 | - >>> cfg |
889 | - {'XXXXsection': {'XXXXkey': 'XXXXvalue'}} |
890 | - >>> def transform(section, key): |
891 | - ... val = section[key] |
892 | - ... newkey = key.replace('XXXX', 'CLIENT1') |
893 | - ... section.rename(key, newkey) |
894 | - ... if isinstance(val, (tuple, list, dict)): |
895 | - ... pass |
896 | - ... else: |
897 | - ... val = val.replace('XXXX', 'CLIENT1') |
898 | - ... section[newkey] = val |
899 | - >>> cfg.walk(transform, call_on_sections=True) |
900 | - {'CLIENT1section': {'CLIENT1key': None}} |
901 | - >>> cfg |
902 | - {'CLIENT1section': {'CLIENT1key': 'CLIENT1value'}} |
903 | - """ |
904 | - out = {} |
905 | - # scalars first |
906 | - for i in range(len(self.scalars)): |
907 | - entry = self.scalars[i] |
908 | - try: |
909 | - val = function(self, entry, **keywargs) |
910 | - # bound again in case name has changed |
911 | - entry = self.scalars[i] |
912 | - out[entry] = val |
913 | - except Exception: |
914 | - if raise_errors: |
915 | - raise |
916 | - else: |
917 | - entry = self.scalars[i] |
918 | - out[entry] = False |
919 | - # then sections |
920 | - for i in range(len(self.sections)): |
921 | - entry = self.sections[i] |
922 | - if call_on_sections: |
923 | - try: |
924 | - function(self, entry, **keywargs) |
925 | - except Exception: |
926 | - if raise_errors: |
927 | - raise |
928 | - else: |
929 | - entry = self.sections[i] |
930 | - out[entry] = False |
931 | - # bound again in case name has changed |
932 | - entry = self.sections[i] |
933 | - # previous result is discarded |
934 | - out[entry] = self[entry].walk( |
935 | - function, |
936 | - raise_errors=raise_errors, |
937 | - call_on_sections=call_on_sections, |
938 | - **keywargs) |
939 | - return out |
940 | - |
941 | - def decode(self, encoding): |
942 | - """ |
943 | - Decode all strings and values to unicode, using the specified encoding. |
944 | - |
945 | - Works with subsections and list values. |
946 | - |
947 | - Uses the ``walk`` method. |
948 | - |
949 | - Testing ``encode`` and ``decode``. |
950 | - >>> m = ConfigObj(a) |
951 | - >>> m.decode('ascii') |
952 | - >>> def testuni(val): |
953 | - ... for entry in val: |
954 | - ... if not isinstance(entry, unicode): |
955 | - ... print >> sys.stderr, type(entry) |
956 | - ... raise AssertionError, 'decode failed.' |
957 | - ... if isinstance(val[entry], dict): |
958 | - ... testuni(val[entry]) |
959 | - ... elif not isinstance(val[entry], unicode): |
960 | - ... raise AssertionError, 'decode failed.' |
961 | - >>> testuni(m) |
962 | - >>> m.encode('ascii') |
963 | - >>> a == m |
964 | - 1 |
965 | - """ |
966 | - warn('use of ``decode`` is deprecated.', DeprecationWarning) |
967 | - |
968 | - def decode(section, key, encoding=encoding, warn=True): |
969 | - """ """ |
970 | - val = section[key] |
971 | - if isinstance(val, (list, tuple)): |
972 | - newval = [] |
973 | - for entry in val: |
974 | - newval.append(entry.decode(encoding)) |
975 | - elif isinstance(val, dict): |
976 | - newval = val |
977 | - else: |
978 | - newval = val.decode(encoding) |
979 | - newkey = key.decode(encoding) |
980 | - section.rename(key, newkey) |
981 | - section[newkey] = newval |
982 | - # using ``call_on_sections`` allows us to modify section names |
983 | - self.walk(decode, call_on_sections=True) |
984 | - |
985 | - def encode(self, encoding): |
986 | - """ |
987 | - Encode all strings and values from unicode, |
988 | - using the specified encoding. |
989 | - |
990 | - Works with subsections and list values. |
991 | - Uses the ``walk`` method. |
992 | - """ |
993 | - warn('use of ``encode`` is deprecated.', DeprecationWarning) |
994 | - def encode(section, key, encoding=encoding): |
995 | - """ """ |
996 | - val = section[key] |
997 | - if isinstance(val, (list, tuple)): |
998 | - newval = [] |
999 | - for entry in val: |
1000 | - newval.append(entry.encode(encoding)) |
1001 | - elif isinstance(val, dict): |
1002 | - newval = val |
1003 | - else: |
1004 | - newval = val.encode(encoding) |
1005 | - newkey = key.encode(encoding) |
1006 | - section.rename(key, newkey) |
1007 | - section[newkey] = newval |
1008 | - self.walk(encode, call_on_sections=True) |
1009 | - |
1010 | - def istrue(self, key): |
1011 | - """A deprecated version of ``as_bool``.""" |
1012 | - warn('use of ``istrue`` is deprecated. Use ``as_bool`` method ' |
1013 | - 'instead.', DeprecationWarning) |
1014 | - return self.as_bool(key) |
1015 | - |
1016 | - def as_bool(self, key): |
1017 | - """ |
1018 | - Accepts a key as input. The corresponding value must be a string or |
1019 | - the objects (``True`` or 1) or (``False`` or 0). We allow 0 and 1 to |
1020 | - retain compatibility with Python 2.2. |
1021 | - |
1022 | - If the string is one of ``True``, ``On``, ``Yes``, or ``1`` it returns |
1023 | - ``True``. |
1024 | - |
1025 | - If the string is one of ``False``, ``Off``, ``No``, or ``0`` it returns |
1026 | - ``False``. |
1027 | - |
1028 | - ``as_bool`` is not case sensitive. |
1029 | - |
1030 | - Any other input will raise a ``ValueError``. |
1031 | - |
1032 | - >>> a = ConfigObj() |
1033 | - >>> a['a'] = 'fish' |
1034 | - >>> a.as_bool('a') |
1035 | - Traceback (most recent call last): |
1036 | - ValueError: Value "fish" is neither True nor False |
1037 | - >>> a['b'] = 'True' |
1038 | - >>> a.as_bool('b') |
1039 | - 1 |
1040 | - >>> a['b'] = 'off' |
1041 | - >>> a.as_bool('b') |
1042 | - 0 |
1043 | - """ |
1044 | - val = self[key] |
1045 | - if val == True: |
1046 | - return True |
1047 | - elif val == False: |
1048 | - return False |
1049 | - else: |
1050 | - try: |
1051 | - if not isinstance(val, StringTypes): |
1052 | - raise KeyError |
1053 | - else: |
1054 | - return self.main._bools[val.lower()] |
1055 | - except KeyError: |
1056 | - raise ValueError('Value "%s" is neither True nor False' % val) |
1057 | - |
1058 | - def as_int(self, key): |
1059 | - """ |
1060 | - A convenience method which coerces the specified value to an integer. |
1061 | - |
1062 | - If the value is an invalid literal for ``int``, a ``ValueError`` will |
1063 | - be raised. |
1064 | - |
1065 | - >>> a = ConfigObj() |
1066 | - >>> a['a'] = 'fish' |
1067 | - >>> a.as_int('a') |
1068 | - Traceback (most recent call last): |
1069 | - ValueError: invalid literal for int(): fish |
1070 | - >>> a['b'] = '1' |
1071 | - >>> a.as_int('b') |
1072 | - 1 |
1073 | - >>> a['b'] = '3.2' |
1074 | - >>> a.as_int('b') |
1075 | - Traceback (most recent call last): |
1076 | - ValueError: invalid literal for int(): 3.2 |
1077 | - """ |
1078 | - return int(self[key]) |
1079 | - |
1080 | - def as_float(self, key): |
1081 | - """ |
1082 | - A convenience method which coerces the specified value to a float. |
1083 | - |
1084 | - If the value is an invalid literal for ``float``, a ``ValueError`` will |
1085 | - be raised. |
1086 | - |
1087 | - >>> a = ConfigObj() |
1088 | - >>> a['a'] = 'fish' |
1089 | - >>> a.as_float('a') |
1090 | - Traceback (most recent call last): |
1091 | - ValueError: invalid literal for float(): fish |
1092 | - >>> a['b'] = '1' |
1093 | - >>> a.as_float('b') |
1094 | - 1.0 |
1095 | - >>> a['b'] = '3.2' |
1096 | - >>> a.as_float('b') |
1097 | - 3.2000000000000002 |
1098 | - """ |
1099 | - return float(self[key]) |
1100 | - |
1101 | - |
1102 | -class ConfigObj(Section): |
1103 | - """ |
1104 | - An object to read, create, and write config files. |
1105 | - |
1106 | - Testing with duplicate keys and sections. |
1107 | - |
1108 | - >>> c = ''' |
1109 | - ... [hello] |
1110 | - ... member = value |
1111 | - ... [hello again] |
1112 | - ... member = value |
1113 | - ... [ "hello" ] |
1114 | - ... member = value |
1115 | - ... ''' |
1116 | - >>> ConfigObj(c.split('\\n'), raise_errors = True) |
1117 | - Traceback (most recent call last): |
1118 | - DuplicateError: Duplicate section name at line 5. |
1119 | - |
1120 | - >>> d = ''' |
1121 | - ... [hello] |
1122 | - ... member = value |
1123 | - ... [hello again] |
1124 | - ... member1 = value |
1125 | - ... member2 = value |
1126 | - ... 'member1' = value |
1127 | - ... [ "and again" ] |
1128 | - ... member = value |
1129 | - ... ''' |
1130 | - >>> ConfigObj(d.split('\\n'), raise_errors = True) |
1131 | - Traceback (most recent call last): |
1132 | - DuplicateError: Duplicate keyword name at line 6. |
1133 | - """ |
1134 | - |
1135 | - _keyword = re.compile(r'''^ # line start |
1136 | - (\s*) # indentation |
1137 | - ( # keyword |
1138 | - (?:".*?")| # double quotes |
1139 | - (?:'.*?')| # single quotes |
1140 | - (?:[^'"=].*?) # no quotes |
1141 | - ) |
1142 | - \s*=\s* # divider |
1143 | - (.*) # value (including list values and comments) |
1144 | - $ # line end |
1145 | - ''', |
1146 | - re.VERBOSE) |
1147 | - |
1148 | - _sectionmarker = re.compile(r'''^ |
1149 | - (\s*) # 1: indentation |
1150 | - ((?:\[\s*)+) # 2: section marker open |
1151 | - ( # 3: section name open |
1152 | - (?:"\s*\S.*?\s*")| # at least one non-space with double quotes |
1153 | - (?:'\s*\S.*?\s*')| # at least one non-space with single quotes |
1154 | - (?:[^'"\s].*?) # at least one non-space unquoted |
1155 | - ) # section name close |
1156 | - ((?:\s*\])+) # 4: section marker close |
1157 | - \s*(\#.*)? # 5: optional comment |
1158 | - $''', |
1159 | - re.VERBOSE) |
1160 | - |
1161 | - # this regexp pulls list values out as a single string |
1162 | - # or single values and comments |
1163 | - # FIXME: this regex adds a '' to the end of comma terminated lists |
1164 | - # workaround in ``_handle_value`` |
1165 | - _valueexp = re.compile(r'''^ |
1166 | - (?: |
1167 | - (?: |
1168 | - ( |
1169 | - (?: |
1170 | - (?: |
1171 | - (?:".*?")| # double quotes |
1172 | - (?:'.*?')| # single quotes |
1173 | - (?:[^'",\#][^,\#]*?) # unquoted |
1174 | - ) |
1175 | - \s*,\s* # comma |
1176 | - )* # match all list items ending in a comma (if any) |
1177 | - ) |
1178 | - ( |
1179 | - (?:".*?")| # double quotes |
1180 | - (?:'.*?')| # single quotes |
1181 | - (?:[^'",\#\s][^,]*?)| # unquoted |
1182 | - (?:(?<!,)) # Empty value |
1183 | - )? # last item in a list - or string value |
1184 | - )| |
1185 | - (,) # alternatively a single comma - empty list |
1186 | - ) |
1187 | - \s*(\#.*)? # optional comment |
1188 | - $''', |
1189 | - re.VERBOSE) |
1190 | - |
1191 | - # use findall to get the members of a list value |
1192 | - _listvalueexp = re.compile(r''' |
1193 | - ( |
1194 | - (?:".*?")| # double quotes |
1195 | - (?:'.*?')| # single quotes |
1196 | - (?:[^'",\#].*?) # unquoted |
1197 | - ) |
1198 | - \s*,\s* # comma |
1199 | - ''', |
1200 | - re.VERBOSE) |
1201 | - |
1202 | - # this regexp is used for the value |
1203 | - # when lists are switched off |
1204 | - _nolistvalue = re.compile(r'''^ |
1205 | - ( |
1206 | - (?:".*?")| # double quotes |
1207 | - (?:'.*?')| # single quotes |
1208 | - (?:[^'"\#].*?)| # unquoted |
1209 | - (?:) # Empty value |
1210 | - ) |
1211 | - \s*(\#.*)? # optional comment |
1212 | - $''', |
1213 | - re.VERBOSE) |
1214 | - |
1215 | - # regexes for finding triple quoted values on one line |
1216 | - _single_line_single = re.compile(r"^'''(.*?)'''\s*(#.*)?$") |
1217 | - _single_line_double = re.compile(r'^"""(.*?)"""\s*(#.*)?$') |
1218 | - _multi_line_single = re.compile(r"^(.*?)'''\s*(#.*)?$") |
1219 | - _multi_line_double = re.compile(r'^(.*?)"""\s*(#.*)?$') |
1220 | - |
1221 | - _triple_quote = { |
1222 | - "'''": (_single_line_single, _multi_line_single), |
1223 | - '"""': (_single_line_double, _multi_line_double), |
1224 | - } |
1225 | - |
1226 | - # Used by the ``istrue`` Section method |
1227 | - _bools = { |
1228 | - 'yes': True, 'no': False, |
1229 | - 'on': True, 'off': False, |
1230 | - '1': True, '0': False, |
1231 | - 'true': True, 'false': False, |
1232 | - } |
1233 | - |
1234 | - def __init__(self, infile=None, options=None, **kwargs): |
1235 | - """ |
1236 | - Parse or create a config file object. |
1237 | - |
1238 | - ``ConfigObj(infile=None, options=None, **kwargs)`` |
1239 | - """ |
1240 | - if infile is None: |
1241 | - infile = [] |
1242 | - if options is None: |
1243 | - options = {} |
1244 | - # keyword arguments take precedence over an options dictionary |
1245 | - options.update(kwargs) |
1246 | - # init the superclass |
1247 | - Section.__init__(self, self, 0, self) |
1248 | - # |
1249 | - defaults = OPTION_DEFAULTS.copy() |
1250 | - for entry in options.keys(): |
1251 | - if entry not in defaults.keys(): |
1252 | - raise TypeError, 'Unrecognised option "%s".' % entry |
1253 | - # TODO: check the values too. |
1254 | - # |
1255 | - # Add any explicit options to the defaults |
1256 | - defaults.update(options) |
1257 | - # |
1258 | - # initialise a few variables |
1259 | - self.filename = None |
1260 | - self._errors = [] |
1261 | - self.raise_errors = defaults['raise_errors'] |
1262 | - self.interpolation = defaults['interpolation'] |
1263 | - self.list_values = defaults['list_values'] |
1264 | - self.create_empty = defaults['create_empty'] |
1265 | - self.file_error = defaults['file_error'] |
1266 | - self.stringify = defaults['stringify'] |
1267 | - self.indent_type = defaults['indent_type'] |
1268 | - self.encoding = defaults['encoding'] |
1269 | - self.default_encoding = defaults['default_encoding'] |
1270 | - self.BOM = False |
1271 | - self.newlines = None |
1272 | - self.write_empty_values = defaults['write_empty_values'] |
1273 | - self.unrepr = defaults['unrepr'] |
1274 | - # |
1275 | - self.initial_comment = [] |
1276 | - self.final_comment = [] |
1277 | - # |
1278 | - if isinstance(infile, StringTypes): |
1279 | - self.filename = infile |
1280 | - if os.path.isfile(infile): |
1281 | - infile = open(infile).read() or [] |
1282 | - elif self.file_error: |
1283 | - # raise an error if the file doesn't exist |
1284 | - raise IOError, 'Config file not found: "%s".' % self.filename |
1285 | - else: |
1286 | - # file doesn't already exist |
1287 | - if self.create_empty: |
1288 | - # this is a good test that the filename specified |
1289 | - # isn't impossible - like on a non existent device |
1290 | - h = open(infile, 'w') |
1291 | - h.write('') |
1292 | - h.close() |
1293 | - infile = [] |
1294 | - elif isinstance(infile, (list, tuple)): |
1295 | - infile = list(infile) |
1296 | - elif isinstance(infile, dict): |
1297 | - # initialise self |
1298 | - # the Section class handles creating subsections |
1299 | - if isinstance(infile, ConfigObj): |
1300 | - # get a copy of our ConfigObj |
1301 | - infile = infile.dict() |
1302 | - for entry in infile: |
1303 | - self[entry] = infile[entry] |
1304 | - del self._errors |
1305 | - if defaults['configspec'] is not None: |
1306 | - self._handle_configspec(defaults['configspec']) |
1307 | - else: |
1308 | - self.configspec = None |
1309 | - return |
1310 | - elif hasattr(infile, 'read'): |
1311 | - # This supports file like objects |
1312 | - infile = infile.read() or [] |
1313 | - # needs splitting into lines - but needs doing *after* decoding |
1314 | - # in case it's not an 8 bit encoding |
1315 | - else: |
1316 | - raise TypeError, ('infile must be a filename,' |
1317 | - ' file like object, or list of lines.') |
1318 | - # |
1319 | - if infile: |
1320 | - # don't do it for the empty ConfigObj |
1321 | - infile = self._handle_bom(infile) |
1322 | - # infile is now *always* a list |
1323 | - # |
1324 | - # Set the newlines attribute (first line ending it finds) |
1325 | - # and strip trailing '\n' or '\r' from lines |
1326 | - for line in infile: |
1327 | - if (not line) or (line[-1] not in '\r\n'): |
1328 | - continue |
1329 | - for end in ('\r\n', '\n', '\r'): |
1330 | - if line.endswith(end): |
1331 | - self.newlines = end |
1332 | - break |
1333 | - break |
1334 | - infile = [line.rstrip('\r\n') for line in infile] |
1335 | - # |
1336 | - self._parse(infile) |
1337 | - # if we had any errors, now is the time to raise them |
1338 | - if self._errors: |
1339 | - error = ConfigObjError("Parsing failed.") |
1340 | - # set the errors attribute; it's a list of tuples: |
1341 | - # (error_type, message, line_number) |
1342 | - error.errors = self._errors |
1343 | - # set the config attribute |
1344 | - error.config = self |
1345 | - raise error |
1346 | - # delete private attributes |
1347 | - del self._errors |
1348 | - # |
1349 | - if defaults['configspec'] is None: |
1350 | - self.configspec = None |
1351 | - else: |
1352 | - self._handle_configspec(defaults['configspec']) |
1353 | - |
1354 | - def _handle_bom(self, infile): |
1355 | - """ |
1356 | - Handle any BOM, and decode if necessary. |
1357 | - |
1358 | - If an encoding is specified, that *must* be used - but the BOM should |
1359 | - still be removed (and the BOM attribute set). |
1360 | - |
1361 | - (If the encoding is wrongly specified, then a BOM for an alternative |
1362 | - encoding won't be discovered or removed.) |
1363 | - |
1364 | - If an encoding is not specified, UTF8 or UTF16 BOM will be detected and |
1365 | - removed. The BOM attribute will be set. UTF16 will be decoded to |
1366 | - unicode. |
1367 | - |
1368 | - NOTE: This method must not be called with an empty ``infile``. |
1369 | - |
1370 | - Specifying the *wrong* encoding is likely to cause a |
1371 | - ``UnicodeDecodeError``. |
1372 | - |
1373 | - ``infile`` must always be returned as a list of lines, but may be |
1374 | - passed in as a single string. |
1375 | - """ |
1376 | - if ((self.encoding is not None) and |
1377 | - (self.encoding.lower() not in BOM_LIST)): |
1378 | - # No need to check for a BOM |
1379 | - # encoding specified doesn't have one |
1380 | - # just decode |
1381 | - return self._decode(infile, self.encoding) |
1382 | - # |
1383 | - if isinstance(infile, (list, tuple)): |
1384 | - line = infile[0] |
1385 | - else: |
1386 | - line = infile |
1387 | - if self.encoding is not None: |
1388 | - # encoding explicitly supplied |
1389 | - # And it could have an associated BOM |
1390 | - # TODO: if encoding is just UTF16 - we ought to check for both |
1391 | - # TODO: big endian and little endian versions. |
1392 | - enc = BOM_LIST[self.encoding.lower()] |
1393 | - if enc == 'utf_16': |
1394 | - # For UTF16 we try big endian and little endian |
1395 | - for BOM, (encoding, final_encoding) in BOMS.items(): |
1396 | - if not final_encoding: |
1397 | - # skip UTF8 |
1398 | - continue |
1399 | - if infile.startswith(BOM): |
1400 | - ### BOM discovered |
1401 | - ##self.BOM = True |
1402 | - # Don't need to remove BOM |
1403 | - return self._decode(infile, encoding) |
1404 | - # |
1405 | - # If we get this far, will *probably* raise a DecodeError |
1406 | - # As it doesn't appear to start with a BOM |
1407 | - return self._decode(infile, self.encoding) |
1408 | - # |
1409 | - # Must be UTF8 |
1410 | - BOM = BOM_SET[enc] |
1411 | - if not line.startswith(BOM): |
1412 | - return self._decode(infile, self.encoding) |
1413 | - # |
1414 | - newline = line[len(BOM):] |
1415 | - # |
1416 | - # BOM removed |
1417 | - if isinstance(infile, (list, tuple)): |
1418 | - infile[0] = newline |
1419 | - else: |
1420 | - infile = newline |
1421 | - self.BOM = True |
1422 | - return self._decode(infile, self.encoding) |
1423 | - # |
1424 | - # No encoding specified - so we need to check for UTF8/UTF16 |
1425 | - for BOM, (encoding, final_encoding) in BOMS.items(): |
1426 | - if not line.startswith(BOM): |
1427 | - continue |
1428 | - else: |
1429 | - # BOM discovered |
1430 | - self.encoding = final_encoding |
1431 | - if not final_encoding: |
1432 | - self.BOM = True |
1433 | - # UTF8 |
1434 | - # remove BOM |
1435 | - newline = line[len(BOM):] |
1436 | - if isinstance(infile, (list, tuple)): |
1437 | - infile[0] = newline |
1438 | - else: |
1439 | - infile = newline |
1440 | - # UTF8 - don't decode |
1441 | - if isinstance(infile, StringTypes): |
1442 | - return infile.splitlines(True) |
1443 | - else: |
1444 | - return infile |
1445 | - # UTF16 - have to decode |
1446 | - return self._decode(infile, encoding) |
1447 | - # |
1448 | - # No BOM discovered and no encoding specified, just return |
1449 | - if isinstance(infile, StringTypes): |
1450 | - # infile read from a file will be a single string |
1451 | - return infile.splitlines(True) |
1452 | - else: |
1453 | - return infile |
1454 | - |
1455 | - def _a_to_u(self, string): |
1456 | - """Decode ascii strings to unicode if a self.encoding is specified.""" |
1457 | - if not self.encoding: |
1458 | - return string |
1459 | - else: |
1460 | - return string.decode('ascii') |
1461 | - |
1462 | - def _decode(self, infile, encoding): |
1463 | - """ |
1464 | - Decode infile to unicode. Using the specified encoding. |
1465 | - |
1466 | - if is a string, it also needs converting to a list. |
1467 | - """ |
1468 | - if isinstance(infile, StringTypes): |
1469 | - # can't be unicode |
1470 | - # NOTE: Could raise a ``UnicodeDecodeError`` |
1471 | - return infile.decode(encoding).splitlines(True) |
1472 | - for i, line in enumerate(infile): |
1473 | - if not isinstance(line, unicode): |
1474 | - # NOTE: The isinstance test here handles mixed lists of unicode/string |
1475 | - # NOTE: But the decode will break on any non-string values |
1476 | - # NOTE: Or could raise a ``UnicodeDecodeError`` |
1477 | - infile[i] = line.decode(encoding) |
1478 | - return infile |
1479 | - |
1480 | - def _decode_element(self, line): |
1481 | - """Decode element to unicode if necessary.""" |
1482 | - if not self.encoding: |
1483 | - return line |
1484 | - if isinstance(line, str) and self.default_encoding: |
1485 | - return line.decode(self.default_encoding) |
1486 | - return line |
1487 | - |
1488 | - def _str(self, value): |
1489 | - """ |
1490 | - Used by ``stringify`` within validate, to turn non-string values |
1491 | - into strings. |
1492 | - """ |
1493 | - if not isinstance(value, StringTypes): |
1494 | - return str(value) |
1495 | - else: |
1496 | - return value |
1497 | - |
1498 | - def _parse(self, infile): |
1499 | - """ |
1500 | - Actually parse the config file |
1501 | - |
1502 | - Testing Interpolation |
1503 | - |
1504 | - >>> c = ConfigObj() |
1505 | - >>> c['DEFAULT'] = { |
1506 | - ... 'b': 'goodbye', |
1507 | - ... 'userdir': 'c:\\\\home', |
1508 | - ... 'c': '%(d)s', |
1509 | - ... 'd': '%(c)s' |
1510 | - ... } |
1511 | - >>> c['section'] = { |
1512 | - ... 'a': '%(datadir)s\\\\some path\\\\file.py', |
1513 | - ... 'b': '%(userdir)s\\\\some path\\\\file.py', |
1514 | - ... 'c': 'Yo %(a)s', |
1515 | - ... 'd': '%(not_here)s', |
1516 | - ... 'e': '%(c)s', |
1517 | - ... } |
1518 | - >>> c['section']['DEFAULT'] = { |
1519 | - ... 'datadir': 'c:\\\\silly_test', |
1520 | - ... 'a': 'hello - %(b)s', |
1521 | - ... } |
1522 | - >>> c['section']['a'] == 'c:\\\\silly_test\\\\some path\\\\file.py' |
1523 | - 1 |
1524 | - >>> c['section']['b'] == 'c:\\\\home\\\\some path\\\\file.py' |
1525 | - 1 |
1526 | - >>> c['section']['c'] == 'Yo hello - goodbye' |
1527 | - 1 |
1528 | - |
1529 | - Switching Interpolation Off |
1530 | - |
1531 | - >>> c.interpolation = False |
1532 | - >>> c['section']['a'] == '%(datadir)s\\\\some path\\\\file.py' |
1533 | - 1 |
1534 | - >>> c['section']['b'] == '%(userdir)s\\\\some path\\\\file.py' |
1535 | - 1 |
1536 | - >>> c['section']['c'] == 'Yo %(a)s' |
1537 | - 1 |
1538 | - |
1539 | - Testing the interpolation errors. |
1540 | - |
1541 | - >>> c.interpolation = True |
1542 | - >>> c['section']['d'] |
1543 | - Traceback (most recent call last): |
1544 | - MissingInterpolationOption: missing option "not_here" in interpolation. |
1545 | - >>> c['section']['e'] |
1546 | - Traceback (most recent call last): |
1547 | - InterpolationDepthError: max interpolation depth exceeded in value "%(c)s". |
1548 | - |
1549 | - Testing our quoting. |
1550 | - |
1551 | - >>> i._quote('\"""\'\'\'') |
1552 | - Traceback (most recent call last): |
1553 | - SyntaxError: EOF while scanning triple-quoted string |
1554 | - >>> try: |
1555 | - ... i._quote('\\n', multiline=False) |
1556 | - ... except ConfigObjError, e: |
1557 | - ... e.msg |
1558 | - 'Value "\\n" cannot be safely quoted.' |
1559 | - >>> k._quote(' "\' ', multiline=False) |
1560 | - Traceback (most recent call last): |
1561 | - SyntaxError: EOL while scanning single-quoted string |
1562 | - |
1563 | - Testing with "stringify" off. |
1564 | - >>> c.stringify = False |
1565 | - >>> c['test'] = 1 |
1566 | - Traceback (most recent call last): |
1567 | - TypeError: Value is not a string "1". |
1568 | - |
1569 | - Testing Empty values. |
1570 | - >>> cfg_with_empty = ''' |
1571 | - ... k = |
1572 | - ... k2 =# comment test |
1573 | - ... val = test |
1574 | - ... val2 = , |
1575 | - ... val3 = 1, |
1576 | - ... val4 = 1, 2 |
1577 | - ... val5 = 1, 2, '''.splitlines() |
1578 | - >>> cwe = ConfigObj(cfg_with_empty) |
1579 | - >>> cwe == {'k': '', 'k2': '', 'val': 'test', 'val2': [], |
1580 | - ... 'val3': ['1'], 'val4': ['1', '2'], 'val5': ['1', '2']} |
1581 | - 1 |
1582 | - >>> cwe = ConfigObj(cfg_with_empty, list_values=False) |
1583 | - >>> cwe == {'k': '', 'k2': '', 'val': 'test', 'val2': ',', |
1584 | - ... 'val3': '1,', 'val4': '1, 2', 'val5': '1, 2,'} |
1585 | - 1 |
1586 | - """ |
1587 | - temp_list_values = self.list_values |
1588 | - if self.unrepr: |
1589 | - self.list_values = False |
1590 | - comment_list = [] |
1591 | - done_start = False |
1592 | - this_section = self |
1593 | - maxline = len(infile) - 1 |
1594 | - cur_index = -1 |
1595 | - reset_comment = False |
1596 | - while cur_index < maxline: |
1597 | - if reset_comment: |
1598 | - comment_list = [] |
1599 | - cur_index += 1 |
1600 | - line = infile[cur_index] |
1601 | - sline = line.strip() |
1602 | - # do we have anything on the line ? |
1603 | - if not sline or sline.startswith('#'): |
1604 | - reset_comment = False |
1605 | - comment_list.append(line) |
1606 | - continue |
1607 | - if not done_start: |
1608 | - # preserve initial comment |
1609 | - self.initial_comment = comment_list |
1610 | - comment_list = [] |
1611 | - done_start = True |
1612 | - reset_comment = True |
1613 | - # first we check if it's a section marker |
1614 | - mat = self._sectionmarker.match(line) |
1615 | -## print >> sys.stderr, sline, mat |
1616 | - if mat is not None: |
1617 | - # is a section line |
1618 | - (indent, sect_open, sect_name, sect_close, comment) = ( |
1619 | - mat.groups()) |
1620 | - if indent and (self.indent_type is None): |
1621 | - self.indent_type = indent[0] |
1622 | - cur_depth = sect_open.count('[') |
1623 | - if cur_depth != sect_close.count(']'): |
1624 | - self._handle_error( |
1625 | - "Cannot compute the section depth at line %s.", |
1626 | - NestingError, infile, cur_index) |
1627 | - continue |
1628 | - if cur_depth < this_section.depth: |
1629 | - # the new section is dropping back to a previous level |
1630 | - try: |
1631 | - parent = self._match_depth( |
1632 | - this_section, |
1633 | - cur_depth).parent |
1634 | - except SyntaxError: |
1635 | - self._handle_error( |
1636 | - "Cannot compute nesting level at line %s.", |
1637 | - NestingError, infile, cur_index) |
1638 | - continue |
1639 | - elif cur_depth == this_section.depth: |
1640 | - # the new section is a sibling of the current section |
1641 | - parent = this_section.parent |
1642 | - elif cur_depth == this_section.depth + 1: |
1643 | - # the new section is a child the current section |
1644 | - parent = this_section |
1645 | - else: |
1646 | - self._handle_error( |
1647 | - "Section too nested at line %s.", |
1648 | - NestingError, infile, cur_index) |
1649 | - # |
1650 | - sect_name = self._unquote(sect_name) |
1651 | - if parent.has_key(sect_name): |
1652 | -## print >> sys.stderr, sect_name |
1653 | - self._handle_error( |
1654 | - 'Duplicate section name at line %s.', |
1655 | - DuplicateError, infile, cur_index) |
1656 | - continue |
1657 | - # create the new section |
1658 | - this_section = Section( |
1659 | - parent, |
1660 | - cur_depth, |
1661 | - self, |
1662 | - name=sect_name) |
1663 | - parent[sect_name] = this_section |
1664 | - parent.inline_comments[sect_name] = comment |
1665 | - parent.comments[sect_name] = comment_list |
1666 | -## print >> sys.stderr, parent[sect_name] is this_section |
1667 | - continue |
1668 | - # |
1669 | - # it's not a section marker, |
1670 | - # so it should be a valid ``key = value`` line |
1671 | - mat = self._keyword.match(line) |
1672 | -## print >> sys.stderr, sline, mat |
1673 | - if mat is not None: |
1674 | - # is a keyword value |
1675 | - # value will include any inline comment |
1676 | - (indent, key, value) = mat.groups() |
1677 | - if indent and (self.indent_type is None): |
1678 | - self.indent_type = indent[0] |
1679 | - # check for a multiline value |
1680 | - if value[:3] in ['"""', "'''"]: |
1681 | - try: |
1682 | - (value, comment, cur_index) = self._multiline( |
1683 | - value, infile, cur_index, maxline) |
1684 | - except SyntaxError: |
1685 | - self._handle_error( |
1686 | - 'Parse error in value at line %s.', |
1687 | - ParseError, infile, cur_index) |
1688 | - continue |
1689 | - else: |
1690 | - if self.unrepr: |
1691 | - value = unrepr(value) |
1692 | - else: |
1693 | - # extract comment and lists |
1694 | - try: |
1695 | - (value, comment) = self._handle_value(value) |
1696 | - except SyntaxError: |
1697 | - self._handle_error( |
1698 | - 'Parse error in value at line %s.', |
1699 | - ParseError, infile, cur_index) |
1700 | - continue |
1701 | - # |
1702 | -## print >> sys.stderr, sline |
1703 | - key = self._unquote(key) |
1704 | - if this_section.has_key(key): |
1705 | - self._handle_error( |
1706 | - 'Duplicate keyword name at line %s.', |
1707 | - DuplicateError, infile, cur_index) |
1708 | - continue |
1709 | - # add the key |
1710 | -## print >> sys.stderr, this_section.name |
1711 | - # we set unrepr because if we have got this far we will never |
1712 | - # be creating a new section |
1713 | - this_section.__setitem__(key, value, unrepr=True) |
1714 | - this_section.inline_comments[key] = comment |
1715 | - this_section.comments[key] = comment_list |
1716 | -## print >> sys.stderr, key, this_section[key] |
1717 | -## if this_section.name is not None: |
1718 | -## print >> sys.stderr, this_section |
1719 | -## print >> sys.stderr, this_section.parent |
1720 | -## print >> sys.stderr, this_section.parent[this_section.name] |
1721 | - continue |
1722 | - # |
1723 | - # it neither matched as a keyword |
1724 | - # or a section marker |
1725 | - self._handle_error( |
1726 | - 'Invalid line at line "%s".', |
1727 | - ParseError, infile, cur_index) |
1728 | - if self.indent_type is None: |
1729 | - # no indentation used, set the type accordingly |
1730 | - self.indent_type = '' |
1731 | - # preserve the final comment |
1732 | - if not self and not self.initial_comment: |
1733 | - self.initial_comment = comment_list |
1734 | - elif not reset_comment: |
1735 | - self.final_comment = comment_list |
1736 | - self.list_values = temp_list_values |
1737 | - |
1738 | - def _match_depth(self, sect, depth): |
1739 | - """ |
1740 | - Given a section and a depth level, walk back through the sections |
1741 | - parents to see if the depth level matches a previous section. |
1742 | - |
1743 | - Return a reference to the right section, |
1744 | - or raise a SyntaxError. |
1745 | - """ |
1746 | - while depth < sect.depth: |
1747 | - if sect is sect.parent: |
1748 | - # we've reached the top level already |
1749 | - raise SyntaxError |
1750 | - sect = sect.parent |
1751 | - if sect.depth == depth: |
1752 | - return sect |
1753 | - # shouldn't get here |
1754 | - raise SyntaxError |
1755 | - |
1756 | - def _handle_error(self, text, ErrorClass, infile, cur_index): |
1757 | - """ |
1758 | - Handle an error according to the error settings. |
1759 | - |
1760 | - Either raise the error or store it. |
1761 | - The error will have occured at ``cur_index`` |
1762 | - """ |
1763 | - line = infile[cur_index] |
1764 | - message = text % cur_index |
1765 | - error = ErrorClass(message, cur_index, line) |
1766 | - if self.raise_errors: |
1767 | - # raise the error - parsing stops here |
1768 | - raise error |
1769 | - # store the error |
1770 | - # reraise when parsing has finished |
1771 | - self._errors.append(error) |
1772 | - |
1773 | - def _unquote(self, value): |
1774 | - """Return an unquoted version of a value""" |
1775 | - if (value[0] == value[-1]) and (value[0] in ('"', "'")): |
1776 | - value = value[1:-1] |
1777 | - return value |
1778 | - |
1779 | - def _quote(self, value, multiline=True): |
1780 | - """ |
1781 | - Return a safely quoted version of a value. |
1782 | - |
1783 | - Raise a ConfigObjError if the value cannot be safely quoted. |
1784 | - If multiline is ``True`` (default) then use triple quotes |
1785 | - if necessary. |
1786 | - |
1787 | - Don't quote values that don't need it. |
1788 | - Recursively quote members of a list and return a comma joined list. |
1789 | - Multiline is ``False`` for lists. |
1790 | - Obey list syntax for empty and single member lists. |
1791 | - |
1792 | - If ``list_values=False`` then the value is only quoted if it contains |
1793 | - a ``\n`` (is multiline). |
1794 | - |
1795 | - If ``write_empty_values`` is set, and the value is an empty string, it |
1796 | - won't be quoted. |
1797 | - """ |
1798 | - if multiline and self.write_empty_values and value == '': |
1799 | - # Only if multiline is set, so that it is used for values not |
1800 | - # keys, and not values that are part of a list |
1801 | - return '' |
1802 | - if multiline and isinstance(value, (list, tuple)): |
1803 | - if not value: |
1804 | - return ',' |
1805 | - elif len(value) == 1: |
1806 | - return self._quote(value[0], multiline=False) + ',' |
1807 | - return ', '.join([self._quote(val, multiline=False) |
1808 | - for val in value]) |
1809 | - if not isinstance(value, StringTypes): |
1810 | - if self.stringify: |
1811 | - value = str(value) |
1812 | - else: |
1813 | - raise TypeError, 'Value "%s" is not a string.' % value |
1814 | - squot = "'%s'" |
1815 | - dquot = '"%s"' |
1816 | - noquot = "%s" |
1817 | - wspace_plus = ' \r\t\n\v\t\'"' |
1818 | - tsquot = '"""%s"""' |
1819 | - tdquot = "'''%s'''" |
1820 | - if not value: |
1821 | - return '""' |
1822 | - if (not self.list_values and '\n' not in value) or not (multiline and |
1823 | - ((("'" in value) and ('"' in value)) or ('\n' in value))): |
1824 | - if not self.list_values: |
1825 | - # we don't quote if ``list_values=False`` |
1826 | - quot = noquot |
1827 | - # for normal values either single or double quotes will do |
1828 | - elif '\n' in value: |
1829 | - # will only happen if multiline is off - e.g. '\n' in key |
1830 | - raise ConfigObjError, ('Value "%s" cannot be safely quoted.' % |
1831 | - value) |
1832 | - elif ((value[0] not in wspace_plus) and |
1833 | - (value[-1] not in wspace_plus) and |
1834 | - (',' not in value)): |
1835 | - quot = noquot |
1836 | - else: |
1837 | - if ("'" in value) and ('"' in value): |
1838 | - raise ConfigObjError, ( |
1839 | - 'Value "%s" cannot be safely quoted.' % value) |
1840 | - elif '"' in value: |
1841 | - quot = squot |
1842 | - else: |
1843 | - quot = dquot |
1844 | - else: |
1845 | - # if value has '\n' or "'" *and* '"', it will need triple quotes |
1846 | - if (value.find('"""') != -1) and (value.find("'''") != -1): |
1847 | - raise ConfigObjError, ( |
1848 | - 'Value "%s" cannot be safely quoted.' % value) |
1849 | - if value.find('"""') == -1: |
1850 | - quot = tdquot |
1851 | - else: |
1852 | - quot = tsquot |
1853 | - return quot % value |
1854 | - |
1855 | - def _handle_value(self, value): |
1856 | - """ |
1857 | - Given a value string, unquote, remove comment, |
1858 | - handle lists. (including empty and single member lists) |
1859 | - |
1860 | - Testing list values. |
1861 | - |
1862 | - >>> testconfig3 = ''' |
1863 | - ... a = , |
1864 | - ... b = test, |
1865 | - ... c = test1, test2 , test3 |
1866 | - ... d = test1, test2, test3, |
1867 | - ... ''' |
1868 | - >>> d = ConfigObj(testconfig3.split('\\n'), raise_errors=True) |
1869 | - >>> d['a'] == [] |
1870 | - 1 |
1871 | - >>> d['b'] == ['test'] |
1872 | - 1 |
1873 | - >>> d['c'] == ['test1', 'test2', 'test3'] |
1874 | - 1 |
1875 | - >>> d['d'] == ['test1', 'test2', 'test3'] |
1876 | - 1 |
1877 | - |
1878 | - Testing with list values off. |
1879 | - |
1880 | - >>> e = ConfigObj( |
1881 | - ... testconfig3.split('\\n'), |
1882 | - ... raise_errors=True, |
1883 | - ... list_values=False) |
1884 | - >>> e['a'] == ',' |
1885 | - 1 |
1886 | - >>> e['b'] == 'test,' |
1887 | - 1 |
1888 | - >>> e['c'] == 'test1, test2 , test3' |
1889 | - 1 |
1890 | - >>> e['d'] == 'test1, test2, test3,' |
1891 | - 1 |
1892 | - |
1893 | - Testing creating from a dictionary. |
1894 | - |
1895 | - >>> f = { |
1896 | - ... 'key1': 'val1', |
1897 | - ... 'key2': 'val2', |
1898 | - ... 'section 1': { |
1899 | - ... 'key1': 'val1', |
1900 | - ... 'key2': 'val2', |
1901 | - ... 'section 1b': { |
1902 | - ... 'key1': 'val1', |
1903 | - ... 'key2': 'val2', |
1904 | - ... }, |
1905 | - ... }, |
1906 | - ... 'section 2': { |
1907 | - ... 'key1': 'val1', |
1908 | - ... 'key2': 'val2', |
1909 | - ... 'section 2b': { |
1910 | - ... 'key1': 'val1', |
1911 | - ... 'key2': 'val2', |
1912 | - ... }, |
1913 | - ... }, |
1914 | - ... 'key3': 'val3', |
1915 | - ... } |
1916 | - >>> g = ConfigObj(f) |
1917 | - >>> f == g |
1918 | - 1 |
1919 | - |
1920 | - Testing we correctly detect badly built list values (4 of them). |
1921 | - |
1922 | - >>> testconfig4 = ''' |
1923 | - ... config = 3,4,, |
1924 | - ... test = 3,,4 |
1925 | - ... fish = ,, |
1926 | - ... dummy = ,,hello, goodbye |
1927 | - ... ''' |
1928 | - >>> try: |
1929 | - ... ConfigObj(testconfig4.split('\\n')) |
1930 | - ... except ConfigObjError, e: |
1931 | - ... len(e.errors) |
1932 | - 4 |
1933 | - |
1934 | - Testing we correctly detect badly quoted values (4 of them). |
1935 | - |
1936 | - >>> testconfig5 = ''' |
1937 | - ... config = "hello # comment |
1938 | - ... test = 'goodbye |
1939 | - ... fish = 'goodbye # comment |
1940 | - ... dummy = "hello again |
1941 | - ... ''' |
1942 | - >>> try: |
1943 | - ... ConfigObj(testconfig5.split('\\n')) |
1944 | - ... except ConfigObjError, e: |
1945 | - ... len(e.errors) |
1946 | - 4 |
1947 | - """ |
1948 | - # do we look for lists in values ? |
1949 | - if not self.list_values: |
1950 | - mat = self._nolistvalue.match(value) |
1951 | - if mat is None: |
1952 | - raise SyntaxError |
1953 | - (value, comment) = mat.groups() |
1954 | - if not self.unrepr: |
1955 | - # NOTE: we don't unquote here |
1956 | - return (value, comment) |
1957 | - else: |
1958 | - return (unrepr(value), comment) |
1959 | - mat = self._valueexp.match(value) |
1960 | - if mat is None: |
1961 | - # the value is badly constructed, probably badly quoted, |
1962 | - # or an invalid list |
1963 | - raise SyntaxError |
1964 | - (list_values, single, empty_list, comment) = mat.groups() |
1965 | - if (list_values == '') and (single is None): |
1966 | - # change this if you want to accept empty values |
1967 | - raise SyntaxError |
1968 | - # NOTE: note there is no error handling from here if the regex |
1969 | - # is wrong: then incorrect values will slip through |
1970 | - if empty_list is not None: |
1971 | - # the single comma - meaning an empty list |
1972 | - return ([], comment) |
1973 | - if single is not None: |
1974 | - # handle empty values |
1975 | - if list_values and not single: |
1976 | - # FIXME: the '' is a workaround because our regex now matches |
1977 | - # '' at the end of a list if it has a trailing comma |
1978 | - single = None |
1979 | - else: |
1980 | - single = single or '""' |
1981 | - single = self._unquote(single) |
1982 | - if list_values == '': |
1983 | - # not a list value |
1984 | - return (single, comment) |
1985 | - the_list = self._listvalueexp.findall(list_values) |
1986 | - the_list = [self._unquote(val) for val in the_list] |
1987 | - if single is not None: |
1988 | - the_list += [single] |
1989 | - return (the_list, comment) |
1990 | - |
1991 | - def _multiline(self, value, infile, cur_index, maxline): |
1992 | - """ |
1993 | - Extract the value, where we are in a multiline situation |
1994 | - |
1995 | - Testing multiline values. |
1996 | - |
1997 | - >>> i == { |
1998 | - ... 'name4': ' another single line value ', |
1999 | - ... 'multi section': { |
2000 | - ... 'name4': '\\n Well, this is a\\n multiline ' |
2001 | - ... 'value\\n ', |
2002 | - ... 'name2': '\\n Well, this is a\\n multiline ' |
2003 | - ... 'value\\n ', |
2004 | - ... 'name3': '\\n Well, this is a\\n multiline ' |
2005 | - ... 'value\\n ', |
2006 | - ... 'name1': '\\n Well, this is a\\n multiline ' |
2007 | - ... 'value\\n ', |
2008 | - ... }, |
2009 | - ... 'name2': ' another single line value ', |
2010 | - ... 'name3': ' a single line value ', |
2011 | - ... 'name1': ' a single line value ', |
2012 | - ... } |
2013 | - 1 |
2014 | - """ |
2015 | - quot = value[:3] |
2016 | - newvalue = value[3:] |
2017 | - single_line = self._triple_quote[quot][0] |
2018 | - multi_line = self._triple_quote[quot][1] |
2019 | - mat = single_line.match(value) |
2020 | - if mat is not None: |
2021 | - retval = list(mat.groups()) |
2022 | - retval.append(cur_index) |
2023 | - return retval |
2024 | - elif newvalue.find(quot) != -1: |
2025 | - # somehow the triple quote is missing |
2026 | - raise SyntaxError |
2027 | - # |
2028 | - while cur_index < maxline: |
2029 | - cur_index += 1 |
2030 | - newvalue += '\n' |
2031 | - line = infile[cur_index] |
2032 | - if line.find(quot) == -1: |
2033 | - newvalue += line |
2034 | - else: |
2035 | - # end of multiline, process it |
2036 | - break |
2037 | - else: |
2038 | - # we've got to the end of the config, oops... |
2039 | - raise SyntaxError |
2040 | - mat = multi_line.match(line) |
2041 | - if mat is None: |
2042 | - # a badly formed line |
2043 | - raise SyntaxError |
2044 | - (value, comment) = mat.groups() |
2045 | - return (newvalue + value, comment, cur_index) |
2046 | - |
2047 | - def _handle_configspec(self, configspec): |
2048 | - """Parse the configspec.""" |
2049 | - # FIXME: Should we check that the configspec was created with the |
2050 | - # correct settings ? (i.e. ``list_values=False``) |
2051 | - if not isinstance(configspec, ConfigObj): |
2052 | - try: |
2053 | - configspec = ConfigObj( |
2054 | - configspec, |
2055 | - raise_errors=True, |
2056 | - file_error=True, |
2057 | - list_values=False) |
2058 | - except ConfigObjError, e: |
2059 | - # FIXME: Should these errors have a reference |
2060 | - # to the already parsed ConfigObj ? |
2061 | - raise ConfigspecError('Parsing configspec failed: %s' % e) |
2062 | - except IOError, e: |
2063 | - raise IOError('Reading configspec failed: %s' % e) |
2064 | - self._set_configspec_value(configspec, self) |
2065 | - |
2066 | - def _set_configspec_value(self, configspec, section): |
2067 | - """Used to recursively set configspec values.""" |
2068 | - if '__many__' in configspec.sections: |
2069 | - section.configspec['__many__'] = configspec['__many__'] |
2070 | - if len(configspec.sections) > 1: |
2071 | - # FIXME: can we supply any useful information here ? |
2072 | - raise RepeatSectionError |
2073 | - if hasattr(configspec, 'initial_comment'): |
2074 | - section._configspec_initial_comment = configspec.initial_comment |
2075 | - section._configspec_final_comment = configspec.final_comment |
2076 | - section._configspec_encoding = configspec.encoding |
2077 | - section._configspec_BOM = configspec.BOM |
2078 | - section._configspec_newlines = configspec.newlines |
2079 | - section._configspec_indent_type = configspec.indent_type |
2080 | - for entry in configspec.scalars: |
2081 | - section._configspec_comments[entry] = configspec.comments[entry] |
2082 | - section._configspec_inline_comments[entry] = ( |
2083 | - configspec.inline_comments[entry]) |
2084 | - section.configspec[entry] = configspec[entry] |
2085 | - section._order.append(entry) |
2086 | - for entry in configspec.sections: |
2087 | - if entry == '__many__': |
2088 | - continue |
2089 | - section._cs_section_comments[entry] = configspec.comments[entry] |
2090 | - section._cs_section_inline_comments[entry] = ( |
2091 | - configspec.inline_comments[entry]) |
2092 | - if not section.has_key(entry): |
2093 | - section[entry] = {} |
2094 | - self._set_configspec_value(configspec[entry], section[entry]) |
2095 | - |
2096 | - def _handle_repeat(self, section, configspec): |
2097 | - """Dynamically assign configspec for repeated section.""" |
2098 | - try: |
2099 | - section_keys = configspec.sections |
2100 | - scalar_keys = configspec.scalars |
2101 | - except AttributeError: |
2102 | - section_keys = [entry for entry in configspec |
2103 | - if isinstance(configspec[entry], dict)] |
2104 | - scalar_keys = [entry for entry in configspec |
2105 | - if not isinstance(configspec[entry], dict)] |
2106 | - if '__many__' in section_keys and len(section_keys) > 1: |
2107 | - # FIXME: can we supply any useful information here ? |
2108 | - raise RepeatSectionError |
2109 | - scalars = {} |
2110 | - sections = {} |
2111 | - for entry in scalar_keys: |
2112 | - val = configspec[entry] |
2113 | - scalars[entry] = val |
2114 | - for entry in section_keys: |
2115 | - val = configspec[entry] |
2116 | - if entry == '__many__': |
2117 | - scalars[entry] = val |
2118 | - continue |
2119 | - sections[entry] = val |
2120 | - # |
2121 | - section.configspec = scalars |
2122 | - for entry in sections: |
2123 | - if not section.has_key(entry): |
2124 | - section[entry] = {} |
2125 | - self._handle_repeat(section[entry], sections[entry]) |
2126 | - |
2127 | - def _write_line(self, indent_string, entry, this_entry, comment): |
2128 | - """Write an individual line, for the write method""" |
2129 | - # NOTE: the calls to self._quote here handles non-StringType values. |
2130 | - if not self.unrepr: |
2131 | - val = self._decode_element(self._quote(this_entry)) |
2132 | - else: |
2133 | - val = repr(this_entry) |
2134 | - return '%s%s%s%s%s' % ( |
2135 | - indent_string, |
2136 | - self._decode_element(self._quote(entry, multiline=False)), |
2137 | - self._a_to_u(' = '), |
2138 | - val, |
2139 | - self._decode_element(comment)) |
2140 | - |
2141 | - def _write_marker(self, indent_string, depth, entry, comment): |
2142 | - """Write a section marker line""" |
2143 | - return '%s%s%s%s%s' % ( |
2144 | - indent_string, |
2145 | - self._a_to_u('[' * depth), |
2146 | - self._quote(self._decode_element(entry), multiline=False), |
2147 | - self._a_to_u(']' * depth), |
2148 | - self._decode_element(comment)) |
2149 | - |
2150 | - def _handle_comment(self, comment): |
2151 | - """ |
2152 | - Deal with a comment. |
2153 | - |
2154 | - >>> filename = a.filename |
2155 | - >>> a.filename = None |
2156 | - >>> values = a.write() |
2157 | - >>> index = 0 |
2158 | - >>> while index < 23: |
2159 | - ... index += 1 |
2160 | - ... line = values[index-1] |
2161 | - ... assert line.endswith('# comment ' + str(index)) |
2162 | - >>> a.filename = filename |
2163 | - |
2164 | - >>> start_comment = ['# Initial Comment', '', '#'] |
2165 | - >>> end_comment = ['', '#', '# Final Comment'] |
2166 | - >>> newconfig = start_comment + testconfig1.split('\\n') + end_comment |
2167 | - >>> nc = ConfigObj(newconfig) |
2168 | - >>> nc.initial_comment |
2169 | - ['# Initial Comment', '', '#'] |
2170 | - >>> nc.final_comment |
2171 | - ['', '#', '# Final Comment'] |
2172 | - >>> nc.initial_comment == start_comment |
2173 | - 1 |
2174 | - >>> nc.final_comment == end_comment |
2175 | - 1 |
2176 | - """ |
2177 | - if not comment: |
2178 | - return '' |
2179 | - if self.indent_type == '\t': |
2180 | - start = self._a_to_u('\t') |
2181 | - else: |
2182 | - start = self._a_to_u(' ' * NUM_INDENT_SPACES) |
2183 | - if not comment.startswith('#'): |
2184 | - start += self._a_to_u('# ') |
2185 | - return (start + comment) |
2186 | - |
2187 | - def _compute_indent_string(self, depth): |
2188 | - """ |
2189 | - Compute the indent string, according to current indent_type and depth |
2190 | - """ |
2191 | - if self.indent_type == '': |
2192 | - # no indentation at all |
2193 | - return '' |
2194 | - if self.indent_type == '\t': |
2195 | - return '\t' * depth |
2196 | - if self.indent_type == ' ': |
2197 | - return ' ' * NUM_INDENT_SPACES * depth |
2198 | - raise SyntaxError |
2199 | - |
2200 | - # Public methods |
2201 | - |
2202 | - def write(self, outfile=None, section=None): |
2203 | - """ |
2204 | - Write the current ConfigObj as a file |
2205 | - |
2206 | - tekNico: FIXME: use StringIO instead of real files |
2207 | - |
2208 | - >>> filename = a.filename |
2209 | - >>> a.filename = 'test.ini' |
2210 | - >>> a.write() |
2211 | - >>> a.filename = filename |
2212 | - >>> a == ConfigObj('test.ini', raise_errors=True) |
2213 | - 1 |
2214 | - >>> os.remove('test.ini') |
2215 | - >>> b.filename = 'test.ini' |
2216 | - >>> b.write() |
2217 | - >>> b == ConfigObj('test.ini', raise_errors=True) |
2218 | - 1 |
2219 | - >>> os.remove('test.ini') |
2220 | - >>> i.filename = 'test.ini' |
2221 | - >>> i.write() |
2222 | - >>> i == ConfigObj('test.ini', raise_errors=True) |
2223 | - 1 |
2224 | - >>> os.remove('test.ini') |
2225 | - >>> a = ConfigObj() |
2226 | - >>> a['DEFAULT'] = {'a' : 'fish'} |
2227 | - >>> a['a'] = '%(a)s' |
2228 | - >>> a.write() |
2229 | - ['a = %(a)s', '[DEFAULT]', 'a = fish'] |
2230 | - """ |
2231 | - if self.indent_type is None: |
2232 | - # this can be true if initialised from a dictionary |
2233 | - self.indent_type = DEFAULT_INDENT_TYPE |
2234 | - # |
2235 | - out = [] |
2236 | - cs = self._a_to_u('#') |
2237 | - csp = self._a_to_u('# ') |
2238 | - if section is None: |
2239 | - int_val = self.interpolation |
2240 | - self.interpolation = False |
2241 | - section = self |
2242 | - for line in self.initial_comment: |
2243 | - line = self._decode_element(line) |
2244 | - stripped_line = line.strip() |
2245 | - if stripped_line and not stripped_line.startswith(cs): |
2246 | - line = csp + line |
2247 | - out.append(line) |
2248 | - # |
2249 | - indent_string = self._a_to_u( |
2250 | - self._compute_indent_string(section.depth)) |
2251 | - for entry in (section.scalars + section.sections): |
2252 | - if entry in section.defaults: |
2253 | - # don't write out default values |
2254 | - continue |
2255 | - for comment_line in section.comments[entry]: |
2256 | - comment_line = self._decode_element(comment_line.lstrip()) |
2257 | - if comment_line and not comment_line.startswith(cs): |
2258 | - comment_line = csp + comment_line |
2259 | - out.append(indent_string + comment_line) |
2260 | - this_entry = section[entry] |
2261 | - comment = self._handle_comment(section.inline_comments[entry]) |
2262 | - # |
2263 | - if isinstance(this_entry, dict): |
2264 | - # a section |
2265 | - out.append(self._write_marker( |
2266 | - indent_string, |
2267 | - this_entry.depth, |
2268 | - entry, |
2269 | - comment)) |
2270 | - out.extend(self.write(section=this_entry)) |
2271 | - else: |
2272 | - out.append(self._write_line( |
2273 | - indent_string, |
2274 | - entry, |
2275 | - this_entry, |
2276 | - comment)) |
2277 | - # |
2278 | - if section is self: |
2279 | - for line in self.final_comment: |
2280 | - line = self._decode_element(line) |
2281 | - stripped_line = line.strip() |
2282 | - if stripped_line and not stripped_line.startswith(cs): |
2283 | - line = csp + line |
2284 | - out.append(line) |
2285 | - self.interpolation = int_val |
2286 | - # |
2287 | - if section is not self: |
2288 | - return out |
2289 | - # |
2290 | - if (self.filename is None) and (outfile is None): |
2291 | - # output a list of lines |
2292 | - # might need to encode |
2293 | - # NOTE: This will *screw* UTF16, each line will start with the BOM |
2294 | - if self.encoding: |
2295 | - out = [l.encode(self.encoding) for l in out] |
2296 | - if (self.BOM and ((self.encoding is None) or |
2297 | - (BOM_LIST.get(self.encoding.lower()) == 'utf_8'))): |
2298 | - # Add the UTF8 BOM |
2299 | - if not out: |
2300 | - out.append('') |
2301 | - out[0] = BOM_UTF8 + out[0] |
2302 | - return out |
2303 | - # |
2304 | - # Turn the list to a string, joined with correct newlines |
2305 | - output = (self._a_to_u(self.newlines or os.linesep)).join(out) |
2306 | - if self.encoding: |
2307 | - output = output.encode(self.encoding) |
2308 | - if (self.BOM and ((self.encoding is None) or |
2309 | - (BOM_LIST.get(self.encoding.lower()) == 'utf_8'))): |
2310 | - # Add the UTF8 BOM |
2311 | - output = BOM_UTF8 + output |
2312 | - if outfile is not None: |
2313 | - outfile.write(output) |
2314 | - else: |
2315 | - h = open(self.filename, 'wb') |
2316 | - h.write(output) |
2317 | - h.close() |
2318 | - |
2319 | - def validate(self, validator, preserve_errors=False, copy=False, |
2320 | - section=None): |
2321 | - """ |
2322 | - Test the ConfigObj against a configspec. |
2323 | - |
2324 | - It uses the ``validator`` object from *validate.py*. |
2325 | - |
2326 | - To run ``validate`` on the current ConfigObj, call: :: |
2327 | - |
2328 | - test = config.validate(validator) |
2329 | - |
2330 | - (Normally having previously passed in the configspec when the ConfigObj |
2331 | - was created - you can dynamically assign a dictionary of checks to the |
2332 | - ``configspec`` attribute of a section though). |
2333 | - |
2334 | - It returns ``True`` if everything passes, or a dictionary of |
2335 | - pass/fails (True/False). If every member of a subsection passes, it |
2336 | - will just have the value ``True``. (It also returns ``False`` if all |
2337 | - members fail). |
2338 | - |
2339 | - In addition, it converts the values from strings to their native |
2340 | - types if their checks pass (and ``stringify`` is set). |
2341 | - |
2342 | - If ``preserve_errors`` is ``True`` (``False`` is default) then instead |
2343 | - of a marking a fail with a ``False``, it will preserve the actual |
2344 | - exception object. This can contain info about the reason for failure. |
2345 | - For example the ``VdtValueTooSmallError`` indeicates that the value |
2346 | - supplied was too small. If a value (or section) is missing it will |
2347 | - still be marked as ``False``. |
2348 | - |
2349 | - You must have the validate module to use ``preserve_errors=True``. |
2350 | - |
2351 | - You can then use the ``flatten_errors`` function to turn your nested |
2352 | - results dictionary into a flattened list of failures - useful for |
2353 | - displaying meaningful error messages. |
2354 | - |
2355 | - >>> try: |
2356 | - ... from validate import Validator |
2357 | - ... except ImportError: |
2358 | - ... print >> sys.stderr, 'Cannot import the Validator object, skipping the related tests' |
2359 | - ... else: |
2360 | - ... config = ''' |
2361 | - ... test1=40 |
2362 | - ... test2=hello |
2363 | - ... test3=3 |
2364 | - ... test4=5.0 |
2365 | - ... [section] |
2366 | - ... test1=40 |
2367 | - ... test2=hello |
2368 | - ... test3=3 |
2369 | - ... test4=5.0 |
2370 | - ... [[sub section]] |
2371 | - ... test1=40 |
2372 | - ... test2=hello |
2373 | - ... test3=3 |
2374 | - ... test4=5.0 |
2375 | - ... '''.split('\\n') |
2376 | - ... configspec = ''' |
2377 | - ... test1= integer(30,50) |
2378 | - ... test2= string |
2379 | - ... test3=integer |
2380 | - ... test4=float(6.0) |
2381 | - ... [section ] |
2382 | - ... test1=integer(30,50) |
2383 | - ... test2=string |
2384 | - ... test3=integer |
2385 | - ... test4=float(6.0) |
2386 | - ... [[sub section]] |
2387 | - ... test1=integer(30,50) |
2388 | - ... test2=string |
2389 | - ... test3=integer |
2390 | - ... test4=float(6.0) |
2391 | - ... '''.split('\\n') |
2392 | - ... val = Validator() |
2393 | - ... c1 = ConfigObj(config, configspec=configspec) |
2394 | - ... test = c1.validate(val) |
2395 | - ... test == { |
2396 | - ... 'test1': True, |
2397 | - ... 'test2': True, |
2398 | - ... 'test3': True, |
2399 | - ... 'test4': False, |
2400 | - ... 'section': { |
2401 | - ... 'test1': True, |
2402 | - ... 'test2': True, |
2403 | - ... 'test3': True, |
2404 | - ... 'test4': False, |
2405 | - ... 'sub section': { |
2406 | - ... 'test1': True, |
2407 | - ... 'test2': True, |
2408 | - ... 'test3': True, |
2409 | - ... 'test4': False, |
2410 | - ... }, |
2411 | - ... }, |
2412 | - ... } |
2413 | - 1 |
2414 | - >>> val.check(c1.configspec['test4'], c1['test4']) |
2415 | - Traceback (most recent call last): |
2416 | - VdtValueTooSmallError: the value "5.0" is too small. |
2417 | - |
2418 | - >>> val_test_config = ''' |
2419 | - ... key = 0 |
2420 | - ... key2 = 1.1 |
2421 | - ... [section] |
2422 | - ... key = some text |
2423 | - ... key2 = 1.1, 3.0, 17, 6.8 |
2424 | - ... [[sub-section]] |
2425 | - ... key = option1 |
2426 | - ... key2 = True'''.split('\\n') |
2427 | - >>> val_test_configspec = ''' |
2428 | - ... key = integer |
2429 | - ... key2 = float |
2430 | - ... [section] |
2431 | - ... key = string |
2432 | - ... key2 = float_list(4) |
2433 | - ... [[sub-section]] |
2434 | - ... key = option(option1, option2) |
2435 | - ... key2 = boolean'''.split('\\n') |
2436 | - >>> val_test = ConfigObj(val_test_config, configspec=val_test_configspec) |
2437 | - >>> val_test.validate(val) |
2438 | - 1 |
2439 | - >>> val_test['key'] = 'text not a digit' |
2440 | - >>> val_res = val_test.validate(val) |
2441 | - >>> val_res == {'key2': True, 'section': True, 'key': False} |
2442 | - 1 |
2443 | - >>> configspec = ''' |
2444 | - ... test1=integer(30,50, default=40) |
2445 | - ... test2=string(default="hello") |
2446 | - ... test3=integer(default=3) |
2447 | - ... test4=float(6.0, default=6.0) |
2448 | - ... [section ] |
2449 | - ... test1=integer(30,50, default=40) |
2450 | - ... test2=string(default="hello") |
2451 | - ... test3=integer(default=3) |
2452 | - ... test4=float(6.0, default=6.0) |
2453 | - ... [[sub section]] |
2454 | - ... test1=integer(30,50, default=40) |
2455 | - ... test2=string(default="hello") |
2456 | - ... test3=integer(default=3) |
2457 | - ... test4=float(6.0, default=6.0) |
2458 | - ... '''.split('\\n') |
2459 | - >>> default_test = ConfigObj(['test1=30'], configspec=configspec) |
2460 | - >>> default_test |
2461 | - {'test1': '30', 'section': {'sub section': {}}} |
2462 | - >>> default_test.validate(val) |
2463 | - 1 |
2464 | - >>> default_test == { |
2465 | - ... 'test1': 30, |
2466 | - ... 'test2': 'hello', |
2467 | - ... 'test3': 3, |
2468 | - ... 'test4': 6.0, |
2469 | - ... 'section': { |
2470 | - ... 'test1': 40, |
2471 | - ... 'test2': 'hello', |
2472 | - ... 'test3': 3, |
2473 | - ... 'test4': 6.0, |
2474 | - ... 'sub section': { |
2475 | - ... 'test1': 40, |
2476 | - ... 'test3': 3, |
2477 | - ... 'test2': 'hello', |
2478 | - ... 'test4': 6.0, |
2479 | - ... }, |
2480 | - ... }, |
2481 | - ... } |
2482 | - 1 |
2483 | - |
2484 | - Now testing with repeated sections : BIG TEST |
2485 | - |
2486 | - >>> repeated_1 = ''' |
2487 | - ... [dogs] |
2488 | - ... [[__many__]] # spec for a dog |
2489 | - ... fleas = boolean(default=True) |
2490 | - ... tail = option(long, short, default=long) |
2491 | - ... name = string(default=rover) |
2492 | - ... [[[__many__]]] # spec for a puppy |
2493 | - ... name = string(default="son of rover") |
2494 | - ... age = float(default=0.0) |
2495 | - ... [cats] |
2496 | - ... [[__many__]] # spec for a cat |
2497 | - ... fleas = boolean(default=True) |
2498 | - ... tail = option(long, short, default=short) |
2499 | - ... name = string(default=pussy) |
2500 | - ... [[[__many__]]] # spec for a kitten |
2501 | - ... name = string(default="son of pussy") |
2502 | - ... age = float(default=0.0) |
2503 | - ... '''.split('\\n') |
2504 | - >>> repeated_2 = ''' |
2505 | - ... [dogs] |
2506 | - ... |
2507 | - ... # blank dogs with puppies |
2508 | - ... # should be filled in by the configspec |
2509 | - ... [[dog1]] |
2510 | - ... [[[puppy1]]] |
2511 | - ... [[[puppy2]]] |
2512 | - ... [[[puppy3]]] |
2513 | - ... [[dog2]] |
2514 | - ... [[[puppy1]]] |
2515 | - ... [[[puppy2]]] |
2516 | - ... [[[puppy3]]] |
2517 | - ... [[dog3]] |
2518 | - ... [[[puppy1]]] |
2519 | - ... [[[puppy2]]] |
2520 | - ... [[[puppy3]]] |
2521 | - ... [cats] |
2522 | - ... |
2523 | - ... # blank cats with kittens |
2524 | - ... # should be filled in by the configspec |
2525 | - ... [[cat1]] |
2526 | - ... [[[kitten1]]] |
2527 | - ... [[[kitten2]]] |
2528 | - ... [[[kitten3]]] |
2529 | - ... [[cat2]] |
2530 | - ... [[[kitten1]]] |
2531 | - ... [[[kitten2]]] |
2532 | - ... [[[kitten3]]] |
2533 | - ... [[cat3]] |
2534 | - ... [[[kitten1]]] |
2535 | - ... [[[kitten2]]] |
2536 | - ... [[[kitten3]]] |
2537 | - ... '''.split('\\n') |
2538 | - >>> repeated_3 = ''' |
2539 | - ... [dogs] |
2540 | - ... |
2541 | - ... [[dog1]] |
2542 | - ... [[dog2]] |
2543 | - ... [[dog3]] |
2544 | - ... [cats] |
2545 | - ... |
2546 | - ... [[cat1]] |
2547 | - ... [[cat2]] |
2548 | - ... [[cat3]] |
2549 | - ... '''.split('\\n') |
2550 | - >>> repeated_4 = ''' |
2551 | - ... [__many__] |
2552 | - ... |
2553 | - ... name = string(default=Michael) |
2554 | - ... age = float(default=0.0) |
2555 | - ... sex = option(m, f, default=m) |
2556 | - ... '''.split('\\n') |
2557 | - >>> repeated_5 = ''' |
2558 | - ... [cats] |
2559 | - ... [[__many__]] |
2560 | - ... fleas = boolean(default=True) |
2561 | - ... tail = option(long, short, default=short) |
2562 | - ... name = string(default=pussy) |
2563 | - ... [[[description]]] |
2564 | - ... height = float(default=3.3) |
2565 | - ... weight = float(default=6) |
2566 | - ... [[[[coat]]]] |
2567 | - ... fur = option(black, grey, brown, "tortoise shell", default=black) |
2568 | - ... condition = integer(0,10, default=5) |
2569 | - ... '''.split('\\n') |
2570 | - >>> from validate import Validator |
2571 | - >>> val= Validator() |
2572 | - >>> repeater = ConfigObj(repeated_2, configspec=repeated_1) |
2573 | - >>> repeater.validate(val) |
2574 | - 1 |
2575 | - >>> repeater == { |
2576 | - ... 'dogs': { |
2577 | - ... 'dog1': { |
2578 | - ... 'fleas': True, |
2579 | - ... 'tail': 'long', |
2580 | - ... 'name': 'rover', |
2581 | - ... 'puppy1': {'name': 'son of rover', 'age': 0.0}, |
2582 | - ... 'puppy2': {'name': 'son of rover', 'age': 0.0}, |
2583 | - ... 'puppy3': {'name': 'son of rover', 'age': 0.0}, |
2584 | - ... }, |
2585 | - ... 'dog2': { |
2586 | - ... 'fleas': True, |
2587 | - ... 'tail': 'long', |
2588 | - ... 'name': 'rover', |
2589 | - ... 'puppy1': {'name': 'son of rover', 'age': 0.0}, |
2590 | - ... 'puppy2': {'name': 'son of rover', 'age': 0.0}, |
2591 | - ... 'puppy3': {'name': 'son of rover', 'age': 0.0}, |
2592 | - ... }, |
2593 | - ... 'dog3': { |
2594 | - ... 'fleas': True, |
2595 | - ... 'tail': 'long', |
2596 | - ... 'name': 'rover', |
2597 | - ... 'puppy1': {'name': 'son of rover', 'age': 0.0}, |
2598 | - ... 'puppy2': {'name': 'son of rover', 'age': 0.0}, |
2599 | - ... 'puppy3': {'name': 'son of rover', 'age': 0.0}, |
2600 | - ... }, |
2601 | - ... }, |
2602 | - ... 'cats': { |
2603 | - ... 'cat1': { |
2604 | - ... 'fleas': True, |
2605 | - ... 'tail': 'short', |
2606 | - ... 'name': 'pussy', |
2607 | - ... 'kitten1': {'name': 'son of pussy', 'age': 0.0}, |
2608 | - ... 'kitten2': {'name': 'son of pussy', 'age': 0.0}, |
2609 | - ... 'kitten3': {'name': 'son of pussy', 'age': 0.0}, |
2610 | - ... }, |
2611 | - ... 'cat2': { |
2612 | - ... 'fleas': True, |
2613 | - ... 'tail': 'short', |
2614 | - ... 'name': 'pussy', |
2615 | - ... 'kitten1': {'name': 'son of pussy', 'age': 0.0}, |
2616 | - ... 'kitten2': {'name': 'son of pussy', 'age': 0.0}, |
2617 | - ... 'kitten3': {'name': 'son of pussy', 'age': 0.0}, |
2618 | - ... }, |
2619 | - ... 'cat3': { |
2620 | - ... 'fleas': True, |
2621 | - ... 'tail': 'short', |
2622 | - ... 'name': 'pussy', |
2623 | - ... 'kitten1': {'name': 'son of pussy', 'age': 0.0}, |
2624 | - ... 'kitten2': {'name': 'son of pussy', 'age': 0.0}, |
2625 | - ... 'kitten3': {'name': 'son of pussy', 'age': 0.0}, |
2626 | - ... }, |
2627 | - ... }, |
2628 | - ... } |
2629 | - 1 |
2630 | - >>> repeater = ConfigObj(repeated_3, configspec=repeated_1) |
2631 | - >>> repeater.validate(val) |
2632 | - 1 |
2633 | - >>> repeater == { |
2634 | - ... 'cats': { |
2635 | - ... 'cat1': {'fleas': True, 'tail': 'short', 'name': 'pussy'}, |
2636 | - ... 'cat2': {'fleas': True, 'tail': 'short', 'name': 'pussy'}, |
2637 | - ... 'cat3': {'fleas': True, 'tail': 'short', 'name': 'pussy'}, |
2638 | - ... }, |
2639 | - ... 'dogs': { |
2640 | - ... 'dog1': {'fleas': True, 'tail': 'long', 'name': 'rover'}, |
2641 | - ... 'dog2': {'fleas': True, 'tail': 'long', 'name': 'rover'}, |
2642 | - ... 'dog3': {'fleas': True, 'tail': 'long', 'name': 'rover'}, |
2643 | - ... }, |
2644 | - ... } |
2645 | - 1 |
2646 | - >>> repeater = ConfigObj(configspec=repeated_4) |
2647 | - >>> repeater['Michael'] = {} |
2648 | - >>> repeater.validate(val) |
2649 | - 1 |
2650 | - >>> repeater == { |
2651 | - ... 'Michael': {'age': 0.0, 'name': 'Michael', 'sex': 'm'}, |
2652 | - ... } |
2653 | - 1 |
2654 | - >>> repeater = ConfigObj(repeated_3, configspec=repeated_5) |
2655 | - >>> repeater == { |
2656 | - ... 'dogs': {'dog1': {}, 'dog2': {}, 'dog3': {}}, |
2657 | - ... 'cats': {'cat1': {}, 'cat2': {}, 'cat3': {}}, |
2658 | - ... } |
2659 | - 1 |
2660 | - >>> repeater.validate(val) |
2661 | - 1 |
2662 | - >>> repeater == { |
2663 | - ... 'dogs': {'dog1': {}, 'dog2': {}, 'dog3': {}}, |
2664 | - ... 'cats': { |
2665 | - ... 'cat1': { |
2666 | - ... 'fleas': True, |
2667 | - ... 'tail': 'short', |
2668 | - ... 'name': 'pussy', |
2669 | - ... 'description': { |
2670 | - ... 'weight': 6.0, |
2671 | - ... 'height': 3.2999999999999998, |
2672 | - ... 'coat': {'fur': 'black', 'condition': 5}, |
2673 | - ... }, |
2674 | - ... }, |
2675 | - ... 'cat2': { |
2676 | - ... 'fleas': True, |
2677 | - ... 'tail': 'short', |
2678 | - ... 'name': 'pussy', |
2679 | - ... 'description': { |
2680 | - ... 'weight': 6.0, |
2681 | - ... 'height': 3.2999999999999998, |
2682 | - ... 'coat': {'fur': 'black', 'condition': 5}, |
2683 | - ... }, |
2684 | - ... }, |
2685 | - ... 'cat3': { |
2686 | - ... 'fleas': True, |
2687 | - ... 'tail': 'short', |
2688 | - ... 'name': 'pussy', |
2689 | - ... 'description': { |
2690 | - ... 'weight': 6.0, |
2691 | - ... 'height': 3.2999999999999998, |
2692 | - ... 'coat': {'fur': 'black', 'condition': 5}, |
2693 | - ... }, |
2694 | - ... }, |
2695 | - ... }, |
2696 | - ... } |
2697 | - 1 |
2698 | - |
2699 | - Test that interpolation is preserved for validated string values. |
2700 | - Also check that interpolation works in configspecs. |
2701 | - >>> t = ConfigObj() |
2702 | - >>> t['DEFAULT'] = {} |
2703 | - >>> t['DEFAULT']['test'] = 'a' |
2704 | - >>> t['test'] = '%(test)s' |
2705 | - >>> t['test'] |
2706 | - 'a' |
2707 | - >>> v = Validator() |
2708 | - >>> t.configspec = {'test': 'string'} |
2709 | - >>> t.validate(v) |
2710 | - 1 |
2711 | - >>> t.interpolation = False |
2712 | - >>> t |
2713 | - {'test': '%(test)s', 'DEFAULT': {'test': 'a'}} |
2714 | - >>> specs = [ |
2715 | - ... 'interpolated string = string(default="fuzzy-%(man)s")', |
2716 | - ... '[DEFAULT]', |
2717 | - ... 'man = wuzzy', |
2718 | - ... ] |
2719 | - >>> c = ConfigObj(configspec=specs) |
2720 | - >>> c.validate(v) |
2721 | - 1 |
2722 | - >>> c['interpolated string'] |
2723 | - 'fuzzy-wuzzy' |
2724 | - |
2725 | - FIXME: Above tests will fail if we couldn't import Validator (the ones |
2726 | - that don't raise errors will produce different output and still fail as |
2727 | - tests) |
2728 | - |
2729 | - Test |
2730 | - """ |
2731 | - if section is None: |
2732 | - if self.configspec is None: |
2733 | - raise ValueError, 'No configspec supplied.' |
2734 | - if preserve_errors: |
2735 | - if VdtMissingValue is None: |
2736 | - raise ImportError('Missing validate module.') |
2737 | - section = self |
2738 | - # |
2739 | - spec_section = section.configspec |
2740 | - if copy and hasattr(section, '_configspec_initial_comment'): |
2741 | - section.initial_comment = section._configspec_initial_comment |
2742 | - section.final_comment = section._configspec_final_comment |
2743 | - section.encoding = section._configspec_encoding |
2744 | - section.BOM = section._configspec_BOM |
2745 | - section.newlines = section._configspec_newlines |
2746 | - section.indent_type = section._configspec_indent_type |
2747 | - if '__many__' in section.configspec: |
2748 | - many = spec_section['__many__'] |
2749 | - # dynamically assign the configspecs |
2750 | - # for the sections below |
2751 | - for entry in section.sections: |
2752 | - self._handle_repeat(section[entry], many) |
2753 | - # |
2754 | - out = {} |
2755 | - ret_true = True |
2756 | - ret_false = True |
2757 | - order = [k for k in section._order if k in spec_section] |
2758 | - order += [k for k in spec_section if k not in order] |
2759 | - for entry in order: |
2760 | - if entry == '__many__': |
2761 | - continue |
2762 | - if (not entry in section.scalars) or (entry in section.defaults): |
2763 | - # missing entries |
2764 | - # or entries from defaults |
2765 | - missing = True |
2766 | - val = None |
2767 | - if copy and not entry in section.scalars: |
2768 | - # copy comments |
2769 | - section.comments[entry] = ( |
2770 | - section._configspec_comments.get(entry, [])) |
2771 | - section.inline_comments[entry] = ( |
2772 | - section._configspec_inline_comments.get(entry, '')) |
2773 | - # |
2774 | - else: |
2775 | - missing = False |
2776 | - val = section[entry] |
2777 | - try: |
2778 | - check = validator.check(spec_section[entry], |
2779 | - val, |
2780 | - missing=missing |
2781 | - ) |
2782 | - except validator.baseErrorClass, e: |
2783 | - if not preserve_errors or isinstance(e, VdtMissingValue): |
2784 | - out[entry] = False |
2785 | - else: |
2786 | - # preserve the error |
2787 | - out[entry] = e |
2788 | - ret_false = False |
2789 | - ret_true = False |
2790 | - else: |
2791 | - ret_false = False |
2792 | - out[entry] = True |
2793 | - if self.stringify or missing: |
2794 | - # if we are doing type conversion |
2795 | - # or the value is a supplied default |
2796 | - if not self.stringify: |
2797 | - if isinstance(check, (list, tuple)): |
2798 | - # preserve lists |
2799 | - check = [self._str(item) for item in check] |
2800 | - elif missing and check is None: |
2801 | - # convert the None from a default to a '' |
2802 | - check = '' |
2803 | - else: |
2804 | - check = self._str(check) |
2805 | - if (check != val) or missing: |
2806 | - section[entry] = check |
2807 | - if not copy and missing and entry not in section.defaults: |
2808 | - section.defaults.append(entry) |
2809 | - # |
2810 | - # Missing sections will have been created as empty ones when the |
2811 | - # configspec was read. |
2812 | - for entry in section.sections: |
2813 | - # FIXME: this means DEFAULT is not copied in copy mode |
2814 | - if section is self and entry == 'DEFAULT': |
2815 | - continue |
2816 | - if copy: |
2817 | - section.comments[entry] = section._cs_section_comments[entry] |
2818 | - section.inline_comments[entry] = ( |
2819 | - section._cs_section_inline_comments[entry]) |
2820 | - check = self.validate(validator, preserve_errors=preserve_errors, |
2821 | - copy=copy, section=section[entry]) |
2822 | - out[entry] = check |
2823 | - if check == False: |
2824 | - ret_true = False |
2825 | - elif check == True: |
2826 | - ret_false = False |
2827 | - else: |
2828 | - ret_true = False |
2829 | - ret_false = False |
2830 | - # |
2831 | - if ret_true: |
2832 | - return True |
2833 | - elif ret_false: |
2834 | - return False |
2835 | - else: |
2836 | - return out |
2837 | - |
2838 | - |
2839 | -class SimpleVal(object): |
2840 | - """ |
2841 | - A simple validator. |
2842 | - Can be used to check that all members expected are present. |
2843 | - |
2844 | - To use it, provide a configspec with all your members in (the value given |
2845 | - will be ignored). Pass an instance of ``SimpleVal`` to the ``validate`` |
2846 | - method of your ``ConfigObj``. ``validate`` will return ``True`` if all |
2847 | - members are present, or a dictionary with True/False meaning |
2848 | - present/missing. (Whole missing sections will be replaced with ``False``) |
2849 | - |
2850 | - >>> val = SimpleVal() |
2851 | - >>> config = ''' |
2852 | - ... test1=40 |
2853 | - ... test2=hello |
2854 | - ... test3=3 |
2855 | - ... test4=5.0 |
2856 | - ... [section] |
2857 | - ... test1=40 |
2858 | - ... test2=hello |
2859 | - ... test3=3 |
2860 | - ... test4=5.0 |
2861 | - ... [[sub section]] |
2862 | - ... test1=40 |
2863 | - ... test2=hello |
2864 | - ... test3=3 |
2865 | - ... test4=5.0 |
2866 | - ... '''.split('\\n') |
2867 | - >>> configspec = ''' |
2868 | - ... test1='' |
2869 | - ... test2='' |
2870 | - ... test3='' |
2871 | - ... test4='' |
2872 | - ... [section] |
2873 | - ... test1='' |
2874 | - ... test2='' |
2875 | - ... test3='' |
2876 | - ... test4='' |
2877 | - ... [[sub section]] |
2878 | - ... test1='' |
2879 | - ... test2='' |
2880 | - ... test3='' |
2881 | - ... test4='' |
2882 | - ... '''.split('\\n') |
2883 | - >>> o = ConfigObj(config, configspec=configspec) |
2884 | - >>> o.validate(val) |
2885 | - 1 |
2886 | - >>> o = ConfigObj(configspec=configspec) |
2887 | - >>> o.validate(val) |
2888 | - 0 |
2889 | - """ |
2890 | - |
2891 | - def __init__(self): |
2892 | - self.baseErrorClass = ConfigObjError |
2893 | - |
2894 | - def check(self, check, member, missing=False): |
2895 | - """A dummy check method, always returns the value unchanged.""" |
2896 | - if missing: |
2897 | - raise self.baseErrorClass |
2898 | - return member |
2899 | - |
2900 | -# Check / processing functions for options |
2901 | -def flatten_errors(cfg, res, levels=None, results=None): |
2902 | - """ |
2903 | - An example function that will turn a nested dictionary of results |
2904 | - (as returned by ``ConfigObj.validate``) into a flat list. |
2905 | - |
2906 | - ``cfg`` is the ConfigObj instance being checked, ``res`` is the results |
2907 | - dictionary returned by ``validate``. |
2908 | - |
2909 | - (This is a recursive function, so you shouldn't use the ``levels`` or |
2910 | - ``results`` arguments - they are used by the function. |
2911 | - |
2912 | - Returns a list of keys that failed. Each member of the list is a tuple : |
2913 | - :: |
2914 | - |
2915 | - ([list of sections...], key, result) |
2916 | - |
2917 | - If ``validate`` was called with ``preserve_errors=False`` (the default) |
2918 | - then ``result`` will always be ``False``. |
2919 | - |
2920 | - *list of sections* is a flattened list of sections that the key was found |
2921 | - in. |
2922 | - |
2923 | - If the section was missing then key will be ``None``. |
2924 | - |
2925 | - If the value (or section) was missing then ``result`` will be ``False``. |
2926 | - |
2927 | - If ``validate`` was called with ``preserve_errors=True`` and a value |
2928 | - was present, but failed the check, then ``result`` will be the exception |
2929 | - object returned. You can use this as a string that describes the failure. |
2930 | - |
2931 | - For example *The value "3" is of the wrong type*. |
2932 | - |
2933 | - # FIXME: is the ordering of the output arbitrary ? |
2934 | - >>> import validate |
2935 | - >>> vtor = validate.Validator() |
2936 | - >>> my_ini = ''' |
2937 | - ... option1 = True |
2938 | - ... [section1] |
2939 | - ... option1 = True |
2940 | - ... [section2] |
2941 | - ... another_option = Probably |
2942 | - ... [section3] |
2943 | - ... another_option = True |
2944 | - ... [[section3b]] |
2945 | - ... value = 3 |
2946 | - ... value2 = a |
2947 | - ... value3 = 11 |
2948 | - ... ''' |
2949 | - >>> my_cfg = ''' |
2950 | - ... option1 = boolean() |
2951 | - ... option2 = boolean() |
2952 | - ... option3 = boolean(default=Bad_value) |
2953 | - ... [section1] |
2954 | - ... option1 = boolean() |
2955 | - ... option2 = boolean() |
2956 | - ... option3 = boolean(default=Bad_value) |
2957 | - ... [section2] |
2958 | - ... another_option = boolean() |
2959 | - ... [section3] |
2960 | - ... another_option = boolean() |
2961 | - ... [[section3b]] |
2962 | - ... value = integer |
2963 | - ... value2 = integer |
2964 | - ... value3 = integer(0, 10) |
2965 | - ... [[[section3b-sub]]] |
2966 | - ... value = string |
2967 | - ... [section4] |
2968 | - ... another_option = boolean() |
2969 | - ... ''' |
2970 | - >>> cs = my_cfg.split('\\n') |
2971 | - >>> ini = my_ini.split('\\n') |
2972 | - >>> cfg = ConfigObj(ini, configspec=cs) |
2973 | - >>> res = cfg.validate(vtor, preserve_errors=True) |
2974 | - >>> errors = [] |
2975 | - >>> for entry in flatten_errors(cfg, res): |
2976 | - ... section_list, key, error = entry |
2977 | - ... section_list.insert(0, '[root]') |
2978 | - ... if key is not None: |
2979 | - ... section_list.append(key) |
2980 | - ... else: |
2981 | - ... section_list.append('[missing]') |
2982 | - ... section_string = ', '.join(section_list) |
2983 | - ... errors.append((section_string, ' = ', error)) |
2984 | - >>> errors.sort() |
2985 | - >>> for entry in errors: |
2986 | - ... print entry[0], entry[1], (entry[2] or 0) |
2987 | - [root], option2 = 0 |
2988 | - [root], option3 = the value "Bad_value" is of the wrong type. |
2989 | - [root], section1, option2 = 0 |
2990 | - [root], section1, option3 = the value "Bad_value" is of the wrong type. |
2991 | - [root], section2, another_option = the value "Probably" is of the wrong type. |
2992 | - [root], section3, section3b, section3b-sub, [missing] = 0 |
2993 | - [root], section3, section3b, value2 = the value "a" is of the wrong type. |
2994 | - [root], section3, section3b, value3 = the value "11" is too big. |
2995 | - [root], section4, [missing] = 0 |
2996 | - """ |
2997 | - if levels is None: |
2998 | - # first time called |
2999 | - levels = [] |
3000 | - results = [] |
3001 | - if res is True: |
3002 | - return results |
3003 | - if res is False: |
3004 | - results.append((levels[:], None, False)) |
3005 | - if levels: |
3006 | - levels.pop() |
3007 | - return results |
3008 | - for (key, val) in res.items(): |
3009 | - if val == True: |
3010 | - continue |
3011 | - if isinstance(cfg.get(key), dict): |
3012 | - # Go down one level |
3013 | - levels.append(key) |
3014 | - flatten_errors(cfg[key], val, levels, results) |
3015 | - continue |
3016 | - results.append((levels[:], key, val)) |
3017 | - # |
3018 | - # Go up one level |
3019 | - if levels: |
3020 | - levels.pop() |
3021 | - # |
3022 | - return results |
3023 | - |
3024 | - |
3025 | -# FIXME: test error code for badly built multiline values |
3026 | -# FIXME: test handling of StringIO |
3027 | -# FIXME: test interpolation with writing |
3028 | - |
3029 | -def _doctest(): |
3030 | - """ |
3031 | - Dummy function to hold some of the doctests. |
3032 | - |
3033 | - >>> a.depth |
3034 | - 0 |
3035 | - >>> a == { |
3036 | - ... 'key2': 'val', |
3037 | - ... 'key1': 'val', |
3038 | - ... 'lev1c': { |
3039 | - ... 'lev2c': { |
3040 | - ... 'lev3c': { |
3041 | - ... 'key1': 'val', |
3042 | - ... }, |
3043 | - ... }, |
3044 | - ... }, |
3045 | - ... 'lev1b': { |
3046 | - ... 'key2': 'val', |
3047 | - ... 'key1': 'val', |
3048 | - ... 'lev2ba': { |
3049 | - ... 'key1': 'val', |
3050 | - ... }, |
3051 | - ... 'lev2bb': { |
3052 | - ... 'key1': 'val', |
3053 | - ... }, |
3054 | - ... }, |
3055 | - ... 'lev1a': { |
3056 | - ... 'key2': 'val', |
3057 | - ... 'key1': 'val', |
3058 | - ... }, |
3059 | - ... } |
3060 | - 1 |
3061 | - >>> b.depth |
3062 | - 0 |
3063 | - >>> b == { |
3064 | - ... 'key3': 'val3', |
3065 | - ... 'key2': 'val2', |
3066 | - ... 'key1': 'val1', |
3067 | - ... 'section 1': { |
3068 | - ... 'keys11': 'val1', |
3069 | - ... 'keys13': 'val3', |
3070 | - ... 'keys12': 'val2', |
3071 | - ... }, |
3072 | - ... 'section 2': { |
3073 | - ... 'section 2 sub 1': { |
3074 | - ... 'fish': '3', |
3075 | - ... }, |
3076 | - ... 'keys21': 'val1', |
3077 | - ... 'keys22': 'val2', |
3078 | - ... 'keys23': 'val3', |
3079 | - ... }, |
3080 | - ... } |
3081 | - 1 |
3082 | - >>> t = ''' |
3083 | - ... 'a' = b # !"$%^&*(),::;'@~#= 33 |
3084 | - ... "b" = b #= 6, 33 |
3085 | - ... ''' .split('\\n') |
3086 | - >>> t2 = ConfigObj(t) |
3087 | - >>> assert t2 == {'a': 'b', 'b': 'b'} |
3088 | - >>> t2.inline_comments['b'] = '' |
3089 | - >>> del t2['a'] |
3090 | - >>> assert t2.write() == ['','b = b', ''] |
3091 | - |
3092 | - # Test ``list_values=False`` stuff |
3093 | - >>> c = ''' |
3094 | - ... key1 = no quotes |
3095 | - ... key2 = 'single quotes' |
3096 | - ... key3 = "double quotes" |
3097 | - ... key4 = "list", 'with', several, "quotes" |
3098 | - ... ''' |
3099 | - >>> cfg = ConfigObj(c.splitlines(), list_values=False) |
3100 | - >>> cfg == {'key1': 'no quotes', 'key2': "'single quotes'", |
3101 | - ... 'key3': '"double quotes"', |
3102 | - ... 'key4': '"list", \\'with\\', several, "quotes"' |
3103 | - ... } |
3104 | - 1 |
3105 | - >>> cfg = ConfigObj(list_values=False) |
3106 | - >>> cfg['key1'] = 'Multiline\\nValue' |
3107 | - >>> cfg['key2'] = '''"Value" with 'quotes' !''' |
3108 | - >>> cfg.write() |
3109 | - ["key1 = '''Multiline\\nValue'''", 'key2 = "Value" with \\'quotes\\' !'] |
3110 | - >>> cfg.list_values = True |
3111 | - >>> cfg.write() == ["key1 = '''Multiline\\nValue'''", |
3112 | - ... 'key2 = \\'\\'\\'"Value" with \\'quotes\\' !\\'\\'\\''] |
3113 | - 1 |
3114 | - |
3115 | - Test flatten_errors: |
3116 | - |
3117 | - >>> from validate import Validator, VdtValueTooSmallError |
3118 | - >>> config = ''' |
3119 | - ... test1=40 |
3120 | - ... test2=hello |
3121 | - ... test3=3 |
3122 | - ... test4=5.0 |
3123 | - ... [section] |
3124 | - ... test1=40 |
3125 | - ... test2=hello |
3126 | - ... test3=3 |
3127 | - ... test4=5.0 |
3128 | - ... [[sub section]] |
3129 | - ... test1=40 |
3130 | - ... test2=hello |
3131 | - ... test3=3 |
3132 | - ... test4=5.0 |
3133 | - ... '''.split('\\n') |
3134 | - >>> configspec = ''' |
3135 | - ... test1= integer(30,50) |
3136 | - ... test2= string |
3137 | - ... test3=integer |
3138 | - ... test4=float(6.0) |
3139 | - ... [section ] |
3140 | - ... test1=integer(30,50) |
3141 | - ... test2=string |
3142 | - ... test3=integer |
3143 | - ... test4=float(6.0) |
3144 | - ... [[sub section]] |
3145 | - ... test1=integer(30,50) |
3146 | - ... test2=string |
3147 | - ... test3=integer |
3148 | - ... test4=float(6.0) |
3149 | - ... '''.split('\\n') |
3150 | - >>> val = Validator() |
3151 | - >>> c1 = ConfigObj(config, configspec=configspec) |
3152 | - >>> res = c1.validate(val) |
3153 | - >>> flatten_errors(c1, res) == [([], 'test4', False), (['section', |
3154 | - ... 'sub section'], 'test4', False), (['section'], 'test4', False)] |
3155 | - True |
3156 | - >>> res = c1.validate(val, preserve_errors=True) |
3157 | - >>> check = flatten_errors(c1, res) |
3158 | - >>> check[0][:2] |
3159 | - ([], 'test4') |
3160 | - >>> check[1][:2] |
3161 | - (['section', 'sub section'], 'test4') |
3162 | - >>> check[2][:2] |
3163 | - (['section'], 'test4') |
3164 | - >>> for entry in check: |
3165 | - ... isinstance(entry[2], VdtValueTooSmallError) |
3166 | - ... print str(entry[2]) |
3167 | - True |
3168 | - the value "5.0" is too small. |
3169 | - True |
3170 | - the value "5.0" is too small. |
3171 | - True |
3172 | - the value "5.0" is too small. |
3173 | - |
3174 | - Test unicode handling, BOM, write witha file like object and line endings : |
3175 | - >>> u_base = ''' |
3176 | - ... # initial comment |
3177 | - ... # inital comment 2 |
3178 | - ... |
3179 | - ... test1 = some value |
3180 | - ... # comment |
3181 | - ... test2 = another value # inline comment |
3182 | - ... # section comment |
3183 | - ... [section] # inline comment |
3184 | - ... test = test # another inline comment |
3185 | - ... test2 = test2 |
3186 | - ... |
3187 | - ... # final comment |
3188 | - ... # final comment2 |
3189 | - ... ''' |
3190 | - >>> u = u_base.encode('utf_8').splitlines(True) |
3191 | - >>> u[0] = BOM_UTF8 + u[0] |
3192 | - >>> uc = ConfigObj(u) |
3193 | - >>> uc.encoding = None |
3194 | - >>> uc.BOM == True |
3195 | - 1 |
3196 | - >>> uc == {'test1': 'some value', 'test2': 'another value', |
3197 | - ... 'section': {'test': 'test', 'test2': 'test2'}} |
3198 | - 1 |
3199 | - >>> uc = ConfigObj(u, encoding='utf_8', default_encoding='latin-1') |
3200 | - >>> uc.BOM |
3201 | - 1 |
3202 | - >>> isinstance(uc['test1'], unicode) |
3203 | - 1 |
3204 | - >>> uc.encoding |
3205 | - 'utf_8' |
3206 | - >>> uc.newlines |
3207 | - '\\n' |
3208 | - >>> uc['latin1'] = "This costs lot's of " |
3209 | - >>> a_list = uc.write() |
3210 | - >>> len(a_list) |
3211 | - 15 |
3212 | - >>> isinstance(a_list[0], str) |
3213 | - 1 |
3214 | - >>> a_list[0].startswith(BOM_UTF8) |
3215 | - 1 |
3216 | - >>> u = u_base.replace('\\n', '\\r\\n').encode('utf_8').splitlines(True) |
3217 | - >>> uc = ConfigObj(u) |
3218 | - >>> uc.newlines |
3219 | - '\\r\\n' |
3220 | - >>> uc.newlines = '\\r' |
3221 | - >>> from cStringIO import StringIO |
3222 | - >>> file_like = StringIO() |
3223 | - >>> uc.write(file_like) |
3224 | - >>> file_like.seek(0) |
3225 | - >>> uc2 = ConfigObj(file_like) |
3226 | - >>> uc2 == uc |
3227 | - 1 |
3228 | - >>> uc2.filename == None |
3229 | - 1 |
3230 | - >>> uc2.newlines == '\\r' |
3231 | - 1 |
3232 | - |
3233 | - Test validate in copy mode |
3234 | - >>> a = ''' |
3235 | - ... # Initial Comment |
3236 | - ... |
3237 | - ... key1 = string(default=Hello) # comment 1 |
3238 | - ... |
3239 | - ... # section comment |
3240 | - ... [section] # inline comment |
3241 | - ... # key1 comment |
3242 | - ... key1 = integer(default=6) # an integer value |
3243 | - ... # key2 comment |
3244 | - ... key2 = boolean(default=True) # a boolean |
3245 | - ... |
3246 | - ... # subsection comment |
3247 | - ... [[sub-section]] # inline comment |
3248 | - ... # another key1 comment |
3249 | - ... key1 = float(default=3.0) # a float'''.splitlines() |
3250 | - >>> b = ConfigObj(configspec=a) |
3251 | - >>> b.validate(val, copy=True) |
3252 | - 1 |
3253 | - >>> b.write() == ['', |
3254 | - ... '# Initial Comment', |
3255 | - ... '', |
3256 | - ... 'key1 = Hello # comment 1', |
3257 | - ... '', |
3258 | - ... '# section comment', |
3259 | - ... '[section] # inline comment', |
3260 | - ... ' # key1 comment', |
3261 | - ... ' key1 = 6 # an integer value', |
3262 | - ... ' # key2 comment', |
3263 | - ... ' key2 = True # a boolean', |
3264 | - ... ' ', |
3265 | - ... ' # subsection comment', |
3266 | - ... ' [[sub-section]] # inline comment', |
3267 | - ... ' # another key1 comment', |
3268 | - ... ' key1 = 3.0 # a float'] |
3269 | - 1 |
3270 | - |
3271 | - Test Writing Empty Values |
3272 | - >>> a = ''' |
3273 | - ... key1 = |
3274 | - ... key2 =# a comment''' |
3275 | - >>> b = ConfigObj(a.splitlines()) |
3276 | - >>> b.write() |
3277 | - ['', 'key1 = ""', 'key2 = "" # a comment'] |
3278 | - >>> b.write_empty_values = True |
3279 | - >>> b.write() |
3280 | - ['', 'key1 = ', 'key2 = # a comment'] |
3281 | - |
3282 | - Test unrepr when reading |
3283 | - >>> a = ''' |
3284 | - ... key1 = (1, 2, 3) # comment |
3285 | - ... key2 = True |
3286 | - ... key3 = 'a string' |
3287 | - ... key4 = [1, 2, 3, 'a mixed list'] |
3288 | - ... '''.splitlines() |
3289 | - >>> b = ConfigObj(a, unrepr=True) |
3290 | - >>> b == {'key1': (1, 2, 3), |
3291 | - ... 'key2': True, |
3292 | - ... 'key3': 'a string', |
3293 | - ... 'key4': [1, 2, 3, 'a mixed list']} |
3294 | - 1 |
3295 | - |
3296 | - Test unrepr when writing |
3297 | - >>> c = ConfigObj(b.write(), unrepr=True) |
3298 | - >>> c == b |
3299 | - 1 |
3300 | - |
3301 | - Test unrepr with multiline values |
3302 | - >>> a = '''k = \"""{ |
3303 | - ... 'k1': 3, |
3304 | - ... 'k2': 6.0}\""" |
3305 | - ... '''.splitlines() |
3306 | - >>> c = ConfigObj(a, unrepr=True) |
3307 | - >>> c == {'k': {'k1': 3, 'k2': 6.0}} |
3308 | - 1 |
3309 | - |
3310 | - Test unrepr with a dictionary |
3311 | - >>> a = 'k = {"a": 1}'.splitlines() |
3312 | - >>> c = ConfigObj(a, unrepr=True) |
3313 | - >>> type(c['k']) == dict |
3314 | - 1 |
3315 | - """ |
3316 | - |
3317 | -if __name__ == '__main__': |
3318 | - # run the code tests in doctest format |
3319 | - # |
3320 | - testconfig1 = """\ |
3321 | - key1= val # comment 1 |
3322 | - key2= val # comment 2 |
3323 | - # comment 3 |
3324 | - [lev1a] # comment 4 |
3325 | - key1= val # comment 5 |
3326 | - key2= val # comment 6 |
3327 | - # comment 7 |
3328 | - [lev1b] # comment 8 |
3329 | - key1= val # comment 9 |
3330 | - key2= val # comment 10 |
3331 | - # comment 11 |
3332 | - [[lev2ba]] # comment 12 |
3333 | - key1= val # comment 13 |
3334 | - # comment 14 |
3335 | - [[lev2bb]] # comment 15 |
3336 | - key1= val # comment 16 |
3337 | - # comment 17 |
3338 | - [lev1c] # comment 18 |
3339 | - # comment 19 |
3340 | - [[lev2c]] # comment 20 |
3341 | - # comment 21 |
3342 | - [[[lev3c]]] # comment 22 |
3343 | - key1 = val # comment 23""" |
3344 | - # |
3345 | - testconfig2 = """\ |
3346 | - key1 = 'val1' |
3347 | - key2 = "val2" |
3348 | - key3 = val3 |
3349 | - ["section 1"] # comment |
3350 | - keys11 = val1 |
3351 | - keys12 = val2 |
3352 | - keys13 = val3 |
3353 | - [section 2] |
3354 | - keys21 = val1 |
3355 | - keys22 = val2 |
3356 | - keys23 = val3 |
3357 | - |
3358 | - [['section 2 sub 1']] |
3359 | - fish = 3 |
3360 | - """ |
3361 | - # |
3362 | - testconfig6 = ''' |
3363 | - name1 = """ a single line value """ # comment |
3364 | - name2 = \''' another single line value \''' # comment |
3365 | - name3 = """ a single line value """ |
3366 | - name4 = \''' another single line value \''' |
3367 | - [ "multi section" ] |
3368 | - name1 = """ |
3369 | - Well, this is a |
3370 | - multiline value |
3371 | - """ |
3372 | - name2 = \''' |
3373 | - Well, this is a |
3374 | - multiline value |
3375 | - \''' |
3376 | - name3 = """ |
3377 | - Well, this is a |
3378 | - multiline value |
3379 | - """ # a comment |
3380 | - name4 = \''' |
3381 | - Well, this is a |
3382 | - multiline value |
3383 | - \''' # I guess this is a comment too |
3384 | - ''' |
3385 | - # |
3386 | - import doctest |
3387 | - m = sys.modules.get('__main__') |
3388 | - globs = m.__dict__.copy() |
3389 | - a = ConfigObj(testconfig1.split('\n'), raise_errors=True) |
3390 | - b = ConfigObj(testconfig2.split('\n'), raise_errors=True) |
3391 | - i = ConfigObj(testconfig6.split('\n'), raise_errors=True) |
3392 | - globs.update({ |
3393 | - 'INTP_VER': INTP_VER, |
3394 | - 'a': a, |
3395 | - 'b': b, |
3396 | - 'i': i, |
3397 | - }) |
3398 | - doctest.testmod(m, globs=globs) |
3399 | - |
3400 | -""" |
3401 | - BUGS |
3402 | - ==== |
3403 | - |
3404 | - None known. |
3405 | - |
3406 | - TODO |
3407 | - ==== |
3408 | - |
3409 | - Better support for configuration from multiple files, including tracking |
3410 | - *where* the original file came from and writing changes to the correct |
3411 | - file. |
3412 | - |
3413 | - Make ``newline`` an option (as well as an attribute) ? |
3414 | - |
3415 | - ``UTF16`` encoded files, when returned as a list of lines, will have the |
3416 | - BOM at the start of every line. Should this be removed from all but the |
3417 | - first line ? |
3418 | - |
3419 | - Option to set warning type for unicode decode ? (Defaults to strict). |
3420 | - |
3421 | - A method to optionally remove uniform indentation from multiline values. |
3422 | - (do as an example of using ``walk`` - along with string-escape) |
3423 | - |
3424 | - Should the results dictionary from validate be an ordered dictionary if |
3425 | - `odict <http://www.voidspace.org.uk/python/odict.html>`_ is available ? |
3426 | - |
3427 | - Implement a better ``__repr__`` ? (``ConfigObj({})``) |
3428 | - |
3429 | - Implement some of the sequence methods (which include slicing) from the |
3430 | - newer ``odict`` ? |
3431 | - |
3432 | - ISSUES |
3433 | - ====== |
3434 | - |
3435 | - There is currently no way to specify the encoding of a configspec. |
3436 | - |
3437 | - When using ``copy`` mode for validation, it won't copy ``DEFAULT`` |
3438 | - sections. This is so that you *can* use interpolation in configspec |
3439 | - files. |
3440 | - |
3441 | - ``validate`` doesn't report *extra* values or sections. |
3442 | - |
3443 | - You can't have a keyword with the same name as a section (in the same |
3444 | - section). They are both dictionary keys - so they would overlap. |
3445 | - |
3446 | - ConfigObj doesn't quote and unquote values if ``list_values=False``. |
3447 | - This means that leading or trailing whitespace in values will be lost when |
3448 | - writing. (Unless you manually quote). |
3449 | - |
3450 | - Interpolation checks first the 'DEFAULT' subsection of the current |
3451 | - section, next it checks the 'DEFAULT' section of the parent section, |
3452 | - last it checks the 'DEFAULT' section of the main section. |
3453 | - |
3454 | - Logically a 'DEFAULT' section should apply to all subsections of the *same |
3455 | - parent* - this means that checking the 'DEFAULT' subsection in the |
3456 | - *current section* is not necessarily logical ? |
3457 | - |
3458 | - Does it matter that we don't support the ':' divider, which is supported |
3459 | - by ``ConfigParser`` ? |
3460 | - |
3461 | - String interpolation and validation don't play well together. When |
3462 | - validation changes type it sets the value. This will correctly fetch the |
3463 | - value using interpolation - but then overwrite the interpolation reference. |
3464 | - If the value is unchanged by validation (it's a string) - but other types |
3465 | - will be. |
3466 | - |
3467 | - |
3468 | - List Value Syntax |
3469 | - ================= |
3470 | - |
3471 | - List values allow you to specify multiple values for a keyword. This |
3472 | - maps to a list as the resulting Python object when parsed. |
3473 | - |
3474 | - The syntax for lists is easy. A list is a comma separated set of values. |
3475 | - If these values contain quotes, the hash mark, or commas, then the values |
3476 | - can be surrounded by quotes. e.g. : :: |
3477 | - |
3478 | - keyword = value1, 'value 2', "value 3" |
3479 | - |
3480 | - If a value needs to be a list, but only has one member, then you indicate |
3481 | - this with a trailing comma. e.g. : :: |
3482 | - |
3483 | - keyword = "single value", |
3484 | - |
3485 | - If a value needs to be a list, but it has no members, then you indicate |
3486 | - this with a single comma. e.g. : :: |
3487 | - |
3488 | - keyword = , # an empty list |
3489 | - |
3490 | - Using triple quotes it will be possible for single values to contain |
3491 | - newlines and *both* single quotes and double quotes. Triple quotes aren't |
3492 | - allowed in list values. This means that the members of list values can't |
3493 | - contain carriage returns (or line feeds :-) or both quote values. |
3494 | - |
3495 | - CHANGELOG |
3496 | - ========= |
3497 | - |
3498 | - 2006/03/20 |
3499 | - ---------- |
3500 | - |
3501 | - Empty values are now valid syntax. They are read as an empty string ``''``. |
3502 | - (``key =``, or ``key = # comment``.) |
3503 | - |
3504 | - ``validate`` now honours the order of the configspec. |
3505 | - |
3506 | - Added the ``copy`` mode to validate. |
3507 | - |
3508 | - Fixed bug where files written on windows could be given '\r\r\n' line |
3509 | - terminators. |
3510 | - |
3511 | - Fixed bug where last occuring comment line could be interpreted as the |
3512 | - final comment if the last line isn't terminated. |
3513 | - |
3514 | - Fixed bug where nested list values would be flattened when ``write`` is |
3515 | - called. Now sub-lists have a string representation written instead. |
3516 | - |
3517 | - Deprecated ``encode`` and ``decode`` methods instead. |
3518 | - |
3519 | - You can now pass in a COnfigObj instance as a configspec (remember to read |
3520 | - the file using ``list_values=False``). |
3521 | - |
3522 | - 2006/02/04 |
3523 | - ---------- |
3524 | - |
3525 | - Removed ``BOM_UTF8`` from ``__all__``. |
3526 | - |
3527 | - The ``BOM`` attribute has become a boolean. (Defaults to ``False``.) It can |
3528 | - be ``True`` for the ``UTF16/UTF8`` encodings. |
3529 | - |
3530 | - File like objects no longer need a ``seek`` attribute. |
3531 | - |
3532 | - ConfigObj no longer keeps a reference to file like objects. Instead the |
3533 | - ``write`` method takes a file like object as an optional argument. (Which |
3534 | - will be used in preference of the ``filename`` attribute if htat exists as |
3535 | - well.) |
3536 | - |
3537 | - Full unicode support added. New options/attributes ``encoding``, |
3538 | - ``default_encoding``. |
3539 | - |
3540 | - utf16 files decoded to unicode. |
3541 | - |
3542 | - If ``BOM`` is ``True``, but no encoding specified, then the utf8 BOM is |
3543 | - written out at the start of the file. (It will normally only be ``True`` if |
3544 | - the utf8 BOM was found when the file was read.) |
3545 | - |
3546 | - File paths are *not* converted to absolute paths, relative paths will |
3547 | - remain relative as the ``filename`` attribute. |
3548 | - |
3549 | - Fixed bug where ``final_comment`` wasn't returned if ``write`` is returning |
3550 | - a list of lines. |
3551 | - |
3552 | - 2006/01/31 |
3553 | - ---------- |
3554 | - |
3555 | - Added ``True``, ``False``, and ``enumerate`` if they are not defined. |
3556 | - (``True`` and ``False`` are needed for *early* versions of Python 2.2, |
3557 | - ``enumerate`` is needed for all versions ofPython 2.2) |
3558 | - |
3559 | - Deprecated ``istrue``, replaced it with ``as_bool``. |
3560 | - |
3561 | - Added ``as_int`` and ``as_float``. |
3562 | - |
3563 | - utf8 and utf16 BOM handled in an endian agnostic way. |
3564 | - |
3565 | - 2005/12/14 |
3566 | - ---------- |
3567 | - |
3568 | - Validation no longer done on the 'DEFAULT' section (only in the root |
3569 | - level). This allows interpolation in configspecs. |
3570 | - |
3571 | - Change in validation syntax implemented in validate 0.2.1 |
3572 | - |
3573 | - 4.1.0 |
3574 | - |
3575 | - 2005/12/10 |
3576 | - ---------- |
3577 | - |
3578 | - Added ``merge``, a recursive update. |
3579 | - |
3580 | - Added ``preserve_errors`` to ``validate`` and the ``flatten_errors`` |
3581 | - example function. |
3582 | - |
3583 | - Thanks to Matthew Brett for suggestions and helping me iron out bugs. |
3584 | - |
3585 | - Fixed bug where a config file is *all* comment, the comment will now be |
3586 | - ``initial_comment`` rather than ``final_comment``. |
3587 | - |
3588 | - 2005/12/02 |
3589 | - ---------- |
3590 | - |
3591 | - Fixed bug in ``create_empty``. Thanks to Paul Jimenez for the report. |
3592 | - |
3593 | - 2005/11/04 |
3594 | - ---------- |
3595 | - |
3596 | - Fixed bug in ``Section.walk`` when transforming names as well as values. |
3597 | - |
3598 | - Added the ``istrue`` method. (Fetches the boolean equivalent of a string |
3599 | - value). |
3600 | - |
3601 | - Fixed ``list_values=False`` - they are now only quoted/unquoted if they |
3602 | - are multiline values. |
3603 | - |
3604 | - List values are written as ``item, item`` rather than ``item,item``. |
3605 | - |
3606 | - 4.0.1 |
3607 | - |
3608 | - 2005/10/09 |
3609 | - ---------- |
3610 | - |
3611 | - Fixed typo in ``write`` method. (Testing for the wrong value when resetting |
3612 | - ``interpolation``). |
3613 | - |
3614 | - 4.0.0 Final |
3615 | - |
3616 | - 2005/09/16 |
3617 | - ---------- |
3618 | - |
3619 | - Fixed bug in ``setdefault`` - creating a new section *wouldn't* return |
3620 | - a reference to the new section. |
3621 | - |
3622 | - 2005/09/09 |
3623 | - ---------- |
3624 | - |
3625 | - Removed ``PositionError``. |
3626 | - |
3627 | - Allowed quotes around keys as documented. |
3628 | - |
3629 | - Fixed bug with commas in comments. (matched as a list value) |
3630 | - |
3631 | - Beta 5 |
3632 | - |
3633 | - 2005/09/07 |
3634 | - ---------- |
3635 | - |
3636 | - Fixed bug in initialising ConfigObj from a ConfigObj. |
3637 | - |
3638 | - Changed the mailing list address. |
3639 | - |
3640 | - Beta 4 |
3641 | - |
3642 | - 2005/09/03 |
3643 | - ---------- |
3644 | - |
3645 | - Fixed bug in ``Section.__delitem__`` oops. |
3646 | - |
3647 | - 2005/08/28 |
3648 | - ---------- |
3649 | - |
3650 | - Interpolation is switched off before writing out files. |
3651 | - |
3652 | - Fixed bug in handling ``StringIO`` instances. (Thanks to report from |
3653 | - "Gustavo Niemeyer" <gustavo@niemeyer.net>) |
3654 | - |
3655 | - Moved the doctests from the ``__init__`` method to a separate function. |
3656 | - (For the sake of IDE calltips). |
3657 | - |
3658 | - Beta 3 |
3659 | - |
3660 | - 2005/08/26 |
3661 | - ---------- |
3662 | - |
3663 | - String values unchanged by validation *aren't* reset. This preserves |
3664 | - interpolation in string values. |
3665 | - |
3666 | - 2005/08/18 |
3667 | - ---------- |
3668 | - |
3669 | - None from a default is turned to '' if stringify is off - because setting |
3670 | - a value to None raises an error. |
3671 | - |
3672 | - Version 4.0.0-beta2 |
3673 | - |
3674 | - 2005/08/16 |
3675 | - ---------- |
3676 | - |
3677 | - By Nicola Larosa |
3678 | - |
3679 | - Actually added the RepeatSectionError class ;-) |
3680 | - |
3681 | - 2005/08/15 |
3682 | - ---------- |
3683 | - |
3684 | - If ``stringify`` is off - list values are preserved by the ``validate`` |
3685 | - method. (Bugfix) |
3686 | - |
3687 | - 2005/08/14 |
3688 | - ---------- |
3689 | - |
3690 | - By Michael Foord |
3691 | - |
3692 | - Fixed ``simpleVal``. |
3693 | - |
3694 | - Added ``RepeatSectionError`` error if you have additional sections in a |
3695 | - section with a ``__many__`` (repeated) section. |
3696 | - |
3697 | - By Nicola Larosa |
3698 | - |
3699 | - Reworked the ConfigObj._parse, _handle_error and _multiline methods: |
3700 | - mutated the self._infile, self._index and self._maxline attributes into |
3701 | - local variables and method parameters |
3702 | - |
3703 | - Reshaped the ConfigObj._multiline method to better reflect its semantics |
3704 | - |
3705 | - Changed the "default_test" test in ConfigObj.validate to check the fix for |
3706 | - the bug in validate.Validator.check |
3707 | - |
3708 | - 2005/08/13 |
3709 | - ---------- |
3710 | - |
3711 | - By Nicola Larosa |
3712 | - |
3713 | - Updated comments at top |
3714 | - |
3715 | - 2005/08/11 |
3716 | - ---------- |
3717 | - |
3718 | - By Michael Foord |
3719 | - |
3720 | - Implemented repeated sections. |
3721 | - |
3722 | - By Nicola Larosa |
3723 | - |
3724 | - Added test for interpreter version: raises RuntimeError if earlier than |
3725 | - 2.2 |
3726 | - |
3727 | - 2005/08/10 |
3728 | - ---------- |
3729 | - |
3730 | - By Michael Foord |
3731 | - |
3732 | - Implemented default values in configspecs. |
3733 | - |
3734 | - By Nicola Larosa |
3735 | - |
3736 | - Fixed naked except: clause in validate that was silencing the fact |
3737 | - that Python2.2 does not have dict.pop |
3738 | - |
3739 | - 2005/08/08 |
3740 | - ---------- |
3741 | - |
3742 | - By Michael Foord |
3743 | - |
3744 | - Bug fix causing error if file didn't exist. |
3745 | - |
3746 | - 2005/08/07 |
3747 | - ---------- |
3748 | - |
3749 | - By Nicola Larosa |
3750 | - |
3751 | - Adjusted doctests for Python 2.2.3 compatibility |
3752 | - |
3753 | - 2005/08/04 |
3754 | - ---------- |
3755 | - |
3756 | - By Michael Foord |
3757 | - |
3758 | - Added the inline_comments attribute |
3759 | - |
3760 | - We now preserve and rewrite all comments in the config file |
3761 | - |
3762 | - configspec is now a section attribute |
3763 | - |
3764 | - The validate method changes values in place |
3765 | - |
3766 | - Added InterpolationError |
3767 | - |
3768 | - The errors now have line number, line, and message attributes. This |
3769 | - simplifies error handling |
3770 | - |
3771 | - Added __docformat__ |
3772 | - |
3773 | - 2005/08/03 |
3774 | - ---------- |
3775 | - |
3776 | - By Michael Foord |
3777 | - |
3778 | - Fixed bug in Section.pop (now doesn't raise KeyError if a default value |
3779 | - is specified) |
3780 | - |
3781 | - Replaced ``basestring`` with ``types.StringTypes`` |
3782 | - |
3783 | - Removed the ``writein`` method |
3784 | - |
3785 | - Added __version__ |
3786 | - |
3787 | - 2005/07/29 |
3788 | - ---------- |
3789 | - |
3790 | - By Nicola Larosa |
3791 | - |
3792 | - Indentation in config file is not significant anymore, subsections are |
3793 | - designated by repeating square brackets |
3794 | - |
3795 | - Adapted all tests and docs to the new format |
3796 | - |
3797 | - 2005/07/28 |
3798 | - ---------- |
3799 | - |
3800 | - By Nicola Larosa |
3801 | - |
3802 | - Added more tests |
3803 | - |
3804 | - 2005/07/23 |
3805 | - ---------- |
3806 | - |
3807 | - By Nicola Larosa |
3808 | - |
3809 | - Reformatted final docstring in ReST format, indented it for easier folding |
3810 | - |
3811 | - Code tests converted to doctest format, and scattered them around |
3812 | - in various docstrings |
3813 | - |
3814 | - Walk method rewritten using scalars and sections attributes |
3815 | - |
3816 | - 2005/07/22 |
3817 | - ---------- |
3818 | - |
3819 | - By Nicola Larosa |
3820 | - |
3821 | - Changed Validator and SimpleVal "test" methods to "check" |
3822 | - |
3823 | - More code cleanup |
3824 | - |
3825 | - 2005/07/21 |
3826 | - ---------- |
3827 | - |
3828 | - Changed Section.sequence to Section.scalars and Section.sections |
3829 | - |
3830 | - Added Section.configspec |
3831 | - |
3832 | - Sections in the root section now have no extra indentation |
3833 | - |
3834 | - Comments now better supported in Section and preserved by ConfigObj |
3835 | - |
3836 | - Comments also written out |
3837 | - |
3838 | - Implemented initial_comment and final_comment |
3839 | - |
3840 | - A scalar value after a section will now raise an error |
3841 | - |
3842 | - 2005/07/20 |
3843 | - ---------- |
3844 | - |
3845 | - Fixed a couple of bugs |
3846 | - |
3847 | - Can now pass a tuple instead of a list |
3848 | - |
3849 | - Simplified dict and walk methods |
3850 | - |
3851 | - Added __str__ to Section |
3852 | - |
3853 | - 2005/07/10 |
3854 | - ---------- |
3855 | - |
3856 | - By Nicola Larosa |
3857 | - |
3858 | - More code cleanup |
3859 | - |
3860 | - 2005/07/08 |
3861 | - ---------- |
3862 | - |
3863 | - The stringify option implemented. On by default. |
3864 | - |
3865 | - 2005/07/07 |
3866 | - ---------- |
3867 | - |
3868 | - Renamed private attributes with a single underscore prefix. |
3869 | - |
3870 | - Changes to interpolation - exceeding recursion depth, or specifying a |
3871 | - missing value, now raise errors. |
3872 | - |
3873 | - Changes for Python 2.2 compatibility. (changed boolean tests - removed |
3874 | - ``is True`` and ``is False``) |
3875 | - |
3876 | - Added test for duplicate section and member (and fixed bug) |
3877 | - |
3878 | - 2005/07/06 |
3879 | - ---------- |
3880 | - |
3881 | - By Nicola Larosa |
3882 | - |
3883 | - Code cleanup |
3884 | - |
3885 | - 2005/07/02 |
3886 | - ---------- |
3887 | - |
3888 | - Version 0.1.0 |
3889 | - |
3890 | - Now properly handles values including comments and lists. |
3891 | - |
3892 | - Better error handling. |
3893 | - |
3894 | - String interpolation. |
3895 | - |
3896 | - Some options implemented. |
3897 | - |
3898 | - You can pass a Section a dictionary to initialise it. |
3899 | - |
3900 | - Setting a Section member to a dictionary will create a Section instance. |
3901 | - |
3902 | - 2005/06/26 |
3903 | - ---------- |
3904 | - |
3905 | - Version 0.0.1 |
3906 | - |
3907 | - Experimental reader. |
3908 | - |
3909 | - A reasonably elegant implementation - a basic reader in 160 lines of code. |
3910 | - |
3911 | - *A programming language is a medium of expression.* - Paul Graham |
3912 | -""" |
3913 | |
3914 | === modified file 'landscape/lib/persist.py' |
3915 | --- landscape/lib/persist.py 2011-07-12 09:34:49 +0000 |
3916 | +++ landscape/lib/persist.py 2013-04-05 16:00:29 +0000 |
3917 | @@ -24,7 +24,7 @@ |
3918 | import re |
3919 | |
3920 | |
3921 | -__all__ = ["Persist", "PickleBackend", "BPickleBackend", "ConfigObjBackend", |
3922 | +__all__ = ["Persist", "PickleBackend", "BPickleBackend", |
3923 | "path_string_to_tuple", "path_tuple_to_string", "RootedPersist", |
3924 | "PersistError", "PersistReadOnlyError"] |
3925 | |
3926 | @@ -141,6 +141,9 @@ |
3927 | |
3928 | If None is specified, then the filename passed during construction will |
3929 | be used. |
3930 | + |
3931 | + If the destination file already exists, it will be renamed |
3932 | + to C{<filepath>.old}. |
3933 | """ |
3934 | if filepath is None: |
3935 | if self.filename is None: |
3936 | @@ -414,6 +417,21 @@ |
3937 | |
3938 | |
3939 | def path_string_to_tuple(path): |
3940 | + """Convert a L{Persist} path string to a path tuple. |
3941 | + |
3942 | + Examples: |
3943 | + |
3944 | + >>> path_string_to_tuple("ab") |
3945 | + ("ab",) |
3946 | + >>> path_string_to_tuple("ab.cd") |
3947 | + ("ab", "cd")) |
3948 | + >>> path_string_to_tuple("ab[0][1]") |
3949 | + ("ab", 0, 1) |
3950 | + >>> path_string_to_tuple("ab[0].cd[1]") |
3951 | + ("ab", 0, "cd", 1) |
3952 | + |
3953 | + Raises L{PersistError} if the given path string is invalid. |
3954 | + """ |
3955 | if "." not in path and "[" not in path: |
3956 | return (path,) |
3957 | result = [] |
3958 | @@ -441,6 +459,35 @@ |
3959 | |
3960 | |
3961 | class Backend(object): |
3962 | + """ |
3963 | + Base class for L{Persist} backends implementing hierarchical storage |
3964 | + functionality. |
3965 | + |
3966 | + Each node of the hierarchy is an object of type C{dict}, C{list} |
3967 | + or C{tuple}. A node can have zero or more children, each child can be |
3968 | + another node or a leaf value compatible with the backend's serialization |
3969 | + mechanism. |
3970 | + |
3971 | + Each child element is associated with a unique key, that can be used to |
3972 | + get, set or remove the child itself from its containing node. If the node |
3973 | + object is of type C{dict}, then the child keys will be the keys of the |
3974 | + dictionary, otherwise if the node object is of type C{list} or C{tuple} |
3975 | + the child element keys are the indexes of the available items, or the value |
3976 | + of items theselves. |
3977 | + |
3978 | + The root node object is always a C{dict}. |
3979 | + |
3980 | + For example: |
3981 | + |
3982 | + >>> root = backend.new() |
3983 | + >>> backend.set(root, "foo", "bar") |
3984 | + 'bar' |
3985 | + >>> egg = backend.set(root, "egg", [1, 2, 3]) |
3986 | + >>> backend.set(egg, 0, 10) |
3987 | + 10 |
3988 | + >>> root |
3989 | + {'foo': 'bar', 'egg': [10, 2, 3]} |
3990 | + """ |
3991 | |
3992 | def new(self): |
3993 | raise NotImplementedError |
3994 | @@ -452,6 +499,7 @@ |
3995 | raise NotImplementedError |
3996 | |
3997 | def get(self, obj, elem, _marker=NOTHING): |
3998 | + """Lookup a child in the given node object.""" |
3999 | if type(obj) is dict: |
4000 | newobj = obj.get(elem, _marker) |
4001 | elif type(obj) in (tuple, list): |
4002 | @@ -469,6 +517,7 @@ |
4003 | return newobj |
4004 | |
4005 | def set(self, obj, elem, value): |
4006 | + """Set the value of the given child in the given node object.""" |
4007 | if type(obj) is dict: |
4008 | newobj = obj[elem] = value |
4009 | elif type(obj) is list and type(elem) is int: |
4010 | @@ -485,6 +534,12 @@ |
4011 | return newobj |
4012 | |
4013 | def remove(self, obj, elem, isvalue): |
4014 | + """Remove a the given child in the given node object. |
4015 | + |
4016 | + @param isvalue: In case the node object is a C{list}, a boolean |
4017 | + indicating if C{elem} is the index of the child or the value |
4018 | + of the child itself. |
4019 | + """ |
4020 | result = False |
4021 | if type(obj) is dict: |
4022 | if elem in obj: |
4023 | @@ -505,20 +560,24 @@ |
4024 | return result |
4025 | |
4026 | def copy(self, value): |
4027 | + """Copy a node or a value.""" |
4028 | if type(value) in (dict, list): |
4029 | return copy.deepcopy(value) |
4030 | return value |
4031 | |
4032 | def empty(self, obj): |
4033 | + """Whether the given node object has no children.""" |
4034 | return (not obj) |
4035 | |
4036 | def has(self, obj, elem): |
4037 | + """Whether the given node object contains the given child element.""" |
4038 | contains = getattr(obj, "__contains__", None) |
4039 | if contains: |
4040 | return contains(elem) |
4041 | return NotImplemented |
4042 | |
4043 | def keys(self, obj): |
4044 | + """Return the keys of the child elements of the given node object.""" |
4045 | keys = getattr(obj, "keys", None) |
4046 | if keys: |
4047 | return keys() |
4048 | @@ -574,49 +633,4 @@ |
4049 | finally: |
4050 | file.close() |
4051 | |
4052 | - |
4053 | -class ConfigObjBackend(Backend): |
4054 | - |
4055 | - def __init__(self): |
4056 | - from landscape.lib import configobj |
4057 | - self.ConfigObj = configobj.ConfigObj |
4058 | - self.Section = configobj.Section |
4059 | - |
4060 | - def new(self): |
4061 | - return self.ConfigObj(unrepr=True) |
4062 | - |
4063 | - def load(self, filepath): |
4064 | - return self.ConfigObj(filepath, unrepr=True) |
4065 | - |
4066 | - def save(self, filepath, map): |
4067 | - file = open(filepath, "w") |
4068 | - try: |
4069 | - map.write(file) |
4070 | - finally: |
4071 | - file.close() |
4072 | - |
4073 | - def get(self, obj, elem, _marker=NOTHING): |
4074 | - if isinstance(obj, self.Section): |
4075 | - return obj.get(elem, _marker) |
4076 | - return Backend.get(self, obj, elem) |
4077 | - |
4078 | - def set(self, obj, elem, value): |
4079 | - if isinstance(obj, self.Section): |
4080 | - obj[elem] = value |
4081 | - return obj[elem] |
4082 | - return Backend.set(self, obj, elem, value) |
4083 | - |
4084 | - def remove(self, obj, elem, isvalue): |
4085 | - if isinstance(obj, self.Section): |
4086 | - if elem in obj: |
4087 | - del obj[elem] |
4088 | - return True |
4089 | - return False |
4090 | - return Backend.remove(self, obj, elem, isvalue) |
4091 | - |
4092 | - def copy(self, value): |
4093 | - if isinstance(value, self.Section): |
4094 | - return value.dict() |
4095 | - return Backend.copy(self, value) |
4096 | - |
4097 | # vim:ts=4:sw=4:et |
4098 | |
4099 | === modified file 'landscape/lib/tests/test_persist.py' |
4100 | --- landscape/lib/tests/test_persist.py 2011-07-12 09:34:49 +0000 |
4101 | +++ landscape/lib/tests/test_persist.py 2013-04-05 16:00:29 +0000 |
4102 | @@ -3,7 +3,7 @@ |
4103 | |
4104 | from landscape.lib.persist import ( |
4105 | path_string_to_tuple, path_tuple_to_string, Persist, RootedPersist, |
4106 | - PickleBackend, ConfigObjBackend, PersistError, PersistReadOnlyError) |
4107 | + PickleBackend, PersistError, PersistReadOnlyError) |
4108 | from landscape.tests.helpers import LandscapeTest |
4109 | |
4110 | |
4111 | @@ -444,12 +444,6 @@ |
4112 | return Persist(PickleBackend(), *args, **kwargs) |
4113 | |
4114 | |
4115 | -class ConfigObjPersistTest(GeneralPersistTest, SaveLoadPersistTest): |
4116 | - |
4117 | - def build_persist(self, *args, **kwargs): |
4118 | - return Persist(ConfigObjBackend(), *args, **kwargs) |
4119 | - |
4120 | - |
4121 | class RootedPersistTest(GeneralPersistTest): |
4122 | |
4123 | def build_persist(self, *args, **kwargs): |
4124 | |
4125 | === removed file 'landscape/lib/twisted_amp.py' |
4126 | --- landscape/lib/twisted_amp.py 2010-06-08 07:59:32 +0000 |
4127 | +++ landscape/lib/twisted_amp.py 1970-01-01 00:00:00 +0000 |
4128 | @@ -1,2223 +0,0 @@ |
4129 | -# -*- test-case-name: twisted.test.test_amp -*- |
4130 | -# Copyright (c) 2005 Divmod, Inc. |
4131 | -# Copyright (c) 2007 Twisted Matrix Laboratories. |
4132 | -# See LICENSE for details. |
4133 | - |
4134 | -""" |
4135 | -This module implements AMP, the Asynchronous Messaging Protocol. |
4136 | - |
4137 | -AMP is a protocol for sending multiple asynchronous request/response pairs over |
4138 | -the same connection. Requests and responses are both collections of key/value |
4139 | -pairs. |
4140 | - |
4141 | -AMP is a very simple protocol which is not an application. This module is a |
4142 | -"protocol construction kit" of sorts; it attempts to be the simplest wire-level |
4143 | -implementation of Deferreds. AMP provides the following base-level features: |
4144 | - |
4145 | - - Asynchronous request/response handling (hence the name) |
4146 | - |
4147 | - - Requests and responses are both key/value pairs |
4148 | - |
4149 | - - Binary transfer of all data: all data is length-prefixed. Your |
4150 | - application will never need to worry about quoting. |
4151 | - |
4152 | - - Command dispatching (like HTTP Verbs): the protocol is extensible, and |
4153 | - multiple AMP sub-protocols can be grouped together easily. |
4154 | - |
4155 | -The protocol implementation also provides a few additional features which are |
4156 | -not part of the core wire protocol, but are nevertheless very useful: |
4157 | - |
4158 | - - Tight TLS integration, with an included StartTLS command. |
4159 | - |
4160 | - - Handshaking to other protocols: because AMP has well-defined message |
4161 | - boundaries and maintains all incoming and outgoing requests for you, you |
4162 | - can start a connection over AMP and then switch to another protocol. |
4163 | - This makes it ideal for firewall-traversal applications where you may |
4164 | - have only one forwarded port but multiple applications that want to use |
4165 | - it. |
4166 | - |
4167 | -Using AMP with Twisted is simple. Each message is a command, with a response. |
4168 | -You begin by defining a command type. Commands specify their input and output |
4169 | -in terms of the types that they expect to see in the request and response |
4170 | -key-value pairs. Here's an example of a command that adds two integers, 'a' |
4171 | -and 'b':: |
4172 | - |
4173 | - class Sum(amp.Command): |
4174 | - arguments = [('a', amp.Integer()), |
4175 | - ('b', amp.Integer())] |
4176 | - response = [('total', amp.Integer())] |
4177 | - |
4178 | -Once you have specified a command, you need to make it part of a protocol, and |
4179 | -define a responder for it. Here's a 'JustSum' protocol that includes a |
4180 | -responder for our 'Sum' command:: |
4181 | - |
4182 | - class JustSum(amp.AMP): |
4183 | - def sum(self, a, b): |
4184 | - total = a + b |
4185 | - print 'Did a sum: %d + %d = %d' % (a, b, total) |
4186 | - return {'total': total} |
4187 | - Sum.responder(sum) |
4188 | - |
4189 | -Later, when you want to actually do a sum, the following expression will return |
4190 | -a Deferred which will fire with the result:: |
4191 | - |
4192 | - ClientCreator(reactor, amp.AMP).connectTCP(...).addCallback( |
4193 | - lambda p: p.callRemote(Sum, a=13, b=81)).addCallback( |
4194 | - lambda result: result['total']) |
4195 | - |
4196 | -You can also define the propagation of specific errors in AMP. For example, |
4197 | -for the slightly more complicated case of division, we might have to deal with |
4198 | -division by zero:: |
4199 | - |
4200 | - class Divide(amp.Command): |
4201 | - arguments = [('numerator', amp.Integer()), |
4202 | - ('denominator', amp.Integer())] |
4203 | - response = [('result', amp.Float())] |
4204 | - errors = {ZeroDivisionError: 'ZERO_DIVISION'} |
4205 | - |
4206 | -The 'errors' mapping here tells AMP that if a responder to Divide emits a |
4207 | -L{ZeroDivisionError}, then the other side should be informed that an error of |
4208 | -the type 'ZERO_DIVISION' has occurred. Writing a responder which takes |
4209 | -advantage of this is very simple - just raise your exception normally:: |
4210 | - |
4211 | - class JustDivide(amp.AMP): |
4212 | - def divide(self, numerator, denominator): |
4213 | - result = numerator / denominator |
4214 | - print 'Divided: %d / %d = %d' % (numerator, denominator, total) |
4215 | - return {'result': result} |
4216 | - Divide.responder(divide) |
4217 | - |
4218 | -On the client side, the errors mapping will be used to determine what the |
4219 | -'ZERO_DIVISION' error means, and translated into an asynchronous exception, |
4220 | -which can be handled normally as any L{Deferred} would be:: |
4221 | - |
4222 | - def trapZero(result): |
4223 | - result.trap(ZeroDivisionError) |
4224 | - print "Divided by zero: returning INF" |
4225 | - return 1e1000 |
4226 | - ClientCreator(reactor, amp.AMP).connectTCP(...).addCallback( |
4227 | - lambda p: p.callRemote(Divide, numerator=1234, |
4228 | - denominator=0) |
4229 | - ).addErrback(trapZero) |
4230 | - |
4231 | -For a complete, runnable example of both of these commands, see the files in |
4232 | -the Twisted repository:: |
4233 | - |
4234 | - doc/core/examples/ampserver.py |
4235 | - doc/core/examples/ampclient.py |
4236 | - |
4237 | -On the wire, AMP is a protocol which uses 2-byte lengths to prefix keys and |
4238 | -values, and empty keys to separate messages:: |
4239 | - |
4240 | - <2-byte length><key><2-byte length><value> |
4241 | - <2-byte length><key><2-byte length><value> |
4242 | - ... |
4243 | - <2-byte length><key><2-byte length><value> |
4244 | - <NUL><NUL> # Empty Key == End of Message |
4245 | - |
4246 | -And so on. Because it's tedious to refer to lengths and NULs constantly, the |
4247 | -documentation will refer to packets as if they were newline delimited, like |
4248 | -so:: |
4249 | - |
4250 | - C: _command: sum |
4251 | - C: _ask: ef639e5c892ccb54 |
4252 | - C: a: 13 |
4253 | - C: b: 81 |
4254 | - |
4255 | - S: _answer: ef639e5c892ccb54 |
4256 | - S: total: 94 |
4257 | - |
4258 | -Notes: |
4259 | - |
4260 | -Values are limited to the maximum encodable size in a 16-bit length, 65535 |
4261 | -bytes. |
4262 | - |
4263 | -Keys are limited to the maximum encodable size in a 8-bit length, 255 bytes. |
4264 | -Note that we still use 2-byte lengths to encode keys. This small redundancy |
4265 | -has several features: |
4266 | - |
4267 | - - If an implementation becomes confused and starts emitting corrupt data, |
4268 | - or gets keys confused with values, many common errors will be |
4269 | - signalled immediately instead of delivering obviously corrupt packets. |
4270 | - |
4271 | - - A single NUL will separate every key, and a double NUL separates |
4272 | - messages. This provides some redundancy when debugging traffic dumps. |
4273 | - |
4274 | - - NULs will be present at regular intervals along the protocol, providing |
4275 | - some padding for otherwise braindead C implementations of the protocol, |
4276 | - so that <stdio.h> string functions will see the NUL and stop. |
4277 | - |
4278 | - - This makes it possible to run an AMP server on a port also used by a |
4279 | - plain-text protocol, and easily distinguish between non-AMP clients (like |
4280 | - web browsers) which issue non-NUL as the first byte, and AMP clients, |
4281 | - which always issue NUL as the first byte. |
4282 | - |
4283 | -""" |
4284 | - |
4285 | -__metaclass__ = type |
4286 | - |
4287 | -import types, warnings |
4288 | - |
4289 | -from cStringIO import StringIO |
4290 | -from struct import pack |
4291 | - |
4292 | -from zope.interface import Interface, implements |
4293 | - |
4294 | -from twisted.python.reflect import accumulateClassDict |
4295 | -from twisted.python.failure import Failure |
4296 | -from twisted.python import log, filepath |
4297 | - |
4298 | -from twisted.internet.main import CONNECTION_LOST |
4299 | -from twisted.internet.error import ConnectionLost |
4300 | -from twisted.internet.defer import Deferred, maybeDeferred, fail |
4301 | -from twisted.protocols.basic import Int16StringReceiver, StatefulStringProtocol |
4302 | - |
4303 | -#from twisted.internet._sslverify import problemsFromTransport |
4304 | - |
4305 | -# I'd like this to use the exposed public API, but for some reason, when it was |
4306 | -# moved, these names were not exposed by internet.ssl. |
4307 | - |
4308 | -#from twisted.internet.ssl import CertificateOptions, Certificate, DN, KeyPair |
4309 | - |
4310 | -ASK = '_ask' |
4311 | -ANSWER = '_answer' |
4312 | -COMMAND = '_command' |
4313 | -ERROR = '_error' |
4314 | -ERROR_CODE = '_error_code' |
4315 | -ERROR_DESCRIPTION = '_error_description' |
4316 | -UNKNOWN_ERROR_CODE = 'UNKNOWN' |
4317 | -UNHANDLED_ERROR_CODE = 'UNHANDLED' |
4318 | - |
4319 | -MAX_KEY_LENGTH = 0xff |
4320 | -MAX_VALUE_LENGTH = 0xffff |
4321 | - |
4322 | - |
4323 | -class IBoxSender(Interface): |
4324 | - """ |
4325 | - A transport which can send L{AmpBox} objects. |
4326 | - """ |
4327 | - |
4328 | - def sendBox(box): |
4329 | - """ |
4330 | - Send an L{AmpBox}. |
4331 | - |
4332 | - @raise ProtocolSwitched: if the underlying protocol has been |
4333 | - switched. |
4334 | - |
4335 | - @raise ConnectionLost: if the underlying connection has already been |
4336 | - lost. |
4337 | - """ |
4338 | - |
4339 | - def unhandledError(failure): |
4340 | - """ |
4341 | - An unhandled error occurred in response to a box. Log it |
4342 | - appropriately. |
4343 | - |
4344 | - @param failure: a L{Failure} describing the error that occurred. |
4345 | - """ |
4346 | - |
4347 | - |
4348 | - |
4349 | -class IBoxReceiver(Interface): |
4350 | - """ |
4351 | - An application object which can receive L{AmpBox} objects and dispatch them |
4352 | - appropriately. |
4353 | - """ |
4354 | - |
4355 | - def startReceivingBoxes(boxSender): |
4356 | - """ |
4357 | - The L{ampBoxReceived} method will start being called; boxes may be |
4358 | - responded to by responding to the given L{IBoxSender}. |
4359 | - |
4360 | - @param boxSender: an L{IBoxSender} provider. |
4361 | - """ |
4362 | - |
4363 | - |
4364 | - def ampBoxReceived(box): |
4365 | - """ |
4366 | - A box was received from the transport; dispatch it appropriately. |
4367 | - """ |
4368 | - |
4369 | - |
4370 | - def stopReceivingBoxes(reason): |
4371 | - """ |
4372 | - No further boxes will be received on this connection. |
4373 | - |
4374 | - @type reason: L{Failure} |
4375 | - """ |
4376 | - |
4377 | - |
4378 | - |
4379 | -class IResponderLocator(Interface): |
4380 | - """ |
4381 | - An application object which can look up appropriate responder methods for |
4382 | - AMP commands. |
4383 | - """ |
4384 | - |
4385 | - def locateResponder(self, name): |
4386 | - """ |
4387 | - Locate a responder method appropriate for the named command. |
4388 | - |
4389 | - @param name: the wire-level name (commandName) of the AMP command to be |
4390 | - responded to. |
4391 | - |
4392 | - @return: a 1-argument callable that takes an L{AmpBox} with argument |
4393 | - values for the given command, and returns an L{AmpBox} containing |
4394 | - argument values for the named command, or a L{Deferred} that fires the |
4395 | - same. |
4396 | - """ |
4397 | - |
4398 | - |
4399 | - |
4400 | -class AmpError(Exception): |
4401 | - """ |
4402 | - Base class of all Amp-related exceptions. |
4403 | - """ |
4404 | - |
4405 | - |
4406 | - |
4407 | -class ProtocolSwitched(Exception): |
4408 | - """ |
4409 | - Connections which have been switched to other protocols can no longer |
4410 | - accept traffic at the AMP level. This is raised when you try to send it. |
4411 | - """ |
4412 | - |
4413 | - |
4414 | - |
4415 | -class OnlyOneTLS(AmpError): |
4416 | - """ |
4417 | - This is an implementation limitation; TLS may only be started once per |
4418 | - connection. |
4419 | - """ |
4420 | - |
4421 | - |
4422 | - |
4423 | -class NoEmptyBoxes(AmpError): |
4424 | - """ |
4425 | - You can't have empty boxes on the connection. This is raised when you |
4426 | - receive or attempt to send one. |
4427 | - """ |
4428 | - |
4429 | - |
4430 | - |
4431 | -class InvalidSignature(AmpError): |
4432 | - """ |
4433 | - You didn't pass all the required arguments. |
4434 | - """ |
4435 | - |
4436 | - |
4437 | - |
4438 | -class TooLong(AmpError): |
4439 | - """ |
4440 | - One of the protocol's length limitations was violated. |
4441 | - |
4442 | - @ivar isKey: true if the string being encoded in a key position, false if |
4443 | - it was in a value position. |
4444 | - |
4445 | - @ivar isLocal: Was the string encoded locally, or received too long from |
4446 | - the network? (It's only physically possible to encode "too long" values on |
4447 | - the network for keys.) |
4448 | - |
4449 | - @ivar value: The string that was too long. |
4450 | - |
4451 | - @ivar keyName: If the string being encoded was in a value position, what |
4452 | - key was it being encoded for? |
4453 | - """ |
4454 | - |
4455 | - def __init__(self, isKey, isLocal, value, keyName=None): |
4456 | - AmpError.__init__(self) |
4457 | - self.isKey = isKey |
4458 | - self.isLocal = isLocal |
4459 | - self.value = value |
4460 | - self.keyName = keyName |
4461 | - |
4462 | - |
4463 | - def __repr__(self): |
4464 | - hdr = self.isKey and "key" or "value" |
4465 | - if not self.isKey: |
4466 | - hdr += ' ' + repr(self.keyName) |
4467 | - lcl = self.isLocal and "local" or "remote" |
4468 | - return "%s %s too long: %d" % (lcl, hdr, len(self.value)) |
4469 | - |
4470 | - |
4471 | - |
4472 | -class BadLocalReturn(AmpError): |
4473 | - """ |
4474 | - A bad value was returned from a local command; we were unable to coerce it. |
4475 | - """ |
4476 | - def __init__(self, message, enclosed): |
4477 | - AmpError.__init__(self) |
4478 | - self.message = message |
4479 | - self.enclosed = enclosed |
4480 | - |
4481 | - |
4482 | - def __repr__(self): |
4483 | - return self.message + " " + self.enclosed.getBriefTraceback() |
4484 | - |
4485 | - __str__ = __repr__ |
4486 | - |
4487 | - |
4488 | - |
4489 | -class RemoteAmpError(AmpError): |
4490 | - """ |
4491 | - This error indicates that something went wrong on the remote end of the |
4492 | - connection, and the error was serialized and transmitted to you. |
4493 | - """ |
4494 | - def __init__(self, errorCode, description, fatal=False, local=None): |
4495 | - """Create a remote error with an error code and description. |
4496 | - |
4497 | - @param errorCode: the AMP error code of this error. |
4498 | - |
4499 | - @param description: some text to show to the user. |
4500 | - |
4501 | - @param fatal: a boolean, true if this error should terminate the |
4502 | - connection. |
4503 | - |
4504 | - @param local: a local Failure, if one exists. |
4505 | - """ |
4506 | - if local: |
4507 | - localwhat = ' (local)' |
4508 | - othertb = local.getBriefTraceback() |
4509 | - else: |
4510 | - localwhat = '' |
4511 | - othertb = '' |
4512 | - Exception.__init__(self, "Code<%s>%s: %s%s" % ( |
4513 | - errorCode, localwhat, |
4514 | - description, othertb)) |
4515 | - self.local = local |
4516 | - self.errorCode = errorCode |
4517 | - self.description = description |
4518 | - self.fatal = fatal |
4519 | - |
4520 | - |
4521 | - |
4522 | -class UnknownRemoteError(RemoteAmpError): |
4523 | - """ |
4524 | - This means that an error whose type we can't identify was raised from the |
4525 | - other side. |
4526 | - """ |
4527 | - def __init__(self, description): |
4528 | - errorCode = UNKNOWN_ERROR_CODE |
4529 | - RemoteAmpError.__init__(self, errorCode, description) |
4530 | - |
4531 | - |
4532 | - |
4533 | -class MalformedAmpBox(AmpError): |
4534 | - """ |
4535 | - This error indicates that the wire-level protocol was malformed. |
4536 | - """ |
4537 | - |
4538 | - |
4539 | - |
4540 | -class UnhandledCommand(AmpError): |
4541 | - """ |
4542 | - A command received via amp could not be dispatched. |
4543 | - """ |
4544 | - |
4545 | - |
4546 | - |
4547 | -class IncompatibleVersions(AmpError): |
4548 | - """ |
4549 | - It was impossible to negotiate a compatible version of the protocol with |
4550 | - the other end of the connection. |
4551 | - """ |
4552 | - |
4553 | - |
4554 | -PROTOCOL_ERRORS = {UNHANDLED_ERROR_CODE: UnhandledCommand} |
4555 | - |
4556 | -class AmpBox(dict): |
4557 | - """ |
4558 | - I am a packet in the AMP protocol, much like a regular str:str dictionary. |
4559 | - """ |
4560 | - __slots__ = [] # be like a regular dictionary, don't magically |
4561 | - # acquire a __dict__... |
4562 | - |
4563 | - |
4564 | - def copy(self): |
4565 | - """ |
4566 | - Return another AmpBox just like me. |
4567 | - """ |
4568 | - newBox = self.__class__() |
4569 | - newBox.update(self) |
4570 | - return newBox |
4571 | - |
4572 | - |
4573 | - def serialize(self): |
4574 | - """ |
4575 | - Convert me into a wire-encoded string. |
4576 | - |
4577 | - @return: a str encoded according to the rules described in the module |
4578 | - docstring. |
4579 | - """ |
4580 | - i = self.items() |
4581 | - i.sort() |
4582 | - L = [] |
4583 | - w = L.append |
4584 | - for k, v in i: |
4585 | - if len(k) > MAX_KEY_LENGTH: |
4586 | - raise TooLong(True, True, k, None) |
4587 | - if len(v) > MAX_VALUE_LENGTH: |
4588 | - raise TooLong(False, True, v, k) |
4589 | - for kv in k, v: |
4590 | - w(pack("!H", len(kv))) |
4591 | - w(kv) |
4592 | - w(pack("!H", 0)) |
4593 | - return ''.join(L) |
4594 | - |
4595 | - |
4596 | - def _sendTo(self, proto): |
4597 | - """ |
4598 | - Serialize and send this box to a Amp instance. By the time it is being |
4599 | - sent, several keys are required. I must have exactly ONE of:: |
4600 | - |
4601 | - _ask |
4602 | - _answer |
4603 | - _error |
4604 | - |
4605 | - If the '_ask' key is set, then the '_command' key must also be |
4606 | - set. |
4607 | - |
4608 | - @param proto: an AMP instance. |
4609 | - """ |
4610 | - proto.sendBox(self) |
4611 | - |
4612 | - def __repr__(self): |
4613 | - return 'AmpBox(%s)' % (dict.__repr__(self),) |
4614 | - |
4615 | -# amp.Box => AmpBox |
4616 | - |
4617 | -Box = AmpBox |
4618 | - |
4619 | -class QuitBox(AmpBox): |
4620 | - """ |
4621 | - I am an AmpBox that, upon being sent, terminates the connection. |
4622 | - """ |
4623 | - __slots__ = [] |
4624 | - |
4625 | - |
4626 | - def __repr__(self): |
4627 | - return 'QuitBox(**%s)' % (super(QuitBox, self).__repr__(),) |
4628 | - |
4629 | - |
4630 | - def _sendTo(self, proto): |
4631 | - """ |
4632 | - Immediately call loseConnection after sending. |
4633 | - """ |
4634 | - super(QuitBox, self)._sendTo(proto) |
4635 | - proto.transport.loseConnection() |
4636 | - |
4637 | - |
4638 | - |
4639 | -class _SwitchBox(AmpBox): |
4640 | - """ |
4641 | - Implementation detail of ProtocolSwitchCommand: I am a AmpBox which sets |
4642 | - up state for the protocol to switch. |
4643 | - """ |
4644 | - |
4645 | - # DON'T set __slots__ here; we do have an attribute. |
4646 | - |
4647 | - def __init__(self, innerProto, **kw): |
4648 | - """ |
4649 | - Create a _SwitchBox with the protocol to switch to after being sent. |
4650 | - |
4651 | - @param innerProto: the protocol instance to switch to. |
4652 | - @type innerProto: an IProtocol provider. |
4653 | - """ |
4654 | - super(_SwitchBox, self).__init__(**kw) |
4655 | - self.innerProto = innerProto |
4656 | - |
4657 | - |
4658 | - def __repr__(self): |
4659 | - return '_SwitchBox(%r, **%s)' % (self.innerProto, |
4660 | - dict.__repr__(self),) |
4661 | - |
4662 | - |
4663 | - def _sendTo(self, proto): |
4664 | - """ |
4665 | - Send me; I am the last box on the connection. All further traffic will be |
4666 | - over the new protocol. |
4667 | - """ |
4668 | - super(_SwitchBox, self)._sendTo(proto) |
4669 | - proto._lockForSwitch() |
4670 | - proto._switchTo(self.innerProto) |
4671 | - |
4672 | - |
4673 | - |
4674 | -class BoxDispatcher: |
4675 | - """ |
4676 | - A L{BoxDispatcher} dispatches '_ask', '_answer', and '_error' L{AmpBox}es, |
4677 | - both incoming and outgoing, to their appropriate destinations. |
4678 | - |
4679 | - Outgoing commands are converted into L{Deferred}s and outgoing boxes, and |
4680 | - associated tracking state to fire those L{Deferred} when '_answer' boxes |
4681 | - come back. Incoming '_answer' and '_error' boxes are converted into |
4682 | - callbacks and errbacks on those L{Deferred}s, respectively. |
4683 | - |
4684 | - Incoming '_ask' boxes are converted into method calls on a supplied method |
4685 | - locator. |
4686 | - |
4687 | - @ivar _outstandingRequests: a dictionary mapping request IDs to |
4688 | - L{Deferred}s which were returned for those requests. |
4689 | - |
4690 | - @ivar locator: an object with a L{locateResponder} method that locates a |
4691 | - responder function that takes a Box and returns a result (either a Box or a |
4692 | - Deferred which fires one). |
4693 | - |
4694 | - @ivar boxSender: an object which can send boxes, via the L{_sendBox} |
4695 | - method, such as an L{AMP} instance. |
4696 | - @type boxSender: L{IBoxSender} |
4697 | - """ |
4698 | - |
4699 | - implements(IBoxReceiver) |
4700 | - |
4701 | - _failAllReason = None |
4702 | - _outstandingRequests = None |
4703 | - _counter = 0L |
4704 | - boxSender = None |
4705 | - |
4706 | - def __init__(self, locator): |
4707 | - self._outstandingRequests = {} |
4708 | - self.locator = locator |
4709 | - |
4710 | - |
4711 | - def startReceivingBoxes(self, boxSender): |
4712 | - """ |
4713 | - The given boxSender is going to start calling boxReceived on this |
4714 | - L{BoxDispatcher}. |
4715 | - |
4716 | - @param boxSender: The L{IBoxSender} to send command responses to. |
4717 | - """ |
4718 | - self.boxSender = boxSender |
4719 | - |
4720 | - |
4721 | - def stopReceivingBoxes(self, reason): |
4722 | - """ |
4723 | - No further boxes will be received here. Terminate all currently |
4724 | - oustanding command deferreds with the given reason. |
4725 | - """ |
4726 | - self.failAllOutgoing(reason) |
4727 | - |
4728 | - |
4729 | - def failAllOutgoing(self, reason): |
4730 | - """ |
4731 | - Call the errback on all outstanding requests awaiting responses. |
4732 | - |
4733 | - @param reason: the Failure instance to pass to those errbacks. |
4734 | - """ |
4735 | - self._failAllReason = reason |
4736 | - OR = self._outstandingRequests.items() |
4737 | - self._outstandingRequests = None # we can never send another request |
4738 | - for key, value in OR: |
4739 | - value.errback(reason) |
4740 | - |
4741 | - |
4742 | - def _nextTag(self): |
4743 | - """ |
4744 | - Generate protocol-local serial numbers for _ask keys. |
4745 | - |
4746 | - @return: a string that has not yet been used on this connection. |
4747 | - """ |
4748 | - self._counter += 1 |
4749 | - return '%x' % (self._counter,) |
4750 | - |
4751 | - |
4752 | - def _sendBoxCommand(self, command, box, requiresAnswer=True): |
4753 | - """ |
4754 | - Send a command across the wire with the given C{amp.Box}. |
4755 | - |
4756 | - Mutate the given box to give it any additional keys (_command, _ask) |
4757 | - required for the command and request/response machinery, then send it. |
4758 | - |
4759 | - If requiresAnswer is True, returns a C{Deferred} which fires when a |
4760 | - response is received. The C{Deferred} is fired with an C{amp.Box} on |
4761 | - success, or with an C{amp.RemoteAmpError} if an error is received. |
4762 | - |
4763 | - If the Deferred fails and the error is not handled by the caller of |
4764 | - this method, the failure will be logged and the connection dropped. |
4765 | - |
4766 | - @param command: a str, the name of the command to issue. |
4767 | - |
4768 | - @param box: an AmpBox with the arguments for the command. |
4769 | - |
4770 | - @param requiresAnswer: a boolean. Defaults to True. If True, return a |
4771 | - Deferred which will fire when the other side responds to this command. |
4772 | - If False, return None and do not ask the other side for acknowledgement. |
4773 | - |
4774 | - @return: a Deferred which fires the AmpBox that holds the response to |
4775 | - this command, or None, as specified by requiresAnswer. |
4776 | - |
4777 | - @raise ProtocolSwitched: if the protocol has been switched. |
4778 | - """ |
4779 | - if self._failAllReason is not None: |
4780 | - return fail(self._failAllReason) |
4781 | - box[COMMAND] = command |
4782 | - tag = self._nextTag() |
4783 | - if requiresAnswer: |
4784 | - box[ASK] = tag |
4785 | - box._sendTo(self.boxSender) |
4786 | - if requiresAnswer: |
4787 | - result = self._outstandingRequests[tag] = Deferred() |
4788 | - else: |
4789 | - result = None |
4790 | - return result |
4791 | - |
4792 | - |
4793 | - def callRemoteString(self, command, requiresAnswer=True, **kw): |
4794 | - """ |
4795 | - This is a low-level API, designed only for optimizing simple messages |
4796 | - for which the overhead of parsing is too great. |
4797 | - |
4798 | - @param command: a str naming the command. |
4799 | - |
4800 | - @param kw: arguments to the amp box. |
4801 | - |
4802 | - @param requiresAnswer: a boolean. Defaults to True. If True, return a |
4803 | - Deferred which will fire when the other side responds to this command. |
4804 | - If False, return None and do not ask the other side for acknowledgement. |
4805 | - |
4806 | - @return: a Deferred which fires the AmpBox that holds the response to |
4807 | - this command, or None, as specified by requiresAnswer. |
4808 | - """ |
4809 | - box = Box(kw) |
4810 | - return self._sendBoxCommand(command, box) |
4811 | - |
4812 | - |
4813 | - def callRemote(self, commandType, *a, **kw): |
4814 | - """ |
4815 | - This is the primary high-level API for sending messages via AMP. Invoke it |
4816 | - with a command and appropriate arguments to send a message to this |
4817 | - connection's peer. |
4818 | - |
4819 | - @param commandType: a subclass of Command. |
4820 | - @type commandType: L{type} |
4821 | - |
4822 | - @param a: Positional (special) parameters taken by the command. |
4823 | - Positional parameters will typically not be sent over the wire. The |
4824 | - only command included with AMP which uses positional parameters is |
4825 | - L{ProtocolSwitchCommand}, which takes the protocol that will be |
4826 | - switched to as its first argument. |
4827 | - |
4828 | - @param kw: Keyword arguments taken by the command. These are the |
4829 | - arguments declared in the command's 'arguments' attribute. They will |
4830 | - be encoded and sent to the peer as arguments for the L{commandType}. |
4831 | - |
4832 | - @return: If L{commandType} has a C{requiresAnswer} attribute set to |
4833 | - L{False}, then return L{None}. Otherwise, return a L{Deferred} which |
4834 | - fires with a dictionary of objects representing the result of this |
4835 | - call. Additionally, this L{Deferred} may fail with an exception |
4836 | - representing a connection failure, with L{UnknownRemoteError} if the |
4837 | - other end of the connection fails for an unknown reason, or with any |
4838 | - error specified as a key in L{commandType}'s C{errors} dictionary. |
4839 | - """ |
4840 | - |
4841 | - # XXX this takes command subclasses and not command objects on purpose. |
4842 | - # There's really no reason to have all this back-and-forth between |
4843 | - # command objects and the protocol, and the extra object being created |
4844 | - # (the Command instance) is pointless. Command is kind of like |
4845 | - # Interface, and should be more like it. |
4846 | - |
4847 | - # In other words, the fact that commandType is instantiated here is an |
4848 | - # implementation detail. Don't rely on it. |
4849 | - |
4850 | - co = commandType(*a, **kw) |
4851 | - return co._doCommand(self) |
4852 | - |
4853 | - |
4854 | - def unhandledError(self, failure): |
4855 | - """ |
4856 | - This is a terminal callback called after application code has had a |
4857 | - chance to quash any errors. |
4858 | - """ |
4859 | - return self.boxSender.unhandledError(failure) |
4860 | - |
4861 | - |
4862 | - def _answerReceived(self, box): |
4863 | - """ |
4864 | - An AMP box was received that answered a command previously sent with |
4865 | - L{callRemote}. |
4866 | - |
4867 | - @param box: an AmpBox with a value for its L{ANSWER} key. |
4868 | - """ |
4869 | - question = self._outstandingRequests.pop(box[ANSWER]) |
4870 | - question.addErrback(self.unhandledError) |
4871 | - question.callback(box) |
4872 | - |
4873 | - |
4874 | - def _errorReceived(self, box): |
4875 | - """ |
4876 | - An AMP box was received that answered a command previously sent with |
4877 | - L{callRemote}, with an error. |
4878 | - |
4879 | - @param box: an L{AmpBox} with a value for its L{ERROR}, L{ERROR_CODE}, |
4880 | - and L{ERROR_DESCRIPTION} keys. |
4881 | - """ |
4882 | - question = self._outstandingRequests.pop(box[ERROR]) |
4883 | - question.addErrback(self.unhandledError) |
4884 | - errorCode = box[ERROR_CODE] |
4885 | - description = box[ERROR_DESCRIPTION] |
4886 | - if errorCode in PROTOCOL_ERRORS: |
4887 | - exc = PROTOCOL_ERRORS[errorCode](errorCode, description) |
4888 | - else: |
4889 | - exc = RemoteAmpError(errorCode, description) |
4890 | - question.errback(Failure(exc)) |
4891 | - |
4892 | - |
4893 | - def _commandReceived(self, box): |
4894 | - """ |
4895 | - @param box: an L{AmpBox} with a value for its L{COMMAND} and L{ASK} |
4896 | - keys. |
4897 | - """ |
4898 | - cmd = box[COMMAND] |
4899 | - def formatAnswer(answerBox): |
4900 | - answerBox[ANSWER] = box[ASK] |
4901 | - return answerBox |
4902 | - def formatError(error): |
4903 | - if error.check(RemoteAmpError): |
4904 | - code = error.value.errorCode |
4905 | - desc = error.value.description |
4906 | - if error.value.fatal: |
4907 | - errorBox = QuitBox() |
4908 | - else: |
4909 | - errorBox = AmpBox() |
4910 | - else: |
4911 | - errorBox = QuitBox() |
4912 | - log.err(error) # here is where server-side logging happens |
4913 | - # if the error isn't handled |
4914 | - code = UNKNOWN_ERROR_CODE |
4915 | - desc = "Unknown Error" |
4916 | - errorBox[ERROR] = box[ASK] |
4917 | - errorBox[ERROR_DESCRIPTION] = desc |
4918 | - errorBox[ERROR_CODE] = code |
4919 | - return errorBox |
4920 | - deferred = self.dispatchCommand(box) |
4921 | - if ASK in box: |
4922 | - deferred.addCallbacks(formatAnswer, formatError) |
4923 | - deferred.addCallback(self._safeEmit) |
4924 | - deferred.addErrback(self.unhandledError) |
4925 | - |
4926 | - |
4927 | - def ampBoxReceived(self, box): |
4928 | - """ |
4929 | - An AmpBox was received, representing a command, or an answer to a |
4930 | - previously issued command (either successful or erroneous). Respond to |
4931 | - it according to its contents. |
4932 | - |
4933 | - @param box: an AmpBox |
4934 | - |
4935 | - @raise NoEmptyBoxes: when a box is received that does not contain an |
4936 | - '_answer', '_command' / '_ask', or '_error' key; i.e. one which does not |
4937 | - fit into the command / response protocol defined by AMP. |
4938 | - """ |
4939 | - if ANSWER in box: |
4940 | - self._answerReceived(box) |
4941 | - elif ERROR in box: |
4942 | - self._errorReceived(box) |
4943 | - elif COMMAND in box: |
4944 | - self._commandReceived(box) |
4945 | - else: |
4946 | - raise NoEmptyBoxes(box) |
4947 | - |
4948 | - |
4949 | - def _safeEmit(self, aBox): |
4950 | - """ |
4951 | - Emit a box, ignoring L{ProtocolSwitched} and L{ConnectionLost} errors |
4952 | - which cannot be usefully handled. |
4953 | - """ |
4954 | - try: |
4955 | - aBox._sendTo(self.boxSender) |
4956 | - except (ProtocolSwitched, ConnectionLost): |
4957 | - pass |
4958 | - |
4959 | - |
4960 | - def dispatchCommand(self, box): |
4961 | - """ |
4962 | - A box with a _command key was received. |
4963 | - |
4964 | - Dispatch it to a local handler call it. |
4965 | - |
4966 | - @param proto: an AMP instance. |
4967 | - @param box: an AmpBox to be dispatched. |
4968 | - """ |
4969 | - cmd = box[COMMAND] |
4970 | - responder = self.locator.locateResponder(cmd) |
4971 | - if responder is None: |
4972 | - return fail(RemoteAmpError( |
4973 | - UNHANDLED_ERROR_CODE, |
4974 | - "Unhandled Command: %r" % (cmd,), |
4975 | - False, |
4976 | - local=Failure(UnhandledCommand()))) |
4977 | - return maybeDeferred(responder, box) |
4978 | - |
4979 | - |
4980 | - |
4981 | -class CommandLocator: |
4982 | - """ |
4983 | - A L{CommandLocator} is a collection of responders to AMP L{Command}s, with |
4984 | - the help of the L{Command.responder} decorator. |
4985 | - """ |
4986 | - |
4987 | - class __metaclass__(type): |
4988 | - """ |
4989 | - This metaclass keeps track of all of the Command.responder-decorated |
4990 | - methods defined since the last CommandLocator subclass was defined. It |
4991 | - assumes (usually correctly, but unfortunately not necessarily so) that |
4992 | - those commands responders were all declared as methods of the class |
4993 | - being defined. Note that this list can be incorrect if users use the |
4994 | - Command.responder decorator outside the context of a CommandLocator |
4995 | - class declaration. |
4996 | - |
4997 | - The Command.responder decorator explicitly cooperates with this |
4998 | - metaclass. |
4999 | - """ |
5000 | - |
+1, nice cleanup
Thanks for adding documentation!