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

Subscribers

People subscribed via source and target branches

to all changes: