Skip to content

Commit 8d6e7c0

Browse files
committed
PYCBC-1725: Defer decoding to *Result object
Motivation ========== In order to more closely align with RFCs, it is preferred to defer decoding results (for applicable operations) to happen within the specific result object (e.g. GetResult) when the application accesses the content from the result object. Modification ============ * Move transcoding logic from decorators to base Result object * Update remaining to *Result objects (where applicable) to handle transcoder accordingly * Update tests to confirm functionality Change-Id: Idb1322a69a354bad9bc9455e77f00b35b4452bca Reviewed-on: https://review.couchbase.org/c/couchbase-python-client/+/236000 Tested-by: Build Bot <build@couchbase.com> Reviewed-by: Dimitris Christodoulou <dimitris.christodoulou@couchbase.com>
1 parent c770506 commit 8d6e7c0

9 files changed

Lines changed: 70 additions & 106 deletions

File tree

acouchbase/logic/wrappers.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
ExceptionMap,
2626
MissingConnectionException,
2727
ServiceUnavailableException)
28-
from couchbase.logic import decode_replicas, decode_value
28+
from couchbase.logic import decode_replicas
2929

3030

3131
def call_async_fn(ft, self, fn, *args, **kwargs):
@@ -286,17 +286,12 @@ def on_ok(res):
286286
)
287287
return
288288

289-
value = res.raw_result.get('value', None)
290-
flags = res.raw_result.get('flags', None)
291-
292-
res.raw_result['value'] = decode_value(transcoder, value, flags, is_subdoc=is_subdoc)
293-
294289
if return_cls is None:
295290
retval = None
296291
elif return_cls is True:
297292
retval = res
298293
else:
299-
retval = return_cls(res)
294+
retval = return_cls(res, transcoder=transcoder, is_subdoc=is_subdoc)
300295
self.loop.call_soon_threadsafe(ft.set_result, retval)
301296
except CouchbaseException as e:
302297
self.loop.call_soon_threadsafe(ft.set_exception, e)

acouchbase/tests/collection_t.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
DocumentNotLockedException,
3131
DocumentUnretrievableException,
3232
InvalidArgumentException,
33-
PathNotFoundException,
3433
TemporaryFailException)
3534
from couchbase.options import (GetAllReplicasOptions,
3635
GetAnyReplicaOptions,
@@ -245,10 +244,13 @@ async def cas_matches(cb, new_cas):
245244

246245
@pytest.mark.asyncio
247246
async def test_project_bad_path(self, cb_env, default_kvp):
248-
cb = cb_env.collection
249247
key = default_kvp.key
250-
with pytest.raises(PathNotFoundException):
251-
await cb.get(key, GetOptions(project=["some", "qzx"]))
248+
# CXXCBC-295 - b8bb98c31d377100934dd4b33998f0a118df41e8, bad path no longer raises PathNotFoundException
249+
result = await cb_env.collection.get(key, GetOptions(project=['qzx']))
250+
assert result.cas is not None
251+
res_dict = result.content_as[dict]
252+
assert res_dict == {}
253+
assert 'qzx' not in res_dict
252254

253255
@pytest.mark.asyncio
254256
async def test_project_project_not_list(self, cb_env, default_kvp):

couchbase/collection.py

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@
3737
QueueEmpty)
3838
from couchbase.exceptions import exception as CouchbaseBaseException
3939
from couchbase.kv_range_scan import RangeScanRequest
40-
from couchbase.logic import (BlockingWrapper,
41-
decode_replicas,
42-
decode_value)
40+
from couchbase.logic import BlockingWrapper, decode_replicas
4341
from couchbase.logic.collection import CollectionLogic
4442
from couchbase.logic.supportability import Supportability
4543
from couchbase.management.queries import CollectionQueryIndexManager
@@ -2010,17 +2008,8 @@ def get_multi(self,
20102008
op_type=op_type,
20112009
op_args=op_args
20122010
)
2013-
for k, v in res.raw_result.items():
2014-
if k == 'all_okay':
2015-
continue
2016-
if isinstance(v, CouchbaseBaseException):
2017-
continue
2018-
value = v.raw_result.get('value', None)
2019-
flags = v.raw_result.get('flags', None)
2020-
tc = transcoders[k]
2021-
v.raw_result['value'] = decode_value(tc, value, flags)
20222011

2023-
return MultiGetResult(res, return_exceptions)
2012+
return MultiGetResult(res, return_exceptions, transcoders)
20242013

20252014
def get_any_replica_multi(self,
20262015
keys, # type: List[str]
@@ -2104,17 +2093,8 @@ def get_any_replica_multi(self,
21042093
op_type=op_type,
21052094
op_args=op_args
21062095
)
2107-
for k, v in res.raw_result.items():
2108-
if k == 'all_okay':
2109-
continue
2110-
if isinstance(v, CouchbaseBaseException):
2111-
continue
2112-
value = v.raw_result.get('value', None)
2113-
flags = v.raw_result.get('flags', None)
2114-
tc = transcoders[k]
2115-
v.raw_result['value'] = decode_value(tc, value, flags)
21162096

2117-
return MultiGetReplicaResult(res, return_exceptions)
2097+
return MultiGetReplicaResult(res, return_exceptions, transcoders)
21182098

21192099
def get_all_replicas_multi(self,
21202100
keys, # type: List[str]
@@ -2309,17 +2289,8 @@ def get_and_lock_multi(self,
23092289
op_type=op_type,
23102290
op_args=op_args
23112291
)
2312-
for k, v in res.raw_result.items():
2313-
if k == 'all_okay':
2314-
continue
2315-
if isinstance(v, CouchbaseBaseException):
2316-
continue
2317-
value = v.raw_result.get('value', None)
2318-
flags = v.raw_result.get('flags', None)
2319-
tc = transcoders[k]
2320-
v.raw_result['value'] = decode_value(tc, value, flags)
23212292

2322-
return MultiGetResult(res, return_exceptions)
2293+
return MultiGetResult(res, return_exceptions, transcoders)
23232294

23242295
def exists_multi(self,
23252296
keys, # type: List[str]

couchbase/logic/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,3 @@
1515

1616
from .wrappers import BlockingWrapper # noqa: F401
1717
from .wrappers import decode_replicas # noqa: F401
18-
from .wrappers import decode_value # noqa: F401

couchbase/logic/kv_range_scan.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
from couchbase.exceptions import ErrorMapper, InvalidArgumentException
2323
from couchbase.exceptions import exception as CouchbaseBaseException
24-
from couchbase.logic.wrappers import decode_value
2524
from couchbase.pycbc_core import kv_range_scan_operation
2625
from couchbase.result import ScanResult
2726

@@ -159,9 +158,4 @@ def _get_next_row(self):
159158
if isinstance(resp, CouchbaseBaseException):
160159
raise ErrorMapper.build_exception(resp)
161160

162-
value = resp.raw_result.get('value', None)
163-
flags = resp.raw_result.get('flags', None)
164-
if value:
165-
resp.raw_result['value'] = decode_value(self.transcoder, value, flags)
166-
167-
return ScanResult(resp, self._ids_only)
161+
return ScanResult(resp, self._ids_only, self.transcoder)

couchbase/logic/wrappers.py

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@
1515

1616
from __future__ import annotations
1717

18-
from copy import copy
1918
from functools import wraps
2019

21-
from couchbase.constants import FMT_JSON
2220
from couchbase.exceptions import (PYCBC_ERROR_MAP,
2321
CouchbaseException,
2422
DocumentExistsException,
@@ -33,25 +31,6 @@
3331
from couchbase.exceptions import exception as CouchbaseBaseException
3432

3533

36-
def decode_value(transcoder, value, flags, is_subdoc=False):
37-
if is_subdoc is False:
38-
return transcoder.decode_value(value, flags)
39-
40-
final_value = []
41-
for f in value:
42-
if 'value' in f:
43-
tmp = copy(f)
44-
old = tmp.pop('value', None)
45-
if old:
46-
# no custom transcoder for subdoc ops, use JSON
47-
tmp['value'] = transcoder.decode_value(old, FMT_JSON)
48-
final_value.append(tmp)
49-
else:
50-
final_value.append(f)
51-
52-
return final_value
53-
54-
5534
def decode_replicas(transcoder, result, return_cls, is_subdoc=False):
5635
while True:
5736
try:
@@ -66,10 +45,7 @@ def decode_replicas(transcoder, result, return_cls, is_subdoc=False):
6645
if res is None:
6746
return
6847

69-
value = res.raw_result.get('value', None)
70-
flags = res.raw_result.get('flags', None)
71-
res.raw_result['value'] = decode_value(transcoder, value, flags, is_subdoc=is_subdoc)
72-
yield return_cls(res)
48+
yield return_cls(res, transcoder=transcoder, is_subdoc=is_subdoc)
7349

7450

7551
class BlockingWrapper:
@@ -123,16 +99,12 @@ def wrapped_fn(self, *args, **kwargs):
12399
if fn.__name__ in ['_get_all_replicas_internal', '_lookup_in_all_replicas_internal']:
124100
return decode_replicas(transcoder, ret, return_cls, is_subdoc=is_subdoc)
125101

126-
value = ret.raw_result.get('value', None)
127-
flags = ret.raw_result.get('flags', None)
128-
129-
ret.raw_result['value'] = decode_value(transcoder, value, flags, is_subdoc=is_subdoc)
130102
if return_cls is None:
131103
return None
132104
elif return_cls is True:
133105
retval = ret
134106
else:
135-
retval = return_cls(ret)
107+
retval = return_cls(ret, transcoder=transcoder, is_subdoc=is_subdoc)
136108
return retval
137109
except CouchbaseException as e:
138110
raise e

couchbase/result.py

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from __future__ import annotations
1717

1818
import json
19+
from copy import copy
1920
from datetime import datetime
2021
from typing import (Any,
2122
Dict,
@@ -27,6 +28,7 @@
2728
from acouchbase.n1ql import AsyncN1QLRequest
2829
from acouchbase.search import AsyncFullTextSearchRequest
2930
from acouchbase.views import AsyncViewRequest
31+
from couchbase.constants import FMT_JSON
3032
from couchbase.diagnostics import (ClusterState,
3133
EndpointDiagnosticsReport,
3234
EndpointPingReport,
@@ -36,22 +38,30 @@
3638
from couchbase.exceptions import exception as CouchbaseBaseException
3739
from couchbase.pycbc_core import result
3840
from couchbase.subdocument import parse_subdocument_content_as, parse_subdocument_exists
41+
from couchbase.transcoder import Transcoder
3942

4043

4144
class Result:
4245
def __init__(
4346
self,
44-
orig, # type: result
47+
orig, # type: result
48+
transcoder=None, # type: Optional[Transcoder]
49+
is_subdoc=None, # type: Optional[bool]
4550
):
46-
4751
self._orig = orig
52+
self._transcoder = transcoder
53+
self._decoded_value: Optional[Any] = None
54+
self._is_subdoc = is_subdoc if is_subdoc is not None else False
4855

4956
@property
5057
def value(self) -> Optional[Any]:
5158
"""
5259
Optional[Any]: The content of the document, if it exists.
5360
"""
54-
return self._orig.raw_result.get("value", None)
61+
if self._decoded_value is not None:
62+
return self._decoded_value
63+
self._decode_value()
64+
return self._decoded_value
5565

5666
@property
5767
def cas(self) -> Optional[int]:
@@ -81,6 +91,27 @@ def success(self) -> bool:
8191
"""
8292
return self.cas != 0
8393

94+
def _decode_value(self) -> None:
95+
if self._transcoder is None:
96+
return
97+
98+
value = self._orig.raw_result.get('value', None)
99+
if self._is_subdoc is False:
100+
flags = self._orig.raw_result.get('flags', None)
101+
self._decoded_value = self._transcoder.decode_value(value, flags)
102+
else:
103+
self._decoded_value = []
104+
for f in value:
105+
if 'value' in f:
106+
tmp = copy(f)
107+
old = tmp.pop('value', None)
108+
if old:
109+
# no custom transcoder for subdoc ops, use JSON
110+
tmp['value'] = self._transcoder.decode_value(old, FMT_JSON)
111+
self._decoded_value.append(tmp)
112+
else:
113+
self._decoded_value.append(f)
114+
84115

85116
class ContentProxy:
86117
"""
@@ -353,7 +384,8 @@ class MultiResult:
353384
def __init__(self,
354385
orig, # type: result
355386
result_type, # type: Union[GetReplicaResult, GetResult]
356-
return_exceptions # type: bool
387+
return_exceptions, # type: bool
388+
transcoders=None # type: Optional[Dict[str, Transcoder]]
357389
):
358390
self._orig = orig
359391
self._all_ok = self._orig.raw_result.pop('all_okay', False)
@@ -369,7 +401,9 @@ def __init__(self,
369401
if isinstance(v, list):
370402
self._results[k] = v
371403
else:
372-
self._results[k] = result_type(v)
404+
if transcoders is None:
405+
raise InvalidArgumentException("Transcoders dictionary must be provided")
406+
self._results[k] = result_type(v, transcoder=transcoders[k])
373407

374408
@property
375409
def all_ok(self) -> bool:
@@ -394,9 +428,10 @@ def exceptions(self) -> Dict[str, CouchbaseBaseException]:
394428
class MultiGetReplicaResult(MultiResult):
395429
def __init__(self,
396430
orig, # type: result
397-
return_exceptions # type: bool
431+
return_exceptions, # type: bool
432+
transcoders=None # type: Optional[Dict[str, Transcoder]]
398433
):
399-
super().__init__(orig, GetReplicaResult, return_exceptions)
434+
super().__init__(orig, GetReplicaResult, return_exceptions, transcoders)
400435

401436
@property
402437
def results(self) -> Dict[str, GetReplicaResult]:
@@ -423,9 +458,10 @@ def __repr__(self):
423458
class MultiGetResult(MultiResult):
424459
def __init__(self,
425460
orig, # type: result
426-
return_exceptions # type: bool
461+
return_exceptions, # type: bool
462+
transcoders # type: Dict[str, Transcoder]
427463
):
428-
super().__init__(orig, GetResult, return_exceptions)
464+
super().__init__(orig, GetResult, return_exceptions, transcoders)
429465

430466
@property
431467
def results(self) -> Dict[str, GetResult]:
@@ -948,8 +984,8 @@ def __init__(
948984

949985
class ScanResult(Result):
950986

951-
def __init__(self, orig, ids_only):
952-
super().__init__(orig)
987+
def __init__(self, orig, ids_only, transcoder):
988+
super().__init__(orig, transcoder=transcoder)
953989
self._ids_only = ids_only
954990

955991
@property

0 commit comments

Comments
 (0)