Merge lp:~barry/lazr.config/lp1096512 into lp:lazr.config
- lp1096512
- Merge into trunk
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 | ||||
Related bugs: |
|
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 |
Commit message
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.)
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
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 rocks' |
1662 | + >>> print(section_schema_1['key3']) |
1663 | + Launchpad 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 rocks |
1686 | key4 : Fc;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 rocks' |
2153 | - >>> section_1['key5'] |
2154 | - 'local value' |
2155 | + >>> print(section_1['key3']) |
2156 | + Launchpad 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 rocks |
2186 | key4 : Fc;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 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 rocks |
2351 | key4 : Fc;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 rocks |
2461 | key4 : Fc;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 rocks |
2585 | key4 : Fc;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))) |
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.