Skip to content

Commit ee7b78d

Browse files
committed
Extend Result's API
* Introduce `Result.fetch(n)` * Revert `Result.single()` to be lenient again when not exactly one record is left in the stream. Partially reverts #646 * Add `strict` parameter to `Result.single()` to enable strict checking of the number of records in the stream.
1 parent 2af7588 commit ee7b78d

File tree

10 files changed

+372
-77
lines changed

10 files changed

+372
-77
lines changed

CHANGELOG.md

-2
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,6 @@
5454
- Creation of a driver with `bolt[+s[sc]]://` scheme has been deprecated and
5555
will raise an error in the Future. The routing context was and will be
5656
silently ignored until then.
57-
- `Result.single` now raises `ResultNotSingleError` if not exactly one result is
58-
available.
5957
- Bookmarks
6058
- `Session.last_bookmark` was deprecated. Its behaviour is partially incorrect
6159
and cannot be fixed without breaking its signature.

docs/source/api.rst

+5
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,8 @@ A :class:`neo4j.Result` is attached to an active connection, through a :class:`n
798798

799799
.. automethod:: single
800800

801+
.. automethod:: fetch
802+
801803
.. automethod:: peek
802804

803805
.. automethod:: graph
@@ -1368,6 +1370,9 @@ Connectivity Errors
13681370
.. autoclass:: neo4j.exceptions.ResultConsumedError
13691371
:show-inheritance:
13701372
1373+
.. autoclass:: neo4j.exceptions.ResultNotSingleError
1374+
:show-inheritance:
1375+
13711376
13721377
13731378
Internal Driver Errors

docs/source/async_api.rst

+2
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,8 @@ A :class:`neo4j.AsyncResult` is attached to an active connection, through a :cla
505505

506506
.. automethod:: single
507507

508+
.. automethod:: fetch
509+
508510
.. automethod:: peek
509511

510512
.. automethod:: graph

neo4j/_async/work/result.py

+97-22
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818

1919
from collections import deque
20+
from warnings import warn
2021

2122
from ..._async_compat.util import AsyncUtil
2223
from ...data import DataDehydrator
@@ -248,11 +249,11 @@ async def _buffer(self, n=None):
248249
record_buffer.append(record)
249250
if n is not None and len(record_buffer) >= n:
250251
break
251-
self._exhausted = False
252252
if n is None:
253253
self._record_buffer = record_buffer
254254
else:
255255
self._record_buffer.extend(record_buffer)
256+
self._exhausted = not self._record_buffer
256257

257258
async def _buffer_all(self):
258259
"""Sets the Result object in an detached state by fetching all records
@@ -286,12 +287,20 @@ def keys(self):
286287
"""
287288
return self._keys
288289

290+
async def _exhaust(self):
291+
# Exhaust the result, ditching all remaining records.
292+
if not self._exhausted:
293+
self._discarding = True
294+
self._record_buffer.clear()
295+
async for _ in self:
296+
pass
297+
289298
async def _tx_end(self):
290299
# Handle closure of the associated transaction.
291300
#
292301
# This will consume the result and mark it at out of scope.
293302
# Subsequent calls to `next` will raise a ResultConsumedError.
294-
await self.consume()
303+
await self._exhaust()
295304
self._out_of_scope = True
296305

297306
async def consume(self):
@@ -329,43 +338,93 @@ async def get_two_tx(tx):
329338
values, info = session.read_transaction(get_two_tx)
330339
331340
:returns: The :class:`neo4j.ResultSummary` for this result
341+
342+
:raises ResultConsumedError: if the transaction from which this result
343+
was obtained has been closed.
344+
345+
.. versionchanged:: 5.0
346+
Can raise :exc:`ResultConsumedError`.
332347
"""
333-
if self._exhausted is False:
334-
self._discarding = True
335-
async for _ in self:
336-
pass
348+
if self._out_of_scope:
349+
raise ResultConsumedError(self, _RESULT_OUT_OF_SCOPE_ERROR)
350+
if self._consumed:
351+
return self._obtain_summary()
337352

353+
await self._exhaust()
338354
summary = self._obtain_summary()
339355
self._consumed = True
340356
return summary
341357

342-
async def single(self):
343-
"""Obtain the next and only remaining record from this result if available else return None.
358+
async def single(self, strict=False):
359+
"""Obtain the next and only remaining record or None.
360+
344361
Calling this method always exhausts the result.
345362
346363
A warning is generated if more than one record is available but
347364
the first of these is still returned.
348365
349-
:returns: the next :class:`neo4j.AsyncRecord`.
366+
:param strict:
367+
If :const:`True`, raise a :class:`neo4j.ResultNotSingleError`
368+
instead of returning None if there is more than one record or
369+
warning if there are more than 1 record.
370+
:const:`False` by default.
371+
:type strict: bool
372+
373+
:returns: the next :class:`neo4j.Record` or :const:`None` if none remain
374+
:warns: if more than one record is available
375+
376+
:raises ResultNotSingleError:
377+
If ``strict=True`` and not exactly one record is available.
378+
:raises ResultConsumedError: if the transaction from which this result
379+
was obtained has been closed or the Result has been explicitly
380+
consumed.
350381
351-
:raises ResultNotSingleError: if not exactly one record is available.
352-
:raises ResultConsumedError: if the transaction from which this result was
353-
obtained has been closed.
382+
.. versionchanged:: 5.0
383+
Added ``strict`` parameter.
384+
.. versionchanged:: 5.0
385+
Can raise :exc:`ResultConsumedError`.
354386
"""
355387
await self._buffer(2)
356-
if not self._record_buffer:
388+
buffer = self._record_buffer
389+
self._record_buffer = deque()
390+
await self._exhaust()
391+
if not buffer:
392+
if not strict:
393+
return None
357394
raise ResultNotSingleError(
358395
self,
359396
"No records found. "
360397
"Make sure your query returns exactly one record."
361398
)
362-
elif len(self._record_buffer) > 1:
363-
raise ResultNotSingleError(
364-
self,
365-
"More than one record found. "
366-
"Make sure your query returns exactly one record."
367-
)
368-
return self._record_buffer.popleft()
399+
elif len(buffer) > 1:
400+
res = buffer.popleft()
401+
if not strict:
402+
warn("Expected a result with a single record, "
403+
"but found multiple.")
404+
return res
405+
else:
406+
raise ResultNotSingleError(
407+
self,
408+
"More than one record found. "
409+
"Make sure your query returns exactly one record."
410+
)
411+
return buffer.popleft()
412+
413+
async def fetch(self, n):
414+
"""Obtain up to n records from this result.
415+
416+
:param n: the maximum number of records to fetch.
417+
:type n: int
418+
419+
:returns: list of :class:`neo4j.AsyncRecord`
420+
421+
.. versionadded:: 5.0
422+
"""
423+
await self._buffer(n)
424+
return [
425+
self._record_buffer.popleft()
426+
for _ in range(min(n, len(self._record_buffer)))
427+
]
369428

370429
async def peek(self):
371430
"""Obtain the next record from this result without consuming it.
@@ -376,6 +435,9 @@ async def peek(self):
376435
:raises ResultConsumedError: if the transaction from which this result
377436
was obtained has been closed or the Result has been explicitly
378437
consumed.
438+
439+
.. versionchanged:: 5.0
440+
Can raise :exc:`ResultConsumedError`.
379441
"""
380442
await self._buffer(1)
381443
if self._record_buffer:
@@ -392,6 +454,9 @@ async def graph(self):
392454
:raises ResultConsumedError: if the transaction from which this result
393455
was obtained has been closed or the Result has been explicitly
394456
consumed.
457+
458+
.. versionchanged:: 5.0
459+
Can raise :exc:`ResultConsumedError`.
395460
"""
396461
await self._buffer_all()
397462
return self._hydrant.graph
@@ -410,6 +475,9 @@ async def value(self, key=0, default=None):
410475
:raises ResultConsumedError: if the transaction from which this result
411476
was obtained has been closed or the Result has been explicitly
412477
consumed.
478+
479+
.. versionchanged:: 5.0
480+
Can raise :exc:`ResultConsumedError`.
413481
"""
414482
return [record.value(key, default) async for record in self]
415483

@@ -426,6 +494,9 @@ async def values(self, *keys):
426494
:raises ResultConsumedError: if the transaction from which this result
427495
was obtained has been closed or the Result has been explicitly
428496
consumed.
497+
498+
.. versionchanged:: 5.0
499+
Can raise :exc:`ResultConsumedError`.
429500
"""
430501
return [record.values(*keys) async for record in self]
431502

@@ -439,8 +510,12 @@ async def data(self, *keys):
439510
:returns: list of dictionaries
440511
:rtype: list
441512
442-
:raises ResultConsumedError: if the transaction from which this result was
443-
obtained has been closed.
513+
:raises ResultConsumedError: if the transaction from which this result
514+
was obtained has been closed or the Result has been explicitly
515+
consumed.
516+
517+
.. versionchanged:: 5.0
518+
Can raise :exc:`ResultConsumedError`.
444519
"""
445520
return [record.data(*keys) async for record in self]
446521

0 commit comments

Comments
 (0)