Merge lp:~barry/lazr.delegates/lp1096513 into lp:lazr.delegates

Proposed by Barry Warsaw
Status: Merged
Merged at revision: 7
Proposed branch: lp:~barry/lazr.delegates/lp1096513
Merge into: lp:lazr.delegates
Diff against target: 2350 lines (+1248/-848)
28 files modified
.bzrignore (+2/-0)
HACKING.rst (+16/-2)
MANIFEST.in (+3/-4)
_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/delegates/NEWS.rst (+6/-0)
lazr/delegates/__init__.py (+15/-6)
lazr/delegates/_delegates.py (+9/-37)
lazr/delegates/_passthrough.py (+55/-0)
lazr/delegates/_python2.py (+38/-0)
lazr/delegates/_python3.py (+38/-0)
lazr/delegates/docs/fixture.py (+34/-0)
lazr/delegates/docs/usage.rst (+136/-0)
lazr/delegates/docs/usage_fixture.py (+27/-0)
lazr/delegates/tests/__init__.py (+0/-16)
lazr/delegates/tests/test_api.py (+35/-0)
lazr/delegates/tests/test_passthrough.py (+85/-0)
lazr/delegates/tests/test_python2.py (+166/-0)
lazr/delegates/version.txt (+1/-1)
setup.cfg (+9/-0)
setup.py (+15/-19)
src/lazr/delegates/README.txt (+0/-292)
src/lazr/delegates/tests/test_docs.py (+0/-51)
To merge this branch: bzr merge lp:~barry/lazr.delegates/lp1096513
Reviewer Review Type Date Requested Status
Gavin Panella Approve
Curtis Hovey Pending
Gary Poster Pending
Review via email: mp+142052@code.launchpad.net

Description of the change

See bug for API changes and more details.

To post a comment you must log in.
lp:~barry/lazr.delegates/lp1096513 updated
19. By Barry Warsaw

Update the MANIFEST.

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

On Jan 07, 2013, at 11:02 AM, Gavin Panella wrote:

>Review: Approve
>
>Looks good. Some minor points. I did a fairly quick once-over
>sanity-check rather than an in-depth review, so you might want to ask
>someone else to take a look if you think it warrants it.

Thanks for the review! You caught some great things.

I'll see if Gary or Curtis wants to take a look, otherwise I'll JFDI :).

BTW, what did you think of @delegate_to as the choice of name?

>+def delegate_to(*interfaces, **kws):
>+    context = kws.pop('context', 'context')
>+    if len(kws) > 0:
>+        raise TypeError('Too many arguments')
>+    def _decorator(cls):
>+        if len(interfaces) == 0:
>+            raise TypeError('At least one interface is required')
>
>I think it would make more sense to check interfaces at the same time
>as kws, i.e. in delegate_to, rather than _decorator.
>
>In both _python2.py and python3.py.

Good idea, fixed.

>--- lazr/delegates/docs/fixture.py 1970-01-01 00:00:00 +0000
>+++ lazr/delegates/docs/fixture.py 2013-01-07 10:44:28 +0000
>@@ -0,0 +1,34 @@
>+# Copyright 2009-2013 Canonical Ltd.  All rights reserved.
>+#
>+# This file is part of lazr.smtptest
>
>s/smtptest/delegates/
>
>In lazr/delegates/docs/usage_fixture.py too.

Fixed everywhere.

>+The ``@delegation`` decorator makes a class implement zero or more interfaces
>
>s/delegation/delegate_to/

Fixed.

>=== added file 'lazr/delegates/docs/usage_fixture.py'
>...
>+__all__ = [
>+    'globs',
>+    ]
>+
>+
>+from lazr.delegates.docs.fixture import globs
>
>What does this module do?

It's a nosetests artifact. AFAICT, under nose (which is generally pretty
awesome) the only way to add a fixture for a doctest is to provide a suffix
which is appended to the doctest base name. So, given "_fixture" as the
suffix, for usage.rst it will only search for usage_fixture.py.

In lazr.delegates case, that's not a big deal, but I copied this over from
lazr.smtptest (which I'm porting to Python 3 in a very similar way) and it has
more than one doctest. So the idea was to put the common fixtures in a shared
module, e.g. lazr.delegates.doc.fixture, and then import what's needed into
the nose-required module name.

BTW, I opened a bug on nose for making this nicer.

https://github.com/nose-devs/nose/issues/594

>+class TestAPI(unittest.TestCase):
>+    """Test various corner cases in the API."""
>+
>+    def test_no_interfaces(self):
>+        try:
>+            @delegate_to()
>+            class SomeClass(object):
>+                pass
>+        except TypeError:
>+            pass
>+        else:
>+            raise AssertionError('TypeError expected')
>
>assertRaises would be more concise, but I can see that there's some
>clarity here from using decorator syntax. Use self.fail() instead of
>`raise AssertionError`?

Yeah, I did it this way to use the syntax that you'd see more commonly in the
code. Good idea about self.fail() though - fixed!

Update pushed, and again, thanks!

Would you take a quick look at the lazr.smtptest mp?
https://code.launchpad.net/~barry/lazr.smtptest/lp1096475

Revision history for this message
Gavin Panella (allenap) wrote :

Looks good. Some minor points. I did a fairly quick once-over
sanity-check rather than an in-depth review, so you might want to ask
someone else to take a look if you think it warrants it.

[1]

+def delegate_to(*interfaces, **kws):
+    context = kws.pop('context', 'context')
+    if len(kws) > 0:
+        raise TypeError('Too many arguments')
+    def _decorator(cls):
+        if len(interfaces) == 0:
+            raise TypeError('At least one interface is required')

I think it would make more sense to check interfaces at the same time
as kws, i.e. in delegate_to, rather than _decorator.

In both _python2.py and python3.py.

[2]

--- lazr/delegates/docs/fixture.py 1970-01-01 00:00:00 +0000
+++ lazr/delegates/docs/fixture.py 2013-01-07 10:44:28 +0000
@@ -0,0 +1,34 @@
+# Copyright 2009-2013 Canonical Ltd.  All rights reserved.
+#
+# This file is part of lazr.smtptest

s/smtptest/delegates/

In lazr/delegates/docs/usage_fixture.py too.

[3]

+The ``@delegation`` decorator makes a class implement zero or more interfaces

s/delegation/delegate_to/

[4]

=== added file 'lazr/delegates/docs/usage_fixture.py'
...
+__all__ = [
+    'globs',
+    ]
+
+
+from lazr.delegates.docs.fixture import globs

What does this module do?

[5]

+class TestAPI(unittest.TestCase):
+    """Test various corner cases in the API."""
+
+    def test_no_interfaces(self):
+        try:
+            @delegate_to()
+            class SomeClass(object):
+                pass
+        except TypeError:
+            pass
+        else:
+            raise AssertionError('TypeError expected')

assertRaises would be more concise, but I can see that there's some
clarity here from using decorator syntax. Use self.fail() instead of
`raise AssertionError`?

review: Approve
lp:~barry/lazr.delegates/lp1096513 updated
20. By Barry Warsaw

Fixes based on Gavin Panella's review:

* delgate_to(): Check the length of *interfaces in the wrapper instead of in
  the embedded decorator.
* Fix typos.
* Use self.fail() instead of raising an AssertionError.

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

On Jan 08, 2013, at 12:28 PM, Gavin Panella wrote:

>> BTW, what did you think of @delegate_to as the choice of name?
>
>It's good, and I can't think of anything better.
>
>...
>> Would you take a quick look at the lazr.smtptest mp?
>> https://code.launchpad.net/~barry/lazr.smtptest/lp1096475
>
>I was too late :)

Yeah, it's futile to try to beat Curtis. :)

Thanks!

Revision history for this message
Gavin Panella (allenap) wrote :

> BTW, what did you think of @delegate_to as the choice of name?

It's good, and I can't think of anything better.

...
> Would you take a quick look at the lazr.smtptest mp?
> https://code.launchpad.net/~barry/lazr.smtptest/lp1096475

I was too late :)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file '.bzrignore'
--- .bzrignore 2009-03-24 18:52:52 +0000
+++ .bzrignore 2013-01-07 15:18:26 +0000
@@ -9,3 +9,5 @@
9build9build
10*.egg10*.egg
11dist11dist
12__pycache__
13.coverage
1214
=== renamed file 'HACKING.txt' => 'HACKING.rst'
--- HACKING.txt 2009-03-24 19:51:29 +0000
+++ HACKING.rst 2013-01-07 15:18:26 +0000
@@ -13,8 +13,6 @@
13 You should have received a copy of the GNU Lesser General Public License13 You should have received a copy of the GNU Lesser General Public License
14 along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.14 along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
1515
16This project uses zc.buildout for development.
17
18============16============
19Introduction17Introduction
20============18============
@@ -39,3 +37,19 @@
39or send a message to:37or send a message to:
4038
41 lazr-developers@lists.launchpad.net39 lazr-developers@lists.launchpad.net
40
41
42Running the tests
43=================
44
45The tests suite requires nose_ and is compatible with both Python 2 and
46Python 3. To run the full test suite::
47
48 $ python setup.py nosetests
49
50Where ``python`` is the Python executable to use for the tests. E.g. this
51might be ``python3`` for Python 3, or the Python executable from a
52virtualenv_.
53
54.. _nose: https://nose.readthedocs.org/en/latest/
55.. _virtualenv: http://www.virtualenv.org/en/latest/
4256
=== modified file 'MANIFEST.in'
--- MANIFEST.in 2009-08-29 17:51:36 +0000
+++ MANIFEST.in 2013-01-07 15:18:26 +0000
@@ -1,4 +1,3 @@
1include ez_setup.py1include distribute_setup.py
2recursive-include src *.txt *.pt *.zcml *.xsd2recursive-include lazr *.txt *.pt *.zcml *.xsd
3exclude MANIFEST.in buildout.cfg bootstrap.py .bzrignore3exclude MANIFEST.in .bzrignore
4prune _bootstrap
54
=== renamed file 'README.txt' => 'README.rst'
=== removed directory '_bootstrap'
=== removed file '_bootstrap/COPYRIGHT.txt'
--- _bootstrap/COPYRIGHT.txt 2009-03-24 18:52:52 +0000
+++ _bootstrap/COPYRIGHT.txt 1970-01-01 00:00:00 +0000
@@ -1,9 +0,0 @@
1Copyright (c) 2004-2009 Zope Corporation and Contributors.
2All Rights Reserved.
3
4This software is subject to the provisions of the Zope Public License,
5Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
6THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
7WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
8WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
9FOR A PARTICULAR PURPOSE.
100
=== removed file '_bootstrap/LICENSE.txt'
--- _bootstrap/LICENSE.txt 2009-03-24 18:52:52 +0000
+++ _bootstrap/LICENSE.txt 1970-01-01 00:00:00 +0000
@@ -1,54 +0,0 @@
1Zope Public License (ZPL) Version 2.1
2-------------------------------------
3
4A copyright notice accompanies this license document that
5identifies the copyright holders.
6
7This license has been certified as open source. It has also
8been designated as GPL compatible by the Free Software
9Foundation (FSF).
10
11Redistribution and use in source and binary forms, with or
12without modification, are permitted provided that the
13following conditions are met:
14
151. Redistributions in source code must retain the
16 accompanying copyright notice, this list of conditions,
17 and the following disclaimer.
18
192. Redistributions in binary form must reproduce the accompanying
20 copyright notice, this list of conditions, and the
21 following disclaimer in the documentation and/or other
22 materials provided with the distribution.
23
243. Names of the copyright holders must not be used to
25 endorse or promote products derived from this software
26 without prior written permission from the copyright
27 holders.
28
294. The right to distribute this software or to use it for
30 any purpose does not give you the right to use
31 Servicemarks (sm) or Trademarks (tm) of the copyright
32 holders. Use of them is covered by separate agreement
33 with the copyright holders.
34
355. If any files are modified, you must cause the modified
36 files to carry prominent notices stating that you changed
37 the files and the date of any change.
38
39Disclaimer
40
41 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS''
42 AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
43 NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
44 AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
45 NO EVENT SHALL THE COPYRIGHT HOLDERS BE
46 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
47 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
48 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
49 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
50 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
51 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
52 OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
53 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
54 DAMAGE.
55\ No newline at end of file0\ No newline at end of file
561
=== removed file '_bootstrap/bootstrap.py'
--- _bootstrap/bootstrap.py 2009-03-24 18:52:52 +0000
+++ _bootstrap/bootstrap.py 1970-01-01 00:00:00 +0000
@@ -1,77 +0,0 @@
1##############################################################################
2#
3# Copyright (c) 2006 Zope Corporation and Contributors.
4# All Rights Reserved.
5#
6# This software is subject to the provisions of the Zope Public License,
7# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
8# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11# FOR A PARTICULAR PURPOSE.
12#
13##############################################################################
14"""Bootstrap a buildout-based project
15
16Simply run this script in a directory containing a buildout.cfg.
17The script accepts buildout command-line options, so you can
18use the -c option to specify an alternate configuration file.
19
20$Id$
21"""
22
23import os, shutil, sys, tempfile, urllib2
24
25tmpeggs = tempfile.mkdtemp()
26
27is_jython = sys.platform.startswith('java')
28
29try:
30 import pkg_resources
31except ImportError:
32 ez = {}
33 exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
34 ).read() in ez
35 ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
36
37 import pkg_resources
38
39if sys.platform == 'win32':
40 def quote(c):
41 if ' ' in c:
42 return '"%s"' % c # work around spawn lamosity on windows
43 else:
44 return c
45else:
46 def quote (c):
47 return c
48
49cmd = 'from setuptools.command.easy_install import main; main()'
50ws = pkg_resources.working_set
51
52if is_jython:
53 import subprocess
54
55 assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd',
56 quote(tmpeggs), 'zc.buildout'],
57 env=dict(os.environ,
58 PYTHONPATH=
59 ws.find(pkg_resources.Requirement.parse('setuptools')).location
60 ),
61 ).wait() == 0
62
63else:
64 assert os.spawnle(
65 os.P_WAIT, sys.executable, quote (sys.executable),
66 '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout',
67 dict(os.environ,
68 PYTHONPATH=
69 ws.find(pkg_resources.Requirement.parse('setuptools')).location
70 ),
71 ) == 0
72
73ws.add_entry(tmpeggs)
74ws.require('zc.buildout')
75import zc.buildout.buildout
76zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap'])
77shutil.rmtree(tmpeggs)
780
=== removed symlink 'bootstrap.py'
=== target was u'_bootstrap/bootstrap.py'
=== removed file 'buildout.cfg'
--- buildout.cfg 2009-03-24 19:51:29 +0000
+++ buildout.cfg 1970-01-01 00:00:00 +0000
@@ -1,31 +0,0 @@
1[buildout]
2parts =
3 interpreter
4 test
5 docs
6 tags
7unzip = true
8
9develop = .
10
11[test]
12recipe = zc.recipe.testrunner
13eggs = lazr.delegates
14defaults = '--tests-pattern ^tests --exit-with-status --suite-name additional_tests'.split()
15
16[docs]
17recipe = z3c.recipe.sphinxdoc
18eggs = lazr.delegates [docs]
19index-doc = README
20default.css =
21layout.html =
22
23[interpreter]
24recipe = zc.recipe.egg
25interpreter = py
26eggs = lazr.delegates
27 docutils
28
29[tags]
30recipe = z3c.recipe.tag:tags
31eggs = lazr.delegates
320
=== added file 'distribute_setup.py'
--- distribute_setup.py 1970-01-01 00:00:00 +0000
+++ distribute_setup.py 2013-01-07 15:18:26 +0000
@@ -0,0 +1,546 @@
1#!python
2"""Bootstrap distribute installation
3
4If you want to use setuptools in your package's setup.py, just include this
5file in the same directory with it, and add this to the top of your setup.py::
6
7 from distribute_setup import use_setuptools
8 use_setuptools()
9
10If you want to require a specific version of setuptools, set a download
11mirror, or use an alternate download directory, you can do so by supplying
12the appropriate options to ``use_setuptools()``.
13
14This file can also be run as a script to install or upgrade setuptools.
15"""
16import os
17import shutil
18import sys
19import time
20import fnmatch
21import tempfile
22import tarfile
23import optparse
24
25from distutils import log
26
27try:
28 from site import USER_SITE
29except ImportError:
30 USER_SITE = None
31
32try:
33 import subprocess
34
35 def _python_cmd(*args):
36 args = (sys.executable,) + args
37 return subprocess.call(args) == 0
38
39except ImportError:
40 # will be used for python 2.3
41 def _python_cmd(*args):
42 args = (sys.executable,) + args
43 # quoting arguments if windows
44 if sys.platform == 'win32':
45 def quote(arg):
46 if ' ' in arg:
47 return '"%s"' % arg
48 return arg
49 args = [quote(arg) for arg in args]
50 return os.spawnl(os.P_WAIT, sys.executable, *args) == 0
51
52DEFAULT_VERSION = "0.6.34"
53DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/"
54SETUPTOOLS_FAKED_VERSION = "0.6c11"
55
56SETUPTOOLS_PKG_INFO = """\
57Metadata-Version: 1.0
58Name: setuptools
59Version: %s
60Summary: xxxx
61Home-page: xxx
62Author: xxx
63Author-email: xxx
64License: xxx
65Description: xxx
66""" % SETUPTOOLS_FAKED_VERSION
67
68
69def _install(tarball, install_args=()):
70 # extracting the tarball
71 tmpdir = tempfile.mkdtemp()
72 log.warn('Extracting in %s', tmpdir)
73 old_wd = os.getcwd()
74 try:
75 os.chdir(tmpdir)
76 tar = tarfile.open(tarball)
77 _extractall(tar)
78 tar.close()
79
80 # going in the directory
81 subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
82 os.chdir(subdir)
83 log.warn('Now working in %s', subdir)
84
85 # installing
86 log.warn('Installing Distribute')
87 if not _python_cmd('setup.py', 'install', *install_args):
88 log.warn('Something went wrong during the installation.')
89 log.warn('See the error message above.')
90 # exitcode will be 2
91 return 2
92 finally:
93 os.chdir(old_wd)
94 shutil.rmtree(tmpdir)
95
96
97def _build_egg(egg, tarball, to_dir):
98 # extracting the tarball
99 tmpdir = tempfile.mkdtemp()
100 log.warn('Extracting in %s', tmpdir)
101 old_wd = os.getcwd()
102 try:
103 os.chdir(tmpdir)
104 tar = tarfile.open(tarball)
105 _extractall(tar)
106 tar.close()
107
108 # going in the directory
109 subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
110 os.chdir(subdir)
111 log.warn('Now working in %s', subdir)
112
113 # building an egg
114 log.warn('Building a Distribute egg in %s', to_dir)
115 _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
116
117 finally:
118 os.chdir(old_wd)
119 shutil.rmtree(tmpdir)
120 # returning the result
121 log.warn(egg)
122 if not os.path.exists(egg):
123 raise IOError('Could not build the egg.')
124
125
126def _do_download(version, download_base, to_dir, download_delay):
127 egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg'
128 % (version, sys.version_info[0], sys.version_info[1]))
129 if not os.path.exists(egg):
130 tarball = download_setuptools(version, download_base,
131 to_dir, download_delay)
132 _build_egg(egg, tarball, to_dir)
133 sys.path.insert(0, egg)
134 import setuptools
135 setuptools.bootstrap_install_from = egg
136
137
138def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
139 to_dir=os.curdir, download_delay=15, no_fake=True):
140 # making sure we use the absolute path
141 to_dir = os.path.abspath(to_dir)
142 was_imported = 'pkg_resources' in sys.modules or \
143 'setuptools' in sys.modules
144 try:
145 try:
146 import pkg_resources
147 if not hasattr(pkg_resources, '_distribute'):
148 if not no_fake:
149 _fake_setuptools()
150 raise ImportError
151 except ImportError:
152 return _do_download(version, download_base, to_dir, download_delay)
153 try:
154 pkg_resources.require("distribute>=" + version)
155 return
156 except pkg_resources.VersionConflict:
157 e = sys.exc_info()[1]
158 if was_imported:
159 sys.stderr.write(
160 "The required version of distribute (>=%s) is not available,\n"
161 "and can't be installed while this script is running. Please\n"
162 "install a more recent version first, using\n"
163 "'easy_install -U distribute'."
164 "\n\n(Currently using %r)\n" % (version, e.args[0]))
165 sys.exit(2)
166 else:
167 del pkg_resources, sys.modules['pkg_resources'] # reload ok
168 return _do_download(version, download_base, to_dir,
169 download_delay)
170 except pkg_resources.DistributionNotFound:
171 return _do_download(version, download_base, to_dir,
172 download_delay)
173 finally:
174 if not no_fake:
175 _create_fake_setuptools_pkg_info(to_dir)
176
177
178def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
179 to_dir=os.curdir, delay=15):
180 """Download distribute from a specified location and return its filename
181
182 `version` should be a valid distribute version number that is available
183 as an egg for download under the `download_base` URL (which should end
184 with a '/'). `to_dir` is the directory where the egg will be downloaded.
185 `delay` is the number of seconds to pause before an actual download
186 attempt.
187 """
188 # making sure we use the absolute path
189 to_dir = os.path.abspath(to_dir)
190 try:
191 from urllib.request import urlopen
192 except ImportError:
193 from urllib2 import urlopen
194 tgz_name = "distribute-%s.tar.gz" % version
195 url = download_base + tgz_name
196 saveto = os.path.join(to_dir, tgz_name)
197 src = dst = None
198 if not os.path.exists(saveto): # Avoid repeated downloads
199 try:
200 log.warn("Downloading %s", url)
201 src = urlopen(url)
202 # Read/write all in one block, so we don't create a corrupt file
203 # if the download is interrupted.
204 data = src.read()
205 dst = open(saveto, "wb")
206 dst.write(data)
207 finally:
208 if src:
209 src.close()
210 if dst:
211 dst.close()
212 return os.path.realpath(saveto)
213
214
215def _no_sandbox(function):
216 def __no_sandbox(*args, **kw):
217 try:
218 from setuptools.sandbox import DirectorySandbox
219 if not hasattr(DirectorySandbox, '_old'):
220 def violation(*args):
221 pass
222 DirectorySandbox._old = DirectorySandbox._violation
223 DirectorySandbox._violation = violation
224 patched = True
225 else:
226 patched = False
227 except ImportError:
228 patched = False
229
230 try:
231 return function(*args, **kw)
232 finally:
233 if patched:
234 DirectorySandbox._violation = DirectorySandbox._old
235 del DirectorySandbox._old
236
237 return __no_sandbox
238
239
240def _patch_file(path, content):
241 """Will backup the file then patch it"""
242 f = open(path)
243 existing_content = f.read()
244 f.close()
245 if existing_content == content:
246 # already patched
247 log.warn('Already patched.')
248 return False
249 log.warn('Patching...')
250 _rename_path(path)
251 f = open(path, 'w')
252 try:
253 f.write(content)
254 finally:
255 f.close()
256 return True
257
258_patch_file = _no_sandbox(_patch_file)
259
260
261def _same_content(path, content):
262 f = open(path)
263 existing_content = f.read()
264 f.close()
265 return existing_content == content
266
267
268def _rename_path(path):
269 new_name = path + '.OLD.%s' % time.time()
270 log.warn('Renaming %s to %s', path, new_name)
271 os.rename(path, new_name)
272 return new_name
273
274
275def _remove_flat_installation(placeholder):
276 if not os.path.isdir(placeholder):
277 log.warn('Unkown installation at %s', placeholder)
278 return False
279 found = False
280 for file in os.listdir(placeholder):
281 if fnmatch.fnmatch(file, 'setuptools*.egg-info'):
282 found = True
283 break
284 if not found:
285 log.warn('Could not locate setuptools*.egg-info')
286 return
287
288 log.warn('Moving elements out of the way...')
289 pkg_info = os.path.join(placeholder, file)
290 if os.path.isdir(pkg_info):
291 patched = _patch_egg_dir(pkg_info)
292 else:
293 patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO)
294
295 if not patched:
296 log.warn('%s already patched.', pkg_info)
297 return False
298 # now let's move the files out of the way
299 for element in ('setuptools', 'pkg_resources.py', 'site.py'):
300 element = os.path.join(placeholder, element)
301 if os.path.exists(element):
302 _rename_path(element)
303 else:
304 log.warn('Could not find the %s element of the '
305 'Setuptools distribution', element)
306 return True
307
308_remove_flat_installation = _no_sandbox(_remove_flat_installation)
309
310
311def _after_install(dist):
312 log.warn('After install bootstrap.')
313 placeholder = dist.get_command_obj('install').install_purelib
314 _create_fake_setuptools_pkg_info(placeholder)
315
316
317def _create_fake_setuptools_pkg_info(placeholder):
318 if not placeholder or not os.path.exists(placeholder):
319 log.warn('Could not find the install location')
320 return
321 pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1])
322 setuptools_file = 'setuptools-%s-py%s.egg-info' % \
323 (SETUPTOOLS_FAKED_VERSION, pyver)
324 pkg_info = os.path.join(placeholder, setuptools_file)
325 if os.path.exists(pkg_info):
326 log.warn('%s already exists', pkg_info)
327 return
328
329 log.warn('Creating %s', pkg_info)
330 try:
331 f = open(pkg_info, 'w')
332 except EnvironmentError:
333 log.warn("Don't have permissions to write %s, skipping", pkg_info)
334 return
335 try:
336 f.write(SETUPTOOLS_PKG_INFO)
337 finally:
338 f.close()
339
340 pth_file = os.path.join(placeholder, 'setuptools.pth')
341 log.warn('Creating %s', pth_file)
342 f = open(pth_file, 'w')
343 try:
344 f.write(os.path.join(os.curdir, setuptools_file))
345 finally:
346 f.close()
347
348_create_fake_setuptools_pkg_info = _no_sandbox(
349 _create_fake_setuptools_pkg_info
350)
351
352
353def _patch_egg_dir(path):
354 # let's check if it's already patched
355 pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
356 if os.path.exists(pkg_info):
357 if _same_content(pkg_info, SETUPTOOLS_PKG_INFO):
358 log.warn('%s already patched.', pkg_info)
359 return False
360 _rename_path(path)
361 os.mkdir(path)
362 os.mkdir(os.path.join(path, 'EGG-INFO'))
363 pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
364 f = open(pkg_info, 'w')
365 try:
366 f.write(SETUPTOOLS_PKG_INFO)
367 finally:
368 f.close()
369 return True
370
371_patch_egg_dir = _no_sandbox(_patch_egg_dir)
372
373
374def _before_install():
375 log.warn('Before install bootstrap.')
376 _fake_setuptools()
377
378
379def _under_prefix(location):
380 if 'install' not in sys.argv:
381 return True
382 args = sys.argv[sys.argv.index('install') + 1:]
383 for index, arg in enumerate(args):
384 for option in ('--root', '--prefix'):
385 if arg.startswith('%s=' % option):
386 top_dir = arg.split('root=')[-1]
387 return location.startswith(top_dir)
388 elif arg == option:
389 if len(args) > index:
390 top_dir = args[index + 1]
391 return location.startswith(top_dir)
392 if arg == '--user' and USER_SITE is not None:
393 return location.startswith(USER_SITE)
394 return True
395
396
397def _fake_setuptools():
398 log.warn('Scanning installed packages')
399 try:
400 import pkg_resources
401 except ImportError:
402 # we're cool
403 log.warn('Setuptools or Distribute does not seem to be installed.')
404 return
405 ws = pkg_resources.working_set
406 try:
407 setuptools_dist = ws.find(
408 pkg_resources.Requirement.parse('setuptools', replacement=False)
409 )
410 except TypeError:
411 # old distribute API
412 setuptools_dist = ws.find(
413 pkg_resources.Requirement.parse('setuptools')
414 )
415
416 if setuptools_dist is None:
417 log.warn('No setuptools distribution found')
418 return
419 # detecting if it was already faked
420 setuptools_location = setuptools_dist.location
421 log.warn('Setuptools installation detected at %s', setuptools_location)
422
423 # if --root or --preix was provided, and if
424 # setuptools is not located in them, we don't patch it
425 if not _under_prefix(setuptools_location):
426 log.warn('Not patching, --root or --prefix is installing Distribute'
427 ' in another location')
428 return
429
430 # let's see if its an egg
431 if not setuptools_location.endswith('.egg'):
432 log.warn('Non-egg installation')
433 res = _remove_flat_installation(setuptools_location)
434 if not res:
435 return
436 else:
437 log.warn('Egg installation')
438 pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO')
439 if (os.path.exists(pkg_info) and
440 _same_content(pkg_info, SETUPTOOLS_PKG_INFO)):
441 log.warn('Already patched.')
442 return
443 log.warn('Patching...')
444 # let's create a fake egg replacing setuptools one
445 res = _patch_egg_dir(setuptools_location)
446 if not res:
447 return
448 log.warn('Patching complete.')
449 _relaunch()
450
451
452def _relaunch():
453 log.warn('Relaunching...')
454 # we have to relaunch the process
455 # pip marker to avoid a relaunch bug
456 _cmd1 = ['-c', 'install', '--single-version-externally-managed']
457 _cmd2 = ['-c', 'install', '--record']
458 if sys.argv[:3] == _cmd1 or sys.argv[:3] == _cmd2:
459 sys.argv[0] = 'setup.py'
460 args = [sys.executable] + sys.argv
461 sys.exit(subprocess.call(args))
462
463
464def _extractall(self, path=".", members=None):
465 """Extract all members from the archive to the current working
466 directory and set owner, modification time and permissions on
467 directories afterwards. `path' specifies a different directory
468 to extract to. `members' is optional and must be a subset of the
469 list returned by getmembers().
470 """
471 import copy
472 import operator
473 from tarfile import ExtractError
474 directories = []
475
476 if members is None:
477 members = self
478
479 for tarinfo in members:
480 if tarinfo.isdir():
481 # Extract directories with a safe mode.
482 directories.append(tarinfo)
483 tarinfo = copy.copy(tarinfo)
484 tarinfo.mode = 448 # decimal for oct 0700
485 self.extract(tarinfo, path)
486
487 # Reverse sort directories.
488 if sys.version_info < (2, 4):
489 def sorter(dir1, dir2):
490 return cmp(dir1.name, dir2.name)
491 directories.sort(sorter)
492 directories.reverse()
493 else:
494 directories.sort(key=operator.attrgetter('name'), reverse=True)
495
496 # Set correct owner, mtime and filemode on directories.
497 for tarinfo in directories:
498 dirpath = os.path.join(path, tarinfo.name)
499 try:
500 self.chown(tarinfo, dirpath)
501 self.utime(tarinfo, dirpath)
502 self.chmod(tarinfo, dirpath)
503 except ExtractError:
504 e = sys.exc_info()[1]
505 if self.errorlevel > 1:
506 raise
507 else:
508 self._dbg(1, "tarfile: %s" % e)
509
510
511def _build_install_args(options):
512 """
513 Build the arguments to 'python setup.py install' on the distribute package
514 """
515 install_args = []
516 if options.user_install:
517 if sys.version_info < (2, 6):
518 log.warn("--user requires Python 2.6 or later")
519 raise SystemExit(1)
520 install_args.append('--user')
521 return install_args
522
523def _parse_args():
524 """
525 Parse the command line for options
526 """
527 parser = optparse.OptionParser()
528 parser.add_option(
529 '--user', dest='user_install', action='store_true', default=False,
530 help='install in user site package (requires Python 2.6 or later)')
531 parser.add_option(
532 '--download-base', dest='download_base', metavar="URL",
533 default=DEFAULT_URL,
534 help='alternative URL from where to download the distribute package')
535 options, args = parser.parse_args()
536 # positional arguments are ignored
537 return options
538
539def main(version=DEFAULT_VERSION):
540 """Install or upgrade setuptools and EasyInstall"""
541 options = _parse_args()
542 tarball = download_setuptools(download_base=options.download_base)
543 return _install(tarball, _build_install_args(options))
544
545if __name__ == '__main__':
546 sys.exit(main())
0547
=== removed file 'ez_setup.py'
--- ez_setup.py 2009-03-24 18:52:52 +0000
+++ ez_setup.py 1970-01-01 00:00:00 +0000
@@ -1,241 +0,0 @@
1#!python
2"""Bootstrap setuptools installation
3
4If you want to use setuptools in your package's setup.py, just include this
5file in the same directory with it, and add this to the top of your setup.py::
6
7 from ez_setup import use_setuptools
8 use_setuptools()
9
10If you want to require a specific version of setuptools, set a download
11mirror, or use an alternate download directory, you can do so by supplying
12the appropriate options to ``use_setuptools()``.
13
14This file can also be run as a script to install or upgrade setuptools.
15"""
16import sys
17DEFAULT_VERSION = "0.6c8"
18DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
19
20md5_data = {
21 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
22 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
23 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
24 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
25 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
26 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
27 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
28 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
29 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
30 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
31 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
32 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
33 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
34 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
35 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
36 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
37 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
38 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
39 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
40 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
41 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
42 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
43 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
44 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
45 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
46 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
47 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
48 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
49 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
50 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
51}
52
53import sys, os
54
55def _validate_md5(egg_name, data):
56 if egg_name in md5_data:
57 from md5 import md5
58 digest = md5(data).hexdigest()
59 if digest != md5_data[egg_name]:
60 print >>sys.stderr, (
61 "md5 validation of %s failed! (Possible download problem?)"
62 % egg_name
63 )
64 sys.exit(2)
65 return data
66
67
68def use_setuptools(
69 version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
70 download_delay=15, min_version=None
71):
72 """Automatically find/download setuptools and make it available on sys.path
73
74 `version` should be a valid setuptools version number that is available
75 as an egg for download under the `download_base` URL (which should end with
76 a '/'). `to_dir` is the directory where setuptools will be downloaded, if
77 it is not already available. If `download_delay` is specified, it should
78 be the number of seconds that will be paused before initiating a download,
79 should one be required. If an older version of setuptools is installed,
80 this routine will print a message to ``sys.stderr`` and raise SystemExit in
81 an attempt to abort the calling script.
82 """
83 # Work around a hack in the ez_setup.py file from simplejson==1.7.3.
84 if min_version:
85 version = min_version
86
87 was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
88 def do_download():
89 egg = download_setuptools(version, download_base, to_dir, download_delay)
90 sys.path.insert(0, egg)
91 import setuptools; setuptools.bootstrap_install_from = egg
92 try:
93 import pkg_resources
94 except ImportError:
95 return do_download()
96 try:
97 pkg_resources.require("setuptools>="+version); return
98 except pkg_resources.VersionConflict, e:
99 if was_imported:
100 print >>sys.stderr, (
101 "The required version of setuptools (>=%s) is not available, and\n"
102 "can't be installed while this script is running. Please install\n"
103 " a more recent version first, using 'easy_install -U setuptools'."
104 "\n\n(Currently using %r)"
105 ) % (version, e.args[0])
106 sys.exit(2)
107 else:
108 del pkg_resources, sys.modules['pkg_resources'] # reload ok
109 return do_download()
110 except pkg_resources.DistributionNotFound:
111 return do_download()
112
113def download_setuptools(
114 version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
115 delay = 15
116):
117 """Download setuptools from a specified location and return its filename
118
119 `version` should be a valid setuptools version number that is available
120 as an egg for download under the `download_base` URL (which should end
121 with a '/'). `to_dir` is the directory where the egg will be downloaded.
122 `delay` is the number of seconds to pause before an actual download attempt.
123 """
124 import urllib2, shutil
125 egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
126 url = download_base + egg_name
127 saveto = os.path.join(to_dir, egg_name)
128 src = dst = None
129 if not os.path.exists(saveto): # Avoid repeated downloads
130 try:
131 from distutils import log
132 if delay:
133 log.warn("""
134---------------------------------------------------------------------------
135This script requires setuptools version %s to run (even to display
136help). I will attempt to download it for you (from
137%s), but
138you may need to enable firewall access for this script first.
139I will start the download in %d seconds.
140
141(Note: if this machine does not have network access, please obtain the file
142
143 %s
144
145and place it in this directory before rerunning this script.)
146---------------------------------------------------------------------------""",
147 version, download_base, delay, url
148 ); from time import sleep; sleep(delay)
149 log.warn("Downloading %s", url)
150 src = urllib2.urlopen(url)
151 # Read/write all in one block, so we don't create a corrupt file
152 # if the download is interrupted.
153 data = _validate_md5(egg_name, src.read())
154 dst = open(saveto,"wb"); dst.write(data)
155 finally:
156 if src: src.close()
157 if dst: dst.close()
158 return os.path.realpath(saveto)
159
160def main(argv, version=DEFAULT_VERSION):
161 """Install or upgrade setuptools and EasyInstall"""
162 try:
163 import setuptools
164 except ImportError:
165 egg = None
166 try:
167 egg = download_setuptools(version, delay=0)
168 sys.path.insert(0,egg)
169 from setuptools.command.easy_install import main
170 return main(list(argv)+[egg]) # we're done here
171 finally:
172 if egg and os.path.exists(egg):
173 os.unlink(egg)
174 else:
175 if setuptools.__version__ == '0.0.1':
176 print >>sys.stderr, (
177 "You have an obsolete version of setuptools installed. Please\n"
178 "remove it from your system entirely before rerunning this script."
179 )
180 sys.exit(2)
181
182 req = "setuptools>="+version
183 import pkg_resources
184 try:
185 pkg_resources.require(req)
186 except pkg_resources.VersionConflict:
187 try:
188 from setuptools.command.easy_install import main
189 except ImportError:
190 from easy_install import main
191 main(list(argv)+[download_setuptools(delay=0)])
192 sys.exit(0) # try to force an exit
193 else:
194 if argv:
195 from setuptools.command.easy_install import main
196 main(argv)
197 else:
198 print "Setuptools version",version,"or greater has been installed."
199 print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
200
201def update_md5(filenames):
202 """Update our built-in md5 registry"""
203
204 import re
205 from md5 import md5
206
207 for name in filenames:
208 base = os.path.basename(name)
209 f = open(name,'rb')
210 md5_data[base] = md5(f.read()).hexdigest()
211 f.close()
212
213 data = [" %r: %r,\n" % it for it in md5_data.items()]
214 data.sort()
215 repl = "".join(data)
216
217 import inspect
218 srcfile = inspect.getsourcefile(sys.modules[__name__])
219 f = open(srcfile, 'rb'); src = f.read(); f.close()
220
221 match = re.search("\nmd5_data = {\n([^}]+)}", src)
222 if not match:
223 print >>sys.stderr, "Internal error!"
224 sys.exit(2)
225
226 src = src[:match.start(1)] + repl + src[match.end(1):]
227 f = open(srcfile,'w')
228 f.write(src)
229 f.close()
230
231
232if __name__=='__main__':
233 if len(sys.argv)>2 and sys.argv[1]=='--md5update':
234 update_md5(sys.argv[2:])
235 else:
236 main(sys.argv[1:])
237
238
239
240
241
2420
=== renamed directory 'src/lazr' => 'lazr'
=== modified file 'lazr/__init__.py'
--- src/lazr/__init__.py 2009-03-24 19:51:29 +0000
+++ lazr/__init__.py 2013-01-07 15:18:26 +0000
@@ -1,4 +1,4 @@
1# Copyright 2008 Canonical Ltd. All rights reserved.1# Copyright 2008-2013 Canonical Ltd. All rights reserved.
2#2#
3# This file is part of lazr.delegates.3# This file is part of lazr.delegates.
4#4#
@@ -14,10 +14,14 @@
14# You should have received a copy of the GNU Lesser General Public License14# You should have received a copy of the GNU Lesser General Public License
15# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.15# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
1616
17# this is a namespace package17# This is a namespace package, however under >= Python 3.3, let it be a true
18try:18# namespace package (i.e. this cruft isn't necessary).
19 import pkg_resources19import sys
20 pkg_resources.declare_namespace(__name__)20
21except ImportError:21if sys.hexversion < 0x30300f0:
22 import pkgutil22 try:
23 __path__ = pkgutil.extend_path(__path__, __name__)23 import pkg_resources
24 pkg_resources.declare_namespace(__name__)
25 except ImportError:
26 import pkgutil
27 __path__ = pkgutil.extend_path(__path__, __name__)
2428
=== renamed file 'src/lazr/delegates/NEWS.txt' => 'lazr/delegates/NEWS.rst'
--- src/lazr/delegates/NEWS.txt 2010-07-16 13:23:43 +0000
+++ lazr/delegates/NEWS.rst 2013-01-07 15:18:26 +0000
@@ -2,6 +2,12 @@
2NEWS for lazr.delegates2NEWS for lazr.delegates
3=======================3=======================
44
52.0 (2013-01-06)
6================
7
8- Port to Python 3, which requires the use of the ``@delegate_to`` class
9 decorator instead of the ``delegates()`` function call.
10
51.2.0 (2010-07-16)111.2.0 (2010-07-16)
6==================12==================
713
814
=== modified file 'lazr/delegates/__init__.py'
--- src/lazr/delegates/__init__.py 2009-08-29 17:51:36 +0000
+++ lazr/delegates/__init__.py 2013-01-07 15:18:26 +0000
@@ -1,4 +1,4 @@
1# Copyright 2008 Canonical Ltd. All rights reserved.1# Copyright 2008-2013 Canonical Ltd. All rights reserved.
2#2#
3# This file is part of lazr.delegates.3# This file is part of lazr.delegates.
4#4#
@@ -16,12 +16,21 @@
1616
17"""Decorator helpers that simplify class composition."""17"""Decorator helpers that simplify class composition."""
1818
19__all__ = [
20 'delegate_to',
21 ]
22
23
19import pkg_resources24import pkg_resources
20__version__ = pkg_resources.resource_string(25__version__ = pkg_resources.resource_string(
21 "lazr.delegates", "version.txt").strip()26 "lazr.delegates", "version.txt").strip()
2227
23# While we generally frown on "*" imports, this, combined with the fact we28# The class decorator syntax is different in Python 2 vs. Python 3.
24# only test code from this module, means that we can verify what has been29import sys
25# exported.30if sys.version_info[0] == 2:
26from lazr.delegates._delegates import *31 from lazr.delegates._python2 import delegate_to
27from lazr.delegates._delegates import __all__32 # The legacy API is only compatible with Python 2.
33 from lazr.delegates._delegates import delegates
34 __all__.append('delegates')
35else:
36 from lazr.delegates._python3 import delegate_to
2837
=== modified file 'lazr/delegates/_delegates.py'
--- src/lazr/delegates/_delegates.py 2010-07-16 13:14:16 +0000
+++ lazr/delegates/_delegates.py 2013-01-07 15:18:26 +0000
@@ -1,4 +1,4 @@
1# Copyright 2008 Canonical Ltd. All rights reserved.1# Copyright 2008-2013 Canonical Ltd. All rights reserved.
2#2#
3# This file is part of lazr.delegates.3# This file is part of lazr.delegates.
4#4#
@@ -16,11 +16,13 @@
1616
17"""Decorator helpers that simplify class composition."""17"""Decorator helpers that simplify class composition."""
1818
19from __future__ import absolute_import, print_function, unicode_literals
20
1921
20__metaclass__ = type22__metaclass__ = type
2123__all__ = [
2224 'delegates',
23__all__ = ['delegates', 'Passthrough']25 ]
2426
2527
26import sys28import sys
@@ -30,6 +32,8 @@
30from zope.interface import classImplements32from zope.interface import classImplements
31from zope.interface.interfaces import IInterface33from zope.interface.interfaces import IInterface
3234
35from lazr.delegates._passthrough import Passthrough
36
3337
34def delegates(interface_spec, context='context'):38def delegates(interface_spec, context='context'):
35 """Make an adapter into a decorator.39 """Make an adapter into a decorator.
@@ -91,7 +95,7 @@
91 """95 """
92 interface_spec, contextvar = cls.__dict__['__delegates_advice_data__']96 interface_spec, contextvar = cls.__dict__['__delegates_advice_data__']
93 del cls.__delegates_advice_data__97 del cls.__delegates_advice_data__
94 if type(cls) is ClassType:98 if isinstance(cls, ClassType):
95 raise TypeError(99 raise TypeError(
96 'Cannot use delegates() on a classic class: %s.' % cls)100 'Cannot use delegates() on a classic class: %s.' % cls)
97 if IInterface.providedBy(interface_spec):101 if IInterface.providedBy(interface_spec):
@@ -105,35 +109,3 @@
105 if getattr(cls, name, not_found) is not_found:109 if getattr(cls, name, not_found) is not_found:
106 setattr(cls, name, Passthrough(name, contextvar))110 setattr(cls, name, Passthrough(name, contextvar))
107 return cls111 return cls
108
109
110class Passthrough:
111 """Call the delegated class for the decorator class.
112
113 If the ``adaptation`` argument is not None, it should be a callable. It
114 will be called with the context, and should return an object that will
115 have the delegated attribute. The ``adaptation`` argument is expected to
116 be used with an interface, to adapt the context.
117 """
118 def __init__(self, name, contextvar, adaptation=None):
119 self.name = name
120 self.contextvar = contextvar
121 self.adaptation = adaptation
122
123 def __get__(self, inst, cls=None):
124 if inst is None:
125 return self
126 else:
127 context = getattr(inst, self.contextvar)
128 if self.adaptation is not None:
129 context = self.adaptation(context)
130 return getattr(context, self.name)
131
132 def __set__(self, inst, value):
133 context = getattr(inst, self.contextvar)
134 if self.adaptation is not None:
135 context = self.adaptation(context)
136 setattr(context, self.name, value)
137
138 def __delete__(self, inst):
139 raise NotImplementedError
140112
=== added file 'lazr/delegates/_passthrough.py'
--- lazr/delegates/_passthrough.py 1970-01-01 00:00:00 +0000
+++ lazr/delegates/_passthrough.py 2013-01-07 15:18:26 +0000
@@ -0,0 +1,55 @@
1# Copyright 2008-2013 Canonical Ltd. All rights reserved.
2#
3# This file is part of lazr.delegates.
4#
5# lazr.delegates is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Lesser General Public License as published by
7# the Free Software Foundation, version 3 of the License.
8#
9# lazr.delegates is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
12# License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
16
17from __future__ import absolute_import, print_function, unicode_literals
18
19
20__metaclass__ = type
21__all__ = [
22 'Passthrough',
23 ]
24
25
26class Passthrough:
27 """Call the delegated class for the decorator class.
28
29 If the ``adaptation`` argument is not None, it should be a callable. It
30 will be called with the context, and should return an object that will
31 have the delegated attribute. The ``adaptation`` argument is expected to
32 be used with an interface, to adapt the context.
33 """
34 def __init__(self, name, contextvar, adaptation=None):
35 self.name = name
36 self.contextvar = contextvar
37 self.adaptation = adaptation
38
39 def __get__(self, inst, cls=None):
40 if inst is None:
41 return self
42 else:
43 context = getattr(inst, self.contextvar)
44 if self.adaptation is not None:
45 context = self.adaptation(context)
46 return getattr(context, self.name)
47
48 def __set__(self, inst, value):
49 context = getattr(inst, self.contextvar)
50 if self.adaptation is not None:
51 context = self.adaptation(context)
52 setattr(context, self.name, value)
53
54 def __delete__(self, inst):
55 raise NotImplementedError
056
=== added file 'lazr/delegates/_python2.py'
--- lazr/delegates/_python2.py 1970-01-01 00:00:00 +0000
+++ lazr/delegates/_python2.py 2013-01-07 15:18:26 +0000
@@ -0,0 +1,38 @@
1# Copyright 2013 Canonical Ltd. All rights reserved.
2#
3# This file is part of lazr.delegates.
4#
5# lazr.delegates is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Lesser General Public License as published by
7# the Free Software Foundation, version 3 of the License.
8#
9# lazr.delegates is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
12# License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
16
17"""Class decorator definition for Python 2."""
18
19from zope.interface import classImplements
20
21from lazr.delegates._passthrough import Passthrough
22
23
24def delegate_to(*interfaces, **kws):
25 context = kws.pop('context', 'context')
26 if len(kws) > 0:
27 raise TypeError('Too many arguments')
28 if len(interfaces) == 0:
29 raise TypeError('At least one interface is required')
30 def _decorator(cls):
31 missing = object()
32 for interface in interfaces:
33 classImplements(cls, interface)
34 for name in interface:
35 if getattr(cls, name, missing) is missing:
36 setattr(cls, name, Passthrough(name, context))
37 return cls
38 return _decorator
039
=== added file 'lazr/delegates/_python3.py'
--- lazr/delegates/_python3.py 1970-01-01 00:00:00 +0000
+++ lazr/delegates/_python3.py 2013-01-07 15:18:26 +0000
@@ -0,0 +1,38 @@
1# Copyright 2013 Canonical Ltd. All rights reserved.
2#
3# This file is part of lazr.delegates.
4#
5# lazr.delegates is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Lesser General Public License as published by
7# the Free Software Foundation, version 3 of the License.
8#
9# lazr.delegates is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
12# License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
16
17"""Class decorator definition for Python 3.
18
19This syntax is not legal in Python 2.
20"""
21
22from zope.interface import classImplements
23
24from lazr.delegates._passthrough import Passthrough
25
26
27def delegate_to(*interfaces, context='context'):
28 if len(interfaces) == 0:
29 raise TypeError('At least one interface is required')
30 def _decorator(cls):
31 missing = object()
32 for interface in interfaces:
33 classImplements(cls, interface)
34 for name in interface:
35 if getattr(cls, name, missing) is missing:
36 setattr(cls, name, Passthrough(name, context))
37 return cls
38 return _decorator
039
=== added directory 'lazr/delegates/docs'
=== added file 'lazr/delegates/docs/__init__.py'
=== added file 'lazr/delegates/docs/fixture.py'
--- lazr/delegates/docs/fixture.py 1970-01-01 00:00:00 +0000
+++ lazr/delegates/docs/fixture.py 2013-01-07 15:18:26 +0000
@@ -0,0 +1,34 @@
1# Copyright 2009-2013 Canonical Ltd. All rights reserved.
2#
3# This file is part of lazr.delegates
4#
5# lazr.delegates is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Lesser General Public License as published by
7# the Free Software Foundation, version 3 of the License.
8#
9# lazr.delegates is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
12# License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
16
17"""Doctest fixtures for running under nose."""
18
19from __future__ import absolute_import, print_function, unicode_literals
20
21__metaclass__ = type
22__all__ = [
23 'globs',
24 ]
25
26
27def globs(globs):
28 """Set up globals for doctests."""
29 # Enable future statements to make Python 2 act more like Python 3.
30 globs['absolute_import'] = absolute_import
31 globs['print_function'] = print_function
32 globs['unicode_literals'] = unicode_literals
33 # Provide a convenient way to clean things up at the end of the test.
34 return globs
035
=== added file 'lazr/delegates/docs/usage.rst'
--- lazr/delegates/docs/usage.rst 1970-01-01 00:00:00 +0000
+++ lazr/delegates/docs/usage.rst 2013-01-07 15:18:26 +0000
@@ -0,0 +1,136 @@
1==============
2lazr.delegates
3==============
4
5The ``lazr.delegates`` package makes it easy to write objects that delegate
6behavior to another object. The new object adds some property or behavior on
7to the other object, while still providing the underlying interface, and
8delegating behavior.
9
10
11Usage
12=====
13
14The ``@delegate_to`` class decorator makes a class implement zero or more
15interfaces by delegating the implementation to another object. In the case of
16a class providing an adapter, that object will be the *context*, but it can
17really be any object stored in an attribute. So while the interfaces use an
18inheritance mechanism, the classes use a composition mechanism.
19
20For example, we can define two interfaces ``IFoo0`` and ``IFoo1`` where the
21latter inherits from the former. The first interface defines an attribute.
22
23 >>> from zope.interface import Interface, Attribute
24 >>> class IFoo0(Interface):
25 ... one = Attribute('attribute in IFoo0')
26
27The second (i.e. derived) interface defines a method and an attribute.
28
29 >>> class IFoo1(IFoo0):
30 ... def bar():
31 ... """A method in IFoo1"""
32 ... baz = Attribute('attribute in IFoo1')
33
34We also define two classes that mirror the interfaces, and do something
35interesting.
36::
37
38 >>> class Foo0:
39 ... one = 'one'
40
41 >>> class Foo1(Foo0):
42 ... def bar(self):
43 ... return 'bar'
44 ... baz = 'I am baz'
45
46Finally, to tie everything together, we can define a class that delegates the
47implementation of ``IFoo1`` to an attribute on the instance. By default,
48``self.context`` is used as the delegate attribute.
49
50 >>> from lazr.delegates import delegate_to
51 >>> @delegate_to(IFoo1)
52 ... class SomeClass:
53 ... def __init__(self, context):
54 ... self.context = context
55
56When the class doing the delegation is instantiated, an instance of the class
57implementing the interface is passed in.
58
59 >>> delegate = Foo1()
60 >>> s = SomeClass(delegate)
61
62Now, the ``bar()`` method comes from ``Foo1``.
63
64 >>> print(s.bar())
65 bar
66
67The ``baz`` attribute also comes from ``Foo1``.
68
69 >>> print(s.baz)
70 I am baz
71
72The ``one`` attribute comes from ``Foo0``.
73
74 >>> print(s.one)
75 one
76
77Even though the interface of ``SomeClass`` is defined through the delegate,
78the interface is still provided by the instance.
79
80 >>> IFoo1.providedBy(s)
81 True
82
83
84Custom context
85--------------
86
87The ``@delegate_to`` decorator takes an optional keyword argument to customize
88the attribute containing the object to delegate to.
89
90 >>> @delegate_to(IFoo1, context='myfoo')
91 ... class SomeOtherClass:
92 ... def __init__(self, foo):
93 ... self.myfoo = foo
94
95The attributes and methods are still delegated correctly.
96
97 >>> s = SomeOtherClass(delegate)
98 >>> print(s.bar())
99 bar
100 >>> print(s.baz)
101 I am baz
102
103
104Multiple interfaces
105===================
106
107The ``@delegate_to`` decorator accepts more than one interface. Note however,
108that the context attribute must implement all of the named interfaces.
109
110 >>> class IFoo2(Interface):
111 ... another = Attribute('another attribute')
112
113Here is a class that implements the interface. It inherits from the
114implementation class that provides the ``IFoo0`` interface. Thus does this
115class implement both interfaces.
116
117 >>> class Foo2(Foo0):
118 ... another = 'I am another foo'
119
120Again, we tie it all together.
121
122 >>> @delegate_to(IFoo0, IFoo2)
123 ... class SomeOtherClass:
124 ... def __init__(self, context):
125 ... self.context = context
126
127Now, the instance of this class has all the expected attributes, and provides
128the expected interfaces.
129
130 >>> s = SomeOtherClass(Foo2())
131 >>> print(s.another)
132 I am another foo
133 >>> IFoo0.providedBy(s)
134 True
135 >>> IFoo2.providedBy(s)
136 True
0137
=== added file 'lazr/delegates/docs/usage_fixture.py'
--- lazr/delegates/docs/usage_fixture.py 1970-01-01 00:00:00 +0000
+++ lazr/delegates/docs/usage_fixture.py 2013-01-07 15:18:26 +0000
@@ -0,0 +1,27 @@
1# Copyright 2009-2013 Canonical Ltd. All rights reserved.
2#
3# This file is part of lazr.delegates
4#
5# lazr.delegates is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Lesser General Public License as published by
7# the Free Software Foundation, version 3 of the License.
8#
9# lazr.delegates is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
12# License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
16
17"""Doctest fixtures for running under nose."""
18
19from __future__ import absolute_import, print_function, unicode_literals
20
21__metaclass__ = type
22__all__ = [
23 'globs',
24 ]
25
26
27from lazr.delegates.docs.fixture import globs
028
=== modified file 'lazr/delegates/tests/__init__.py'
--- src/lazr/delegates/tests/__init__.py 2009-03-24 19:51:29 +0000
+++ lazr/delegates/tests/__init__.py 2013-01-07 15:18:26 +0000
@@ -1,16 +0,0 @@
1# Copyright 2008 Canonical Ltd. All rights reserved.
2#
3# This file is part of lazr.delegates.
4#
5# lazr.delegates is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Lesser General Public License as published by
7# the Free Software Foundation, version 3 of the License.
8#
9# lazr.delegates is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
12# License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
16"""Tests for lazr.delegates"""
170
=== added file 'lazr/delegates/tests/test_api.py'
--- lazr/delegates/tests/test_api.py 1970-01-01 00:00:00 +0000
+++ lazr/delegates/tests/test_api.py 2013-01-07 15:18:26 +0000
@@ -0,0 +1,35 @@
1# Copyright 2013 Canonical Ltd. All rights reserved.
2#
3# This file is part of lazr.delegates.
4#
5# lazr.delegates is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Lesser General Public License as published by
7# the Free Software Foundation, version 3 of the License.
8#
9# lazr.delegates is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
12# License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
16
17"""Test the new API."""
18
19import unittest
20
21from lazr.delegates import delegate_to
22
23
24class TestAPI(unittest.TestCase):
25 """Test various corner cases in the API."""
26
27 def test_no_interfaces(self):
28 try:
29 @delegate_to()
30 class SomeClass(object):
31 pass
32 except TypeError:
33 pass
34 else:
35 self.fail('TypeError expected')
036
=== added file 'lazr/delegates/tests/test_passthrough.py'
--- lazr/delegates/tests/test_passthrough.py 1970-01-01 00:00:00 +0000
+++ lazr/delegates/tests/test_passthrough.py 2013-01-07 15:18:26 +0000
@@ -0,0 +1,85 @@
1# Copyright 2013 Canonical Ltd. All rights reserved.
2#
3# This file is part of lazr.delegates.
4#
5# lazr.delegates is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Lesser General Public License as published by
7# the Free Software Foundation, version 3 of the License.
8#
9# lazr.delegates is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
12# License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
16
17"""Test the Passthrough implementation."""
18
19import unittest
20
21from lazr.delegates._passthrough import Passthrough
22
23
24class Base:
25 foo = 'foo from Base'
26
27 @classmethod
28 def clsmethod(cls):
29 return cls.__name__
30
31
32class TestPassthrough(unittest.TestCase):
33 def setUp(self):
34 self.p = Passthrough('foo', 'mycontext')
35 self.p2 = Passthrough('clsmethod', 'mycontext')
36
37 self.base = Base()
38 class Adapter:
39 mycontext = self.base
40 self.Adapter = Adapter
41 self.adapter = Adapter()
42
43 def test_get(self):
44 self.assertEqual(self.p.__get__(self.adapter), 'foo from Base')
45 self.assertTrue(self.p.__get__(None, self.Adapter) is self.p)
46 self.assertEqual(self.p2.__get__(self.adapter)(), 'Base')
47
48 def test_set(self):
49 self.p.__set__(self.adapter, 'new value')
50 self.assertEqual(self.base.foo, 'new value')
51
52 def test_no_delete(self):
53 self.assertRaises(NotImplementedError,
54 self.p.__delete__, self.adapter)
55
56 def test_adaptation(self):
57 # Passthrough's third argument (adaptation) is optional and, when
58 # provided, should be a zope.interface.Interface subclass (although in
59 # practice any callable will do) to which the instance is adapted
60 # before getting/setting the delegated attribute.
61 class HasNoFoo(object):
62 _foo = 1
63 no_foo = HasNoFoo()
64 # ... but IHasFooAdapter uses HasNoFoo._foo to provide its own .foo,
65 # so it works like an adapter for HasNoFoo into some interface that
66 # provides a 'foo' attribute.
67 class IHasFooAdapter(object):
68 def __init__(self, inst):
69 self.inst = inst
70 @property
71 def foo(self):
72 return self.inst._foo
73 @foo.setter
74 def foo(self, value):
75 self.inst._foo = value
76
77 class Example(object):
78 context = no_foo
79
80 p = Passthrough('foo', 'context', adaptation=IHasFooAdapter)
81 e = Example()
82
83 self.assertEqual(p.__get__(e), 1)
84 p.__set__(e, 2)
85 self.assertEqual(p.__get__(e), 2)
086
=== added file 'lazr/delegates/tests/test_python2.py'
--- lazr/delegates/tests/test_python2.py 1970-01-01 00:00:00 +0000
+++ lazr/delegates/tests/test_python2.py 2013-01-07 15:18:26 +0000
@@ -0,0 +1,166 @@
1# Copyright 2013 Canonical Ltd. All rights reserved.
2#
3# This file is part of lazr.delegates.
4#
5# lazr.delegates is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Lesser General Public License as published by
7# the Free Software Foundation, version 3 of the License.
8#
9# lazr.delegates is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
12# License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
16
17"""Test the legacy API, which only works in Python 2.
18
19All of these tests are copied almost verbatim from the old README.rst.
20"""
21
22
23from __future__ import absolute_import, print_function, unicode_literals
24
25
26# Don't enable the following or we can't test classic class failures.
27#__metaclass__ = type
28__all__ = [
29 'TestLegacyAPI',
30 ]
31
32
33import sys
34import unittest
35
36from zope.interface import Attribute, Interface, implements, providedBy
37
38from lazr.delegates import delegate_to
39if sys.version_info[0] == 2:
40 from lazr.delegates import delegates
41
42
43class IFoo0(Interface):
44 spoo = Attribute('attribute in IFoo0')
45
46class IFoo(IFoo0):
47 def bar():
48 'some method'
49 baz = Attribute('some attribute')
50
51class BaseFoo0:
52 spoo = 'some spoo'
53
54class BaseFoo(BaseFoo0):
55 def bar(self):
56 return 'bar'
57 baz = 'hi baz!'
58
59class IOther(Interface):
60 another = Attribute('another attribute')
61
62class BaseOtherFoo(BaseFoo):
63 another = 'yes, another'
64
65
66# Python 2.6 doesn't have skips.
67def skip_python3(cls):
68 if sys.version_info[0] > 2:
69 return None
70 return cls
71
72
73@skip_python3
74class TestLegacyAPI(unittest.TestCase):
75 def test_basic_usage(self):
76 class SomeClass(object):
77 delegates(IFoo)
78 def __init__(self, context):
79 self.context = context
80
81 f = BaseFoo()
82 s = SomeClass(f)
83 self.assertEqual(s.bar(), 'bar')
84 self.assertEqual(s.baz, 'hi baz!')
85 self.assertEqual(s.spoo, 'some spoo')
86 self.assertTrue(IFoo.providedBy(s))
87
88 def test_keyword_context(self):
89 class SomeOtherClass(object):
90 delegates(IFoo, context='myfoo')
91 def __init__(self, foo):
92 self.myfoo = foo
93 spoo = 'spoo from SomeOtherClass'
94
95 f = BaseFoo()
96 s = SomeOtherClass(f)
97 self.assertEqual(s.bar(), 'bar')
98 self.assertEqual(s.baz, 'hi baz!')
99 self.assertEqual(s.spoo, 'spoo from SomeOtherClass')
100
101 s.baz = 'fish'
102 self.assertEqual(s.baz, 'fish')
103 self.assertEqual(f.baz, 'fish')
104
105 def test_classic_is_error(self):
106 try:
107 class SomeClassicClass:
108 delegates(IFoo)
109 except TypeError:
110 pass
111 else:
112 self.fail('TypeError expected')
113
114 def test_use_outside_class_is_error(self):
115 self.assertRaises(TypeError, delegates, IFoo)
116
117 def test_multiple_interfaces(self):
118 class SomeOtherClass(object):
119 delegates([IFoo, IOther])
120
121 s = SomeOtherClass()
122 s.context = BaseOtherFoo()
123 self.assertEqual(s.another, 'yes, another')
124 self.assertEqual(s.baz, 'hi baz!')
125 self.assertEqual(s.spoo, 'some spoo')
126 self.assertTrue(IFoo.providedBy(s))
127 self.assertTrue(IOther.providedBy(s))
128
129 def test_decorate_existing_object(self):
130 class MoreFoo(BaseFoo, BaseOtherFoo):
131 implements([IFoo, IOther])
132
133 foo = MoreFoo()
134
135 class WithExtraTeapot(object):
136 delegates(providedBy(foo))
137 teapot = 'i am a teapot'
138
139 foo_with_teapot = WithExtraTeapot()
140 foo_with_teapot.context = foo
141
142 self.assertEqual(foo_with_teapot.baz, 'hi baz!')
143 self.assertEqual(foo_with_teapot.another, 'yes, another')
144 self.assertEqual(foo_with_teapot.teapot, 'i am a teapot')
145 self.assertTrue(IFoo.providedBy(foo_with_teapot))
146 self.assertTrue(IOther.providedBy(foo_with_teapot))
147
148
149@skip_python3
150class TestNewAPI(unittest.TestCase):
151 """Test corner cases in Python 2.
152
153 Most of the new API is tested in the doctest. The implementation of the
154 new API is different between Python 2 and Python 3, so test these corner
155 cases.
156 """
157 def test_type_error(self):
158 # Too many arguments to @delegate_to() raises a TypeError.
159 try:
160 @delegate_to(IFoo0, context='myfoo', other='bogus')
161 class SomeClass(object):
162 pass
163 except TypeError:
164 pass
165 else:
166 self.fail('TypeError expected')
0167
=== modified file 'lazr/delegates/version.txt'
--- src/lazr/delegates/version.txt 2010-07-16 13:23:43 +0000
+++ lazr/delegates/version.txt 2013-01-07 15:18:26 +0000
@@ -1,1 +1,1 @@
11.2.012.0
22
=== added file 'setup.cfg'
--- setup.cfg 1970-01-01 00:00:00 +0000
+++ setup.cfg 2013-01-07 15:18:26 +0000
@@ -0,0 +1,9 @@
1[nosetests]
2verbosity=3
3with-coverage=1
4with-doctest=1
5doctest-extension=.rst
6doctest-options=+ELLIPSIS,+NORMALIZE_WHITESPACE,+REPORT_NDIFF
7doctest-fixtures=_fixture
8cover-package=lazr.delegates
9pdb=1
010
=== modified file 'setup.py'
--- setup.py 2009-08-29 17:51:36 +0000
+++ setup.py 2013-01-07 15:18:26 +0000
@@ -1,6 +1,4 @@
1#!/usr/bin/env python1# Copyright 2008-2013 Canonical Ltd. All rights reserved.
2
3# Copyright 2008-2009 Canonical Ltd. All rights reserved.
4#2#
5# This file is part of lazr.delegates.3# This file is part of lazr.delegates.
6#4#
@@ -16,17 +14,16 @@
16# You should have received a copy of the GNU Lesser General Public License14# You should have received a copy of the GNU Lesser General Public License
17# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.15# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
1816
19import ez_setup17import distribute_setup
20ez_setup.use_setuptools()18distribute_setup.use_setuptools()
2119
22import sys
23from setuptools import setup, find_packages20from setuptools import setup, find_packages
2421
25# generic helpers primarily for the long_description22# generic helpers primarily for the long_description
26def generate(*docname_or_string):23def generate(*docname_or_string):
27 res = []24 res = []
28 for value in docname_or_string:25 for value in docname_or_string:
29 if value.endswith('.txt'):26 if value.endswith('.rst'):
30 f = open(value)27 f = open(value)
31 value = f.read()28 value = f.read()
32 f.close()29 f.close()
@@ -36,22 +33,21 @@
36 return '\n'.join(res)33 return '\n'.join(res)
37# end generic helpers34# end generic helpers
3835
39__version__ = open("src/lazr/delegates/version.txt").read().strip()36__version__ = open("lazr/delegates/version.txt").read().strip()
4037
41setup(38setup(
42 name='lazr.delegates',39 name='lazr.delegates',
43 version=__version__,40 version=__version__,
44 namespace_packages=['lazr'],41 namespace_packages=['lazr'],
45 packages=find_packages('src'),42 packages=find_packages(),
46 package_dir={'':'src'},
47 include_package_data=True,43 include_package_data=True,
48 zip_safe=False,44 zip_safe=False,
49 maintainer='LAZR Developers',45 maintainer='LAZR Developers',
50 maintainer_email='lazr-developers@lists.launchpad.net',46 maintainer_email='lazr-developers@lists.launchpad.net',
51 description=open('README.txt').readline().strip(),47 description=open('README.rst').readline().strip(),
52 long_description=generate(48 long_description=generate(
53 'src/lazr/delegates/README.txt',49 'lazr/delegates/docs/usage.rst',
54 'src/lazr/delegates/NEWS.txt'),50 'lazr/delegates/NEWS.rst'),
55 license='LGPL v3',51 license='LGPL v3',
56 install_requires=[52 install_requires=[
57 'setuptools',53 'setuptools',
@@ -64,10 +60,10 @@
64 "Intended Audience :: Developers",60 "Intended Audience :: Developers",
65 "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",61 "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
66 "Operating System :: OS Independent",62 "Operating System :: OS Independent",
67 "Programming Language :: Python"],63 'Programming Language :: Python',
68 extras_require=dict(64 'Programming Language :: Python :: 2.6',
69 docs=['Sphinx',65 'Programming Language :: Python :: 2.7',
70 'z3c.recipe.sphinxdoc']66 'Programming Language :: Python :: 3',
71 ),67 ],
72 test_suite='lazr.delegates.tests',68 test_suite='nose.collector',
73 )69 )
7470
=== removed directory 'src'
=== removed file 'src/lazr/delegates/README.txt'
--- src/lazr/delegates/README.txt 2010-07-16 13:14:16 +0000
+++ src/lazr/delegates/README.txt 1970-01-01 00:00:00 +0000
@@ -1,292 +0,0 @@
1..
2 This file is part of lazr.delegates.
3
4 lazr.delegates is free software: you can redistribute it and/or modify it
5 under the terms of the GNU Lesser General Public License as published by
6 the Free Software Foundation, version 3 of the License.
7
8 lazr.delegates is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
11 License for more details.
12
13 You should have received a copy of the GNU Lesser General Public License
14 along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
15
16The ``lazr.delegates`` Package
17******************************
18
19The ``lazr.delegates`` package makes it easy to write objects that delegate
20behavior to another object. The new object adds some property or behavior on
21to the other object, while still providing the underlying interface, and
22delegating behavior.
23
24=====
25Usage
26=====
27
28The ``delegates`` function makes a class implement zero or more
29interfaces by delegating the implementation to another object. In the
30case of a class providing an adapter, that object will be the 'context',
31but it can really be any object stored in an attribute. So while the
32interfaces use an inheritance mechanism, the classes use a composition
33mechanism.
34
35For example we can define two interfaces IFoo0 <- IFoo...
36
37 >>> from lazr.delegates import delegates
38 >>> from zope.interface import Interface, Attribute
39 >>> class IFoo0(Interface):
40 ... spoo = Attribute('attribute in IFoo0')
41
42 >>> class IFoo(IFoo0):
43 ... def bar():
44 ... "some method"
45 ... baz = Attribute("some attribute")
46
47And two classes (BaseFoo0 <- BaseFoo) that do something interesting.
48
49 >>> class BaseFoo0:
50 ... spoo = 'some spoo'
51
52 >>> class BaseFoo(BaseFoo0):
53 ... def bar(self):
54 ... return 'bar'
55 ... baz = 'hi baz!'
56
57SomeClass can implement IFoo by delegating to an instance of BaseFoo
58stored in the 'context' attribute. Note that ``delegates`` takes the
59interface as the argument. By default, 'context' is the attribute
60containing the object to which the interface implementation is
61delegated.
62
63 >>> class SomeClass(object):
64 ... delegates(IFoo)
65 ... def __init__(self, context):
66 ... self.context = context
67
68 >>> f = BaseFoo()
69 >>> s = SomeClass(f)
70 >>> s.bar()
71 'bar'
72
73 >>> s.baz
74 'hi baz!'
75
76 >>> s.spoo
77 'some spoo'
78
79 >>> IFoo.providedBy(s)
80 True
81
82The ``delegates()`` function takes an optional keyword argument to change
83attribute containing the object to delegate to. So an existing class,
84such as SomeOtherClass, can declare the name of the attribute to which to
85delegate.
86
87 >>> class SomeOtherClass(object):
88 ... delegates(IFoo, context='myfoo')
89 ... def __init__(self, foo):
90 ... self.myfoo = foo
91 ... spoo = 'spoo from SomeOtherClass'
92
93 >>> f = BaseFoo()
94 >>> s = SomeOtherClass(f)
95 >>> s.bar()
96 'bar'
97
98 >>> s.baz
99 'hi baz!'
100
101 >>> s.spoo
102 'spoo from SomeOtherClass'
103
104 >>> s.baz = 'fish'
105 >>> s.baz
106 'fish'
107
108 >>> f.baz
109 'fish'
110
111The ``delegates()`` function can only be used in new-style classes. An
112error is raised when a classic-style class is modified to implement an
113interface.
114
115 >>> class SomeClassicClass:
116 ... delegates(IFoo)
117 Traceback (most recent call last):
118 ...
119 TypeError: Cannot use delegates() on a classic
120 class: __builtin__.SomeClassicClass.
121
122The ``delegates()`` function cannot be used out side of a class definition,
123such as in a module or in a function.
124
125 >>> delegates(IFoo)
126 Traceback (most recent call last):
127 ...
128 TypeError: delegates() can be used only from a class definition.
129
130Multiple interfaces can be specified by passing an iterable to
131delegates().
132
133 >>> class IOther(Interface):
134 ... another = Attribute("another attribute")
135
136 >>> class BaseOtherFoo(BaseFoo):
137 ... another = 'yes, another'
138
139 >>> class SomeOtherClass(object):
140 ... delegates([IFoo, IOther])
141
142 >>> s = SomeOtherClass()
143 >>> s.context = BaseOtherFoo()
144 >>> s.another
145 'yes, another'
146
147 >>> s.baz
148 'hi baz!'
149
150 >>> s.spoo
151 'some spoo'
152
153 >>> IFoo.providedBy(s)
154 True
155
156 >>> IOther.providedBy(s)
157 True
158
159This can be convenient when decorating an existing object.
160
161 >>> from zope.interface import implements
162 >>> class MoreFoo(BaseFoo, BaseOtherFoo):
163 ... implements(IFoo, IOther)
164
165 >>> foo = MoreFoo()
166
167 >>> from zope.interface import providedBy
168 >>> class WithExtraTeapot(object):
169 ... delegates(providedBy(foo))
170 ... teapot = 'i am a teapot'
171
172 >>> foo_with_teapot = WithExtraTeapot()
173 >>> foo_with_teapot.context = foo
174
175 >>> foo_with_teapot.baz
176 'hi baz!'
177
178 >>> foo_with_teapot.another
179 'yes, another'
180
181 >>> foo_with_teapot.teapot
182 'i am a teapot'
183
184 >>> IFoo.providedBy(foo_with_teapot)
185 True
186
187 >>> IOther.providedBy(foo_with_teapot)
188 True
189
190==============
191Implementation
192==============
193
194The Passthrough class is the implementation machinery of ``delegates()``. It
195uses the descriptor protocol to implement the delegation behaviour provided by
196``delegates()``. It takes at least two arguments: the name of the attribute
197that is delegated, and the name of the attribute containing the object to
198which to delegate.
199
200To illustrate, p and p2 are two Passthrough instances that use the
201instance assigned to 'mycontext' to call the 'foo' attribute and
202the 'clsmethod' method.
203
204 >>> from lazr.delegates import Passthrough
205 >>> p = Passthrough('foo', 'mycontext')
206 >>> p2 = Passthrough('clsmethod', 'mycontext')
207
208Base is a class the implements both 'foo' and 'clsmethod'.
209
210 >>> class Base:
211 ... foo = 'foo from Base'
212 ... def clsmethod(cls):
213 ... return str(cls)
214 ... clsmethod = classmethod(clsmethod)
215
216Adapter is a class that has an instance of Base assigned to the
217attribute 'mycontext'.
218
219 >>> base = Base()
220
221 >>> class Adapter:
222 ... mycontext = base
223
224 >>> adapter = Adapter()
225
226The Passthrough instances can get and set their prescribed attributes
227when passed an instance of adapter.
228
229 >>> p.__get__(adapter)
230 'foo from Base'
231
232 >>> p.__get__(None, Adapter) is p
233 True
234
235 >>> p2.__get__(adapter)()
236 '__builtin__.Base'
237
238 >>> p.__set__(adapter, 'new value')
239 >>> base.foo
240 'new value'
241
242Passthrough does not implement __delete__. An error is raised if
243it is called.
244
245 >>> p.__delete__(adapter)
246 Traceback (most recent call last):
247 ...
248 NotImplementedError
249
250Passthrough's third argument (adaptation) is optional and, when provided,
251should be a zope.interface.Interface subclass (although in practice any
252callable will do) to which the instance is adapted before getting/setting the
253delegated attribute.
254
255 # HasNoFoo does not have a .foo attribute...
256 >>> class HasNoFoo(object):
257 ... _foo = 1
258 >>> no_foo = HasNoFoo()
259
260 # ... but IHasFooAdapter uses HasNoFoo._foo to provide its own .foo, so it
261 # works like an adapter for HasNoFoo into some interface that provides
262 # a 'foo' attribute.
263 >>> class IHasFooAdapter(object):
264 ... def __init__(self, inst):
265 ... self.inst = inst
266 ... def _get_foo(self):
267 ... return self.inst._foo
268 ... def _set_foo(self, value):
269 ... self.inst._foo = value
270 ... foo = property(_get_foo, _set_foo)
271
272 >>> class Example(object):
273 ... context = no_foo
274
275 >>> p = Passthrough('foo', 'context', adaptation=IHasFooAdapter)
276 >>> e = Example()
277 >>> p.__get__(e)
278 1
279 >>> p.__set__(e, 2)
280 >>> p.__get__(e)
281 2
282
283
284===============
285Other Documents
286===============
287
288.. toctree::
289 :glob:
290
291 *
292 docs/*
2930
=== removed file 'src/lazr/delegates/tests/test_docs.py'
--- src/lazr/delegates/tests/test_docs.py 2009-03-24 19:51:29 +0000
+++ src/lazr/delegates/tests/test_docs.py 1970-01-01 00:00:00 +0000
@@ -1,51 +0,0 @@
1# Copyright 2008-2009 Canonical Ltd. All rights reserved.
2#
3# This file is part of lazr.delegates
4#
5# lazr.delegates is free software: you can redistribute it and/or modify it
6# under the terms of the GNU Lesser General Public License as published by
7# the Free Software Foundation, version 3 of the License.
8#
9# lazr.delegates is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
12# License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
16"Test harness for doctests."
17
18# pylint: disable-msg=E0611,W0142
19
20__metaclass__ = type
21__all__ = [
22 'additional_tests',
23 ]
24
25import atexit
26import doctest
27import os
28from pkg_resources import (
29 resource_filename, resource_exists, resource_listdir, cleanup_resources)
30import unittest
31
32DOCTEST_FLAGS = (
33 doctest.ELLIPSIS |
34 doctest.NORMALIZE_WHITESPACE |
35 doctest.REPORT_NDIFF)
36
37
38def additional_tests():
39 "Run the doc tests (README.txt and docs/*, if any exist)"
40 doctest_files = [
41 os.path.abspath(resource_filename('lazr.delegates', 'README.txt'))]
42 if resource_exists('lazr.delegates', 'docs'):
43 for name in resource_listdir('lazr.delegates', 'docs'):
44 if name.endswith('.txt'):
45 doctest_files.append(
46 os.path.abspath(
47 resource_filename('lazr.delegates', 'docs/%s' % name)))
48 kwargs = dict(module_relative=False, optionflags=DOCTEST_FLAGS)
49 atexit.register(cleanup_resources)
50 return unittest.TestSuite((
51 doctest.DocFileSuite(*doctest_files, **kwargs)))

Subscribers

People subscribed via source and target branches