diff --git a/AUTHORS b/AUTHORS index f774d22e..fe8be767 100644 --- a/AUTHORS +++ b/AUTHORS @@ -113,4 +113,5 @@ that much better: * Alexander Koshelev * Jaime Irurzun * Alexandre González - * Thomas Steinacher \ No newline at end of file + * Thomas Steinacher + * Tommi Komulainen \ No newline at end of file diff --git a/LICENSE b/LICENSE index e33b2c59..cef91cca 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ -Copyright (c) 2009-2010 Harry Marr - +Copyright (c) 2009-2012 See AUTHORS + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without @@ -8,10 +8,10 @@ copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND diff --git a/docs/changelog.rst b/docs/changelog.rst index 97926f8d..08091acb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,15 @@ Changelog ========= +Changes in 0.6.X +================ + +- Added Binary support to UUID (MongoEngine/mongoengine#47) +- Fixed MapField lookup for fields without declared lookups (MongoEngine/mongoengine#46) +- Fixed BinaryField python value issue (MongoEngine/mongoengine#48) +- Fixed SequenceField non numeric value lookup (MongoEngine/mongoengine#41) +- Fixed queryset manager issue (MongoEngine/mongoengine#52) +- Fixed FileField comparision (hmarr/mongoengine#547) Changes in 0.6.18 ================= diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 94f79a12..82689929 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -4,9 +4,9 @@ import decimal import gridfs import re import uuid +import warnings from bson import Binary, DBRef, SON, ObjectId - from base import (BaseField, ComplexBaseField, ObjectIdField, ValidationError, get_document, BaseDocument) from queryset import DO_NOTHING, QuerySet @@ -845,12 +845,9 @@ class BinaryField(BaseField): def to_mongo(self, value): return Binary(value) - def to_python(self, value): - return "%s" % value - def validate(self, value): - if not isinstance(value, basestring): - self.error('BinaryField only accepts string values') + if not isinstance(value, (basestring, Binary)): + self.error('BinaryField only accepts string or bson Binary values') if self.max_bytes is not None and len(value) > self.max_bytes: self.error('Binary value is too long') @@ -907,6 +904,8 @@ class GridFSProxy(object): return '<%s: %s>' % (self.__class__.__name__, self.grid_id) def __cmp__(self, other): + if not isinstance(other, GridFSProxy): + return -1 return cmp((self.grid_id, self.collection_name, self.db_alias), (other.grid_id, other.collection_name, other.db_alias)) @@ -1289,7 +1288,7 @@ class SequenceField(IntField): instance._data[self.name] = value instance._mark_as_changed(self.name) - return value + return int(value) if value else None def __set__(self, instance, value): @@ -1309,17 +1308,40 @@ class UUIDField(BaseField): .. versionadded:: 0.6 """ + _binary = None - def __init__(self, **kwargs): + def __init__(self, binary=None, **kwargs): + """ + Store UUID data in the database + + :param binary: (optional) boolean store as binary. + + .. versionchanged:: 0.6.19 + """ + if binary is None: + binary = False + msg = ("UUIDFields will soon default to store as binary, please " + "configure binary=False if you wish to store as a string") + warnings.warn(msg, FutureWarning) + self._binary = binary super(UUIDField, self).__init__(**kwargs) def to_python(self, value): - if not isinstance(value, basestring): - value = unicode(value) - return uuid.UUID(value) + if not self.binary: + if not isinstance(value, basestring): + value = unicode(value) + return uuid.UUID(value) + return value def to_mongo(self, value): - return unicode(value) + if not self._binary: + return unicode(value) + return value + + def prepare_query_value(self, op, value): + if value is None: + return None + return self.to_mongo(value) def validate(self, value): if not isinstance(value, uuid.UUID): diff --git a/mongoengine/queryset.py b/mongoengine/queryset.py index 0e93fd2b..366dd21a 100644 --- a/mongoengine/queryset.py +++ b/mongoengine/queryset.py @@ -641,7 +641,7 @@ class QuerySet(object): from mongoengine.fields import ReferenceField, GenericReferenceField if isinstance(field, (ReferenceField, GenericReferenceField)): raise InvalidQueryError('Cannot perform join in mongoDB: %s' % '__'.join(parts)) - if getattr(field, 'field', None): + if hasattr(getattr(field, 'field', None), 'lookup_member'): new_field = field.field.lookup_member(field_name) else: # Look up subfield on the previous field @@ -1886,10 +1886,10 @@ class QuerySetManager(object): queryset_class = owner._meta['queryset_class'] or QuerySet queryset = queryset_class(owner, owner._get_collection()) if self.get_queryset: - var_names = self.get_queryset.func_code.co_varnames - if len(var_names) == 1: + arg_count = self.get_queryset.func_code.co_argcount + if arg_count == 1: queryset = self.get_queryset(queryset) - elif len(var_names) == 2: + elif arg_count == 2: queryset = self.get_queryset(owner, queryset) else: queryset = partial(self.get_queryset, owner, queryset) diff --git a/tests/test_fields.py b/tests/test_fields.py index 72972cc4..c4013c10 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -6,6 +6,7 @@ import StringIO import tempfile import gridfs +from bson import Binary from decimal import Decimal from mongoengine import * @@ -271,25 +272,54 @@ class FieldTest(unittest.TestCase): person.admin = 'Yes' self.assertRaises(ValidationError, person.validate) - def test_uuid_validation(self): - """Ensure that invalid values cannot be assigned to UUID fields. + def test_uuid_field_string(self): + """Test UUID fields storing as String """ class Person(Document): - api_key = UUIDField() + api_key = UUIDField(binary=False) + + Person.drop_collection() + + uu = uuid.uuid4() + Person(api_key=uu).save() + self.assertEqual(1, Person.objects(api_key=uu).count()) person = Person() - # any uuid type is valid - person.api_key = uuid.uuid4() - person.validate() - person.api_key = uuid.uuid1() - person.validate() + valid = (uuid.uuid4(), uuid.uuid1()) + for api_key in valid: + person.api_key = api_key + person.validate() + + invalid = ('9d159858-549b-4975-9f98-dd2f987c113g', + '9d159858-549b-4975-9f98-dd2f987c113') + for api_key in invalid: + person.api_key = api_key + self.assertRaises(ValidationError, person.validate) + + def test_uuid_field_binary(self): + """Test UUID fields storing as Binary object + """ + class Person(Document): + api_key = UUIDField(binary=True) + + Person.drop_collection() + + uu = uuid.uuid4() + Person(api_key=uu).save() + self.assertEqual(1, Person.objects(api_key=uu).count()) + + person = Person() + valid = (uuid.uuid4(), uuid.uuid1()) + for api_key in valid: + person.api_key = api_key + person.validate() + + invalid = ('9d159858-549b-4975-9f98-dd2f987c113g', + '9d159858-549b-4975-9f98-dd2f987c113') + for api_key in invalid: + person.api_key = api_key + self.assertRaises(ValidationError, person.validate) - # last g cannot belong to an hex number - person.api_key = '9d159858-549b-4975-9f98-dd2f987c113g' - self.assertRaises(ValidationError, person.validate) - # short strings don't validate - person.api_key = '9d159858-549b-4975-9f98-dd2f987c113' - self.assertRaises(ValidationError, person.validate) def test_datetime_validation(self): """Ensure that invalid values cannot be assigned to datetime fields. @@ -928,6 +958,19 @@ class FieldTest(unittest.TestCase): doc = self.db.test.find_one() self.assertEqual(doc['x']['DICTIONARY_KEY']['i'], 2) + def test_map_field_lookup(self): + """Ensure MapField lookups succeed on Fields without a lookup method""" + + class Log(Document): + name = StringField() + visited = MapField(DateTimeField()) + + Log.drop_collection() + Log(name="wilson", visited={'friends': datetime.datetime.now()}).save() + + self.assertEqual(1, Log.objects( + visited__friends__exists=True).count()) + def test_embedded_db_field(self): class Embedded(EmbeddedDocument): @@ -1428,7 +1471,7 @@ class FieldTest(unittest.TestCase): attachment_1 = Attachment.objects().first() self.assertEqual(MIME_TYPE, attachment_1.content_type) - self.assertEqual(BLOB, attachment_1.blob) + self.assertEqual(BLOB, str(attachment_1.blob)) Attachment.drop_collection() @@ -1455,7 +1498,7 @@ class FieldTest(unittest.TestCase): attachment_required = AttachmentRequired() self.assertRaises(ValidationError, attachment_required.validate) - attachment_required.blob = '\xe6\x00\xc4\xff\x07' + attachment_required.blob = Binary('\xe6\x00\xc4\xff\x07') attachment_required.validate() attachment_size_limit = AttachmentSizeLimit(blob='\xe6\x00\xc4\xff\x07') @@ -1467,6 +1510,18 @@ class FieldTest(unittest.TestCase): AttachmentRequired.drop_collection() AttachmentSizeLimit.drop_collection() + def test_binary_field_primary(self): + + class Attachment(Document): + id = BinaryField(primary_key=True) + + Attachment.drop_collection() + + att = Attachment(id=uuid.uuid4().bytes).save() + att.delete() + + self.assertEqual(0, Attachment.objects.count()) + def test_choices_validation(self): """Ensure that value is in a container of allowed values. """ @@ -1567,13 +1622,13 @@ class FieldTest(unittest.TestCase): """Ensure that file fields can be written to and their data retrieved """ class PutFile(Document): - file = FileField() + the_file = FileField() class StreamFile(Document): - file = FileField() + the_file = FileField() class SetFile(Document): - file = FileField() + the_file = FileField() text = 'Hello, World!' more_text = 'Foo Bar' @@ -1584,14 +1639,14 @@ class FieldTest(unittest.TestCase): SetFile.drop_collection() putfile = PutFile() - putfile.file.put(text, content_type=content_type) + putfile.the_file.put(text, content_type=content_type) putfile.save() putfile.validate() result = PutFile.objects.first() self.assertTrue(putfile == result) - self.assertEquals(result.file.read(), text) - self.assertEquals(result.file.content_type, content_type) - result.file.delete() # Remove file from GridFS + self.assertEquals(result.the_file.read(), text) + self.assertEquals(result.the_file.content_type, content_type) + result.the_file.delete() # Remove file from GridFS PutFile.objects.delete() # Ensure file-like objects are stored @@ -1599,53 +1654,53 @@ class FieldTest(unittest.TestCase): putstring = StringIO.StringIO() putstring.write(text) putstring.seek(0) - putfile.file.put(putstring, content_type=content_type) + putfile.the_file.put(putstring, content_type=content_type) putfile.save() putfile.validate() result = PutFile.objects.first() self.assertTrue(putfile == result) - self.assertEquals(result.file.read(), text) - self.assertEquals(result.file.content_type, content_type) - result.file.delete() + self.assertEquals(result.the_file.read(), text) + self.assertEquals(result.the_file.content_type, content_type) + result.the_file.delete() streamfile = StreamFile() - streamfile.file.new_file(content_type=content_type) - streamfile.file.write(text) - streamfile.file.write(more_text) - streamfile.file.close() + streamfile.the_file.new_file(content_type=content_type) + streamfile.the_file.write(text) + streamfile.the_file.write(more_text) + streamfile.the_file.close() streamfile.save() streamfile.validate() result = StreamFile.objects.first() self.assertTrue(streamfile == result) - self.assertEquals(result.file.read(), text + more_text) - self.assertEquals(result.file.content_type, content_type) - result.file.seek(0) - self.assertEquals(result.file.tell(), 0) - self.assertEquals(result.file.read(len(text)), text) - self.assertEquals(result.file.tell(), len(text)) - self.assertEquals(result.file.read(len(more_text)), more_text) - self.assertEquals(result.file.tell(), len(text + more_text)) - result.file.delete() + self.assertEquals(result.the_file.read(), text + more_text) + self.assertEquals(result.the_file.content_type, content_type) + result.the_file.seek(0) + self.assertEquals(result.the_file.tell(), 0) + self.assertEquals(result.the_file.read(len(text)), text) + self.assertEquals(result.the_file.tell(), len(text)) + self.assertEquals(result.the_file.read(len(more_text)), more_text) + self.assertEquals(result.the_file.tell(), len(text + more_text)) + result.the_file.delete() # Ensure deleted file returns None - self.assertTrue(result.file.read() == None) + self.assertTrue(result.the_file.read() == None) setfile = SetFile() - setfile.file = text + setfile.the_file = text setfile.save() setfile.validate() result = SetFile.objects.first() self.assertTrue(setfile == result) - self.assertEquals(result.file.read(), text) + self.assertEquals(result.the_file.read(), text) # Try replacing file with new one - result.file.replace(more_text) + result.the_file.replace(more_text) result.save() result.validate() result = SetFile.objects.first() self.assertTrue(setfile == result) - self.assertEquals(result.file.read(), more_text) - result.file.delete() + self.assertEquals(result.the_file.read(), more_text) + result.the_file.delete() PutFile.drop_collection() StreamFile.drop_collection() @@ -1653,7 +1708,7 @@ class FieldTest(unittest.TestCase): # Make sure FileField is optional and not required class DemoFile(Document): - file = FileField() + the_file = FileField() DemoFile.objects.create() @@ -1704,20 +1759,20 @@ class FieldTest(unittest.TestCase): """ class TestFile(Document): name = StringField() - file = FileField() + the_file = FileField() # First instance - testfile = TestFile() - testfile.name = "Hello, World!" - testfile.file.put('Hello, World!') - testfile.save() + test_file = TestFile() + test_file.name = "Hello, World!" + test_file.the_file.put('Hello, World!') + test_file.save() # Second instance - testfiledupe = TestFile() - data = testfiledupe.file.read() # Should be None + test_file_dupe = TestFile() + data = test_file_dupe.the_file.read() # Should be None - self.assertTrue(testfile.name != testfiledupe.name) - self.assertTrue(testfile.file.read() != data) + self.assertTrue(test_file.name != test_file_dupe.name) + self.assertTrue(test_file.the_file.read() != data) TestFile.drop_collection() @@ -1725,17 +1780,25 @@ class FieldTest(unittest.TestCase): """Ensure that a boolean test of a FileField indicates its presence """ class TestFile(Document): - file = FileField() + the_file = FileField() - testfile = TestFile() - self.assertFalse(bool(testfile.file)) - testfile.file = 'Hello, World!' - testfile.file.content_type = 'text/plain' - testfile.save() - self.assertTrue(bool(testfile.file)) + test_file = TestFile() + self.assertFalse(bool(test_file.the_file)) + test_file.the_file = 'Hello, World!' + test_file.the_file.content_type = 'text/plain' + test_file.save() + self.assertTrue(bool(test_file.the_file)) TestFile.drop_collection() + def test_file_cmp(self): + """Test comparing against other types""" + class TestFile(Document): + the_file = FileField() + + test_file = TestFile() + self.assertFalse(test_file.the_file in [{"test": 1}]) + def test_image_field(self): class TestImage(Document): @@ -1799,30 +1862,30 @@ class FieldTest(unittest.TestCase): def test_file_multidb(self): - register_connection('testfiles', 'testfiles') + register_connection('test_files', 'test_files') class TestFile(Document): name = StringField() - file = FileField(db_alias="testfiles", - collection_name="macumba") + the_file = FileField(db_alias="test_files", + collection_name="macumba") TestFile.drop_collection() # delete old filesystem - get_db("testfiles").macumba.files.drop() - get_db("testfiles").macumba.chunks.drop() + get_db("test_files").macumba.files.drop() + get_db("test_files").macumba.chunks.drop() # First instance - testfile = TestFile() - testfile.name = "Hello, World!" - testfile.file.put('Hello, World!', + test_file = TestFile() + test_file.name = "Hello, World!" + test_file.the_file.put('Hello, World!', name="hello.txt") - testfile.save() + test_file.save() - data = get_db("testfiles").macumba.files.find_one() + data = get_db("test_files").macumba.files.find_one() self.assertEquals(data.get('name'), 'hello.txt') - testfile = TestFile.objects.first() - self.assertEquals(testfile.file.read(), + test_file = TestFile.objects.first() + self.assertEquals(test_file.the_file.read(), 'Hello, World!') def test_geo_indexes(self): diff --git a/tests/test_queryset.py b/tests/test_queryset.py index e623790f..b4ae805b 100644 --- a/tests/test_queryset.py +++ b/tests/test_queryset.py @@ -2287,7 +2287,8 @@ class QuerySetTest(unittest.TestCase): @queryset_manager def objects(cls, qryset): - return qryset(deleted=False) + opts = {"deleted": False} + return qryset(**opts) @queryset_manager def music_posts(doc_cls, queryset, deleted=False):