Merge ~cjwatson/lazr.delegates:no-py2 into lazr.delegates:main
- Git
- lp:~cjwatson/lazr.delegates
- no-py2
- Merge into main
Proposed by
Colin Watson
Status: | Merged |
---|---|
Merged at revision: | edccc841995e77e9dbd61464b30b74ae63cb8dcd |
Proposed branch: | ~cjwatson/lazr.delegates:no-py2 |
Merge into: | lazr.delegates:main |
Diff against target: |
696 lines (+78/-270) 11 files modified
.pre-commit-config.yaml (+5/-0) NEWS.rst (+3/-3) dev/null (+0/-159) setup.py (+4/-5) src/lazr/delegates/__init__.py (+5/-11) src/lazr/delegates/_delegates.py (+51/-58) src/lazr/delegates/docs/conf.py (+0/-2) src/lazr/delegates/tests/test_api.py (+1/-8) src/lazr/delegates/tests/test_docs.py (+0/-9) src/lazr/delegates/tests/test_passthrough.py (+3/-3) tox.ini (+6/-12) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jürgen Gmach | Approve | ||
Review via email: mp+413588@code.launchpad.net |
Commit message
Drop Python 2 support
Description of the change
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) : | # |
Revision history for this message
Jürgen Gmach (jugmac00) wrote : | # |
Looks good! Thank you for the update.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml | |||
2 | index 8691251..5d1fb12 100644 | |||
3 | --- a/.pre-commit-config.yaml | |||
4 | +++ b/.pre-commit-config.yaml | |||
5 | @@ -16,3 +16,8 @@ repos: | |||
6 | 16 | rev: ee781d3ce0ddf835267764f27f4ffdd2dd21fa27 | 16 | rev: ee781d3ce0ddf835267764f27f4ffdd2dd21fa27 |
7 | 17 | hooks: | 17 | hooks: |
8 | 18 | - id: woke-from-source | 18 | - id: woke-from-source |
9 | 19 | - repo: https://github.com/asottile/pyupgrade | ||
10 | 20 | rev: v2.31.0 | ||
11 | 21 | hooks: | ||
12 | 22 | - id: pyupgrade | ||
13 | 23 | args: [--keep-percent-format, --py3-plus] | ||
14 | diff --git a/NEWS.rst b/NEWS.rst | |||
15 | index af21504..80e1684 100644 | |||
16 | --- a/NEWS.rst | |||
17 | +++ b/NEWS.rst | |||
18 | @@ -2,12 +2,12 @@ | |||
19 | 2 | NEWS for lazr.delegates | 2 | NEWS for lazr.delegates |
20 | 3 | ======================= | 3 | ======================= |
21 | 4 | 4 | ||
23 | 5 | 2.0.5 | 5 | 2.1.0 |
24 | 6 | ===== | 6 | ===== |
25 | 7 | - Officially add support for Python 3.6, 3.7, 3.8, 3.9, and 3.10. | 7 | - Officially add support for Python 3.6, 3.7, 3.8, 3.9, and 3.10. |
27 | 8 | - Drop support for Python 3.2, 3.3, and 3.4. | 8 | - Drop support for Python 2, 3.2, 3.3, and 3.4. |
28 | 9 | - Test using ``zope.testrunner`` rather than ``nose``. | 9 | - Test using ``zope.testrunner`` rather than ``nose``. |
30 | 10 | - Combine coverage for Python 2 and 3. | 10 | - Bring coverage to 100%. |
31 | 11 | 11 | ||
32 | 12 | 12 | ||
33 | 13 | 2.0.4 (2017-10-20) | 13 | 2.0.4 (2017-10-20) |
34 | diff --git a/setup.py b/setup.py | |||
35 | index abe2368..9474d1d 100755 | |||
36 | --- a/setup.py | |||
37 | +++ b/setup.py | |||
38 | @@ -49,8 +49,6 @@ delegating behavior. | |||
39 | 49 | "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", | 49 | "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", |
40 | 50 | "Operating System :: OS Independent", | 50 | "Operating System :: OS Independent", |
41 | 51 | 'Programming Language :: Python', | 51 | 'Programming Language :: Python', |
42 | 52 | 'Programming Language :: Python :: 2', | ||
43 | 53 | 'Programming Language :: Python :: 2.7', | ||
44 | 54 | 'Programming Language :: Python :: 3', | 52 | 'Programming Language :: Python :: 3', |
45 | 55 | 'Programming Language :: Python :: 3.5', | 53 | 'Programming Language :: Python :: 3.5', |
46 | 56 | 'Programming Language :: Python :: 3.6', | 54 | 'Programming Language :: Python :: 3.6', |
47 | @@ -59,7 +57,8 @@ delegating behavior. | |||
48 | 59 | 'Programming Language :: Python :: 3.9', | 57 | 'Programming Language :: Python :: 3.9', |
49 | 60 | 'Programming Language :: Python :: 3.10', | 58 | 'Programming Language :: Python :: 3.10', |
50 | 61 | ], | 59 | ], |
54 | 62 | extras_require={ | 60 | extras_require={ |
55 | 63 | "docs": ["Sphinx"], | 61 | "docs": ["Sphinx"], |
56 | 64 | }, | 62 | }, |
57 | 63 | python_requires=">=3.5", | ||
58 | 65 | ) | 64 | ) |
59 | diff --git a/src/lazr/delegates/__init__.py b/src/lazr/delegates/__init__.py | |||
60 | index b0b15de..eb56e29 100644 | |||
61 | --- a/src/lazr/delegates/__init__.py | |||
62 | +++ b/src/lazr/delegates/__init__.py | |||
63 | @@ -17,6 +17,7 @@ | |||
64 | 17 | """Decorator helpers that simplify class composition.""" | 17 | """Decorator helpers that simplify class composition.""" |
65 | 18 | 18 | ||
66 | 19 | __all__ = [ | 19 | __all__ = [ |
67 | 20 | 'Passthrough', | ||
68 | 20 | 'delegate_to', | 21 | 'delegate_to', |
69 | 21 | ] | 22 | ] |
70 | 22 | 23 | ||
71 | @@ -24,14 +25,7 @@ __all__ = [ | |||
72 | 24 | from lazr.delegates._version import __version__ | 25 | from lazr.delegates._version import __version__ |
73 | 25 | __version__ | 26 | __version__ |
74 | 26 | 27 | ||
86 | 27 | from lazr.delegates._passthrough import Passthrough | 28 | from lazr.delegates._delegates import ( |
87 | 28 | 29 | Passthrough, | |
88 | 29 | # The class decorator syntax is different in Python 2 vs. Python 3. | 30 | delegate_to, |
89 | 30 | import sys | 31 | ) |
79 | 31 | if sys.version_info[0] == 2: | ||
80 | 32 | from lazr.delegates._python2 import delegate_to | ||
81 | 33 | # The legacy API is only compatible with Python 2. | ||
82 | 34 | from lazr.delegates._delegates import delegates | ||
83 | 35 | __all__.append('delegates') | ||
84 | 36 | else: | ||
85 | 37 | from lazr.delegates._python3 import delegate_to | ||
90 | diff --git a/src/lazr/delegates/_delegates.py b/src/lazr/delegates/_delegates.py | |||
91 | index ae09920..eb7c1b3 100644 | |||
92 | --- a/src/lazr/delegates/_delegates.py | |||
93 | +++ b/src/lazr/delegates/_delegates.py | |||
94 | @@ -1,4 +1,4 @@ | |||
96 | 1 | # Copyright 2008-2015 Canonical Ltd. All rights reserved. | 1 | # Copyright 2008-2022 Canonical Ltd. All rights reserved. |
97 | 2 | # | 2 | # |
98 | 3 | # This file is part of lazr.delegates. | 3 | # This file is part of lazr.delegates. |
99 | 4 | # | 4 | # |
100 | @@ -16,34 +16,22 @@ | |||
101 | 16 | 16 | ||
102 | 17 | """Decorator helpers that simplify class composition.""" | 17 | """Decorator helpers that simplify class composition.""" |
103 | 18 | 18 | ||
104 | 19 | from __future__ import absolute_import, print_function, unicode_literals | ||
105 | 20 | |||
106 | 21 | |||
107 | 22 | __metaclass__ = type | ||
108 | 23 | __all__ = [ | 19 | __all__ = [ |
110 | 24 | 'delegates', | 20 | 'Passthrough', |
111 | 21 | 'delegate_to', | ||
112 | 25 | ] | 22 | ] |
113 | 26 | 23 | ||
114 | 27 | |||
115 | 28 | import sys | ||
116 | 29 | from types import ClassType | ||
117 | 30 | |||
118 | 31 | from zope.interface.advice import addClassAdvisor | ||
119 | 32 | from zope.interface import classImplements | 24 | from zope.interface import classImplements |
120 | 33 | from zope.interface.interfaces import IInterface | ||
121 | 34 | 25 | ||
122 | 35 | from lazr.delegates._passthrough import Passthrough | ||
123 | 36 | 26 | ||
126 | 37 | 27 | def delegate_to(*interfaces, context='context'): | |
125 | 38 | def delegates(interface_spec, context='context'): | ||
127 | 39 | """Make an adapter into a decorator. | 28 | """Make an adapter into a decorator. |
128 | 40 | 29 | ||
129 | 41 | Use like: | 30 | Use like: |
130 | 42 | 31 | ||
131 | 32 | @implementer(IRosettaProject) | ||
132 | 33 | @delegate_to(IProject) | ||
133 | 43 | class RosettaProject: | 34 | class RosettaProject: |
134 | 44 | implements(IRosettaProject) | ||
135 | 45 | delegates(IProject) | ||
136 | 46 | |||
137 | 47 | def __init__(self, context): | 35 | def __init__(self, context): |
138 | 48 | self.context = context | 36 | self.context = context |
139 | 49 | 37 | ||
140 | @@ -53,10 +41,9 @@ def delegates(interface_spec, context='context'): | |||
141 | 53 | If you want to use a different name than "context" then you can explicitly | 41 | If you want to use a different name than "context" then you can explicitly |
142 | 54 | say so: | 42 | say so: |
143 | 55 | 43 | ||
144 | 44 | @implementer(IRosettaProject) | ||
145 | 45 | @delegate_to(IProject, context='project') | ||
146 | 56 | class RosettaProject: | 46 | class RosettaProject: |
147 | 57 | implements(IRosettaProject) | ||
148 | 58 | delegates(IProject, context='project') | ||
149 | 59 | |||
150 | 60 | def __init__(self, project): | 47 | def __init__(self, project): |
151 | 61 | self.project = project | 48 | self.project = project |
152 | 62 | 49 | ||
153 | @@ -67,45 +54,51 @@ def delegates(interface_spec, context='context'): | |||
154 | 67 | 54 | ||
155 | 68 | The minimal decorator looks like this: | 55 | The minimal decorator looks like this: |
156 | 69 | 56 | ||
157 | 57 | @delegate_to(IProject) | ||
158 | 70 | class RosettaProject: | 58 | class RosettaProject: |
159 | 71 | delegates(IProject) | ||
160 | 72 | |||
161 | 73 | def __init__(self, context): | 59 | def __init__(self, context): |
162 | 74 | self.context = context | 60 | self.context = context |
163 | 75 | |||
164 | 76 | """ | 61 | """ |
183 | 77 | # pylint: disable-msg=W0212 | 62 | if len(interfaces) == 0: |
184 | 78 | frame = sys._getframe(1) | 63 | raise TypeError('At least one interface is required') |
185 | 79 | locals = frame.f_locals | 64 | def _decorator(cls): |
186 | 80 | 65 | missing = object() | |
187 | 81 | # Try to make sure we were called from a class def | 66 | for interface in interfaces: |
188 | 82 | if (locals is frame.f_globals) or ('__module__' not in locals): | 67 | classImplements(cls, interface) |
189 | 83 | raise TypeError( | 68 | for name in interface: |
190 | 84 | "delegates() can be used only from a class definition.") | 69 | if getattr(cls, name, missing) is missing: |
191 | 85 | 70 | setattr(cls, name, Passthrough(name, context)) | |
192 | 86 | locals['__delegates_advice_data__'] = interface_spec, context | 71 | return cls |
193 | 87 | addClassAdvisor(_delegates_advice, depth=2) | 72 | return _decorator |
194 | 88 | 73 | ||
195 | 89 | 74 | ||
196 | 90 | def _delegates_advice(cls): | 75 | class Passthrough: |
197 | 91 | """Add a Passthrough class for each missing interface attribute. | 76 | """Call the delegated class for the decorator class. |
198 | 92 | 77 | ||
199 | 93 | This function connects the decorator class to the delegate class. | 78 | If the ``adaptation`` argument is not None, it should be a callable. It |
200 | 94 | Only new-style classes are supported. | 79 | will be called with the context, and should return an object that will |
201 | 80 | have the delegated attribute. The ``adaptation`` argument is expected to | ||
202 | 81 | be used with an interface, to adapt the context. | ||
203 | 95 | """ | 82 | """ |
220 | 96 | interface_spec, contextvar = cls.__dict__['__delegates_advice_data__'] | 83 | def __init__(self, name, contextvar, adaptation=None): |
221 | 97 | del cls.__delegates_advice_data__ | 84 | self.name = name |
222 | 98 | if isinstance(cls, ClassType): | 85 | self.contextvar = contextvar |
223 | 99 | raise TypeError( | 86 | self.adaptation = adaptation |
224 | 100 | 'Cannot use delegates() on a classic class: %s.' % cls) | 87 | |
225 | 101 | if IInterface.providedBy(interface_spec): | 88 | def __get__(self, inst, cls=None): |
226 | 102 | interfaces = [interface_spec] | 89 | if inst is None: |
227 | 103 | else: | 90 | return self |
228 | 104 | interfaces = interface_spec | 91 | else: |
229 | 105 | not_found = object() | 92 | context = getattr(inst, self.contextvar) |
230 | 106 | for interface in interfaces: | 93 | if self.adaptation is not None: |
231 | 107 | classImplements(cls, interface) | 94 | context = self.adaptation(context) |
232 | 108 | for name in interface: | 95 | return getattr(context, self.name) |
233 | 109 | if getattr(cls, name, not_found) is not_found: | 96 | |
234 | 110 | setattr(cls, name, Passthrough(name, contextvar)) | 97 | def __set__(self, inst, value): |
235 | 111 | return cls | 98 | context = getattr(inst, self.contextvar) |
236 | 99 | if self.adaptation is not None: | ||
237 | 100 | context = self.adaptation(context) | ||
238 | 101 | setattr(context, self.name, value) | ||
239 | 102 | |||
240 | 103 | def __delete__(self, inst): | ||
241 | 104 | raise NotImplementedError | ||
242 | diff --git a/src/lazr/delegates/_passthrough.py b/src/lazr/delegates/_passthrough.py | |||
243 | 112 | deleted file mode 100644 | 105 | deleted file mode 100644 |
244 | index 144f689..0000000 | |||
245 | --- a/src/lazr/delegates/_passthrough.py | |||
246 | +++ /dev/null | |||
247 | @@ -1,55 +0,0 @@ | |||
248 | 1 | # Copyright 2008-2015 Canonical Ltd. All rights reserved. | ||
249 | 2 | # | ||
250 | 3 | # This file is part of lazr.delegates. | ||
251 | 4 | # | ||
252 | 5 | # lazr.delegates is free software: you can redistribute it and/or modify it | ||
253 | 6 | # under the terms of the GNU Lesser General Public License as published by | ||
254 | 7 | # the Free Software Foundation, version 3 of the License. | ||
255 | 8 | # | ||
256 | 9 | # lazr.delegates is distributed in the hope that it will be useful, but | ||
257 | 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY | ||
258 | 11 | # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public | ||
259 | 12 | # License for more details. | ||
260 | 13 | # | ||
261 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
262 | 15 | # along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>. | ||
263 | 16 | |||
264 | 17 | from __future__ import absolute_import, print_function, unicode_literals | ||
265 | 18 | |||
266 | 19 | |||
267 | 20 | __metaclass__ = type | ||
268 | 21 | __all__ = [ | ||
269 | 22 | 'Passthrough', | ||
270 | 23 | ] | ||
271 | 24 | |||
272 | 25 | |||
273 | 26 | class Passthrough: | ||
274 | 27 | """Call the delegated class for the decorator class. | ||
275 | 28 | |||
276 | 29 | If the ``adaptation`` argument is not None, it should be a callable. It | ||
277 | 30 | will be called with the context, and should return an object that will | ||
278 | 31 | have the delegated attribute. The ``adaptation`` argument is expected to | ||
279 | 32 | be used with an interface, to adapt the context. | ||
280 | 33 | """ | ||
281 | 34 | def __init__(self, name, contextvar, adaptation=None): | ||
282 | 35 | self.name = name | ||
283 | 36 | self.contextvar = contextvar | ||
284 | 37 | self.adaptation = adaptation | ||
285 | 38 | |||
286 | 39 | def __get__(self, inst, cls=None): | ||
287 | 40 | if inst is None: | ||
288 | 41 | return self | ||
289 | 42 | else: | ||
290 | 43 | context = getattr(inst, self.contextvar) | ||
291 | 44 | if self.adaptation is not None: | ||
292 | 45 | context = self.adaptation(context) | ||
293 | 46 | return getattr(context, self.name) | ||
294 | 47 | |||
295 | 48 | def __set__(self, inst, value): | ||
296 | 49 | context = getattr(inst, self.contextvar) | ||
297 | 50 | if self.adaptation is not None: | ||
298 | 51 | context = self.adaptation(context) | ||
299 | 52 | setattr(context, self.name, value) | ||
300 | 53 | |||
301 | 54 | def __delete__(self, inst): | ||
302 | 55 | raise NotImplementedError | ||
303 | diff --git a/src/lazr/delegates/_python2.py b/src/lazr/delegates/_python2.py | |||
304 | 56 | deleted file mode 100644 | 0 | deleted file mode 100644 |
305 | index 2ce472a..0000000 | |||
306 | --- a/src/lazr/delegates/_python2.py | |||
307 | +++ /dev/null | |||
308 | @@ -1,38 +0,0 @@ | |||
309 | 1 | # Copyright 2014-2015 Canonical Ltd. All rights reserved. | ||
310 | 2 | # | ||
311 | 3 | # This file is part of lazr.delegates. | ||
312 | 4 | # | ||
313 | 5 | # lazr.delegates is free software: you can redistribute it and/or modify it | ||
314 | 6 | # under the terms of the GNU Lesser General Public License as published by | ||
315 | 7 | # the Free Software Foundation, version 3 of the License. | ||
316 | 8 | # | ||
317 | 9 | # lazr.delegates is distributed in the hope that it will be useful, but | ||
318 | 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY | ||
319 | 11 | # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public | ||
320 | 12 | # License for more details. | ||
321 | 13 | # | ||
322 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
323 | 15 | # along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>. | ||
324 | 16 | |||
325 | 17 | """Class decorator definition for Python 2.""" | ||
326 | 18 | |||
327 | 19 | from zope.interface import classImplements | ||
328 | 20 | |||
329 | 21 | from lazr.delegates._passthrough import Passthrough | ||
330 | 22 | |||
331 | 23 | |||
332 | 24 | def delegate_to(*interfaces, **kws): | ||
333 | 25 | context = kws.pop('context', 'context') | ||
334 | 26 | if len(kws) > 0: | ||
335 | 27 | raise TypeError('Too many arguments') | ||
336 | 28 | if len(interfaces) == 0: | ||
337 | 29 | raise TypeError('At least one interface is required') | ||
338 | 30 | def _decorator(cls): | ||
339 | 31 | missing = object() | ||
340 | 32 | for interface in interfaces: | ||
341 | 33 | classImplements(cls, interface) | ||
342 | 34 | for name in interface: | ||
343 | 35 | if getattr(cls, name, missing) is missing: | ||
344 | 36 | setattr(cls, name, Passthrough(name, context)) | ||
345 | 37 | return cls | ||
346 | 38 | return _decorator | ||
347 | diff --git a/src/lazr/delegates/_python3.py b/src/lazr/delegates/_python3.py | |||
348 | 39 | deleted file mode 100644 | 0 | deleted file mode 100644 |
349 | index f9430f5..0000000 | |||
350 | --- a/src/lazr/delegates/_python3.py | |||
351 | +++ /dev/null | |||
352 | @@ -1,38 +0,0 @@ | |||
353 | 1 | # Copyright 2014-2015 Canonical Ltd. All rights reserved. | ||
354 | 2 | # | ||
355 | 3 | # This file is part of lazr.delegates. | ||
356 | 4 | # | ||
357 | 5 | # lazr.delegates is free software: you can redistribute it and/or modify it | ||
358 | 6 | # under the terms of the GNU Lesser General Public License as published by | ||
359 | 7 | # the Free Software Foundation, version 3 of the License. | ||
360 | 8 | # | ||
361 | 9 | # lazr.delegates is distributed in the hope that it will be useful, but | ||
362 | 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY | ||
363 | 11 | # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public | ||
364 | 12 | # License for more details. | ||
365 | 13 | # | ||
366 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
367 | 15 | # along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>. | ||
368 | 16 | |||
369 | 17 | """Class decorator definition for Python 3. | ||
370 | 18 | |||
371 | 19 | This syntax is not legal in Python 2. | ||
372 | 20 | """ | ||
373 | 21 | |||
374 | 22 | from zope.interface import classImplements | ||
375 | 23 | |||
376 | 24 | from lazr.delegates._passthrough import Passthrough | ||
377 | 25 | |||
378 | 26 | |||
379 | 27 | def delegate_to(*interfaces, context='context'): | ||
380 | 28 | if len(interfaces) == 0: | ||
381 | 29 | raise TypeError('At least one interface is required') | ||
382 | 30 | def _decorator(cls): | ||
383 | 31 | missing = object() | ||
384 | 32 | for interface in interfaces: | ||
385 | 33 | classImplements(cls, interface) | ||
386 | 34 | for name in interface: | ||
387 | 35 | if getattr(cls, name, missing) is missing: | ||
388 | 36 | setattr(cls, name, Passthrough(name, context)) | ||
389 | 37 | return cls | ||
390 | 38 | return _decorator | ||
391 | diff --git a/src/lazr/delegates/docs/conf.py b/src/lazr/delegates/docs/conf.py | |||
392 | index fed69d1..adc0c50 100644 | |||
393 | --- a/src/lazr/delegates/docs/conf.py | |||
394 | +++ b/src/lazr/delegates/docs/conf.py | |||
395 | @@ -1,5 +1,3 @@ | |||
396 | 1 | # -*- coding: utf-8 -*- | ||
397 | 2 | # | ||
398 | 3 | # lazr.delegates documentation build configuration file, created by | 1 | # lazr.delegates documentation build configuration file, created by |
399 | 4 | # sphinx-quickstart on Mon Jan 7 10:37:37 2013. | 2 | # sphinx-quickstart on Mon Jan 7 10:37:37 2013. |
400 | 5 | # | 3 | # |
401 | diff --git a/src/lazr/delegates/tests/test_api.py b/src/lazr/delegates/tests/test_api.py | |||
402 | index 5ecea47..4f6f200 100644 | |||
403 | --- a/src/lazr/delegates/tests/test_api.py | |||
404 | +++ b/src/lazr/delegates/tests/test_api.py | |||
405 | @@ -25,11 +25,4 @@ class TestAPI(unittest.TestCase): | |||
406 | 25 | """Test various corner cases in the API.""" | 25 | """Test various corner cases in the API.""" |
407 | 26 | 26 | ||
408 | 27 | def test_no_interfaces(self): | 27 | def test_no_interfaces(self): |
417 | 28 | try: | 28 | self.assertRaises(TypeError, delegate_to) |
410 | 29 | @delegate_to() | ||
411 | 30 | class SomeClass(object): | ||
412 | 31 | pass | ||
413 | 32 | except TypeError: | ||
414 | 33 | pass | ||
415 | 34 | else: | ||
416 | 35 | self.fail('TypeError expected') | ||
418 | diff --git a/src/lazr/delegates/tests/test_docs.py b/src/lazr/delegates/tests/test_docs.py | |||
419 | index 7ca6971..9e19ce4 100644 | |||
420 | --- a/src/lazr/delegates/tests/test_docs.py | |||
421 | +++ b/src/lazr/delegates/tests/test_docs.py | |||
422 | @@ -16,9 +16,6 @@ | |||
423 | 16 | 16 | ||
424 | 17 | """Test harness for doctests.""" | 17 | """Test harness for doctests.""" |
425 | 18 | 18 | ||
426 | 19 | from __future__ import absolute_import, print_function, unicode_literals | ||
427 | 20 | |||
428 | 21 | __metaclass__ = type | ||
429 | 22 | __all__ = [] | 19 | __all__ = [] |
430 | 23 | 20 | ||
431 | 24 | import atexit | 21 | import atexit |
432 | @@ -50,17 +47,11 @@ def load_tests(loader, tests, pattern): | |||
433 | 50 | ) | 47 | ) |
434 | 51 | ) | 48 | ) |
435 | 52 | atexit.register(cleanup_resources) | 49 | atexit.register(cleanup_resources) |
436 | 53 | globs = { | ||
437 | 54 | "absolute_import": absolute_import, | ||
438 | 55 | "print_function": print_function, | ||
439 | 56 | "unicode_literals": unicode_literals, | ||
440 | 57 | } | ||
441 | 58 | tests.addTest( | 50 | tests.addTest( |
442 | 59 | doctest.DocFileSuite( | 51 | doctest.DocFileSuite( |
443 | 60 | *doctest_files, | 52 | *doctest_files, |
444 | 61 | module_relative=False, | 53 | module_relative=False, |
445 | 62 | optionflags=DOCTEST_FLAGS, | 54 | optionflags=DOCTEST_FLAGS, |
446 | 63 | globs=globs, | ||
447 | 64 | encoding="UTF-8" | 55 | encoding="UTF-8" |
448 | 65 | ) | 56 | ) |
449 | 66 | ) | 57 | ) |
450 | diff --git a/src/lazr/delegates/tests/test_passthrough.py b/src/lazr/delegates/tests/test_passthrough.py | |||
451 | index 009ed73..0905e3b 100644 | |||
452 | --- a/src/lazr/delegates/tests/test_passthrough.py | |||
453 | +++ b/src/lazr/delegates/tests/test_passthrough.py | |||
454 | @@ -58,13 +58,13 @@ class TestPassthrough(unittest.TestCase): | |||
455 | 58 | # provided, should be a zope.interface.Interface subclass (although in | 58 | # provided, should be a zope.interface.Interface subclass (although in |
456 | 59 | # practice any callable will do) to which the instance is adapted | 59 | # practice any callable will do) to which the instance is adapted |
457 | 60 | # before getting/setting the delegated attribute. | 60 | # before getting/setting the delegated attribute. |
459 | 61 | class HasNoFoo(object): | 61 | class HasNoFoo: |
460 | 62 | _foo = 1 | 62 | _foo = 1 |
461 | 63 | no_foo = HasNoFoo() | 63 | no_foo = HasNoFoo() |
462 | 64 | # ... but IHasFooAdapter uses HasNoFoo._foo to provide its own .foo, | 64 | # ... but IHasFooAdapter uses HasNoFoo._foo to provide its own .foo, |
463 | 65 | # so it works like an adapter for HasNoFoo into some interface that | 65 | # so it works like an adapter for HasNoFoo into some interface that |
464 | 66 | # provides a 'foo' attribute. | 66 | # provides a 'foo' attribute. |
466 | 67 | class IHasFooAdapter(object): | 67 | class IHasFooAdapter: |
467 | 68 | def __init__(self, inst): | 68 | def __init__(self, inst): |
468 | 69 | self.inst = inst | 69 | self.inst = inst |
469 | 70 | @property | 70 | @property |
470 | @@ -74,7 +74,7 @@ class TestPassthrough(unittest.TestCase): | |||
471 | 74 | def foo(self, value): | 74 | def foo(self, value): |
472 | 75 | self.inst._foo = value | 75 | self.inst._foo = value |
473 | 76 | 76 | ||
475 | 77 | class Example(object): | 77 | class Example: |
476 | 78 | context = no_foo | 78 | context = no_foo |
477 | 79 | 79 | ||
478 | 80 | p = Passthrough('foo', 'context', adaptation=IHasFooAdapter) | 80 | p = Passthrough('foo', 'context', adaptation=IHasFooAdapter) |
479 | diff --git a/src/lazr/delegates/tests/test_python2.py b/src/lazr/delegates/tests/test_python2.py | |||
480 | 81 | deleted file mode 100644 | 81 | deleted file mode 100644 |
481 | index ad3de7e..0000000 | |||
482 | --- a/src/lazr/delegates/tests/test_python2.py | |||
483 | +++ /dev/null | |||
484 | @@ -1,159 +0,0 @@ | |||
485 | 1 | # Copyright 2013-2015 Canonical Ltd. All rights reserved. | ||
486 | 2 | # | ||
487 | 3 | # This file is part of lazr.delegates. | ||
488 | 4 | # | ||
489 | 5 | # lazr.delegates is free software: you can redistribute it and/or modify it | ||
490 | 6 | # under the terms of the GNU Lesser General Public License as published by | ||
491 | 7 | # the Free Software Foundation, version 3 of the License. | ||
492 | 8 | # | ||
493 | 9 | # lazr.delegates is distributed in the hope that it will be useful, but | ||
494 | 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY | ||
495 | 11 | # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public | ||
496 | 12 | # License for more details. | ||
497 | 13 | # | ||
498 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
499 | 15 | # along with lazr.delegates. If not, see <http://www.gnu.org/licenses/>. | ||
500 | 16 | |||
501 | 17 | """Test the legacy API, which only works in Python 2. | ||
502 | 18 | |||
503 | 19 | All of these tests are copied almost verbatim from the old README.rst. | ||
504 | 20 | """ | ||
505 | 21 | |||
506 | 22 | |||
507 | 23 | from __future__ import absolute_import, print_function, unicode_literals | ||
508 | 24 | |||
509 | 25 | |||
510 | 26 | # Don't enable the following or we can't test classic class failures. | ||
511 | 27 | #__metaclass__ = type | ||
512 | 28 | __all__ = [ | ||
513 | 29 | 'TestLegacyAPI', | ||
514 | 30 | ] | ||
515 | 31 | |||
516 | 32 | |||
517 | 33 | import sys | ||
518 | 34 | import unittest | ||
519 | 35 | |||
520 | 36 | from zope.interface import Attribute, Interface, implements, providedBy | ||
521 | 37 | |||
522 | 38 | from lazr.delegates import delegate_to | ||
523 | 39 | if sys.version_info[0] == 2: | ||
524 | 40 | from lazr.delegates import delegates | ||
525 | 41 | |||
526 | 42 | |||
527 | 43 | class IFoo0(Interface): | ||
528 | 44 | spoo = Attribute('attribute in IFoo0') | ||
529 | 45 | |||
530 | 46 | class IFoo(IFoo0): | ||
531 | 47 | def bar(): | ||
532 | 48 | 'some method' | ||
533 | 49 | baz = Attribute('some attribute') | ||
534 | 50 | |||
535 | 51 | class BaseFoo0: | ||
536 | 52 | spoo = 'some spoo' | ||
537 | 53 | |||
538 | 54 | class BaseFoo(BaseFoo0): | ||
539 | 55 | def bar(self): | ||
540 | 56 | return 'bar' | ||
541 | 57 | baz = 'hi baz!' | ||
542 | 58 | |||
543 | 59 | class IOther(Interface): | ||
544 | 60 | another = Attribute('another attribute') | ||
545 | 61 | |||
546 | 62 | class BaseOtherFoo(BaseFoo): | ||
547 | 63 | another = 'yes, another' | ||
548 | 64 | |||
549 | 65 | |||
550 | 66 | @unittest.skipIf(sys.version_info[0] > 2, "only relevant in Python 2") | ||
551 | 67 | class TestLegacyAPI(unittest.TestCase): | ||
552 | 68 | def test_basic_usage(self): | ||
553 | 69 | class SomeClass(object): | ||
554 | 70 | delegates(IFoo) | ||
555 | 71 | def __init__(self, context): | ||
556 | 72 | self.context = context | ||
557 | 73 | |||
558 | 74 | f = BaseFoo() | ||
559 | 75 | s = SomeClass(f) | ||
560 | 76 | self.assertEqual(s.bar(), 'bar') | ||
561 | 77 | self.assertEqual(s.baz, 'hi baz!') | ||
562 | 78 | self.assertEqual(s.spoo, 'some spoo') | ||
563 | 79 | self.assertTrue(IFoo.providedBy(s)) | ||
564 | 80 | |||
565 | 81 | def test_keyword_context(self): | ||
566 | 82 | class SomeOtherClass(object): | ||
567 | 83 | delegates(IFoo, context='myfoo') | ||
568 | 84 | def __init__(self, foo): | ||
569 | 85 | self.myfoo = foo | ||
570 | 86 | spoo = 'spoo from SomeOtherClass' | ||
571 | 87 | |||
572 | 88 | f = BaseFoo() | ||
573 | 89 | s = SomeOtherClass(f) | ||
574 | 90 | self.assertEqual(s.bar(), 'bar') | ||
575 | 91 | self.assertEqual(s.baz, 'hi baz!') | ||
576 | 92 | self.assertEqual(s.spoo, 'spoo from SomeOtherClass') | ||
577 | 93 | |||
578 | 94 | s.baz = 'fish' | ||
579 | 95 | self.assertEqual(s.baz, 'fish') | ||
580 | 96 | self.assertEqual(f.baz, 'fish') | ||
581 | 97 | |||
582 | 98 | def test_classic_is_error(self): | ||
583 | 99 | try: | ||
584 | 100 | class SomeClassicClass: | ||
585 | 101 | delegates(IFoo) | ||
586 | 102 | except TypeError: | ||
587 | 103 | pass | ||
588 | 104 | else: | ||
589 | 105 | self.fail('TypeError expected') | ||
590 | 106 | |||
591 | 107 | def test_use_outside_class_is_error(self): | ||
592 | 108 | self.assertRaises(TypeError, delegates, IFoo) | ||
593 | 109 | |||
594 | 110 | def test_multiple_interfaces(self): | ||
595 | 111 | class SomeOtherClass(object): | ||
596 | 112 | delegates([IFoo, IOther]) | ||
597 | 113 | |||
598 | 114 | s = SomeOtherClass() | ||
599 | 115 | s.context = BaseOtherFoo() | ||
600 | 116 | self.assertEqual(s.another, 'yes, another') | ||
601 | 117 | self.assertEqual(s.baz, 'hi baz!') | ||
602 | 118 | self.assertEqual(s.spoo, 'some spoo') | ||
603 | 119 | self.assertTrue(IFoo.providedBy(s)) | ||
604 | 120 | self.assertTrue(IOther.providedBy(s)) | ||
605 | 121 | |||
606 | 122 | def test_decorate_existing_object(self): | ||
607 | 123 | class MoreFoo(BaseFoo, BaseOtherFoo): | ||
608 | 124 | implements([IFoo, IOther]) | ||
609 | 125 | |||
610 | 126 | foo = MoreFoo() | ||
611 | 127 | |||
612 | 128 | class WithExtraTeapot(object): | ||
613 | 129 | delegates(providedBy(foo)) | ||
614 | 130 | teapot = 'i am a teapot' | ||
615 | 131 | |||
616 | 132 | foo_with_teapot = WithExtraTeapot() | ||
617 | 133 | foo_with_teapot.context = foo | ||
618 | 134 | |||
619 | 135 | self.assertEqual(foo_with_teapot.baz, 'hi baz!') | ||
620 | 136 | self.assertEqual(foo_with_teapot.another, 'yes, another') | ||
621 | 137 | self.assertEqual(foo_with_teapot.teapot, 'i am a teapot') | ||
622 | 138 | self.assertTrue(IFoo.providedBy(foo_with_teapot)) | ||
623 | 139 | self.assertTrue(IOther.providedBy(foo_with_teapot)) | ||
624 | 140 | |||
625 | 141 | |||
626 | 142 | @unittest.skipIf(sys.version_info[0] > 2, "only relevant in Python 2") | ||
627 | 143 | class TestNewAPI(unittest.TestCase): | ||
628 | 144 | """Test corner cases in Python 2. | ||
629 | 145 | |||
630 | 146 | Most of the new API is tested in the doctest. The implementation of the | ||
631 | 147 | new API is different between Python 2 and Python 3, so test these corner | ||
632 | 148 | cases. | ||
633 | 149 | """ | ||
634 | 150 | def test_type_error(self): | ||
635 | 151 | # Too many arguments to @delegate_to() raises a TypeError. | ||
636 | 152 | try: | ||
637 | 153 | @delegate_to(IFoo0, context='myfoo', other='bogus') | ||
638 | 154 | class SomeClass(object): | ||
639 | 155 | pass | ||
640 | 156 | except TypeError: | ||
641 | 157 | pass | ||
642 | 158 | else: | ||
643 | 159 | self.fail('TypeError expected') | ||
644 | diff --git a/tox.ini b/tox.ini | |||
645 | index 7bdfc27..451caed 100644 | |||
646 | --- a/tox.ini | |||
647 | +++ b/tox.ini | |||
648 | @@ -1,7 +1,6 @@ | |||
649 | 1 | [tox] | 1 | [tox] |
650 | 2 | envlist = | 2 | envlist = |
651 | 3 | lint | 3 | lint |
652 | 4 | py27 | ||
653 | 5 | py35 | 4 | py35 |
654 | 6 | py36 | 5 | py36 |
655 | 7 | py37 | 6 | py37 |
656 | @@ -13,11 +12,9 @@ envlist = | |||
657 | 13 | 12 | ||
658 | 14 | [testenv] | 13 | [testenv] |
659 | 15 | deps = | 14 | deps = |
660 | 16 | . | ||
661 | 17 | zope.testrunner | 15 | zope.testrunner |
662 | 18 | coverage | ||
663 | 19 | commands = | 16 | commands = |
665 | 20 | coverage run -m zope.testrunner --test-path src --tests-pattern ^tests {posargs} | 17 | zope-testrunner --test-path src --tests-pattern ^tests {posargs} |
666 | 21 | 18 | ||
667 | 22 | [testenv:lint] | 19 | [testenv:lint] |
668 | 23 | basepython = python3.8 | 20 | basepython = python3.8 |
669 | @@ -36,22 +33,19 @@ deps = | |||
670 | 36 | .[docs] | 33 | .[docs] |
671 | 37 | 34 | ||
672 | 38 | [testenv:coverage] | 35 | [testenv:coverage] |
673 | 39 | description = Run coverage either via `tox` or `tox -e py27,py38,coverage` | ||
674 | 40 | basepython = python3 | 36 | basepython = python3 |
675 | 41 | skip_install = true | ||
676 | 42 | depends = | ||
677 | 43 | py27 | ||
678 | 44 | py38 | ||
679 | 45 | deps = | 37 | deps = |
680 | 46 | coverage | 38 | coverage |
681 | 39 | zope.testrunner | ||
682 | 47 | commands = | 40 | commands = |
685 | 48 | coverage combine | 41 | coverage erase |
686 | 49 | coverage report -m --fail-under=97 | 42 | coverage run -m zope.testrunner --test-path src --tests-pattern ^tests {posargs} |
687 | 43 | coverage html | ||
688 | 44 | coverage report -m --fail-under=100 | ||
689 | 50 | 45 | ||
690 | 51 | [coverage:run] | 46 | [coverage:run] |
691 | 52 | source = lazr.delegates | 47 | source = lazr.delegates |
692 | 53 | omit = */docs/conf.py | 48 | omit = */docs/conf.py |
693 | 54 | parallel = true | ||
694 | 55 | 49 | ||
695 | 56 | [coverage:paths] | 50 | [coverage:paths] |
696 | 57 | source = | 51 | source = |
LGTM with two minor nitpicks