Skip to content

Commit

Permalink
Fix some issues with six dependency
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
jmoldow committed Sep 18, 2017
1 parent 967ec6a commit a03a5bf
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 6 deletions.
58 changes: 53 additions & 5 deletions boxsdk/util/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from datetime import timedelta

import six
from six.moves import map


if not hasattr(timedelta, 'total_seconds'):
Expand Down Expand Up @@ -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] <https://github.com/benjaminp/six/pull/178>
"""
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
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
27 changes: 26 additions & 1 deletion test/unit/util/test_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=(
Expand Down Expand Up @@ -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)

0 comments on commit a03a5bf

Please sign in to comment.