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
1=== modified file '.bzrignore'
2--- .bzrignore 2009-03-24 18:52:52 +0000
3+++ .bzrignore 2013-01-07 15:18:26 +0000
4@@ -9,3 +9,5 @@
5 build
6 *.egg
7 dist
8+__pycache__
9+.coverage
10
11=== renamed file 'HACKING.txt' => 'HACKING.rst'
12--- HACKING.txt 2009-03-24 19:51:29 +0000
13+++ HACKING.rst 2013-01-07 15:18:26 +0000
14@@ -13,8 +13,6 @@
15 You should have received a copy of the GNU Lesser General Public License
16 along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
17
18-This project uses zc.buildout for development.
19-
20 ============
21 Introduction
22 ============
23@@ -39,3 +37,19 @@
24 or send a message to:
25
26 lazr-developers@lists.launchpad.net
27+
28+
29+Running the tests
30+=================
31+
32+The tests suite requires nose_ and is compatible with both Python 2 and
33+Python 3. To run the full test suite::
34+
35+ $ python setup.py nosetests
36+
37+Where ``python`` is the Python executable to use for the tests. E.g. this
38+might be ``python3`` for Python 3, or the Python executable from a
39+virtualenv_.
40+
41+.. _nose: https://nose.readthedocs.org/en/latest/
42+.. _virtualenv: http://www.virtualenv.org/en/latest/
43
44=== modified file 'MANIFEST.in'
45--- MANIFEST.in 2009-08-29 17:51:36 +0000
46+++ MANIFEST.in 2013-01-07 15:18:26 +0000
47@@ -1,4 +1,3 @@
48-include ez_setup.py
49-recursive-include src *.txt *.pt *.zcml *.xsd
50-exclude MANIFEST.in buildout.cfg bootstrap.py .bzrignore
51-prune _bootstrap
52+include distribute_setup.py
53+recursive-include lazr *.txt *.pt *.zcml *.xsd
54+exclude MANIFEST.in .bzrignore
55
56=== renamed file 'README.txt' => 'README.rst'
57=== removed directory '_bootstrap'
58=== removed file '_bootstrap/COPYRIGHT.txt'
59--- _bootstrap/COPYRIGHT.txt 2009-03-24 18:52:52 +0000
60+++ _bootstrap/COPYRIGHT.txt 1970-01-01 00:00:00 +0000
61@@ -1,9 +0,0 @@
62-Copyright (c) 2004-2009 Zope Corporation and Contributors.
63-All Rights Reserved.
64-
65-This software is subject to the provisions of the Zope Public License,
66-Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
67-THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
68-WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
69-WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
70-FOR A PARTICULAR PURPOSE.
71
72=== removed file '_bootstrap/LICENSE.txt'
73--- _bootstrap/LICENSE.txt 2009-03-24 18:52:52 +0000
74+++ _bootstrap/LICENSE.txt 1970-01-01 00:00:00 +0000
75@@ -1,54 +0,0 @@
76-Zope Public License (ZPL) Version 2.1
77--------------------------------------
78-
79-A copyright notice accompanies this license document that
80-identifies the copyright holders.
81-
82-This license has been certified as open source. It has also
83-been designated as GPL compatible by the Free Software
84-Foundation (FSF).
85-
86-Redistribution and use in source and binary forms, with or
87-without modification, are permitted provided that the
88-following conditions are met:
89-
90-1. Redistributions in source code must retain the
91- accompanying copyright notice, this list of conditions,
92- and the following disclaimer.
93-
94-2. Redistributions in binary form must reproduce the accompanying
95- copyright notice, this list of conditions, and the
96- following disclaimer in the documentation and/or other
97- materials provided with the distribution.
98-
99-3. Names of the copyright holders must not be used to
100- endorse or promote products derived from this software
101- without prior written permission from the copyright
102- holders.
103-
104-4. The right to distribute this software or to use it for
105- any purpose does not give you the right to use
106- Servicemarks (sm) or Trademarks (tm) of the copyright
107- holders. Use of them is covered by separate agreement
108- with the copyright holders.
109-
110-5. If any files are modified, you must cause the modified
111- files to carry prominent notices stating that you changed
112- the files and the date of any change.
113-
114-Disclaimer
115-
116- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS''
117- AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
118- NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
119- AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
120- NO EVENT SHALL THE COPYRIGHT HOLDERS BE
121- LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
122- EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
123- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
124- LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
125- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
126- CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
127- OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
128- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
129- DAMAGE.
130\ No newline at end of file
131
132=== removed file '_bootstrap/bootstrap.py'
133--- _bootstrap/bootstrap.py 2009-03-24 18:52:52 +0000
134+++ _bootstrap/bootstrap.py 1970-01-01 00:00:00 +0000
135@@ -1,77 +0,0 @@
136-##############################################################################
137-#
138-# Copyright (c) 2006 Zope Corporation and Contributors.
139-# All Rights Reserved.
140-#
141-# This software is subject to the provisions of the Zope Public License,
142-# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
143-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
144-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
145-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
146-# FOR A PARTICULAR PURPOSE.
147-#
148-##############################################################################
149-"""Bootstrap a buildout-based project
150-
151-Simply run this script in a directory containing a buildout.cfg.
152-The script accepts buildout command-line options, so you can
153-use the -c option to specify an alternate configuration file.
154-
155-$Id$
156-"""
157-
158-import os, shutil, sys, tempfile, urllib2
159-
160-tmpeggs = tempfile.mkdtemp()
161-
162-is_jython = sys.platform.startswith('java')
163-
164-try:
165- import pkg_resources
166-except ImportError:
167- ez = {}
168- exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
169- ).read() in ez
170- ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
171-
172- import pkg_resources
173-
174-if sys.platform == 'win32':
175- def quote(c):
176- if ' ' in c:
177- return '"%s"' % c # work around spawn lamosity on windows
178- else:
179- return c
180-else:
181- def quote (c):
182- return c
183-
184-cmd = 'from setuptools.command.easy_install import main; main()'
185-ws = pkg_resources.working_set
186-
187-if is_jython:
188- import subprocess
189-
190- assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd',
191- quote(tmpeggs), 'zc.buildout'],
192- env=dict(os.environ,
193- PYTHONPATH=
194- ws.find(pkg_resources.Requirement.parse('setuptools')).location
195- ),
196- ).wait() == 0
197-
198-else:
199- assert os.spawnle(
200- os.P_WAIT, sys.executable, quote (sys.executable),
201- '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout',
202- dict(os.environ,
203- PYTHONPATH=
204- ws.find(pkg_resources.Requirement.parse('setuptools')).location
205- ),
206- ) == 0
207-
208-ws.add_entry(tmpeggs)
209-ws.require('zc.buildout')
210-import zc.buildout.buildout
211-zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap'])
212-shutil.rmtree(tmpeggs)
213
214=== removed symlink 'bootstrap.py'
215=== target was u'_bootstrap/bootstrap.py'
216=== removed file 'buildout.cfg'
217--- buildout.cfg 2009-03-24 19:51:29 +0000
218+++ buildout.cfg 1970-01-01 00:00:00 +0000
219@@ -1,31 +0,0 @@
220-[buildout]
221-parts =
222- interpreter
223- test
224- docs
225- tags
226-unzip = true
227-
228-develop = .
229-
230-[test]
231-recipe = zc.recipe.testrunner
232-eggs = lazr.delegates
233-defaults = '--tests-pattern ^tests --exit-with-status --suite-name additional_tests'.split()
234-
235-[docs]
236-recipe = z3c.recipe.sphinxdoc
237-eggs = lazr.delegates [docs]
238-index-doc = README
239-default.css =
240-layout.html =
241-
242-[interpreter]
243-recipe = zc.recipe.egg
244-interpreter = py
245-eggs = lazr.delegates
246- docutils
247-
248-[tags]
249-recipe = z3c.recipe.tag:tags
250-eggs = lazr.delegates
251
252=== added file 'distribute_setup.py'
253--- distribute_setup.py 1970-01-01 00:00:00 +0000
254+++ distribute_setup.py 2013-01-07 15:18:26 +0000
255@@ -0,0 +1,546 @@
256+#!python
257+"""Bootstrap distribute installation
258+
259+If you want to use setuptools in your package's setup.py, just include this
260+file in the same directory with it, and add this to the top of your setup.py::
261+
262+ from distribute_setup import use_setuptools
263+ use_setuptools()
264+
265+If you want to require a specific version of setuptools, set a download
266+mirror, or use an alternate download directory, you can do so by supplying
267+the appropriate options to ``use_setuptools()``.
268+
269+This file can also be run as a script to install or upgrade setuptools.
270+"""
271+import os
272+import shutil
273+import sys
274+import time
275+import fnmatch
276+import tempfile
277+import tarfile
278+import optparse
279+
280+from distutils import log
281+
282+try:
283+ from site import USER_SITE
284+except ImportError:
285+ USER_SITE = None
286+
287+try:
288+ import subprocess
289+
290+ def _python_cmd(*args):
291+ args = (sys.executable,) + args
292+ return subprocess.call(args) == 0
293+
294+except ImportError:
295+ # will be used for python 2.3
296+ def _python_cmd(*args):
297+ args = (sys.executable,) + args
298+ # quoting arguments if windows
299+ if sys.platform == 'win32':
300+ def quote(arg):
301+ if ' ' in arg:
302+ return '"%s"' % arg
303+ return arg
304+ args = [quote(arg) for arg in args]
305+ return os.spawnl(os.P_WAIT, sys.executable, *args) == 0
306+
307+DEFAULT_VERSION = "0.6.34"
308+DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/"
309+SETUPTOOLS_FAKED_VERSION = "0.6c11"
310+
311+SETUPTOOLS_PKG_INFO = """\
312+Metadata-Version: 1.0
313+Name: setuptools
314+Version: %s
315+Summary: xxxx
316+Home-page: xxx
317+Author: xxx
318+Author-email: xxx
319+License: xxx
320+Description: xxx
321+""" % SETUPTOOLS_FAKED_VERSION
322+
323+
324+def _install(tarball, install_args=()):
325+ # extracting the tarball
326+ tmpdir = tempfile.mkdtemp()
327+ log.warn('Extracting in %s', tmpdir)
328+ old_wd = os.getcwd()
329+ try:
330+ os.chdir(tmpdir)
331+ tar = tarfile.open(tarball)
332+ _extractall(tar)
333+ tar.close()
334+
335+ # going in the directory
336+ subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
337+ os.chdir(subdir)
338+ log.warn('Now working in %s', subdir)
339+
340+ # installing
341+ log.warn('Installing Distribute')
342+ if not _python_cmd('setup.py', 'install', *install_args):
343+ log.warn('Something went wrong during the installation.')
344+ log.warn('See the error message above.')
345+ # exitcode will be 2
346+ return 2
347+ finally:
348+ os.chdir(old_wd)
349+ shutil.rmtree(tmpdir)
350+
351+
352+def _build_egg(egg, tarball, to_dir):
353+ # extracting the tarball
354+ tmpdir = tempfile.mkdtemp()
355+ log.warn('Extracting in %s', tmpdir)
356+ old_wd = os.getcwd()
357+ try:
358+ os.chdir(tmpdir)
359+ tar = tarfile.open(tarball)
360+ _extractall(tar)
361+ tar.close()
362+
363+ # going in the directory
364+ subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
365+ os.chdir(subdir)
366+ log.warn('Now working in %s', subdir)
367+
368+ # building an egg
369+ log.warn('Building a Distribute egg in %s', to_dir)
370+ _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
371+
372+ finally:
373+ os.chdir(old_wd)
374+ shutil.rmtree(tmpdir)
375+ # returning the result
376+ log.warn(egg)
377+ if not os.path.exists(egg):
378+ raise IOError('Could not build the egg.')
379+
380+
381+def _do_download(version, download_base, to_dir, download_delay):
382+ egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg'
383+ % (version, sys.version_info[0], sys.version_info[1]))
384+ if not os.path.exists(egg):
385+ tarball = download_setuptools(version, download_base,
386+ to_dir, download_delay)
387+ _build_egg(egg, tarball, to_dir)
388+ sys.path.insert(0, egg)
389+ import setuptools
390+ setuptools.bootstrap_install_from = egg
391+
392+
393+def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
394+ to_dir=os.curdir, download_delay=15, no_fake=True):
395+ # making sure we use the absolute path
396+ to_dir = os.path.abspath(to_dir)
397+ was_imported = 'pkg_resources' in sys.modules or \
398+ 'setuptools' in sys.modules
399+ try:
400+ try:
401+ import pkg_resources
402+ if not hasattr(pkg_resources, '_distribute'):
403+ if not no_fake:
404+ _fake_setuptools()
405+ raise ImportError
406+ except ImportError:
407+ return _do_download(version, download_base, to_dir, download_delay)
408+ try:
409+ pkg_resources.require("distribute>=" + version)
410+ return
411+ except pkg_resources.VersionConflict:
412+ e = sys.exc_info()[1]
413+ if was_imported:
414+ sys.stderr.write(
415+ "The required version of distribute (>=%s) is not available,\n"
416+ "and can't be installed while this script is running. Please\n"
417+ "install a more recent version first, using\n"
418+ "'easy_install -U distribute'."
419+ "\n\n(Currently using %r)\n" % (version, e.args[0]))
420+ sys.exit(2)
421+ else:
422+ del pkg_resources, sys.modules['pkg_resources'] # reload ok
423+ return _do_download(version, download_base, to_dir,
424+ download_delay)
425+ except pkg_resources.DistributionNotFound:
426+ return _do_download(version, download_base, to_dir,
427+ download_delay)
428+ finally:
429+ if not no_fake:
430+ _create_fake_setuptools_pkg_info(to_dir)
431+
432+
433+def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
434+ to_dir=os.curdir, delay=15):
435+ """Download distribute from a specified location and return its filename
436+
437+ `version` should be a valid distribute version number that is available
438+ as an egg for download under the `download_base` URL (which should end
439+ with a '/'). `to_dir` is the directory where the egg will be downloaded.
440+ `delay` is the number of seconds to pause before an actual download
441+ attempt.
442+ """
443+ # making sure we use the absolute path
444+ to_dir = os.path.abspath(to_dir)
445+ try:
446+ from urllib.request import urlopen
447+ except ImportError:
448+ from urllib2 import urlopen
449+ tgz_name = "distribute-%s.tar.gz" % version
450+ url = download_base + tgz_name
451+ saveto = os.path.join(to_dir, tgz_name)
452+ src = dst = None
453+ if not os.path.exists(saveto): # Avoid repeated downloads
454+ try:
455+ log.warn("Downloading %s", url)
456+ src = urlopen(url)
457+ # Read/write all in one block, so we don't create a corrupt file
458+ # if the download is interrupted.
459+ data = src.read()
460+ dst = open(saveto, "wb")
461+ dst.write(data)
462+ finally:
463+ if src:
464+ src.close()
465+ if dst:
466+ dst.close()
467+ return os.path.realpath(saveto)
468+
469+
470+def _no_sandbox(function):
471+ def __no_sandbox(*args, **kw):
472+ try:
473+ from setuptools.sandbox import DirectorySandbox
474+ if not hasattr(DirectorySandbox, '_old'):
475+ def violation(*args):
476+ pass
477+ DirectorySandbox._old = DirectorySandbox._violation
478+ DirectorySandbox._violation = violation
479+ patched = True
480+ else:
481+ patched = False
482+ except ImportError:
483+ patched = False
484+
485+ try:
486+ return function(*args, **kw)
487+ finally:
488+ if patched:
489+ DirectorySandbox._violation = DirectorySandbox._old
490+ del DirectorySandbox._old
491+
492+ return __no_sandbox
493+
494+
495+def _patch_file(path, content):
496+ """Will backup the file then patch it"""
497+ f = open(path)
498+ existing_content = f.read()
499+ f.close()
500+ if existing_content == content:
501+ # already patched
502+ log.warn('Already patched.')
503+ return False
504+ log.warn('Patching...')
505+ _rename_path(path)
506+ f = open(path, 'w')
507+ try:
508+ f.write(content)
509+ finally:
510+ f.close()
511+ return True
512+
513+_patch_file = _no_sandbox(_patch_file)
514+
515+
516+def _same_content(path, content):
517+ f = open(path)
518+ existing_content = f.read()
519+ f.close()
520+ return existing_content == content
521+
522+
523+def _rename_path(path):
524+ new_name = path + '.OLD.%s' % time.time()
525+ log.warn('Renaming %s to %s', path, new_name)
526+ os.rename(path, new_name)
527+ return new_name
528+
529+
530+def _remove_flat_installation(placeholder):
531+ if not os.path.isdir(placeholder):
532+ log.warn('Unkown installation at %s', placeholder)
533+ return False
534+ found = False
535+ for file in os.listdir(placeholder):
536+ if fnmatch.fnmatch(file, 'setuptools*.egg-info'):
537+ found = True
538+ break
539+ if not found:
540+ log.warn('Could not locate setuptools*.egg-info')
541+ return
542+
543+ log.warn('Moving elements out of the way...')
544+ pkg_info = os.path.join(placeholder, file)
545+ if os.path.isdir(pkg_info):
546+ patched = _patch_egg_dir(pkg_info)
547+ else:
548+ patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO)
549+
550+ if not patched:
551+ log.warn('%s already patched.', pkg_info)
552+ return False
553+ # now let's move the files out of the way
554+ for element in ('setuptools', 'pkg_resources.py', 'site.py'):
555+ element = os.path.join(placeholder, element)
556+ if os.path.exists(element):
557+ _rename_path(element)
558+ else:
559+ log.warn('Could not find the %s element of the '
560+ 'Setuptools distribution', element)
561+ return True
562+
563+_remove_flat_installation = _no_sandbox(_remove_flat_installation)
564+
565+
566+def _after_install(dist):
567+ log.warn('After install bootstrap.')
568+ placeholder = dist.get_command_obj('install').install_purelib
569+ _create_fake_setuptools_pkg_info(placeholder)
570+
571+
572+def _create_fake_setuptools_pkg_info(placeholder):
573+ if not placeholder or not os.path.exists(placeholder):
574+ log.warn('Could not find the install location')
575+ return
576+ pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1])
577+ setuptools_file = 'setuptools-%s-py%s.egg-info' % \
578+ (SETUPTOOLS_FAKED_VERSION, pyver)
579+ pkg_info = os.path.join(placeholder, setuptools_file)
580+ if os.path.exists(pkg_info):
581+ log.warn('%s already exists', pkg_info)
582+ return
583+
584+ log.warn('Creating %s', pkg_info)
585+ try:
586+ f = open(pkg_info, 'w')
587+ except EnvironmentError:
588+ log.warn("Don't have permissions to write %s, skipping", pkg_info)
589+ return
590+ try:
591+ f.write(SETUPTOOLS_PKG_INFO)
592+ finally:
593+ f.close()
594+
595+ pth_file = os.path.join(placeholder, 'setuptools.pth')
596+ log.warn('Creating %s', pth_file)
597+ f = open(pth_file, 'w')
598+ try:
599+ f.write(os.path.join(os.curdir, setuptools_file))
600+ finally:
601+ f.close()
602+
603+_create_fake_setuptools_pkg_info = _no_sandbox(
604+ _create_fake_setuptools_pkg_info
605+)
606+
607+
608+def _patch_egg_dir(path):
609+ # let's check if it's already patched
610+ pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
611+ if os.path.exists(pkg_info):
612+ if _same_content(pkg_info, SETUPTOOLS_PKG_INFO):
613+ log.warn('%s already patched.', pkg_info)
614+ return False
615+ _rename_path(path)
616+ os.mkdir(path)
617+ os.mkdir(os.path.join(path, 'EGG-INFO'))
618+ pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
619+ f = open(pkg_info, 'w')
620+ try:
621+ f.write(SETUPTOOLS_PKG_INFO)
622+ finally:
623+ f.close()
624+ return True
625+
626+_patch_egg_dir = _no_sandbox(_patch_egg_dir)
627+
628+
629+def _before_install():
630+ log.warn('Before install bootstrap.')
631+ _fake_setuptools()
632+
633+
634+def _under_prefix(location):
635+ if 'install' not in sys.argv:
636+ return True
637+ args = sys.argv[sys.argv.index('install') + 1:]
638+ for index, arg in enumerate(args):
639+ for option in ('--root', '--prefix'):
640+ if arg.startswith('%s=' % option):
641+ top_dir = arg.split('root=')[-1]
642+ return location.startswith(top_dir)
643+ elif arg == option:
644+ if len(args) > index:
645+ top_dir = args[index + 1]
646+ return location.startswith(top_dir)
647+ if arg == '--user' and USER_SITE is not None:
648+ return location.startswith(USER_SITE)
649+ return True
650+
651+
652+def _fake_setuptools():
653+ log.warn('Scanning installed packages')
654+ try:
655+ import pkg_resources
656+ except ImportError:
657+ # we're cool
658+ log.warn('Setuptools or Distribute does not seem to be installed.')
659+ return
660+ ws = pkg_resources.working_set
661+ try:
662+ setuptools_dist = ws.find(
663+ pkg_resources.Requirement.parse('setuptools', replacement=False)
664+ )
665+ except TypeError:
666+ # old distribute API
667+ setuptools_dist = ws.find(
668+ pkg_resources.Requirement.parse('setuptools')
669+ )
670+
671+ if setuptools_dist is None:
672+ log.warn('No setuptools distribution found')
673+ return
674+ # detecting if it was already faked
675+ setuptools_location = setuptools_dist.location
676+ log.warn('Setuptools installation detected at %s', setuptools_location)
677+
678+ # if --root or --preix was provided, and if
679+ # setuptools is not located in them, we don't patch it
680+ if not _under_prefix(setuptools_location):
681+ log.warn('Not patching, --root or --prefix is installing Distribute'
682+ ' in another location')
683+ return
684+
685+ # let's see if its an egg
686+ if not setuptools_location.endswith('.egg'):
687+ log.warn('Non-egg installation')
688+ res = _remove_flat_installation(setuptools_location)
689+ if not res:
690+ return
691+ else:
692+ log.warn('Egg installation')
693+ pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO')
694+ if (os.path.exists(pkg_info) and
695+ _same_content(pkg_info, SETUPTOOLS_PKG_INFO)):
696+ log.warn('Already patched.')
697+ return
698+ log.warn('Patching...')
699+ # let's create a fake egg replacing setuptools one
700+ res = _patch_egg_dir(setuptools_location)
701+ if not res:
702+ return
703+ log.warn('Patching complete.')
704+ _relaunch()
705+
706+
707+def _relaunch():
708+ log.warn('Relaunching...')
709+ # we have to relaunch the process
710+ # pip marker to avoid a relaunch bug
711+ _cmd1 = ['-c', 'install', '--single-version-externally-managed']
712+ _cmd2 = ['-c', 'install', '--record']
713+ if sys.argv[:3] == _cmd1 or sys.argv[:3] == _cmd2:
714+ sys.argv[0] = 'setup.py'
715+ args = [sys.executable] + sys.argv
716+ sys.exit(subprocess.call(args))
717+
718+
719+def _extractall(self, path=".", members=None):
720+ """Extract all members from the archive to the current working
721+ directory and set owner, modification time and permissions on
722+ directories afterwards. `path' specifies a different directory
723+ to extract to. `members' is optional and must be a subset of the
724+ list returned by getmembers().
725+ """
726+ import copy
727+ import operator
728+ from tarfile import ExtractError
729+ directories = []
730+
731+ if members is None:
732+ members = self
733+
734+ for tarinfo in members:
735+ if tarinfo.isdir():
736+ # Extract directories with a safe mode.
737+ directories.append(tarinfo)
738+ tarinfo = copy.copy(tarinfo)
739+ tarinfo.mode = 448 # decimal for oct 0700
740+ self.extract(tarinfo, path)
741+
742+ # Reverse sort directories.
743+ if sys.version_info < (2, 4):
744+ def sorter(dir1, dir2):
745+ return cmp(dir1.name, dir2.name)
746+ directories.sort(sorter)
747+ directories.reverse()
748+ else:
749+ directories.sort(key=operator.attrgetter('name'), reverse=True)
750+
751+ # Set correct owner, mtime and filemode on directories.
752+ for tarinfo in directories:
753+ dirpath = os.path.join(path, tarinfo.name)
754+ try:
755+ self.chown(tarinfo, dirpath)
756+ self.utime(tarinfo, dirpath)
757+ self.chmod(tarinfo, dirpath)
758+ except ExtractError:
759+ e = sys.exc_info()[1]
760+ if self.errorlevel > 1:
761+ raise
762+ else:
763+ self._dbg(1, "tarfile: %s" % e)
764+
765+
766+def _build_install_args(options):
767+ """
768+ Build the arguments to 'python setup.py install' on the distribute package
769+ """
770+ install_args = []
771+ if options.user_install:
772+ if sys.version_info < (2, 6):
773+ log.warn("--user requires Python 2.6 or later")
774+ raise SystemExit(1)
775+ install_args.append('--user')
776+ return install_args
777+
778+def _parse_args():
779+ """
780+ Parse the command line for options
781+ """
782+ parser = optparse.OptionParser()
783+ parser.add_option(
784+ '--user', dest='user_install', action='store_true', default=False,
785+ help='install in user site package (requires Python 2.6 or later)')
786+ parser.add_option(
787+ '--download-base', dest='download_base', metavar="URL",
788+ default=DEFAULT_URL,
789+ help='alternative URL from where to download the distribute package')
790+ options, args = parser.parse_args()
791+ # positional arguments are ignored
792+ return options
793+
794+def main(version=DEFAULT_VERSION):
795+ """Install or upgrade setuptools and EasyInstall"""
796+ options = _parse_args()
797+ tarball = download_setuptools(download_base=options.download_base)
798+ return _install(tarball, _build_install_args(options))
799+
800+if __name__ == '__main__':
801+ sys.exit(main())
802
803=== removed file 'ez_setup.py'
804--- ez_setup.py 2009-03-24 18:52:52 +0000
805+++ ez_setup.py 1970-01-01 00:00:00 +0000
806@@ -1,241 +0,0 @@
807-#!python
808-"""Bootstrap setuptools installation
809-
810-If you want to use setuptools in your package's setup.py, just include this
811-file in the same directory with it, and add this to the top of your setup.py::
812-
813- from ez_setup import use_setuptools
814- use_setuptools()
815-
816-If you want to require a specific version of setuptools, set a download
817-mirror, or use an alternate download directory, you can do so by supplying
818-the appropriate options to ``use_setuptools()``.
819-
820-This file can also be run as a script to install or upgrade setuptools.
821-"""
822-import sys
823-DEFAULT_VERSION = "0.6c8"
824-DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
825-
826-md5_data = {
827- 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
828- 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
829- 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
830- 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
831- 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
832- 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
833- 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
834- 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
835- 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
836- 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
837- 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
838- 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
839- 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
840- 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
841- 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
842- 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
843- 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
844- 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
845- 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
846- 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
847- 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
848- 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
849- 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
850- 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
851- 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
852- 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
853- 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
854- 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
855- 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
856- 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
857-}
858-
859-import sys, os
860-
861-def _validate_md5(egg_name, data):
862- if egg_name in md5_data:
863- from md5 import md5
864- digest = md5(data).hexdigest()
865- if digest != md5_data[egg_name]:
866- print >>sys.stderr, (
867- "md5 validation of %s failed! (Possible download problem?)"
868- % egg_name
869- )
870- sys.exit(2)
871- return data
872-
873-
874-def use_setuptools(
875- version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
876- download_delay=15, min_version=None
877-):
878- """Automatically find/download setuptools and make it available on sys.path
879-
880- `version` should be a valid setuptools version number that is available
881- as an egg for download under the `download_base` URL (which should end with
882- a '/'). `to_dir` is the directory where setuptools will be downloaded, if
883- it is not already available. If `download_delay` is specified, it should
884- be the number of seconds that will be paused before initiating a download,
885- should one be required. If an older version of setuptools is installed,
886- this routine will print a message to ``sys.stderr`` and raise SystemExit in
887- an attempt to abort the calling script.
888- """
889- # Work around a hack in the ez_setup.py file from simplejson==1.7.3.
890- if min_version:
891- version = min_version
892-
893- was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
894- def do_download():
895- egg = download_setuptools(version, download_base, to_dir, download_delay)
896- sys.path.insert(0, egg)
897- import setuptools; setuptools.bootstrap_install_from = egg
898- try:
899- import pkg_resources
900- except ImportError:
901- return do_download()
902- try:
903- pkg_resources.require("setuptools>="+version); return
904- except pkg_resources.VersionConflict, e:
905- if was_imported:
906- print >>sys.stderr, (
907- "The required version of setuptools (>=%s) is not available, and\n"
908- "can't be installed while this script is running. Please install\n"
909- " a more recent version first, using 'easy_install -U setuptools'."
910- "\n\n(Currently using %r)"
911- ) % (version, e.args[0])
912- sys.exit(2)
913- else:
914- del pkg_resources, sys.modules['pkg_resources'] # reload ok
915- return do_download()
916- except pkg_resources.DistributionNotFound:
917- return do_download()
918-
919-def download_setuptools(
920- version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
921- delay = 15
922-):
923- """Download setuptools from a specified location and return its filename
924-
925- `version` should be a valid setuptools version number that is available
926- as an egg for download under the `download_base` URL (which should end
927- with a '/'). `to_dir` is the directory where the egg will be downloaded.
928- `delay` is the number of seconds to pause before an actual download attempt.
929- """
930- import urllib2, shutil
931- egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
932- url = download_base + egg_name
933- saveto = os.path.join(to_dir, egg_name)
934- src = dst = None
935- if not os.path.exists(saveto): # Avoid repeated downloads
936- try:
937- from distutils import log
938- if delay:
939- log.warn("""
940----------------------------------------------------------------------------
941-This script requires setuptools version %s to run (even to display
942-help). I will attempt to download it for you (from
943-%s), but
944-you may need to enable firewall access for this script first.
945-I will start the download in %d seconds.
946-
947-(Note: if this machine does not have network access, please obtain the file
948-
949- %s
950-
951-and place it in this directory before rerunning this script.)
952----------------------------------------------------------------------------""",
953- version, download_base, delay, url
954- ); from time import sleep; sleep(delay)
955- log.warn("Downloading %s", url)
956- src = urllib2.urlopen(url)
957- # Read/write all in one block, so we don't create a corrupt file
958- # if the download is interrupted.
959- data = _validate_md5(egg_name, src.read())
960- dst = open(saveto,"wb"); dst.write(data)
961- finally:
962- if src: src.close()
963- if dst: dst.close()
964- return os.path.realpath(saveto)
965-
966-def main(argv, version=DEFAULT_VERSION):
967- """Install or upgrade setuptools and EasyInstall"""
968- try:
969- import setuptools
970- except ImportError:
971- egg = None
972- try:
973- egg = download_setuptools(version, delay=0)
974- sys.path.insert(0,egg)
975- from setuptools.command.easy_install import main
976- return main(list(argv)+[egg]) # we're done here
977- finally:
978- if egg and os.path.exists(egg):
979- os.unlink(egg)
980- else:
981- if setuptools.__version__ == '0.0.1':
982- print >>sys.stderr, (
983- "You have an obsolete version of setuptools installed. Please\n"
984- "remove it from your system entirely before rerunning this script."
985- )
986- sys.exit(2)
987-
988- req = "setuptools>="+version
989- import pkg_resources
990- try:
991- pkg_resources.require(req)
992- except pkg_resources.VersionConflict:
993- try:
994- from setuptools.command.easy_install import main
995- except ImportError:
996- from easy_install import main
997- main(list(argv)+[download_setuptools(delay=0)])
998- sys.exit(0) # try to force an exit
999- else:
1000- if argv:
1001- from setuptools.command.easy_install import main
1002- main(argv)
1003- else:
1004- print "Setuptools version",version,"or greater has been installed."
1005- print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
1006-
1007-def update_md5(filenames):
1008- """Update our built-in md5 registry"""
1009-
1010- import re
1011- from md5 import md5
1012-
1013- for name in filenames:
1014- base = os.path.basename(name)
1015- f = open(name,'rb')
1016- md5_data[base] = md5(f.read()).hexdigest()
1017- f.close()
1018-
1019- data = [" %r: %r,\n" % it for it in md5_data.items()]
1020- data.sort()
1021- repl = "".join(data)
1022-
1023- import inspect
1024- srcfile = inspect.getsourcefile(sys.modules[__name__])
1025- f = open(srcfile, 'rb'); src = f.read(); f.close()
1026-
1027- match = re.search("\nmd5_data = {\n([^}]+)}", src)
1028- if not match:
1029- print >>sys.stderr, "Internal error!"
1030- sys.exit(2)
1031-
1032- src = src[:match.start(1)] + repl + src[match.end(1):]
1033- f = open(srcfile,'w')
1034- f.write(src)
1035- f.close()
1036-
1037-
1038-if __name__=='__main__':
1039- if len(sys.argv)>2 and sys.argv[1]=='--md5update':
1040- update_md5(sys.argv[2:])
1041- else:
1042- main(sys.argv[1:])
1043-
1044-
1045-
1046-
1047-
1048
1049=== renamed directory 'src/lazr' => 'lazr'
1050=== modified file 'lazr/__init__.py'
1051--- src/lazr/__init__.py 2009-03-24 19:51:29 +0000
1052+++ lazr/__init__.py 2013-01-07 15:18:26 +0000
1053@@ -1,4 +1,4 @@
1054-# Copyright 2008 Canonical Ltd. All rights reserved.
1055+# Copyright 2008-2013 Canonical Ltd. All rights reserved.
1056 #
1057 # This file is part of lazr.delegates.
1058 #
1059@@ -14,10 +14,14 @@
1060 # You should have received a copy of the GNU Lesser General Public License
1061 # along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
1062
1063-# this is a namespace package
1064-try:
1065- import pkg_resources
1066- pkg_resources.declare_namespace(__name__)
1067-except ImportError:
1068- import pkgutil
1069- __path__ = pkgutil.extend_path(__path__, __name__)
1070+# This is a namespace package, however under >= Python 3.3, let it be a true
1071+# namespace package (i.e. this cruft isn't necessary).
1072+import sys
1073+
1074+if sys.hexversion < 0x30300f0:
1075+ try:
1076+ import pkg_resources
1077+ pkg_resources.declare_namespace(__name__)
1078+ except ImportError:
1079+ import pkgutil
1080+ __path__ = pkgutil.extend_path(__path__, __name__)
1081
1082=== renamed file 'src/lazr/delegates/NEWS.txt' => 'lazr/delegates/NEWS.rst'
1083--- src/lazr/delegates/NEWS.txt 2010-07-16 13:23:43 +0000
1084+++ lazr/delegates/NEWS.rst 2013-01-07 15:18:26 +0000
1085@@ -2,6 +2,12 @@
1086 NEWS for lazr.delegates
1087 =======================
1088
1089+2.0 (2013-01-06)
1090+================
1091+
1092+- Port to Python 3, which requires the use of the ``@delegate_to`` class
1093+ decorator instead of the ``delegates()`` function call.
1094+
1095 1.2.0 (2010-07-16)
1096 ==================
1097
1098
1099=== modified file 'lazr/delegates/__init__.py'
1100--- src/lazr/delegates/__init__.py 2009-08-29 17:51:36 +0000
1101+++ lazr/delegates/__init__.py 2013-01-07 15:18:26 +0000
1102@@ -1,4 +1,4 @@
1103-# Copyright 2008 Canonical Ltd. All rights reserved.
1104+# Copyright 2008-2013 Canonical Ltd. All rights reserved.
1105 #
1106 # This file is part of lazr.delegates.
1107 #
1108@@ -16,12 +16,21 @@
1109
1110 """Decorator helpers that simplify class composition."""
1111
1112+__all__ = [
1113+ 'delegate_to',
1114+ ]
1115+
1116+
1117 import pkg_resources
1118 __version__ = pkg_resources.resource_string(
1119 "lazr.delegates", "version.txt").strip()
1120
1121-# While we generally frown on "*" imports, this, combined with the fact we
1122-# only test code from this module, means that we can verify what has been
1123-# exported.
1124-from lazr.delegates._delegates import *
1125-from lazr.delegates._delegates import __all__
1126+# The class decorator syntax is different in Python 2 vs. Python 3.
1127+import sys
1128+if sys.version_info[0] == 2:
1129+ from lazr.delegates._python2 import delegate_to
1130+ # The legacy API is only compatible with Python 2.
1131+ from lazr.delegates._delegates import delegates
1132+ __all__.append('delegates')
1133+else:
1134+ from lazr.delegates._python3 import delegate_to
1135
1136=== modified file 'lazr/delegates/_delegates.py'
1137--- src/lazr/delegates/_delegates.py 2010-07-16 13:14:16 +0000
1138+++ lazr/delegates/_delegates.py 2013-01-07 15:18:26 +0000
1139@@ -1,4 +1,4 @@
1140-# Copyright 2008 Canonical Ltd. All rights reserved.
1141+# Copyright 2008-2013 Canonical Ltd. All rights reserved.
1142 #
1143 # This file is part of lazr.delegates.
1144 #
1145@@ -16,11 +16,13 @@
1146
1147 """Decorator helpers that simplify class composition."""
1148
1149+from __future__ import absolute_import, print_function, unicode_literals
1150+
1151
1152 __metaclass__ = type
1153-
1154-
1155-__all__ = ['delegates', 'Passthrough']
1156+__all__ = [
1157+ 'delegates',
1158+ ]
1159
1160
1161 import sys
1162@@ -30,6 +32,8 @@
1163 from zope.interface import classImplements
1164 from zope.interface.interfaces import IInterface
1165
1166+from lazr.delegates._passthrough import Passthrough
1167+
1168
1169 def delegates(interface_spec, context='context'):
1170 """Make an adapter into a decorator.
1171@@ -91,7 +95,7 @@
1172 """
1173 interface_spec, contextvar = cls.__dict__['__delegates_advice_data__']
1174 del cls.__delegates_advice_data__
1175- if type(cls) is ClassType:
1176+ if isinstance(cls, ClassType):
1177 raise TypeError(
1178 'Cannot use delegates() on a classic class: %s.' % cls)
1179 if IInterface.providedBy(interface_spec):
1180@@ -105,35 +109,3 @@
1181 if getattr(cls, name, not_found) is not_found:
1182 setattr(cls, name, Passthrough(name, contextvar))
1183 return cls
1184-
1185-
1186-class Passthrough:
1187- """Call the delegated class for the decorator class.
1188-
1189- If the ``adaptation`` argument is not None, it should be a callable. It
1190- will be called with the context, and should return an object that will
1191- have the delegated attribute. The ``adaptation`` argument is expected to
1192- be used with an interface, to adapt the context.
1193- """
1194- def __init__(self, name, contextvar, adaptation=None):
1195- self.name = name
1196- self.contextvar = contextvar
1197- self.adaptation = adaptation
1198-
1199- def __get__(self, inst, cls=None):
1200- if inst is None:
1201- return self
1202- else:
1203- context = getattr(inst, self.contextvar)
1204- if self.adaptation is not None:
1205- context = self.adaptation(context)
1206- return getattr(context, self.name)
1207-
1208- def __set__(self, inst, value):
1209- context = getattr(inst, self.contextvar)
1210- if self.adaptation is not None:
1211- context = self.adaptation(context)
1212- setattr(context, self.name, value)
1213-
1214- def __delete__(self, inst):
1215- raise NotImplementedError
1216
1217=== added file 'lazr/delegates/_passthrough.py'
1218--- lazr/delegates/_passthrough.py 1970-01-01 00:00:00 +0000
1219+++ lazr/delegates/_passthrough.py 2013-01-07 15:18:26 +0000
1220@@ -0,0 +1,55 @@
1221+# Copyright 2008-2013 Canonical Ltd. All rights reserved.
1222+#
1223+# This file is part of lazr.delegates.
1224+#
1225+# lazr.delegates is free software: you can redistribute it and/or modify it
1226+# under the terms of the GNU Lesser General Public License as published by
1227+# the Free Software Foundation, version 3 of the License.
1228+#
1229+# lazr.delegates is distributed in the hope that it will be useful, but
1230+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
1231+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
1232+# License for more details.
1233+#
1234+# You should have received a copy of the GNU Lesser General Public License
1235+# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
1236+
1237+from __future__ import absolute_import, print_function, unicode_literals
1238+
1239+
1240+__metaclass__ = type
1241+__all__ = [
1242+ 'Passthrough',
1243+ ]
1244+
1245+
1246+class Passthrough:
1247+ """Call the delegated class for the decorator class.
1248+
1249+ If the ``adaptation`` argument is not None, it should be a callable. It
1250+ will be called with the context, and should return an object that will
1251+ have the delegated attribute. The ``adaptation`` argument is expected to
1252+ be used with an interface, to adapt the context.
1253+ """
1254+ def __init__(self, name, contextvar, adaptation=None):
1255+ self.name = name
1256+ self.contextvar = contextvar
1257+ self.adaptation = adaptation
1258+
1259+ def __get__(self, inst, cls=None):
1260+ if inst is None:
1261+ return self
1262+ else:
1263+ context = getattr(inst, self.contextvar)
1264+ if self.adaptation is not None:
1265+ context = self.adaptation(context)
1266+ return getattr(context, self.name)
1267+
1268+ def __set__(self, inst, value):
1269+ context = getattr(inst, self.contextvar)
1270+ if self.adaptation is not None:
1271+ context = self.adaptation(context)
1272+ setattr(context, self.name, value)
1273+
1274+ def __delete__(self, inst):
1275+ raise NotImplementedError
1276
1277=== added file 'lazr/delegates/_python2.py'
1278--- lazr/delegates/_python2.py 1970-01-01 00:00:00 +0000
1279+++ lazr/delegates/_python2.py 2013-01-07 15:18:26 +0000
1280@@ -0,0 +1,38 @@
1281+# Copyright 2013 Canonical Ltd. All rights reserved.
1282+#
1283+# This file is part of lazr.delegates.
1284+#
1285+# lazr.delegates is free software: you can redistribute it and/or modify it
1286+# under the terms of the GNU Lesser General Public License as published by
1287+# the Free Software Foundation, version 3 of the License.
1288+#
1289+# lazr.delegates is distributed in the hope that it will be useful, but
1290+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
1291+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
1292+# License for more details.
1293+#
1294+# You should have received a copy of the GNU Lesser General Public License
1295+# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
1296+
1297+"""Class decorator definition for Python 2."""
1298+
1299+from zope.interface import classImplements
1300+
1301+from lazr.delegates._passthrough import Passthrough
1302+
1303+
1304+def delegate_to(*interfaces, **kws):
1305+ context = kws.pop('context', 'context')
1306+ if len(kws) > 0:
1307+ raise TypeError('Too many arguments')
1308+ if len(interfaces) == 0:
1309+ raise TypeError('At least one interface is required')
1310+ def _decorator(cls):
1311+ missing = object()
1312+ for interface in interfaces:
1313+ classImplements(cls, interface)
1314+ for name in interface:
1315+ if getattr(cls, name, missing) is missing:
1316+ setattr(cls, name, Passthrough(name, context))
1317+ return cls
1318+ return _decorator
1319
1320=== added file 'lazr/delegates/_python3.py'
1321--- lazr/delegates/_python3.py 1970-01-01 00:00:00 +0000
1322+++ lazr/delegates/_python3.py 2013-01-07 15:18:26 +0000
1323@@ -0,0 +1,38 @@
1324+# Copyright 2013 Canonical Ltd. All rights reserved.
1325+#
1326+# This file is part of lazr.delegates.
1327+#
1328+# lazr.delegates is free software: you can redistribute it and/or modify it
1329+# under the terms of the GNU Lesser General Public License as published by
1330+# the Free Software Foundation, version 3 of the License.
1331+#
1332+# lazr.delegates is distributed in the hope that it will be useful, but
1333+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
1334+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
1335+# License for more details.
1336+#
1337+# You should have received a copy of the GNU Lesser General Public License
1338+# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
1339+
1340+"""Class decorator definition for Python 3.
1341+
1342+This syntax is not legal in Python 2.
1343+"""
1344+
1345+from zope.interface import classImplements
1346+
1347+from lazr.delegates._passthrough import Passthrough
1348+
1349+
1350+def delegate_to(*interfaces, context='context'):
1351+ if len(interfaces) == 0:
1352+ raise TypeError('At least one interface is required')
1353+ def _decorator(cls):
1354+ missing = object()
1355+ for interface in interfaces:
1356+ classImplements(cls, interface)
1357+ for name in interface:
1358+ if getattr(cls, name, missing) is missing:
1359+ setattr(cls, name, Passthrough(name, context))
1360+ return cls
1361+ return _decorator
1362
1363=== added directory 'lazr/delegates/docs'
1364=== added file 'lazr/delegates/docs/__init__.py'
1365=== added file 'lazr/delegates/docs/fixture.py'
1366--- lazr/delegates/docs/fixture.py 1970-01-01 00:00:00 +0000
1367+++ lazr/delegates/docs/fixture.py 2013-01-07 15:18:26 +0000
1368@@ -0,0 +1,34 @@
1369+# Copyright 2009-2013 Canonical Ltd. All rights reserved.
1370+#
1371+# This file is part of lazr.delegates
1372+#
1373+# lazr.delegates is free software: you can redistribute it and/or modify it
1374+# under the terms of the GNU Lesser General Public License as published by
1375+# the Free Software Foundation, version 3 of the License.
1376+#
1377+# lazr.delegates is distributed in the hope that it will be useful, but WITHOUT
1378+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
1379+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
1380+# License for more details.
1381+#
1382+# You should have received a copy of the GNU Lesser General Public License
1383+# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
1384+
1385+"""Doctest fixtures for running under nose."""
1386+
1387+from __future__ import absolute_import, print_function, unicode_literals
1388+
1389+__metaclass__ = type
1390+__all__ = [
1391+ 'globs',
1392+ ]
1393+
1394+
1395+def globs(globs):
1396+ """Set up globals for doctests."""
1397+ # Enable future statements to make Python 2 act more like Python 3.
1398+ globs['absolute_import'] = absolute_import
1399+ globs['print_function'] = print_function
1400+ globs['unicode_literals'] = unicode_literals
1401+ # Provide a convenient way to clean things up at the end of the test.
1402+ return globs
1403
1404=== added file 'lazr/delegates/docs/usage.rst'
1405--- lazr/delegates/docs/usage.rst 1970-01-01 00:00:00 +0000
1406+++ lazr/delegates/docs/usage.rst 2013-01-07 15:18:26 +0000
1407@@ -0,0 +1,136 @@
1408+==============
1409+lazr.delegates
1410+==============
1411+
1412+The ``lazr.delegates`` package makes it easy to write objects that delegate
1413+behavior to another object. The new object adds some property or behavior on
1414+to the other object, while still providing the underlying interface, and
1415+delegating behavior.
1416+
1417+
1418+Usage
1419+=====
1420+
1421+The ``@delegate_to`` class decorator makes a class implement zero or more
1422+interfaces by delegating the implementation to another object. In the case of
1423+a class providing an adapter, that object will be the *context*, but it can
1424+really be any object stored in an attribute. So while the interfaces use an
1425+inheritance mechanism, the classes use a composition mechanism.
1426+
1427+For example, we can define two interfaces ``IFoo0`` and ``IFoo1`` where the
1428+latter inherits from the former. The first interface defines an attribute.
1429+
1430+ >>> from zope.interface import Interface, Attribute
1431+ >>> class IFoo0(Interface):
1432+ ... one = Attribute('attribute in IFoo0')
1433+
1434+The second (i.e. derived) interface defines a method and an attribute.
1435+
1436+ >>> class IFoo1(IFoo0):
1437+ ... def bar():
1438+ ... """A method in IFoo1"""
1439+ ... baz = Attribute('attribute in IFoo1')
1440+
1441+We also define two classes that mirror the interfaces, and do something
1442+interesting.
1443+::
1444+
1445+ >>> class Foo0:
1446+ ... one = 'one'
1447+
1448+ >>> class Foo1(Foo0):
1449+ ... def bar(self):
1450+ ... return 'bar'
1451+ ... baz = 'I am baz'
1452+
1453+Finally, to tie everything together, we can define a class that delegates the
1454+implementation of ``IFoo1`` to an attribute on the instance. By default,
1455+``self.context`` is used as the delegate attribute.
1456+
1457+ >>> from lazr.delegates import delegate_to
1458+ >>> @delegate_to(IFoo1)
1459+ ... class SomeClass:
1460+ ... def __init__(self, context):
1461+ ... self.context = context
1462+
1463+When the class doing the delegation is instantiated, an instance of the class
1464+implementing the interface is passed in.
1465+
1466+ >>> delegate = Foo1()
1467+ >>> s = SomeClass(delegate)
1468+
1469+Now, the ``bar()`` method comes from ``Foo1``.
1470+
1471+ >>> print(s.bar())
1472+ bar
1473+
1474+The ``baz`` attribute also comes from ``Foo1``.
1475+
1476+ >>> print(s.baz)
1477+ I am baz
1478+
1479+The ``one`` attribute comes from ``Foo0``.
1480+
1481+ >>> print(s.one)
1482+ one
1483+
1484+Even though the interface of ``SomeClass`` is defined through the delegate,
1485+the interface is still provided by the instance.
1486+
1487+ >>> IFoo1.providedBy(s)
1488+ True
1489+
1490+
1491+Custom context
1492+--------------
1493+
1494+The ``@delegate_to`` decorator takes an optional keyword argument to customize
1495+the attribute containing the object to delegate to.
1496+
1497+ >>> @delegate_to(IFoo1, context='myfoo')
1498+ ... class SomeOtherClass:
1499+ ... def __init__(self, foo):
1500+ ... self.myfoo = foo
1501+
1502+The attributes and methods are still delegated correctly.
1503+
1504+ >>> s = SomeOtherClass(delegate)
1505+ >>> print(s.bar())
1506+ bar
1507+ >>> print(s.baz)
1508+ I am baz
1509+
1510+
1511+Multiple interfaces
1512+===================
1513+
1514+The ``@delegate_to`` decorator accepts more than one interface. Note however,
1515+that the context attribute must implement all of the named interfaces.
1516+
1517+ >>> class IFoo2(Interface):
1518+ ... another = Attribute('another attribute')
1519+
1520+Here is a class that implements the interface. It inherits from the
1521+implementation class that provides the ``IFoo0`` interface. Thus does this
1522+class implement both interfaces.
1523+
1524+ >>> class Foo2(Foo0):
1525+ ... another = 'I am another foo'
1526+
1527+Again, we tie it all together.
1528+
1529+ >>> @delegate_to(IFoo0, IFoo2)
1530+ ... class SomeOtherClass:
1531+ ... def __init__(self, context):
1532+ ... self.context = context
1533+
1534+Now, the instance of this class has all the expected attributes, and provides
1535+the expected interfaces.
1536+
1537+ >>> s = SomeOtherClass(Foo2())
1538+ >>> print(s.another)
1539+ I am another foo
1540+ >>> IFoo0.providedBy(s)
1541+ True
1542+ >>> IFoo2.providedBy(s)
1543+ True
1544
1545=== added file 'lazr/delegates/docs/usage_fixture.py'
1546--- lazr/delegates/docs/usage_fixture.py 1970-01-01 00:00:00 +0000
1547+++ lazr/delegates/docs/usage_fixture.py 2013-01-07 15:18:26 +0000
1548@@ -0,0 +1,27 @@
1549+# Copyright 2009-2013 Canonical Ltd. All rights reserved.
1550+#
1551+# This file is part of lazr.delegates
1552+#
1553+# lazr.delegates is free software: you can redistribute it and/or modify it
1554+# under the terms of the GNU Lesser General Public License as published by
1555+# the Free Software Foundation, version 3 of the License.
1556+#
1557+# lazr.delegates is distributed in the hope that it will be useful, but WITHOUT
1558+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
1559+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
1560+# License for more details.
1561+#
1562+# You should have received a copy of the GNU Lesser General Public License
1563+# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
1564+
1565+"""Doctest fixtures for running under nose."""
1566+
1567+from __future__ import absolute_import, print_function, unicode_literals
1568+
1569+__metaclass__ = type
1570+__all__ = [
1571+ 'globs',
1572+ ]
1573+
1574+
1575+from lazr.delegates.docs.fixture import globs
1576
1577=== modified file 'lazr/delegates/tests/__init__.py'
1578--- src/lazr/delegates/tests/__init__.py 2009-03-24 19:51:29 +0000
1579+++ lazr/delegates/tests/__init__.py 2013-01-07 15:18:26 +0000
1580@@ -1,16 +0,0 @@
1581-# Copyright 2008 Canonical Ltd. All rights reserved.
1582-#
1583-# This file is part of lazr.delegates.
1584-#
1585-# lazr.delegates is free software: you can redistribute it and/or modify it
1586-# under the terms of the GNU Lesser General Public License as published by
1587-# the Free Software Foundation, version 3 of the License.
1588-#
1589-# lazr.delegates is distributed in the hope that it will be useful, but
1590-# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
1591-# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
1592-# License for more details.
1593-#
1594-# You should have received a copy of the GNU Lesser General Public License
1595-# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
1596-"""Tests for lazr.delegates"""
1597
1598=== added file 'lazr/delegates/tests/test_api.py'
1599--- lazr/delegates/tests/test_api.py 1970-01-01 00:00:00 +0000
1600+++ lazr/delegates/tests/test_api.py 2013-01-07 15:18:26 +0000
1601@@ -0,0 +1,35 @@
1602+# Copyright 2013 Canonical Ltd. All rights reserved.
1603+#
1604+# This file is part of lazr.delegates.
1605+#
1606+# lazr.delegates is free software: you can redistribute it and/or modify it
1607+# under the terms of the GNU Lesser General Public License as published by
1608+# the Free Software Foundation, version 3 of the License.
1609+#
1610+# lazr.delegates is distributed in the hope that it will be useful, but
1611+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
1612+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
1613+# License for more details.
1614+#
1615+# You should have received a copy of the GNU Lesser General Public License
1616+# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
1617+
1618+"""Test the new API."""
1619+
1620+import unittest
1621+
1622+from lazr.delegates import delegate_to
1623+
1624+
1625+class TestAPI(unittest.TestCase):
1626+ """Test various corner cases in the API."""
1627+
1628+ def test_no_interfaces(self):
1629+ try:
1630+ @delegate_to()
1631+ class SomeClass(object):
1632+ pass
1633+ except TypeError:
1634+ pass
1635+ else:
1636+ self.fail('TypeError expected')
1637
1638=== added file 'lazr/delegates/tests/test_passthrough.py'
1639--- lazr/delegates/tests/test_passthrough.py 1970-01-01 00:00:00 +0000
1640+++ lazr/delegates/tests/test_passthrough.py 2013-01-07 15:18:26 +0000
1641@@ -0,0 +1,85 @@
1642+# Copyright 2013 Canonical Ltd. All rights reserved.
1643+#
1644+# This file is part of lazr.delegates.
1645+#
1646+# lazr.delegates is free software: you can redistribute it and/or modify it
1647+# under the terms of the GNU Lesser General Public License as published by
1648+# the Free Software Foundation, version 3 of the License.
1649+#
1650+# lazr.delegates is distributed in the hope that it will be useful, but
1651+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
1652+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
1653+# License for more details.
1654+#
1655+# You should have received a copy of the GNU Lesser General Public License
1656+# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
1657+
1658+"""Test the Passthrough implementation."""
1659+
1660+import unittest
1661+
1662+from lazr.delegates._passthrough import Passthrough
1663+
1664+
1665+class Base:
1666+ foo = 'foo from Base'
1667+
1668+ @classmethod
1669+ def clsmethod(cls):
1670+ return cls.__name__
1671+
1672+
1673+class TestPassthrough(unittest.TestCase):
1674+ def setUp(self):
1675+ self.p = Passthrough('foo', 'mycontext')
1676+ self.p2 = Passthrough('clsmethod', 'mycontext')
1677+
1678+ self.base = Base()
1679+ class Adapter:
1680+ mycontext = self.base
1681+ self.Adapter = Adapter
1682+ self.adapter = Adapter()
1683+
1684+ def test_get(self):
1685+ self.assertEqual(self.p.__get__(self.adapter), 'foo from Base')
1686+ self.assertTrue(self.p.__get__(None, self.Adapter) is self.p)
1687+ self.assertEqual(self.p2.__get__(self.adapter)(), 'Base')
1688+
1689+ def test_set(self):
1690+ self.p.__set__(self.adapter, 'new value')
1691+ self.assertEqual(self.base.foo, 'new value')
1692+
1693+ def test_no_delete(self):
1694+ self.assertRaises(NotImplementedError,
1695+ self.p.__delete__, self.adapter)
1696+
1697+ def test_adaptation(self):
1698+ # Passthrough's third argument (adaptation) is optional and, when
1699+ # provided, should be a zope.interface.Interface subclass (although in
1700+ # practice any callable will do) to which the instance is adapted
1701+ # before getting/setting the delegated attribute.
1702+ class HasNoFoo(object):
1703+ _foo = 1
1704+ no_foo = HasNoFoo()
1705+ # ... but IHasFooAdapter uses HasNoFoo._foo to provide its own .foo,
1706+ # so it works like an adapter for HasNoFoo into some interface that
1707+ # provides a 'foo' attribute.
1708+ class IHasFooAdapter(object):
1709+ def __init__(self, inst):
1710+ self.inst = inst
1711+ @property
1712+ def foo(self):
1713+ return self.inst._foo
1714+ @foo.setter
1715+ def foo(self, value):
1716+ self.inst._foo = value
1717+
1718+ class Example(object):
1719+ context = no_foo
1720+
1721+ p = Passthrough('foo', 'context', adaptation=IHasFooAdapter)
1722+ e = Example()
1723+
1724+ self.assertEqual(p.__get__(e), 1)
1725+ p.__set__(e, 2)
1726+ self.assertEqual(p.__get__(e), 2)
1727
1728=== added file 'lazr/delegates/tests/test_python2.py'
1729--- lazr/delegates/tests/test_python2.py 1970-01-01 00:00:00 +0000
1730+++ lazr/delegates/tests/test_python2.py 2013-01-07 15:18:26 +0000
1731@@ -0,0 +1,166 @@
1732+# Copyright 2013 Canonical Ltd. All rights reserved.
1733+#
1734+# This file is part of lazr.delegates.
1735+#
1736+# lazr.delegates is free software: you can redistribute it and/or modify it
1737+# under the terms of the GNU Lesser General Public License as published by
1738+# the Free Software Foundation, version 3 of the License.
1739+#
1740+# lazr.delegates is distributed in the hope that it will be useful, but
1741+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
1742+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
1743+# License for more details.
1744+#
1745+# You should have received a copy of the GNU Lesser General Public License
1746+# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
1747+
1748+"""Test the legacy API, which only works in Python 2.
1749+
1750+All of these tests are copied almost verbatim from the old README.rst.
1751+"""
1752+
1753+
1754+from __future__ import absolute_import, print_function, unicode_literals
1755+
1756+
1757+# Don't enable the following or we can't test classic class failures.
1758+#__metaclass__ = type
1759+__all__ = [
1760+ 'TestLegacyAPI',
1761+ ]
1762+
1763+
1764+import sys
1765+import unittest
1766+
1767+from zope.interface import Attribute, Interface, implements, providedBy
1768+
1769+from lazr.delegates import delegate_to
1770+if sys.version_info[0] == 2:
1771+ from lazr.delegates import delegates
1772+
1773+
1774+class IFoo0(Interface):
1775+ spoo = Attribute('attribute in IFoo0')
1776+
1777+class IFoo(IFoo0):
1778+ def bar():
1779+ 'some method'
1780+ baz = Attribute('some attribute')
1781+
1782+class BaseFoo0:
1783+ spoo = 'some spoo'
1784+
1785+class BaseFoo(BaseFoo0):
1786+ def bar(self):
1787+ return 'bar'
1788+ baz = 'hi baz!'
1789+
1790+class IOther(Interface):
1791+ another = Attribute('another attribute')
1792+
1793+class BaseOtherFoo(BaseFoo):
1794+ another = 'yes, another'
1795+
1796+
1797+# Python 2.6 doesn't have skips.
1798+def skip_python3(cls):
1799+ if sys.version_info[0] > 2:
1800+ return None
1801+ return cls
1802+
1803+
1804+@skip_python3
1805+class TestLegacyAPI(unittest.TestCase):
1806+ def test_basic_usage(self):
1807+ class SomeClass(object):
1808+ delegates(IFoo)
1809+ def __init__(self, context):
1810+ self.context = context
1811+
1812+ f = BaseFoo()
1813+ s = SomeClass(f)
1814+ self.assertEqual(s.bar(), 'bar')
1815+ self.assertEqual(s.baz, 'hi baz!')
1816+ self.assertEqual(s.spoo, 'some spoo')
1817+ self.assertTrue(IFoo.providedBy(s))
1818+
1819+ def test_keyword_context(self):
1820+ class SomeOtherClass(object):
1821+ delegates(IFoo, context='myfoo')
1822+ def __init__(self, foo):
1823+ self.myfoo = foo
1824+ spoo = 'spoo from SomeOtherClass'
1825+
1826+ f = BaseFoo()
1827+ s = SomeOtherClass(f)
1828+ self.assertEqual(s.bar(), 'bar')
1829+ self.assertEqual(s.baz, 'hi baz!')
1830+ self.assertEqual(s.spoo, 'spoo from SomeOtherClass')
1831+
1832+ s.baz = 'fish'
1833+ self.assertEqual(s.baz, 'fish')
1834+ self.assertEqual(f.baz, 'fish')
1835+
1836+ def test_classic_is_error(self):
1837+ try:
1838+ class SomeClassicClass:
1839+ delegates(IFoo)
1840+ except TypeError:
1841+ pass
1842+ else:
1843+ self.fail('TypeError expected')
1844+
1845+ def test_use_outside_class_is_error(self):
1846+ self.assertRaises(TypeError, delegates, IFoo)
1847+
1848+ def test_multiple_interfaces(self):
1849+ class SomeOtherClass(object):
1850+ delegates([IFoo, IOther])
1851+
1852+ s = SomeOtherClass()
1853+ s.context = BaseOtherFoo()
1854+ self.assertEqual(s.another, 'yes, another')
1855+ self.assertEqual(s.baz, 'hi baz!')
1856+ self.assertEqual(s.spoo, 'some spoo')
1857+ self.assertTrue(IFoo.providedBy(s))
1858+ self.assertTrue(IOther.providedBy(s))
1859+
1860+ def test_decorate_existing_object(self):
1861+ class MoreFoo(BaseFoo, BaseOtherFoo):
1862+ implements([IFoo, IOther])
1863+
1864+ foo = MoreFoo()
1865+
1866+ class WithExtraTeapot(object):
1867+ delegates(providedBy(foo))
1868+ teapot = 'i am a teapot'
1869+
1870+ foo_with_teapot = WithExtraTeapot()
1871+ foo_with_teapot.context = foo
1872+
1873+ self.assertEqual(foo_with_teapot.baz, 'hi baz!')
1874+ self.assertEqual(foo_with_teapot.another, 'yes, another')
1875+ self.assertEqual(foo_with_teapot.teapot, 'i am a teapot')
1876+ self.assertTrue(IFoo.providedBy(foo_with_teapot))
1877+ self.assertTrue(IOther.providedBy(foo_with_teapot))
1878+
1879+
1880+@skip_python3
1881+class TestNewAPI(unittest.TestCase):
1882+ """Test corner cases in Python 2.
1883+
1884+ Most of the new API is tested in the doctest. The implementation of the
1885+ new API is different between Python 2 and Python 3, so test these corner
1886+ cases.
1887+ """
1888+ def test_type_error(self):
1889+ # Too many arguments to @delegate_to() raises a TypeError.
1890+ try:
1891+ @delegate_to(IFoo0, context='myfoo', other='bogus')
1892+ class SomeClass(object):
1893+ pass
1894+ except TypeError:
1895+ pass
1896+ else:
1897+ self.fail('TypeError expected')
1898
1899=== modified file 'lazr/delegates/version.txt'
1900--- src/lazr/delegates/version.txt 2010-07-16 13:23:43 +0000
1901+++ lazr/delegates/version.txt 2013-01-07 15:18:26 +0000
1902@@ -1,1 +1,1 @@
1903-1.2.0
1904+2.0
1905
1906=== added file 'setup.cfg'
1907--- setup.cfg 1970-01-01 00:00:00 +0000
1908+++ setup.cfg 2013-01-07 15:18:26 +0000
1909@@ -0,0 +1,9 @@
1910+[nosetests]
1911+verbosity=3
1912+with-coverage=1
1913+with-doctest=1
1914+doctest-extension=.rst
1915+doctest-options=+ELLIPSIS,+NORMALIZE_WHITESPACE,+REPORT_NDIFF
1916+doctest-fixtures=_fixture
1917+cover-package=lazr.delegates
1918+pdb=1
1919
1920=== modified file 'setup.py'
1921--- setup.py 2009-08-29 17:51:36 +0000
1922+++ setup.py 2013-01-07 15:18:26 +0000
1923@@ -1,6 +1,4 @@
1924-#!/usr/bin/env python
1925-
1926-# Copyright 2008-2009 Canonical Ltd. All rights reserved.
1927+# Copyright 2008-2013 Canonical Ltd. All rights reserved.
1928 #
1929 # This file is part of lazr.delegates.
1930 #
1931@@ -16,17 +14,16 @@
1932 # You should have received a copy of the GNU Lesser General Public License
1933 # along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
1934
1935-import ez_setup
1936-ez_setup.use_setuptools()
1937+import distribute_setup
1938+distribute_setup.use_setuptools()
1939
1940-import sys
1941 from setuptools import setup, find_packages
1942
1943 # generic helpers primarily for the long_description
1944 def generate(*docname_or_string):
1945 res = []
1946 for value in docname_or_string:
1947- if value.endswith('.txt'):
1948+ if value.endswith('.rst'):
1949 f = open(value)
1950 value = f.read()
1951 f.close()
1952@@ -36,22 +33,21 @@
1953 return '\n'.join(res)
1954 # end generic helpers
1955
1956-__version__ = open("src/lazr/delegates/version.txt").read().strip()
1957+__version__ = open("lazr/delegates/version.txt").read().strip()
1958
1959 setup(
1960 name='lazr.delegates',
1961 version=__version__,
1962 namespace_packages=['lazr'],
1963- packages=find_packages('src'),
1964- package_dir={'':'src'},
1965+ packages=find_packages(),
1966 include_package_data=True,
1967 zip_safe=False,
1968 maintainer='LAZR Developers',
1969 maintainer_email='lazr-developers@lists.launchpad.net',
1970- description=open('README.txt').readline().strip(),
1971+ description=open('README.rst').readline().strip(),
1972 long_description=generate(
1973- 'src/lazr/delegates/README.txt',
1974- 'src/lazr/delegates/NEWS.txt'),
1975+ 'lazr/delegates/docs/usage.rst',
1976+ 'lazr/delegates/NEWS.rst'),
1977 license='LGPL v3',
1978 install_requires=[
1979 'setuptools',
1980@@ -64,10 +60,10 @@
1981 "Intended Audience :: Developers",
1982 "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
1983 "Operating System :: OS Independent",
1984- "Programming Language :: Python"],
1985- extras_require=dict(
1986- docs=['Sphinx',
1987- 'z3c.recipe.sphinxdoc']
1988- ),
1989- test_suite='lazr.delegates.tests',
1990+ 'Programming Language :: Python',
1991+ 'Programming Language :: Python :: 2.6',
1992+ 'Programming Language :: Python :: 2.7',
1993+ 'Programming Language :: Python :: 3',
1994+ ],
1995+ test_suite='nose.collector',
1996 )
1997
1998=== removed directory 'src'
1999=== removed file 'src/lazr/delegates/README.txt'
2000--- src/lazr/delegates/README.txt 2010-07-16 13:14:16 +0000
2001+++ src/lazr/delegates/README.txt 1970-01-01 00:00:00 +0000
2002@@ -1,292 +0,0 @@
2003-..
2004- This file is part of lazr.delegates.
2005-
2006- lazr.delegates is free software: you can redistribute it and/or modify it
2007- under the terms of the GNU Lesser General Public License as published by
2008- the Free Software Foundation, version 3 of the License.
2009-
2010- lazr.delegates is distributed in the hope that it will be useful, but
2011- WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
2012- or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
2013- License for more details.
2014-
2015- You should have received a copy of the GNU Lesser General Public License
2016- along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
2017-
2018-The ``lazr.delegates`` Package
2019-******************************
2020-
2021-The ``lazr.delegates`` package makes it easy to write objects that delegate
2022-behavior to another object. The new object adds some property or behavior on
2023-to the other object, while still providing the underlying interface, and
2024-delegating behavior.
2025-
2026-=====
2027-Usage
2028-=====
2029-
2030-The ``delegates`` function makes a class implement zero or more
2031-interfaces by delegating the implementation to another object. In the
2032-case of a class providing an adapter, that object will be the 'context',
2033-but it can really be any object stored in an attribute. So while the
2034-interfaces use an inheritance mechanism, the classes use a composition
2035-mechanism.
2036-
2037-For example we can define two interfaces IFoo0 <- IFoo...
2038-
2039- >>> from lazr.delegates import delegates
2040- >>> from zope.interface import Interface, Attribute
2041- >>> class IFoo0(Interface):
2042- ... spoo = Attribute('attribute in IFoo0')
2043-
2044- >>> class IFoo(IFoo0):
2045- ... def bar():
2046- ... "some method"
2047- ... baz = Attribute("some attribute")
2048-
2049-And two classes (BaseFoo0 <- BaseFoo) that do something interesting.
2050-
2051- >>> class BaseFoo0:
2052- ... spoo = 'some spoo'
2053-
2054- >>> class BaseFoo(BaseFoo0):
2055- ... def bar(self):
2056- ... return 'bar'
2057- ... baz = 'hi baz!'
2058-
2059-SomeClass can implement IFoo by delegating to an instance of BaseFoo
2060-stored in the 'context' attribute. Note that ``delegates`` takes the
2061-interface as the argument. By default, 'context' is the attribute
2062-containing the object to which the interface implementation is
2063-delegated.
2064-
2065- >>> class SomeClass(object):
2066- ... delegates(IFoo)
2067- ... def __init__(self, context):
2068- ... self.context = context
2069-
2070- >>> f = BaseFoo()
2071- >>> s = SomeClass(f)
2072- >>> s.bar()
2073- 'bar'
2074-
2075- >>> s.baz
2076- 'hi baz!'
2077-
2078- >>> s.spoo
2079- 'some spoo'
2080-
2081- >>> IFoo.providedBy(s)
2082- True
2083-
2084-The ``delegates()`` function takes an optional keyword argument to change
2085-attribute containing the object to delegate to. So an existing class,
2086-such as SomeOtherClass, can declare the name of the attribute to which to
2087-delegate.
2088-
2089- >>> class SomeOtherClass(object):
2090- ... delegates(IFoo, context='myfoo')
2091- ... def __init__(self, foo):
2092- ... self.myfoo = foo
2093- ... spoo = 'spoo from SomeOtherClass'
2094-
2095- >>> f = BaseFoo()
2096- >>> s = SomeOtherClass(f)
2097- >>> s.bar()
2098- 'bar'
2099-
2100- >>> s.baz
2101- 'hi baz!'
2102-
2103- >>> s.spoo
2104- 'spoo from SomeOtherClass'
2105-
2106- >>> s.baz = 'fish'
2107- >>> s.baz
2108- 'fish'
2109-
2110- >>> f.baz
2111- 'fish'
2112-
2113-The ``delegates()`` function can only be used in new-style classes. An
2114-error is raised when a classic-style class is modified to implement an
2115-interface.
2116-
2117- >>> class SomeClassicClass:
2118- ... delegates(IFoo)
2119- Traceback (most recent call last):
2120- ...
2121- TypeError: Cannot use delegates() on a classic
2122- class: __builtin__.SomeClassicClass.
2123-
2124-The ``delegates()`` function cannot be used out side of a class definition,
2125-such as in a module or in a function.
2126-
2127- >>> delegates(IFoo)
2128- Traceback (most recent call last):
2129- ...
2130- TypeError: delegates() can be used only from a class definition.
2131-
2132-Multiple interfaces can be specified by passing an iterable to
2133-delegates().
2134-
2135- >>> class IOther(Interface):
2136- ... another = Attribute("another attribute")
2137-
2138- >>> class BaseOtherFoo(BaseFoo):
2139- ... another = 'yes, another'
2140-
2141- >>> class SomeOtherClass(object):
2142- ... delegates([IFoo, IOther])
2143-
2144- >>> s = SomeOtherClass()
2145- >>> s.context = BaseOtherFoo()
2146- >>> s.another
2147- 'yes, another'
2148-
2149- >>> s.baz
2150- 'hi baz!'
2151-
2152- >>> s.spoo
2153- 'some spoo'
2154-
2155- >>> IFoo.providedBy(s)
2156- True
2157-
2158- >>> IOther.providedBy(s)
2159- True
2160-
2161-This can be convenient when decorating an existing object.
2162-
2163- >>> from zope.interface import implements
2164- >>> class MoreFoo(BaseFoo, BaseOtherFoo):
2165- ... implements(IFoo, IOther)
2166-
2167- >>> foo = MoreFoo()
2168-
2169- >>> from zope.interface import providedBy
2170- >>> class WithExtraTeapot(object):
2171- ... delegates(providedBy(foo))
2172- ... teapot = 'i am a teapot'
2173-
2174- >>> foo_with_teapot = WithExtraTeapot()
2175- >>> foo_with_teapot.context = foo
2176-
2177- >>> foo_with_teapot.baz
2178- 'hi baz!'
2179-
2180- >>> foo_with_teapot.another
2181- 'yes, another'
2182-
2183- >>> foo_with_teapot.teapot
2184- 'i am a teapot'
2185-
2186- >>> IFoo.providedBy(foo_with_teapot)
2187- True
2188-
2189- >>> IOther.providedBy(foo_with_teapot)
2190- True
2191-
2192-==============
2193-Implementation
2194-==============
2195-
2196-The Passthrough class is the implementation machinery of ``delegates()``. It
2197-uses the descriptor protocol to implement the delegation behaviour provided by
2198-``delegates()``. It takes at least two arguments: the name of the attribute
2199-that is delegated, and the name of the attribute containing the object to
2200-which to delegate.
2201-
2202-To illustrate, p and p2 are two Passthrough instances that use the
2203-instance assigned to 'mycontext' to call the 'foo' attribute and
2204-the 'clsmethod' method.
2205-
2206- >>> from lazr.delegates import Passthrough
2207- >>> p = Passthrough('foo', 'mycontext')
2208- >>> p2 = Passthrough('clsmethod', 'mycontext')
2209-
2210-Base is a class the implements both 'foo' and 'clsmethod'.
2211-
2212- >>> class Base:
2213- ... foo = 'foo from Base'
2214- ... def clsmethod(cls):
2215- ... return str(cls)
2216- ... clsmethod = classmethod(clsmethod)
2217-
2218-Adapter is a class that has an instance of Base assigned to the
2219-attribute 'mycontext'.
2220-
2221- >>> base = Base()
2222-
2223- >>> class Adapter:
2224- ... mycontext = base
2225-
2226- >>> adapter = Adapter()
2227-
2228-The Passthrough instances can get and set their prescribed attributes
2229-when passed an instance of adapter.
2230-
2231- >>> p.__get__(adapter)
2232- 'foo from Base'
2233-
2234- >>> p.__get__(None, Adapter) is p
2235- True
2236-
2237- >>> p2.__get__(adapter)()
2238- '__builtin__.Base'
2239-
2240- >>> p.__set__(adapter, 'new value')
2241- >>> base.foo
2242- 'new value'
2243-
2244-Passthrough does not implement __delete__. An error is raised if
2245-it is called.
2246-
2247- >>> p.__delete__(adapter)
2248- Traceback (most recent call last):
2249- ...
2250- NotImplementedError
2251-
2252-Passthrough's third argument (adaptation) is optional and, when provided,
2253-should be a zope.interface.Interface subclass (although in practice any
2254-callable will do) to which the instance is adapted before getting/setting the
2255-delegated attribute.
2256-
2257- # HasNoFoo does not have a .foo attribute...
2258- >>> class HasNoFoo(object):
2259- ... _foo = 1
2260- >>> no_foo = HasNoFoo()
2261-
2262- # ... but IHasFooAdapter uses HasNoFoo._foo to provide its own .foo, so it
2263- # works like an adapter for HasNoFoo into some interface that provides
2264- # a 'foo' attribute.
2265- >>> class IHasFooAdapter(object):
2266- ... def __init__(self, inst):
2267- ... self.inst = inst
2268- ... def _get_foo(self):
2269- ... return self.inst._foo
2270- ... def _set_foo(self, value):
2271- ... self.inst._foo = value
2272- ... foo = property(_get_foo, _set_foo)
2273-
2274- >>> class Example(object):
2275- ... context = no_foo
2276-
2277- >>> p = Passthrough('foo', 'context', adaptation=IHasFooAdapter)
2278- >>> e = Example()
2279- >>> p.__get__(e)
2280- 1
2281- >>> p.__set__(e, 2)
2282- >>> p.__get__(e)
2283- 2
2284-
2285-
2286-===============
2287-Other Documents
2288-===============
2289-
2290-.. toctree::
2291- :glob:
2292-
2293- *
2294- docs/*
2295
2296=== removed file 'src/lazr/delegates/tests/test_docs.py'
2297--- src/lazr/delegates/tests/test_docs.py 2009-03-24 19:51:29 +0000
2298+++ src/lazr/delegates/tests/test_docs.py 1970-01-01 00:00:00 +0000
2299@@ -1,51 +0,0 @@
2300-# Copyright 2008-2009 Canonical Ltd. All rights reserved.
2301-#
2302-# This file is part of lazr.delegates
2303-#
2304-# lazr.delegates is free software: you can redistribute it and/or modify it
2305-# under the terms of the GNU Lesser General Public License as published by
2306-# the Free Software Foundation, version 3 of the License.
2307-#
2308-# lazr.delegates is distributed in the hope that it will be useful, but
2309-# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
2310-# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
2311-# License for more details.
2312-#
2313-# You should have received a copy of the GNU Lesser General Public License
2314-# along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>.
2315-"Test harness for doctests."
2316-
2317-# pylint: disable-msg=E0611,W0142
2318-
2319-__metaclass__ = type
2320-__all__ = [
2321- 'additional_tests',
2322- ]
2323-
2324-import atexit
2325-import doctest
2326-import os
2327-from pkg_resources import (
2328- resource_filename, resource_exists, resource_listdir, cleanup_resources)
2329-import unittest
2330-
2331-DOCTEST_FLAGS = (
2332- doctest.ELLIPSIS |
2333- doctest.NORMALIZE_WHITESPACE |
2334- doctest.REPORT_NDIFF)
2335-
2336-
2337-def additional_tests():
2338- "Run the doc tests (README.txt and docs/*, if any exist)"
2339- doctest_files = [
2340- os.path.abspath(resource_filename('lazr.delegates', 'README.txt'))]
2341- if resource_exists('lazr.delegates', 'docs'):
2342- for name in resource_listdir('lazr.delegates', 'docs'):
2343- if name.endswith('.txt'):
2344- doctest_files.append(
2345- os.path.abspath(
2346- resource_filename('lazr.delegates', 'docs/%s' % name)))
2347- kwargs = dict(module_relative=False, optionflags=DOCTEST_FLAGS)
2348- atexit.register(cleanup_resources)
2349- return unittest.TestSuite((
2350- doctest.DocFileSuite(*doctest_files, **kwargs)))

Subscribers

People subscribed via source and target branches