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

Resolve special stenzas lazily with no side-effects on the body #198

Merged
merged 4 commits into from
Oct 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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']