diff --git a/.pylintrc b/.pylintrc index 20209a128..2f8c804c0 100644 --- a/.pylintrc +++ b/.pylintrc @@ -40,7 +40,8 @@ # W0108 => unnecessary lambda # W0142 => Used * or ** magic # R0401 => cyclic-import -disable=I, C0111, W0108, W0142, R0921, R0922, W0232, R0401, R0801 +# R0204 => redefined-variable-type +disable=I, C0111, W0108, W0142, R0921, R0922, W0232, R0401, R0801, R0204 [REPORTS] diff --git a/boxsdk/util/compat.py b/boxsdk/util/compat.py index 9f5897a47..9f6132c76 100644 --- a/boxsdk/util/compat.py +++ b/boxsdk/util/compat.py @@ -5,6 +5,7 @@ from datetime import timedelta import six +from six.moves import map # pylint:disable=redefined-builtin if not hasattr(timedelta, 'total_seconds'): @@ -59,19 +60,66 @@ class Subclass(temporary_class): ``bases``, then errors might occur. For example, this was a problem when used with ``enum.EnumMeta`` in Python 3.6. Here we make sure that ``__prepare__()`` is defined on the temporary metaclass, and pass ``bases`` - to ``meta.__prepare__()``. + to ``meta.__prepare__()``. This is fixed in six>=1.11.0 by PR #178 [1]. Since ``temporary_class`` doesn't have the correct bases, in theory this could cause other problems, besides the previous one, in certain edge cases. To make sure that doesn't become a problem, we make sure that ``temporary_class`` has ``bases`` as its bases, just like the final class. + + [1] """ temporary_class = six.with_metaclass(meta, *bases, **with_metaclass_kwargs) temporary_metaclass = type(temporary_class) - class TemporaryMetaSubclass(temporary_metaclass): - @classmethod - def __prepare__(cls, name, this_bases, **kwds): # pylint:disable=unused-argument - return meta.__prepare__(name, bases, **kwds) + class TemporaryMetaSubclass(temporary_metaclass, _most_derived_metaclass(meta, bases)): + + if '__prepare__' not in temporary_metaclass.__dict__: + # six<1.11.0, __prepare__ is not defined on the temporary metaclass. + + @classmethod + def __prepare__(mcs, name, this_bases, **kwds): # pylint:disable=unused-argument,arguments-differ,bad-classmethod-argument + return meta.__prepare__(name, bases, **kwds) return type.__new__(TemporaryMetaSubclass, str('temporary_class'), bases, {}) + + +def raise_from(value, _): # pylint:disable=unused-argument + """Fallback for six.raise_from(), when using six<1.9.0.""" + raise value + + +raise_from = getattr(six, 'raise_from', raise_from) # pylint:disable=invalid-name + + +def _most_derived_metaclass(meta, bases): + """Selects the most derived metaclass of all the given metaclasses. + + This will be the same metaclass that is selected by + + .. code-block:: python + + class temporary_class(*bases, metaclass=meta): pass + + or equivalently by + + .. code-block:: python + + types.prepare_class('temporary_class', bases, metaclass=meta) + + "Most derived" means the item in {meta, type(bases[0]), type(bases[1]), ...} + which is a non-strict subclass of every item in that set. + + If no such item exists, then :exc:`TypeError` is raised. + + :type meta: `type` + :type bases: :class:`Iterable` of `type` + """ + most_derived_metaclass = meta + for base_type in map(type, bases): + if issubclass(base_type, most_derived_metaclass): + most_derived_metaclass = base_type + elif not issubclass(most_derived_metaclass, base_type): + # Raises TypeError('metaclass conflict: ...') + return type.__new__(meta, str('temporary_class'), bases, {}) + return most_derived_metaclass diff --git a/test/unit/util/test_compat.py b/test/unit/util/test_compat.py index 42263756b..96287e39b 100644 --- a/test/unit/util/test_compat.py +++ b/test/unit/util/test_compat.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from datetime import datetime, timedelta import pytest -from boxsdk.util.compat import total_seconds, with_metaclass +from boxsdk.util.compat import raise_from, total_seconds, with_metaclass @pytest.fixture(params=( @@ -50,3 +50,28 @@ class Subclass(temporary_class): assert type(Subclass) is Meta # pylint:disable=unidiomatic-typecheck assert Subclass.__bases__ == bases + + +class MyError1(Exception): + pass + + +class MyError2(Exception): + pass + + +class MyError3(Exception): + pass + + +@pytest.mark.parametrize('custom_context', [None, False, True]) +def test_raise_from(custom_context): + try: + raise MyError1 + except MyError1 as context: + if custom_context is False: + custom_context = context + elif custom_context is True: + custom_context = MyError2() + with pytest.raises(MyError3): + raise_from(MyError3(), custom_context)