Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ max-branches=12
max-statements=50

# Maximum number of parents for a class (see R0901).
max-parents=7
max-parents=14

# Maximum number of attributes for a class (see R0902).
max-attributes=15
Expand Down
13 changes: 12 additions & 1 deletion HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,17 @@ Release History

**Features**

- Added ability to create custom subclasses of SDK objects with ``_item_type`` defined.
- Added more flexibility to the object translation system:

- Can create non-global ``Translator`` instances, which can extend or
not-extend the global default ``Translator``.
- Can initialize ``BoxSession`` with a custom ``Translator``.
- Can register custom subclasses on the ``Translator`` which is associated
with a ``BoxSession`` or a ``Client``.
- All translation of API responses now use the ``Translator`` that is
referenced by the ``BoxSession``, instead of directly using the global
default ``Translator``.

- Added an ``Event`` class.

**Other**
Expand All @@ -36,6 +46,7 @@ Release History
``BaseObject`` is the parent of all objects that are a part of the REST API. Another subclass of
``BaseAPIJSONObject``, ``APIJSONObject``, was created to represent pseudo-smart objects such as ``Event`` that are not
directly accessible through an API endpoint.
- Fixed an exception that was being raised from ``ExtendableEnumMeta.__dir__()``.

1.5.3 (2016-05-26)
++++++++++++++++++
Expand Down
31 changes: 19 additions & 12 deletions boxsdk/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from ..object.search import Search
from ..object.events import Events
from ..util.shared_link import get_shared_link_header
from ..util.translator import Translator


class Client(Cloneable):
Expand Down Expand Up @@ -62,6 +61,14 @@ def session(self):
"""
return self._session

@property
def translator(self):
"""The translator used for translating Box API JSON responses into `BaseAPIJSONObject` smart objects.

:rtype: :class:`Translator`
"""
return self._session.translator

def folder(self, folder_id):
"""
Initialize a :class:`Folder` object, whose box id is folder_id.
Expand All @@ -75,7 +82,7 @@ def folder(self, folder_id):
:rtype:
:class:`Folder`
"""
return Translator().translate('folder')(session=self._session, object_id=folder_id)
return self.translator.translate('folder')(session=self._session, object_id=folder_id)

def file(self, file_id):
"""
Expand All @@ -90,7 +97,7 @@ def file(self, file_id):
:rtype:
:class:`File`
"""
return Translator().translate('file')(session=self._session, object_id=file_id)
return self.translator.translate('file')(session=self._session, object_id=file_id)

def user(self, user_id='me'):
"""
Expand All @@ -105,7 +112,7 @@ def user(self, user_id='me'):
:rtype:
:class:`User`
"""
return Translator().translate('user')(session=self._session, object_id=user_id)
return self.translator.translate('user')(session=self._session, object_id=user_id)

def group(self, group_id):
"""
Expand All @@ -120,7 +127,7 @@ def group(self, group_id):
:rtype:
:class:`Group`
"""
return Translator().translate('group')(session=self._session, object_id=group_id)
return self.translator.translate('group')(session=self._session, object_id=group_id)

def collaboration(self, collab_id):
"""
Expand All @@ -135,7 +142,7 @@ def collaboration(self, collab_id):
:rtype:
:class:`Collaboration`
"""
return Translator().translate('collaboration')(session=self._session, object_id=collab_id)
return self.translator.translate('collaboration')(session=self._session, object_id=collab_id)

@api_call
def users(self, limit=None, offset=0, filter_term=None):
Expand Down Expand Up @@ -167,7 +174,7 @@ def users(self, limit=None, offset=0, filter_term=None):
params['filter_term'] = filter_term
box_response = self._session.get(url, params=params)
response = box_response.json()
user_class = Translator().translate('user')
user_class = self.translator.translate('user')
return [user_class(
session=self._session,
object_id=item['id'],
Expand Down Expand Up @@ -256,7 +263,7 @@ def group_membership(self, group_membership_id):
:rtype:
:class:`GroupMembership`
"""
return Translator().translate('group_membership')(
return self.translator.translate('group_membership')(
session=self._session,
object_id=group_membership_id,
)
Expand All @@ -274,7 +281,7 @@ def groups(self):
url = '{0}/groups'.format(API.BASE_API_URL)
box_response = self._session.get(url)
response = box_response.json()
group_class = Translator().translate('group')
group_class = self.translator.translate('group')
return [group_class(
session=self._session,
object_id=item['id'],
Expand Down Expand Up @@ -303,7 +310,7 @@ def create_group(self, name):
}
box_response = self._session.post(url, data=json.dumps(body_attributes))
response = box_response.json()
return Translator().translate('group')(
return self.translator.translate('group')(
session=self._session,
object_id=response['id'],
response_object=response,
Expand Down Expand Up @@ -334,7 +341,7 @@ def get_shared_item(self, shared_link, password=None):
'{0}/shared_items'.format(API.BASE_API_URL),
headers=get_shared_link_header(shared_link, password),
).json()
return Translator().translate(response['type'])(
return self.translator.translate(response['type'])(
session=self._session.with_shared_link(shared_link, password),
object_id=response['id'],
response_object=response,
Expand Down Expand Up @@ -389,7 +396,7 @@ def create_user(self, name, login=None, **user_attributes):
user_attributes['is_platform_access_only'] = True
box_response = self._session.post(url, data=json.dumps(user_attributes))
response = box_response.json()
return Translator().translate('user')(
return self.translator.translate('user')(
session=self._session,
object_id=response['id'],
response_object=response,
Expand Down
58 changes: 52 additions & 6 deletions boxsdk/object/base_api_json_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,68 @@

class BaseAPIJSONObjectMeta(type):
"""
Metaclass for Box API objects. Registers classes so that API responses can be translated to the correct type.
Relies on the _item_type field defined on the classes to match the type property of the response json.
But the type-class mapping will only be registered if the module of the class is imported.
So it's also important to add the module name to __all__ in object/__init__.py.
Metaclass for Box API objects.
Registers classes with the default translator, so that API responses can be
translated to the correct type. This relies on the _item_type field, which
must be defined in the class's namespace dict (and must be re-defined, in
order to register a custom subclass), to match the 'type' field of the
response json. But the type-class mapping will only be registered if the
module of the class is imported.
For example, events returned from the API look like
.. code-block:: json
{'type': 'event', ...}
so a class for that type could be created and registered with the default
translator like this:
.. code-block:: python
class Event(BaseAPIJSONObject):
_item_type = 'event'
...
NOTE: The default translator registration functionality is a private
implementation detail of the SDK, to make it easy to register the default
API object classes with the default translator. For convenience and
backwards-compatability, developers are allowed to re-define the _item_type
field in their own custom subclasses in order to take advantage of this
functionality, but are encouraged not to. Since this is a private
implementation detail, it may change or be removed in any major or minor
release. Additionally, it has the usual hazards of mutable global state.
The supported and recommended ways for registering custom subclasses are:
- Constructing a new :class:`Translator`, calling `Translator.register()`
as necessary, and passing it to the :class:`BoxSession` constructor.
- Calling `session.translator.register()` on an existing
:class:`BoxSession`.
- Calling `client.translator.register()` on an existing :class:`Client`.
"""

def __init__(cls, name, bases, attrs):
super(BaseAPIJSONObjectMeta, cls).__init__(name, bases, attrs)
item_type = getattr(cls, '_item_type', None)
item_type = attrs.get('_item_type', None)
if item_type is not None:
Translator().register(item_type, cls)
Translator._default_translator.register(item_type, cls) # pylint:disable=protected-access
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a method Translator.register_default method instead of accessing this protected attribute?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I started this work, I started it with Translator.default_translator. At some point I decided to switch it to Translator._default_translator, to discourage applications from using it.

I could add Translator.register_default, but then that might encourage people to use it.

If we're going to make something public, I think it'd be more useful to make Translator.default_translator public, rather than making it private but adding this public method. Unless you think there's utility in only making the register method public.

I think it's okay for us to access the protected attribute, since it's clearly documented as being for internal use.



@six.add_metaclass(BaseAPIJSONObjectMeta)
class BaseAPIJSONObject(object):
"""Base class containing basic logic shared between true REST objects and other objects (such as an Event)"""

# :attr _item_type:
# (protected) The Box resource type that this class represents.
# For API object/resource classes this should equal the expected value
# of the 'type' field in API JSON responses. Otherwise, this should be
# `None`.
# :type _item_type: `unicode` or `None`
#
# NOTE: When defining a leaf class with an _item_type in this SDK, it's
# also important to add the module name to __all__ in object/__init__.py,
# so that it will be imported and registered with the default translator.
_item_type = None

def __init__(self, response_object=None, **kwargs):
Expand Down
8 changes: 8 additions & 0 deletions boxsdk/object/base_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ def session(self):
"""
return self._session

@property
def translator(self):
"""The translator used for translating Box API JSON responses into `BaseAPIJSONObject` smart objects.

:rtype: :class:`Translator`
"""
return self._session.translator

def get_url(self, endpoint, *args):
"""
Return the URL used to access the endpoint.
Expand Down
3 changes: 1 addition & 2 deletions boxsdk/object/base_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

from .base_endpoint import BaseEndpoint
from .base_api_json_object import BaseAPIJSONObject
from ..util.translator import Translator
from ..util.api_call_decorator import api_call


Expand Down Expand Up @@ -208,7 +207,7 @@ def _paging_wrapper(self, url, starting_index, limit, factory=None):
for index_in_current_page, item in enumerate(response['entries']):
instance_factory = factory
if not instance_factory:
instance_factory = Translator().translate(item['type'])
instance_factory = self.translator.translate(item['type'])
instance = instance_factory(self._session, item['id'], item)
yield instance, current_page_size, index_in_current_page

Expand Down
3 changes: 1 addition & 2 deletions boxsdk/object/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from ..util.enum import ExtendableEnumMeta
from ..util.lru_cache import LRUCache
from ..util.text_enum import TextEnum
from ..util.translator import Translator


# pylint:disable=too-many-ancestors
Expand Down Expand Up @@ -94,7 +93,7 @@ def get_events(self, limit=100, stream_position=0, stream_type=UserEventsStreamT
box_response = self._session.get(url, params=params)
response = box_response.json().copy()
if 'entries' in response:
response['entries'] = [Translator().translate(item['type'])(item) for item in response['entries']]
response['entries'] = [self.translator.translate(item['type'])(item) for item in response['entries']]
return response

@api_call
Expand Down
7 changes: 3 additions & 4 deletions boxsdk/object/folder.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from boxsdk.object.user import User
from boxsdk.util.api_call_decorator import api_call
from boxsdk.util.text_enum import TextEnum
from boxsdk.util.translator import Translator


class FolderSyncState(TextEnum):
Expand Down Expand Up @@ -153,7 +152,7 @@ def get_items(self, limit, offset=0, fields=None):
params['fields'] = ','.join(fields)
box_response = self._session.get(url, params=params)
response = box_response.json()
return [Translator().translate(item['type'])(self._session, item['id'], item) for item in response['entries']]
return [self.translator.translate(item['type'])(self._session, item['id'], item) for item in response['entries']]

@api_call
def upload_stream(
Expand Down Expand Up @@ -218,7 +217,7 @@ def upload_stream(
box_response = self._session.post(url, data=data, files=files, expect_json_response=False)
file_response = box_response.json()['entries'][0]
file_id = file_response['id']
return Translator().translate(file_response['type'])(
return self.translator.translate(file_response['type'])(
session=self._session,
object_id=file_id,
response_object=file_response,
Expand Down Expand Up @@ -364,7 +363,7 @@ def add_collaborator(self, collaborator, role, notify=False):
box_response = self._session.post(url, expect_json_response=True, data=data, params=params)
collaboration_response = box_response.json()
collab_id = collaboration_response['id']
return Translator().translate(collaboration_response['type'])(
return self.translator.translate(collaboration_response['type'])(
session=self._session,
object_id=collab_id,
response_object=collaboration_response,
Expand Down
5 changes: 2 additions & 3 deletions boxsdk/object/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from .base_object import BaseObject
from ..config import API
from ..util.translator import Translator
from ..util.api_call_decorator import api_call


Expand Down Expand Up @@ -48,7 +47,7 @@ def membership(self, starting_index=0, limit=100, include_page_info=False):
"""
url = self.get_url('memberships')

membership_factory = partial(Translator().translate("group_membership"), group=self)
membership_factory = partial(self.translator.translate("group_membership"), group=self)
for group_membership_tuple in self._paging_wrapper(url, starting_index, limit, membership_factory):
if include_page_info:
yield group_membership_tuple
Expand Down Expand Up @@ -83,4 +82,4 @@ def add_member(self, user, role):
box_response = self._session.post(url, data=json.dumps(body_attributes))
response = box_response.json()

return Translator().translate(response['type'])(self._session, response['id'], response, user=user, group=self)
return self.translator.translate(response['type'])(self._session, response['id'], response, user=user, group=self)
5 changes: 2 additions & 3 deletions boxsdk/object/group_membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from __future__ import unicode_literals, absolute_import

from .base_object import BaseObject
from ..util.translator import Translator


class GroupMembership(BaseObject):
Expand Down Expand Up @@ -70,8 +69,8 @@ def _init_user_and_group_instances(session, response_object, user, group):
user_info = response_object.get('user')
group_info = response_object.get('group')

user = user or Translator().translate(user_info['type'])(session, user_info['id'], user_info)
group = group or Translator().translate(group_info['type'])(session, group_info['id'], group_info)
user = user or session.translator.translate(user_info['type'])(session, user_info['id'], user_info)
group = group or session.translator.translate(group_info['type'])(session, group_info['id'], group_info)

return user, group

Expand Down
3 changes: 1 addition & 2 deletions boxsdk/object/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import json

from .base_endpoint import BaseEndpoint
from ..util.translator import Translator
from ..util.api_call_decorator import api_call


Expand Down Expand Up @@ -231,4 +230,4 @@ def search(
params.update(kwargs)
box_response = self._session.get(url, params=params)
response = box_response.json()
return [Translator().translate(item['type'])(self._session, item['id'], item) for item in response['entries']]
return [self.translator.translate(item['type'])(self._session, item['id'], item) for item in response['entries']]
Loading