Skip to content

Commit 9892fff

Browse files
committed
Cherry pick Python3.6 support from master branch
1 parent e230592 commit 9892fff

File tree

7 files changed

+149
-20
lines changed

7 files changed

+149
-20
lines changed

HISTORY.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Upcoming
1414
((access token), (refresh token or None)), instead of just the access token).
1515
In particular, this fixes an exception in ``BoxSession`` that always occurred
1616
when it tried to refresh any ``JWTAuth`` object.
17+
- Fixed an exception that was being raised from ``ExtendableEnumMeta.__dir__()``.
18+
- CPython 3.6 support.
1719

1820
1.5.3 (2016-05-26)
1921
++++++++++++++++++

boxsdk/object/events.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
# coding: utf-8
22

3-
from __future__ import unicode_literals
4-
3+
from __future__ import unicode_literals, absolute_import
54
from requests.exceptions import Timeout
6-
from six import with_metaclass
75

8-
from boxsdk.object.base_endpoint import BaseEndpoint
9-
from boxsdk.util.enum import ExtendableEnumMeta
10-
from boxsdk.util.lru_cache import LRUCache
11-
from boxsdk.util.text_enum import TextEnum
6+
from .base_endpoint import BaseEndpoint
7+
from ..util.compat import with_metaclass
8+
from ..util.enum import ExtendableEnumMeta
9+
from ..util.lru_cache import LRUCache
10+
from ..util.text_enum import TextEnum
1211

1312

1413
# pylint:disable=too-many-ancestors
@@ -55,6 +54,7 @@ class Events(BaseEndpoint):
5554

5655
def get_url(self, *args):
5756
"""Base class override."""
57+
# pylint:disable=arguments-differ
5858
return super(Events, self).get_url('events', *args)
5959

6060
def get_events(self, limit=100, stream_position=0, stream_type=UserEventsStreamType.ALL):

boxsdk/util/compat.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# coding: utf-8
22

3-
from __future__ import division, unicode_literals
4-
3+
from __future__ import absolute_import, division, unicode_literals
54

65
from datetime import timedelta
76

7+
import six
8+
9+
810
if not hasattr(timedelta, 'total_seconds'):
911
def total_seconds(delta):
1012
"""
@@ -15,3 +17,61 @@ def total_seconds(delta):
1517
else:
1618
def total_seconds(delta):
1719
return delta.total_seconds()
20+
21+
22+
def with_metaclass(meta, *bases, **with_metaclass_kwargs):
23+
"""Extends the behavior of six.with_metaclass.
24+
25+
The normal usage (expanded to include temporaries, to make the illustration
26+
easier) is:
27+
28+
.. code-block:: python
29+
30+
temporary_class = six.with_metaclass(meta, *bases)
31+
temporary_metaclass = type(temporary_class)
32+
33+
class Subclass(temporary_class):
34+
...
35+
36+
SubclassMeta = type(Subclass)
37+
38+
In this example:
39+
40+
- ``temporary_class`` is a class with ``(object,)`` as its bases.
41+
- ``temporary_metaclass`` is a metaclass with ``(meta,)`` as its bases.
42+
- ``Subclass`` is a class with ``bases`` as its bases.
43+
- ``SubclassMeta`` is ``meta``.
44+
45+
``six.with_metaclass()`` is defined in such a way that it can make sure
46+
that ``Subclass`` has the correct metaclass and bases, while only using
47+
syntax which is common to both Python 2 and Python 3.
48+
``temporary_metaclass()`` returns an instance of ``meta``, rather than an
49+
instance of itself / a subclass of ``temporary_class``, which is how
50+
``SubclassMeta`` ends up being ``meta``, and how the temporaries don't
51+
appear anywhere in the final subclass.
52+
53+
There are two problems with the current (as of six==1.10.0) implementation
54+
of ``six.with_metaclass()``, which this function solves.
55+
56+
``six.with_metaclass()`` does not define ``__prepare__()`` on the temporary
57+
metaclass. This means that ``meta.__prepare__()`` gets called directly,
58+
with bases set to ``(object,)``. If it needed to actually receive
59+
``bases``, then errors might occur. For example, this was a problem when
60+
used with ``enum.EnumMeta`` in Python 3.6. Here we make sure that
61+
``__prepare__()`` is defined on the temporary metaclass, and pass ``bases``
62+
to ``meta.__prepare__()``.
63+
64+
Since ``temporary_class`` doesn't have the correct bases, in theory this
65+
could cause other problems, besides the previous one, in certain edge
66+
cases. To make sure that doesn't become a problem, we make sure that
67+
``temporary_class`` has ``bases`` as its bases, just like the final class.
68+
"""
69+
temporary_class = six.with_metaclass(meta, *bases, **with_metaclass_kwargs)
70+
temporary_metaclass = type(temporary_class)
71+
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)
76+
77+
return type.__new__(TemporaryMetaSubclass, str('temporary_class'), bases, {})

boxsdk/util/enum.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# coding: utf-8
2+
# pylint:disable=no-value-for-parameter
23

34
from __future__ import absolute_import, unicode_literals
45

@@ -20,7 +21,9 @@ class ExtendableEnumMeta(EnumMeta):
2021
2122
This allows you to define hierarchies such as this:
2223
23-
class EnumBase(six.with_metaclass(ExtendableEnumMeta, Enum)): pass
24+
from box.util.compat import with_metaclass
25+
26+
class EnumBase(with_metaclass(ExtendableEnumMeta, Enum)): pass
2427
2528
class Enum1(EnumBase):
2629
A = 'A'
@@ -99,7 +102,7 @@ def in_(subclass):
99102
return any(map(in_, cls.__subclasses__()))
100103

101104
def __dir__(cls):
102-
return list(set(super(ExtendableEnumMeta, cls).__dir__()).union(set(map(dir, cls.__subclasses__()))))
105+
return list(set(super(ExtendableEnumMeta, cls).__dir__()).union(*map(dir, cls.__subclasses__())))
103106

104107
def __getitem__(cls, name):
105108
try:
@@ -129,7 +132,7 @@ def __getattr__(cls, name):
129132
# and __getitem__ have the same behavior. And __getitem__ has
130133
# the advantage of never grabbing anything other than enum
131134
# members.
132-
return cls[name]
135+
return cls[name] # pylint:disable=unsubscriptable-object
133136
except KeyError:
134137
pass
135138
# This needs to be `reraise()`, and not just `raise`. Otherwise,

test/functional/mock_box/util/chaos_utils.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,35 @@
11
# coding: utf-8
22

3-
from __future__ import unicode_literals
4-
from bottle import template
3+
from __future__ import absolute_import, unicode_literals
4+
55
from functools import wraps
66
import json
7-
import jsonpatch
87
from time import sleep
8+
9+
from bottle import template
910
import six
11+
1012
from test.functional.mock_box.util.http_utils import abort
1113

1214

15+
try:
16+
import jsonpatch
17+
except ValueError:
18+
# jsonpatch==1.14 on Python 3.6 cannot be imported.
19+
# It fails with the following stacktrace:
20+
#
21+
# .tox/py/lib/python3.6/site-packages/jsonpatch.py:114: in <module>
22+
# json.load = get_loadjson()
23+
# .tox/py/lib/python3.6/site-packages/jsonpatch.py:108: in get_loadjson
24+
# argspec = inspect.getargspec(json.load)
25+
# lib/python3.6/inspect.py:1039: in getargspec
26+
# raise ValueError("Function has keyword-only arguments or annotations"
27+
# E ValueError: Function has keyword-only arguments or annotations, use getfullargspec() API which can support them
28+
#
29+
# Until jsonpatch fixes this issue, we cannot use jsonpatch on Python >=3.6.
30+
jsonpatch = None
31+
32+
1333
def allow_chaos(method):
1434
"""Decorator for a method to allow erroneous operation."""
1535
method.call_number = 0
@@ -63,10 +83,19 @@ def xml(method):
6383

6484

6585
def patch(operations):
66-
json_patch = jsonpatch.JsonPatch(operations)
86+
if jsonpatch:
87+
json_patch = jsonpatch.JsonPatch(operations)
88+
89+
def patcher(doc):
90+
return json_patch.apply(doc)
91+
92+
else:
6793

68-
def patcher(doc):
69-
return json_patch.apply(doc)
94+
def patcher(doc):
95+
# If jsonpatch could not be imported, then `@chaos_utils.patch()`
96+
# will be disabled, and will silently return values unmodified,
97+
# without applying the JSON patch operations.
98+
return doc
7099

71100
def inner(patched_function):
72101
def patched_inner(*args, **kwargs):

test/unit/util/test_compat.py

Lines changed: 32 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
6+
from boxsdk.util.compat import total_seconds, with_metaclass
77

88

99
@pytest.fixture(params=(
@@ -19,3 +19,34 @@ def test_total_seconds(total_seconds_data):
1919
# pylint:disable=redefined-outer-name
2020
delta, seconds = total_seconds_data
2121
assert total_seconds(delta) == seconds
22+
23+
24+
def test_with_metaclass():
25+
26+
class Class1(object):
27+
pass
28+
29+
class Class2(object):
30+
pass
31+
32+
bases = (Class1, Class2)
33+
34+
class Meta(type):
35+
@classmethod
36+
def __prepare__(metacls, name, this_bases, **kwds): # pylint:disable=unused-argument
37+
assert this_bases == bases
38+
return {}
39+
40+
def __new__(metacls, name, this_bases, namespace, **kwds):
41+
assert this_bases == bases
42+
return super(Meta, metacls).__new__(metacls, name, this_bases, namespace, **kwds)
43+
44+
temporary_class = with_metaclass(Meta, *bases)
45+
assert isinstance(temporary_class, Meta)
46+
assert temporary_class.__bases__ == bases
47+
48+
class Subclass(temporary_class):
49+
pass
50+
51+
assert type(Subclass) is Meta # pylint:disable=unidiomatic-typecheck
52+
assert Subclass.__bases__ == bases

test/unit/util/test_enum.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
from enum import Enum
66
import pytest
7-
from six import with_metaclass
87

8+
from boxsdk.util.compat import with_metaclass
99
from boxsdk.util.enum import ExtendableEnumMeta
1010
from boxsdk.util.ordered_dict import OrderedDict
1111

@@ -169,3 +169,7 @@ def test_len(EnumBaseWithSubclassesDefined, enum_members):
169169
def test_reversed(EnumBaseWithSubclassesDefined):
170170
EnumBase = EnumBaseWithSubclassesDefined
171171
assert list(reversed(list(reversed(EnumBase)))) == list(EnumBase)
172+
173+
174+
def test_dir(EnumBaseWithSubclassesDefined):
175+
assert set(enum_member_names).issubset(dir(EnumBaseWithSubclassesDefined))

0 commit comments

Comments
 (0)