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