Skip to content
This repository has been archived by the owner on Sep 14, 2020. It is now read-only.

Commit

Permalink
Merge pull request #198 from nolar/typehints-lazydictview
Browse files Browse the repository at this point in the history
Resolve special stenzas lazily with no side-effects on the body
  • Loading branch information
nolar authored Oct 5, 2019
2 parents d848601 + 4330354 commit d6bafae
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 21 deletions.
13 changes: 7 additions & 6 deletions kopf/reactor/invocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from typing import Callable

from kopf import config
from kopf.structs import dicts


async def invoke(
Expand All @@ -37,9 +38,9 @@ async def invoke(
kwargs.update(
type=event['type'],
body=event['object'],
spec=event['object'].setdefault('spec', {}),
meta=event['object'].setdefault('metadata', {}),
status=event['object'].setdefault('status', {}),
spec=dicts.DictView(event['object'], 'spec'),
meta=dicts.DictView(event['object'], 'metadata'),
status=dicts.DictView(event['object'], 'status'),
uid=event['object'].get('metadata', {}).get('uid'),
name=event['object'].get('metadata', {}).get('name'),
namespace=event['object'].get('metadata', {}).get('namespace'),
Expand All @@ -54,9 +55,9 @@ async def invoke(
new=cause.new,
patch=cause.patch,
logger=cause.logger,
spec=cause.body.setdefault('spec', {}),
meta=cause.body.setdefault('metadata', {}),
status=cause.body.setdefault('status', {}),
spec=dicts.DictView(cause.body, 'spec'),
meta=dicts.DictView(cause.body, 'metadata'),
status=dicts.DictView(cause.body, 'status'),
uid=cause.body.get('metadata', {}).get('uid'),
name=cause.body.get('metadata', {}).get('name'),
namespace=cause.body.get('metadata', {}).get('namespace'),
Expand Down
43 changes: 42 additions & 1 deletion kopf/structs/dicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
Some basic dicts and field-in-a-dict manipulation helpers.
"""
import collections.abc
from typing import Any, Union, MutableMapping, Mapping, Tuple, List, Text, Iterable, Optional
from typing import (Any, Union, MutableMapping, Mapping, Tuple, List, Text,
Iterable, Iterator, Optional)

FieldPath = Tuple[str, ...]
FieldSpec = Union[None, Text, FieldPath, List[str]]
Expand Down Expand Up @@ -124,3 +125,43 @@ def walk(
yield from walk(obj, nested=nested)
else:
yield objs # NB: not a mapping, no nested sub-fields.


class DictView(Mapping[Any, Any]):
"""
A lazy resolver for the "on-demand" dict keys.
This is needed to have ``spec``, ``status``, and other special fields
to be *assumed* as dicts, even if they are actually not present.
And to prevent their implicit creation with ``.setdefault('spec', {})``,
which produces unwanted side-effects (actually adds this field).
>>> body = {}
>>> spec = DictView(body, 'spec')
>>> spec.get('field', 'default')
... 'default'
>>> body['spec'] = {'field': 'value'}
>>> spec.get('field', 'default')
... 'value'
"""

def __init__(self, __src: Mapping[Any, Any], __path: FieldSpec = None):
super().__init__()
self._src = __src
self._path = parse_field(__path)

def __repr__(self):
return repr(dict(self))

def __len__(self) -> int:
return len(resolve(self._src, self._path, {}, assume_empty=True))

def __iter__(self) -> Iterator[Any]:
return iter(resolve(self._src, self._path, {}, assume_empty=True))

def __getitem__(self, item: Any) -> Any:
return resolve(self._src, self._path + (item,))
27 changes: 13 additions & 14 deletions tests/invocations/test_callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import traceback

import pytest
from asynctest import Mock, MagicMock
from asynctest import MagicMock

from kopf.reactor.invocation import invoke, is_async_fn

Expand Down Expand Up @@ -129,24 +129,21 @@ async def test_async_detection(fn, expected):
@syncasyncparams
async def test_stacktrace_visibility(fn, expected):
stack_trace_marker = STACK_TRACE_MARKER # searched by fn
cause = Mock()
found = await invoke(fn, cause=cause)
found = await invoke(fn)
assert found is expected


@fns
async def test_result_returned(fn):
fn = MagicMock(fn, return_value=999)
cause = Mock()
result = await invoke(fn, cause=cause)
result = await invoke(fn)
assert result == 999


@fns
async def test_explicit_args_passed_properly(fn):
fn = MagicMock(fn)
cause = Mock()
await invoke(fn, 100, 200, cause=cause, kw1=300, kw2=400)
await invoke(fn, 100, 200, kw1=300, kw2=400)

assert fn.called
assert fn.call_count == 1
Expand All @@ -163,7 +160,9 @@ async def test_explicit_args_passed_properly(fn):
@fns
async def test_special_kwargs_added(fn):
fn = MagicMock(fn)
cause = MagicMock(body={'metadata': {'uid': 'uid', 'name': 'name', 'namespace': 'ns'}})
cause = MagicMock(body={'metadata': {'uid': 'uid', 'name': 'name', 'namespace': 'ns'},
'spec': {'field': 'value'},
'status': {'info': 'payload'}})
await invoke(fn, cause=cause)

assert fn.called
Expand All @@ -173,14 +172,14 @@ async def test_special_kwargs_added(fn):
assert fn.call_args[1]['cause'] is cause
assert fn.call_args[1]['event'] is cause.event
assert fn.call_args[1]['body'] is cause.body
assert fn.call_args[1]['spec'] is cause.body['spec']
assert fn.call_args[1]['meta'] is cause.body['metadata']
assert fn.call_args[1]['status'] is cause.body['status']
assert fn.call_args[1]['spec'] == cause.body['spec']
assert fn.call_args[1]['meta'] == cause.body['metadata']
assert fn.call_args[1]['status'] == cause.body['status']
assert fn.call_args[1]['diff'] is cause.diff
assert fn.call_args[1]['old'] is cause.old
assert fn.call_args[1]['new'] is cause.new
assert fn.call_args[1]['patch'] is cause.patch
assert fn.call_args[1]['logger'] is cause.logger
assert fn.call_args[1]['uid'] is cause.body['metadata']['uid']
assert fn.call_args[1]['name'] is cause.body['metadata']['name']
assert fn.call_args[1]['namespace'] is cause.body['metadata']['namespace']
assert fn.call_args[1]['uid'] == cause.body['metadata']['uid']
assert fn.call_args[1]['name'] == cause.body['metadata']['name']
assert fn.call_args[1]['namespace'] == cause.body['metadata']['namespace']

0 comments on commit d6bafae

Please sign in to comment.