Skip to content

Commit ad58f15

Browse files
authored
Fix some issues with six dependency (#241)
Add a fallback for `six.raise_from`, which isn't available in six 1.4.0. It isn't available until six 1.9.0. We could also have raised the lower bound for the six requirement, but this is an easy way to allow clients to keep using their existing versions of six. Fix support for the latest version of six, 1.11.0. That release changed the temporary metaclass returned from `with_metaclass()`, such that it directly inherits from `type`, instead of inheriting from the target metaclass [1]. We depended on this detail, and the change caused .. code-block:: python TypeError('metaclass conflict: ...') to be raised when defining a class with `with_metaclass()`. We fix this by manually selecting the most derived metaclass, and including it in our temporary metaclass. Also, `__prepare__` is now defined on the temporary metaclass, in six 1.11.0 [2]. This allows us to skip our own definition of that method, when using six>=1.11.0. Fixes #228. Fixes #239. [1] <benjaminp/six#191> [2] <benjaminp/six#178>
1 parent 967ec6a commit ad58f15

File tree

3 files changed

+81
-7
lines changed

3 files changed

+81
-7
lines changed

.pylintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
# W0108 => unnecessary lambda
4141
# W0142 => Used * or ** magic
4242
# R0401 => cyclic-import
43-
disable=I, C0111, W0108, W0142, R0921, R0922, W0232, R0401, R0801
43+
# R0204 => redefined-variable-type
44+
disable=I, C0111, W0108, W0142, R0921, R0922, W0232, R0401, R0801, R0204
4445

4546

4647
[REPORTS]

boxsdk/util/compat.py

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from datetime import timedelta
66

77
import six
8+
from six.moves import map # pylint:disable=redefined-builtin
89

910

1011
if not hasattr(timedelta, 'total_seconds'):
@@ -59,19 +60,66 @@ class Subclass(temporary_class):
5960
``bases``, then errors might occur. For example, this was a problem when
6061
used with ``enum.EnumMeta`` in Python 3.6. Here we make sure that
6162
``__prepare__()`` is defined on the temporary metaclass, and pass ``bases``
62-
to ``meta.__prepare__()``.
63+
to ``meta.__prepare__()``. This is fixed in six>=1.11.0 by PR #178 [1].
6364
6465
Since ``temporary_class`` doesn't have the correct bases, in theory this
6566
could cause other problems, besides the previous one, in certain edge
6667
cases. To make sure that doesn't become a problem, we make sure that
6768
``temporary_class`` has ``bases`` as its bases, just like the final class.
69+
70+
[1] <https://github.com/benjaminp/six/pull/178>
6871
"""
6972
temporary_class = six.with_metaclass(meta, *bases, **with_metaclass_kwargs)
7073
temporary_metaclass = type(temporary_class)
7174

72-
class TemporaryMetaSubclass(temporary_metaclass):
73-
@classmethod
74-
def __prepare__(cls, name, this_bases, **kwds): # pylint:disable=unused-argument
75-
return meta.__prepare__(name, bases, **kwds)
75+
class TemporaryMetaSubclass(temporary_metaclass, _most_derived_metaclass(meta, bases)):
76+
77+
if '__prepare__' not in temporary_metaclass.__dict__:
78+
# six<1.11.0, __prepare__ is not defined on the temporary metaclass.
79+
80+
@classmethod
81+
def __prepare__(mcs, name, this_bases, **kwds): # pylint:disable=unused-argument,arguments-differ,bad-classmethod-argument
82+
return meta.__prepare__(name, bases, **kwds)
7683

7784
return type.__new__(TemporaryMetaSubclass, str('temporary_class'), bases, {})
85+
86+
87+
def raise_from(value, _): # pylint:disable=unused-argument
88+
"""Fallback for six.raise_from(), when using six<1.9.0."""
89+
raise value
90+
91+
92+
raise_from = getattr(six, 'raise_from', raise_from) # pylint:disable=invalid-name
93+
94+
95+
def _most_derived_metaclass(meta, bases):
96+
"""Selects the most derived metaclass of all the given metaclasses.
97+
98+
This will be the same metaclass that is selected by
99+
100+
.. code-block:: python
101+
102+
class temporary_class(*bases, metaclass=meta): pass
103+
104+
or equivalently by
105+
106+
.. code-block:: python
107+
108+
types.prepare_class('temporary_class', bases, metaclass=meta)
109+
110+
"Most derived" means the item in {meta, type(bases[0]), type(bases[1]), ...}
111+
which is a non-strict subclass of every item in that set.
112+
113+
If no such item exists, then :exc:`TypeError` is raised.
114+
115+
:type meta: `type`
116+
:type bases: :class:`Iterable` of `type`
117+
"""
118+
most_derived_metaclass = meta
119+
for base_type in map(type, bases):
120+
if issubclass(base_type, most_derived_metaclass):
121+
most_derived_metaclass = base_type
122+
elif not issubclass(most_derived_metaclass, base_type):
123+
# Raises TypeError('metaclass conflict: ...')
124+
return type.__new__(meta, str('temporary_class'), bases, {})
125+
return most_derived_metaclass

test/unit/util/test_compat.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import unicode_literals
44
from datetime import datetime, timedelta
55
import pytest
6-
from boxsdk.util.compat import total_seconds, with_metaclass
6+
from boxsdk.util.compat import raise_from, total_seconds, with_metaclass
77

88

99
@pytest.fixture(params=(
@@ -50,3 +50,28 @@ class Subclass(temporary_class):
5050

5151
assert type(Subclass) is Meta # pylint:disable=unidiomatic-typecheck
5252
assert Subclass.__bases__ == bases
53+
54+
55+
class MyError1(Exception):
56+
pass
57+
58+
59+
class MyError2(Exception):
60+
pass
61+
62+
63+
class MyError3(Exception):
64+
pass
65+
66+
67+
@pytest.mark.parametrize('custom_context', [None, False, True])
68+
def test_raise_from(custom_context):
69+
try:
70+
raise MyError1
71+
except MyError1 as context:
72+
if custom_context is False:
73+
custom_context = context
74+
elif custom_context is True:
75+
custom_context = MyError2()
76+
with pytest.raises(MyError3):
77+
raise_from(MyError3(), custom_context)

0 commit comments

Comments
 (0)