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
1=== renamed file 'README.txt' => 'README.rst'
2=== removed directory '_bootstrap'
3=== removed file '_bootstrap/COPYRIGHT.txt'
4--- _bootstrap/COPYRIGHT.txt 2009-03-24 17:31:47 +0000
5+++ _bootstrap/COPYRIGHT.txt 1970-01-01 00:00:00 +0000
6@@ -1,9 +0,0 @@
7-Copyright (c) 2004-2009 Zope Corporation and Contributors.
8-All Rights Reserved.
9-
10-This software is subject to the provisions of the Zope Public License,
11-Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
12-THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
13-WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14-WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
15-FOR A PARTICULAR PURPOSE.
16
17=== removed file '_bootstrap/LICENSE.txt'
18--- _bootstrap/LICENSE.txt 2009-03-24 17:31:47 +0000
19+++ _bootstrap/LICENSE.txt 1970-01-01 00:00:00 +0000
20@@ -1,54 +0,0 @@
21-Zope Public License (ZPL) Version 2.1
22--------------------------------------
23-
24-A copyright notice accompanies this license document that
25-identifies the copyright holders.
26-
27-This license has been certified as open source. It has also
28-been designated as GPL compatible by the Free Software
29-Foundation (FSF).
30-
31-Redistribution and use in source and binary forms, with or
32-without modification, are permitted provided that the
33-following conditions are met:
34-
35-1. Redistributions in source code must retain the
36- accompanying copyright notice, this list of conditions,
37- and the following disclaimer.
38-
39-2. Redistributions in binary form must reproduce the accompanying
40- copyright notice, this list of conditions, and the
41- following disclaimer in the documentation and/or other
42- materials provided with the distribution.
43-
44-3. Names of the copyright holders must not be used to
45- endorse or promote products derived from this software
46- without prior written permission from the copyright
47- holders.
48-
49-4. The right to distribute this software or to use it for
50- any purpose does not give you the right to use
51- Servicemarks (sm) or Trademarks (tm) of the copyright
52- holders. Use of them is covered by separate agreement
53- with the copyright holders.
54-
55-5. If any files are modified, you must cause the modified
56- files to carry prominent notices stating that you changed
57- the files and the date of any change.
58-
59-Disclaimer
60-
61- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS''
62- AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
63- NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
64- AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
65- NO EVENT SHALL THE COPYRIGHT HOLDERS BE
66- LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
67- EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
68- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
69- LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
70- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
71- CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
72- OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
73- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
74- DAMAGE.
75\ No newline at end of file
76
77=== removed file '_bootstrap/bootstrap.py'
78--- _bootstrap/bootstrap.py 2009-03-24 17:31:47 +0000
79+++ _bootstrap/bootstrap.py 1970-01-01 00:00:00 +0000
80@@ -1,77 +0,0 @@
81-##############################################################################
82-#
83-# Copyright (c) 2006 Zope Corporation and Contributors.
84-# All Rights Reserved.
85-#
86-# This software is subject to the provisions of the Zope Public License,
87-# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
88-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
89-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
90-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
91-# FOR A PARTICULAR PURPOSE.
92-#
93-##############################################################################
94-"""Bootstrap a buildout-based project
95-
96-Simply run this script in a directory containing a buildout.cfg.
97-The script accepts buildout command-line options, so you can
98-use the -c option to specify an alternate configuration file.
99-
100-$Id$
101-"""
102-
103-import os, shutil, sys, tempfile, urllib2
104-
105-tmpeggs = tempfile.mkdtemp()
106-
107-is_jython = sys.platform.startswith('java')
108-
109-try:
110- import pkg_resources
111-except ImportError:
112- ez = {}
113- exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
114- ).read() in ez
115- ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
116-
117- import pkg_resources
118-
119-if sys.platform == 'win32':
120- def quote(c):
121- if ' ' in c:
122- return '"%s"' % c # work around spawn lamosity on windows
123- else:
124- return c
125-else:
126- def quote (c):
127- return c
128-
129-cmd = 'from setuptools.command.easy_install import main; main()'
130-ws = pkg_resources.working_set
131-
132-if is_jython:
133- import subprocess
134-
135- assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd',
136- quote(tmpeggs), 'zc.buildout'],
137- env=dict(os.environ,
138- PYTHONPATH=
139- ws.find(pkg_resources.Requirement.parse('setuptools')).location
140- ),
141- ).wait() == 0
142-
143-else:
144- assert os.spawnle(
145- os.P_WAIT, sys.executable, quote (sys.executable),
146- '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout',
147- dict(os.environ,
148- PYTHONPATH=
149- ws.find(pkg_resources.Requirement.parse('setuptools')).location
150- ),
151- ) == 0
152-
153-ws.add_entry(tmpeggs)
154-ws.require('zc.buildout')
155-import zc.buildout.buildout
156-zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap'])
157-shutil.rmtree(tmpeggs)
158
159=== removed symlink 'bootstrap.py'
160=== target was u'_bootstrap/bootstrap.py'
161=== removed file 'buildout.cfg'
162--- buildout.cfg 2009-03-24 17:36:13 +0000
163+++ buildout.cfg 1970-01-01 00:00:00 +0000
164@@ -1,31 +0,0 @@
165-[buildout]
166-parts =
167- interpreter
168- test
169- docs
170- tags
171-unzip = true
172-
173-develop = .
174-
175-[test]
176-recipe = zc.recipe.testrunner
177-eggs = lazr.config
178-defaults = '--tests-pattern ^tests --exit-with-status --suite-name additional_tests'.split()
179-
180-[docs]
181-recipe = z3c.recipe.sphinxdoc
182-eggs = lazr.config [docs]
183-index-doc = README
184-default.css =
185-layout.html =
186-
187-[interpreter]
188-recipe = zc.recipe.egg
189-interpreter=py
190-eggs = lazr.config
191- docutils
192-
193-[tags]
194-recipe = z3c.recipe.tag:tags
195-eggs = lazr.config
196
197=== added file 'distribute_setup.py'
198--- distribute_setup.py 1970-01-01 00:00:00 +0000
199+++ distribute_setup.py 2013-01-10 15:47:20 +0000
200@@ -0,0 +1,546 @@
201+#!python
202+"""Bootstrap distribute installation
203+
204+If you want to use setuptools in your package's setup.py, just include this
205+file in the same directory with it, and add this to the top of your setup.py::
206+
207+ from distribute_setup import use_setuptools
208+ use_setuptools()
209+
210+If you want to require a specific version of setuptools, set a download
211+mirror, or use an alternate download directory, you can do so by supplying
212+the appropriate options to ``use_setuptools()``.
213+
214+This file can also be run as a script to install or upgrade setuptools.
215+"""
216+import os
217+import shutil
218+import sys
219+import time
220+import fnmatch
221+import tempfile
222+import tarfile
223+import optparse
224+
225+from distutils import log
226+
227+try:
228+ from site import USER_SITE
229+except ImportError:
230+ USER_SITE = None
231+
232+try:
233+ import subprocess
234+
235+ def _python_cmd(*args):
236+ args = (sys.executable,) + args
237+ return subprocess.call(args) == 0
238+
239+except ImportError:
240+ # will be used for python 2.3
241+ def _python_cmd(*args):
242+ args = (sys.executable,) + args
243+ # quoting arguments if windows
244+ if sys.platform == 'win32':
245+ def quote(arg):
246+ if ' ' in arg:
247+ return '"%s"' % arg
248+ return arg
249+ args = [quote(arg) for arg in args]
250+ return os.spawnl(os.P_WAIT, sys.executable, *args) == 0
251+
252+DEFAULT_VERSION = "0.6.34"
253+DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/"
254+SETUPTOOLS_FAKED_VERSION = "0.6c11"
255+
256+SETUPTOOLS_PKG_INFO = """\
257+Metadata-Version: 1.0
258+Name: setuptools
259+Version: %s
260+Summary: xxxx
261+Home-page: xxx
262+Author: xxx
263+Author-email: xxx
264+License: xxx
265+Description: xxx
266+""" % SETUPTOOLS_FAKED_VERSION
267+
268+
269+def _install(tarball, install_args=()):
270+ # extracting the tarball
271+ tmpdir = tempfile.mkdtemp()
272+ log.warn('Extracting in %s', tmpdir)
273+ old_wd = os.getcwd()
274+ try:
275+ os.chdir(tmpdir)
276+ tar = tarfile.open(tarball)
277+ _extractall(tar)
278+ tar.close()
279+
280+ # going in the directory
281+ subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
282+ os.chdir(subdir)
283+ log.warn('Now working in %s', subdir)
284+
285+ # installing
286+ log.warn('Installing Distribute')
287+ if not _python_cmd('setup.py', 'install', *install_args):
288+ log.warn('Something went wrong during the installation.')
289+ log.warn('See the error message above.')
290+ # exitcode will be 2
291+ return 2
292+ finally:
293+ os.chdir(old_wd)
294+ shutil.rmtree(tmpdir)
295+
296+
297+def _build_egg(egg, tarball, to_dir):
298+ # extracting the tarball
299+ tmpdir = tempfile.mkdtemp()
300+ log.warn('Extracting in %s', tmpdir)
301+ old_wd = os.getcwd()
302+ try:
303+ os.chdir(tmpdir)
304+ tar = tarfile.open(tarball)
305+ _extractall(tar)
306+ tar.close()
307+
308+ # going in the directory
309+ subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
310+ os.chdir(subdir)
311+ log.warn('Now working in %s', subdir)
312+
313+ # building an egg
314+ log.warn('Building a Distribute egg in %s', to_dir)
315+ _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
316+
317+ finally:
318+ os.chdir(old_wd)
319+ shutil.rmtree(tmpdir)
320+ # returning the result
321+ log.warn(egg)
322+ if not os.path.exists(egg):
323+ raise IOError('Could not build the egg.')
324+
325+
326+def _do_download(version, download_base, to_dir, download_delay):
327+ egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg'
328+ % (version, sys.version_info[0], sys.version_info[1]))
329+ if not os.path.exists(egg):
330+ tarball = download_setuptools(version, download_base,
331+ to_dir, download_delay)
332+ _build_egg(egg, tarball, to_dir)
333+ sys.path.insert(0, egg)
334+ import setuptools
335+ setuptools.bootstrap_install_from = egg
336+
337+
338+def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
339+ to_dir=os.curdir, download_delay=15, no_fake=True):
340+ # making sure we use the absolute path
341+ to_dir = os.path.abspath(to_dir)
342+ was_imported = 'pkg_resources' in sys.modules or \
343+ 'setuptools' in sys.modules
344+ try:
345+ try:
346+ import pkg_resources
347+ if not hasattr(pkg_resources, '_distribute'):
348+ if not no_fake:
349+ _fake_setuptools()
350+ raise ImportError
351+ except ImportError:
352+ return _do_download(version, download_base, to_dir, download_delay)
353+ try:
354+ pkg_resources.require("distribute>=" + version)
355+ return
356+ except pkg_resources.VersionConflict:
357+ e = sys.exc_info()[1]
358+ if was_imported:
359+ sys.stderr.write(
360+ "The required version of distribute (>=%s) is not available,\n"
361+ "and can't be installed while this script is running. Please\n"
362+ "install a more recent version first, using\n"
363+ "'easy_install -U distribute'."
364+ "\n\n(Currently using %r)\n" % (version, e.args[0]))
365+ sys.exit(2)
366+ else:
367+ del pkg_resources, sys.modules['pkg_resources'] # reload ok
368+ return _do_download(version, download_base, to_dir,
369+ download_delay)
370+ except pkg_resources.DistributionNotFound:
371+ return _do_download(version, download_base, to_dir,
372+ download_delay)
373+ finally:
374+ if not no_fake:
375+ _create_fake_setuptools_pkg_info(to_dir)
376+
377+
378+def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
379+ to_dir=os.curdir, delay=15):
380+ """Download distribute from a specified location and return its filename
381+
382+ `version` should be a valid distribute version number that is available
383+ as an egg for download under the `download_base` URL (which should end
384+ with a '/'). `to_dir` is the directory where the egg will be downloaded.
385+ `delay` is the number of seconds to pause before an actual download
386+ attempt.
387+ """
388+ # making sure we use the absolute path
389+ to_dir = os.path.abspath(to_dir)
390+ try:
391+ from urllib.request import urlopen
392+ except ImportError:
393+ from urllib2 import urlopen
394+ tgz_name = "distribute-%s.tar.gz" % version
395+ url = download_base + tgz_name
396+ saveto = os.path.join(to_dir, tgz_name)
397+ src = dst = None
398+ if not os.path.exists(saveto): # Avoid repeated downloads
399+ try:
400+ log.warn("Downloading %s", url)
401+ src = urlopen(url)
402+ # Read/write all in one block, so we don't create a corrupt file
403+ # if the download is interrupted.
404+ data = src.read()
405+ dst = open(saveto, "wb")
406+ dst.write(data)
407+ finally:
408+ if src:
409+ src.close()
410+ if dst:
411+ dst.close()
412+ return os.path.realpath(saveto)
413+
414+
415+def _no_sandbox(function):
416+ def __no_sandbox(*args, **kw):
417+ try:
418+ from setuptools.sandbox import DirectorySandbox
419+ if not hasattr(DirectorySandbox, '_old'):
420+ def violation(*args):
421+ pass
422+ DirectorySandbox._old = DirectorySandbox._violation
423+ DirectorySandbox._violation = violation
424+ patched = True
425+ else:
426+ patched = False
427+ except ImportError:
428+ patched = False
429+
430+ try:
431+ return function(*args, **kw)
432+ finally:
433+ if patched:
434+ DirectorySandbox._violation = DirectorySandbox._old
435+ del DirectorySandbox._old
436+
437+ return __no_sandbox
438+
439+
440+def _patch_file(path, content):
441+ """Will backup the file then patch it"""
442+ f = open(path)
443+ existing_content = f.read()
444+ f.close()
445+ if existing_content == content:
446+ # already patched
447+ log.warn('Already patched.')
448+ return False
449+ log.warn('Patching...')
450+ _rename_path(path)
451+ f = open(path, 'w')
452+ try:
453+ f.write(content)
454+ finally:
455+ f.close()
456+ return True
457+
458+_patch_file = _no_sandbox(_patch_file)
459+
460+
461+def _same_content(path, content):
462+ f = open(path)
463+ existing_content = f.read()
464+ f.close()
465+ return existing_content == content
466+
467+
468+def _rename_path(path):
469+ new_name = path + '.OLD.%s' % time.time()
470+ log.warn('Renaming %s to %s', path, new_name)
471+ os.rename(path, new_name)
472+ return new_name
473+
474+
475+def _remove_flat_installation(placeholder):
476+ if not os.path.isdir(placeholder):
477+ log.warn('Unkown installation at %s', placeholder)
478+ return False
479+ found = False
480+ for file in os.listdir(placeholder):
481+ if fnmatch.fnmatch(file, 'setuptools*.egg-info'):
482+ found = True
483+ break
484+ if not found:
485+ log.warn('Could not locate setuptools*.egg-info')
486+ return
487+
488+ log.warn('Moving elements out of the way...')
489+ pkg_info = os.path.join(placeholder, file)
490+ if os.path.isdir(pkg_info):
491+ patched = _patch_egg_dir(pkg_info)
492+ else:
493+ patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO)
494+
495+ if not patched:
496+ log.warn('%s already patched.', pkg_info)
497+ return False
498+ # now let's move the files out of the way
499+ for element in ('setuptools', 'pkg_resources.py', 'site.py'):
500+ element = os.path.join(placeholder, element)
501+ if os.path.exists(element):
502+ _rename_path(element)
503+ else:
504+ log.warn('Could not find the %s element of the '
505+ 'Setuptools distribution', element)
506+ return True
507+
508+_remove_flat_installation = _no_sandbox(_remove_flat_installation)
509+
510+
511+def _after_install(dist):
512+ log.warn('After install bootstrap.')
513+ placeholder = dist.get_command_obj('install').install_purelib
514+ _create_fake_setuptools_pkg_info(placeholder)
515+
516+
517+def _create_fake_setuptools_pkg_info(placeholder):
518+ if not placeholder or not os.path.exists(placeholder):
519+ log.warn('Could not find the install location')
520+ return
521+ pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1])
522+ setuptools_file = 'setuptools-%s-py%s.egg-info' % \
523+ (SETUPTOOLS_FAKED_VERSION, pyver)
524+ pkg_info = os.path.join(placeholder, setuptools_file)
525+ if os.path.exists(pkg_info):
526+ log.warn('%s already exists', pkg_info)
527+ return
528+
529+ log.warn('Creating %s', pkg_info)
530+ try:
531+ f = open(pkg_info, 'w')
532+ except EnvironmentError:
533+ log.warn("Don't have permissions to write %s, skipping", pkg_info)
534+ return
535+ try:
536+ f.write(SETUPTOOLS_PKG_INFO)
537+ finally:
538+ f.close()
539+
540+ pth_file = os.path.join(placeholder, 'setuptools.pth')
541+ log.warn('Creating %s', pth_file)
542+ f = open(pth_file, 'w')
543+ try:
544+ f.write(os.path.join(os.curdir, setuptools_file))
545+ finally:
546+ f.close()
547+
548+_create_fake_setuptools_pkg_info = _no_sandbox(
549+ _create_fake_setuptools_pkg_info
550+)
551+
552+
553+def _patch_egg_dir(path):
554+ # let's check if it's already patched
555+ pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
556+ if os.path.exists(pkg_info):
557+ if _same_content(pkg_info, SETUPTOOLS_PKG_INFO):
558+ log.warn('%s already patched.', pkg_info)
559+ return False
560+ _rename_path(path)
561+ os.mkdir(path)
562+ os.mkdir(os.path.join(path, 'EGG-INFO'))
563+ pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
564+ f = open(pkg_info, 'w')
565+ try:
566+ f.write(SETUPTOOLS_PKG_INFO)
567+ finally:
568+ f.close()
569+ return True
570+
571+_patch_egg_dir = _no_sandbox(_patch_egg_dir)
572+
573+
574+def _before_install():
575+ log.warn('Before install bootstrap.')
576+ _fake_setuptools()
577+
578+
579+def _under_prefix(location):
580+ if 'install' not in sys.argv:
581+ return True
582+ args = sys.argv[sys.argv.index('install') + 1:]
583+ for index, arg in enumerate(args):
584+ for option in ('--root', '--prefix'):
585+ if arg.startswith('%s=' % option):
586+ top_dir = arg.split('root=')[-1]
587+ return location.startswith(top_dir)
588+ elif arg == option:
589+ if len(args) > index:
590+ top_dir = args[index + 1]
591+ return location.startswith(top_dir)
592+ if arg == '--user' and USER_SITE is not None:
593+ return location.startswith(USER_SITE)
594+ return True
595+
596+
597+def _fake_setuptools():
598+ log.warn('Scanning installed packages')
599+ try:
600+ import pkg_resources
601+ except ImportError:
602+ # we're cool
603+ log.warn('Setuptools or Distribute does not seem to be installed.')
604+ return
605+ ws = pkg_resources.working_set
606+ try:
607+ setuptools_dist = ws.find(
608+ pkg_resources.Requirement.parse('setuptools', replacement=False)
609+ )
610+ except TypeError:
611+ # old distribute API
612+ setuptools_dist = ws.find(
613+ pkg_resources.Requirement.parse('setuptools')
614+ )
615+
616+ if setuptools_dist is None:
617+ log.warn('No setuptools distribution found')
618+ return
619+ # detecting if it was already faked
620+ setuptools_location = setuptools_dist.location
621+ log.warn('Setuptools installation detected at %s', setuptools_location)
622+
623+ # if --root or --preix was provided, and if
624+ # setuptools is not located in them, we don't patch it
625+ if not _under_prefix(setuptools_location):
626+ log.warn('Not patching, --root or --prefix is installing Distribute'
627+ ' in another location')
628+ return
629+
630+ # let's see if its an egg
631+ if not setuptools_location.endswith('.egg'):
632+ log.warn('Non-egg installation')
633+ res = _remove_flat_installation(setuptools_location)
634+ if not res:
635+ return
636+ else:
637+ log.warn('Egg installation')
638+ pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO')
639+ if (os.path.exists(pkg_info) and
640+ _same_content(pkg_info, SETUPTOOLS_PKG_INFO)):
641+ log.warn('Already patched.')
642+ return
643+ log.warn('Patching...')
644+ # let's create a fake egg replacing setuptools one
645+ res = _patch_egg_dir(setuptools_location)
646+ if not res:
647+ return
648+ log.warn('Patching complete.')
649+ _relaunch()
650+
651+
652+def _relaunch():
653+ log.warn('Relaunching...')
654+ # we have to relaunch the process
655+ # pip marker to avoid a relaunch bug
656+ _cmd1 = ['-c', 'install', '--single-version-externally-managed']
657+ _cmd2 = ['-c', 'install', '--record']
658+ if sys.argv[:3] == _cmd1 or sys.argv[:3] == _cmd2:
659+ sys.argv[0] = 'setup.py'
660+ args = [sys.executable] + sys.argv
661+ sys.exit(subprocess.call(args))
662+
663+
664+def _extractall(self, path=".", members=None):
665+ """Extract all members from the archive to the current working
666+ directory and set owner, modification time and permissions on
667+ directories afterwards. `path' specifies a different directory
668+ to extract to. `members' is optional and must be a subset of the
669+ list returned by getmembers().
670+ """
671+ import copy
672+ import operator
673+ from tarfile import ExtractError
674+ directories = []
675+
676+ if members is None:
677+ members = self
678+
679+ for tarinfo in members:
680+ if tarinfo.isdir():
681+ # Extract directories with a safe mode.
682+ directories.append(tarinfo)
683+ tarinfo = copy.copy(tarinfo)
684+ tarinfo.mode = 448 # decimal for oct 0700
685+ self.extract(tarinfo, path)
686+
687+ # Reverse sort directories.
688+ if sys.version_info < (2, 4):
689+ def sorter(dir1, dir2):
690+ return cmp(dir1.name, dir2.name)
691+ directories.sort(sorter)
692+ directories.reverse()
693+ else:
694+ directories.sort(key=operator.attrgetter('name'), reverse=True)
695+
696+ # Set correct owner, mtime and filemode on directories.
697+ for tarinfo in directories:
698+ dirpath = os.path.join(path, tarinfo.name)
699+ try:
700+ self.chown(tarinfo, dirpath)
701+ self.utime(tarinfo, dirpath)
702+ self.chmod(tarinfo, dirpath)
703+ except ExtractError:
704+ e = sys.exc_info()[1]
705+ if self.errorlevel > 1:
706+ raise
707+ else:
708+ self._dbg(1, "tarfile: %s" % e)
709+
710+
711+def _build_install_args(options):
712+ """
713+ Build the arguments to 'python setup.py install' on the distribute package
714+ """
715+ install_args = []
716+ if options.user_install:
717+ if sys.version_info < (2, 6):
718+ log.warn("--user requires Python 2.6 or later")
719+ raise SystemExit(1)
720+ install_args.append('--user')
721+ return install_args
722+
723+def _parse_args():
724+ """
725+ Parse the command line for options
726+ """
727+ parser = optparse.OptionParser()
728+ parser.add_option(
729+ '--user', dest='user_install', action='store_true', default=False,
730+ help='install in user site package (requires Python 2.6 or later)')
731+ parser.add_option(
732+ '--download-base', dest='download_base', metavar="URL",
733+ default=DEFAULT_URL,
734+ help='alternative URL from where to download the distribute package')
735+ options, args = parser.parse_args()
736+ # positional arguments are ignored
737+ return options
738+
739+def main(version=DEFAULT_VERSION):
740+ """Install or upgrade setuptools and EasyInstall"""
741+ options = _parse_args()
742+ tarball = download_setuptools(download_base=options.download_base)
743+ return _install(tarball, _build_install_args(options))
744+
745+if __name__ == '__main__':
746+ sys.exit(main())
747
748=== removed file 'ez_setup.py'
749--- ez_setup.py 2009-03-24 17:31:47 +0000
750+++ ez_setup.py 1970-01-01 00:00:00 +0000
751@@ -1,241 +0,0 @@
752-#!python
753-"""Bootstrap setuptools installation
754-
755-If you want to use setuptools in your package's setup.py, just include this
756-file in the same directory with it, and add this to the top of your setup.py::
757-
758- from ez_setup import use_setuptools
759- use_setuptools()
760-
761-If you want to require a specific version of setuptools, set a download
762-mirror, or use an alternate download directory, you can do so by supplying
763-the appropriate options to ``use_setuptools()``.
764-
765-This file can also be run as a script to install or upgrade setuptools.
766-"""
767-import sys
768-DEFAULT_VERSION = "0.6c8"
769-DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
770-
771-md5_data = {
772- 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
773- 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
774- 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
775- 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
776- 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
777- 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
778- 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
779- 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
780- 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
781- 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
782- 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
783- 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
784- 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
785- 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
786- 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
787- 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
788- 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
789- 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
790- 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
791- 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
792- 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
793- 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
794- 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
795- 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
796- 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
797- 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
798- 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
799- 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
800- 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
801- 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
802-}
803-
804-import sys, os
805-
806-def _validate_md5(egg_name, data):
807- if egg_name in md5_data:
808- from md5 import md5
809- digest = md5(data).hexdigest()
810- if digest != md5_data[egg_name]:
811- print >>sys.stderr, (
812- "md5 validation of %s failed! (Possible download problem?)"
813- % egg_name
814- )
815- sys.exit(2)
816- return data
817-
818-
819-def use_setuptools(
820- version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
821- download_delay=15, min_version=None
822-):
823- """Automatically find/download setuptools and make it available on sys.path
824-
825- `version` should be a valid setuptools version number that is available
826- as an egg for download under the `download_base` URL (which should end with
827- a '/'). `to_dir` is the directory where setuptools will be downloaded, if
828- it is not already available. If `download_delay` is specified, it should
829- be the number of seconds that will be paused before initiating a download,
830- should one be required. If an older version of setuptools is installed,
831- this routine will print a message to ``sys.stderr`` and raise SystemExit in
832- an attempt to abort the calling script.
833- """
834- # Work around a hack in the ez_setup.py file from simplejson==1.7.3.
835- if min_version:
836- version = min_version
837-
838- was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
839- def do_download():
840- egg = download_setuptools(version, download_base, to_dir, download_delay)
841- sys.path.insert(0, egg)
842- import setuptools; setuptools.bootstrap_install_from = egg
843- try:
844- import pkg_resources
845- except ImportError:
846- return do_download()
847- try:
848- pkg_resources.require("setuptools>="+version); return
849- except pkg_resources.VersionConflict, e:
850- if was_imported:
851- print >>sys.stderr, (
852- "The required version of setuptools (>=%s) is not available, and\n"
853- "can't be installed while this script is running. Please install\n"
854- " a more recent version first, using 'easy_install -U setuptools'."
855- "\n\n(Currently using %r)"
856- ) % (version, e.args[0])
857- sys.exit(2)
858- else:
859- del pkg_resources, sys.modules['pkg_resources'] # reload ok
860- return do_download()
861- except pkg_resources.DistributionNotFound:
862- return do_download()
863-
864-def download_setuptools(
865- version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
866- delay = 15
867-):
868- """Download setuptools from a specified location and return its filename
869-
870- `version` should be a valid setuptools version number that is available
871- as an egg for download under the `download_base` URL (which should end
872- with a '/'). `to_dir` is the directory where the egg will be downloaded.
873- `delay` is the number of seconds to pause before an actual download attempt.
874- """
875- import urllib2, shutil
876- egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
877- url = download_base + egg_name
878- saveto = os.path.join(to_dir, egg_name)
879- src = dst = None
880- if not os.path.exists(saveto): # Avoid repeated downloads
881- try:
882- from distutils import log
883- if delay:
884- log.warn("""
885----------------------------------------------------------------------------
886-This script requires setuptools version %s to run (even to display
887-help). I will attempt to download it for you (from
888-%s), but
889-you may need to enable firewall access for this script first.
890-I will start the download in %d seconds.
891-
892-(Note: if this machine does not have network access, please obtain the file
893-
894- %s
895-
896-and place it in this directory before rerunning this script.)
897----------------------------------------------------------------------------""",
898- version, download_base, delay, url
899- ); from time import sleep; sleep(delay)
900- log.warn("Downloading %s", url)
901- src = urllib2.urlopen(url)
902- # Read/write all in one block, so we don't create a corrupt file
903- # if the download is interrupted.
904- data = _validate_md5(egg_name, src.read())
905- dst = open(saveto,"wb"); dst.write(data)
906- finally:
907- if src: src.close()
908- if dst: dst.close()
909- return os.path.realpath(saveto)
910-
911-def main(argv, version=DEFAULT_VERSION):
912- """Install or upgrade setuptools and EasyInstall"""
913- try:
914- import setuptools
915- except ImportError:
916- egg = None
917- try:
918- egg = download_setuptools(version, delay=0)
919- sys.path.insert(0,egg)
920- from setuptools.command.easy_install import main
921- return main(list(argv)+[egg]) # we're done here
922- finally:
923- if egg and os.path.exists(egg):
924- os.unlink(egg)
925- else:
926- if setuptools.__version__ == '0.0.1':
927- print >>sys.stderr, (
928- "You have an obsolete version of setuptools installed. Please\n"
929- "remove it from your system entirely before rerunning this script."
930- )
931- sys.exit(2)
932-
933- req = "setuptools>="+version
934- import pkg_resources
935- try:
936- pkg_resources.require(req)
937- except pkg_resources.VersionConflict:
938- try:
939- from setuptools.command.easy_install import main
940- except ImportError:
941- from easy_install import main
942- main(list(argv)+[download_setuptools(delay=0)])
943- sys.exit(0) # try to force an exit
944- else:
945- if argv:
946- from setuptools.command.easy_install import main
947- main(argv)
948- else:
949- print "Setuptools version",version,"or greater has been installed."
950- print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
951-
952-def update_md5(filenames):
953- """Update our built-in md5 registry"""
954-
955- import re
956- from md5 import md5
957-
958- for name in filenames:
959- base = os.path.basename(name)
960- f = open(name,'rb')
961- md5_data[base] = md5(f.read()).hexdigest()
962- f.close()
963-
964- data = [" %r: %r,\n" % it for it in md5_data.items()]
965- data.sort()
966- repl = "".join(data)
967-
968- import inspect
969- srcfile = inspect.getsourcefile(sys.modules[__name__])
970- f = open(srcfile, 'rb'); src = f.read(); f.close()
971-
972- match = re.search("\nmd5_data = {\n([^}]+)}", src)
973- if not match:
974- print >>sys.stderr, "Internal error!"
975- sys.exit(2)
976-
977- src = src[:match.start(1)] + repl + src[match.end(1):]
978- f = open(srcfile,'w')
979- f.write(src)
980- f.close()
981-
982-
983-if __name__=='__main__':
984- if len(sys.argv)>2 and sys.argv[1]=='--md5update':
985- update_md5(sys.argv[2:])
986- else:
987- main(sys.argv[1:])
988-
989-
990-
991-
992-
993
994=== renamed directory 'src/lazr' => 'lazr'
995=== modified file 'lazr/__init__.py'
996--- src/lazr/__init__.py 2009-03-24 17:31:47 +0000
997+++ lazr/__init__.py 2013-01-10 15:47:20 +0000
998@@ -1,4 +1,4 @@
999-# Copyright 2008-2009 Canonical Ltd. All rights reserved.
1000+# Copyright 2008-2013 Canonical Ltd. All rights reserved.
1001 #
1002 # This file is part of lazr.config.
1003 #
1004@@ -14,10 +14,14 @@
1005 # You should have received a copy of the GNU Lesser General Public License
1006 # along with lazr.config. If not, see <http://www.gnu.org/licenses/>.
1007
1008-# this is a namespace package
1009-try:
1010- import pkg_resources
1011- pkg_resources.declare_namespace(__name__)
1012-except ImportError:
1013- import pkgutil
1014- __path__ = pkgutil.extend_path(__path__, __name__)
1015+# This is a namespace package, however under >= Python 3.3, let it be a true
1016+# namespace package (i.e. this cruft isn't necessary).
1017+import sys
1018+
1019+if sys.hexversion < 0x30300f0:
1020+ try:
1021+ import pkg_resources
1022+ pkg_resources.declare_namespace(__name__)
1023+ except ImportError:
1024+ import pkgutil
1025+ __path__ = pkgutil.extend_path(__path__, __name__)
1026
1027=== modified file 'lazr/config/__init__.py'
1028--- src/lazr/config/__init__.py 2009-08-25 13:46:10 +0000
1029+++ lazr/config/__init__.py 2013-01-10 15:47:20 +0000
1030@@ -17,7 +17,8 @@
1031 """A configuration file system."""
1032
1033 import pkg_resources
1034-__version__ = pkg_resources.resource_string("lazr.config", "version.txt").strip()
1035+__version__ = pkg_resources.resource_string(
1036+ "lazr.config", "version.txt").strip()
1037
1038 # Re-export in such a way that __version__ can still be imported if
1039 # dependencies are not yet available.
1040
1041=== modified file 'lazr/config/_config.py'
1042--- src/lazr/config/_config.py 2009-03-24 17:31:47 +0000
1043+++ lazr/config/_config.py 2013-01-10 15:47:20 +0000
1044@@ -1,4 +1,4 @@
1045-# Copyright 2008-2009 Canonical Ltd. All rights reserved.
1046+# Copyright 2008-2013 Canonical Ltd. All rights reserved.
1047 #
1048 # This file is part of lazr.config.
1049 #
1050@@ -16,8 +16,9 @@
1051
1052 """Implementation classes for config."""
1053
1054+from __future__ import absolute_import, print_function, unicode_literals
1055+
1056 __metaclass__ = type
1057-
1058 __all__ = [
1059 'Config',
1060 'ConfigData',
1061@@ -34,7 +35,6 @@
1062 ]
1063
1064
1065-import StringIO
1066 import datetime
1067 import grp
1068 import logging
1069@@ -42,35 +42,39 @@
1070 import pwd
1071 import re
1072
1073-from ConfigParser import NoSectionError, RawConfigParser
1074 from os.path import abspath, basename, dirname
1075 from textwrap import dedent
1076
1077-from zope.interface import implements
1078+try:
1079+ from io import StringIO
1080+ from configparser import NoSectionError, RawConfigParser
1081+except ImportError:
1082+ # Python 2.
1083+ from StringIO import StringIO
1084+ from ConfigParser import NoSectionError, RawConfigParser
1085+
1086+
1087+from zope.interface import implementer
1088
1089 from lazr.config.interfaces import (
1090 ConfigErrors, ICategory, IConfigData, IConfigLoader, IConfigSchema,
1091 InvalidSectionNameError, ISection, ISectionSchema, IStackableConfig,
1092 NoCategoryError, NoConfigError, RedefinedSectionError, UnknownKeyError,
1093 UnknownSectionError)
1094-from lazr.delegates import delegates
1095+from lazr.delegates import delegate_to
1096
1097 _missing = object()
1098
1099
1100 def read_content(filename):
1101 """Return the content of a file at filename as a string."""
1102- source_file = open(filename, 'r')
1103- try:
1104- raw_data = source_file.read()
1105- finally:
1106- source_file.close()
1107- return raw_data
1108-
1109-
1110+ with open(filename, 'rt') as fp:
1111+ return fp.read()
1112+
1113+
1114+@implementer(ISectionSchema)
1115 class SectionSchema:
1116 """See `ISectionSchema`."""
1117- implements(ISectionSchema)
1118
1119 def __init__(self, name, options, is_optional=False, is_master=False):
1120 """Create an `ISectionSchema` from the name and options.
1121@@ -89,7 +93,8 @@
1122
1123 def __iter__(self):
1124 """See `ISectionSchema`"""
1125- return self._options.iterkeys()
1126+ for key in self._options.keys():
1127+ yield key
1128
1129 def __contains__(self, name):
1130 """See `ISectionSchema`"""
1131@@ -113,12 +118,11 @@
1132 self.optional, self.master)
1133
1134
1135+@delegate_to(ISectionSchema, context='schema')
1136+@implementer(ISection)
1137 class Section:
1138 """See `ISection`."""
1139
1140- implements(ISection)
1141- delegates(ISectionSchema, context='schema')
1142-
1143 def __init__(self, schema, _options=None):
1144 """Create an `ISection` from schema.
1145
1146@@ -223,9 +227,9 @@
1147 return self._convert(value)
1148
1149
1150+@implementer(IConfigSchema, IConfigLoader)
1151 class ConfigSchema:
1152 """See `IConfigSchema`."""
1153- implements(IConfigSchema, IConfigLoader)
1154
1155 _section_factory = Section
1156
1157@@ -267,7 +271,7 @@
1158 """
1159 raw_schema = read_content(filename)
1160 # Verify that the string is ascii.
1161- raw_schema.encode('ascii', 'ignore')
1162+ raw_schema.encode('ascii', 'strict')
1163 # Verify that no sections are redefined.
1164 section_names = []
1165 for section_name in re.findall(r'^\s*\[[^\]]+\]', raw_schema, re.M):
1166@@ -275,7 +279,7 @@
1167 raise RedefinedSectionError(section_name)
1168 else:
1169 section_names.append(section_name)
1170- return StringIO.StringIO(raw_schema)
1171+ return StringIO(raw_schema)
1172
1173 def _setSectionSchemasAndCategoryNames(self, parser):
1174 """Set the SectionSchemas and category_names from the config."""
1175@@ -301,7 +305,7 @@
1176 section_name, options, is_optional, is_master)
1177 if category_name is not None:
1178 category_names.add(category_name)
1179- self._category_names = list(category_names)
1180+ self._category_names = sorted(category_names)
1181
1182 _section_name_pattern = re.compile(r'\w[\w.-]+\w')
1183
1184@@ -355,7 +359,8 @@
1185
1186 def __iter__(self):
1187 """See `IConfigSchema`."""
1188- return self._section_schemas.itervalues()
1189+ for value in self._section_schemas.values():
1190+ yield value
1191
1192 def __contains__(self, name):
1193 """See `IConfigSchema`."""
1194@@ -423,9 +428,9 @@
1195 _section_factory = ImplicitTypeSection
1196
1197
1198+@implementer(IConfigData)
1199 class ConfigData:
1200 """See `IConfigData`."""
1201- implements(IConfigData)
1202
1203 def __init__(self, filename, sections, extends=None, errors=None):
1204 """Set the configuration data."""
1205@@ -456,7 +461,8 @@
1206
1207 def __iter__(self):
1208 """See `IConfigData`."""
1209- return self._sections.itervalues()
1210+ for value in self._sections.values():
1211+ yield value
1212
1213 def __contains__(self, name):
1214 """See `IConfigData`."""
1215@@ -484,12 +490,12 @@
1216 return sections
1217
1218
1219+@delegate_to(IConfigData, context='data')
1220+@implementer(IStackableConfig)
1221 class Config:
1222 """See `IStackableConfig`."""
1223 # LAZR config classes may access ConfigData private data.
1224 # pylint: disable-msg=W0212
1225- implements(IStackableConfig)
1226- delegates(IConfigData, context='data')
1227
1228 def __init__(self, schema):
1229 """Set the schema and configuration."""
1230@@ -567,7 +573,7 @@
1231 confs = []
1232 encoding_errors = self._verifyEncoding(conf_data)
1233 parser = RawConfigParser()
1234- parser.readfp(StringIO.StringIO(conf_data), conf_filename)
1235+ parser.readfp(StringIO(conf_data), conf_filename)
1236 confs.append((conf_filename, parser, encoding_errors))
1237 if parser.has_option('meta', 'extends'):
1238 base_path = dirname(conf_filename)
1239@@ -660,8 +666,11 @@
1240 """
1241 errors = []
1242 try:
1243- config_data.encode('ascii', 'ignore')
1244- except UnicodeDecodeError, error:
1245+ if isinstance(config_data, bytes):
1246+ config_data.decode('ascii', 'strict')
1247+ else:
1248+ config_data.encode('ascii', 'strict')
1249+ except UnicodeError as error:
1250 errors.append(error)
1251 return errors
1252
1253@@ -706,9 +715,9 @@
1254 raise NoConfigError('No config with name: %s.' % conf_name)
1255
1256
1257+@implementer(ICategory)
1258 class Category:
1259 """See `ICategory`."""
1260- implements(ICategory)
1261
1262 def __init__(self, name, sections):
1263 """Initialize the Category its name and a list of sections."""
1264@@ -786,12 +795,8 @@
1265 return user, group
1266
1267
1268-def _sort_order(a, b):
1269- """Sort timedelta suffixes from greatest to least."""
1270- if len(a) == 0:
1271- return -1
1272- if len(b) == 0:
1273- return 1
1274+def _sortkey(item):
1275+ """Return a value that sorted(..., key=_sortkey) can use."""
1276 order = dict(
1277 w=0, # weeks
1278 d=1, # days
1279@@ -799,20 +804,14 @@
1280 m=3, # minutes
1281 s=4, # seconds
1282 )
1283- suffix_a = order.get(a[-1])
1284- suffix_b = order.get(b[-1])
1285- if suffix_a is None or suffix_b is None:
1286- raise ValueError
1287- return cmp(suffix_a, suffix_b)
1288-
1289+ return order.get(item[-1])
1290
1291 def as_timedelta(value):
1292 """Convert a value string to the equivalent timedeta."""
1293 # Technically, the regex will match multiple decimal points in the
1294 # left-hand side, but that's okay because the float/int conversion below
1295 # will properly complain if there's more than one dot.
1296- components = sorted(re.findall(r'([\d.]+[smhdw])', value),
1297- cmp=_sort_order)
1298+ components = sorted(re.findall(r'([\d.]+[smhdw])', value), key=_sortkey)
1299 # Complain if the components are out of order.
1300 if ''.join(components) != value:
1301 raise ValueError
1302
1303=== added directory 'lazr/config/docs'
1304=== renamed file 'src/lazr/config/CHANGES.txt' => 'lazr/config/docs/NEWS.rst'
1305--- src/lazr/config/CHANGES.txt 2009-03-24 17:31:47 +0000
1306+++ lazr/config/docs/NEWS.rst 2013-01-10 15:47:20 +0000
1307@@ -2,6 +2,23 @@
1308 Changes
1309 =======
1310
1311+
1312+2.0 (2013-01-06)
1313+================
1314+- Ported to Python 3.
1315+
1316+
1317+1.1.3 (2009-08-25)
1318+==================
1319+
1320+- Fixed a build problem.
1321+
1322+1.1.2 (2009-08-25)
1323+==================
1324+
1325+- Got rid of a sys.path hack.
1326+
1327+
1328 1.1.1 (2009-03-24)
1329 ==================
1330
1331
1332=== added file 'lazr/config/docs/__init__.py'
1333=== added file 'lazr/config/docs/fixture.py'
1334--- lazr/config/docs/fixture.py 1970-01-01 00:00:00 +0000
1335+++ lazr/config/docs/fixture.py 2013-01-10 15:47:20 +0000
1336@@ -0,0 +1,34 @@
1337+# Copyright 2009-2013 Canonical Ltd. All rights reserved.
1338+#
1339+# This file is part of lazr.smtptest
1340+#
1341+# lazr.smtptest is free software: you can redistribute it and/or modify it
1342+# under the terms of the GNU Lesser General Public License as published by
1343+# the Free Software Foundation, version 3 of the License.
1344+#
1345+# lazr.smtptest is distributed in the hope that it will be useful, but WITHOUT
1346+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
1347+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
1348+# License for more details.
1349+#
1350+# You should have received a copy of the GNU Lesser General Public License
1351+# along with lazr.smtptest. If not, see <http://www.gnu.org/licenses/>.
1352+
1353+"""Doctest fixtures for running under nose."""
1354+
1355+from __future__ import absolute_import, print_function, unicode_literals
1356+
1357+__metaclass__ = type
1358+__all__ = [
1359+ 'globs',
1360+ ]
1361+
1362+
1363+def globs(globs):
1364+ """Set up globals for doctests."""
1365+ # Enable future statements to make Python 2 act more like Python 3.
1366+ globs['absolute_import'] = absolute_import
1367+ globs['print_function'] = print_function
1368+ globs['unicode_literals'] = unicode_literals
1369+ # Provide a convenient way to clean things up at the end of the test.
1370+ return globs
1371
1372=== renamed file 'src/lazr/config/README.txt' => 'lazr/config/docs/usage.rst'
1373--- src/lazr/config/README.txt 2009-03-24 17:36:13 +0000
1374+++ lazr/config/docs/usage.rst 2013-01-10 15:47:20 +0000
1375@@ -1,60 +1,40 @@
1376-..
1377- This file is part of lazr.config.
1378-
1379- lazr.config is free software: you can redistribute it and/or modify it
1380- under the terms of the GNU Lesser General Public License as published by
1381- the Free Software Foundation, version 3 of the License.
1382-
1383- lazr.config is distributed in the hope that it will be useful, but WITHOUT
1384- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
1385- FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
1386- License for more details.
1387-
1388- You should have received a copy of the GNU Lesser General Public License
1389- along with lazr.config. If not, see <http://www.gnu.org/licenses/>.
1390-
1391+===========
1392 LAZR config
1393-***********
1394+===========
1395
1396 The LAZR config system is typically used to manage process configuration.
1397-Process configuration is for saying how things change when we run
1398-systems on different machines, or under different circumstances.
1399-
1400-This system uses ini-like file format of section, keys, and values.
1401-The config file supports inheritance to minimize duplication of
1402-information across files. The format supports schema validation.
1403-
1404-
1405-============
1406+Process configuration is for saying how things change when we run systems on
1407+different machines, or under different circumstances.
1408+
1409+This system uses ini-like file format of section, keys, and values. The
1410+config file supports inheritance to minimize duplication of information across
1411+files. The format supports schema validation.
1412+
1413+
1414 ConfigSchema
1415 ============
1416
1417-A schema is loaded by instantiating the ConfigSchema class with
1418-the path to a configuration file. The schema is explicitly derived from
1419-the information in the configuration file.
1420-
1421- >>> from os import path
1422- >>> from zope.interface.verify import verifyObject
1423- >>> from lazr.config import ConfigSchema
1424- >>> from lazr.config.interfaces import IConfigSchema
1425-
1426- >>> import lazr.config
1427- >>> testfiles_dir = path.normpath(path.join(
1428- ... path.dirname(lazr.config.__file__), 'tests', 'testdata'))
1429- >>> base_conf = path.join(testfiles_dir, 'base.conf')
1430-
1431-The config file contains sections enclosed in square brackets ([]).
1432-The section name may be divided into major and minor categories using a
1433-dot (.). Beneath each section is a list of key-value pairs, separated
1434-by a colon (:). Multiple sections with the same major category may have
1435-their keys defined in another section that appends the '.template'
1436-suffix to the category name. A section with '.optional' suffix is not
1437-required. Lines that start with a hash (#) are comments.
1438-
1439- >>> schema_file = open(base_conf, 'r')
1440- >>> raw_schema = schema_file.read()
1441- >>> schema_file.close()
1442- >>> print raw_schema
1443+A schema is loaded by instantiating the ConfigSchema class with the path to a
1444+configuration file. The schema is explicitly derived from the information in
1445+the configuration file.
1446+
1447+ >>> from pkg_resources import resource_string
1448+ >>> raw_schema = resource_string('lazr.config.tests.testdata', 'base.conf')
1449+
1450+The config file contains sections enclosed in square brackets
1451+(e.g. ``[section]``). The section name may be divided into major and minor
1452+categories using a dot (``.``). Beneath each section is a list of key-value
1453+pairs, separated by a colon (``:``).
1454+
1455+Multiple sections with the same major category may have their keys defined in
1456+another section that appends the ``.template`` suffix to the category name.
1457+
1458+A section with ``.optional`` suffix is not required. Lines that start with a
1459+hash (``#``) are comments.
1460+
1461+ >>> from pkg_resources import resource_string
1462+ >>> raw_schema = resource_string('lazr.config.tests.testdata', 'base.conf')
1463+ >>> print(raw_schema.decode('utf-8'))
1464 # This section defines required keys and default values.
1465 [section_1]
1466 key1: foo
1467@@ -86,36 +66,46 @@
1468 key2: multiline value 1
1469 multiline value 2
1470
1471+To create the schema, provide a file name.
1472+
1473+ >>> from lazr.config import ConfigSchema
1474+ >>> from lazr.config.interfaces import IConfigSchema
1475+ >>> from pkg_resources import resource_filename
1476+ >>> from zope.interface.verify import verifyObject
1477+ >>> base_conf = resource_filename(
1478+ ... 'lazr.config.tests.testdata', 'base.conf')
1479 >>> schema = ConfigSchema(base_conf)
1480 >>> verifyObject(IConfigSchema, schema)
1481 True
1482
1483- >>> schema.name
1484- 'base.conf'
1485- >>> schema.filename
1486- '...lazr/config/tests/testdata/base.conf'
1487+The schema has a name and a file name.
1488+
1489+ >>> print(schema.name)
1490+ base.conf
1491+ >>> print('file:', schema.filename)
1492+ file: ...lazr/config/tests/testdata/base.conf
1493
1494 If you provide an optional file-like object as a second argument to the
1495 constructor, that is used instead of opening the named file implicitly.
1496
1497- >>> file_object = open(base_conf)
1498- >>> other_schema = ConfigSchema('/does/not/exist.conf', file_object)
1499+ >>> with open(base_conf, 'r') as file_object:
1500+ ... other_schema = ConfigSchema('/does/not/exist.conf', file_object)
1501 >>> verifyObject(IConfigSchema, other_schema)
1502 True
1503
1504- >>> print other_schema.name
1505+For such schemas, the file name is taken from the first argument.
1506+
1507+ >>> print(other_schema.name)
1508 exist.conf
1509- >>> print other_schema.filename
1510+ >>> print(other_schema.filename)
1511 /does/not/exist.conf
1512
1513- >>> file_object.close()
1514-
1515 A schema is made up of multiple SchemaSections. They can be iterated
1516 over in a loop as needed.
1517
1518 >>> from operator import attrgetter
1519 >>> for section_schema in sorted(schema, key=attrgetter('name')):
1520- ... print section_schema.name
1521+ ... print(section_schema.name)
1522 section-2.app-b
1523 section-5
1524 section_1
1525@@ -124,7 +114,7 @@
1526 section_33
1527
1528 >>> for section_schema in sorted(other_schema, key=attrgetter('name')):
1529- ... print section_schema.name
1530+ ... print(section_schema.name)
1531 section-2.app-b
1532 section-5
1533 section_1
1534@@ -140,78 +130,69 @@
1535 >>> 'section-4' in schema
1536 False
1537
1538-A SectionSchema can be retrieved from the schema using the []
1539-operator
1540+A SectionSchema can be retrieved from the schema using the ``[]`` operator.
1541
1542 >>> section_schema_1 = schema['section_1']
1543- >>> section_schema_1.name
1544- 'section_1'
1545-
1546-A SectionNotFound error is raised if the name does not match any of the
1547-SectionSchemas.
1548-
1549- >>> section_schema_app_a = schema['section_3.app_a']
1550- >>> schema['section-4']
1551- Traceback (most recent call last):
1552- ...
1553- NoSectionError: ...
1554-
1555-Processes often require resources like databases or vhosts that have a
1556-common category of keys. The list of all category names can be retrieved
1557-via the categories attribute.
1558-
1559- >>> schema.category_names
1560- ['section_3', 'section-2']
1561+ >>> print(section_schema_1.name)
1562+ section_1
1563+
1564+Processes often require resources like databases or virtual hosts that have a
1565+common category of keys. The list of all category names can be retrieved via
1566+the categories attribute.
1567+
1568+ >>> for name in schema.category_names:
1569+ ... print(name)
1570+ section-2
1571+ section_3
1572
1573 The list of SchemaSections that share common category can be retrieved
1574-using getByCategory().
1575+using ``getByCategory()``.
1576
1577 >>> all_section_3 = schema.getByCategory('section_3')
1578 >>> for section_schema in sorted(all_section_3, key=attrgetter('name')):
1579- ... print section_schema.name
1580+ ... print(section_schema.name)
1581 section_3.app_a
1582 section_3.app_b
1583
1584-An error is raised when accessing a category does not exist.
1585-
1586- >>> schema.getByCategory('non-section')
1587- Traceback (most recent call last):
1588- ...
1589- NoCategoryError: ...
1590-
1591-You can pass a default argument to getByCategory() to avoid the exception.
1592+You can pass a default argument to ``getByCategory()`` to avoid the exception.
1593
1594 >>> missing = object()
1595 >>> schema.getByCategory('non-section', missing) is missing
1596 True
1597
1598
1599-=============
1600 SchemaSection
1601 =============
1602
1603-A SchemaSection behaves similar to a dictionary. It has keys and
1604-values. Each SchemaSection has a name.
1605+A SchemaSection behaves similar to a dictionary. It has keys and values.
1606
1607 >>> from lazr.config.interfaces import ISectionSchema
1608 >>> section_schema_1 = schema['section_1']
1609 >>> verifyObject(ISectionSchema, section_schema_1)
1610 True
1611
1612- >>> section_schema_1.name
1613- 'section_1'
1614-
1615-A SchemaSection can return a 2-tuple of its category name and specific
1616-name parts. The category name will be None if the SchemaSection's name
1617-does not contain a category.
1618-
1619- >>> schema['section_3.app_b'].category_and_section_names
1620- ('section_3', 'app_b')
1621-
1622- >>> section_schema_1.category_and_section_names
1623- (None, 'section_1')
1624-
1625-Optional sections have the optional attribute set to True:
1626+Each SchemaSection has a name.
1627+
1628+ >>> print(section_schema_1.name)
1629+ section_1
1630+
1631+A SchemaSection can return a 2-tuple of its category name and specific name
1632+parts.
1633+
1634+ >>> for name in schema['section_3.app_b'].category_and_section_names:
1635+ ... print(name)
1636+ section_3
1637+ app_b
1638+
1639+The category name will be ``None`` if the SchemaSection's name does not
1640+contain a category.
1641+
1642+ >>> for name in section_schema_1.category_and_section_names:
1643+ ... print(name)
1644+ None
1645+ section_1
1646+
1647+Optional sections have the optional attribute set to ``True``:
1648
1649 >>> section_schema_1.optional
1650 False
1651@@ -225,11 +206,11 @@
1652 >>> 'nonkey' in section_schema_1
1653 False
1654
1655-A key can be accessed directly using as a subscript of the SchemaSection.
1656-The value is always a string.
1657+A key can be accessed directly using as a subscript of the SchemaSection. The
1658+value is always a string.
1659
1660- >>> section_schema_1['key3']
1661- 'Launchpad&nbsp;rocks'
1662+ >>> print(section_schema_1['key3'])
1663+ Launchpad&nbsp;rocks
1664 >>> section_schema_1['key5']
1665 ''
1666
1667@@ -240,29 +221,29 @@
1668 ...
1669 KeyError: ...
1670
1671-In the conf file, '[section_1]' is a default section that defines keys
1672-and values. The values specified in the section schema will be used as
1673-default values if not overriden in the configuration. In the case of
1674-key5, the key had no explicit value, so the value is an empty string.
1675+In the conf file, ``[section_1]`` is a default section that defines keys and
1676+values. The values specified in the section schema will be used as default
1677+values if not overridden in the configuration. In the case of *key5*, the key
1678+had no explicit value, so the value is an empty string.
1679
1680 >>> for key in sorted(section_schema_1):
1681- ... print key, ':', section_schema_1[key]
1682+ ... print(key, ':', section_schema_1[key])
1683 key1 : foo
1684 key2 : bar and baz
1685 key3 : Launchpad&nbsp;rocks
1686 key4 : F&#028c;k yeah!
1687 key5 :
1688
1689-In the conf file '[section_3.template]' defines a common set of keys and
1690-default values for '[section_3.app_a]' and '[section_3.app_b]'. When a
1691-section defines different keys and default values s from the template,
1692-the new data overlays the template data. This is the case for section
1693-'[section_3.app_b]'.
1694+In the conf file ``[section_3.template]`` defines a common set of keys and
1695+default values for ``[section_3.app_a]`` and ``[section_3.app_b]``. When a
1696+section defines different keys and default values from the template, the new
1697+data overlays the template data. This is the case for section
1698+``[section_3.app_b]``.
1699
1700 >>> for section_schema in sorted(all_section_3, key=attrgetter('name')):
1701- ... print section_schema.name
1702+ ... print(section_schema.name)
1703 ... for key in sorted(section_schema):
1704- ... print key, ':', section_schema[key]
1705+ ... print(key, ':', section_schema[key])
1706 section_3.app_a
1707 key1 : 17
1708 key2 : 3.1415
1709@@ -272,93 +253,45 @@
1710 key3 : unique
1711
1712
1713-=======================
1714 ConfigSchema validation
1715 =======================
1716
1717-ConfigSchema will raise an error if the schema file cannot be opened.
1718-
1719- >>> ConfigSchema("no-such-file")
1720- Traceback (most recent call last):
1721- ...
1722- IOError: [Errno 2] No such file or directory: ...
1723-
1724-The schema parser is self-validating. It will check that the character
1725-encoding is ascii. It will check that the data is not ambiguous or
1726-self-contradicting.
1727-
1728-Schema files that contain non-ASCII characters raise a
1729-UnicodeDecodeError.
1730-
1731- >>> ConfigSchema(path.join(testfiles_dir, 'bad-nonascii.conf'))
1732- Traceback (most recent call last):
1733- ...
1734- UnicodeDecodeError: ...
1735-
1736-Keys without sections raise MissingSectionHeaderError.
1737-
1738- >>> ConfigSchema(path.join(testfiles_dir, 'bad-sectionless.conf'))
1739- Traceback (most recent call last):
1740- ...
1741- MissingSectionHeaderError: File contains no section headers. ...
1742-
1743-Redefining a section in a config file will raise a RedefinedSectionError.
1744-
1745- >>> ConfigSchema(path.join(testfiles_dir, 'bad-redefined-section.conf'))
1746- Traceback (most recent call last):
1747- ...
1748- RedefinedSectionError: ...
1749-
1750-# XXX sinzui 2007-12-13:
1751-# ConfigSchema should raise RedefinedKeyError when a section redefines
1752-# a key.
1753-
1754-Defining a section that belongs to many categories will raise
1755-a InvalidSectionNameError.
1756-
1757- >>> ConfigSchema(path.join(testfiles_dir, 'bad-invalid-name.conf'))
1758- Traceback (most recent call last):
1759- ...
1760- InvalidSectionNameError: [category.other_category.name.optional] ...
1761-
1762-As does using non word characters other than a dot or dash in the
1763-section name.
1764-
1765- >>> ConfigSchema(path.join(testfiles_dir, 'bad-invalid-name-chars.conf'))
1766- Traceback (most recent call last):
1767- ...
1768- InvalidSectionNameError: [$category.name_part.optional] ...
1769-
1770-
1771-=============
1772+The schema parser is self-validating. It checks that the character encoding
1773+is ASCII, and that the data is not ambiguous or self-contradicting. Keys must
1774+exist inside sections and section names may not be defined twice. Sections
1775+may belong to only one category, and only letters, numbers, dots and dashes
1776+may be present in section names.
1777+
1778+.. For multilingual Python support reasons, we don't include testable examples
1779+ here. See ``test_config.py`` and ``lazr/config/interfaces.py`` for details.
1780+
1781+
1782 IConfigLoader
1783 =============
1784
1785-ConfigSchema implements the two methods in the IConfigLoader interface.
1786-A Config is created by a schema using either the load() or loadFile()
1787+ConfigSchema implements the two methods in the IConfigLoader interface. A
1788+Config is created by a schema using either the ``load()`` or ``loadFile()``
1789 methods to return a Config instance.
1790
1791 >>> from lazr.config.interfaces import IConfigLoader
1792 >>> verifyObject(IConfigLoader, schema)
1793 True
1794
1795-The load() method accepts a filename.
1796+The ``load()`` method accepts a filename.
1797
1798- >>> local_conf = path.join(testfiles_dir, 'local.conf')
1799+ >>> local_conf = resource_filename(
1800+ ... 'lazr.config.tests.testdata', 'local.conf')
1801 >>> config = schema.load(local_conf)
1802
1803-Passing a filename to a non-existent file will raise an IOError.
1804-
1805- >>> schema.load("fnord.conf")
1806- Traceback (most recent call last):
1807- ...
1808- IOError: [Errno 2] No such file or directory: 'fnord.conf'
1809-
1810-The loadFile method accepts a file-like object and an optional filename
1811-keyword arg. The filename arg must be passed if the file-like object
1812-does not have a name attribute.
1813-
1814- >>> import StringIO
1815+The ``loadFile()`` method accepts a file-like object and an optional filename
1816+keyword argument. The filename argument must be passed if the file-like
1817+object does not have a ``name`` attribute.
1818+
1819+ >>> try:
1820+ ... from io import StringIO
1821+ ... except ImportError:
1822+ ... # Python 2
1823+ ... from StringIO import StringIO
1824 >>> bad_data = ("""
1825 ... [meta]
1826 ... metakey: unsupported
1827@@ -369,30 +302,24 @@
1828 ... key1: bad character in caf\xc3)
1829 ... [section_3.template]
1830 ... key1: schema suffixes are not permitted""")
1831- >>> schema.loadFile(StringIO.StringIO(bad_data))
1832- Traceback (most recent call last):
1833- ...
1834- AttributeError: StringIO instance has no attribute 'name'
1835-
1836 >>> bad_config = schema.loadFile(
1837- ... StringIO.StringIO(bad_data), 'bad conf')
1838-
1839-The bad_config example will be used for validation tests.
1840-
1841-
1842-======
1843+ ... StringIO(bad_data), 'bad conf')
1844+
1845+.. The bad_config example will be used for validation tests.
1846+
1847+
1848 Config
1849 ======
1850
1851-The config represents the local configuration of the process on a
1852-system. It is validated with a schema. It extends the schema, or other
1853-conf files to define the specific differences from the extended files
1854-that are required to run the local processes.
1855+The config represents the local configuration of the process on a system. It
1856+is validated with a schema. It extends the schema, or other conf files, to
1857+define the specific differences from the extended files that are required to
1858+run the local processes.
1859
1860-The object returned by load() provides both the IConfigData and
1861-IStackableConfig interfaces. IConfigData is for read-only access to the
1862-configuration data. A process configuration is made up of a stack of
1863-different IConfigData. The IStackableConfig interface provides the
1864+The object returned by ``load()`` provides both the ``IConfigData`` and
1865+``IStackableConfig`` interfaces. ``IConfigData`` is for read-only access to
1866+the configuration data. A process configuration is made up of a stack of
1867+different ``IConfigData``. The ``IStackableConfig`` interface provides the
1868 methods used to manipulate that stack of configuration overlays.
1869
1870 >>> from lazr.config.interfaces import IConfigData, IStackableConfig
1871@@ -401,15 +328,14 @@
1872 >>> verifyObject(IStackableConfig, config)
1873 True
1874
1875-Like the schema file, the conf file is made up of sections with keys.
1876-The sections may belong to a category. Unlike the schema file, it does
1877-not have template or optional sections. The [meta] has the extends
1878-key that declares that this conf extends shared.conf.
1879+Like the schema file, the conf file is made up of sections with keys. The
1880+sections may belong to a category. Unlike the schema file, it does not have
1881+template or optional sections. The ``[meta]`` section has the extends key
1882+that declares that this conf extends ``shared.conf``.
1883
1884- >>> local_file = open(local_conf, 'r')
1885- >>> raw_conf = local_file.read()
1886- >>> local_file.close()
1887- >>> print raw_conf
1888+ >>> with open(local_conf, 'rt') as local_file:
1889+ ... raw_conf = local_file.read()
1890+ >>> print(raw_conf)
1891 [meta]
1892 extends: shared.conf
1893 # Localize a key for section_1.
1894@@ -418,66 +344,71 @@
1895 # Accept the default values for the optional section-5.
1896 [section-5]
1897
1898-The .master section allows admins to define configurations for an arbitrary
1899-number of processes. If the schema defines .master sections, then the conf
1900-file can contain sections that extend the .master section. These are like
1901-categories with templates except that the section names extending .master need
1902-not be named in the schema file.
1903+The ``.master`` section allows admins to define configurations for an
1904+arbitrary number of processes. If the schema defines ``.master`` sections,
1905+then the conf file can contain sections that extend the ``.master`` section.
1906+These are like categories with templates except that the section names
1907+extending ``.master`` need not be named in the schema file.
1908
1909- >>> master_schema_conf = path.join(testfiles_dir, 'master.conf')
1910- >>> master_local_conf = path.join(testfiles_dir, 'master-local.conf')
1911+ >>> master_schema_conf = resource_filename(
1912+ ... 'lazr.config.tests.testdata', 'master.conf')
1913+ >>> master_local_conf = resource_filename(
1914+ ... 'lazr.config.tests.testdata', 'master-local.conf')
1915 >>> master_schema = ConfigSchema(master_schema_conf)
1916 >>> sections = master_schema.getByCategory('thing')
1917- >>> sorted(section.name for section in sections)
1918- ['thing.master']
1919+ >>> for name in sorted(section.name for section in sections):
1920+ ... print(name)
1921+ thing.master
1922 >>> master_conf = master_schema.load(master_local_conf)
1923 >>> sections = master_conf.getByCategory('thing')
1924- >>> sorted(section.name for section in sections)
1925- ['thing.one', 'thing.two']
1926- >>> sorted(section.foo for section in sections)
1927- ['1', '2']
1928- >>> print master_conf.thing.one.name
1929- thing.one
1930-
1931-The shared.conf file derives the keys and default values from the
1932-schema. This config was loaded before local.conf because its sections
1933-and values are required to be in place before local.conf applies its
1934-changes.
1935-
1936- >>> shared_conf = path.join(testfiles_dir, 'shared.conf')
1937- >>> shared_file = open(shared_conf, 'r')
1938- >>> raw_conf = shared_file.read()
1939- >>> shared_file.close()
1940- >>> print raw_conf
1941+ >>> for name in sorted(section.name for section in sections):
1942+ ... print(name)
1943+ thing.one
1944+ thing.two
1945+ >>> for name in sorted(section.foo for section in sections):
1946+ ... print(name)
1947+ 1
1948+ 2
1949+ >>> print(master_conf.thing.one.name)
1950+ thing.one
1951+
1952+The ``shared.conf`` file derives the keys and default values from the schema.
1953+This config was loaded before ``local.conf`` because its sections and values
1954+are required to be in place before ``local.conf`` applies its changes.
1955+
1956+ >>> shared_config = resource_filename(
1957+ ... 'lazr.config.tests.testdata', 'shared.conf')
1958+ >>> with open(shared_config, 'rt') as shared_file:
1959+ ... raw_conf = shared_file.read()
1960+ >>> print(raw_conf)
1961 # The schema is defined by base.conf.
1962 # Localize a key for section_1.
1963 [section_1]
1964 key2: sharing is fun
1965 key5: shared value
1966
1967-The config that was loaded has name and filename attributes to identify
1968-the configuration.
1969+The config that was loaded has ``name`` and ``filename`` attributes to
1970+identify the configuration.
1971
1972- >>> config.name
1973- 'local.conf'
1974- >>> config.filename
1975- '...lazr/config/tests/testdata/local.conf'
1976+ >>> print(config.name)
1977+ local.conf
1978+ >>> print('file:', config.filename)
1979+ file: ...lazr/config/tests/testdata/local.conf
1980
1981 The config can access the schema via the schema property.
1982
1983- >>> config.schema.name
1984- 'base.conf'
1985+ >>> print(config.schema.name)
1986+ base.conf
1987 >>> config.schema is schema
1988 True
1989
1990-A config is made up of multiple Sections like the schema. They can be
1991-iterated over in a loop as needed. This config inherited several
1992-sections defined in schema. Note that the meta section is not present
1993-because it pertains to the config system, not to the processes being
1994-configured.
1995+A config is made up of multiple Sections like the schema. They can be
1996+iterated over in a loop as needed. This config inherited several sections
1997+defined in schema. Note that the meta section is not present because it
1998+pertains to the config system, not to the processes being configured.
1999
2000 >>> for section in sorted(config, key=attrgetter('name')):
2001- ... print section.name
2002+ ... print(section.name)
2003 section-2.app-b
2004 section-5
2005 section_1
2006@@ -491,11 +422,11 @@
2007 >>> 'bad-section' in config
2008 False
2009
2010-Optional SchemaSections are not inherited by the config. A config file
2011-must declare all optional sections. Including the section heading is
2012-enough to inherit the section and its keys. The config file may localize
2013-the keys by declaring them too. The local.conf file includes
2014-'section-5', but not 'section_3.app_a'
2015+Optional SchemaSections are not inherited by the config. A config file must
2016+declare all optional sections. Including the section heading is enough to
2017+inherit the section and its keys. The config file may localize the keys by
2018+declaring them too. The ``local.conf`` file includes ``section-5``, but not
2019+``section_3.app_a``.
2020
2021
2022 >>> 'section_3.app_a' in config
2023@@ -504,7 +435,6 @@
2024 True
2025 >>> config.schema['section_3.app_a'].optional
2026 True
2027-
2028 >>> 'section-5' in config
2029 True
2030 >>> 'section-5' in config.schema
2031@@ -512,81 +442,65 @@
2032 >>> config.schema['section-5'].optional
2033 True
2034
2035-A Section can be accessed using subscript notation. Accessing a section
2036-that does not exist will raise a NoSectionError.
2037+A Section can be accessed using subscript notation. Accessing a section that
2038+does not exist will raise a NoSectionError. NoSectionError is raised for a
2039+undeclared optional sections too.
2040
2041 >>> section_1 = config['section_1']
2042 >>> section_1.name in config
2043 True
2044
2045- >>> config['section-4']
2046- Traceback (most recent call last):
2047- ...
2048- NoSectionError: ...
2049-
2050-NoSectionError is raised for a undeclared optional sections too.
2051-
2052- >>> config['section_3.app_a']
2053- Traceback (most recent call last):
2054- ...
2055- NoSectionError: ...
2056-
2057-Config supports category access like Schema does. The list of
2058-categories are returned by the category_names property.
2059-
2060- >>> sorted(config.category_names)
2061- ['section-2', 'section_3']
2062+Config supports category access like Schema does. The list of categories are
2063+returned by the ``category_names`` property.
2064+
2065+ >>> for name in sorted(config.category_names):
2066+ ... print(name)
2067+ section-2
2068+ section_3
2069
2070 All the sections that belong to a category can be retrieved using the
2071-getByCategory() method.
2072+``getByCategory()`` method.
2073
2074 >>> for section in config.getByCategory('section_3'):
2075- ... print section_schema.name
2076+ ... print(section_schema.name)
2077 section_3.app_b
2078
2079 Passing a non-existent category_name to the method will raise a
2080-NoCategoryError.
2081-
2082- >>> config.getByCategory('non-section')
2083- Traceback (most recent call last):
2084- ...
2085- NoCategoryError: ...
2086-
2087-As with schemas, you can pass a default argument to getByCategory() to avoid
2088-the exception.
2089+NoCategoryError. As with schemas, you can pass a default argument to
2090+``getByCategory()`` to avoid the exception.
2091
2092 >>> missing = object()
2093 >>> config.getByCategory('non-section', missing) is missing
2094 True
2095
2096
2097-=======
2098 Section
2099 =======
2100
2101-A Section behaves similar to a dictionary. It has keys and values.
2102-It supports some specialize access methods and properties for working
2103-with the values. Each Section has a name. Continuing with section_1
2104-from above....
2105+A Section behaves similar to a dictionary. It has keys and values. It
2106+supports some specialize access methods and properties for working with the
2107+values. Each Section has a name.
2108
2109 >>> from lazr.config.interfaces import ISection
2110 >>> verifyObject(ISection, section_1)
2111 True
2112-
2113- >>> section_1.name
2114- 'section_1'
2115-
2116-Like SectionSchemas, sections can return a 2-tuple of their category
2117-name and specific name parts. The category name will be None if the
2118-section's name does not contain a category.
2119-
2120- >>> config['section_3.app_b'].category_and_section_names
2121- ('section_3', 'app_b')
2122-
2123- >>> section_1.category_and_section_names
2124- (None, 'section_1')
2125-
2126-The Section's type is the same type as the ConfigSchema.section_factory.
2127+ >>> print(section_1.name)
2128+ section_1
2129+
2130+Like SectionSchemas, sections can return a 2-tuple of their category name and
2131+specific name parts. The category name will be ``None`` if the section's name
2132+does not contain a category.
2133+
2134+ >>> for name in config['section_3.app_b'].category_and_section_names:
2135+ ... print(name)
2136+ section_3
2137+ app_b
2138+ >>> for name in section_1.category_and_section_names:
2139+ ... print(name)
2140+ None
2141+ section_1
2142+
2143+The Section's type is the same type as the ``ConfigSchema.section_factory``.
2144
2145 >>> section_1
2146 <lazr.config...Section object at ...>
2147@@ -603,10 +517,10 @@
2148 A key can be accessed directly using as a subscript of the Section.
2149 The value is always a string.
2150
2151- >>> section_1['key3']
2152- 'Launchpad&nbsp;rocks'
2153- >>> section_1['key5']
2154- 'local value'
2155+ >>> print(section_1['key3'])
2156+ Launchpad&nbsp;rocks
2157+ >>> print(section_1['key5'])
2158+ local value
2159
2160 An error is raised if a non-existent key is accessed via a subscript.
2161
2162@@ -615,37 +529,36 @@
2163 ...
2164 KeyError: ...
2165
2166-The Section keys can be iterated. The section has all the keys from the
2167-SectionSchema. The values came form the schema's default values, then
2168-the values from shared.conf were applied, and lastly, the values from
2169-local.conf were applied. The schema provided the values of key1, key3,
2170-and key4, shared.conf provided the value of key2. local.conf provided
2171-key5. While shared.conf provided a key5, local.conf takes precedence.
2172+The Section keys can be iterated over. The section has all the keys from the
2173+SectionSchema. The values came form the schema's default values, then the
2174+values from ``shared.conf`` were applied, and lastly, the values from
2175+``local.conf`` were applied. The schema provided the values of ``key1``,
2176+``key3``, and ``key4``. ``shared.conf`` provided the value of ``key2``
2177+. ``local.conf`` provided ``key5``. While ``shared.conf`` provided a
2178+``key5``, ``local.conf`` takes precedence.
2179
2180 >>> for key in sorted(section_1):
2181- ... print key, ':', section_1[key]
2182+ ... print(key, ':', section_1[key])
2183 key1 : foo
2184 key2 : sharing is fun
2185 key3 : Launchpad&nbsp;rocks
2186 key4 : F&#028c;k yeah!
2187 key5 : local value
2188-
2189 >>> section_1.schema['key5']
2190 ''
2191
2192-The schema provided mandatory sections and default values to the
2193-config. So while the config file did not declare all the sections, they
2194-are present. In the case of section_3.app_b, its keys were defined in a
2195-template section.
2196+The schema provided mandatory sections and default values to the config. So
2197+while the config file did not declare all the sections, they are present. In
2198+the case of ``section_3.app_b``, its keys were defined in a template section.
2199
2200 >>> for key in sorted(config['section_3.app_b']):
2201- ... print key, ':', config['section_3.app_b'][key]
2202+ ... print(key, ':', config['section_3.app_b'][key])
2203 key1 : 17
2204 key2 : changed
2205 key3 : unique
2206
2207-Sections attributes cannot be directly set to shadow config options. An
2208-AttributeError is raised when a callsite attempts to mutate the config.
2209+Sections attributes cannot be directly set to shadow config options. An
2210+``AttributeError`` is raised when an attempt is made to mutate the config.
2211
2212 >>> config['section_3.app_b'].key1 = 'fail'
2213 Traceback (most recent call last):
2214@@ -660,96 +573,73 @@
2215 AttributeError: Config options cannot be set directly.
2216
2217
2218-==================
2219 Validating configs
2220 ==================
2221
2222-Config provides the validate() method to verify that the config is valid
2223-according to the schema. The method returns True if the config is valid.
2224+Config provides the ``validate()`` method to verify that the config is valid
2225+according to the schema. The method returns ``True`` if the config is valid.
2226
2227 >>> config.validate()
2228 True
2229
2230-When the config is not valid, a ConfigErrors is raised. The
2231-exception has an errors property that contains a list of all the
2232-errors in the config.
2233-
2234- >>> from lazr.config.interfaces import ConfigErrors
2235-
2236- >>> try:
2237- ... bad_config.validate()
2238- ... except ConfigErrors, validation_error:
2239- ... print validation_error
2240- ... for error in validation_error.errors:
2241- ... print "%s: %s" % (error.__class__.__name__, error)
2242- ConfigErrors: bad conf is not valid.
2243- UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in ... range(128)
2244- UnknownKeyError: section_1 does not have a keyn key.
2245- UnknownKeyError: The meta section does not have a metakey key.
2246- UnknownSectionError: base.conf does not have a unknown-section section.
2247-
2248-
2249-===============
2250+When the config is not valid, a ConfigErrors is raised. The exception has an
2251+``errors`` property that contains a list of all the errors in the config.
2252+
2253+
2254 Config overlays
2255 ===============
2256
2257-A conf file may contains a meta section that is used by the config
2258-system. The config data can access the config it extended using the
2259-extends property. The object is just the config data; it does not
2260-have any config methods.
2261+A conf file may contain a meta section that is used by the config system. The
2262+config data can access the config it extended using the ``extends`` property.
2263+The object is just the config data; it does not have any config methods.
2264
2265- >>> config.extends.name
2266- 'shared.conf'
2267+ >>> print(config.extends.name)
2268+ shared.conf
2269
2270 >>> verifyObject(IConfigData, config.extends)
2271 True
2272- >>> verifyObject(IStackableConfig, config.extends)
2273- Traceback (most recent call last):
2274- ...
2275- DoesNotImplement: ...
2276
2277-As Config supports inheritance through the extends key, each conf file
2278-produces instance of ConfigData, called an overlay. ConfigData
2279-represents the state of a config. The overlays property is a stack of
2280-ConfigData as it was constructed from the schema's config to the last
2281-config file that was loaded.
2282+As Config supports inheritance through the ``extends`` key, each conf file
2283+produces instance of ConfigData, called an *overlay*. ConfigData represents
2284+the state of a config. The ``overlays`` property is a stack of ConfigData as
2285+it was constructed from the schema's config to the last config file that was
2286+loaded.
2287
2288 >>> for config_data in config.overlays:
2289- ... print config_data.name
2290+ ... print(config_data.name)
2291 local.conf
2292 shared.conf
2293 base.conf
2294-
2295 >>> verifyObject(IConfigData, config.overlays[-1])
2296 True
2297
2298-Conf files can use the extends key to specify that it extends a schema
2299-without incurring a processing penalty by loading the schema twice in a
2300-row. The schema can never be the second item in the overlays stack.
2301+Conf files can use the ``extends`` key to specify that it extends a schema
2302+without incurring a processing penalty by loading the schema twice in a row.
2303+The schema can never be the second item in the overlays stack.
2304
2305 >>> single_config = schema.load(schema.filename)
2306 >>> for config_data in single_config.overlays:
2307- ... print config_data.name
2308+ ... print(config_data.name)
2309 base.conf
2310-
2311- >>> single_config.push(schema.filename, raw_schema)
2312+ >>> single_config.push(schema.filename, raw_schema.decode('utf-8'))
2313 >>> for config_data in single_config.overlays:
2314- ... print config_data.name
2315+ ... print(config_data.name)
2316 base.conf
2317
2318
2319 push()
2320 ======
2321
2322-Raw config data can be merged with the config to create a new overlay
2323-for testing. The push() method accepts a string of config data. The
2324-data must conform to the schema. The 'section_1' sections's keys are
2325-updated when the unparsed data is pushed onto the config. Note that
2326-indented unparsed data is passed to push() in thie example; push()
2327-does not require tests to dedent the test data.
2328+Raw config data can be merged with the config to create a new overlay for
2329+testing. The ``push()`` method accepts a string of config data. The data
2330+must conform to the schema. The ``section_1`` sections's keys are updated
2331+when the unparsed data is pushed onto the config. Note that indented,
2332+unparsed data is passed to ``push()`` in this example; ``push()`` does not
2333+require tests to dedent the test data.
2334+::
2335
2336 >>> for key in sorted(config['section_1']):
2337- ... print key, ':', config['section_1'][key]
2338+ ... print(key, ':', config['section_1'][key])
2339 key1 : foo
2340 key2 : sharing is fun
2341 key3 : Launchpad&nbsp;rocks
2342@@ -763,16 +653,17 @@
2343 >>> config.push('test config', test_data)
2344
2345 >>> for key in sorted(config['section_1']):
2346- ... print key, ':', config['section_1'][key]
2347+ ... print(key, ':', config['section_1'][key])
2348 key1 : test1
2349 key2 : sharing is fun
2350 key3 : Launchpad&nbsp;rocks
2351 key4 : F&#028c;k yeah!
2352 key5 :
2353
2354-Besides updating section keys, optional sections can be enabled too.
2355-The 'section_3.app_a' section is enabled with the default keys from the
2356-schema in this example.
2357+Besides updating section keys, optional sections can be enabled too. The
2358+``section_3.app_a`` section is enabled with the default keys from the schema
2359+in this example.
2360+::
2361
2362 >>> config.schema['section_3.app_a'].optional
2363 True
2364@@ -785,41 +676,41 @@
2365 >>> 'section_3.app_a' in config
2366 True
2367 >>> for key in sorted(config['section_3.app_a']):
2368- ... print key, ':', config['section_3.app_a'][key]
2369+ ... print(key, ':', config['section_3.app_a'][key])
2370 key1 : 17
2371 key2 : 3.1415
2372
2373 >>> for key in sorted(config.schema['section_3.app_a']):
2374- ... print key, ':', config.schema['section_3.app_a'][key]
2375+ ... print(key, ':', config.schema['section_3.app_a'][key])
2376 key1 : 17
2377 key2 : 3.1415
2378
2379-The config's name and overlays are updated by push().
2380+The config's name and overlays are updated by ``push()``.
2381
2382- >>> config.name
2383- 'test app_a'
2384- >>> config.filename
2385- 'test app_a'
2386+ >>> print(config.name)
2387+ test app_a
2388+ >>> print(config.filename)
2389+ test app_a
2390 >>> for config_data in config.overlays:
2391- ... print config_data.name
2392+ ... print(config_data.name)
2393 test app_a
2394 test config
2395 local.conf
2396 shared.conf
2397 base.conf
2398
2399-The 'test app_a' did not declare an extends key in a meta section. Its
2400-extends property is None, even though it implicitly extends 'test
2401-config'. The extends property only provides access to configs that are
2402-explicitly extended.
2403-
2404- >>> config.extends.name
2405- 'test config'
2406-
2407-The config's sections are updated with 'section_3.app_a' too.
2408+The ``test app_a`` config did not declare an ``extends`` key in a ``meta``
2409+section. Its ``extends`` property is ``None``, even though it implicitly
2410+extends ``test config``. The ``extends`` property only provides access to
2411+configs that are explicitly extended.
2412+
2413+ >>> print(config.extends.name)
2414+ test config
2415+
2416+The config's sections are updated with ``section_3.app_a`` too.
2417
2418 >>> for section in sorted(config, key=attrgetter('name')):
2419- ... print section.name
2420+ ... print(section.name)
2421 section-2.app-b
2422 section-5
2423 section_1
2424@@ -827,17 +718,18 @@
2425 section_3.app_b
2426 section_33
2427
2428-A config file may state that it extends its schema (to clearly connect
2429-the config to the schema). The schema can also be pushed to reset the
2430-values in the config to the schema's default values.
2431+A config file may state that it extends its schema (to clearly connect the
2432+config to the schema). The schema can also be pushed to reset the values in
2433+the config to the schema's default values.
2434
2435- >>> extender_conf_name = path.join(testfiles_dir, 'extender.conf')
2436+ >>> extender_conf_name = resource_filename(
2437+ ... 'lazr.config.tests.testdata', 'extender.conf')
2438 >>> extender_conf_data = ("""
2439 ... [meta]
2440 ... extends: base.conf""")
2441 >>> config.push(extender_conf_name, extender_conf_data)
2442 >>> for config_data in config.overlays:
2443- ... print config_data.name
2444+ ... print(config_data.name)
2445 extender.conf
2446 base.conf
2447 test app_a
2448@@ -846,22 +738,23 @@
2449 shared.conf
2450 base.conf
2451
2452-The 'section_1' section was restored to the schema's default values.
2453+The ``section_1`` section was restored to the schema's default values.
2454
2455 >>> for key in sorted(config['section_1']):
2456- ... print key, ':', config['section_1'][key]
2457+ ... print(key, ':', config['section_1'][key])
2458 key1 : foo
2459 key2 : bar and baz
2460 key3 : Launchpad&nbsp;rocks
2461 key4 : F&#028c;k yeah!
2462 key5 :
2463
2464-push() can also be used to extend master sections.
2465+``push()`` can also be used to extend master sections.
2466+::
2467
2468 >>> sections = sorted(master_conf.getByCategory('bar'),
2469 ... key=attrgetter('name'))
2470 >>> for section in sections:
2471- ... print section.name, section.baz
2472+ ... print(section.name, section.baz)
2473 bar.master badger
2474 bar.soup cougar
2475
2476@@ -872,7 +765,7 @@
2477 >>> sections = sorted(master_conf.getByCategory('bar'),
2478 ... key=attrgetter('name'))
2479 >>> for section in sections:
2480- ... print section.name, section.baz
2481+ ... print(section.name, section.baz)
2482 bar.soup cougar
2483 bar.two dolphin
2484
2485@@ -883,67 +776,70 @@
2486 >>> sections = sorted(master_conf.getByCategory('bar'),
2487 ... key=attrgetter('name'))
2488 >>> for section in sections:
2489- ... print section.name, section.baz
2490+ ... print(section.name, section.baz)
2491 bar.soup cougar
2492 bar.three emu
2493 bar.two dolphin
2494
2495-push() works with master sections too.
2496+``push()`` works with master sections too.
2497+::
2498
2499- >>> schema_file = StringIO.StringIO("""\
2500+ >>> schema_file = StringIO("""\
2501 ... [thing.master]
2502 ... foo: 0
2503 ... bar: 0
2504 ... """)
2505 >>> push_schema = ConfigSchema('schema.cfg', schema_file)
2506
2507- >>> config_file = StringIO.StringIO("""\
2508+ >>> config_file = StringIO("""\
2509 ... [thing.one]
2510 ... foo: 1
2511 ... """)
2512 >>> push_config = push_schema.loadFile(config_file, 'config.cfg')
2513- >>> print push_config.thing.one.foo
2514+ >>> print(push_config.thing.one.foo)
2515 1
2516- >>> print push_config.thing.one.bar
2517+ >>> print(push_config.thing.one.bar)
2518 0
2519
2520 >>> push_config.push('test.cfg', """\
2521 ... [thing.one]
2522 ... bar: 2
2523 ... """)
2524- >>> print push_config.thing.one.foo
2525+ >>> print(push_config.thing.one.foo)
2526 1
2527- >>> print push_config.thing.one.bar
2528+ >>> print(push_config.thing.one.bar)
2529 2
2530
2531
2532 pop()
2533 =====
2534
2535-ConfigData can be removed from the stack of overlays using the pop()
2536-method. The methods returns the list of ConfigData that was removed--a
2537+ConfigData can be removed from the stack of overlays using the ``pop()``
2538+method. The methods returns the list of ConfigData that was removed -- a
2539 slice from the specified ConfigData to the top of the stack.
2540+::
2541
2542 >>> overlays = config.pop('test config')
2543 >>> for config_data in overlays:
2544- ... config_data.name
2545- 'extender.conf'
2546- 'base.conf'
2547- 'test app_a'
2548- 'test config'
2549+ ... print(config_data.name)
2550+ extender.conf
2551+ base.conf
2552+ test app_a
2553+ test config
2554
2555 >>> for config_data in config.overlays:
2556- ... print config_data.name
2557+ ... print(config_data.name)
2558 local.conf
2559 shared.conf
2560 base.conf
2561
2562-The config's state was restored to the ConfigData that is top of the
2563-overlay stack. Section 'section_3.app_a' was removed completely. The
2564-keys ('key1' and 'key5') for 'section_1' were restored.
2565+The config's state was restored to the ConfigData that is on top of the
2566+overlay stack. Section ``section_3.app_a`` was removed completely. The keys
2567+(``key1`` and ``key5``) for ``section_1`` were restored.
2568+::
2569
2570 >>> for section in sorted(config, key=attrgetter('name')):
2571- ... print section.name
2572+ ... print(section.name)
2573 section-2.app-b
2574 section-5
2575 section_1
2576@@ -951,46 +847,32 @@
2577 section_33
2578
2579 >>> for key in sorted(config['section_1']):
2580- ... print key, ':', config['section_1'][key]
2581+ ... print(key, ':', config['section_1'][key])
2582 key1 : foo
2583 key2 : sharing is fun
2584 key3 : Launchpad&nbsp;rocks
2585 key4 : F&#028c;k yeah!
2586 key5 : local value
2587
2588-Call the pop() method with an unknown conf_name raises an error
2589-
2590- >>> overlays = config.pop('bad-name')
2591- Traceback (most recent call last):
2592- ...
2593- NoConfigError: No config with name: bad-name.
2594-
2595-A Config must have at least one ConfigData in the overlays stack so that
2596-it has data. The bottom ConfigData in the overlays was made from the
2597-schema's required sections. It cannot be removed by the pop() method.
2598-
2599- >>> overlays = config.pop('base.conf')
2600- Traceback (most recent call last):
2601- ...
2602- NoConfigError: Cannot pop the schema's default config.
2603+A Config must have at least one ConfigData in the overlays stack so that it
2604+has data. The bottom ConfigData in the overlays was made from the schema's
2605+required sections. It cannot be removed by the ``pop()`` method.
2606
2607 If all but the bottom ConfigData is popped from overlays, the extends
2608 property returns None.
2609
2610 >>> overlays = config.pop('shared.conf')
2611- >>> print config.extends
2612+ >>> print(config.extends)
2613 None
2614
2615
2616-===============================
2617 Attribute access to config data
2618 ===============================
2619
2620-Config provides attribute-based access to its members. So long as the
2621-section, category, and key names conform to Python identifier naming
2622-rules, they can be accessed as attributes. The Python code will not
2623-compile, or will cause a runtime error if the object being accessed has
2624-a bad name.
2625+Config provides attribute-based access to its members. So long as the
2626+section, category, and key names conform to Python identifier naming rules,
2627+they can be accessed as attributes. The Python code will not compile, or will
2628+cause a runtime error if the object being accessed has a bad name.
2629
2630 Sections appear to be attributes of the config.
2631
2632@@ -998,15 +880,15 @@
2633 >>> config.section_1 is config['section_1']
2634 True
2635
2636-Accessing an unknown section, or a section whose name is not a valid
2637-Python identifier will raise an AttributeError.
2638+Accessing an unknown section, or a section whose name is not a valid Python
2639+identifier will raise an AttributeError.
2640
2641 >>> config.section-5
2642 Traceback (most recent call last):
2643 ...
2644 AttributeError: No section or category named section.
2645
2646-Categories may be accessed as attributes too. The ICategory interface
2647+Categories may be accessed as attributes too. The ICategory interface
2648 provides access to its sections as members.
2649
2650 >>> from lazr.config.interfaces import ICategory
2651@@ -1016,8 +898,8 @@
2652 >>> config_category.app_b is config['section_3.app_b']
2653 True
2654
2655-Like a config, a category will raise an AttributeError if it does not
2656-have a section that matches the identifier name.
2657+Like a config, a category will raise an AttributeError if it does not have a
2658+section that matches the identifier name.
2659
2660 >>> config_category.no_such_section
2661 Traceback (most recent call last):
2662@@ -1026,10 +908,10 @@
2663
2664 Section keys can be accessed directly as members.
2665
2666- >>> config.section_1.key2
2667- 'sharing is fun'
2668- >>> config.section_3.app_b.key2
2669- 'changed'
2670+ >>> print(config.section_1.key2)
2671+ sharing is fun
2672+ >>> print(config.section_3.app_b.key2)
2673+ changed
2674
2675 Accessing a non-existent section key as an attribute will raise an
2676 AttributeError.
2677@@ -1040,27 +922,25 @@
2678 AttributeError: No section key named non_key.
2679
2680
2681-====================
2682 Implicit data typing
2683 ====================
2684
2685-The ImplicitTypeSchema can create configs that support implicit
2686-datatypes. The value of a Section key is automatically converted from
2687-str to the type the value appears to be. Implicit typing does not add
2688-any validation support; it adds type casting conveniences for the
2689-developer.
2690+The ImplicitTypeSchema can create configs that support implicit datatypes.
2691+The value of a Section key is automatically converted from ``str`` to the type
2692+the value appears to be. Implicit typing does not add any validation support;
2693+it adds type casting conveniences for the developer.
2694
2695-An ImplicitTypeSchema can be used to parse the same schema and conf
2696-files that Schema uses.
2697+An ImplicitTypeSchema can be used to parse the same schema and conf files that
2698+Schema uses.
2699
2700 >>> from lazr.config import ImplicitTypeSchema
2701-
2702 >>> implicit_schema = ImplicitTypeSchema(base_conf)
2703 >>> verifyObject(IConfigSchema, implicit_schema)
2704 True
2705
2706 The config loaded by ImplicitTypeSchema is the same class with the same
2707 sections as is made by Schema.
2708+::
2709
2710 >>> implicit_config = implicit_schema.load(local_conf)
2711 >>> implicit_config
2712@@ -1082,8 +962,9 @@
2713 >>> implicit_config['section_3.app_b']
2714 <lazr.config...ImplicitTypeSection object at ...>
2715
2716-ImplicitTypeSection, in contrast to Section, converts values that
2717-appear to be integer or boolean into ints and bools.
2718+ImplicitTypeSection, in contrast to Section, converts values that appear to be
2719+integer or boolean into ints and bools.
2720+::
2721
2722 >>> config['section_3.app_b']['key1']
2723 '17'
2724@@ -1103,11 +984,11 @@
2725 >>> implicit_config['section-2.app-b'].key1
2726 True
2727
2728-ImplicitTypeSection uses a private method that employs heuristic rules
2729-to convert strings into simple types. It may return a str, bool, or int.
2730-When the argument is the word 'true' or 'false' (in any case), a bool is
2731-returned. Values like 'yes', 'no', '0', and '1' are not converted to
2732-bool.
2733+ImplicitTypeSection uses a private method that employs heuristic rules to
2734+convert strings into simple types. It may return a str, bool, or int. When
2735+the argument is the word 'true' or 'false' (in any case), a bool is returned.
2736+Values like 'yes', 'no', '0', and '1' are not converted to bool.
2737+::
2738
2739 >>> convert = implicit_config['section_1']._convert
2740
2741@@ -1118,31 +999,33 @@
2742 >>> convert('tRue')
2743 True
2744
2745- >>> convert('yes')
2746- 'yes'
2747+ >>> print(convert('yes'))
2748+ yes
2749 >>> convert('1')
2750 1
2751- >>> convert('True or False')
2752- 'True or False'
2753-
2754-When the argument is the word 'none', None is returned. The token in the
2755-config means the key has no value.
2756-
2757- >>> print convert('none')
2758- None
2759- >>> print convert('None')
2760- None
2761- >>> print convert('nonE')
2762- None
2763-
2764- >>> convert('none today')
2765- 'none today'
2766- >>> convert('nonevident')
2767- 'nonevident'
2768-
2769-When the argument is an unbroken sequence of numbers, an int is
2770-returned. The number may have a leading positive or negative. Octal and
2771-hex notation is not supported.
2772+ >>> print(convert('True or False'))
2773+ True or False
2774+
2775+When the argument is the word ``none``, ``None`` is returned. The token in
2776+the config means the key has no value.
2777+::
2778+
2779+ >>> print(convert('none'))
2780+ None
2781+ >>> print(convert('None'))
2782+ None
2783+ >>> print(convert('nonE'))
2784+ None
2785+
2786+ >>> print(convert('none today'))
2787+ none today
2788+ >>> print(convert('nonevident'))
2789+ nonevident
2790+
2791+When the argument is an unbroken sequence of numbers, an int is returned. The
2792+number may have a leading positive or negative. Octal and hex notation is not
2793+supported.
2794+::
2795
2796 >>> convert('0')
2797 0
2798@@ -1155,37 +1038,28 @@
2799 >>> convert('0100')
2800 100
2801
2802- >>> convert('2001-01-01')
2803- '2001-01-01'
2804- >>> convert('1000*60*5')
2805- '1000*60*5'
2806- >>> convert('1000 * 60 * 5')
2807- '1000 * 60 * 5'
2808- >>> convert('1,024')
2809- '1,024'
2810- >>> convert('0.5')
2811- '0.5'
2812- >>> convert('0x100')
2813- '0x100'
2814+ >>> print(convert('2001-01-01'))
2815+ 2001-01-01
2816+ >>> print(convert('1000*60*5'))
2817+ 1000*60*5
2818+ >>> print(convert('1000 * 60 * 5'))
2819+ 1000 * 60 * 5
2820+ >>> print(convert('1,024'))
2821+ 1,024
2822+ >>> print(convert('0.5'))
2823+ 0.5
2824+ >>> print(convert('0x100'))
2825+ 0x100
2826
2827 Multiline values are always strings, with white space (and line breaks)
2828-removed from the beginning/end.
2829-
2830- >>> convert("""multiline value 1
2831- ... multiline value 2""")
2832- 'multiline value 1\n multiline value 2'
2833-
2834- >>> convert("""
2835- ... multiline value 1
2836- ... multiline value 2
2837- ... """)
2838- 'multiline value 1\n multiline value 2'
2839-
2840- >>> implicit_config['section_33'].key2
2841- 'multiline value 1\nmultiline value 2'
2842-
2843-
2844-=======================
2845+removed from the beginning and end.
2846+
2847+ >>> print(convert("""multiline value 1
2848+ ... multiline value 2"""))
2849+ multiline value 1
2850+ multiline value 2
2851+
2852+
2853 Type conversion helpers
2854 =======================
2855
2856@@ -1195,10 +1069,10 @@
2857
2858
2859 Booleans
2860-========
2861+--------
2862
2863-There is a helper for turning various strings into the boolean values True and
2864-False.
2865+There is a helper for turning various strings into the boolean values ``True``
2866+and ``False``.
2867
2868 >>> from lazr.config import as_boolean
2869
2870@@ -1206,8 +1080,8 @@
2871 enable.
2872
2873 >>> for value in ('true', 'yes', 'on', 'enable', 'enabled', '1'):
2874- ... print value, '->', as_boolean(value)
2875- ... print value.upper(), '->', as_boolean(value.upper())
2876+ ... print(value, '->', as_boolean(value))
2877+ ... print(value.upper(), '->', as_boolean(value.upper()))
2878 true -> True
2879 TRUE -> True
2880 yes -> True
2881@@ -1225,8 +1099,8 @@
2882 disable.
2883
2884 >>> for value in ('false', 'no', 'off', 'disable', 'disabled', '0'):
2885- ... print value, '->', as_boolean(value)
2886- ... print value.upper(), '->', as_boolean(value.upper())
2887+ ... print(value, '->', as_boolean(value))
2888+ ... print(value.upper(), '->', as_boolean(value.upper()))
2889 false -> False
2890 FALSE -> False
2891 no -> False
2892@@ -1249,46 +1123,53 @@
2893
2894
2895 Host and port
2896-=============
2897+-------------
2898
2899-There is a helper for converting from a host:port string to a 2-tuple of
2900-(host, port).
2901+There is a helper for converting from a ``host:port`` string to a 2-tuple of
2902+``(host, port)``.
2903
2904 >>> from lazr.config import as_host_port
2905- >>> as_host_port('host:25')
2906- ('host', 25)
2907+ >>> host, port = as_host_port('host:25')
2908+ >>> print(host, port)
2909+ host 25
2910
2911 The port string is optional, in which case, port 25 is the default (for
2912 historical reasons).
2913
2914- >>> as_host_port('host')
2915- ('host', 25)
2916+ >>> host, port = as_host_port('host')
2917+ >>> print(host, port)
2918+ host 25
2919
2920 The default port can be overridden.
2921
2922- >>> as_host_port('host', default_port=22)
2923- ('host', 22)
2924+ >>> host, port = as_host_port('host', default_port=22)
2925+ >>> print(host, port)
2926+ host 22
2927
2928 The default port is ignored if it is given in the value.
2929
2930- >>> as_host_port('host:80', default_port=22)
2931- ('host', 80)
2932+ >>> host, port = as_host_port('host:80', default_port=22)
2933+ >>> print(host, port)
2934+ host 80
2935
2936 The host name is also optional, as denoted by a leading colon. When omitted,
2937 localhost is used.
2938
2939- >>> as_host_port(':80')
2940- ('localhost', 80)
2941+ >>> host, port = as_host_port(':80')
2942+ >>> print(host, port)
2943+ localhost 80
2944
2945 The default host name can be overridden though.
2946
2947- >>> as_host_port(':80', default_host='myhost')
2948- ('myhost', 80)
2949+ >>> host, port = as_host_port(':80', default_host='myhost')
2950+ >>> print(host, port)
2951+ myhost 80
2952
2953 The default host name is ignored if the value string contains it.
2954
2955- >>> as_host_port('yourhost:80', default_host='myhost')
2956- ('yourhost', 80)
2957+ >>> host, port = as_host_port('yourhost:80', default_host='myhost')
2958+ >>> print(host, port)
2959+ yourhost 80
2960
2961 A ValueError occurs if the port number in the configuration value string is
2962 not an integer.
2963@@ -1300,10 +1181,10 @@
2964
2965
2966 User and group
2967-==============
2968+--------------
2969
2970-A helper is provided for turning a chown(1)-style user:group specification
2971-into a 2-tuple of the user name and group name.
2972+A helper is provided for turning a ``chown(1)``-style ``user:group``
2973+specification into a 2-tuple of the user name and group name.
2974
2975 >>> from lazr.config import as_username_groupname
2976
2977@@ -1317,14 +1198,16 @@
2978
2979 When both are given, the strings are returned unchanged or validated.
2980
2981- >>> as_username_groupname('person:group')
2982- ('person', 'group')
2983+ >>> user, group = as_username_groupname('person:group')
2984+ >>> print(user, group)
2985+ person group
2986
2987 Numeric values can be given, but they are not converted into their symbolic
2988 names.
2989
2990- >>> as_username_groupname('25:26')
2991- ('25', '26')
2992+ >>> uid, gid = as_username_groupname('25:26')
2993+ >>> print(uid, gid)
2994+ 25 26
2995
2996 By default the current user and group names are returned.
2997
2998@@ -1337,10 +1220,10 @@
2999
3000
3001 Time intervals
3002-==============
3003+--------------
3004
3005-One such converter accepts a range of 'time interval specifications', and
3006-returns a Python timedelta.
3007+This converter accepts a range of *time interval specifications*, and returns
3008+a Python timedelta_.
3009
3010 >>> from lazr.config import as_timedelta
3011
3012@@ -1349,22 +1232,22 @@
3013 >>> as_timedelta('45s')
3014 datetime.timedelta(0, 45)
3015
3016-The function also accepts suffixes 'm' for minutes...
3017+The function also accepts suffixes ``m`` for minutes...
3018
3019 >>> as_timedelta('3m')
3020 datetime.timedelta(0, 180)
3021
3022-...'h' for hours...
3023+...``h`` for hours...
3024
3025 >>> as_timedelta('2h')
3026 datetime.timedelta(0, 7200)
3027
3028-...and 'd' for days...
3029+...and ``d`` for days...
3030
3031 >>> as_timedelta('4d')
3032 datetime.timedelta(4)
3033
3034-...and 'w' for weeks.
3035+...and ``w`` for weeks.
3036
3037 >>> as_timedelta('4w')
3038 datetime.timedelta(28)
3039@@ -1381,7 +1264,7 @@
3040 >>> as_timedelta('4w2d9h3s')
3041 datetime.timedelta(30, 32403)
3042
3043-But doesn't accept 'weird' or duplicate combinations.
3044+But doesn't accept "weird" or duplicate combinations.
3045
3046 >>> as_timedelta('3s2s')
3047 Traceback (most recent call last):
3048@@ -1414,10 +1297,10 @@
3049
3050
3051 Log levels
3052-==========
3053+----------
3054
3055 It's convenient to be able to use symbolic log level names when using
3056-lazr.config to configure the Python logger.
3057+``lazr.config`` to configure the Python logger.
3058
3059 >>> from lazr.config import as_log_level
3060
3061@@ -1425,8 +1308,8 @@
3062
3063 >>> for value in ('critical', 'error', 'warning', 'info',
3064 ... 'debug', 'notset'):
3065- ... print value, '->', as_log_level(value)
3066- ... print value.upper(), '->', as_log_level(value.upper())
3067+ ... print(value, '->', as_log_level(value))
3068+ ... print(value.upper(), '->', as_log_level(value.upper()))
3069 critical -> 50
3070 CRITICAL -> 50
3071 error -> 40
3072@@ -1447,7 +1330,7 @@
3073 ...
3074 AttributeError: 'module' object has no attribute 'CHEESE'
3075
3076-===============
3077+
3078 Other Documents
3079 ===============
3080
3081@@ -1456,3 +1339,5 @@
3082
3083 *
3084 docs/*
3085+
3086+.. _timedelta: http://docs.python.org/3/library/datetime.html#timedelta-objects
3087
3088=== added file 'lazr/config/docs/usage_fixture.py'
3089--- lazr/config/docs/usage_fixture.py 1970-01-01 00:00:00 +0000
3090+++ lazr/config/docs/usage_fixture.py 2013-01-10 15:47:20 +0000
3091@@ -0,0 +1,27 @@
3092+# Copyright 2009-2013 Canonical Ltd. All rights reserved.
3093+#
3094+# This file is part of lazr.smtptest
3095+#
3096+# lazr.smtptest is free software: you can redistribute it and/or modify it
3097+# under the terms of the GNU Lesser General Public License as published by
3098+# the Free Software Foundation, version 3 of the License.
3099+#
3100+# lazr.smtptest is distributed in the hope that it will be useful, but WITHOUT
3101+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
3102+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
3103+# License for more details.
3104+#
3105+# You should have received a copy of the GNU Lesser General Public License
3106+# along with lazr.smtptest. If not, see <http://www.gnu.org/licenses/>.
3107+
3108+"""Doctest fixtures for running under nose."""
3109+
3110+from __future__ import absolute_import, print_function, unicode_literals
3111+
3112+__metaclass__ = type
3113+__all__ = [
3114+ 'globs',
3115+ ]
3116+
3117+
3118+from lazr.config.docs.fixture import globs
3119
3120=== modified file 'lazr/config/interfaces.py'
3121--- src/lazr/config/interfaces.py 2009-03-24 17:31:47 +0000
3122+++ lazr/config/interfaces.py 2013-01-10 15:47:20 +0000
3123@@ -1,4 +1,4 @@
3124-# Copyright 2007-2009 Canonical Ltd. All rights reserved.
3125+# Copyright 2007-2013 Canonical Ltd. All rights reserved.
3126 #
3127 # This file is part of lazr.config
3128 #
3129@@ -17,8 +17,9 @@
3130 # pylint: disable-msg=E0211,E0213,W0231
3131 """Interfaces for process configuration.."""
3132
3133+from __future__ import absolute_import, print_function, unicode_literals
3134+
3135 __metaclass__ = type
3136-
3137 __all__ = [
3138 'ConfigErrors',
3139 'ConfigSchemaError',
3140@@ -37,12 +38,8 @@
3141 'UnknownKeyError',
3142 'UnknownSectionError']
3143
3144-from warnings import filterwarnings
3145 from zope.interface import Interface, Attribute
3146
3147-# Ignore Python 2.6 deprecation warnings.
3148-filterwarnings('ignore', category=DeprecationWarning, module=r'lazr\.config')
3149-
3150
3151 class ConfigSchemaError(Exception):
3152 """A base class of all `IConfigSchema` errors."""
3153
3154=== modified file 'lazr/config/tests/__init__.py'
3155--- src/lazr/config/tests/__init__.py 2009-03-24 17:31:47 +0000
3156+++ lazr/config/tests/__init__.py 2013-01-10 15:47:20 +0000
3157@@ -1,17 +0,0 @@
3158-# Copyright 2007-2009 Canonical Ltd. All rights reserved.
3159-#
3160-# This file is part of lazr.config
3161-#
3162-# lazr.config is free software: you can redistribute it and/or modify it
3163-# under the terms of the GNU Lesser General Public License as published by
3164-# the Free Software Foundation, version 3 of the License.
3165-#
3166-# lazr.config is distributed in the hope that it will be useful, but WITHOUT
3167-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
3168-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
3169-# License for more details.
3170-#
3171-# You should have received a copy of the GNU Lesser General Public License
3172-# along with lazr.config. If not, see <http://www.gnu.org/licenses/>.
3173-
3174-"""Test package for lazr.config."""
3175
3176=== added file 'lazr/config/tests/test_config.py'
3177--- lazr/config/tests/test_config.py 1970-01-01 00:00:00 +0000
3178+++ lazr/config/tests/test_config.py 2013-01-10 15:47:20 +0000
3179@@ -0,0 +1,205 @@
3180+# Copyright 2008-2013 Canonical Ltd. All rights reserved.
3181+#
3182+# This file is part of lazr.config.
3183+#
3184+# lazr.config is free software: you can redistribute it and/or modify it
3185+# under the terms of the GNU Lesser General Public License as published by
3186+# the Free Software Foundation, version 3 of the License.
3187+#
3188+# lazr.config is distributed in the hope that it will be useful, but WITHOUT
3189+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
3190+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
3191+# License for more details.
3192+#
3193+# You should have received a copy of the GNU Lesser General Public License
3194+# along with lazr.config. If not, see <http://www.gnu.org/licenses/>.
3195+
3196+"""Tests of lazr.config."""
3197+
3198+from __future__ import absolute_import, print_function, unicode_literals
3199+
3200+__metaclass__ = type
3201+__all__ = [
3202+ 'TestConfig',
3203+ ]
3204+
3205+
3206+import unittest
3207+import pkg_resources
3208+try:
3209+ from configparser import MissingSectionHeaderError, NoSectionError
3210+except ImportError:
3211+ # Python 2
3212+ from ConfigParser import MissingSectionHeaderError, NoSectionError
3213+try:
3214+ from io import StringIO
3215+except ImportError:
3216+ # Python 2
3217+ from StringIO import StringIO
3218+
3219+from operator import attrgetter
3220+from zope.interface.exceptions import DoesNotImplement
3221+from zope.interface.verify import verifyObject
3222+
3223+from lazr.config import ConfigSchema, ImplicitTypeSchema
3224+from lazr.config.interfaces import (
3225+ ConfigErrors, IStackableConfig, InvalidSectionNameError, NoCategoryError,
3226+ NoConfigError, RedefinedSectionError, UnknownKeyError,
3227+ UnknownSectionError)
3228+
3229+
3230+class TestConfig(unittest.TestCase):
3231+ def setUp(self):
3232+ # Python 2.6 does not have assertMultilineEqual
3233+ self.meq = getattr(self, 'assertMultiLineEqual', self.assertEqual)
3234+
3235+ def _testfile(self, conf_file):
3236+ return pkg_resources.resource_filename(
3237+ 'lazr.config.tests.testdata', conf_file)
3238+
3239+ def test_missing_category(self):
3240+ schema = ConfigSchema(self._testfile('base.conf'))
3241+ self.assertRaises(NoCategoryError, schema.getByCategory, 'non-section')
3242+
3243+ def test_missing_file(self):
3244+ self.assertRaises(IOError, ConfigSchema, '/does/not/exist')
3245+
3246+ def test_must_be_ascii(self):
3247+ self.assertRaises(UnicodeError,
3248+ ConfigSchema, self._testfile('bad-nonascii.conf'))
3249+
3250+ def test_missing_schema_section(self):
3251+ schema = ConfigSchema(self._testfile('base.conf'))
3252+ self.assertRaises(NoSectionError, schema.__getitem__, 'section-4')
3253+
3254+ def test_missing_header_section(self):
3255+ self.assertRaises(MissingSectionHeaderError,
3256+ ConfigSchema, self._testfile('bad-sectionless.conf'))
3257+
3258+ def test_redefined_section(self):
3259+ self.assertRaises(RedefinedSectionError,
3260+ ConfigSchema,
3261+ self._testfile('bad-redefined-section.conf'))
3262+ # XXX sinzui 2007-12-13:
3263+ # ConfigSchema should raise RedefinedKeyError when a section redefines
3264+ # a key.
3265+
3266+ def test_invalid_section_name(self):
3267+ self.assertRaises(InvalidSectionNameError,
3268+ ConfigSchema,
3269+ self._testfile('bad-invalid-name.conf'))
3270+
3271+ def test_invalid_characters(self):
3272+ self.assertRaises(InvalidSectionNameError,
3273+ ConfigSchema,
3274+ self._testfile('bad-invalid-name-chars.conf'))
3275+
3276+ def test_load_missing_file(self):
3277+ schema = ConfigSchema(self._testfile('base.conf'))
3278+ self.assertRaises(IOError, schema.load, '/no/such/file.conf')
3279+
3280+ def test_no_name_argument(self):
3281+ config = """
3282+[meta]
3283+metakey: unsupported
3284+[unknown-section]
3285+key1 = value1
3286+[section_1]
3287+keyn: unknown key
3288+key1: bad character in caf\xc3)
3289+[section_3.template]
3290+key1: schema suffixes are not permitted
3291+"""
3292+ schema = ConfigSchema(self._testfile('base.conf'))
3293+ self.assertRaises(AttributeError, schema.loadFile, StringIO(config))
3294+
3295+ def test_missing_section(self):
3296+ schema = ConfigSchema(self._testfile('base.conf'))
3297+ config = schema.load(self._testfile('local.conf'))
3298+ self.assertRaises(NoSectionError, config.__getitem__, 'section-4')
3299+
3300+ def test_undeclared_optional_section(self):
3301+ schema = ConfigSchema(self._testfile('base.conf'))
3302+ config = schema.load(self._testfile('local.conf'))
3303+ self.assertRaises(NoSectionError,
3304+ config.__getitem__, 'section_3.app_a')
3305+
3306+ def test_nonexistent_category_name(self):
3307+ schema = ConfigSchema(self._testfile('base.conf'))
3308+ config = schema.load(self._testfile('local.conf'))
3309+ self.assertRaises(NoCategoryError,
3310+ config.getByCategory, 'non-section')
3311+
3312+ def test_all_config_errors(self):
3313+ schema = ConfigSchema(self._testfile('base.conf'))
3314+ config = schema.loadFile(StringIO("""
3315+[meta]
3316+metakey: unsupported
3317+[unknown-section]
3318+key1 = value1
3319+[section_1]
3320+keyn: unknown key
3321+key1: bad character in caf\xc3)
3322+[section_3.template]
3323+key1: schema suffixes are not permitted
3324+"""), 'bad config')
3325+ try:
3326+ config.validate()
3327+ except ConfigErrors as errors:
3328+ sorted_errors = sorted(
3329+ errors.errors, key=attrgetter('__class__.__name__'))
3330+ self.assertEqual(str(errors),
3331+ 'ConfigErrors: bad config is not valid.')
3332+ else:
3333+ self.fail('ConfigErrors expected')
3334+ self.assertEqual(len(sorted_errors), 4)
3335+ self.assertEqual([error.__class__ for error in sorted_errors],
3336+ [UnicodeEncodeError, UnknownKeyError,
3337+ UnknownKeyError, UnknownSectionError])
3338+
3339+ def test_not_stackable(self):
3340+ schema = ConfigSchema(self._testfile('base.conf'))
3341+ config = schema.load(self._testfile('local.conf'))
3342+ self.assertRaises(DoesNotImplement,
3343+ verifyObject, IStackableConfig, config.extends)
3344+
3345+ def test_bad_pop(self):
3346+ schema = ConfigSchema(self._testfile('base.conf'))
3347+ config = schema.load(self._testfile('local.conf'))
3348+ config.push('one', '')
3349+ config.push('two', '')
3350+ self.assertRaises(NoConfigError, config.pop, 'bad-name')
3351+
3352+ def test_cannot_pop_bottom(self):
3353+ schema = ConfigSchema(self._testfile('base.conf'))
3354+ config = schema.load(self._testfile('local.conf'))
3355+ config.pop('local.conf')
3356+ self.assertRaises(NoConfigError, config.pop, 'base.conf')
3357+
3358+ def test_multiline_preserves_indentation(self):
3359+ schema = ImplicitTypeSchema(self._testfile('base.conf'))
3360+ config = schema.load(self._testfile('local.conf'))
3361+ convert = config['section_1']._convert
3362+ orig = """\
3363+multiline value 1
3364+ multiline value 2"""
3365+ new = convert(orig)
3366+ self.meq(new, orig)
3367+
3368+ def test_multiline_strips_leading_and_trailing_whitespace(self):
3369+ schema = ImplicitTypeSchema(self._testfile('base.conf'))
3370+ config = schema.load(self._testfile('local.conf'))
3371+ convert = config['section_1']._convert
3372+ orig = """
3373+ multiline value 1
3374+ multiline value 2
3375+ """
3376+ new = convert(orig)
3377+ self.meq(new, orig.strip())
3378+
3379+ def test_multiline_key(self):
3380+ schema = ImplicitTypeSchema(self._testfile('base.conf'))
3381+ config = schema.load(self._testfile('local.conf'))
3382+ self.meq(config['section_33'].key2, """\
3383+multiline value 1
3384+multiline value 2""")
3385
3386=== added file 'lazr/config/tests/testdata/__init__.py'
3387=== modified file 'lazr/config/version.txt'
3388--- src/lazr/config/version.txt 2009-08-25 18:56:38 +0000
3389+++ lazr/config/version.txt 2013-01-10 15:47:20 +0000
3390@@ -1,1 +1,1 @@
3391-1.1.3
3392+2.0
3393
3394=== added file 'setup.cfg'
3395--- setup.cfg 1970-01-01 00:00:00 +0000
3396+++ setup.cfg 2013-01-10 15:47:20 +0000
3397@@ -0,0 +1,9 @@
3398+[nosetests]
3399+verbosity=3
3400+with-coverage=1
3401+with-doctest=1
3402+doctest-extension=.rst
3403+doctest-options=+ELLIPSIS,+NORMALIZE_WHITESPACE,+REPORT_NDIFF
3404+doctest-fixtures=_fixture
3405+cover-package=lazr.config
3406+pdb=1
3407
3408=== modified file 'setup.py'
3409--- setup.py 2009-08-25 13:58:54 +0000
3410+++ setup.py 2013-01-10 15:47:20 +0000
3411@@ -1,6 +1,4 @@
3412-#!/usr/bin/env python
3413-
3414-# Copyright 2008-2009 Canonical Ltd. All rights reserved.
3415+# Copyright 2008-2013 Canonical Ltd. All rights reserved.
3416 #
3417 # This file is part of lazr.config.
3418 #
3419@@ -16,10 +14,9 @@
3420 # You should have received a copy of the GNU Lesser General Public License
3421 # along with lazr.config. If not, see <http://www.gnu.org/licenses/>.
3422
3423-import ez_setup
3424-ez_setup.use_setuptools()
3425+import distribute_setup
3426+distribute_setup.use_setuptools()
3427
3428-import sys
3429 from setuptools import setup, find_packages
3430
3431 # generic helpers primarily for the long_description
3432@@ -37,22 +34,21 @@
3433 # end generic helpers
3434
3435
3436-__version__ = open("src/lazr/config/version.txt").read().strip()
3437+__version__ = open("lazr/config/version.txt").read().strip()
3438
3439 setup(
3440 name='lazr.config',
3441 version=__version__,
3442 namespace_packages=['lazr'],
3443- packages=find_packages('src'),
3444- package_dir={'':'src'},
3445+ packages=find_packages(),
3446 include_package_data=True,
3447 zip_safe=False,
3448 maintainer='LAZR Developers',
3449 maintainer_email='lazr-developers@lists.launchpad.net',
3450- description=open('README.txt').readline().strip(),
3451+ description=open('README.rst').readline().strip(),
3452 long_description=generate(
3453- 'src/lazr/config/README.txt',
3454- 'src/lazr/config/CHANGES.txt'),
3455+ 'lazr/config/README.rst',
3456+ 'lazr/config/CHANGES.rst'),
3457 license='LGPL v3',
3458 install_requires=[
3459 'setuptools',
3460@@ -60,16 +56,16 @@
3461 'lazr.delegates',
3462 ],
3463 url='https://launchpad.net/lazr.config',
3464- download_url= 'https://launchpad.net/lazr.config/+download',
3465+ download_url='https://launchpad.net/lazr.config/+download',
3466 classifiers=[
3467 "Development Status :: 5 - Production/Stable",
3468 "Intended Audience :: Developers",
3469 "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
3470 "Operating System :: OS Independent",
3471- "Programming Language :: Python"],
3472- extras_require=dict(
3473- docs=['Sphinx',
3474- 'z3c.recipe.sphinxdoc']
3475- ),
3476+ 'Programming Language :: Python',
3477+ 'Programming Language :: Python :: 2.6',
3478+ 'Programming Language :: Python :: 2.7',
3479+ 'Programming Language :: Python :: 3',
3480+ ],
3481 test_suite='lazr.config.tests',
3482 )
3483
3484=== removed directory 'src'
3485=== removed file 'src/lazr/config/NEWS.txt'
3486--- src/lazr/config/NEWS.txt 2009-08-25 18:56:38 +0000
3487+++ src/lazr/config/NEWS.txt 1970-01-01 00:00:00 +0000
3488@@ -1,15 +0,0 @@
3489-1.1.3 (2009-08-25)
3490-==================
3491-
3492-Fixed a build problem.
3493-
3494-1.1.2 (2009-08-25)
3495-==================
3496-
3497-Got rid of a sys.path hack.
3498-
3499-
3500-1.1.1
3501-=====
3502-
3503-Initial release
3504
3505=== removed file 'src/lazr/config/tests/test_docs.py'
3506--- src/lazr/config/tests/test_docs.py 2009-03-24 17:31:47 +0000
3507+++ src/lazr/config/tests/test_docs.py 1970-01-01 00:00:00 +0000
3508@@ -1,51 +0,0 @@
3509-# Copyright 2009 Canonical Ltd. All rights reserved.
3510-#
3511-# This file is part of lazr.config
3512-#
3513-# lazr.config is free software: you can redistribute it and/or modify it
3514-# under the terms of the GNU Lesser General Public License as published by
3515-# the Free Software Foundation, version 3 of the License.
3516-#
3517-# lazr.config is distributed in the hope that it will be useful, but WITHOUT
3518-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
3519-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
3520-# License for more details.
3521-#
3522-# You should have received a copy of the GNU Lesser General Public License
3523-# along with lazr.config. If not, see <http://www.gnu.org/licenses/>.
3524-"Test harness for doctests."
3525-
3526-# pylint: disable-msg=E0611,W0142
3527-
3528-__metaclass__ = type
3529-__all__ = [
3530- 'additional_tests',
3531- ]
3532-
3533-import atexit
3534-import doctest
3535-import os
3536-from pkg_resources import (
3537- resource_filename, resource_exists, resource_listdir, cleanup_resources)
3538-import unittest
3539-
3540-DOCTEST_FLAGS = (
3541- doctest.ELLIPSIS |
3542- doctest.NORMALIZE_WHITESPACE |
3543- doctest.REPORT_NDIFF)
3544-
3545-
3546-def additional_tests():
3547- "Run the doc tests (README.txt and docs/*, if any exist)"
3548- doctest_files = [
3549- os.path.abspath(resource_filename('lazr.config', 'README.txt'))]
3550- if resource_exists('lazr.config', 'docs'):
3551- for name in resource_listdir('lazr.config', 'docs'):
3552- if name.endswith('.txt'):
3553- doctest_files.append(
3554- os.path.abspath(
3555- resource_filename('lazr.config', 'docs/%s' % name)))
3556- kwargs = dict(module_relative=False, optionflags=DOCTEST_FLAGS)
3557- atexit.register(cleanup_resources)
3558- return unittest.TestSuite((
3559- doctest.DocFileSuite(*doctest_files, **kwargs)))

Subscribers

People subscribed via source and target branches