queries.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. """
  2. Contains the querying interface.
  3. Starting with :class:`~tinydb.queries.Query` you can construct complex
  4. queries:
  5. >>> ((where('f1') == 5) & (where('f2') != 2)) | where('s').matches(r'^\w+$')
  6. (('f1' == 5) and ('f2' != 2)) or ('s' ~= ^\w+$ )
  7. Queries are executed by using the ``__call__``:
  8. >>> q = where('val') == 5
  9. >>> q({'val': 5})
  10. True
  11. >>> q({'val': 1})
  12. False
  13. """
  14. import re
  15. import sys
  16. from .utils import catch_warning, freeze
  17. __all__ = ('Query', 'where')
  18. def is_sequence(obj):
  19. return hasattr(obj, '__iter__')
  20. class QueryImpl(object):
  21. """
  22. A query implementation.
  23. This query implementation wraps a test function which is run when the
  24. query is evaluated by calling the object.
  25. Queries can be combined with logical and/or and modified with logical not.
  26. """
  27. def __init__(self, test, hashval):
  28. self.test = test
  29. self.hashval = hashval
  30. def __call__(self, value):
  31. return self.test(value)
  32. def __hash__(self):
  33. return hash(self.hashval)
  34. def __repr__(self):
  35. return 'QueryImpl{}'.format(self.hashval)
  36. def __eq__(self, other):
  37. return self.hashval == other.hashval
  38. # --- Query modifiers -----------------------------------------------------
  39. def __and__(self, other):
  40. # We use a frozenset for the hash as the AND operation is commutative
  41. # (a & b == b & a)
  42. return QueryImpl(lambda value: self(value) and other(value),
  43. ('and', frozenset([self.hashval, other.hashval])))
  44. def __or__(self, other):
  45. # We use a frozenset for the hash as the OR operation is commutative
  46. # (a | b == b | a)
  47. return QueryImpl(lambda value: self(value) or other(value),
  48. ('or', frozenset([self.hashval, other.hashval])))
  49. def __invert__(self):
  50. return QueryImpl(lambda value: not self(value),
  51. ('not', self.hashval))
  52. class Query(object):
  53. """
  54. TinyDB Queries.
  55. Allows to build queries for TinyDB databases. There are two main ways of
  56. using queries:
  57. 1) ORM-like usage:
  58. >>> User = Query()
  59. >>> db.search(User.name == 'John Doe')
  60. >>> db.search(User['logged-in'] == True)
  61. 2) Classical usage:
  62. >>> db.search(where('value') == True)
  63. Note that ``where(...)`` is a shorthand for ``Query(...)`` allowing for
  64. a more fluent syntax.
  65. Besides the methods documented here you can combine queries using the
  66. binary AND and OR operators:
  67. >>> db.search(where('field1').exists() & where('field2') == 5) # Binary AND
  68. >>> db.search(where('field1').exists() | where('field2') == 5) # Binary OR
  69. Queries are executed by calling the resulting object. They expect to get
  70. the document to test as the first argument and return ``True`` or
  71. ``False`` depending on whether the documents matches the query or not.
  72. """
  73. def __init__(self):
  74. self._path = []
  75. def __getattr__(self, item):
  76. query = Query()
  77. query._path = self._path + [item]
  78. return query
  79. __getitem__ = __getattr__
  80. def _generate_test(self, test, hashval):
  81. """
  82. Generate a query based on a test function.
  83. :param test: The test the query executes.
  84. :param hashval: The hash of the query.
  85. :return: A :class:`~tinydb.queries.QueryImpl` object
  86. """
  87. if not self._path:
  88. raise ValueError('Query has no path')
  89. def impl(value):
  90. try:
  91. # Resolve the path
  92. for part in self._path:
  93. value = value[part]
  94. except (KeyError, TypeError):
  95. return False
  96. else:
  97. return test(value)
  98. return QueryImpl(impl, hashval)
  99. def __eq__(self, rhs):
  100. """
  101. Test a dict value for equality.
  102. >>> Query().f1 == 42
  103. :param rhs: The value to compare against
  104. """
  105. if sys.version_info <= (3, 0): # pragma: no cover
  106. # Special UTF-8 handling on Python 2
  107. def test(value):
  108. with catch_warning(UnicodeWarning):
  109. try:
  110. return value == rhs
  111. except UnicodeWarning:
  112. # Dealing with a case, where 'value' or 'rhs'
  113. # is unicode and the other is a byte string.
  114. if isinstance(value, str):
  115. return value.decode('utf-8') == rhs
  116. elif isinstance(rhs, str):
  117. return value == rhs.decode('utf-8')
  118. else: # pragma: no cover
  119. def test(value):
  120. return value == rhs
  121. return self._generate_test(
  122. lambda value: test(value),
  123. ('==', tuple(self._path), freeze(rhs))
  124. )
  125. def __ne__(self, rhs):
  126. """
  127. Test a dict value for inequality.
  128. >>> Query().f1 != 42
  129. :param rhs: The value to compare against
  130. """
  131. return self._generate_test(
  132. lambda value: value != rhs,
  133. ('!=', tuple(self._path), freeze(rhs))
  134. )
  135. def __lt__(self, rhs):
  136. """
  137. Test a dict value for being lower than another value.
  138. >>> Query().f1 < 42
  139. :param rhs: The value to compare against
  140. """
  141. return self._generate_test(
  142. lambda value: value < rhs,
  143. ('<', tuple(self._path), rhs)
  144. )
  145. def __le__(self, rhs):
  146. """
  147. Test a dict value for being lower than or equal to another value.
  148. >>> where('f1') <= 42
  149. :param rhs: The value to compare against
  150. """
  151. return self._generate_test(
  152. lambda value: value <= rhs,
  153. ('<=', tuple(self._path), rhs)
  154. )
  155. def __gt__(self, rhs):
  156. """
  157. Test a dict value for being greater than another value.
  158. >>> Query().f1 > 42
  159. :param rhs: The value to compare against
  160. """
  161. return self._generate_test(
  162. lambda value: value > rhs,
  163. ('>', tuple(self._path), rhs)
  164. )
  165. def __ge__(self, rhs):
  166. """
  167. Test a dict value for being greater than or equal to another value.
  168. >>> Query().f1 >= 42
  169. :param rhs: The value to compare against
  170. """
  171. return self._generate_test(
  172. lambda value: value >= rhs,
  173. ('>=', tuple(self._path), rhs)
  174. )
  175. def exists(self):
  176. """
  177. Test for a dict where a provided key exists.
  178. >>> Query().f1.exists()
  179. """
  180. return self._generate_test(
  181. lambda _: True,
  182. ('exists', tuple(self._path))
  183. )
  184. def matches(self, regex):
  185. """
  186. Run a regex test against a dict value (whole string has to match).
  187. >>> Query().f1.matches(r'^\w+$')
  188. :param regex: The regular expression to use for matching
  189. """
  190. return self._generate_test(
  191. lambda value: re.match(regex, value),
  192. ('matches', tuple(self._path), regex)
  193. )
  194. def search(self, regex):
  195. """
  196. Run a regex test against a dict value (only substring string has to
  197. match).
  198. >>> Query().f1.search(r'^\w+$')
  199. :param regex: The regular expression to use for matching
  200. """
  201. return self._generate_test(
  202. lambda value: re.search(regex, value),
  203. ('search', tuple(self._path), regex)
  204. )
  205. def test(self, func, *args):
  206. """
  207. Run a user-defined test function against a dict value.
  208. >>> def test_func(val):
  209. ... return val == 42
  210. ...
  211. >>> Query().f1.test(test_func)
  212. :param func: The function to call, passing the dict as the first
  213. argument
  214. :param args: Additional arguments to pass to the test function
  215. """
  216. return self._generate_test(
  217. lambda value: func(value, *args),
  218. ('test', tuple(self._path), func, args)
  219. )
  220. def any(self, cond):
  221. """
  222. Check if a condition is met by any document in a list,
  223. where a condition can also be a sequence (e.g. list).
  224. >>> Query().f1.any(Query().f2 == 1)
  225. Matches::
  226. {'f1': [{'f2': 1}, {'f2': 0}]}
  227. >>> Query().f1.any([1, 2, 3])
  228. Matches::
  229. {'f1': [1, 2]}
  230. {'f1': [3, 4, 5]}
  231. :param cond: Either a query that at least one document has to match or
  232. a list of which at least one document has to be contained
  233. in the tested document.
  234. """
  235. if callable(cond):
  236. def _cmp(value):
  237. return is_sequence(value) and any(cond(e) for e in value)
  238. else:
  239. def _cmp(value):
  240. return is_sequence(value) and any(e in cond for e in value)
  241. return self._generate_test(
  242. lambda value: _cmp(value),
  243. ('any', tuple(self._path), freeze(cond))
  244. )
  245. def all(self, cond):
  246. """
  247. Check if a condition is met by all documents in a list,
  248. where a condition can also be a sequence (e.g. list).
  249. >>> Query().f1.all(Query().f2 == 1)
  250. Matches::
  251. {'f1': [{'f2': 1}, {'f2': 1}]}
  252. >>> Query().f1.all([1, 2, 3])
  253. Matches::
  254. {'f1': [1, 2, 3, 4, 5]}
  255. :param cond: Either a query that all documents have to match or a list
  256. which has to be contained in the tested document.
  257. """
  258. if callable(cond):
  259. def _cmp(value):
  260. return is_sequence(value) and all(cond(e) for e in value)
  261. else:
  262. def _cmp(value):
  263. return is_sequence(value) and all(e in value for e in cond)
  264. return self._generate_test(
  265. lambda value: _cmp(value),
  266. ('all', tuple(self._path), freeze(cond))
  267. )
  268. def one_of(self, items):
  269. """
  270. Check if the value is contained in a list or generator.
  271. >>> Query().f1.one_of(['value 1', 'value 2'])
  272. :param items: The list of items to check with
  273. """
  274. return self._generate_test(
  275. lambda value: value in items,
  276. ('one_of', tuple(self._path), freeze(items))
  277. )
  278. def where(key):
  279. return Query()[key]