Commits

Andy Mikhailenko  committed 278980f

Added ability to use fetch dicts instead of Document objects by db.find(dict). Fixed some query-related bugs. Improved tests (now they all pass but some are marked as "expected failure").

  • Participants
  • Parent commits f0884a7

Comments (0)

Files changed (10)

File doqu/backend_base.py

         """
         Populates a model instance with given data and initializes its state
         object with current storage and given key.
+
+        .. note::
+
+            It is OK to pass a plain dictionary as a model. However, it is
+            recommended to use a more advanced class (such as
+            :class:`~doqu.document_base.Document`) to correctly handle schemata
+            and save the document back.
+
         """
+        # TODO: use a more open, explicit and flexible DocumentMeta API so that
+        # users can easily create from scratch (or at least compose from
+        # available components) their own document classes
+
+        if not hasattr(model, 'meta'):
+            return model(**data)
 
         if model.meta.structure:
             pythonized_data = {}
         Returns a generator for backend-specific conditions based on a
         dictionary of backend-agnostic ones.
         """
+        # TODO: enable other query APIs (Mongo-like, TC-like, etc.)
         for lookup, value in conditions.iteritems():
             if '__' in lookup:
                 name, operation = lookup.split('__')    # XXX check if there are 2 parts

File doqu/document_base.py

 import types
 import re
 
-#from backend import BaseStorage
 import validators
 from utils import camel_case_to_underscores
 from utils.data_structures import DotDict, ProxyDict, ReprMixin
             break
         except validators.ValidationError:
             # XXX should preserve call stack and add sensible message
-            msg = 'Value {value} is invalid for {cls}.{field} ({test})'
+            msg = 'Value {value!r} is invalid for {cls}.{field} ({test})'
             raise validators.ValidationError(msg.format(
-                value=repr(value), cls=type(doc).__name__,
+                value=value, cls=type(doc).__name__,
                 field=key, test=test))
 
 def _validate_value_type(cls, key, value):

File doqu/ext/mongodb/lookups.py

     'day':          lambda v: (None, re.compile(r'^......{0:02}'.format(v))),
 }
 meta_lookups = {
-    'between': lambda values: [('gt', values[0]),
-                               ('lt', values[1])],
+    'between': lambda values: [('gte', values[0]),
+                               ('lte', values[1])],
 }
 inline_negation = {
     'equals': '$ne',

File doqu/ext/tokyo_cabinet/__init__.py

 dist.check_dependencies(__name__)
 
 from decimal import Decimal    # for order_by introspection
+import logging
 
 import tokyo.cabinet as tc
 
 __all__ = ['StorageAdapter']
 
 
+log = logging.getLogger(__name__)
+
+
 class QueryAdapter(CachedIterator, BaseQueryAdapter):
     """
     The Query class.
         if self._iter is None:
             self._query = self.storage.connection.query()
             for condition in self._conditions:
-#                print 'condition:', condition
                 col, op, expr = condition  #.prepare()
                 if not isinstance(expr, basestring):
                     expr = str(expr)
         The conditions are defined exactly as in Pyrant's high-level query API.
         See pyrant.query.Query.filter documentation for details.
         """
+        log.debug('lookups: %s', lookups)
+
         conditions = list(self._get_native_conditions(lookups, negate))
 
+        log.debug('condition types: %s', [type(x) for x in conditions])
+
         # XXX hm, the manager returns a plain list without grouping; I guess
         # it's broken and needs a redesign
         conditions = [conditions] if conditions else None
 
+        log.debug('conditions: %s (after wrapping)', conditions)
+
         #for x in native_conditions:
         #    q = q.filter(**x)
         #conditions = [Condition(k, v, negate) for k, v in lookups]

File doqu/ext/tokyo_cabinet/lookups.py

         return k, op, p(v)
     return parse
 
+def lowercased(handler):
+    """decorator for lookup handlers; turns the resulting value to lowercase
+    (required in full-text search lookups)
+    """
+    def decorator(k, v, p):
+        k, o, v = handler(k, v, p)
+        return k, o, unicode(v).lower()
+    return decorator
+
 mapping = {
-    'between':      lambda k,v,p: (k, tc.TDBQCNUMBT, [int(p(x)) for x in v]),
+    'between':      lambda k,v,p: (k, tc.TDBQCNUMBT, ', '.join(p(x) for x in v)),
     'contains':     str_or_list('contains'),
     'contains_any': lambda k,v,p: (k, tc.TDBQCSTROR, p(v)),
     'endswith':     lambda k,v,p: (k, tc.TDBQCSTREW, p(v)),
     'gt':           lambda k,v,p: (k, tc.TDBQCNUMGT, p(v)),
     'gte':          lambda k,v,p: (k, tc.TDBQCNUMGE, p(v)),
     'in':           str_or_num('in'),
-    'like':         str_or_list('like'),
-    'like_any':     lambda k,v,p: (k, tc.TDBQCFTSOR, p(v)),
+    'like':         lowercased(str_or_list('like')),
+    'like_any':     lambda k,v,p: (k, tc.TDBQCFTSOR, p(v).lower()),
     'lt':           lambda k,v,p: (k, tc.TDBQCNUMLT, p(v)),
     'lte':          lambda k,v,p: (k, tc.TDBQCNUMLE, p(v)),
     'matches':      lambda k,v,p: (k, tc.TDBQCSTRRX, p(v)),

File tests/base_query.py

     'day':          lambda k,v,p: (k, tc.TDBQCSTRRX, '^......%0.2d'%v),
     """
 
+    # Note: docstrings are not included below because they would hide the
+    # module name in test results (which is crucial to determine the exact
+    # source of the error because the tests themselves are located here for all
+    # backends)
+
     def test_op_between(self):
+        # Operator `between` is inclusive.
         self.assert_finds('John', age__between=(26, 55))
         self.assert_finds('John', 'Mary', age__between=(25, 55))
 
     def test_op_contains(self):
+        # Operator `contains` accepts literals and lists.
         self.assert_finds('John', name__contains='J')
         self.assert_finds('John', name__contains=['J', 'o'])
         self.assert_finds_nobody(name__contains=['J', 'M'])  # nobody

File tests/test_ext_shelve.py

 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 "Shelve (BDB) backend tests."
+import unittest2 as unittest
+
 from doqu.ext.shelve_db import StorageAdapter
 import base_query
 
 class ShelveQueryTestCase(base_query.BaseQueryTestCase):
     def get_connection(self):
         return StorageAdapter(path=self._tmp_filename)
+
+    @unittest.expectedFailure
+    def test_op_exists(self):
+        super(ShelveQueryTestCase, self).test_op_exists()

File tests/test_ext_shove.py

 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 "Shove backend tests."
+import unittest2 as unittest
+
 from doqu.ext.shove_db import StorageAdapter
 import base_query
 
 class ShoveQueryTestCase(base_query.BaseQueryTestCase):
     def get_connection(self):
         return StorageAdapter()
+
+    @unittest.expectedFailure
+    def test_op_exists(self):
+        super(ShoveQueryTestCase, self).test_op_exists()

File tests/test_ext_tokyo_cabinet.py

 # -*- coding: utf-8 -*-
 "Tokyo Cabinet backend tests."
 import os
+import unittest2 as unittest
+
 from doqu.ext.tokyo_cabinet import StorageAdapter
 import base_query
 
 
 class TokyoCabinetQueryTestCase(base_query.BaseQueryTestCase):
+    TMP_FILENAME_EXTENSION = 'tct'
+
     def get_connection(self):
         return StorageAdapter(path=self._tmp_filename)
 
+    @unittest.expectedFailure
+    def test_op_contains(self):
+        super(TokyoCabinetQueryTestCase, self).test_op_contains()
+
+    @unittest.expectedFailure
+    def test_op_contains_any(self):
+        super(TokyoCabinetQueryTestCase, self).test_op_contains_any()
+
+    @unittest.expectedFailure
+    def test_op_exists(self):
+        super(TokyoCabinetQueryTestCase, self).test_op_exists()
+
     def test_op_like(self):
-        raise NotImplementedError
+        self.assert_finds('John', name__like='Jo')
+        self.assert_finds('John', name__like='jo')
+        self.assert_finds('John', name__like=('J', 'o'))
 
     def test_op_like_any(self):
-        raise NotImplementedError
+        self.assert_finds('John', name__like_any=['J'])
+        self.assert_finds('John', 'Mary', name__like_any=('J', 'M'))
+        self.assert_finds('John', 'Mary', name__like_any=('j', 'm'))
 
     def test_op_search(self):
-        raise NotImplementedError
+        self.assert_finds('John', name__search='J')
+        self.assert_finds('John', name__search='J && o')
+        self.assert_finds('John', 'Mary', name__search='J || M')

File tests/test_ext_tokyo_tyrant.py

 "TokyoTyrant backend tests."
 import os
 import time
+import unittest2 as unittest
 
 from doqu.ext.tokyo_tyrant import StorageAdapter
 import test_ext_tokyo_cabinet as tc
 
 
 class TokyoTyrantQueryTestCase(tc.TokyoCabinetQueryTestCase):
-    TMP_FILENAME_EXTENSION = 'tct'
     # A sandbox Tyrant instance parametres:
     TYRANT_HOST = '127.0.0.1'
     TYRANT_PORT = 1983    # default is 1978 so we avoid clashes