Merge lp:~barry/lazr.config/lp1096512 into lp:lazr.config

Proposed by Barry Warsaw
Status: Merged
Merged at revision: 6
Proposed branch: lp:~barry/lazr.config/lp1096512
Merge into: lp:lazr.config
Diff against target: 3559 lines (+1390/-1165)
21 files modified
_bootstrap/COPYRIGHT.txt (+0/-9)
_bootstrap/LICENSE.txt (+0/-54)
_bootstrap/bootstrap.py (+0/-77)
buildout.cfg (+0/-31)
distribute_setup.py (+546/-0)
ez_setup.py (+0/-241)
lazr/__init__.py (+12/-8)
lazr/config/__init__.py (+2/-1)
lazr/config/_config.py (+45/-46)
lazr/config/docs/NEWS.rst (+17/-0)
lazr/config/docs/fixture.py (+34/-0)
lazr/config/docs/usage.rst (+475/-590)
lazr/config/docs/usage_fixture.py (+27/-0)
lazr/config/interfaces.py (+3/-6)
lazr/config/tests/__init__.py (+0/-17)
lazr/config/tests/test_config.py (+205/-0)
lazr/config/version.txt (+1/-1)
setup.cfg (+9/-0)
setup.py (+14/-18)
src/lazr/config/NEWS.txt (+0/-15)
src/lazr/config/tests/test_docs.py (+0/-51)
To merge this branch: bzr merge lp:~barry/lazr.config/lp1096512
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Gary Poster Pending
Gavin Panella Pending
LAZR Developers Pending
Review via email: mp+142715@code.launchpad.net

Description of the change

Another port to Python 3. This one is just like the ports for lazr.smtptest and lazr.delegates. In fact, this depends on the branch for LP: #1096513 which Gavin has approved, but I haven't yet landed and released.

To test this, you'll need to create a virtualenv and install the branch for LP: #1096513 into it. Then you should be able to run the nosetests.

Just like the other two packages, I might have some post-porting fixups for the Sphinx documentation, but I'll clean all that up once this branch lands and I'm ready to do a PyPI release.

(Side note: not all of usage.rst could be retained due to printable repr differences between Python 2 and 3. Where necessary, such tests are moved to unittests, but coverage is still 99%-100% depending on the Python version used.)

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you for this update. I think this is fine to land, but I think there are changes in behaviour that deserve documentation:
   1. decode('ascii', 'strict'): appear to mean that someone upgrading might see more unicode errors raised.
   2. sorted category names: is nice to have, but different from the past.

review: Approve (code)
Revision history for this message
Barry Warsaw (barry) wrote :

On Jan 10, 2013, at 07:17 PM, Curtis Hovey wrote:

> 1. decode('ascii', 'strict'): appear to mean that someone upgrading might
> see more unicode errors raised.

Interestingly, the documentation says that only ASCII is acceptable but the
use of 'ignore' didn't enforce that, at least AFAICT.

> 2. sorted category names: is nice to have, but different from the past.

True. I thought about sorting in the test instead (you have to sort one place
or the other for doctest reproducibility). I mildly preferred to guarantee it
in the API.

Thanks for the review. I'll make sure the NEWS file and the release
announcement properly document these issues.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== renamed file 'README.txt' => 'README.rst'
=== removed directory '_bootstrap'
=== removed file '_bootstrap/COPYRIGHT.txt'
--- _bootstrap/COPYRIGHT.txt 2009-03-24 17:31:47 +0000
+++ _bootstrap/COPYRIGHT.txt 1970-01-01 00:00:00 +0000
@@ -1,9 +0,0 @@
1Copyright (c) 2004-2009 Zope Corporation and Contributors.
2All Rights Reserved.
3
4This software is subject to the provisions of the Zope Public License,
5Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
6THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
7WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
8WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
9FOR A PARTICULAR PURPOSE.
100
=== removed file '_bootstrap/LICENSE.txt'
--- _bootstrap/LICENSE.txt 2009-03-24 17:31:47 +0000
+++ _bootstrap/LICENSE.txt 1970-01-01 00:00:00 +0000
@@ -1,54 +0,0 @@
1Zope Public License (ZPL) Version 2.1
2-------------------------------------
3
4A copyright notice accompanies this license document that
5identifies the copyright holders.
6
7This license has been certified as open source. It has also
8been designated as GPL compatible by the Free Software
9Foundation (FSF).
10
11Redistribution and use in source and binary forms, with or
12without modification, are permitted provided that the
13following conditions are met:
14
151. Redistributions in source code must retain the
16 accompanying copyright notice, this list of conditions,
17 and the following disclaimer.
18
192. Redistributions in binary form must reproduce the accompanying
20 copyright notice, this list of conditions, and the
21 following disclaimer in the documentation and/or other
22 materials provided with the distribution.
23
243. Names of the copyright holders must not be used to
25 endorse or promote products derived from this software
26 without prior written permission from the copyright
27 holders.
28
294. The right to distribute this software or to use it for
30 any purpose does not give you the right to use
31 Servicemarks (sm) or Trademarks (tm) of the copyright
32 holders. Use of them is covered by separate agreement
33 with the copyright holders.
34
355. If any files are modified, you must cause the modified
36 files to carry prominent notices stating that you changed
37 the files and the date of any change.
38
39Disclaimer
40
41 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS''
42 AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
43 NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
44 AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
45 NO EVENT SHALL THE COPYRIGHT HOLDERS BE
46 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
47 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
48 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
49 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
50 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
51 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
52 OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
53 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
54 DAMAGE.
55\ No newline at end of file0\ No newline at end of file
561
=== removed file '_bootstrap/bootstrap.py'
--- _bootstrap/bootstrap.py 2009-03-24 17:31:47 +0000
+++ _bootstrap/bootstrap.py 1970-01-01 00:00:00 +0000
@@ -1,77 +0,0 @@
1##############################################################################
2#
3# Copyright (c) 2006 Zope Corporation and Contributors.
4# All Rights Reserved.
5#
6# This software is subject to the provisions of the Zope Public License,
7# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
8# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11# FOR A PARTICULAR PURPOSE.
12#
13##############################################################################
14"""Bootstrap a buildout-based project
15
16Simply run this script in a directory containing a buildout.cfg.
17The script accepts buildout command-line options, so you can
18use the -c option to specify an alternate configuration file.
19
20$Id$
21"""
22
23import os, shutil, sys, tempfile, urllib2
24
25tmpeggs = tempfile.mkdtemp()
26
27is_jython = sys.platform.startswith('java')
28
29try:
30 import pkg_resources
31except ImportError:
32 ez = {}
33 exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
34 ).read() in ez
35 ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
36
37 import pkg_resources
38
39if sys.platform == 'win32':
40 def quote(c):
41 if ' ' in c:
42 return '"%s"' % c # work around spawn lamosity on windows
43 else:
44 return c
45else:
46 def quote (c):
47 return c
48
49cmd = 'from setuptools.command.easy_install import main; main()'
50ws = pkg_resources.working_set
51
52if is_jython:
53 import subprocess
54
55 assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd',
56 quote(tmpeggs), 'zc.buildout'],
57 env=dict(os.environ,
58 PYTHONPATH=
59 ws.find(pkg_resources.Requirement.parse('setuptools')).location
60 ),
61 ).wait() == 0
62
63else:
64 assert os.spawnle(
65 os.P_WAIT, sys.executable, quote (sys.executable),
66 '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout',
67 dict(os.environ,
68 PYTHONPATH=
69 ws.find(pkg_resources.Requirement.parse('setuptools')).location
70 ),
71 ) == 0
72
73ws.add_entry(tmpeggs)
74ws.require('zc.buildout')
75import zc.buildout.buildout
76zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap'])
77shutil.rmtree(tmpeggs)
780
=== removed symlink 'bootstrap.py'
=== target was u'_bootstrap/bootstrap.py'
=== removed file 'buildout.cfg'
--- buildout.cfg 2009-03-24 17:36:13 +0000
+++ buildout.cfg 1970-01-01 00:00:00 +0000
@@ -1,31 +0,0 @@
1[buildout]
2parts =
3 interpreter
4 test
5 docs
6 tags
7unzip = true
8
9develop = .
10
11[test]
12recipe = zc.recipe.testrunner
13eggs = lazr.config
14defaults = '--tests-pattern ^tests --exit-with-status --suite-name additional_tests'.split()
15
16[docs]
17recipe = z3c.recipe.sphinxdoc
18eggs = lazr.config [docs]
19index-doc = README
20default.css =
21layout.html =
22
23[interpreter]
24recipe = zc.recipe.egg
25interpreter=py
26eggs = lazr.config
27 docutils
28
29[tags]
30recipe = z3c.recipe.tag:tags
31eggs = lazr.config
320
=== added file 'distribute_setup.py'
--- distribute_setup.py 1970-01-01 00:00:00 +0000
+++ distribute_setup.py 2013-01-10 15:47:20 +0000
@@ -0,0 +1,546 @@
1#!python
2"""Bootstrap distribute installation
3
4If you want to use setuptools in your package's setup.py, just include this
5file in the same directory with it, and add this to the top of your setup.py::
6
7 from distribute_setup import use_setuptools
8 use_setuptools()
9
10If you want to require a specific version of setuptools, set a download
11mirror, or use an alternate download directory, you can do so by supplying
12the appropriate options to ``use_setuptools()``.
13
14This file can also be run as a script to install or upgrade setuptools.
15"""
16import os
17import shutil
18import sys
19import time
20import fnmatch
21import tempfile
22import tarfile
23import optparse
24
25from distutils import log
26
27try:
28 from site import USER_SITE
29except ImportError:
30 USER_SITE = None
31
32try:
33 import subprocess
34
35 def _python_cmd(*args):
36 args = (sys.executable,) + args
37 return subprocess.call(args) == 0
38
39except ImportError:
40 # will be used for python 2.3
41 def _python_cmd(*args):
42 args = (sys.executable,) + args
43 # quoting arguments if windows
44 if sys.platform == 'win32':
45 def quote(arg):
46 if ' ' in arg:
47 return '"%s"' % arg
48 return arg
49 args = [quote(arg) for arg in args]
50 return os.spawnl(os.P_WAIT, sys.executable, *args) == 0
51
52DEFAULT_VERSION = "0.6.34"
53DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/"
54SETUPTOOLS_FAKED_VERSION = "0.6c11"
55
56SETUPTOOLS_PKG_INFO = """\
57Metadata-Version: 1.0
58Name: setuptools
59Version: %s
60Summary: xxxx
61Home-page: xxx
62Author: xxx
63Author-email: xxx
64License: xxx
65Description: xxx
66""" % SETUPTOOLS_FAKED_VERSION
67
68
69def _install(tarball, install_args=()):
70 # extracting the tarball
71 tmpdir = tempfile.mkdtemp()
72 log.warn('Extracting in %s', tmpdir)
73 old_wd = os.getcwd()
74 try:
75 os.chdir(tmpdir)
76 tar = tarfile.open(tarball)
77 _extractall(tar)
78 tar.close()
79
80 # going in the directory
81 subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
82 os.chdir(subdir)
83 log.warn('Now working in %s', subdir)
84
85 # installing
86 log.warn('Installing Distribute')
87 if not _python_cmd('setup.py', 'install', *install_args):
88 log.warn('Something went wrong during the installation.')
89 log.warn('See the error message above.')
90 # exitcode will be 2
91 return 2
92 finally:
93 os.chdir(old_wd)
94 shutil.rmtree(tmpdir)
95
96
97def _build_egg(egg, tarball, to_dir):
98 # extracting the tarball
99 tmpdir = tempfile.mkdtemp()
100 log.warn('Extracting in %s', tmpdir)
101 old_wd = os.getcwd()
102 try:
103 os.chdir(tmpdir)
104 tar = tarfile.open(tarball)
105 _extractall(tar)
106 tar.close()
107
108 # going in the directory
109 subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
110 os.chdir(subdir)
111 log.warn('Now working in %s', subdir)
112
113 # building an egg
114 log.warn('Building a Distribute egg in %s', to_dir)
115 _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
116
117 finally:
118 os.chdir(old_wd)
119 shutil.rmtree(tmpdir)
120 # returning the result
121 log.warn(egg)
122 if not os.path.exists(egg):
123 raise IOError('Could not build the egg.')
124
125
126def _do_download(version, download_base, to_dir, download_delay):
127 egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg'
128 % (version, sys.version_info[0], sys.version_info[1]))
129 if not os.path.exists(egg):
130 tarball = download_setuptools(version, download_base,
131 to_dir, download_delay)
132 _build_egg(egg, tarball, to_dir)
133 sys.path.insert(0, egg)
134 import setuptools
135 setuptools.bootstrap_install_from = egg
136
137
138def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
139 to_dir=os.curdir, download_delay=15, no_fake=True):
140 # making sure we use the absolute path
141 to_dir = os.path.abspath(to_dir)
142 was_imported = 'pkg_resources' in sys.modules or \
143 'setuptools' in sys.modules
144 try:
145 try:
146 import pkg_resources
147 if not hasattr(pkg_resources, '_distribute'):
148 if not no_fake:
149 _fake_setuptools()
150 raise ImportError
151 except ImportError:
152 return _do_download(version, download_base, to_dir, download_delay)
153 try:
154 pkg_resources.require("distribute>=" + version)
155 return
156 except pkg_resources.VersionConflict:
157 e = sys.exc_info()[1]
158 if was_imported:
159 sys.stderr.write(
160 "The required version of distribute (>=%s) is not available,\n"
161 "and can't be installed while this script is running. Please\n"
162 "install a more recent version first, using\n"
163 "'easy_install -U distribute'."
164 "\n\n(Currently using %r)\n" % (version, e.args[0]))
165 sys.exit(2)
166 else:
167 del pkg_resources, sys.modules['pkg_resources'] # reload ok
168 return _do_download(version, download_base, to_dir,
169 download_delay)
170 except pkg_resources.DistributionNotFound:
171 return _do_download(version, download_base, to_dir,
172 download_delay)
173 finally:
174 if not no_fake:
175 _create_fake_setuptools_pkg_info(to_dir)
176
177
178def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
179 to_dir=os.curdir, delay=15):
180 """Download distribute from a specified location and return its filename
181
182 `version` should be a valid distribute version number that is available
183 as an egg for download under the `download_base` URL (which should end
184 with a '/'). `to_dir` is the directory where the egg will be downloaded.
185 `delay` is the number of seconds to pause before an actual download
186 attempt.
187 """
188 # making sure we use the absolute path
189 to_dir = os.path.abspath(to_dir)
190 try:
191 from urllib.request import urlopen
192 except ImportError:
193 from urllib2 import urlopen
194 tgz_name = "distribute-%s.tar.gz" % version
195 url = download_base + tgz_name
196 saveto = os.path.join(to_dir, tgz_name)
197 src = dst = None
198 if not os.path.exists(saveto): # Avoid repeated downloads
199 try:
200 log.warn("Downloading %s", url)
201 src = urlopen(url)
202 # Read/write all in one block, so we don't create a corrupt file
203 # if the download is interrupted.
204 data = src.read()
205 dst = open(saveto, "wb")
206 dst.write(data)
207 finally:
208 if src:
209 src.close()
210 if dst:
211 dst.close()
212 return os.path.realpath(saveto)
213
214
215def _no_sandbox(function):
216 def __no_sandbox(*args, **kw):
217 try:
218 from setuptools.sandbox import DirectorySandbox
219 if not hasattr(DirectorySandbox, '_old'):
220 def violation(*args):
221 pass
222 DirectorySandbox._old = DirectorySandbox._violation
223 DirectorySandbox._violation = violation
224 patched = True
225 else:
226 patched = False
227 except ImportError:
228 patched = False
229
230 try:
231 return function(*args, **kw)
232 finally:
233 if patched:
234 DirectorySandbox._violation = DirectorySandbox._old
235 del DirectorySandbox._old
236
237 return __no_sandbox
238
239
240def _patch_file(path, content):
241 """Will backup the file then patch it"""
242 f = open(path)
243 existing_content = f.read()
244 f.close()
245 if existing_content == content:
246 # already patched
247 log.warn('Already patched.')
248 return False
249 log.warn('Patching...')
250 _rename_path(path)
251 f = open(path, 'w')
252 try:
253 f.write(content)
254 finally:
255 f.close()
256 return True
257
258_patch_file = _no_sandbox(_patch_file)
259
260
261def _same_content(path, content):
262 f = open(path)
263 existing_content = f.read()
264 f.close()
265 return existing_content == content
266
267
268def _rename_path(path):
269 new_name = path + '.OLD.%s' % time.time()
270 log.warn('Renaming %s to %s', path, new_name)
271 os.rename(path, new_name)
272 return new_name
273
274
275def _remove_flat_installation(placeholder):
276 if not os.path.isdir(placeholder):
277 log.warn('Unkown installation at %s', placeholder)
278 return False
279 found = False
280 for file in os.listdir(placeholder):
281 if fnmatch.fnmatch(file, 'setuptools*.egg-info'):
282 found = True
283 break
284 if not found:
285 log.warn('Could not locate setuptools*.egg-info')
286 return
287
288 log.warn('Moving elements out of the way...')
289 pkg_info = os.path.join(placeholder, file)
290 if os.path.isdir(pkg_info):
291 patched = _patch_egg_dir(pkg_info)
292 else:
293 patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO)
294
295 if not patched:
296 log.warn('%s already patched.', pkg_info)
297 return False
298 # now let's move the files out of the way
299 for element in ('setuptools', 'pkg_resources.py', 'site.py'):
300 element = os.path.join(placeholder, element)
301 if os.path.exists(element):
302 _rename_path(element)
303 else:
304 log.warn('Could not find the %s element of the '
305 'Setuptools distribution', element)
306 return True
307
308_remove_flat_installation = _no_sandbox(_remove_flat_installation)
309
310
311def _after_install(dist):
312 log.warn('After install bootstrap.')
313 placeholder = dist.get_command_obj('install').install_purelib
314 _create_fake_setuptools_pkg_info(placeholder)
315
316
317def _create_fake_setuptools_pkg_info(placeholder):
318 if not placeholder or not os.path.exists(placeholder):
319 log.warn('Could not find the install location')
320 return
321 pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1])
322 setuptools_file = 'setuptools-%s-py%s.egg-info' % \
323 (SETUPTOOLS_FAKED_VERSION, pyver)
324 pkg_info = os.path.join(placeholder, setuptools_file)
325 if os.path.exists(pkg_info):
326 log.warn('%s already exists', pkg_info)
327 return
328
329 log.warn('Creating %s', pkg_info)
330 try:
331 f = open(pkg_info, 'w')
332 except EnvironmentError:
333 log.warn("Don't have permissions to write %s, skipping", pkg_info)
334 return
335 try:
336 f.write(SETUPTOOLS_PKG_INFO)
337 finally:
338 f.close()
339
340 pth_file = os.path.join(placeholder, 'setuptools.pth')
341 log.warn('Creating %s', pth_file)
342 f = open(pth_file, 'w')
343 try:
344 f.write(os.path.join(os.curdir, setuptools_file))
345 finally:
346 f.close()
347
348_create_fake_setuptools_pkg_info = _no_sandbox(
349 _create_fake_setuptools_pkg_info
350)
351
352
353def _patch_egg_dir(path):
354 # let's check if it's already patched
355 pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
356 if os.path.exists(pkg_info):
357 if _same_content(pkg_info, SETUPTOOLS_PKG_INFO):
358 log.warn('%s already patched.', pkg_info)
359 return False
360 _rename_path(path)
361 os.mkdir(path)
362 os.mkdir(os.path.join(path, 'EGG-INFO'))
363 pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
364 f = open(pkg_info, 'w')
365 try:
366 f.write(SETUPTOOLS_PKG_INFO)
367 finally:
368 f.close()
369 return True
370
371_patch_egg_dir = _no_sandbox(_patch_egg_dir)
372
373
374def _before_install():
375 log.warn('Before install bootstrap.')
376 _fake_setuptools()
377
378
379def _under_prefix(location):
380 if 'install' not in sys.argv:
381 return True
382 args = sys.argv[sys.argv.index('install') + 1:]
383 for index, arg in enumerate(args):
384 for option in ('--root', '--prefix'):
385 if arg.startswith('%s=' % option):
386 top_dir = arg.split('root=')[-1]
387 return location.startswith(top_dir)
388 elif arg == option:
389 if len(args) > index:
390 top_dir = args[index + 1]
391 return location.startswith(top_dir)
392 if arg == '--user' and USER_SITE is not None:
393 return location.startswith(USER_SITE)
394 return True
395
396
397def _fake_setuptools():
398 log.warn('Scanning installed packages')
399 try:
400 import pkg_resources
401 except ImportError:
402 # we're cool
403 log.warn('Setuptools or Distribute does not seem to be installed.')
404 return
405 ws = pkg_resources.working_set
406 try:
407 setuptools_dist = ws.find(
408 pkg_resources.Requirement.parse('setuptools', replacement=False)
409 )
410 except TypeError:
411 # old distribute API
412 setuptools_dist = ws.find(
413 pkg_resources.Requirement.parse('setuptools')
414 )
415
416 if setuptools_dist is None:
417 log.warn('No setuptools distribution found')
418 return
419 # detecting if it was already faked
420 setuptools_location = setuptools_dist.location
421 log.warn('Setuptools installation detected at %s', setuptools_location)
422
423 # if --root or --preix was provided, and if
424 # setuptools is not located in them, we don't patch it
425 if not _under_prefix(setuptools_location):
426 log.warn('Not patching, --root or --prefix is installing Distribute'
427 ' in another location')
428 return
429
430 # let's see if its an egg
431 if not setuptools_location.endswith('.egg'):
432 log.warn('Non-egg installation')
433 res = _remove_flat_installation(setuptools_location)
434 if not res:
435 return
436 else:
437 log.warn('Egg installation')
438 pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO')
439 if (os.path.exists(pkg_info) and
440 _same_content(pkg_info, SETUPTOOLS_PKG_INFO)):
441 log.warn('Already patched.')
442 return
443 log.warn('Patching...')
444 # let's create a fake egg replacing setuptools one
445 res = _patch_egg_dir(setuptools_location)
446 if not res:
447 return
448 log.warn('Patching complete.')
449 _relaunch()
450
451
452def _relaunch():
453 log.warn('Relaunching...')
454 # we have to relaunch the process
455 # pip marker to avoid a relaunch bug
456 _cmd1 = ['-c', 'install', '--single-version-externally-managed']
457 _cmd2 = ['-c', 'install', '--record']
458 if sys.argv[:3] == _cmd1 or sys.argv[:3] == _cmd2:
459 sys.argv[0] = 'setup.py'
460 args = [sys.executable] + sys.argv
461 sys.exit(subprocess.call(args))
462
463
464def _extractall(self, path=".", members=None):
465 """Extract all members from the archive to the current working
466 directory and set owner, modification time and permissions on
467 directories afterwards. `path' specifies a different directory
468 to extract to. `members' is optional and must be a subset of the
469 list returned by getmembers().
470 """
471 import copy
472 import operator
473 from tarfile import ExtractError
474 directories = []
475
476 if members is None:
477 members = self
478
479 for tarinfo in members:
480 if tarinfo.isdir():
481 # Extract directories with a safe mode.
482 directories.append(tarinfo)
483 tarinfo = copy.copy(tarinfo)
484 tarinfo.mode = 448 # decimal for oct 0700
485 self.extract(tarinfo, path)
486
487 # Reverse sort directories.
488 if sys.version_info < (2, 4):
489 def sorter(dir1, dir2):
490 return cmp(dir1.name, dir2.name)
491 directories.sort(sorter)
492 directories.reverse()
493 else:
494 directories.sort(key=operator.attrgetter('name'), reverse=True)
495
496 # Set correct owner, mtime and filemode on directories.
497 for tarinfo in directories:
498 dirpath = os.path.join(path, tarinfo.name)
499 try:
500 self.chown(tarinfo, dirpath)
501 self.utime(tarinfo, dirpath)
502 self.chmod(tarinfo, dirpath)
503 except ExtractError:
504 e = sys.exc_info()[1]
505 if self.errorlevel > 1:
506 raise
507 else:
508 self._dbg(1, "tarfile: %s" % e)
509
510
511def _build_install_args(options):
512 """
513 Build the arguments to 'python setup.py install' on the distribute package
514 """
515 install_args = []
516 if options.user_install:
517 if sys.version_info < (2, 6):
518 log.warn("--user requires Python 2.6 or later")
519 raise SystemExit(1)
520 install_args.append('--user')
521 return install_args
522
523def _parse_args():
524 """
525 Parse the command line for options
526 """
527 parser = optparse.OptionParser()
528 parser.add_option(
529 '--user', dest='user_install', action='store_true', default=False,
530 help='install in user site package (requires Python 2.6 or later)')
531 parser.add_option(
532 '--download-base', dest='download_base', metavar="URL",
533 default=DEFAULT_URL,
534 help='alternative URL from where to download the distribute package')
535 options, args = parser.parse_args()
536 # positional arguments are ignored
537 return options
538
539def main(version=DEFAULT_VERSION):
540 """Install or upgrade setuptools and EasyInstall"""
541 options = _parse_args()
542 tarball = download_setuptools(download_base=options.download_base)
543 return _install(tarball, _build_install_args(options))
544
545if __name__ == '__main__':
546 sys.exit(main())
0547
=== removed file 'ez_setup.py'
--- ez_setup.py 2009-03-24 17:31:47 +0000
+++ ez_setup.py 1970-01-01 00:00:00 +0000
@@ -1,241 +0,0 @@
1#!python
2"""Bootstrap setuptools installation
3
4If you want to use setuptools in your package's setup.py, just include this
5file in the same directory with it, and add this to the top of your setup.py::
6
7 from ez_setup import use_setuptools
8 use_setuptools()
9
10If you want to require a specific version of setuptools, set a download
11mirror, or use an alternate download directory, you can do so by supplying
12the appropriate options to ``use_setuptools()``.
13
14This file can also be run as a script to install or upgrade setuptools.
15"""
16import sys
17DEFAULT_VERSION = "0.6c8"
18DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
19
20md5_data = {
21 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
22 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
23 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
24 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
25 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
26 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
27 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
28 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
29 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
30 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
31 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
32 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
33 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
34 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
35 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
36 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
37 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
38 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
39 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
40 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
41 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
42 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
43 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
44 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
45 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
46 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
47 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
48 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
49 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
50 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
51}
52
53import sys, os
54
55def _validate_md5(egg_name, data):
56 if egg_name in md5_data:
57 from md5 import md5
58 digest = md5(data).hexdigest()
59 if digest != md5_data[egg_name]:
60 print >>sys.stderr, (
61 "md5 validation of %s failed! (Possible download problem?)"
62 % egg_name
63 )
64 sys.exit(2)
65 return data
66
67
68def use_setuptools(
69 version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
70 download_delay=15, min_version=None
71):
72 """Automatically find/download setuptools and make it available on sys.path
73
74 `version` should be a valid setuptools version number that is available
75 as an egg for download under the `download_base` URL (which should end with
76 a '/'). `to_dir` is the directory where setuptools will be downloaded, if
77 it is not already available. If `download_delay` is specified, it should
78 be the number of seconds that will be paused before initiating a download,
79 should one be required. If an older version of setuptools is installed,
80 this routine will print a message to ``sys.stderr`` and raise SystemExit in
81 an attempt to abort the calling script.
82 """
83 # Work around a hack in the ez_setup.py file from simplejson==1.7.3.
84 if min_version:
85 version = min_version
86
87 was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
88 def do_download():
89 egg = download_setuptools(version, download_base, to_dir, download_delay)
90 sys.path.insert(0, egg)
91 import setuptools; setuptools.bootstrap_install_from = egg
92 try:
93 import pkg_resources
94 except ImportError:
95 return do_download()
96 try:
97 pkg_resources.require("setuptools>="+version); return
98 except pkg_resources.VersionConflict, e:
99 if was_imported:
100 print >>sys.stderr, (
101 "The required version of setuptools (>=%s) is not available, and\n"
102 "can't be installed while this script is running. Please install\n"
103 " a more recent version first, using 'easy_install -U setuptools'."
104 "\n\n(Currently using %r)"
105 ) % (version, e.args[0])
106 sys.exit(2)
107 else:
108 del pkg_resources, sys.modules['pkg_resources'] # reload ok
109 return do_download()
110 except pkg_resources.DistributionNotFound:
111 return do_download()
112
113def download_setuptools(
114 version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
115 delay = 15
116):
117 """Download setuptools from a specified location and return its filename
118
119 `version` should be a valid setuptools version number that is available
120 as an egg for download under the `download_base` URL (which should end
121 with a '/'). `to_dir` is the directory where the egg will be downloaded.
122 `delay` is the number of seconds to pause before an actual download attempt.
123 """
124 import urllib2, shutil
125 egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
126 url = download_base + egg_name
127 saveto = os.path.join(to_dir, egg_name)
128 src = dst = None
129 if not os.path.exists(saveto): # Avoid repeated downloads
130 try:
131 from distutils import log
132 if delay:
133 log.warn("""
134---------------------------------------------------------------------------
135This script requires setuptools version %s to run (even to display
136help). I will attempt to download it for you (from
137%s), but
138you may need to enable firewall access for this script first.
139I will start the download in %d seconds.
140
141(Note: if this machine does not have network access, please obtain the file
142
143 %s
144
145and place it in this directory before rerunning this script.)
146---------------------------------------------------------------------------""",
147 version, download_base, delay, url
148 ); from time import sleep; sleep(delay)
149 log.warn("Downloading %s", url)
150 src = urllib2.urlopen(url)
151 # Read/write all in one block, so we don't create a corrupt file
152 # if the download is interrupted.
153 data = _validate_md5(egg_name, src.read())
154 dst = open(saveto,"wb"); dst.write(data)
155 finally:
156 if src: src.close()
157 if dst: dst.close()
158 return os.path.realpath(saveto)
159
160def main(argv, version=DEFAULT_VERSION):
161 """Install or upgrade setuptools and EasyInstall"""
162 try:
163 import setuptools
164 except ImportError:
165 egg = None
166 try:
167 egg = download_setuptools(version, delay=0)
168 sys.path.insert(0,egg)
169 from setuptools.command.easy_install import main
170 return main(list(argv)+[egg]) # we're done here
171 finally:
172 if egg and os.path.exists(egg):
173 os.unlink(egg)
174 else:
175 if setuptools.__version__ == '0.0.1':
176 print >>sys.stderr, (
177 "You have an obsolete version of setuptools installed. Please\n"
178 "remove it from your system entirely before rerunning this script."
179 )
180 sys.exit(2)
181
182 req = "setuptools>="+version
183 import pkg_resources
184 try:
185 pkg_resources.require(req)
186 except pkg_resources.VersionConflict:
187 try:
188 from setuptools.command.easy_install import main
189 except ImportError:
190 from easy_install import main
191 main(list(argv)+[download_setuptools(delay=0)])
192 sys.exit(0) # try to force an exit
193 else:
194 if argv:
195 from setuptools.command.easy_install import main
196 main(argv)
197 else:
198 print "Setuptools version",version,"or greater has been installed."
199 print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
200
201def update_md5(filenames):
202 """Update our built-in md5 registry"""
203
204 import re
205 from md5 import md5
206
207 for name in filenames:
208 base = os.path.basename(name)
209 f = open(name,'rb')
210 md5_data[base] = md5(f.read()).hexdigest()
211 f.close()
212
213 data = [" %r: %r,\n" % it for it in md5_data.items()]
214 data.sort()
215 repl = "".join(data)
216
217 import inspect
218 srcfile = inspect.getsourcefile(sys.modules[__name__])
219 f = open(srcfile, 'rb'); src = f.read(); f.close()
220
221 match = re.search("\nmd5_data = {\n([^}]+)}", src)
222 if not match:
223 print >>sys.stderr, "Internal error!"
224 sys.exit(2)
225
226 src = src[:match.start(1)] + repl + src[match.end(1):]
227 f = open(srcfile,'w')
228 f.write(src)
229 f.close()
230
231
232if __name__=='__main__':
233 if len(sys.argv)>2 and sys.argv[1]=='--md5update':
234 update_md5(sys.argv[2:])
235 else:
236 main(sys.argv[1:])
237
238
239
240
241
2420
=== renamed directory 'src/lazr' => 'lazr'
=== modified file 'lazr/__init__.py'
--- src/lazr/__init__.py 2009-03-24 17:31:47 +0000
+++ lazr/__init__.py 2013-01-10 15:47:20 +0000
@@ -1,4 +1,4 @@
1# Copyright 2008-2009 Canonical Ltd. All rights reserved.1# Copyright 2008-2013 Canonical Ltd. All rights reserved.
2#2#
3# This file is part of lazr.config.3# This file is part of lazr.config.
4#4#
@@ -14,10 +14,14 @@
14# You should have received a copy of the GNU Lesser General Public License14# You should have received a copy of the GNU Lesser General Public License
15# along with lazr.config. If not, see <http://www.gnu.org/licenses/>.15# along with lazr.config. If not, see <http://www.gnu.org/licenses/>.
1616
17# this is a namespace package17# This is a namespace package, however under >= Python 3.3, let it be a true
18try:18# namespace package (i.e. this cruft isn't necessary).
19 import pkg_resources19import sys
20 pkg_resources.declare_namespace(__name__)20
21except ImportError:21if sys.hexversion < 0x30300f0:
22 import pkgutil22 try:
23 __path__ = pkgutil.extend_path(__path__, __name__)23 import pkg_resources
24 pkg_resources.declare_namespace(__name__)
25 except ImportError:
26 import pkgutil
27 __path__ = pkgutil.extend_path(__path__, __name__)
2428
=== modified file 'lazr/config/__init__.py'
--- src/lazr/config/__init__.py 2009-08-25 13:46:10 +0000
+++ lazr/config/__init__.py 2013-01-10 15:47:20 +0000
@@ -17,7 +17,8 @@
17"""A configuration file system."""17"""A configuration file system."""
1818
19import pkg_resources19import pkg_resources
20__version__ = pkg_resources.resource_string("lazr.config", "version.txt").strip()20__version__ = pkg_resources.resource_string(
21 "lazr.config", "version.txt").strip()
2122
22# Re-export in such a way that __version__ can still be imported if23# Re-export in such a way that __version__ can still be imported if
23# dependencies are not yet available.24# dependencies are not yet available.
2425
=== modified file 'lazr/config/_config.py'
--- src/lazr/config/_config.py 2009-03-24 17:31:47 +0000
+++ lazr/config/_config.py 2013-01-10 15:47:20 +0000
@@ -1,4 +1,4 @@
1# Copyright 2008-2009 Canonical Ltd. All rights reserved.1# Copyright 2008-2013 Canonical Ltd. All rights reserved.
2#2#
3# This file is part of lazr.config.3# This file is part of lazr.config.
4#4#
@@ -16,8 +16,9 @@
1616
17"""Implementation classes for config."""17"""Implementation classes for config."""
1818
19from __future__ import absolute_import, print_function, unicode_literals
20
19__metaclass__ = type21__metaclass__ = type
20
21__all__ = [22__all__ = [
22 'Config',23 'Config',
23 'ConfigData',24 'ConfigData',
@@ -34,7 +35,6 @@
34 ]35 ]
3536
3637
37import StringIO
38import datetime38import datetime
39import grp39import grp
40import logging40import logging
@@ -42,35 +42,39 @@
42import pwd42import pwd
43import re43import re
4444
45from ConfigParser import NoSectionError, RawConfigParser
46from os.path import abspath, basename, dirname45from os.path import abspath, basename, dirname
47from textwrap import dedent46from textwrap import dedent
4847
49from zope.interface import implements48try:
49 from io import StringIO
50 from configparser import NoSectionError, RawConfigParser
51except ImportError:
52 # Python 2.
53 from StringIO import StringIO
54 from ConfigParser import NoSectionError, RawConfigParser
55
56
57from zope.interface import implementer
5058
51from lazr.config.interfaces import (59from lazr.config.interfaces import (
52 ConfigErrors, ICategory, IConfigData, IConfigLoader, IConfigSchema,60 ConfigErrors, ICategory, IConfigData, IConfigLoader, IConfigSchema,
53 InvalidSectionNameError, ISection, ISectionSchema, IStackableConfig,61 InvalidSectionNameError, ISection, ISectionSchema, IStackableConfig,
54 NoCategoryError, NoConfigError, RedefinedSectionError, UnknownKeyError,62 NoCategoryError, NoConfigError, RedefinedSectionError, UnknownKeyError,
55 UnknownSectionError)63 UnknownSectionError)
56from lazr.delegates import delegates64from lazr.delegates import delegate_to
5765
58_missing = object()66_missing = object()
5967
6068
61def read_content(filename):69def read_content(filename):
62 """Return the content of a file at filename as a string."""70 """Return the content of a file at filename as a string."""
63 source_file = open(filename, 'r')71 with open(filename, 'rt') as fp:
64 try:72 return fp.read()
65 raw_data = source_file.read()73
66 finally:74
67 source_file.close()75@implementer(ISectionSchema)
68 return raw_data
69
70
71class SectionSchema:76class SectionSchema:
72 """See `ISectionSchema`."""77 """See `ISectionSchema`."""
73 implements(ISectionSchema)
7478
75 def __init__(self, name, options, is_optional=False, is_master=False):79 def __init__(self, name, options, is_optional=False, is_master=False):
76 """Create an `ISectionSchema` from the name and options.80 """Create an `ISectionSchema` from the name and options.
@@ -89,7 +93,8 @@
8993
90 def __iter__(self):94 def __iter__(self):
91 """See `ISectionSchema`"""95 """See `ISectionSchema`"""
92 return self._options.iterkeys()96 for key in self._options.keys():
97 yield key
9398
94 def __contains__(self, name):99 def __contains__(self, name):
95 """See `ISectionSchema`"""100 """See `ISectionSchema`"""
@@ -113,12 +118,11 @@
113 self.optional, self.master)118 self.optional, self.master)
114119
115120
121@delegate_to(ISectionSchema, context='schema')
122@implementer(ISection)
116class Section:123class Section:
117 """See `ISection`."""124 """See `ISection`."""
118125
119 implements(ISection)
120 delegates(ISectionSchema, context='schema')
121
122 def __init__(self, schema, _options=None):126 def __init__(self, schema, _options=None):
123 """Create an `ISection` from schema.127 """Create an `ISection` from schema.
124128
@@ -223,9 +227,9 @@
223 return self._convert(value)227 return self._convert(value)
224228
225229
230@implementer(IConfigSchema, IConfigLoader)
226class ConfigSchema:231class ConfigSchema:
227 """See `IConfigSchema`."""232 """See `IConfigSchema`."""
228 implements(IConfigSchema, IConfigLoader)
229233
230 _section_factory = Section234 _section_factory = Section
231235
@@ -267,7 +271,7 @@
267 """271 """
268 raw_schema = read_content(filename)272 raw_schema = read_content(filename)
269 # Verify that the string is ascii.273 # Verify that the string is ascii.
270 raw_schema.encode('ascii', 'ignore')274 raw_schema.encode('ascii', 'strict')
271 # Verify that no sections are redefined.275 # Verify that no sections are redefined.
272 section_names = []276 section_names = []
273 for section_name in re.findall(r'^\s*\[[^\]]+\]', raw_schema, re.M):277 for section_name in re.findall(r'^\s*\[[^\]]+\]', raw_schema, re.M):
@@ -275,7 +279,7 @@
275 raise RedefinedSectionError(section_name)279 raise RedefinedSectionError(section_name)
276 else:280 else:
277 section_names.append(section_name)281 section_names.append(section_name)
278 return StringIO.StringIO(raw_schema)282 return StringIO(raw_schema)
279283
280 def _setSectionSchemasAndCategoryNames(self, parser):284 def _setSectionSchemasAndCategoryNames(self, parser):
281 """Set the SectionSchemas and category_names from the config."""285 """Set the SectionSchemas and category_names from the config."""
@@ -301,7 +305,7 @@
301 section_name, options, is_optional, is_master)305 section_name, options, is_optional, is_master)
302 if category_name is not None:306 if category_name is not None:
303 category_names.add(category_name)307 category_names.add(category_name)
304 self._category_names = list(category_names)308 self._category_names = sorted(category_names)
305309
306 _section_name_pattern = re.compile(r'\w[\w.-]+\w')310 _section_name_pattern = re.compile(r'\w[\w.-]+\w')
307311
@@ -355,7 +359,8 @@
355359
356 def __iter__(self):360 def __iter__(self):
357 """See `IConfigSchema`."""361 """See `IConfigSchema`."""
358 return self._section_schemas.itervalues()362 for value in self._section_schemas.values():
363 yield value
359364
360 def __contains__(self, name):365 def __contains__(self, name):
361 """See `IConfigSchema`."""366 """See `IConfigSchema`."""
@@ -423,9 +428,9 @@
423 _section_factory = ImplicitTypeSection428 _section_factory = ImplicitTypeSection
424429
425430
431@implementer(IConfigData)
426class ConfigData:432class ConfigData:
427 """See `IConfigData`."""433 """See `IConfigData`."""
428 implements(IConfigData)
429434
430 def __init__(self, filename, sections, extends=None, errors=None):435 def __init__(self, filename, sections, extends=None, errors=None):
431 """Set the configuration data."""436 """Set the configuration data."""
@@ -456,7 +461,8 @@
456461
457 def __iter__(self):462 def __iter__(self):
458 """See `IConfigData`."""463 """See `IConfigData`."""
459 return self._sections.itervalues()464 for value in self._sections.values():
465 yield value
460466
461 def __contains__(self, name):467 def __contains__(self, name):
462 """See `IConfigData`."""468 """See `IConfigData`."""
@@ -484,12 +490,12 @@
484 return sections490 return sections
485491
486492
493@delegate_to(IConfigData, context='data')
494@implementer(IStackableConfig)
487class Config:495class Config:
488 """See `IStackableConfig`."""496 """See `IStackableConfig`."""
489 # LAZR config classes may access ConfigData private data.497 # LAZR config classes may access ConfigData private data.
490 # pylint: disable-msg=W0212498 # pylint: disable-msg=W0212
491 implements(IStackableConfig)
492 delegates(IConfigData, context='data')
493499
494 def __init__(self, schema):500 def __init__(self, schema):
495 """Set the schema and configuration."""501 """Set the schema and configuration."""
@@ -567,7 +573,7 @@
567 confs = []573 confs = []
568 encoding_errors = self._verifyEncoding(conf_data)574 encoding_errors = self._verifyEncoding(conf_data)
569 parser = RawConfigParser()575 parser = RawConfigParser()
570 parser.readfp(StringIO.StringIO(conf_data), conf_filename)576 parser.readfp(StringIO(conf_data), conf_filename)
571 confs.append((conf_filename, parser, encoding_errors))577 confs.append((conf_filename, parser, encoding_errors))
572 if parser.has_option('meta', 'extends'):578 if parser.has_option('meta', 'extends'):
573 base_path = dirname(conf_filename)579 base_path = dirname(conf_filename)
@@ -660,8 +666,11 @@
660 """666 """
661 errors = []667 errors = []
662 try:668 try:
663 config_data.encode('ascii', 'ignore')669 if isinstance(config_data, bytes):
664 except UnicodeDecodeError, error:670 config_data.decode('ascii', 'strict')
671 else:
672 config_data.encode('ascii', 'strict')
673 except UnicodeError as error:
665 errors.append(error)674 errors.append(error)
666 return errors675 return errors
667676
@@ -706,9 +715,9 @@
706 raise NoConfigError('No config with name: %s.' % conf_name)715 raise NoConfigError('No config with name: %s.' % conf_name)
707716
708717
718@implementer(ICategory)
709class Category:719class Category:
710 """See `ICategory`."""720 """See `ICategory`."""
711 implements(ICategory)
712721
713 def __init__(self, name, sections):722 def __init__(self, name, sections):
714 """Initialize the Category its name and a list of sections."""723 """Initialize the Category its name and a list of sections."""
@@ -786,12 +795,8 @@
786 return user, group795 return user, group
787796
788797
789def _sort_order(a, b):798def _sortkey(item):
790 """Sort timedelta suffixes from greatest to least."""799 """Return a value that sorted(..., key=_sortkey) can use."""
791 if len(a) == 0:
792 return -1
793 if len(b) == 0:
794 return 1
795 order = dict(800 order = dict(
796 w=0, # weeks801 w=0, # weeks
797 d=1, # days802 d=1, # days
@@ -799,20 +804,14 @@
799 m=3, # minutes804 m=3, # minutes
800 s=4, # seconds805 s=4, # seconds
801 )806 )
802 suffix_a = order.get(a[-1])807 return order.get(item[-1])
803 suffix_b = order.get(b[-1])
804 if suffix_a is None or suffix_b is None:
805 raise ValueError
806 return cmp(suffix_a, suffix_b)
807
808808
809def as_timedelta(value):809def as_timedelta(value):
810 """Convert a value string to the equivalent timedeta."""810 """Convert a value string to the equivalent timedeta."""
811 # Technically, the regex will match multiple decimal points in the811 # Technically, the regex will match multiple decimal points in the
812 # left-hand side, but that's okay because the float/int conversion below812 # left-hand side, but that's okay because the float/int conversion below
813 # will properly complain if there's more than one dot.813 # will properly complain if there's more than one dot.
814 components = sorted(re.findall(r'([\d.]+[smhdw])', value),814 components = sorted(re.findall(r'([\d.]+[smhdw])', value), key=_sortkey)
815 cmp=_sort_order)
816 # Complain if the components are out of order.815 # Complain if the components are out of order.
817 if ''.join(components) != value:816 if ''.join(components) != value:
818 raise ValueError817 raise ValueError
819818
=== added directory 'lazr/config/docs'
=== renamed file 'src/lazr/config/CHANGES.txt' => 'lazr/config/docs/NEWS.rst'
--- src/lazr/config/CHANGES.txt 2009-03-24 17:31:47 +0000
+++ lazr/config/docs/NEWS.rst 2013-01-10 15:47:20 +0000
@@ -2,6 +2,23 @@
2Changes2Changes
3=======3=======
44
5
62.0 (2013-01-06)
7================
8- Ported to Python 3.
9
10
111.1.3 (2009-08-25)
12==================
13
14- Fixed a build problem.
15
161.1.2 (2009-08-25)
17==================
18
19- Got rid of a sys.path hack.
20
21
51.1.1 (2009-03-24)221.1.1 (2009-03-24)
6==================23==================
724
825
=== added file 'lazr/config/docs/__init__.py'
=== added file 'lazr/config/docs/fixture.py'
--- lazr/config/docs/fixture.py 1970-01-01 00:00:00 +0000
+++ lazr/config/docs/fixture.py 2013-01-10 15:47:20 +0000
@@ -0,0 +1,34 @@
1# Copyright 2009-2013 Canonical Ltd. All rights reserved.
2#
3# This file is part of lazr.smtptest
4#
5# lazr.smtptest is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Lesser General Public License as published by
7# the Free Software Foundation, version 3 of the License.
8#
9# lazr.smtptest is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
12# License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with lazr.smtptest. If not, see <http://www.gnu.org/licenses/>.
16
17"""Doctest fixtures for running under nose."""
18
19from __future__ import absolute_import, print_function, unicode_literals
20
21__metaclass__ = type
22__all__ = [
23 'globs',
24 ]
25
26
27def globs(globs):
28 """Set up globals for doctests."""
29 # Enable future statements to make Python 2 act more like Python 3.
30 globs['absolute_import'] = absolute_import
31 globs['print_function'] = print_function
32 globs['unicode_literals'] = unicode_literals
33 # Provide a convenient way to clean things up at the end of the test.
34 return globs
035
=== renamed file 'src/lazr/config/README.txt' => 'lazr/config/docs/usage.rst'
--- src/lazr/config/README.txt 2009-03-24 17:36:13 +0000
+++ lazr/config/docs/usage.rst 2013-01-10 15:47:20 +0000
@@ -1,60 +1,40 @@
1..1===========
2 This file is part of lazr.config.
3
4 lazr.config is free software: you can redistribute it and/or modify it
5 under the terms of the GNU Lesser General Public License as published by
6 the Free Software Foundation, version 3 of the License.
7
8 lazr.config is distributed in the hope that it will be useful, but WITHOUT
9 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
10 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
11 License for more details.
12
13 You should have received a copy of the GNU Lesser General Public License
14 along with lazr.config. If not, see <http://www.gnu.org/licenses/>.
15
16LAZR config2LAZR config
17***********3===========
184
19The LAZR config system is typically used to manage process configuration.5The LAZR config system is typically used to manage process configuration.
20Process configuration is for saying how things change when we run6Process configuration is for saying how things change when we run systems on
21systems on different machines, or under different circumstances.7different machines, or under different circumstances.
228
23This system uses ini-like file format of section, keys, and values.9This system uses ini-like file format of section, keys, and values. The
24The config file supports inheritance to minimize duplication of10config file supports inheritance to minimize duplication of information across
25information across files. The format supports schema validation.11files. The format supports schema validation.
2612
2713
28============
29ConfigSchema14ConfigSchema
30============15============
3116
32A schema is loaded by instantiating the ConfigSchema class with17A schema is loaded by instantiating the ConfigSchema class with the path to a
33the path to a configuration file. The schema is explicitly derived from18configuration file. The schema is explicitly derived from the information in
34the information in the configuration file.19the configuration file.
3520
36 >>> from os import path21 >>> from pkg_resources import resource_string
37 >>> from zope.interface.verify import verifyObject22 >>> raw_schema = resource_string('lazr.config.tests.testdata', 'base.conf')
38 >>> from lazr.config import ConfigSchema23
39 >>> from lazr.config.interfaces import IConfigSchema24The config file contains sections enclosed in square brackets
4025(e.g. ``[section]``). The section name may be divided into major and minor
41 >>> import lazr.config26categories using a dot (``.``). Beneath each section is a list of key-value
42 >>> testfiles_dir = path.normpath(path.join(27pairs, separated by a colon (``:``).
43 ... path.dirname(lazr.config.__file__), 'tests', 'testdata'))28
44 >>> base_conf = path.join(testfiles_dir, 'base.conf')29Multiple sections with the same major category may have their keys defined in
4530another section that appends the ``.template`` suffix to the category name.
46The config file contains sections enclosed in square brackets ([]).31
47The section name may be divided into major and minor categories using a32A section with ``.optional`` suffix is not required. Lines that start with a
48dot (.). Beneath each section is a list of key-value pairs, separated33hash (``#``) are comments.
49by a colon (:). Multiple sections with the same major category may have34
50their keys defined in another section that appends the '.template'35 >>> from pkg_resources import resource_string
51suffix to the category name. A section with '.optional' suffix is not36 >>> raw_schema = resource_string('lazr.config.tests.testdata', 'base.conf')
52required. Lines that start with a hash (#) are comments.37 >>> print(raw_schema.decode('utf-8'))
53
54 >>> schema_file = open(base_conf, 'r')
55 >>> raw_schema = schema_file.read()
56 >>> schema_file.close()
57 >>> print raw_schema
58 # This section defines required keys and default values.38 # This section defines required keys and default values.
59 [section_1]39 [section_1]
60 key1: foo40 key1: foo
@@ -86,36 +66,46 @@
86 key2: multiline value 166 key2: multiline value 1
87 multiline value 267 multiline value 2
8868
69To create the schema, provide a file name.
70
71 >>> from lazr.config import ConfigSchema
72 >>> from lazr.config.interfaces import IConfigSchema
73 >>> from pkg_resources import resource_filename
74 >>> from zope.interface.verify import verifyObject
75 >>> base_conf = resource_filename(
76 ... 'lazr.config.tests.testdata', 'base.conf')
89 >>> schema = ConfigSchema(base_conf)77 >>> schema = ConfigSchema(base_conf)
90 >>> verifyObject(IConfigSchema, schema)78 >>> verifyObject(IConfigSchema, schema)
91 True79 True
9280
93 >>> schema.name81The schema has a name and a file name.
94 'base.conf'82
95 >>> schema.filename83 >>> print(schema.name)
96 '...lazr/config/tests/testdata/base.conf'84 base.conf
85 >>> print('file:', schema.filename)
86 file: ...lazr/config/tests/testdata/base.conf
9787
98If you provide an optional file-like object as a second argument to the88If you provide an optional file-like object as a second argument to the
99constructor, that is used instead of opening the named file implicitly.89constructor, that is used instead of opening the named file implicitly.
10090
101 >>> file_object = open(base_conf)91 >>> with open(base_conf, 'r') as file_object:
102 >>> other_schema = ConfigSchema('/does/not/exist.conf', file_object)92 ... other_schema = ConfigSchema('/does/not/exist.conf', file_object)
103 >>> verifyObject(IConfigSchema, other_schema)93 >>> verifyObject(IConfigSchema, other_schema)
104 True94 True
10595
106 >>> print other_schema.name96For such schemas, the file name is taken from the first argument.
97
98 >>> print(other_schema.name)
107 exist.conf99 exist.conf
108 >>> print other_schema.filename100 >>> print(other_schema.filename)
109 /does/not/exist.conf101 /does/not/exist.conf
110102
111 >>> file_object.close()
112
113A schema is made up of multiple SchemaSections. They can be iterated103A schema is made up of multiple SchemaSections. They can be iterated
114over in a loop as needed.104over in a loop as needed.
115105
116 >>> from operator import attrgetter106 >>> from operator import attrgetter
117 >>> for section_schema in sorted(schema, key=attrgetter('name')):107 >>> for section_schema in sorted(schema, key=attrgetter('name')):
118 ... print section_schema.name108 ... print(section_schema.name)
119 section-2.app-b109 section-2.app-b
120 section-5110 section-5
121 section_1111 section_1
@@ -124,7 +114,7 @@
124 section_33114 section_33
125115
126 >>> for section_schema in sorted(other_schema, key=attrgetter('name')):116 >>> for section_schema in sorted(other_schema, key=attrgetter('name')):
127 ... print section_schema.name117 ... print(section_schema.name)
128 section-2.app-b118 section-2.app-b
129 section-5119 section-5
130 section_1120 section_1
@@ -140,78 +130,69 @@
140 >>> 'section-4' in schema130 >>> 'section-4' in schema
141 False131 False
142132
143A SectionSchema can be retrieved from the schema using the []133A SectionSchema can be retrieved from the schema using the ``[]`` operator.
144operator
145134
146 >>> section_schema_1 = schema['section_1']135 >>> section_schema_1 = schema['section_1']
147 >>> section_schema_1.name136 >>> print(section_schema_1.name)
148 'section_1'137 section_1
149138
150A SectionNotFound error is raised if the name does not match any of the139Processes often require resources like databases or virtual hosts that have a
151SectionSchemas.140common category of keys. The list of all category names can be retrieved via
152141the categories attribute.
153 >>> section_schema_app_a = schema['section_3.app_a']142
154 >>> schema['section-4']143 >>> for name in schema.category_names:
155 Traceback (most recent call last):144 ... print(name)
156 ...145 section-2
157 NoSectionError: ...146 section_3
158
159Processes often require resources like databases or vhosts that have a
160common category of keys. The list of all category names can be retrieved
161via the categories attribute.
162
163 >>> schema.category_names
164 ['section_3', 'section-2']
165147
166The list of SchemaSections that share common category can be retrieved148The list of SchemaSections that share common category can be retrieved
167using getByCategory().149using ``getByCategory()``.
168150
169 >>> all_section_3 = schema.getByCategory('section_3')151 >>> all_section_3 = schema.getByCategory('section_3')
170 >>> for section_schema in sorted(all_section_3, key=attrgetter('name')):152 >>> for section_schema in sorted(all_section_3, key=attrgetter('name')):
171 ... print section_schema.name153 ... print(section_schema.name)
172 section_3.app_a154 section_3.app_a
173 section_3.app_b155 section_3.app_b
174156
175An error is raised when accessing a category does not exist.157You can pass a default argument to ``getByCategory()`` to avoid the exception.
176
177 >>> schema.getByCategory('non-section')
178 Traceback (most recent call last):
179 ...
180 NoCategoryError: ...
181
182You can pass a default argument to getByCategory() to avoid the exception.
183158
184 >>> missing = object()159 >>> missing = object()
185 >>> schema.getByCategory('non-section', missing) is missing160 >>> schema.getByCategory('non-section', missing) is missing
186 True161 True
187162
188163
189=============
190SchemaSection164SchemaSection
191=============165=============
192166
193A SchemaSection behaves similar to a dictionary. It has keys and167A SchemaSection behaves similar to a dictionary. It has keys and values.
194values. Each SchemaSection has a name.
195168
196 >>> from lazr.config.interfaces import ISectionSchema169 >>> from lazr.config.interfaces import ISectionSchema
197 >>> section_schema_1 = schema['section_1']170 >>> section_schema_1 = schema['section_1']
198 >>> verifyObject(ISectionSchema, section_schema_1)171 >>> verifyObject(ISectionSchema, section_schema_1)
199 True172 True
200173
201 >>> section_schema_1.name174Each SchemaSection has a name.
202 'section_1'175
203176 >>> print(section_schema_1.name)
204A SchemaSection can return a 2-tuple of its category name and specific177 section_1
205name parts. The category name will be None if the SchemaSection's name178
206does not contain a category.179A SchemaSection can return a 2-tuple of its category name and specific name
207180parts.
208 >>> schema['section_3.app_b'].category_and_section_names181
209 ('section_3', 'app_b')182 >>> for name in schema['section_3.app_b'].category_and_section_names:
210183 ... print(name)
211 >>> section_schema_1.category_and_section_names184 section_3
212 (None, 'section_1')185 app_b
213186
214Optional sections have the optional attribute set to True:187The category name will be ``None`` if the SchemaSection's name does not
188contain a category.
189
190 >>> for name in section_schema_1.category_and_section_names:
191 ... print(name)
192 None
193 section_1
194
195Optional sections have the optional attribute set to ``True``:
215196
216 >>> section_schema_1.optional197 >>> section_schema_1.optional
217 False198 False
@@ -225,11 +206,11 @@
225 >>> 'nonkey' in section_schema_1206 >>> 'nonkey' in section_schema_1
226 False207 False
227208
228A key can be accessed directly using as a subscript of the SchemaSection.209A key can be accessed directly using as a subscript of the SchemaSection. The
229The value is always a string.210value is always a string.
230211
231 >>> section_schema_1['key3']212 >>> print(section_schema_1['key3'])
232 'Launchpad&nbsp;rocks'213 Launchpad&nbsp;rocks
233 >>> section_schema_1['key5']214 >>> section_schema_1['key5']
234 ''215 ''
235216
@@ -240,29 +221,29 @@
240 ...221 ...
241 KeyError: ...222 KeyError: ...
242223
243In the conf file, '[section_1]' is a default section that defines keys224In the conf file, ``[section_1]`` is a default section that defines keys and
244and values. The values specified in the section schema will be used as225values. The values specified in the section schema will be used as default
245default values if not overriden in the configuration. In the case of226values if not overridden in the configuration. In the case of *key5*, the key
246key5, the key had no explicit value, so the value is an empty string.227had no explicit value, so the value is an empty string.
247228
248 >>> for key in sorted(section_schema_1):229 >>> for key in sorted(section_schema_1):
249 ... print key, ':', section_schema_1[key]230 ... print(key, ':', section_schema_1[key])
250 key1 : foo231 key1 : foo
251 key2 : bar and baz232 key2 : bar and baz
252 key3 : Launchpad&nbsp;rocks233 key3 : Launchpad&nbsp;rocks
253 key4 : F&#028c;k yeah!234 key4 : F&#028c;k yeah!
254 key5 :235 key5 :
255236
256In the conf file '[section_3.template]' defines a common set of keys and237In the conf file ``[section_3.template]`` defines a common set of keys and
257default values for '[section_3.app_a]' and '[section_3.app_b]'. When a238default values for ``[section_3.app_a]`` and ``[section_3.app_b]``. When a
258section defines different keys and default values s from the template,239section defines different keys and default values from the template, the new
259the new data overlays the template data. This is the case for section240data overlays the template data. This is the case for section
260'[section_3.app_b]'.241``[section_3.app_b]``.
261242
262 >>> for section_schema in sorted(all_section_3, key=attrgetter('name')):243 >>> for section_schema in sorted(all_section_3, key=attrgetter('name')):
263 ... print section_schema.name244 ... print(section_schema.name)
264 ... for key in sorted(section_schema):245 ... for key in sorted(section_schema):
265 ... print key, ':', section_schema[key]246 ... print(key, ':', section_schema[key])
266 section_3.app_a247 section_3.app_a
267 key1 : 17248 key1 : 17
268 key2 : 3.1415249 key2 : 3.1415
@@ -272,93 +253,45 @@
272 key3 : unique253 key3 : unique
273254
274255
275=======================
276ConfigSchema validation256ConfigSchema validation
277=======================257=======================
278258
279ConfigSchema will raise an error if the schema file cannot be opened.259The schema parser is self-validating. It checks that the character encoding
280260is ASCII, and that the data is not ambiguous or self-contradicting. Keys must
281 >>> ConfigSchema("no-such-file")261exist inside sections and section names may not be defined twice. Sections
282 Traceback (most recent call last):262may belong to only one category, and only letters, numbers, dots and dashes
283 ...263may be present in section names.
284 IOError: [Errno 2] No such file or directory: ...264
285265.. For multilingual Python support reasons, we don't include testable examples
286The schema parser is self-validating. It will check that the character266 here. See ``test_config.py`` and ``lazr/config/interfaces.py`` for details.
287encoding is ascii. It will check that the data is not ambiguous or267
288self-contradicting.268
289
290Schema files that contain non-ASCII characters raise a
291UnicodeDecodeError.
292
293 >>> ConfigSchema(path.join(testfiles_dir, 'bad-nonascii.conf'))
294 Traceback (most recent call last):
295 ...
296 UnicodeDecodeError: ...
297
298Keys without sections raise MissingSectionHeaderError.
299
300 >>> ConfigSchema(path.join(testfiles_dir, 'bad-sectionless.conf'))
301 Traceback (most recent call last):
302 ...
303 MissingSectionHeaderError: File contains no section headers. ...
304
305Redefining a section in a config file will raise a RedefinedSectionError.
306
307 >>> ConfigSchema(path.join(testfiles_dir, 'bad-redefined-section.conf'))
308 Traceback (most recent call last):
309 ...
310 RedefinedSectionError: ...
311
312# XXX sinzui 2007-12-13:
313# ConfigSchema should raise RedefinedKeyError when a section redefines
314# a key.
315
316Defining a section that belongs to many categories will raise
317a InvalidSectionNameError.
318
319 >>> ConfigSchema(path.join(testfiles_dir, 'bad-invalid-name.conf'))
320 Traceback (most recent call last):
321 ...
322 InvalidSectionNameError: [category.other_category.name.optional] ...
323
324As does using non word characters other than a dot or dash in the
325section name.
326
327 >>> ConfigSchema(path.join(testfiles_dir, 'bad-invalid-name-chars.conf'))
328 Traceback (most recent call last):
329 ...
330 InvalidSectionNameError: [$category.name_part.optional] ...
331
332
333=============
334IConfigLoader269IConfigLoader
335=============270=============
336271
337ConfigSchema implements the two methods in the IConfigLoader interface.272ConfigSchema implements the two methods in the IConfigLoader interface. A
338A Config is created by a schema using either the load() or loadFile()273Config is created by a schema using either the ``load()`` or ``loadFile()``
339methods to return a Config instance.274methods to return a Config instance.
340275
341 >>> from lazr.config.interfaces import IConfigLoader276 >>> from lazr.config.interfaces import IConfigLoader
342 >>> verifyObject(IConfigLoader, schema)277 >>> verifyObject(IConfigLoader, schema)
343 True278 True
344279
345The load() method accepts a filename.280The ``load()`` method accepts a filename.
346281
347 >>> local_conf = path.join(testfiles_dir, 'local.conf')282 >>> local_conf = resource_filename(
283 ... 'lazr.config.tests.testdata', 'local.conf')
348 >>> config = schema.load(local_conf)284 >>> config = schema.load(local_conf)
349285
350Passing a filename to a non-existent file will raise an IOError.286The ``loadFile()`` method accepts a file-like object and an optional filename
351287keyword argument. The filename argument must be passed if the file-like
352 >>> schema.load("fnord.conf")288object does not have a ``name`` attribute.
353 Traceback (most recent call last):289
354 ...290 >>> try:
355 IOError: [Errno 2] No such file or directory: 'fnord.conf'291 ... from io import StringIO
356292 ... except ImportError:
357The loadFile method accepts a file-like object and an optional filename293 ... # Python 2
358keyword arg. The filename arg must be passed if the file-like object294 ... from StringIO import StringIO
359does not have a name attribute.
360
361 >>> import StringIO
362 >>> bad_data = ("""295 >>> bad_data = ("""
363 ... [meta]296 ... [meta]
364 ... metakey: unsupported297 ... metakey: unsupported
@@ -369,30 +302,24 @@
369 ... key1: bad character in caf\xc3)302 ... key1: bad character in caf\xc3)
370 ... [section_3.template]303 ... [section_3.template]
371 ... key1: schema suffixes are not permitted""")304 ... key1: schema suffixes are not permitted""")
372 >>> schema.loadFile(StringIO.StringIO(bad_data))
373 Traceback (most recent call last):
374 ...
375 AttributeError: StringIO instance has no attribute 'name'
376
377 >>> bad_config = schema.loadFile(305 >>> bad_config = schema.loadFile(
378 ... StringIO.StringIO(bad_data), 'bad conf')306 ... StringIO(bad_data), 'bad conf')
379307
380The bad_config example will be used for validation tests.308.. The bad_config example will be used for validation tests.
381309
382310
383======
384Config311Config
385======312======
386313
387The config represents the local configuration of the process on a314The config represents the local configuration of the process on a system. It
388system. It is validated with a schema. It extends the schema, or other315is validated with a schema. It extends the schema, or other conf files, to
389conf files to define the specific differences from the extended files316define the specific differences from the extended files that are required to
390that are required to run the local processes.317run the local processes.
391318
392The object returned by load() provides both the IConfigData and319The object returned by ``load()`` provides both the ``IConfigData`` and
393IStackableConfig interfaces. IConfigData is for read-only access to the320``IStackableConfig`` interfaces. ``IConfigData`` is for read-only access to
394configuration data. A process configuration is made up of a stack of321the configuration data. A process configuration is made up of a stack of
395different IConfigData. The IStackableConfig interface provides the322different ``IConfigData``. The ``IStackableConfig`` interface provides the
396methods used to manipulate that stack of configuration overlays.323methods used to manipulate that stack of configuration overlays.
397324
398 >>> from lazr.config.interfaces import IConfigData, IStackableConfig325 >>> from lazr.config.interfaces import IConfigData, IStackableConfig
@@ -401,15 +328,14 @@
401 >>> verifyObject(IStackableConfig, config)328 >>> verifyObject(IStackableConfig, config)
402 True329 True
403330
404Like the schema file, the conf file is made up of sections with keys.331Like the schema file, the conf file is made up of sections with keys. The
405The sections may belong to a category. Unlike the schema file, it does332sections may belong to a category. Unlike the schema file, it does not have
406not have template or optional sections. The [meta] has the extends333template or optional sections. The ``[meta]`` section has the extends key
407key that declares that this conf extends shared.conf.334that declares that this conf extends ``shared.conf``.
408335
409 >>> local_file = open(local_conf, 'r')336 >>> with open(local_conf, 'rt') as local_file:
410 >>> raw_conf = local_file.read()337 ... raw_conf = local_file.read()
411 >>> local_file.close()338 >>> print(raw_conf)
412 >>> print raw_conf
413 [meta]339 [meta]
414 extends: shared.conf340 extends: shared.conf
415 # Localize a key for section_1.341 # Localize a key for section_1.
@@ -418,66 +344,71 @@
418 # Accept the default values for the optional section-5.344 # Accept the default values for the optional section-5.
419 [section-5]345 [section-5]
420346
421The .master section allows admins to define configurations for an arbitrary347The ``.master`` section allows admins to define configurations for an
422number of processes. If the schema defines .master sections, then the conf348arbitrary number of processes. If the schema defines ``.master`` sections,
423file can contain sections that extend the .master section. These are like349then the conf file can contain sections that extend the ``.master`` section.
424categories with templates except that the section names extending .master need350These are like categories with templates except that the section names
425not be named in the schema file.351extending ``.master`` need not be named in the schema file.
426352
427 >>> master_schema_conf = path.join(testfiles_dir, 'master.conf')353 >>> master_schema_conf = resource_filename(
428 >>> master_local_conf = path.join(testfiles_dir, 'master-local.conf')354 ... 'lazr.config.tests.testdata', 'master.conf')
355 >>> master_local_conf = resource_filename(
356 ... 'lazr.config.tests.testdata', 'master-local.conf')
429 >>> master_schema = ConfigSchema(master_schema_conf)357 >>> master_schema = ConfigSchema(master_schema_conf)
430 >>> sections = master_schema.getByCategory('thing')358 >>> sections = master_schema.getByCategory('thing')
431 >>> sorted(section.name for section in sections)359 >>> for name in sorted(section.name for section in sections):
432 ['thing.master']360 ... print(name)
361 thing.master
433 >>> master_conf = master_schema.load(master_local_conf)362 >>> master_conf = master_schema.load(master_local_conf)
434 >>> sections = master_conf.getByCategory('thing')363 >>> sections = master_conf.getByCategory('thing')
435 >>> sorted(section.name for section in sections)364 >>> for name in sorted(section.name for section in sections):
436 ['thing.one', 'thing.two']365 ... print(name)
437 >>> sorted(section.foo for section in sections)366 thing.one
438 ['1', '2']367 thing.two
439 >>> print master_conf.thing.one.name368 >>> for name in sorted(section.foo for section in sections):
440 thing.one369 ... print(name)
441370 1
442The shared.conf file derives the keys and default values from the371 2
443schema. This config was loaded before local.conf because its sections372 >>> print(master_conf.thing.one.name)
444and values are required to be in place before local.conf applies its373 thing.one
445changes.374
446375The ``shared.conf`` file derives the keys and default values from the schema.
447 >>> shared_conf = path.join(testfiles_dir, 'shared.conf')376This config was loaded before ``local.conf`` because its sections and values
448 >>> shared_file = open(shared_conf, 'r')377are required to be in place before ``local.conf`` applies its changes.
449 >>> raw_conf = shared_file.read()378
450 >>> shared_file.close()379 >>> shared_config = resource_filename(
451 >>> print raw_conf380 ... 'lazr.config.tests.testdata', 'shared.conf')
381 >>> with open(shared_config, 'rt') as shared_file:
382 ... raw_conf = shared_file.read()
383 >>> print(raw_conf)
452 # The schema is defined by base.conf.384 # The schema is defined by base.conf.
453 # Localize a key for section_1.385 # Localize a key for section_1.
454 [section_1]386 [section_1]
455 key2: sharing is fun387 key2: sharing is fun
456 key5: shared value388 key5: shared value
457389
458The config that was loaded has name and filename attributes to identify390The config that was loaded has ``name`` and ``filename`` attributes to
459the configuration.391identify the configuration.
460392
461 >>> config.name393 >>> print(config.name)
462 'local.conf'394 local.conf
463 >>> config.filename395 >>> print('file:', config.filename)
464 '...lazr/config/tests/testdata/local.conf'396 file: ...lazr/config/tests/testdata/local.conf
465397
466The config can access the schema via the schema property.398The config can access the schema via the schema property.
467399
468 >>> config.schema.name400 >>> print(config.schema.name)
469 'base.conf'401 base.conf
470 >>> config.schema is schema402 >>> config.schema is schema
471 True403 True
472404
473A config is made up of multiple Sections like the schema. They can be405A config is made up of multiple Sections like the schema. They can be
474iterated over in a loop as needed. This config inherited several406iterated over in a loop as needed. This config inherited several sections
475sections defined in schema. Note that the meta section is not present407defined in schema. Note that the meta section is not present because it
476because it pertains to the config system, not to the processes being408pertains to the config system, not to the processes being configured.
477configured.
478409
479 >>> for section in sorted(config, key=attrgetter('name')):410 >>> for section in sorted(config, key=attrgetter('name')):
480 ... print section.name411 ... print(section.name)
481 section-2.app-b412 section-2.app-b
482 section-5413 section-5
483 section_1414 section_1
@@ -491,11 +422,11 @@
491 >>> 'bad-section' in config422 >>> 'bad-section' in config
492 False423 False
493424
494Optional SchemaSections are not inherited by the config. A config file425Optional SchemaSections are not inherited by the config. A config file must
495must declare all optional sections. Including the section heading is426declare all optional sections. Including the section heading is enough to
496enough to inherit the section and its keys. The config file may localize427inherit the section and its keys. The config file may localize the keys by
497the keys by declaring them too. The local.conf file includes428declaring them too. The ``local.conf`` file includes ``section-5``, but not
498'section-5', but not 'section_3.app_a'429``section_3.app_a``.
499430
500431
501 >>> 'section_3.app_a' in config432 >>> 'section_3.app_a' in config
@@ -504,7 +435,6 @@
504 True435 True
505 >>> config.schema['section_3.app_a'].optional436 >>> config.schema['section_3.app_a'].optional
506 True437 True
507
508 >>> 'section-5' in config438 >>> 'section-5' in config
509 True439 True
510 >>> 'section-5' in config.schema440 >>> 'section-5' in config.schema
@@ -512,81 +442,65 @@
512 >>> config.schema['section-5'].optional442 >>> config.schema['section-5'].optional
513 True443 True
514444
515A Section can be accessed using subscript notation. Accessing a section445A Section can be accessed using subscript notation. Accessing a section that
516that does not exist will raise a NoSectionError.446does not exist will raise a NoSectionError. NoSectionError is raised for a
447undeclared optional sections too.
517448
518 >>> section_1 = config['section_1']449 >>> section_1 = config['section_1']
519 >>> section_1.name in config450 >>> section_1.name in config
520 True451 True
521452
522 >>> config['section-4']453Config supports category access like Schema does. The list of categories are
523 Traceback (most recent call last):454returned by the ``category_names`` property.
524 ...455
525 NoSectionError: ...456 >>> for name in sorted(config.category_names):
526457 ... print(name)
527NoSectionError is raised for a undeclared optional sections too.458 section-2
528459 section_3
529 >>> config['section_3.app_a']
530 Traceback (most recent call last):
531 ...
532 NoSectionError: ...
533
534Config supports category access like Schema does. The list of
535categories are returned by the category_names property.
536
537 >>> sorted(config.category_names)
538 ['section-2', 'section_3']
539460
540All the sections that belong to a category can be retrieved using the461All the sections that belong to a category can be retrieved using the
541getByCategory() method.462``getByCategory()`` method.
542463
543 >>> for section in config.getByCategory('section_3'):464 >>> for section in config.getByCategory('section_3'):
544 ... print section_schema.name465 ... print(section_schema.name)
545 section_3.app_b466 section_3.app_b
546467
547Passing a non-existent category_name to the method will raise a468Passing a non-existent category_name to the method will raise a
548NoCategoryError.469NoCategoryError. As with schemas, you can pass a default argument to
549470``getByCategory()`` to avoid the exception.
550 >>> config.getByCategory('non-section')
551 Traceback (most recent call last):
552 ...
553 NoCategoryError: ...
554
555As with schemas, you can pass a default argument to getByCategory() to avoid
556the exception.
557471
558 >>> missing = object()472 >>> missing = object()
559 >>> config.getByCategory('non-section', missing) is missing473 >>> config.getByCategory('non-section', missing) is missing
560 True474 True
561475
562476
563=======
564Section477Section
565=======478=======
566479
567A Section behaves similar to a dictionary. It has keys and values.480A Section behaves similar to a dictionary. It has keys and values. It
568It supports some specialize access methods and properties for working481supports some specialize access methods and properties for working with the
569with the values. Each Section has a name. Continuing with section_1482values. Each Section has a name.
570from above....
571483
572 >>> from lazr.config.interfaces import ISection484 >>> from lazr.config.interfaces import ISection
573 >>> verifyObject(ISection, section_1)485 >>> verifyObject(ISection, section_1)
574 True486 True
575487 >>> print(section_1.name)
576 >>> section_1.name488 section_1
577 'section_1'489
578490Like SectionSchemas, sections can return a 2-tuple of their category name and
579Like SectionSchemas, sections can return a 2-tuple of their category491specific name parts. The category name will be ``None`` if the section's name
580name and specific name parts. The category name will be None if the492does not contain a category.
581section's name does not contain a category.493
582494 >>> for name in config['section_3.app_b'].category_and_section_names:
583 >>> config['section_3.app_b'].category_and_section_names495 ... print(name)
584 ('section_3', 'app_b')496 section_3
585497 app_b
586 >>> section_1.category_and_section_names498 >>> for name in section_1.category_and_section_names:
587 (None, 'section_1')499 ... print(name)
588500 None
589The Section's type is the same type as the ConfigSchema.section_factory.501 section_1
502
503The Section's type is the same type as the ``ConfigSchema.section_factory``.
590504
591 >>> section_1505 >>> section_1
592 <lazr.config...Section object at ...>506 <lazr.config...Section object at ...>
@@ -603,10 +517,10 @@
603A key can be accessed directly using as a subscript of the Section.517A key can be accessed directly using as a subscript of the Section.
604The value is always a string.518The value is always a string.
605519
606 >>> section_1['key3']520 >>> print(section_1['key3'])
607 'Launchpad&nbsp;rocks'521 Launchpad&nbsp;rocks
608 >>> section_1['key5']522 >>> print(section_1['key5'])
609 'local value'523 local value
610524
611An error is raised if a non-existent key is accessed via a subscript.525An error is raised if a non-existent key is accessed via a subscript.
612526
@@ -615,37 +529,36 @@
615 ...529 ...
616 KeyError: ...530 KeyError: ...
617531
618The Section keys can be iterated. The section has all the keys from the532The Section keys can be iterated over. The section has all the keys from the
619SectionSchema. The values came form the schema's default values, then533SectionSchema. The values came form the schema's default values, then the
620the values from shared.conf were applied, and lastly, the values from534values from ``shared.conf`` were applied, and lastly, the values from
621local.conf were applied. The schema provided the values of key1, key3,535``local.conf`` were applied. The schema provided the values of ``key1``,
622and key4, shared.conf provided the value of key2. local.conf provided536``key3``, and ``key4``. ``shared.conf`` provided the value of ``key2``
623key5. While shared.conf provided a key5, local.conf takes precedence.537. ``local.conf`` provided ``key5``. While ``shared.conf`` provided a
538``key5``, ``local.conf`` takes precedence.
624539
625 >>> for key in sorted(section_1):540 >>> for key in sorted(section_1):
626 ... print key, ':', section_1[key]541 ... print(key, ':', section_1[key])
627 key1 : foo542 key1 : foo
628 key2 : sharing is fun543 key2 : sharing is fun
629 key3 : Launchpad&nbsp;rocks544 key3 : Launchpad&nbsp;rocks
630 key4 : F&#028c;k yeah!545 key4 : F&#028c;k yeah!
631 key5 : local value546 key5 : local value
632
633 >>> section_1.schema['key5']547 >>> section_1.schema['key5']
634 ''548 ''
635549
636The schema provided mandatory sections and default values to the550The schema provided mandatory sections and default values to the config. So
637config. So while the config file did not declare all the sections, they551while the config file did not declare all the sections, they are present. In
638are present. In the case of section_3.app_b, its keys were defined in a552the case of ``section_3.app_b``, its keys were defined in a template section.
639template section.
640553
641 >>> for key in sorted(config['section_3.app_b']):554 >>> for key in sorted(config['section_3.app_b']):
642 ... print key, ':', config['section_3.app_b'][key]555 ... print(key, ':', config['section_3.app_b'][key])
643 key1 : 17556 key1 : 17
644 key2 : changed557 key2 : changed
645 key3 : unique558 key3 : unique
646559
647Sections attributes cannot be directly set to shadow config options. An560Sections attributes cannot be directly set to shadow config options. An
648AttributeError is raised when a callsite attempts to mutate the config.561``AttributeError`` is raised when an attempt is made to mutate the config.
649562
650 >>> config['section_3.app_b'].key1 = 'fail'563 >>> config['section_3.app_b'].key1 = 'fail'
651 Traceback (most recent call last):564 Traceback (most recent call last):
@@ -660,96 +573,73 @@
660 AttributeError: Config options cannot be set directly.573 AttributeError: Config options cannot be set directly.
661574
662575
663==================
664Validating configs576Validating configs
665==================577==================
666578
667Config provides the validate() method to verify that the config is valid579Config provides the ``validate()`` method to verify that the config is valid
668according to the schema. The method returns True if the config is valid.580according to the schema. The method returns ``True`` if the config is valid.
669581
670 >>> config.validate()582 >>> config.validate()
671 True583 True
672584
673When the config is not valid, a ConfigErrors is raised. The585When the config is not valid, a ConfigErrors is raised. The exception has an
674exception has an errors property that contains a list of all the586``errors`` property that contains a list of all the errors in the config.
675errors in the config.587
676588
677 >>> from lazr.config.interfaces import ConfigErrors
678
679 >>> try:
680 ... bad_config.validate()
681 ... except ConfigErrors, validation_error:
682 ... print validation_error
683 ... for error in validation_error.errors:
684 ... print "%s: %s" % (error.__class__.__name__, error)
685 ConfigErrors: bad conf is not valid.
686 UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in ... range(128)
687 UnknownKeyError: section_1 does not have a keyn key.
688 UnknownKeyError: The meta section does not have a metakey key.
689 UnknownSectionError: base.conf does not have a unknown-section section.
690
691
692===============
693Config overlays589Config overlays
694===============590===============
695591
696A conf file may contains a meta section that is used by the config592A conf file may contain a meta section that is used by the config system. The
697system. The config data can access the config it extended using the593config data can access the config it extended using the ``extends`` property.
698extends property. The object is just the config data; it does not594The object is just the config data; it does not have any config methods.
699have any config methods.
700595
701 >>> config.extends.name596 >>> print(config.extends.name)
702 'shared.conf'597 shared.conf
703598
704 >>> verifyObject(IConfigData, config.extends)599 >>> verifyObject(IConfigData, config.extends)
705 True600 True
706 >>> verifyObject(IStackableConfig, config.extends)
707 Traceback (most recent call last):
708 ...
709 DoesNotImplement: ...
710601
711As Config supports inheritance through the extends key, each conf file602As Config supports inheritance through the ``extends`` key, each conf file
712produces instance of ConfigData, called an overlay. ConfigData603produces instance of ConfigData, called an *overlay*. ConfigData represents
713represents the state of a config. The overlays property is a stack of604the state of a config. The ``overlays`` property is a stack of ConfigData as
714ConfigData as it was constructed from the schema's config to the last605it was constructed from the schema's config to the last config file that was
715config file that was loaded.606loaded.
716607
717 >>> for config_data in config.overlays:608 >>> for config_data in config.overlays:
718 ... print config_data.name609 ... print(config_data.name)
719 local.conf610 local.conf
720 shared.conf611 shared.conf
721 base.conf612 base.conf
722
723 >>> verifyObject(IConfigData, config.overlays[-1])613 >>> verifyObject(IConfigData, config.overlays[-1])
724 True614 True
725615
726Conf files can use the extends key to specify that it extends a schema616Conf files can use the ``extends`` key to specify that it extends a schema
727without incurring a processing penalty by loading the schema twice in a617without incurring a processing penalty by loading the schema twice in a row.
728row. The schema can never be the second item in the overlays stack.618The schema can never be the second item in the overlays stack.
729619
730 >>> single_config = schema.load(schema.filename)620 >>> single_config = schema.load(schema.filename)
731 >>> for config_data in single_config.overlays:621 >>> for config_data in single_config.overlays:
732 ... print config_data.name622 ... print(config_data.name)
733 base.conf623 base.conf
734624 >>> single_config.push(schema.filename, raw_schema.decode('utf-8'))
735 >>> single_config.push(schema.filename, raw_schema)
736 >>> for config_data in single_config.overlays:625 >>> for config_data in single_config.overlays:
737 ... print config_data.name626 ... print(config_data.name)
738 base.conf627 base.conf
739628
740629
741push()630push()
742======631======
743632
744Raw config data can be merged with the config to create a new overlay633Raw config data can be merged with the config to create a new overlay for
745for testing. The push() method accepts a string of config data. The634testing. The ``push()`` method accepts a string of config data. The data
746data must conform to the schema. The 'section_1' sections's keys are635must conform to the schema. The ``section_1`` sections's keys are updated
747updated when the unparsed data is pushed onto the config. Note that636when the unparsed data is pushed onto the config. Note that indented,
748indented unparsed data is passed to push() in thie example; push()637unparsed data is passed to ``push()`` in this example; ``push()`` does not
749does not require tests to dedent the test data.638require tests to dedent the test data.
639::
750640
751 >>> for key in sorted(config['section_1']):641 >>> for key in sorted(config['section_1']):
752 ... print key, ':', config['section_1'][key]642 ... print(key, ':', config['section_1'][key])
753 key1 : foo643 key1 : foo
754 key2 : sharing is fun644 key2 : sharing is fun
755 key3 : Launchpad&nbsp;rocks645 key3 : Launchpad&nbsp;rocks
@@ -763,16 +653,17 @@
763 >>> config.push('test config', test_data)653 >>> config.push('test config', test_data)
764654
765 >>> for key in sorted(config['section_1']):655 >>> for key in sorted(config['section_1']):
766 ... print key, ':', config['section_1'][key]656 ... print(key, ':', config['section_1'][key])
767 key1 : test1657 key1 : test1
768 key2 : sharing is fun658 key2 : sharing is fun
769 key3 : Launchpad&nbsp;rocks659 key3 : Launchpad&nbsp;rocks
770 key4 : F&#028c;k yeah!660 key4 : F&#028c;k yeah!
771 key5 :661 key5 :
772662
773Besides updating section keys, optional sections can be enabled too.663Besides updating section keys, optional sections can be enabled too. The
774The 'section_3.app_a' section is enabled with the default keys from the664``section_3.app_a`` section is enabled with the default keys from the schema
775schema in this example.665in this example.
666::
776667
777 >>> config.schema['section_3.app_a'].optional668 >>> config.schema['section_3.app_a'].optional
778 True669 True
@@ -785,41 +676,41 @@
785 >>> 'section_3.app_a' in config676 >>> 'section_3.app_a' in config
786 True677 True
787 >>> for key in sorted(config['section_3.app_a']):678 >>> for key in sorted(config['section_3.app_a']):
788 ... print key, ':', config['section_3.app_a'][key]679 ... print(key, ':', config['section_3.app_a'][key])
789 key1 : 17680 key1 : 17
790 key2 : 3.1415681 key2 : 3.1415
791682
792 >>> for key in sorted(config.schema['section_3.app_a']):683 >>> for key in sorted(config.schema['section_3.app_a']):
793 ... print key, ':', config.schema['section_3.app_a'][key]684 ... print(key, ':', config.schema['section_3.app_a'][key])
794 key1 : 17685 key1 : 17
795 key2 : 3.1415686 key2 : 3.1415
796687
797The config's name and overlays are updated by push().688The config's name and overlays are updated by ``push()``.
798689
799 >>> config.name690 >>> print(config.name)
800 'test app_a'691 test app_a
801 >>> config.filename692 >>> print(config.filename)
802 'test app_a'693 test app_a
803 >>> for config_data in config.overlays:694 >>> for config_data in config.overlays:
804 ... print config_data.name695 ... print(config_data.name)
805 test app_a696 test app_a
806 test config697 test config
807 local.conf698 local.conf
808 shared.conf699 shared.conf
809 base.conf700 base.conf
810701
811The 'test app_a' did not declare an extends key in a meta section. Its702The ``test app_a`` config did not declare an ``extends`` key in a ``meta``
812extends property is None, even though it implicitly extends 'test703section. Its ``extends`` property is ``None``, even though it implicitly
813config'. The extends property only provides access to configs that are704extends ``test config``. The ``extends`` property only provides access to
814explicitly extended.705configs that are explicitly extended.
815706
816 >>> config.extends.name707 >>> print(config.extends.name)
817 'test config'708 test config
818709
819The config's sections are updated with 'section_3.app_a' too.710The config's sections are updated with ``section_3.app_a`` too.
820711
821 >>> for section in sorted(config, key=attrgetter('name')):712 >>> for section in sorted(config, key=attrgetter('name')):
822 ... print section.name713 ... print(section.name)
823 section-2.app-b714 section-2.app-b
824 section-5715 section-5
825 section_1716 section_1
@@ -827,17 +718,18 @@
827 section_3.app_b718 section_3.app_b
828 section_33719 section_33
829720
830A config file may state that it extends its schema (to clearly connect721A config file may state that it extends its schema (to clearly connect the
831the config to the schema). The schema can also be pushed to reset the722config to the schema). The schema can also be pushed to reset the values in
832values in the config to the schema's default values.723the config to the schema's default values.
833724
834 >>> extender_conf_name = path.join(testfiles_dir, 'extender.conf')725 >>> extender_conf_name = resource_filename(
726 ... 'lazr.config.tests.testdata', 'extender.conf')
835 >>> extender_conf_data = ("""727 >>> extender_conf_data = ("""
836 ... [meta]728 ... [meta]
837 ... extends: base.conf""")729 ... extends: base.conf""")
838 >>> config.push(extender_conf_name, extender_conf_data)730 >>> config.push(extender_conf_name, extender_conf_data)
839 >>> for config_data in config.overlays:731 >>> for config_data in config.overlays:
840 ... print config_data.name732 ... print(config_data.name)
841 extender.conf733 extender.conf
842 base.conf734 base.conf
843 test app_a735 test app_a
@@ -846,22 +738,23 @@
846 shared.conf738 shared.conf
847 base.conf739 base.conf
848740
849The 'section_1' section was restored to the schema's default values.741The ``section_1`` section was restored to the schema's default values.
850742
851 >>> for key in sorted(config['section_1']):743 >>> for key in sorted(config['section_1']):
852 ... print key, ':', config['section_1'][key]744 ... print(key, ':', config['section_1'][key])
853 key1 : foo745 key1 : foo
854 key2 : bar and baz746 key2 : bar and baz
855 key3 : Launchpad&nbsp;rocks747 key3 : Launchpad&nbsp;rocks
856 key4 : F&#028c;k yeah!748 key4 : F&#028c;k yeah!
857 key5 :749 key5 :
858750
859push() can also be used to extend master sections.751``push()`` can also be used to extend master sections.
752::
860753
861 >>> sections = sorted(master_conf.getByCategory('bar'),754 >>> sections = sorted(master_conf.getByCategory('bar'),
862 ... key=attrgetter('name'))755 ... key=attrgetter('name'))
863 >>> for section in sections:756 >>> for section in sections:
864 ... print section.name, section.baz757 ... print(section.name, section.baz)
865 bar.master badger758 bar.master badger
866 bar.soup cougar759 bar.soup cougar
867760
@@ -872,7 +765,7 @@
872 >>> sections = sorted(master_conf.getByCategory('bar'),765 >>> sections = sorted(master_conf.getByCategory('bar'),
873 ... key=attrgetter('name'))766 ... key=attrgetter('name'))
874 >>> for section in sections:767 >>> for section in sections:
875 ... print section.name, section.baz768 ... print(section.name, section.baz)
876 bar.soup cougar769 bar.soup cougar
877 bar.two dolphin770 bar.two dolphin
878771
@@ -883,67 +776,70 @@
883 >>> sections = sorted(master_conf.getByCategory('bar'),776 >>> sections = sorted(master_conf.getByCategory('bar'),
884 ... key=attrgetter('name'))777 ... key=attrgetter('name'))
885 >>> for section in sections:778 >>> for section in sections:
886 ... print section.name, section.baz779 ... print(section.name, section.baz)
887 bar.soup cougar780 bar.soup cougar
888 bar.three emu781 bar.three emu
889 bar.two dolphin782 bar.two dolphin
890783
891push() works with master sections too.784``push()`` works with master sections too.
785::
892786
893 >>> schema_file = StringIO.StringIO("""\787 >>> schema_file = StringIO("""\
894 ... [thing.master]788 ... [thing.master]
895 ... foo: 0789 ... foo: 0
896 ... bar: 0790 ... bar: 0
897 ... """)791 ... """)
898 >>> push_schema = ConfigSchema('schema.cfg', schema_file)792 >>> push_schema = ConfigSchema('schema.cfg', schema_file)
899793
900 >>> config_file = StringIO.StringIO("""\794 >>> config_file = StringIO("""\
901 ... [thing.one]795 ... [thing.one]
902 ... foo: 1796 ... foo: 1
903 ... """)797 ... """)
904 >>> push_config = push_schema.loadFile(config_file, 'config.cfg')798 >>> push_config = push_schema.loadFile(config_file, 'config.cfg')
905 >>> print push_config.thing.one.foo799 >>> print(push_config.thing.one.foo)
906 1800 1
907 >>> print push_config.thing.one.bar801 >>> print(push_config.thing.one.bar)
908 0802 0
909803
910 >>> push_config.push('test.cfg', """\804 >>> push_config.push('test.cfg', """\
911 ... [thing.one]805 ... [thing.one]
912 ... bar: 2806 ... bar: 2
913 ... """)807 ... """)
914 >>> print push_config.thing.one.foo808 >>> print(push_config.thing.one.foo)
915 1809 1
916 >>> print push_config.thing.one.bar810 >>> print(push_config.thing.one.bar)
917 2811 2
918812
919813
920pop()814pop()
921=====815=====
922816
923ConfigData can be removed from the stack of overlays using the pop()817ConfigData can be removed from the stack of overlays using the ``pop()``
924method. The methods returns the list of ConfigData that was removed--a818method. The methods returns the list of ConfigData that was removed -- a
925slice from the specified ConfigData to the top of the stack.819slice from the specified ConfigData to the top of the stack.
820::
926821
927 >>> overlays = config.pop('test config')822 >>> overlays = config.pop('test config')
928 >>> for config_data in overlays:823 >>> for config_data in overlays:
929 ... config_data.name824 ... print(config_data.name)
930 'extender.conf'825 extender.conf
931 'base.conf'826 base.conf
932 'test app_a'827 test app_a
933 'test config'828 test config
934829
935 >>> for config_data in config.overlays:830 >>> for config_data in config.overlays:
936 ... print config_data.name831 ... print(config_data.name)
937 local.conf832 local.conf
938 shared.conf833 shared.conf
939 base.conf834 base.conf
940835
941The config's state was restored to the ConfigData that is top of the836The config's state was restored to the ConfigData that is on top of the
942overlay stack. Section 'section_3.app_a' was removed completely. The837overlay stack. Section ``section_3.app_a`` was removed completely. The keys
943keys ('key1' and 'key5') for 'section_1' were restored.838(``key1`` and ``key5``) for ``section_1`` were restored.
839::
944840
945 >>> for section in sorted(config, key=attrgetter('name')):841 >>> for section in sorted(config, key=attrgetter('name')):
946 ... print section.name842 ... print(section.name)
947 section-2.app-b843 section-2.app-b
948 section-5844 section-5
949 section_1845 section_1
@@ -951,46 +847,32 @@
951 section_33847 section_33
952848
953 >>> for key in sorted(config['section_1']):849 >>> for key in sorted(config['section_1']):
954 ... print key, ':', config['section_1'][key]850 ... print(key, ':', config['section_1'][key])
955 key1 : foo851 key1 : foo
956 key2 : sharing is fun852 key2 : sharing is fun
957 key3 : Launchpad&nbsp;rocks853 key3 : Launchpad&nbsp;rocks
958 key4 : F&#028c;k yeah!854 key4 : F&#028c;k yeah!
959 key5 : local value855 key5 : local value
960856
961Call the pop() method with an unknown conf_name raises an error857A Config must have at least one ConfigData in the overlays stack so that it
962858has data. The bottom ConfigData in the overlays was made from the schema's
963 >>> overlays = config.pop('bad-name')859required sections. It cannot be removed by the ``pop()`` method.
964 Traceback (most recent call last):
965 ...
966 NoConfigError: No config with name: bad-name.
967
968A Config must have at least one ConfigData in the overlays stack so that
969it has data. The bottom ConfigData in the overlays was made from the
970schema's required sections. It cannot be removed by the pop() method.
971
972 >>> overlays = config.pop('base.conf')
973 Traceback (most recent call last):
974 ...
975 NoConfigError: Cannot pop the schema's default config.
976860
977If all but the bottom ConfigData is popped from overlays, the extends861If all but the bottom ConfigData is popped from overlays, the extends
978property returns None.862property returns None.
979863
980 >>> overlays = config.pop('shared.conf')864 >>> overlays = config.pop('shared.conf')
981 >>> print config.extends865 >>> print(config.extends)
982 None866 None
983867
984868
985===============================
986Attribute access to config data869Attribute access to config data
987===============================870===============================
988871
989Config provides attribute-based access to its members. So long as the872Config provides attribute-based access to its members. So long as the
990section, category, and key names conform to Python identifier naming873section, category, and key names conform to Python identifier naming rules,
991rules, they can be accessed as attributes. The Python code will not874they can be accessed as attributes. The Python code will not compile, or will
992compile, or will cause a runtime error if the object being accessed has875cause a runtime error if the object being accessed has a bad name.
993a bad name.
994876
995Sections appear to be attributes of the config.877Sections appear to be attributes of the config.
996878
@@ -998,15 +880,15 @@
998 >>> config.section_1 is config['section_1']880 >>> config.section_1 is config['section_1']
999 True881 True
1000882
1001Accessing an unknown section, or a section whose name is not a valid883Accessing an unknown section, or a section whose name is not a valid Python
1002Python identifier will raise an AttributeError.884identifier will raise an AttributeError.
1003885
1004 >>> config.section-5886 >>> config.section-5
1005 Traceback (most recent call last):887 Traceback (most recent call last):
1006 ...888 ...
1007 AttributeError: No section or category named section.889 AttributeError: No section or category named section.
1008890
1009Categories may be accessed as attributes too. The ICategory interface891Categories may be accessed as attributes too. The ICategory interface
1010provides access to its sections as members.892provides access to its sections as members.
1011893
1012 >>> from lazr.config.interfaces import ICategory894 >>> from lazr.config.interfaces import ICategory
@@ -1016,8 +898,8 @@
1016 >>> config_category.app_b is config['section_3.app_b']898 >>> config_category.app_b is config['section_3.app_b']
1017 True899 True
1018900
1019Like a config, a category will raise an AttributeError if it does not901Like a config, a category will raise an AttributeError if it does not have a
1020have a section that matches the identifier name.902section that matches the identifier name.
1021903
1022 >>> config_category.no_such_section904 >>> config_category.no_such_section
1023 Traceback (most recent call last):905 Traceback (most recent call last):
@@ -1026,10 +908,10 @@
1026908
1027Section keys can be accessed directly as members.909Section keys can be accessed directly as members.
1028910
1029 >>> config.section_1.key2911 >>> print(config.section_1.key2)
1030 'sharing is fun'912 sharing is fun
1031 >>> config.section_3.app_b.key2913 >>> print(config.section_3.app_b.key2)
1032 'changed'914 changed
1033915
1034Accessing a non-existent section key as an attribute will raise an916Accessing a non-existent section key as an attribute will raise an
1035AttributeError.917AttributeError.
@@ -1040,27 +922,25 @@
1040 AttributeError: No section key named non_key.922 AttributeError: No section key named non_key.
1041923
1042924
1043====================
1044Implicit data typing925Implicit data typing
1045====================926====================
1046927
1047The ImplicitTypeSchema can create configs that support implicit928The ImplicitTypeSchema can create configs that support implicit datatypes.
1048datatypes. The value of a Section key is automatically converted from929The value of a Section key is automatically converted from ``str`` to the type
1049str to the type the value appears to be. Implicit typing does not add930the value appears to be. Implicit typing does not add any validation support;
1050any validation support; it adds type casting conveniences for the931it adds type casting conveniences for the developer.
1051developer.
1052932
1053An ImplicitTypeSchema can be used to parse the same schema and conf933An ImplicitTypeSchema can be used to parse the same schema and conf files that
1054files that Schema uses.934Schema uses.
1055935
1056 >>> from lazr.config import ImplicitTypeSchema936 >>> from lazr.config import ImplicitTypeSchema
1057
1058 >>> implicit_schema = ImplicitTypeSchema(base_conf)937 >>> implicit_schema = ImplicitTypeSchema(base_conf)
1059 >>> verifyObject(IConfigSchema, implicit_schema)938 >>> verifyObject(IConfigSchema, implicit_schema)
1060 True939 True
1061940
1062The config loaded by ImplicitTypeSchema is the same class with the same941The config loaded by ImplicitTypeSchema is the same class with the same
1063sections as is made by Schema.942sections as is made by Schema.
943::
1064944
1065 >>> implicit_config = implicit_schema.load(local_conf)945 >>> implicit_config = implicit_schema.load(local_conf)
1066 >>> implicit_config946 >>> implicit_config
@@ -1082,8 +962,9 @@
1082 >>> implicit_config['section_3.app_b']962 >>> implicit_config['section_3.app_b']
1083 <lazr.config...ImplicitTypeSection object at ...>963 <lazr.config...ImplicitTypeSection object at ...>
1084964
1085ImplicitTypeSection, in contrast to Section, converts values that965ImplicitTypeSection, in contrast to Section, converts values that appear to be
1086appear to be integer or boolean into ints and bools.966integer or boolean into ints and bools.
967::
1087968
1088 >>> config['section_3.app_b']['key1']969 >>> config['section_3.app_b']['key1']
1089 '17'970 '17'
@@ -1103,11 +984,11 @@
1103 >>> implicit_config['section-2.app-b'].key1984 >>> implicit_config['section-2.app-b'].key1
1104 True985 True
1105986
1106ImplicitTypeSection uses a private method that employs heuristic rules987ImplicitTypeSection uses a private method that employs heuristic rules to
1107to convert strings into simple types. It may return a str, bool, or int.988convert strings into simple types. It may return a str, bool, or int. When
1108When the argument is the word 'true' or 'false' (in any case), a bool is989the argument is the word 'true' or 'false' (in any case), a bool is returned.
1109returned. Values like 'yes', 'no', '0', and '1' are not converted to990Values like 'yes', 'no', '0', and '1' are not converted to bool.
1110bool.991::
1111992
1112 >>> convert = implicit_config['section_1']._convert993 >>> convert = implicit_config['section_1']._convert
1113994
@@ -1118,31 +999,33 @@
1118 >>> convert('tRue')999 >>> convert('tRue')
1119 True1000 True
11201001
1121 >>> convert('yes')1002 >>> print(convert('yes'))
1122 'yes'1003 yes
1123 >>> convert('1')1004 >>> convert('1')
1124 11005 1
1125 >>> convert('True or False')1006 >>> print(convert('True or False'))
1126 'True or False'1007 True or False
11271008
1128When the argument is the word 'none', None is returned. The token in the1009When the argument is the word ``none``, ``None`` is returned. The token in
1129config means the key has no value.1010the config means the key has no value.
11301011::
1131 >>> print convert('none')1012
1132 None1013 >>> print(convert('none'))
1133 >>> print convert('None')1014 None
1134 None1015 >>> print(convert('None'))
1135 >>> print convert('nonE')1016 None
1136 None1017 >>> print(convert('nonE'))
11371018 None
1138 >>> convert('none today')1019
1139 'none today'1020 >>> print(convert('none today'))
1140 >>> convert('nonevident')1021 none today
1141 'nonevident'1022 >>> print(convert('nonevident'))
11421023 nonevident
1143When the argument is an unbroken sequence of numbers, an int is1024
1144returned. The number may have a leading positive or negative. Octal and1025When the argument is an unbroken sequence of numbers, an int is returned. The
1145hex notation is not supported.1026number may have a leading positive or negative. Octal and hex notation is not
1027supported.
1028::
11461029
1147 >>> convert('0')1030 >>> convert('0')
1148 01031 0
@@ -1155,37 +1038,28 @@
1155 >>> convert('0100')1038 >>> convert('0100')
1156 1001039 100
11571040
1158 >>> convert('2001-01-01')1041 >>> print(convert('2001-01-01'))
1159 '2001-01-01'1042 2001-01-01
1160 >>> convert('1000*60*5')1043 >>> print(convert('1000*60*5'))
1161 '1000*60*5'1044 1000*60*5
1162 >>> convert('1000 * 60 * 5')1045 >>> print(convert('1000 * 60 * 5'))
1163 '1000 * 60 * 5'1046 1000 * 60 * 5
1164 >>> convert('1,024')1047 >>> print(convert('1,024'))
1165 '1,024'1048 1,024
1166 >>> convert('0.5')1049 >>> print(convert('0.5'))
1167 '0.5'1050 0.5
1168 >>> convert('0x100')1051 >>> print(convert('0x100'))
1169 '0x100'1052 0x100
11701053
1171Multiline values are always strings, with white space (and line breaks)1054Multiline values are always strings, with white space (and line breaks)
1172removed from the beginning/end.1055removed from the beginning and end.
11731056
1174 >>> convert("""multiline value 11057 >>> print(convert("""multiline value 1
1175 ... multiline value 2""")1058 ... multiline value 2"""))
1176 'multiline value 1\n multiline value 2'1059 multiline value 1
11771060 multiline value 2
1178 >>> convert("""1061
1179 ... multiline value 11062
1180 ... multiline value 2
1181 ... """)
1182 'multiline value 1\n multiline value 2'
1183
1184 >>> implicit_config['section_33'].key2
1185 'multiline value 1\nmultiline value 2'
1186
1187
1188=======================
1189Type conversion helpers1063Type conversion helpers
1190=======================1064=======================
11911065
@@ -1195,10 +1069,10 @@
11951069
11961070
1197Booleans1071Booleans
1198========1072--------
11991073
1200There is a helper for turning various strings into the boolean values True and1074There is a helper for turning various strings into the boolean values ``True``
1201False.1075and ``False``.
12021076
1203 >>> from lazr.config import as_boolean1077 >>> from lazr.config import as_boolean
12041078
@@ -1206,8 +1080,8 @@
1206enable.1080enable.
12071081
1208 >>> for value in ('true', 'yes', 'on', 'enable', 'enabled', '1'):1082 >>> for value in ('true', 'yes', 'on', 'enable', 'enabled', '1'):
1209 ... print value, '->', as_boolean(value)1083 ... print(value, '->', as_boolean(value))
1210 ... print value.upper(), '->', as_boolean(value.upper())1084 ... print(value.upper(), '->', as_boolean(value.upper()))
1211 true -> True1085 true -> True
1212 TRUE -> True1086 TRUE -> True
1213 yes -> True1087 yes -> True
@@ -1225,8 +1099,8 @@
1225disable.1099disable.
12261100
1227 >>> for value in ('false', 'no', 'off', 'disable', 'disabled', '0'):1101 >>> for value in ('false', 'no', 'off', 'disable', 'disabled', '0'):
1228 ... print value, '->', as_boolean(value)1102 ... print(value, '->', as_boolean(value))
1229 ... print value.upper(), '->', as_boolean(value.upper())1103 ... print(value.upper(), '->', as_boolean(value.upper()))
1230 false -> False1104 false -> False
1231 FALSE -> False1105 FALSE -> False
1232 no -> False1106 no -> False
@@ -1249,46 +1123,53 @@
12491123
12501124
1251Host and port1125Host and port
1252=============1126-------------
12531127
1254There is a helper for converting from a host:port string to a 2-tuple of1128There is a helper for converting from a ``host:port`` string to a 2-tuple of
1255(host, port).1129``(host, port)``.
12561130
1257 >>> from lazr.config import as_host_port1131 >>> from lazr.config import as_host_port
1258 >>> as_host_port('host:25')1132 >>> host, port = as_host_port('host:25')
1259 ('host', 25)1133 >>> print(host, port)
1134 host 25
12601135
1261The port string is optional, in which case, port 25 is the default (for1136The port string is optional, in which case, port 25 is the default (for
1262historical reasons).1137historical reasons).
12631138
1264 >>> as_host_port('host')1139 >>> host, port = as_host_port('host')
1265 ('host', 25)1140 >>> print(host, port)
1141 host 25
12661142
1267The default port can be overridden.1143The default port can be overridden.
12681144
1269 >>> as_host_port('host', default_port=22)1145 >>> host, port = as_host_port('host', default_port=22)
1270 ('host', 22)1146 >>> print(host, port)
1147 host 22
12711148
1272The default port is ignored if it is given in the value.1149The default port is ignored if it is given in the value.
12731150
1274 >>> as_host_port('host:80', default_port=22)1151 >>> host, port = as_host_port('host:80', default_port=22)
1275 ('host', 80)1152 >>> print(host, port)
1153 host 80
12761154
1277The host name is also optional, as denoted by a leading colon. When omitted,1155The host name is also optional, as denoted by a leading colon. When omitted,
1278localhost is used.1156localhost is used.
12791157
1280 >>> as_host_port(':80')1158 >>> host, port = as_host_port(':80')
1281 ('localhost', 80)1159 >>> print(host, port)
1160 localhost 80
12821161
1283The default host name can be overridden though.1162The default host name can be overridden though.
12841163
1285 >>> as_host_port(':80', default_host='myhost')1164 >>> host, port = as_host_port(':80', default_host='myhost')
1286 ('myhost', 80)1165 >>> print(host, port)
1166 myhost 80
12871167
1288The default host name is ignored if the value string contains it.1168The default host name is ignored if the value string contains it.
12891169
1290 >>> as_host_port('yourhost:80', default_host='myhost')1170 >>> host, port = as_host_port('yourhost:80', default_host='myhost')
1291 ('yourhost', 80)1171 >>> print(host, port)
1172 yourhost 80
12921173
1293A ValueError occurs if the port number in the configuration value string is1174A ValueError occurs if the port number in the configuration value string is
1294not an integer.1175not an integer.
@@ -1300,10 +1181,10 @@
13001181
13011182
1302User and group1183User and group
1303==============1184--------------
13041185
1305A helper is provided for turning a chown(1)-style user:group specification1186A helper is provided for turning a ``chown(1)``-style ``user:group``
1306into a 2-tuple of the user name and group name.1187specification into a 2-tuple of the user name and group name.
13071188
1308 >>> from lazr.config import as_username_groupname1189 >>> from lazr.config import as_username_groupname
13091190
@@ -1317,14 +1198,16 @@
13171198
1318When both are given, the strings are returned unchanged or validated.1199When both are given, the strings are returned unchanged or validated.
13191200
1320 >>> as_username_groupname('person:group')1201 >>> user, group = as_username_groupname('person:group')
1321 ('person', 'group')1202 >>> print(user, group)
1203 person group
13221204
1323Numeric values can be given, but they are not converted into their symbolic1205Numeric values can be given, but they are not converted into their symbolic
1324names.1206names.
13251207
1326 >>> as_username_groupname('25:26')1208 >>> uid, gid = as_username_groupname('25:26')
1327 ('25', '26')1209 >>> print(uid, gid)
1210 25 26
13281211
1329By default the current user and group names are returned.1212By default the current user and group names are returned.
13301213
@@ -1337,10 +1220,10 @@
13371220
13381221
1339Time intervals1222Time intervals
1340==============1223--------------
13411224
1342One such converter accepts a range of 'time interval specifications', and1225This converter accepts a range of *time interval specifications*, and returns
1343returns a Python timedelta.1226a Python timedelta_.
13441227
1345 >>> from lazr.config import as_timedelta1228 >>> from lazr.config import as_timedelta
13461229
@@ -1349,22 +1232,22 @@
1349 >>> as_timedelta('45s')1232 >>> as_timedelta('45s')
1350 datetime.timedelta(0, 45)1233 datetime.timedelta(0, 45)
13511234
1352The function also accepts suffixes 'm' for minutes...1235The function also accepts suffixes ``m`` for minutes...
13531236
1354 >>> as_timedelta('3m')1237 >>> as_timedelta('3m')
1355 datetime.timedelta(0, 180)1238 datetime.timedelta(0, 180)
13561239
1357...'h' for hours...1240...``h`` for hours...
13581241
1359 >>> as_timedelta('2h')1242 >>> as_timedelta('2h')
1360 datetime.timedelta(0, 7200)1243 datetime.timedelta(0, 7200)
13611244
1362...and 'd' for days...1245...and ``d`` for days...
13631246
1364 >>> as_timedelta('4d')1247 >>> as_timedelta('4d')
1365 datetime.timedelta(4)1248 datetime.timedelta(4)
13661249
1367...and 'w' for weeks.1250...and ``w`` for weeks.
13681251
1369 >>> as_timedelta('4w')1252 >>> as_timedelta('4w')
1370 datetime.timedelta(28)1253 datetime.timedelta(28)
@@ -1381,7 +1264,7 @@
1381 >>> as_timedelta('4w2d9h3s')1264 >>> as_timedelta('4w2d9h3s')
1382 datetime.timedelta(30, 32403)1265 datetime.timedelta(30, 32403)
13831266
1384But doesn't accept 'weird' or duplicate combinations.1267But doesn't accept "weird" or duplicate combinations.
13851268
1386 >>> as_timedelta('3s2s')1269 >>> as_timedelta('3s2s')
1387 Traceback (most recent call last):1270 Traceback (most recent call last):
@@ -1414,10 +1297,10 @@
14141297
14151298
1416Log levels1299Log levels
1417==========1300----------
14181301
1419It's convenient to be able to use symbolic log level names when using1302It's convenient to be able to use symbolic log level names when using
1420lazr.config to configure the Python logger.1303``lazr.config`` to configure the Python logger.
14211304
1422 >>> from lazr.config import as_log_level1305 >>> from lazr.config import as_log_level
14231306
@@ -1425,8 +1308,8 @@
14251308
1426 >>> for value in ('critical', 'error', 'warning', 'info',1309 >>> for value in ('critical', 'error', 'warning', 'info',
1427 ... 'debug', 'notset'):1310 ... 'debug', 'notset'):
1428 ... print value, '->', as_log_level(value)1311 ... print(value, '->', as_log_level(value))
1429 ... print value.upper(), '->', as_log_level(value.upper())1312 ... print(value.upper(), '->', as_log_level(value.upper()))
1430 critical -> 501313 critical -> 50
1431 CRITICAL -> 501314 CRITICAL -> 50
1432 error -> 401315 error -> 40
@@ -1447,7 +1330,7 @@
1447 ...1330 ...
1448 AttributeError: 'module' object has no attribute 'CHEESE'1331 AttributeError: 'module' object has no attribute 'CHEESE'
14491332
1450===============1333
1451Other Documents1334Other Documents
1452===============1335===============
14531336
@@ -1456,3 +1339,5 @@
14561339
1457 *1340 *
1458 docs/*1341 docs/*
1342
1343.. _timedelta: http://docs.python.org/3/library/datetime.html#timedelta-objects
14591344
=== added file 'lazr/config/docs/usage_fixture.py'
--- lazr/config/docs/usage_fixture.py 1970-01-01 00:00:00 +0000
+++ lazr/config/docs/usage_fixture.py 2013-01-10 15:47:20 +0000
@@ -0,0 +1,27 @@
1# Copyright 2009-2013 Canonical Ltd. All rights reserved.
2#
3# This file is part of lazr.smtptest
4#
5# lazr.smtptest is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Lesser General Public License as published by
7# the Free Software Foundation, version 3 of the License.
8#
9# lazr.smtptest is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
12# License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with lazr.smtptest. If not, see <http://www.gnu.org/licenses/>.
16
17"""Doctest fixtures for running under nose."""
18
19from __future__ import absolute_import, print_function, unicode_literals
20
21__metaclass__ = type
22__all__ = [
23 'globs',
24 ]
25
26
27from lazr.config.docs.fixture import globs
028
=== modified file 'lazr/config/interfaces.py'
--- src/lazr/config/interfaces.py 2009-03-24 17:31:47 +0000
+++ lazr/config/interfaces.py 2013-01-10 15:47:20 +0000
@@ -1,4 +1,4 @@
1# Copyright 2007-2009 Canonical Ltd. All rights reserved.1# Copyright 2007-2013 Canonical Ltd. All rights reserved.
2#2#
3# This file is part of lazr.config3# This file is part of lazr.config
4#4#
@@ -17,8 +17,9 @@
17# pylint: disable-msg=E0211,E0213,W023117# pylint: disable-msg=E0211,E0213,W0231
18"""Interfaces for process configuration.."""18"""Interfaces for process configuration.."""
1919
20from __future__ import absolute_import, print_function, unicode_literals
21
20__metaclass__ = type22__metaclass__ = type
21
22__all__ = [23__all__ = [
23 'ConfigErrors',24 'ConfigErrors',
24 'ConfigSchemaError',25 'ConfigSchemaError',
@@ -37,12 +38,8 @@
37 'UnknownKeyError',38 'UnknownKeyError',
38 'UnknownSectionError']39 'UnknownSectionError']
3940
40from warnings import filterwarnings
41from zope.interface import Interface, Attribute41from zope.interface import Interface, Attribute
4242
43# Ignore Python 2.6 deprecation warnings.
44filterwarnings('ignore', category=DeprecationWarning, module=r'lazr\.config')
45
4643
47class ConfigSchemaError(Exception):44class ConfigSchemaError(Exception):
48 """A base class of all `IConfigSchema` errors."""45 """A base class of all `IConfigSchema` errors."""
4946
=== modified file 'lazr/config/tests/__init__.py'
--- src/lazr/config/tests/__init__.py 2009-03-24 17:31:47 +0000
+++ lazr/config/tests/__init__.py 2013-01-10 15:47:20 +0000
@@ -1,17 +0,0 @@
1# Copyright 2007-2009 Canonical Ltd. All rights reserved.
2#
3# This file is part of lazr.config
4#
5# lazr.config is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Lesser General Public License as published by
7# the Free Software Foundation, version 3 of the License.
8#
9# lazr.config is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
12# License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with lazr.config. If not, see <http://www.gnu.org/licenses/>.
16
17"""Test package for lazr.config."""
180
=== added file 'lazr/config/tests/test_config.py'
--- lazr/config/tests/test_config.py 1970-01-01 00:00:00 +0000
+++ lazr/config/tests/test_config.py 2013-01-10 15:47:20 +0000
@@ -0,0 +1,205 @@
1# Copyright 2008-2013 Canonical Ltd. All rights reserved.
2#
3# This file is part of lazr.config.
4#
5# lazr.config is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Lesser General Public License as published by
7# the Free Software Foundation, version 3 of the License.
8#
9# lazr.config is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
12# License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with lazr.config. If not, see <http://www.gnu.org/licenses/>.
16
17"""Tests of lazr.config."""
18
19from __future__ import absolute_import, print_function, unicode_literals
20
21__metaclass__ = type
22__all__ = [
23 'TestConfig',
24 ]
25
26
27import unittest
28import pkg_resources
29try:
30 from configparser import MissingSectionHeaderError, NoSectionError
31except ImportError:
32 # Python 2
33 from ConfigParser import MissingSectionHeaderError, NoSectionError
34try:
35 from io import StringIO
36except ImportError:
37 # Python 2
38 from StringIO import StringIO
39
40from operator import attrgetter
41from zope.interface.exceptions import DoesNotImplement
42from zope.interface.verify import verifyObject
43
44from lazr.config import ConfigSchema, ImplicitTypeSchema
45from lazr.config.interfaces import (
46 ConfigErrors, IStackableConfig, InvalidSectionNameError, NoCategoryError,
47 NoConfigError, RedefinedSectionError, UnknownKeyError,
48 UnknownSectionError)
49
50
51class TestConfig(unittest.TestCase):
52 def setUp(self):
53 # Python 2.6 does not have assertMultilineEqual
54 self.meq = getattr(self, 'assertMultiLineEqual', self.assertEqual)
55
56 def _testfile(self, conf_file):
57 return pkg_resources.resource_filename(
58 'lazr.config.tests.testdata', conf_file)
59
60 def test_missing_category(self):
61 schema = ConfigSchema(self._testfile('base.conf'))
62 self.assertRaises(NoCategoryError, schema.getByCategory, 'non-section')
63
64 def test_missing_file(self):
65 self.assertRaises(IOError, ConfigSchema, '/does/not/exist')
66
67 def test_must_be_ascii(self):
68 self.assertRaises(UnicodeError,
69 ConfigSchema, self._testfile('bad-nonascii.conf'))
70
71 def test_missing_schema_section(self):
72 schema = ConfigSchema(self._testfile('base.conf'))
73 self.assertRaises(NoSectionError, schema.__getitem__, 'section-4')
74
75 def test_missing_header_section(self):
76 self.assertRaises(MissingSectionHeaderError,
77 ConfigSchema, self._testfile('bad-sectionless.conf'))
78
79 def test_redefined_section(self):
80 self.assertRaises(RedefinedSectionError,
81 ConfigSchema,
82 self._testfile('bad-redefined-section.conf'))
83 # XXX sinzui 2007-12-13:
84 # ConfigSchema should raise RedefinedKeyError when a section redefines
85 # a key.
86
87 def test_invalid_section_name(self):
88 self.assertRaises(InvalidSectionNameError,
89 ConfigSchema,
90 self._testfile('bad-invalid-name.conf'))
91
92 def test_invalid_characters(self):
93 self.assertRaises(InvalidSectionNameError,
94 ConfigSchema,
95 self._testfile('bad-invalid-name-chars.conf'))
96
97 def test_load_missing_file(self):
98 schema = ConfigSchema(self._testfile('base.conf'))
99 self.assertRaises(IOError, schema.load, '/no/such/file.conf')
100
101 def test_no_name_argument(self):
102 config = """
103[meta]
104metakey: unsupported
105[unknown-section]
106key1 = value1
107[section_1]
108keyn: unknown key
109key1: bad character in caf\xc3)
110[section_3.template]
111key1: schema suffixes are not permitted
112"""
113 schema = ConfigSchema(self._testfile('base.conf'))
114 self.assertRaises(AttributeError, schema.loadFile, StringIO(config))
115
116 def test_missing_section(self):
117 schema = ConfigSchema(self._testfile('base.conf'))
118 config = schema.load(self._testfile('local.conf'))
119 self.assertRaises(NoSectionError, config.__getitem__, 'section-4')
120
121 def test_undeclared_optional_section(self):
122 schema = ConfigSchema(self._testfile('base.conf'))
123 config = schema.load(self._testfile('local.conf'))
124 self.assertRaises(NoSectionError,
125 config.__getitem__, 'section_3.app_a')
126
127 def test_nonexistent_category_name(self):
128 schema = ConfigSchema(self._testfile('base.conf'))
129 config = schema.load(self._testfile('local.conf'))
130 self.assertRaises(NoCategoryError,
131 config.getByCategory, 'non-section')
132
133 def test_all_config_errors(self):
134 schema = ConfigSchema(self._testfile('base.conf'))
135 config = schema.loadFile(StringIO("""
136[meta]
137metakey: unsupported
138[unknown-section]
139key1 = value1
140[section_1]
141keyn: unknown key
142key1: bad character in caf\xc3)
143[section_3.template]
144key1: schema suffixes are not permitted
145"""), 'bad config')
146 try:
147 config.validate()
148 except ConfigErrors as errors:
149 sorted_errors = sorted(
150 errors.errors, key=attrgetter('__class__.__name__'))
151 self.assertEqual(str(errors),
152 'ConfigErrors: bad config is not valid.')
153 else:
154 self.fail('ConfigErrors expected')
155 self.assertEqual(len(sorted_errors), 4)
156 self.assertEqual([error.__class__ for error in sorted_errors],
157 [UnicodeEncodeError, UnknownKeyError,
158 UnknownKeyError, UnknownSectionError])
159
160 def test_not_stackable(self):
161 schema = ConfigSchema(self._testfile('base.conf'))
162 config = schema.load(self._testfile('local.conf'))
163 self.assertRaises(DoesNotImplement,
164 verifyObject, IStackableConfig, config.extends)
165
166 def test_bad_pop(self):
167 schema = ConfigSchema(self._testfile('base.conf'))
168 config = schema.load(self._testfile('local.conf'))
169 config.push('one', '')
170 config.push('two', '')
171 self.assertRaises(NoConfigError, config.pop, 'bad-name')
172
173 def test_cannot_pop_bottom(self):
174 schema = ConfigSchema(self._testfile('base.conf'))
175 config = schema.load(self._testfile('local.conf'))
176 config.pop('local.conf')
177 self.assertRaises(NoConfigError, config.pop, 'base.conf')
178
179 def test_multiline_preserves_indentation(self):
180 schema = ImplicitTypeSchema(self._testfile('base.conf'))
181 config = schema.load(self._testfile('local.conf'))
182 convert = config['section_1']._convert
183 orig = """\
184multiline value 1
185 multiline value 2"""
186 new = convert(orig)
187 self.meq(new, orig)
188
189 def test_multiline_strips_leading_and_trailing_whitespace(self):
190 schema = ImplicitTypeSchema(self._testfile('base.conf'))
191 config = schema.load(self._testfile('local.conf'))
192 convert = config['section_1']._convert
193 orig = """
194 multiline value 1
195 multiline value 2
196 """
197 new = convert(orig)
198 self.meq(new, orig.strip())
199
200 def test_multiline_key(self):
201 schema = ImplicitTypeSchema(self._testfile('base.conf'))
202 config = schema.load(self._testfile('local.conf'))
203 self.meq(config['section_33'].key2, """\
204multiline value 1
205multiline value 2""")
0206
=== added file 'lazr/config/tests/testdata/__init__.py'
=== modified file 'lazr/config/version.txt'
--- src/lazr/config/version.txt 2009-08-25 18:56:38 +0000
+++ lazr/config/version.txt 2013-01-10 15:47:20 +0000
@@ -1,1 +1,1 @@
11.1.312.0
22
=== added file 'setup.cfg'
--- setup.cfg 1970-01-01 00:00:00 +0000
+++ setup.cfg 2013-01-10 15:47:20 +0000
@@ -0,0 +1,9 @@
1[nosetests]
2verbosity=3
3with-coverage=1
4with-doctest=1
5doctest-extension=.rst
6doctest-options=+ELLIPSIS,+NORMALIZE_WHITESPACE,+REPORT_NDIFF
7doctest-fixtures=_fixture
8cover-package=lazr.config
9pdb=1
010
=== modified file 'setup.py'
--- setup.py 2009-08-25 13:58:54 +0000
+++ setup.py 2013-01-10 15:47:20 +0000
@@ -1,6 +1,4 @@
1#!/usr/bin/env python1# Copyright 2008-2013 Canonical Ltd. All rights reserved.
2
3# Copyright 2008-2009 Canonical Ltd. All rights reserved.
4#2#
5# This file is part of lazr.config.3# This file is part of lazr.config.
6#4#
@@ -16,10 +14,9 @@
16# You should have received a copy of the GNU Lesser General Public License14# You should have received a copy of the GNU Lesser General Public License
17# along with lazr.config. If not, see <http://www.gnu.org/licenses/>.15# along with lazr.config. If not, see <http://www.gnu.org/licenses/>.
1816
19import ez_setup17import distribute_setup
20ez_setup.use_setuptools()18distribute_setup.use_setuptools()
2119
22import sys
23from setuptools import setup, find_packages20from setuptools import setup, find_packages
2421
25# generic helpers primarily for the long_description22# generic helpers primarily for the long_description
@@ -37,22 +34,21 @@
37# end generic helpers34# end generic helpers
3835
3936
40__version__ = open("src/lazr/config/version.txt").read().strip()37__version__ = open("lazr/config/version.txt").read().strip()
4138
42setup(39setup(
43 name='lazr.config',40 name='lazr.config',
44 version=__version__,41 version=__version__,
45 namespace_packages=['lazr'],42 namespace_packages=['lazr'],
46 packages=find_packages('src'),43 packages=find_packages(),
47 package_dir={'':'src'},
48 include_package_data=True,44 include_package_data=True,
49 zip_safe=False,45 zip_safe=False,
50 maintainer='LAZR Developers',46 maintainer='LAZR Developers',
51 maintainer_email='lazr-developers@lists.launchpad.net',47 maintainer_email='lazr-developers@lists.launchpad.net',
52 description=open('README.txt').readline().strip(),48 description=open('README.rst').readline().strip(),
53 long_description=generate(49 long_description=generate(
54 'src/lazr/config/README.txt',50 'lazr/config/README.rst',
55 'src/lazr/config/CHANGES.txt'),51 'lazr/config/CHANGES.rst'),
56 license='LGPL v3',52 license='LGPL v3',
57 install_requires=[53 install_requires=[
58 'setuptools',54 'setuptools',
@@ -60,16 +56,16 @@
60 'lazr.delegates',56 'lazr.delegates',
61 ],57 ],
62 url='https://launchpad.net/lazr.config',58 url='https://launchpad.net/lazr.config',
63 download_url= 'https://launchpad.net/lazr.config/+download',59 download_url='https://launchpad.net/lazr.config/+download',
64 classifiers=[60 classifiers=[
65 "Development Status :: 5 - Production/Stable",61 "Development Status :: 5 - Production/Stable",
66 "Intended Audience :: Developers",62 "Intended Audience :: Developers",
67 "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",63 "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
68 "Operating System :: OS Independent",64 "Operating System :: OS Independent",
69 "Programming Language :: Python"],65 'Programming Language :: Python',
70 extras_require=dict(66 'Programming Language :: Python :: 2.6',
71 docs=['Sphinx',67 'Programming Language :: Python :: 2.7',
72 'z3c.recipe.sphinxdoc']68 'Programming Language :: Python :: 3',
73 ),69 ],
74 test_suite='lazr.config.tests',70 test_suite='lazr.config.tests',
75 )71 )
7672
=== removed directory 'src'
=== removed file 'src/lazr/config/NEWS.txt'
--- src/lazr/config/NEWS.txt 2009-08-25 18:56:38 +0000
+++ src/lazr/config/NEWS.txt 1970-01-01 00:00:00 +0000
@@ -1,15 +0,0 @@
11.1.3 (2009-08-25)
2==================
3
4Fixed a build problem.
5
61.1.2 (2009-08-25)
7==================
8
9Got rid of a sys.path hack.
10
11
121.1.1
13=====
14
15Initial release
160
=== removed file 'src/lazr/config/tests/test_docs.py'
--- src/lazr/config/tests/test_docs.py 2009-03-24 17:31:47 +0000
+++ src/lazr/config/tests/test_docs.py 1970-01-01 00:00:00 +0000
@@ -1,51 +0,0 @@
1# Copyright 2009 Canonical Ltd. All rights reserved.
2#
3# This file is part of lazr.config
4#
5# lazr.config is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Lesser General Public License as published by
7# the Free Software Foundation, version 3 of the License.
8#
9# lazr.config is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
12# License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with lazr.config. If not, see <http://www.gnu.org/licenses/>.
16"Test harness for doctests."
17
18# pylint: disable-msg=E0611,W0142
19
20__metaclass__ = type
21__all__ = [
22 'additional_tests',
23 ]
24
25import atexit
26import doctest
27import os
28from pkg_resources import (
29 resource_filename, resource_exists, resource_listdir, cleanup_resources)
30import unittest
31
32DOCTEST_FLAGS = (
33 doctest.ELLIPSIS |
34 doctest.NORMALIZE_WHITESPACE |
35 doctest.REPORT_NDIFF)
36
37
38def additional_tests():
39 "Run the doc tests (README.txt and docs/*, if any exist)"
40 doctest_files = [
41 os.path.abspath(resource_filename('lazr.config', 'README.txt'))]
42 if resource_exists('lazr.config', 'docs'):
43 for name in resource_listdir('lazr.config', 'docs'):
44 if name.endswith('.txt'):
45 doctest_files.append(
46 os.path.abspath(
47 resource_filename('lazr.config', 'docs/%s' % name)))
48 kwargs = dict(module_relative=False, optionflags=DOCTEST_FLAGS)
49 atexit.register(cleanup_resources)
50 return unittest.TestSuite((
51 doctest.DocFileSuite(*doctest_files, **kwargs)))

Subscribers

People subscribed via source and target branches