From c61067712896cb13ea2161646a773bb39ca2149e Mon Sep 17 00:00:00 2001 From: Roman Krejcik Date: Thu, 17 Jul 2014 13:18:07 +0200 Subject: [PATCH 1/7] Python3 support --- parse_rest/connection.py | 17 ++--- parse_rest/datatypes.py | 31 ++++---- parse_rest/installation.py | 6 +- parse_rest/query.py | 24 +++---- parse_rest/tests.py | 141 ++++++++++++++++++------------------- parse_rest/user.py | 8 +-- setup.py | 10 ++- 7 files changed, 114 insertions(+), 123 deletions(-) diff --git a/parse_rest/connection.py b/parse_rest/connection.py index c18b0ee..ac97c1c 100644 --- a/parse_rest/connection.py +++ b/parse_rest/connection.py @@ -11,18 +11,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -try: - from urllib2 import Request, urlopen, HTTPError - from urllib import urlencode -except ImportError: - # is Python3 - from urllib.request import Request, urlopen - from urllib.error import HTTPError - from urllib.parse import urlencode +from six.moves.urllib.request import Request, urlopen +from six.moves.urllib.error import HTTPError +from six.moves.urllib.parse import urlencode import json -import core +from parse_rest import core API_ROOT = 'https://api.parse.com/1' ACCESS_KEYS = {} @@ -79,6 +74,8 @@ def execute(cls, uri, http_verb, extra_headers=None, batch=False, **kw): if http_verb == 'GET' and data: url += '?%s' % urlencode(kw) data = None + else: + data = data.encode('utf-8') request = Request(url, data, headers) request.add_header('Content-type', 'application/json') @@ -101,7 +98,7 @@ def execute(cls, uri, http_verb, extra_headers=None, batch=False, **kw): }.get(e.code, core.ParseError) raise exc(e.read()) - return json.loads(response.read()) + return json.loads(response.read().decode('utf-8')) @classmethod def GET(cls, uri, **kw): diff --git a/parse_rest/datatypes.py b/parse_rest/datatypes.py index c4cef72..795b6d3 100644 --- a/parse_rest/datatypes.py +++ b/parse_rest/datatypes.py @@ -10,12 +10,14 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import unicode_literals import base64 import datetime +import six -from connection import API_ROOT, ParseBase -from query import QueryManager +from parse_rest.connection import API_ROOT, ParseBase +from parse_rest.query import QueryManager class ParseType(object): @@ -94,7 +96,7 @@ def _prevent_circular(cls, parent_class_name, objectData): # also circular refs through more object are now ignored, in fact lazy loaded references will be best solution objectData = dict(objectData) # now lets see if we have any references to the parent class here - for key, value in objectData.iteritems(): + for key, value in six.iteritems(objectData): if isinstance(value, dict) and "className" in value and value["className"] == parent_class_name: # simply put the reference here as a string -- not sure what the drawbacks are for this but it works for me objectData[key] = value["objectId"] @@ -115,7 +117,6 @@ def from_native(cls, parent_class_name=None, **kw): return klass(**objectData) def __init__(self, obj): - self._object = obj def _to_native(self): @@ -157,7 +158,7 @@ def __init__(self, date): """Can be initialized either with a string or a datetime""" if isinstance(date, datetime.datetime): self._date = date - elif isinstance(date, unicode): + elif isinstance(date, six.string_types): self._date = Date._from_str(date) def _to_native(self): @@ -247,7 +248,6 @@ def _editable_attrs(self): return dict([(k, v) for k, v in self.__dict__.items() if allowed(k)]) def __init__(self, **kw): - for key, value in kw.items(): setattr(self, key, ParseType.convert_from_parse(value, self.__class__.__name__)) @@ -324,19 +324,21 @@ def call_back(response_dict): updatedAt = property(_get_updated_datetime, _set_updated_datetime) def __repr__(self): - return '<%s:%s>' % (unicode(self.__class__.__name__), self.objectId) + return '<%s:%s>' % (self.__class__.__name__, self.objectId) class ObjectMetaclass(type): - def __new__(cls, name, bases, dct): - cls = super(ObjectMetaclass, cls).__new__(cls, name, bases, dct) - cls.set_endpoint_root() - cls.Query = QueryManager(cls) + def __new__(mcs, name, bases, dct): + cls = super(ObjectMetaclass, mcs).__new__(mcs, name, bases, dct) + # attr check must be here because of specific six.with_metaclass implemetantion where metaclass is used also for + # internal NewBase which hasn't set_endpoint_root method + if hasattr(cls, 'set_endpoint_root'): + cls.set_endpoint_root() + cls.Query = QueryManager(cls) return cls -class Object(ParseResource): - __metaclass__ = ObjectMetaclass +class Object(six.with_metaclass(ObjectMetaclass, ParseResource)): ENDPOINT_ROOT = '/'.join([API_ROOT, 'classes']) @classmethod @@ -357,7 +359,8 @@ def set_endpoint_root(cls): @property def _absolute_url(self): - if not self.objectId: return None + if not self.objectId: + return None return '/'.join([self.__class__.ENDPOINT_ROOT, self.objectId]) @property diff --git a/parse_rest/installation.py b/parse_rest/installation.py index 8bfb780..e285f92 100644 --- a/parse_rest/installation.py +++ b/parse_rest/installation.py @@ -11,9 +11,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from connection import API_ROOT -from datatypes import ParseResource -from query import QueryManager +from parse_rest.connection import API_ROOT +from parse_rest.datatypes import ParseResource +from parse_rest.query import QueryManager class Installation(ParseResource): diff --git a/parse_rest/query.py b/parse_rest/query.py index 65f80f3..655630d 100644 --- a/parse_rest/query.py +++ b/parse_rest/query.py @@ -14,11 +14,8 @@ import json import collections import copy +import six -try: - unicode = unicode -except NameError: - unicode = str class QueryResourceDoesNotExist(Exception): '''Query returned no results''' @@ -42,8 +39,7 @@ def _fetch(self, **kw): def _count(self, **kw): kw.update({"count": 1, "limit": 0}) - return self.model_class.GET(self.model_class.ENDPOINT_ROOT, - **kw).get('count') + return self.model_class.GET(self.model_class.ENDPOINT_ROOT, **kw).get('count') def all(self): return Queryset(self) @@ -60,8 +56,8 @@ def get(self, **kw): class QuerysetMetaclass(type): """metaclass to add the dynamically generated comparison functions""" - def __new__(cls, name, bases, dct): - cls = super(QuerysetMetaclass, cls).__new__(cls, name, bases, dct) + def __new__(mcs, name, bases, dct): + cls = super(QuerysetMetaclass, mcs).__new__(mcs, name, bases, dct) for fname in ['limit', 'skip']: def func(self, value, fname=fname): @@ -73,16 +69,15 @@ def func(self, value, fname=fname): return cls -class Queryset(object): - __metaclass__ = QuerysetMetaclass +class Queryset(six.with_metaclass(QuerysetMetaclass, object)): OPERATORS = [ 'lt', 'lte', 'gt', 'gte', 'ne', 'in', 'nin', 'exists', 'select', 'dontSelect', 'all', 'relatedTo' - ] + ] @staticmethod def convert_to_parse(value): - from datatypes import ParseType + from parse_rest.datatypes import ParseType return ParseType.convert_to_parse(value, as_pointer=True) @classmethod @@ -143,8 +138,7 @@ def filter(self, **kw): self._where[attr]['$' + operator] = parse_value except TypeError: # self._where[attr] wasn't settable - raise ValueError("Cannot filter for a constraint " + - "after filtering for a specific value") + raise ValueError("Cannot filter for a constraint after filtering for a specific value") return self def order_by(self, order, descending=False): @@ -171,4 +165,4 @@ def get(self): return results[0] def __repr__(self): - return unicode(self._fetch()) + return repr(self._fetch()) diff --git a/parse_rest/tests.py b/parse_rest/tests.py index be52f98..ef1883d 100755 --- a/parse_rest/tests.py +++ b/parse_rest/tests.py @@ -4,20 +4,21 @@ """ Contains unit tests for the Python Parse REST API wrapper """ +from __future__ import print_function import os import sys import subprocess import unittest import datetime +import six - -from core import ResourceRequestNotFound -from connection import register, ParseBatcher -from datatypes import GeoPoint, Object, Function -from user import User -import query -from installation import Push +from parse_rest.core import ResourceRequestNotFound +from parse_rest.connection import register, ParseBatcher +from parse_rest.datatypes import GeoPoint, Object, Function +from parse_rest.user import User +from parse_rest import query +from parse_rest.installation import Push try: import settings_local @@ -25,17 +26,12 @@ sys.exit('You must create a settings_local.py file with APPLICATION_ID, ' \ 'REST_API_KEY, MASTER_KEY variables set') -try: - unicode = unicode -except NameError: - # is python3 - unicode = str register( getattr(settings_local, 'APPLICATION_ID'), getattr(settings_local, 'REST_API_KEY'), master_key=getattr(settings_local, 'MASTER_KEY') - ) +) GLOBAL_JSON_TEXT = """{ "applications": { @@ -95,48 +91,47 @@ def tearDown(self): score.delete() def testCanInitialize(self): - self.assert_(self.score.score == 1337, 'Could not set score') + self.assertEqual(self.score.score, 1337, 'Could not set score') def testCanInstantiateParseType(self): - self.assert_(self.sao_paulo.location.latitude == -23.5) + self.assertEqual(self.sao_paulo.location.latitude, -23.5) def testCanSaveDates(self): now = datetime.datetime.now() self.score.last_played = now self.score.save() - self.assert_(self.score.last_played == now, 'Could not save date') + self.assertEqual(self.score.last_played, now, 'Could not save date') def testCanCreateNewObject(self): self.score.save() object_id = self.score.objectId - self.assert_(object_id is not None, 'Can not create object') - self.assert_(type(object_id) == unicode) - self.assert_(type(self.score.createdAt) == datetime.datetime) - self.assert_(GameScore.Query.filter(objectId=object_id).exists(), - 'Can not create object') + self.assertIsNotNone(object_id, 'Can not create object') + self.assertIsInstance(object_id, six.string_types) + self.assertIsInstance(self.score.createdAt, datetime.datetime) + self.assertTrue(GameScore.Query.filter(objectId=object_id).exists(), 'Can not create object') def testCanUpdateExistingObject(self): self.sao_paulo.save() self.sao_paulo.country = 'Brazil' self.sao_paulo.save() - self.assert_(type(self.sao_paulo.updatedAt) == datetime.datetime) + self.assertIsInstance(self.sao_paulo.updatedAt, datetime.datetime) city = City.Query.get(name='São Paulo') - self.assert_(city.country == 'Brazil', 'Could not update object') + self.assertEqual(city.country, 'Brazil', 'Could not update object') def testCanDeleteExistingObject(self): self.score.save() object_id = self.score.objectId self.score.delete() - self.assert_(not GameScore.Query.filter(objectId=object_id).exists(), - 'Failed to delete object %s on Parse ' % self.score) + self.assertFalse(GameScore.Query.filter(objectId=object_id).exists(), + 'Failed to delete object %s on Parse ' % self.score) def testCanIncrementField(self): previous_score = self.score.score self.score.save() self.score.increment('score') - self.assert_(GameScore.Query.filter(score=previous_score + 1).exists(), + self.assertTrue(GameScore.Query.filter(score=previous_score + 1).exists(), 'Failed to increment score on backend') def testAssociatedObject(self): @@ -149,10 +144,8 @@ def testAssociatedObject(self): # get the object, see if it has saved qs = GameScore.Query.get(objectId=self.score.objectId) - self.assert_(isinstance(qs.item, Object), - "Associated CollectedItem is not an object") - self.assert_(qs.item.type == "Sword", - "Associated CollectedItem does not have correct attributes") + self.assertIsInstance(qs.item, Object, "Associated CollectedItem is not an object") + self.assertEqual(qs.item.type, "Sword", "Associated CollectedItem does not have correct attributes") def testBatch(self): """test saving, updating and deleting objects in batches""" @@ -160,9 +153,9 @@ def testBatch(self): for s in range(5)] batcher = ParseBatcher() batcher.batch_save(scores) - self.assert_(GameScore.Query.filter(player_name='Jane').count() == 5, + self.assertEqual(GameScore.Query.filter(player_name='Jane').count(), 5, "batch_save didn't create objects") - self.assert_(all(s.objectId is not None for s in scores), + self.assertTrue(all(s.objectId is not None for s in scores), "batch_save didn't record object IDs") # test updating @@ -172,11 +165,11 @@ def testBatch(self): updated_scores = GameScore.Query.filter(player_name='Jane') self.assertEqual(sorted([s.score for s in updated_scores]), - range(10, 15), msg="batch_save didn't update objects") + list(range(10, 15)), msg="batch_save didn't update objects") # test deletion batcher.batch_delete(scores) - self.assert_(GameScore.Query.filter(player_name='Jane').count() == 0, + self.assertEqual(GameScore.Query.filter(player_name='Jane').count(), 0, "batch_delete didn't delete objects") @@ -186,31 +179,31 @@ def setUp(self): self.score = GameScore( score=1337, player_name='John Doe', cheat_mode=False, date_of_birth=self.now - ) + ) self.sao_paulo = City( name='São Paulo', location=GeoPoint(-23.5, -46.6167) - ) + ) def testCanConvertToNative(self): native_data = self.sao_paulo._to_native() - self.assert_(type(native_data) is dict, 'Can not convert object to dict') + self.assertIsInstance(native_data, dict, 'Can not convert object to dict') def testCanConvertNestedLocation(self): native_sao_paulo = self.sao_paulo._to_native() location_dict = native_sao_paulo.get('location') - self.assert_(type(location_dict) is dict, - 'Expected dict after conversion. Got %s' % location_dict) - self.assert_(location_dict.get('latitude') == -23.5, - 'Can not serialize geopoint data') + self.assertIsInstance(location_dict, dict, + 'Expected dict after conversion. Got %s' % location_dict) + self.assertEqual(location_dict.get('latitude'), -23.5, + 'Can not serialize geopoint data') def testCanConvertDate(self): native_date = self.score._to_native().get('date_of_birth') - self.assert_(type(native_date) is dict, - 'Could not serialize date into dict') + self.assertIsInstance(native_date, dict, + 'Could not serialize date into dict') iso_date = native_date.get('iso') now = '{0}Z'.format(self.now.isoformat()[:-3]) - self.assert_(iso_date == now, 'Expected %s. Got %s' % (now, iso_date)) + self.assertEqual(iso_date, now, 'Expected %s. Got %s' % (now, iso_date)) class TestQuery(unittest.TestCase): @@ -235,23 +228,23 @@ def setUp(self): def testExists(self): """test the Queryset.exists() method""" for s in range(1, 6): - self.assert_(GameScore.Query.filter(score=s).exists(), - "exists giving false negative") - self.assert_(not GameScore.Query.filter(score=10).exists(), - "exists giving false positive") + self.assertTrue(GameScore.Query.filter(score=s).exists(), + "exists giving false negative") + self.assertFalse(GameScore.Query.filter(score=10).exists(), + "exists giving false positive") def testCanFilter(self): '''test the Queryset.filter() method''' for s in self.scores: qobj = GameScore.Query.filter(objectId=s.objectId).get() - self.assert_(qobj.objectId == s.objectId, - "Getting object with .filter() failed") - self.assert_(qobj.score == s.score, - "Getting object with .filter() failed") + self.assertEqual(qobj.objectId, s.objectId, + "Getting object with .filter() failed") + self.assertEqual(qobj.score, s.score, + "Getting object with .filter() failed") # test relational query with other Objects num_scores = GameScore.Query.filter(game=self.game).count() - self.assert_(num_scores == len(self.scores), + self.assertTrue(num_scores == len(self.scores), "Relational query with .filter() failed") def testGetExceptions(self): @@ -265,37 +258,37 @@ def testCanQueryDates(self): last_week = datetime.datetime.now() - datetime.timedelta(days=7) score = GameScore(name='test', last_played=last_week) score.save() - self.assert_(GameScore.Query.filter(last_played=last_week).exists(), - 'Could not run query with dates') + self.assertTrue(GameScore.Query.filter(last_played=last_week).exists(), + 'Could not run query with dates') def testComparisons(self): """test comparison operators- gt, gte, lt, lte, ne""" scores_gt_3 = list(GameScore.Query.filter(score__gt=3)) self.assertEqual(len(scores_gt_3), 2) - self.assert_(all([s.score > 3 for s in scores_gt_3])) + self.assertTrue(all([s.score > 3 for s in scores_gt_3])) scores_gte_3 = list(GameScore.Query.filter(score__gte=3)) self.assertEqual(len(scores_gte_3), 3) - self.assert_(all([s.score >= 3 for s in scores_gt_3])) + self.assertTrue(all([s.score >= 3 for s in scores_gt_3])) scores_lt_4 = list(GameScore.Query.filter(score__lt=4)) self.assertEqual(len(scores_lt_4), 3) - self.assert_(all([s.score < 4 for s in scores_lt_4])) + self.assertTrue(all([s.score < 4 for s in scores_lt_4])) scores_lte_4 = list(GameScore.Query.filter(score__lte=4)) self.assertEqual(len(scores_lte_4), 4) - self.assert_(all([s.score <= 4 for s in scores_lte_4])) + self.assertTrue(all([s.score <= 4 for s in scores_lte_4])) scores_ne_2 = list(GameScore.Query.filter(score__ne=2)) self.assertEqual(len(scores_ne_2), 4) - self.assert_(all([s.score != 2 for s in scores_ne_2])) + self.assertTrue(all([s.score != 2 for s in scores_ne_2])) # test chaining lt_4_gt_2 = list(GameScore.Query.filter(score__lt=4).filter(score__gt=2)) - self.assert_(len(lt_4_gt_2) == 1, 'chained lt+gt not working') - self.assert_(lt_4_gt_2[0].score == 3, 'chained lt+gt not working') + self.assertEqual(len(lt_4_gt_2), 1, 'chained lt+gt not working') + self.assertEqual(lt_4_gt_2[0].score, 3, 'chained lt+gt not working') q = GameScore.Query.filter(score__gt=3, score__lt=3) - self.assert_(not q.exists(), "chained lt+gt not working") + self.assertFalse(q.exists(), "chained lt+gt not working") def testOptions(self): """test three options- order, limit, and skip""" @@ -308,15 +301,15 @@ def testOptions(self): [5, 4, 3, 2, 1]) scores_limit_3 = list(GameScore.Query.all().limit(3)) - self.assert_(len(scores_limit_3) == 3, "Limit did not return 3 items") + self.assertTrue(len(scores_limit_3) == 3, "Limit did not return 3 items") scores_skip_3 = list(GameScore.Query.all().skip(3)) - self.assert_(len(scores_skip_3) == 2, "Skip did not return 2 items") + self.assertTrue(len(scores_skip_3) == 2, "Skip did not return 2 items") def testCanCompareDateInequality(self): today = datetime.datetime.today() tomorrow = today + datetime.timedelta(days=1) - self.assert_(GameScore.Query.filter(createdAt__lte=tomorrow).count() == 5, + self.assertTrue(GameScore.Query.filter(createdAt__lte=tomorrow).count() == 5, 'Could not make inequality comparison with dates') def tearDown(self): @@ -407,13 +400,13 @@ def tearDown(self): def testCanSignUp(self): self._destroy_user() user = User.signup(self.username, self.password) - self.assert_(user is not None) - self.assert_(user.username == self.username) + self.assertTrue(user is not None) + self.assertTrue(user.username == self.username) def testCanLogin(self): self._get_user() # User should be created here. user = User.login(self.username, self.password) - self.assert_(user.is_authenticated(), 'Login failed') + self.assertTrue(user.is_authenticated(), 'Login failed') def testCanUpdate(self): user = self._get_logged_user() @@ -423,7 +416,7 @@ def testCanUpdate(self): user.phone = phone_number user.save() - self.assert_(User.Query.filter(phone=phone_number).exists(), + self.assertTrue(User.Query.filter(phone=phone_number).exists(), 'Failed to update user data. New info not on Parse') def testCanBatchUpdate(self): @@ -436,10 +429,10 @@ def testCanBatchUpdate(self): batcher = ParseBatcher() batcher.batch_save([user]) - self.assert_(User.Query.filter(phone=phone_number).exists(), - 'Failed to batch update user data. New info not on Parse') - self.assert_(user.updatedAt != original_updatedAt, - 'Failed to batch update user data: updatedAt not changed') + self.assertTrue(User.Query.filter(phone=phone_number).exists(), + 'Failed to batch update user data. New info not on Parse') + self.assertNotEqual(user.updatedAt, original_updatedAt, + 'Failed to batch update user data: updatedAt not changed') class TestPush(unittest.TestCase): diff --git a/parse_rest/user.py b/parse_rest/user.py index 389766c..1e2f711 100644 --- a/parse_rest/user.py +++ b/parse_rest/user.py @@ -12,10 +12,10 @@ # along with this program. If not, see . -from core import ResourceRequestLoginRequired -from connection import API_ROOT -from datatypes import ParseResource, ParseType -from query import QueryManager +from parse_rest.core import ResourceRequestLoginRequired +from parse_rest.connection import API_ROOT +from parse_rest.datatypes import ParseResource, ParseType +from parse_rest.query import QueryManager def login_required(func): diff --git a/setup.py b/setup.py index 7e3c03d..df4147d 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,7 @@ def run(self): url='https://github.com/dgrtwo/ParsePy', packages=['parse_rest'], package_data={"parse_rest": [os.path.join("cloudcode", "*", "*")]}, + install_requires=['six'], maintainer='David Robinson', maintainer_email='dgrtwo@princeton.edu', cmdclass={'test': TestCommand}, @@ -36,6 +37,9 @@ def run(self): 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Operating System :: OS Independent', - 'Programming Language :: Python' - ] - ) + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + ] +) From 8aaea7fdfd8443fbb28ab30c92e7bded809da717 Mon Sep 17 00:00:00 2001 From: Roman Krejcik Date: Thu, 17 Jul 2014 15:34:36 +0200 Subject: [PATCH 2/7] test improvements - added simple test for select related, optimized number of queries in tests made skip and limit consistent with other query methods --- README.mkd | 5 +- parse_rest/connection.py | 11 ++-- parse_rest/query.py | 37 +++++-------- parse_rest/tests.py | 115 ++++++++++++++++++++------------------- 4 files changed, 82 insertions(+), 86 deletions(-) diff --git a/README.mkd b/README.mkd index efd2eb4..e4635c9 100644 --- a/README.mkd +++ b/README.mkd @@ -52,10 +52,9 @@ in the app and may accidentally replace or change existing objects. * install the [Parse CloudCode tool](https://www.parse.com/docs/cloud_code_guide) -You can then test the installation by running the following in a Python prompt: +You can then test the installation by running the following command: - from parse_rest import tests - tests.run_tests() + python -m 'parse_rest.tests' Usage diff --git a/parse_rest/connection.py b/parse_rest/connection.py index ac97c1c..b106dee 100644 --- a/parse_rest/connection.py +++ b/parse_rest/connection.py @@ -55,8 +55,7 @@ def execute(cls, uri, http_verb, extra_headers=None, batch=False, **kw): command. """ if batch: - ret = {"method": http_verb, - "path": uri.split("parse.com")[1]} + ret = {"method": http_verb, "path": uri.split("parse.com", 1)[1]} if kw: ret["body"] = kw return ret @@ -126,7 +125,9 @@ def batch(self, methods): Given a list of create, update or delete methods to call, call all of them in a single batch operation. """ - queries, callbacks = zip(*[m(batch=True) for m in methods]) + #accepts also empty list (or generator) - it allows call batch directly + # with query result (eventually empty) + queries, callbacks = list(zip(*[m(batch=True) for m in methods])) or ([], []) # perform all the operations in one batch responses = self.execute("", "POST", requests=queries) # perform the callbacks with the response data (updating the existing @@ -136,8 +137,8 @@ def batch(self, methods): def batch_save(self, objects): """save a list of objects in one operation""" - self.batch([o.save for o in objects]) + self.batch(o.save for o in objects) def batch_delete(self, objects): """delete a list of objects in one operation""" - self.batch([o.delete for o in objects]) + self.batch(o.delete for o in objects) diff --git a/parse_rest/query.py b/parse_rest/query.py index 655630d..a6f6303 100644 --- a/parse_rest/query.py +++ b/parse_rest/query.py @@ -13,8 +13,6 @@ import json import collections -import copy -import six class QueryResourceDoesNotExist(Exception): @@ -38,7 +36,7 @@ def _fetch(self, **kw): return [klass(**it) for it in klass.GET(uri, **kw).get('results')] def _count(self, **kw): - kw.update({"count": 1, "limit": 0}) + kw.update({"count": 1}) return self.model_class.GET(self.model_class.ENDPOINT_ROOT, **kw).get('count') def all(self): @@ -54,22 +52,7 @@ def get(self, **kw): return self.filter(**kw).get() -class QuerysetMetaclass(type): - """metaclass to add the dynamically generated comparison functions""" - def __new__(mcs, name, bases, dct): - cls = super(QuerysetMetaclass, mcs).__new__(mcs, name, bases, dct) - - for fname in ['limit', 'skip']: - def func(self, value, fname=fname): - s = copy.deepcopy(self) - s._options[fname] = int(value) - return s - setattr(cls, fname, func) - - return cls - - -class Queryset(six.with_metaclass(QuerysetMetaclass, object)): +class Queryset(object): OPERATORS = [ 'lt', 'lte', 'gt', 'gte', 'ne', 'in', 'nin', 'exists', 'select', 'dontSelect', 'all', 'relatedTo' @@ -99,7 +82,9 @@ def __iter__(self): return iter(self._fetch()) def __len__(self): - return self._fetch(count=True) + #don't use count query for len operator + #count doesn't return real size of result in all cases (eg if query contains skip option) + return len(self._fetch()) def __getitem__(self, key): if isinstance(key, slice): @@ -107,7 +92,7 @@ def __getitem__(self, key): return self._fetch()[key] def _fetch(self, count=False): - if self._result_cache: + if self._result_cache is not None: return len(self._result_cache) if count else self._result_cache """ Return a list of objects matching query, or if count == True return @@ -141,6 +126,14 @@ def filter(self, **kw): raise ValueError("Cannot filter for a constraint after filtering for a specific value") return self + def limit(self, value): + self._options['limit'] = int(value) + return self + + def skip(self, value): + self._options['skip'] = int(value) + return self + def order_by(self, order, descending=False): # add a minus sign before the order value if descending == True self._options['order'] = descending and ('-' + order) or order @@ -151,7 +144,7 @@ def select_related(self, *fields): return self def count(self): - return len(self) + return self._fetch(count=True) def exists(self): return bool(self) diff --git a/parse_rest/tests.py b/parse_rest/tests.py index ef1883d..5e9d622 100755 --- a/parse_rest/tests.py +++ b/parse_rest/tests.py @@ -12,6 +12,7 @@ import unittest import datetime import six +from itertools import chain from parse_rest.core import ResourceRequestNotFound from parse_rest.connection import register, ParseBatcher @@ -72,23 +73,16 @@ class CollectedItem(Object): class TestObject(unittest.TestCase): def setUp(self): - self.score = GameScore( - score=1337, player_name='John Doe', cheat_mode=False - ) - self.sao_paulo = City( - name='São Paulo', location=GeoPoint(-23.5, -46.6167) - ) + self.score = GameScore(score=1337, player_name='John Doe', cheat_mode=False) + self.sao_paulo = City(name='São Paulo', location=GeoPoint(-23.5, -46.6167)) def tearDown(self): city_name = getattr(self.sao_paulo, 'name', None) game_score = getattr(self.score, 'score', None) if city_name: - for city in City.Query.filter(name=city_name): - city.delete() - + ParseBatcher().batch_delete(City.Query.filter(name=city_name)) if game_score: - for score in GameScore.Query.filter(score=game_score): - score.delete() + ParseBatcher().batch_delete(GameScore.Query.filter(score=game_score)) def testCanInitialize(self): self.assertEqual(self.score.score, 1337, 'Could not set score') @@ -149,8 +143,7 @@ def testAssociatedObject(self): def testBatch(self): """test saving, updating and deleting objects in batches""" - scores = [GameScore(score=s, player_name='Jane', cheat_mode=False) - for s in range(5)] + scores = [GameScore(score=s, player_name='Jane', cheat_mode=False) for s in range(5)] batcher = ParseBatcher() batcher.batch_save(scores) self.assertEqual(GameScore.Query.filter(player_name='Jane').count(), 5, @@ -208,22 +201,33 @@ def testCanConvertDate(self): class TestQuery(unittest.TestCase): """Tests of an object's Queryset""" - def setUp(self): + + @classmethod + def setUpClass(cls): """save a bunch of GameScore objects with varying scores""" # first delete any that exist - for s in GameScore.Query.all(): - s.delete() - for g in Game.Query.all(): - g.delete() + ParseBatcher().batch_delete(GameScore.Query.all()) + ParseBatcher().batch_delete(Game.Query.all()) - self.game = Game(title="Candyland") - self.game.save() + cls.game = Game(title="Candyland") + cls.game.save() - self.scores = [ - GameScore(score=s, player_name='John Doe', game=self.game) - for s in range(1, 6)] - for s in self.scores: - s.save() + cls.scores = [GameScore(score=s, player_name='John Doe', game=cls.game) for s in range(1, 6)] + ParseBatcher().batch_save(cls.scores) + + @classmethod + def tearDownClass(cls): + '''delete all GameScore and Game objects''' + ParseBatcher().batch_delete(chain(cls.scores, [cls.game])) + + def setUp(self): + self.test_objects = [] + + def tearDown(self): + '''delete additional helper objects created in perticular tests''' + if self.test_objects: + ParseBatcher().batch_delete(self.test_objects) + self.test_objects = [] def testExists(self): """test the Queryset.exists() method""" @@ -258,65 +262,65 @@ def testCanQueryDates(self): last_week = datetime.datetime.now() - datetime.timedelta(days=7) score = GameScore(name='test', last_played=last_week) score.save() - self.assertTrue(GameScore.Query.filter(last_played=last_week).exists(), - 'Could not run query with dates') + self.test_objects.append(score) + self.assertTrue(GameScore.Query.filter(last_played=last_week).exists(), 'Could not run query with dates') + def testComparisons(self): """test comparison operators- gt, gte, lt, lte, ne""" - scores_gt_3 = list(GameScore.Query.filter(score__gt=3)) + scores_gt_3 = GameScore.Query.filter(score__gt=3) self.assertEqual(len(scores_gt_3), 2) self.assertTrue(all([s.score > 3 for s in scores_gt_3])) - scores_gte_3 = list(GameScore.Query.filter(score__gte=3)) + scores_gte_3 = GameScore.Query.filter(score__gte=3) self.assertEqual(len(scores_gte_3), 3) self.assertTrue(all([s.score >= 3 for s in scores_gt_3])) - scores_lt_4 = list(GameScore.Query.filter(score__lt=4)) + scores_lt_4 = GameScore.Query.filter(score__lt=4) self.assertEqual(len(scores_lt_4), 3) self.assertTrue(all([s.score < 4 for s in scores_lt_4])) - scores_lte_4 = list(GameScore.Query.filter(score__lte=4)) + scores_lte_4 = GameScore.Query.filter(score__lte=4) self.assertEqual(len(scores_lte_4), 4) self.assertTrue(all([s.score <= 4 for s in scores_lte_4])) - scores_ne_2 = list(GameScore.Query.filter(score__ne=2)) + scores_ne_2 = GameScore.Query.filter(score__ne=2) self.assertEqual(len(scores_ne_2), 4) self.assertTrue(all([s.score != 2 for s in scores_ne_2])) # test chaining - lt_4_gt_2 = list(GameScore.Query.filter(score__lt=4).filter(score__gt=2)) + lt_4_gt_2 = GameScore.Query.filter(score__lt=4).filter(score__gt=2) self.assertEqual(len(lt_4_gt_2), 1, 'chained lt+gt not working') self.assertEqual(lt_4_gt_2[0].score, 3, 'chained lt+gt not working') q = GameScore.Query.filter(score__gt=3, score__lt=3) self.assertFalse(q.exists(), "chained lt+gt not working") - def testOptions(self): + def testOrderBy(self): """test three options- order, limit, and skip""" - scores_ordered = list(GameScore.Query.all().order_by("score")) - self.assertEqual([s.score for s in scores_ordered], - [1, 2, 3, 4, 5]) + scores_ordered = GameScore.Query.all().order_by("score") + self.assertEqual([s.score for s in scores_ordered], [1, 2, 3, 4, 5]) - scores_ordered_desc = list(GameScore.Query.all().order_by("score", descending=True)) - self.assertEqual([s.score for s in scores_ordered_desc], - [5, 4, 3, 2, 1]) + scores_ordered_desc = GameScore.Query.all().order_by("score", descending=True) + self.assertEqual([s.score for s in scores_ordered_desc], [5, 4, 3, 2, 1]) - scores_limit_3 = list(GameScore.Query.all().limit(3)) - self.assertTrue(len(scores_limit_3) == 3, "Limit did not return 3 items") + def testLimit(self): + q = GameScore.Query.all().limit(3) + self.assertEqual(len(q), 3) - scores_skip_3 = list(GameScore.Query.all().skip(3)) - self.assertTrue(len(scores_skip_3) == 2, "Skip did not return 2 items") + def testSkip(self): + q = GameScore.Query.all().skip(3) + self.assertEqual(len(q), 2) + + def testSelectRelated(self): + score = GameScore.Query.all().select_related('game').limit(1)[0] + self.assertTrue(score.game.objectId) + #nice to have - also check no more then one query is triggered def testCanCompareDateInequality(self): today = datetime.datetime.today() tomorrow = today + datetime.timedelta(days=1) - self.assertTrue(GameScore.Query.filter(createdAt__lte=tomorrow).count() == 5, - 'Could not make inequality comparison with dates') - - def tearDown(self): - '''delete all GameScore and Game objects''' - for s in GameScore.Query.all(): - s.delete() - self.game.delete() + self.assertEqual(GameScore.Query.filter(createdAt__lte=tomorrow).count(), 5, + 'Could not make inequality comparison with dates') class TestFunction(unittest.TestCase): @@ -339,8 +343,7 @@ def setUp(self): os.chdir(original_dir) def tearDown(self): - for review in Review.Query.all(): - review.delete() + ParseBatcher().batch_delete(Review.Query.all()) def test_simple_functions(self): """test hello world and averageStars functions""" @@ -400,8 +403,8 @@ def tearDown(self): def testCanSignUp(self): self._destroy_user() user = User.signup(self.username, self.password) - self.assertTrue(user is not None) - self.assertTrue(user.username == self.username) + self.assertIsNotNone(user) + self.assertEqual(user.username, self.username) def testCanLogin(self): self._get_user() # User should be created here. From 84ddd38ea1c965ac8af1311c1773283a5865a8cd Mon Sep 17 00:00:00 2001 From: Roman Krejcik Date: Thu, 17 Jul 2014 16:15:59 +0200 Subject: [PATCH 3/7] independent derived queries - filter, limit, skip, select_related methods returns queryset copy instead of self This is proper implementation of idea suggested by orignal limit and skip methods based on metaclass (but not working at all) It works same as Django queries - original unfiltered query can be still executed or used to create different filter. --- parse_rest/query.py | 43 +++++++++++++++++++++++++++---------------- parse_rest/tests.py | 15 ++++++++++++++- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/parse_rest/query.py b/parse_rest/query.py index a6f6303..d940558 100644 --- a/parse_rest/query.py +++ b/parse_rest/query.py @@ -12,6 +12,7 @@ # along with this program. If not, see . import json +import copy import collections @@ -78,6 +79,13 @@ def __init__(self, manager): self._options = {} self._result_cache = None + def __deepcopy__(self, memo): + q = self.__class__(self._manager) + q._where = copy.deepcopy(self._where, memo) + q._options = copy.deepcopy(self._options, memo) + q._select_related.extend(self._select_related) + return q + def __iter__(self): return iter(self._fetch()) @@ -111,37 +119,40 @@ def _fetch(self, count=False): return self._result_cache def filter(self, **kw): + q = copy.deepcopy(self) for name, value in kw.items(): parse_value = Queryset.convert_to_parse(value) attr, operator = Queryset.extract_filter_operator(name) if operator is None: - self._where[attr] = parse_value + q._where[attr] = parse_value elif operator == 'relatedTo': - self._where['$' + operator] = parse_value + q._where['$' + operator] = parse_value else: - try: - self._where[attr]['$' + operator] = parse_value - except TypeError: - # self._where[attr] wasn't settable - raise ValueError("Cannot filter for a constraint after filtering for a specific value") - return self + if not isinstance(q._where[attr], dict): + q._where[attr] = {} + q._where[attr]['$' + operator] = parse_value + return q def limit(self, value): - self._options['limit'] = int(value) - return self + q = copy.deepcopy(self) + q._options['limit'] = int(value) + return q def skip(self, value): - self._options['skip'] = int(value) - return self + q = copy.deepcopy(self) + q._options['skip'] = int(value) + return q def order_by(self, order, descending=False): + q = copy.deepcopy(self) # add a minus sign before the order value if descending == True - self._options['order'] = descending and ('-' + order) or order - return self + q._options['order'] = descending and ('-' + order) or order + return q def select_related(self, *fields): - self._select_related.extend(fields) - return self + q = copy.deepcopy(self) + q._select_related.extend(fields) + return q def count(self): return self._fetch(count=True) diff --git a/parse_rest/tests.py b/parse_rest/tests.py index 5e9d622..7c244d3 100755 --- a/parse_rest/tests.py +++ b/parse_rest/tests.py @@ -288,13 +288,26 @@ def testComparisons(self): self.assertEqual(len(scores_ne_2), 4) self.assertTrue(all([s.score != 2 for s in scores_ne_2])) - # test chaining + def testChaining(self): lt_4_gt_2 = GameScore.Query.filter(score__lt=4).filter(score__gt=2) self.assertEqual(len(lt_4_gt_2), 1, 'chained lt+gt not working') self.assertEqual(lt_4_gt_2[0].score, 3, 'chained lt+gt not working') + q = GameScore.Query.filter(score__gt=3, score__lt=3) self.assertFalse(q.exists(), "chained lt+gt not working") + # test original queries are idependent after filting + q_all = GameScore.Query.all() + q_special = q_all.filter(score__gt=3) + self.assertEqual(len(q_all), 5) + self.assertEqual(len(q_special), 2) + + q_all = GameScore.Query.all() + q_limit = q_all.limit(1) + self.assertEqual(len(q_all), 5) + self.assertEqual(len(q_limit), 1) + + def testOrderBy(self): """test three options- order, limit, and skip""" scores_ordered = GameScore.Query.all().order_by("score") From b3316278d63573750bf8175bcffc9296d88aa10c Mon Sep 17 00:00:00 2001 From: Roman Krejcik Date: Thu, 17 Jul 2014 17:33:03 +0200 Subject: [PATCH 4/7] avoid executing empty batch, it has no effect --- parse_rest/connection.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/parse_rest/connection.py b/parse_rest/connection.py index b106dee..187bce0 100644 --- a/parse_rest/connection.py +++ b/parse_rest/connection.py @@ -125,9 +125,11 @@ def batch(self, methods): Given a list of create, update or delete methods to call, call all of them in a single batch operation. """ - #accepts also empty list (or generator) - it allows call batch directly - # with query result (eventually empty) - queries, callbacks = list(zip(*[m(batch=True) for m in methods])) or ([], []) + methods = list(methods) # methods can be iterator + if not methods: + #accepts also empty list (or generator) - it allows call batch directly with query result (eventually empty) + return + queries, callbacks = list(zip(*[m(batch=True) for m in methods])) # perform all the operations in one batch responses = self.execute("", "POST", requests=queries) # perform the callbacks with the response data (updating the existing From 859fd3e9474b3c6ab467994c60277b9ea9aaf291 Mon Sep 17 00:00:00 2001 From: Roman Krejcik Date: Fri, 18 Jul 2014 22:08:31 +0200 Subject: [PATCH 5/7] lazy loaded objects --- parse_rest/datatypes.py | 126 ++++++++++++++-------------------------- parse_rest/user.py | 2 +- 2 files changed, 46 insertions(+), 82 deletions(-) diff --git a/parse_rest/datatypes.py b/parse_rest/datatypes.py index 795b6d3..7c95eec 100644 --- a/parse_rest/datatypes.py +++ b/parse_rest/datatypes.py @@ -20,10 +20,19 @@ from parse_rest.query import QueryManager +def complex_type(name=None): + '''Decorator for registering complex types''' + def wrapped(cls): + ParseType.type_mapping[name or cls.__name__] = cls + return cls + return wrapped + + class ParseType(object): + type_mapping = {} @staticmethod - def convert_from_parse(parse_data, class_name): + def convert_from_parse(parse_data): is_parse_type = isinstance(parse_data, dict) and '__type' in parse_data @@ -31,28 +40,8 @@ def convert_from_parse(parse_data, class_name): if not is_parse_type: return parse_data - # determine just which kind of parse type this element is - ie: a built in parse type such as File, Pointer, User etc - parse_type = parse_data['__type'] - - # if its a pointer, we need to handle to ensure that we don't mishandle a circular reference - if parse_type == "Pointer": - # grab the pointer object here - return Pointer.from_native(class_name, **parse_data) - - # embedded object by select_related - if parse_type == "Object": - return EmbeddedObject.from_native(class_name, **parse_data) - - # now handle the other parse types accordingly - native = { - 'Date': Date, - 'Bytes': Binary, - 'GeoPoint': GeoPoint, - 'File': File, - 'Relation': Relation - }.get(parse_type) - - return native and native.from_native(**parse_data) or parse_data + native = ParseType.type_mapping.get(parse_data['__type']) + return native.from_native(**parse_data) if native else parse_data @staticmethod def convert_to_parse(python_object, as_pointer=False): @@ -69,7 +58,7 @@ def convert_to_parse(python_object, as_pointer=False): transformation_map = { datetime.datetime: Date, Object: Pointer - } + } if python_type in transformation_map: klass = transformation_map.get(python_type) @@ -85,36 +74,18 @@ def from_native(cls, **kw): return cls(**kw) def _to_native(self): - return self._value + raise NotImplementedError("_to_native must be overridden") +@complex_type('Pointer') class Pointer(ParseType): @classmethod - def _prevent_circular(cls, parent_class_name, objectData): - # TODO this should be replaced with more clever checking, instead of simple class mathching original id should be compared - # also circular refs through more object are now ignored, in fact lazy loaded references will be best solution - objectData = dict(objectData) - # now lets see if we have any references to the parent class here - for key, value in six.iteritems(objectData): - if isinstance(value, dict) and "className" in value and value["className"] == parent_class_name: - # simply put the reference here as a string -- not sure what the drawbacks are for this but it works for me - objectData[key] = value["objectId"] - return objectData - - @classmethod - def from_native(cls, parent_class_name=None, **kw): - # grab the object data manually here so we can manipulate it before passing back an actual object + def from_native(cls, **kw): + # create object with only objectId and unloaded flag. it is automatically loaded when any other field is accessed klass = Object.factory(kw.get('className')) - objectData = klass.GET("/" + kw.get('objectId')) - - # now lets check if we have circular references here - if parent_class_name: - objectData = cls._prevent_circular(parent_class_name, objectData) + return klass(objectId=kw.get('objectId'), _is_loaded=False) - # set a temporary flag that will remove the recursive pointer types etc - klass = Object.factory(kw.get('className')) - return klass(**objectData) def __init__(self, obj): self._object = obj @@ -124,24 +95,25 @@ def _to_native(self): '__type': 'Pointer', 'className': self._object.__class__.__name__, 'objectId': self._object.objectId - } + } +@complex_type('Object') class EmbeddedObject(ParseType): @classmethod - def from_native(cls, parent_class_name=None, **kw): - if parent_class_name: - kw = Pointer._prevent_circular(parent_class_name, kw) + def from_native(cls, **kw): klass = Object.factory(kw.get('className')) return klass(**kw) +@complex_type() class Relation(ParseType): @classmethod def from_native(cls, **kw): pass +@complex_type() class Date(ParseType): FORMAT = '%Y-%m-%dT%H:%M:%S.%f%Z' @@ -167,6 +139,7 @@ def _to_native(self): } +@complex_type('Bytes') class Binary(ParseType): @classmethod @@ -181,6 +154,7 @@ def _to_native(self): return {'__type': 'Bytes', 'base64': self._encoded} +@complex_type() class GeoPoint(ParseType): @classmethod @@ -199,6 +173,7 @@ def _to_native(self): } +@complex_type() class File(ParseType): @classmethod @@ -233,14 +208,10 @@ def __call__(self, **kwargs): return self.POST('/' + self.name, **kwargs) -class ParseResource(ParseBase, Pointer): +class ParseResource(ParseBase): PROTECTED_ATTRIBUTES = ['objectId', 'createdAt', 'updatedAt'] - @classmethod - def retrieve(cls, resource_id): - return cls(**cls.GET('/' + resource_id)) - @property def _editable_attrs(self): protected_attrs = self.__class__.PROTECTED_ATTRIBUTES @@ -248,19 +219,23 @@ def _editable_attrs(self): return dict([(k, v) for k, v in self.__dict__.items() if allowed(k)]) def __init__(self, **kw): - for key, value in kw.items(): - setattr(self, key, ParseType.convert_from_parse(value, self.__class__.__name__)) + self.objectId = None + self._init_attrs(kw) + + def __getattr__(self, attr): + # if object is not loaded and attribute is missing, try to load it + if not self.__dict__.get('_is_loaded', True): + del self._is_loaded + self._init_attrs(self.GET(self._absolute_url)) + return object.__getattribute__(self, attr) #preserve default if attr not exists + + def _init_attrs(self, args): + for key, value in six.iteritems(args): + setattr(self, key, ParseType.convert_from_parse(value)) def _to_native(self): return ParseType.convert_to_parse(self) - def _get_object_id(self): - return self.__dict__.get('_object_id') - - def _set_object_id(self, value): - if '_object_id' in self.__dict__: - raise ValueError('Can not re-set object id') - self._object_id = value def _get_updated_datetime(self): return self.__dict__.get('_updated_at') and self._updated_at._date @@ -294,8 +269,7 @@ def call_back(response_dict): call_back(response) def _update(self, batch=False): - response = self.__class__.PUT(self._absolute_url, batch=batch, - **self._to_native()) + response = self.__class__.PUT(self._absolute_url, batch=batch, **self._to_native()) def call_back(response_dict): self.updatedAt = response_dict['updatedAt'] @@ -307,19 +281,12 @@ def call_back(response_dict): def delete(self, batch=False): response = self.__class__.DELETE(self._absolute_url, batch=batch) - def call_back(response_dict): - self.__dict__ = {} - if batch: - return response, call_back - else: - call_back(response) + return response, lambda response_dict: None + - _absolute_url = property( - lambda self: '/'.join([self.__class__.ENDPOINT_ROOT, self.objectId]) - ) + _absolute_url = property(lambda self: '/'.join([self.__class__.ENDPOINT_ROOT, self.objectId])) - objectId = property(_get_object_id, _set_object_id) createdAt = property(_get_created_datetime, _set_created_datetime) updatedAt = property(_get_updated_datetime, _set_updated_datetime) @@ -365,10 +332,7 @@ def _absolute_url(self): @property def as_pointer(self): - return Pointer(**{ - 'className': self.__class__.__name__, - 'objectId': self.objectId - }) + return Pointer(self) def increment(self, key, amount=1): """ diff --git a/parse_rest/user.py b/parse_rest/user.py index 1e2f711..77dc79e 100644 --- a/parse_rest/user.py +++ b/parse_rest/user.py @@ -46,7 +46,7 @@ def authenticate(self, password=None, session_token=None): if password is not None: self = User.login(self.username, password) - user = User.retrieve(self.objectId) + user = User.Query.get(objectId=self.objectId) if user.objectId == self.objectId and user.sessionToken == session_token: self.sessionToken = session_token From 04a13ff78788e2a66cc9501ed80d94876d7c331c Mon Sep 17 00:00:00 2001 From: Roman Krejcik Date: Fri, 18 Jul 2014 22:11:11 +0200 Subject: [PATCH 6/7] Object.factory returns user defined types if exists. Also returns same type for two calls with same arguments. --- parse_rest/datatypes.py | 19 +++++++++++++------ parse_rest/tests.py | 6 +++++- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/parse_rest/datatypes.py b/parse_rest/datatypes.py index 7c95eec..cafb0ed 100644 --- a/parse_rest/datatypes.py +++ b/parse_rest/datatypes.py @@ -310,12 +310,19 @@ class Object(six.with_metaclass(ObjectMetaclass, ParseResource)): @classmethod def factory(cls, class_name): - - class DerivedClass(cls): - pass - DerivedClass.__name__ = str(class_name) - DerivedClass.set_endpoint_root() - return DerivedClass + """find proper Object subclass matching class_name + system types like _User are mapped to types without underscore (parse_resr.user.User) + If user don't declare matching type, class is created on the fly + """ + class_name = class_name.lstrip('_') + types = cls.__subclasses__() + while types: + t = types.pop() + if t.__name__ == class_name: + return t + types.extend(t.__subclasses__()) + else: + return type(class_name, (Object,), {}) @classmethod def set_endpoint_root(cls): diff --git a/parse_rest/tests.py b/parse_rest/tests.py index 7c244d3..6fefc95 100755 --- a/parse_rest/tests.py +++ b/parse_rest/tests.py @@ -90,6 +90,10 @@ def testCanInitialize(self): def testCanInstantiateParseType(self): self.assertEqual(self.sao_paulo.location.latitude, -23.5) + def testFactory(self): + self.assertEqual(Object.factory('_User'), User) + self.assertEqual(Object.factory('GameScore'), GameScore) + def testCanSaveDates(self): now = datetime.datetime.now() self.score.last_played = now @@ -138,7 +142,7 @@ def testAssociatedObject(self): # get the object, see if it has saved qs = GameScore.Query.get(objectId=self.score.objectId) - self.assertIsInstance(qs.item, Object, "Associated CollectedItem is not an object") + self.assertIsInstance(qs.item, CollectedItem) self.assertEqual(qs.item.type, "Sword", "Associated CollectedItem does not have correct attributes") def testBatch(self): From 25acbfb710419ae22e9506dd595d9221577d79d3 Mon Sep 17 00:00:00 2001 From: Roman Krejcik Date: Sat, 19 Jul 2014 00:21:26 +0200 Subject: [PATCH 7/7] python 2.x fix for Object.factory --- parse_rest/datatypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/parse_rest/datatypes.py b/parse_rest/datatypes.py index cafb0ed..7e325fb 100644 --- a/parse_rest/datatypes.py +++ b/parse_rest/datatypes.py @@ -314,8 +314,8 @@ def factory(cls, class_name): system types like _User are mapped to types without underscore (parse_resr.user.User) If user don't declare matching type, class is created on the fly """ - class_name = class_name.lstrip('_') - types = cls.__subclasses__() + class_name = str(class_name.lstrip('_')) + types = ParseResource.__subclasses__() while types: t = types.pop() if t.__name__ == class_name: