Merge lp:~free.ekanayaka/landscape-client/drop-configobjd-and-amp into lp:~landscape/landscape-client/trunk

Proposed by Free Ekanayaka
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
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.lib.configobj and landscape.lib.persist.ConfigObjectBackend,
  as they are not used

- drops landscape.lib.twisted_amp, which was needed only for Dapper

- improves the module docstring of landscape.broker.store, describing more
  in detail the logic behind "sequence" and "pending offset" when dealing
  with message exchanges

- improves docstrings in landscape.lib.persist

Description of the change

This branch is quite large but only have trivial changes:

- drops landscape.lib.configobj and landscape.lib.persist.ConfigObjectBackend, as they are not used
- drops landscape.lib.twisted_amp, which was needed only for Dapper
- improves the module docstring of landscape.broker.store, describing more in detail the logic behind "sequence" and "pending offset" when dealing with message exchanges
- improves docstrings in landscape.lib.persist

To post a comment you must log in.
Revision history for this message
Alberto Donato (ack) wrote :

+1, nice cleanup

Thanks for adding documentation!

review: Approve
Revision history for this message
Björn Tillenius (bjornt) wrote :

+1!

review: Approve

Preview Diff

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

Subscribers

People subscribed via source and target branches

to all changes: