Skip to content

Commit 5c70481

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 5c70481

File tree

10 files changed

+322
-51
lines changed

10 files changed

+322
-51
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

+72-20
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):
@@ -330,42 +339,85 @@ async def get_two_tx(tx):
330339
331340
:returns: The :class:`neo4j.ResultSummary` for this result
332341
"""
333-
if self._exhausted is False:
334-
self._discarding = True
335-
async for _ in self:
336-
pass
342+
if self._exhausted:
343+
if self._out_of_scope:
344+
raise ResultConsumedError(self, _RESULT_OUT_OF_SCOPE_ERROR)
345+
if self._consumed:
346+
raise ResultConsumedError(self, _RESULT_CONSUMED_ERROR)
347+
else:
348+
await self._exhaust()
337349

338350
summary = self._obtain_summary()
339351
self._consumed = True
340352
return summary
341353

342-
async def single(self):
343-
"""Obtain the next and only remaining record from this result if available else return None.
354+
async def single(self, strict=False):
355+
"""Obtain the next and only remaining record or None.
356+
344357
Calling this method always exhausts the result.
345358
346359
A warning is generated if more than one record is available but
347360
the first of these is still returned.
348361
349-
:returns: the next :class:`neo4j.AsyncRecord`.
362+
:param strict:
363+
If :const:`True`, raise a :class:`neo4j.ResultNotSingleError`
364+
instead of returning None if there is more than one record or
365+
warning if there are more than 1 record.
366+
:const:`False` by default.
367+
:type strict: bool
350368
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.
369+
:returns: the next :class:`neo4j.Record` or :const:`None` if none remain
370+
:warns: if more than one record is available
371+
372+
:raises ResultNotSingleError:
373+
If ``strict=True`` and not exactly one record is available.
374+
:raises ResultConsumedError: if the transaction from which this result
375+
was obtained has been closed.
376+
377+
.. versionchanged:: 5.0
378+
Added ``strict`` parameter.
354379
"""
355380
await self._buffer(2)
356-
if not self._record_buffer:
381+
buffer = self._record_buffer
382+
self._record_buffer = deque()
383+
await self._exhaust()
384+
if not buffer:
385+
if not strict:
386+
return None
357387
raise ResultNotSingleError(
358388
self,
359389
"No records found. "
360390
"Make sure your query returns exactly one record."
361391
)
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()
392+
elif len(buffer) > 1:
393+
res = buffer.popleft()
394+
if not strict:
395+
warn("Expected a result with a single record, "
396+
"but found multiple.")
397+
return res
398+
else:
399+
raise ResultNotSingleError(
400+
self,
401+
"More than one record found. "
402+
"Make sure your query returns exactly one record."
403+
)
404+
return buffer.popleft()
405+
406+
async def fetch(self, n):
407+
"""Obtain up to n records from this result.
408+
409+
:param n: the maximum number of records to fetch.
410+
:type n: int
411+
412+
:returns: list of :class:`neo4j.AsyncRecord`
413+
414+
.. versionadded:: 5.0
415+
"""
416+
await self._buffer(n)
417+
return [
418+
self._record_buffer.popleft()
419+
for _ in range(min(n, len(self._record_buffer)))
420+
]
369421

370422
async def peek(self):
371423
"""Obtain the next record from this result without consuming it.

neo4j/_sync/work/result.py

+72-20
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 Util
2223
from ...data import DataDehydrator
@@ -248,11 +249,11 @@ 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
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+
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+
for _ in self:
296+
pass
297+
289298
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-
self.consume()
303+
self._exhaust()
295304
self._out_of_scope = True
296305

297306
def consume(self):
@@ -330,42 +339,85 @@ def get_two_tx(tx):
330339
331340
:returns: The :class:`neo4j.ResultSummary` for this result
332341
"""
333-
if self._exhausted is False:
334-
self._discarding = True
335-
for _ in self:
336-
pass
342+
if self._exhausted:
343+
if self._out_of_scope:
344+
raise ResultConsumedError(self, _RESULT_OUT_OF_SCOPE_ERROR)
345+
if self._consumed:
346+
raise ResultConsumedError(self, _RESULT_CONSUMED_ERROR)
347+
else:
348+
self._exhaust()
337349

338350
summary = self._obtain_summary()
339351
self._consumed = True
340352
return summary
341353

342-
def single(self):
343-
"""Obtain the next and only remaining record from this result if available else return None.
354+
def single(self, strict=False):
355+
"""Obtain the next and only remaining record or None.
356+
344357
Calling this method always exhausts the result.
345358
346359
A warning is generated if more than one record is available but
347360
the first of these is still returned.
348361
349-
:returns: the next :class:`neo4j.Record`.
362+
:param strict:
363+
If :const:`True`, raise a :class:`neo4j.ResultNotSingleError`
364+
instead of returning None if there is more than one record or
365+
warning if there are more than 1 record.
366+
:const:`False` by default.
367+
:type strict: bool
350368
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.
369+
:returns: the next :class:`neo4j.Record` or :const:`None` if none remain
370+
:warns: if more than one record is available
371+
372+
:raises ResultNotSingleError:
373+
If ``strict=True`` and not exactly one record is available.
374+
:raises ResultConsumedError: if the transaction from which this result
375+
was obtained has been closed.
376+
377+
.. versionchanged:: 5.0
378+
Added ``strict`` parameter.
354379
"""
355380
self._buffer(2)
356-
if not self._record_buffer:
381+
buffer = self._record_buffer
382+
self._record_buffer = deque()
383+
self._exhaust()
384+
if not buffer:
385+
if not strict:
386+
return None
357387
raise ResultNotSingleError(
358388
self,
359389
"No records found. "
360390
"Make sure your query returns exactly one record."
361391
)
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()
392+
elif len(buffer) > 1:
393+
res = buffer.popleft()
394+
if not strict:
395+
warn("Expected a result with a single record, "
396+
"but found multiple.")
397+
return res
398+
else:
399+
raise ResultNotSingleError(
400+
self,
401+
"More than one record found. "
402+
"Make sure your query returns exactly one record."
403+
)
404+
return buffer.popleft()
405+
406+
def fetch(self, n):
407+
"""Obtain up to n records from this result.
408+
409+
:param n: the maximum number of records to fetch.
410+
:type n: int
411+
412+
:returns: list of :class:`neo4j.Record`
413+
414+
.. versionadded:: 5.0
415+
"""
416+
self._buffer(n)
417+
return [
418+
self._record_buffer.popleft()
419+
for _ in range(min(n, len(self._record_buffer)))
420+
]
369421

370422
def peek(self):
371423
"""Obtain the next record from this result without consuming it.

neo4j/exceptions.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ class ResultConsumedError(ResultError):
308308

309309

310310
class ResultNotSingleError(ResultError):
311-
"""Raised when result.single() detects not exactly one record in result."""
311+
"""Raised when a result should have exactly one record but does not."""
312312

313313

314314
class ServiceUnavailable(DriverError):

testkitbackend/_async/requests.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ async def ResultNext(backend, data):
407407
async def ResultSingle(backend, data):
408408
result = backend.results[data["resultId"]]
409409
await backend.send_response("Record", totestkit.record(
410-
await result.single()
410+
await result.single(strict=True)
411411
))
412412

413413

testkitbackend/_sync/requests.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ def ResultNext(backend, data):
407407
def ResultSingle(backend, data):
408408
result = backend.results[data["resultId"]]
409409
backend.send_response("Record", totestkit.record(
410-
result.single()
410+
result.single(strict=True)
411411
))
412412

413413

0 commit comments

Comments
 (0)