diff --git a/kopf/on.py b/kopf/on.py index 9aa4cfe4..ed6f5e4b 100644 --- a/kopf/on.py +++ b/kopf/on.py @@ -17,10 +17,12 @@ def creation_handler(**kwargs): from kopf.reactor import callbacks from kopf.reactor import causation from kopf.reactor import errors as errors_ +from kopf.reactor import handlers from kopf.reactor import handling from kopf.reactor import registries from kopf.structs import bodies from kopf.structs import dicts +from kopf.structs import resources ResourceHandlerDecorator = Callable[[callbacks.ResourceHandlerFn], callbacks.ResourceHandlerFn] ActivityHandlerDecorator = Callable[[callbacks.ActivityHandlerFn], callbacks.ActivityHandlerFn] @@ -37,12 +39,15 @@ def startup( # lgtm[py/similar-function] registry: Optional[registries.OperatorRegistry] = None, ) -> ActivityHandlerDecorator: def decorator(fn: callbacks.ActivityHandlerFn) -> callbacks.ActivityHandlerFn: - actual_registry = registry if registry is not None else registries.get_default_registry() - return actual_registry.register_activity_handler( - fn=fn, id=id, + real_registry = registry if registry is not None else registries.get_default_registry() + real_id = registries.generate_id(fn=fn, id=id) + handler = handlers.ActivityHandler( + fn=fn, id=real_id, errors=errors, timeout=timeout, retries=retries, backoff=backoff, cooldown=cooldown, activity=causation.Activity.STARTUP, ) + real_registry.activity_handlers.append(handler) + return fn return decorator @@ -57,12 +62,15 @@ def cleanup( # lgtm[py/similar-function] registry: Optional[registries.OperatorRegistry] = None, ) -> ActivityHandlerDecorator: def decorator(fn: callbacks.ActivityHandlerFn) -> callbacks.ActivityHandlerFn: - actual_registry = registry if registry is not None else registries.get_default_registry() - return actual_registry.register_activity_handler( - fn=fn, id=id, + real_registry = registry if registry is not None else registries.get_default_registry() + real_id = registries.generate_id(fn=fn, id=id) + handler = handlers.ActivityHandler( + fn=fn, id=real_id, errors=errors, timeout=timeout, retries=retries, backoff=backoff, cooldown=cooldown, activity=causation.Activity.CLEANUP, ) + real_registry.activity_handlers.append(handler) + return fn return decorator @@ -78,12 +86,15 @@ def login( # lgtm[py/similar-function] ) -> ActivityHandlerDecorator: """ ``@kopf.on.login()`` handler for custom (re-)authentication. """ def decorator(fn: callbacks.ActivityHandlerFn) -> callbacks.ActivityHandlerFn: - actual_registry = registry if registry is not None else registries.get_default_registry() - return actual_registry.register_activity_handler( - fn=fn, id=id, + real_registry = registry if registry is not None else registries.get_default_registry() + real_id = registries.generate_id(fn=fn, id=id) + handler = handlers.ActivityHandler( + fn=fn, id=real_id, errors=errors, timeout=timeout, retries=retries, backoff=backoff, cooldown=cooldown, activity=causation.Activity.AUTHENTICATION, ) + real_registry.activity_handlers.append(handler) + return fn return decorator @@ -99,12 +110,15 @@ def probe( # lgtm[py/similar-function] ) -> ActivityHandlerDecorator: """ ``@kopf.on.probe()`` handler for arbitrary liveness metrics. """ def decorator(fn: callbacks.ActivityHandlerFn) -> callbacks.ActivityHandlerFn: - actual_registry = registry if registry is not None else registries.get_default_registry() - return actual_registry.register_activity_handler( - fn=fn, id=id, + real_registry = registry if registry is not None else registries.get_default_registry() + real_id = registries.generate_id(fn=fn, id=id) + handler = handlers.ActivityHandler( + fn=fn, id=real_id, errors=errors, timeout=timeout, retries=retries, backoff=backoff, cooldown=cooldown, activity=causation.Activity.PROBE, ) + real_registry.activity_handlers.append(handler) + return fn return decorator @@ -125,13 +139,18 @@ def resume( # lgtm[py/similar-function] ) -> ResourceHandlerDecorator: """ ``@kopf.on.resume()`` handler for the object resuming on operator (re)start. """ def decorator(fn: callbacks.ResourceHandlerFn) -> callbacks.ResourceHandlerFn: - actual_registry = registry if registry is not None else registries.get_default_registry() - return actual_registry.register_resource_changing_handler( - group=group, version=version, plural=plural, - reason=None, initial=True, deleted=deleted, id=id, + real_registry = registry if registry is not None else registries.get_default_registry() + real_resource = resources.Resource(group, version, plural) + real_id = registries.generate_id(fn=fn, id=id) + handler = handlers.ResourceHandler( + fn=fn, id=real_id, field=None, errors=errors, timeout=timeout, retries=retries, backoff=backoff, cooldown=cooldown, - fn=fn, labels=labels, annotations=annotations, when=when, + labels=labels, annotations=annotations, when=when, + initial=True, deleted=deleted, requires_finalizer=None, + reason=None, ) + real_registry.resource_changing_handlers[real_resource].append(handler) + return fn return decorator @@ -151,13 +170,18 @@ def create( # lgtm[py/similar-function] ) -> ResourceHandlerDecorator: """ ``@kopf.on.create()`` handler for the object creation. """ def decorator(fn: callbacks.ResourceHandlerFn) -> callbacks.ResourceHandlerFn: - actual_registry = registry if registry is not None else registries.get_default_registry() - return actual_registry.register_resource_changing_handler( - group=group, version=version, plural=plural, - reason=causation.Reason.CREATE, id=id, + real_registry = registry if registry is not None else registries.get_default_registry() + real_resource = resources.Resource(group, version, plural) + real_id = registries.generate_id(fn=fn, id=id) + handler = handlers.ResourceHandler( + fn=fn, id=real_id, field=None, errors=errors, timeout=timeout, retries=retries, backoff=backoff, cooldown=cooldown, - fn=fn, labels=labels, annotations=annotations, when=when, + labels=labels, annotations=annotations, when=when, + initial=None, deleted=None, requires_finalizer=None, + reason=causation.Reason.CREATE, ) + real_registry.resource_changing_handlers[real_resource].append(handler) + return fn return decorator @@ -177,13 +201,18 @@ def update( # lgtm[py/similar-function] ) -> ResourceHandlerDecorator: """ ``@kopf.on.update()`` handler for the object update or change. """ def decorator(fn: callbacks.ResourceHandlerFn) -> callbacks.ResourceHandlerFn: - actual_registry = registry if registry is not None else registries.get_default_registry() - return actual_registry.register_resource_changing_handler( - group=group, version=version, plural=plural, - reason=causation.Reason.UPDATE, id=id, + real_registry = registry if registry is not None else registries.get_default_registry() + real_resource = resources.Resource(group, version, plural) + real_id = registries.generate_id(fn=fn, id=id) + handler = handlers.ResourceHandler( + fn=fn, id=real_id, field=None, errors=errors, timeout=timeout, retries=retries, backoff=backoff, cooldown=cooldown, - fn=fn, labels=labels, annotations=annotations, when=when, + labels=labels, annotations=annotations, when=when, + initial=None, deleted=None, requires_finalizer=None, + reason=causation.Reason.UPDATE, ) + real_registry.resource_changing_handlers[real_resource].append(handler) + return fn return decorator @@ -204,14 +233,18 @@ def delete( # lgtm[py/similar-function] ) -> ResourceHandlerDecorator: """ ``@kopf.on.delete()`` handler for the object deletion. """ def decorator(fn: callbacks.ResourceHandlerFn) -> callbacks.ResourceHandlerFn: - actual_registry = registry if registry is not None else registries.get_default_registry() - return actual_registry.register_resource_changing_handler( - group=group, version=version, plural=plural, - reason=causation.Reason.DELETE, id=id, + real_registry = registry if registry is not None else registries.get_default_registry() + real_resource = resources.Resource(group, version, plural) + real_id = registries.generate_id(fn=fn, id=id) + handler = handlers.ResourceHandler( + fn=fn, id=real_id, field=None, errors=errors, timeout=timeout, retries=retries, backoff=backoff, cooldown=cooldown, - fn=fn, requires_finalizer=bool(not optional), labels=labels, annotations=annotations, when=when, + initial=None, deleted=None, requires_finalizer=bool(not optional), + reason=causation.Reason.DELETE, ) + real_registry.resource_changing_handlers[real_resource].append(handler) + return fn return decorator @@ -232,13 +265,19 @@ def field( # lgtm[py/similar-function] ) -> ResourceHandlerDecorator: """ ``@kopf.on.field()`` handler for the individual field changes. """ def decorator(fn: callbacks.ResourceHandlerFn) -> callbacks.ResourceHandlerFn: - actual_registry = registry if registry is not None else registries.get_default_registry() - return actual_registry.register_resource_changing_handler( - group=group, version=version, plural=plural, - reason=None, field=field, id=id, + real_registry = registry if registry is not None else registries.get_default_registry() + real_resource = resources.Resource(group, version, plural) + real_field = dicts.parse_field(field) or None # to not store tuple() as a no-field case. + real_id = registries.generate_id(fn=fn, id=id, suffix=".".join(real_field or [])) + handler = handlers.ResourceHandler( + fn=fn, id=real_id, field=real_field, errors=errors, timeout=timeout, retries=retries, backoff=backoff, cooldown=cooldown, - fn=fn, labels=labels, annotations=annotations, when=when, + labels=labels, annotations=annotations, when=when, + initial=None, deleted=None, requires_finalizer=None, + reason=None, ) + real_registry.resource_changing_handlers[real_resource].append(handler) + return fn return decorator @@ -253,11 +292,18 @@ def event( # lgtm[py/similar-function] ) -> ResourceHandlerDecorator: """ ``@kopf.on.event()`` handler for the silent spies on the events. """ def decorator(fn: callbacks.ResourceHandlerFn) -> callbacks.ResourceHandlerFn: - actual_registry = registry if registry is not None else registries.get_default_registry() - return actual_registry.register_resource_watching_handler( - group=group, version=version, plural=plural, - id=id, fn=fn, labels=labels, annotations=annotations, when=when, + real_registry = registry if registry is not None else registries.get_default_registry() + real_resource = resources.Resource(group, version, plural) + real_id = registries.generate_id(fn=fn, id=id) + handler = handlers.ResourceHandler( + fn=fn, id=real_id, field=None, + errors=None, timeout=None, retries=None, backoff=None, cooldown=None, + labels=labels, annotations=annotations, when=when, + initial=None, deleted=None, requires_finalizer=None, + reason=None, ) + real_registry.resource_watching_handlers[real_resource].append(handler) + return fn return decorator @@ -272,6 +318,9 @@ def this( # lgtm[py/similar-function] backoff: Optional[float] = None, cooldown: Optional[float] = None, # deprecated, use `backoff` registry: Optional[registries.ResourceChangingRegistry] = None, + labels: Optional[bodies.Labels] = None, + annotations: Optional[bodies.Annotations] = None, + when: Optional[callbacks.WhenHandlerFn] = None, ) -> ResourceHandlerDecorator: """ ``@kopf.on.this()`` decorator for the dynamically generated sub-handlers. @@ -303,11 +352,19 @@ def create_task(*, spec, task=task, **kwargs): create function will have its own value, not the latest in the for-cycle. """ def decorator(fn: callbacks.ResourceHandlerFn) -> callbacks.ResourceHandlerFn: - actual_registry = registry if registry is not None else handling.subregistry_var.get() - return actual_registry.register( - id=id, fn=fn, + parent_handler = handling.handler_var.get() + real_registry = registry if registry is not None else handling.subregistry_var.get() + real_id = registries.generate_id(fn=fn, id=id, + prefix=parent_handler.id if parent_handler else None) + handler = handlers.ResourceHandler( + fn=fn, id=real_id, field=None, errors=errors, timeout=timeout, retries=retries, backoff=backoff, cooldown=cooldown, + labels=labels, annotations=annotations, when=when, + initial=None, deleted=None, requires_finalizer=None, + reason=None, ) + real_registry.append(handler) + return fn return decorator @@ -321,6 +378,9 @@ def register( # lgtm[py/similar-function] backoff: Optional[float] = None, cooldown: Optional[float] = None, # deprecated, use `backoff` registry: Optional[registries.ResourceChangingRegistry] = None, + labels: Optional[bodies.Labels] = None, + annotations: Optional[bodies.Annotations] = None, + when: Optional[callbacks.WhenHandlerFn] = None, ) -> callbacks.ResourceHandlerFn: """ Register a function as a sub-handler of the currently executed handler. @@ -349,5 +409,6 @@ def create_single_task(task=task, **_): decorator = this( id=id, registry=registry, errors=errors, timeout=timeout, retries=retries, backoff=backoff, cooldown=cooldown, + labels=labels, annotations=annotations, when=when, ) return decorator(fn) diff --git a/kopf/reactor/activities.py b/kopf/reactor/activities.py index 24004f62..3922a591 100644 --- a/kopf/reactor/activities.py +++ b/kopf/reactor/activities.py @@ -100,7 +100,7 @@ async def run_activity( # For the activity handlers, we have neither bodies, nor patches, just the state. cause = causation.ActivityCause(logger=logger, activity=activity) - handlers = registry.get_activity_handlers(activity=activity) + handlers = registry.activity_handlers.get_handlers(activity=activity) outcomes = await handling.run_handlers_until_done( cause=cause, handlers=handlers, diff --git a/kopf/reactor/handlers.py b/kopf/reactor/handlers.py index 6aba37a9..b8d20973 100644 --- a/kopf/reactor/handlers.py +++ b/kopf/reactor/handlers.py @@ -46,7 +46,7 @@ def __getattribute__(self, name: str) -> Any: @dataclasses.dataclass class ActivityHandler(BaseHandler): fn: callbacks.ActivityHandlerFn # type clarification - activity: Optional[causation.Activity] = None + activity: Optional[causation.Activity] _fallback: bool = False # non-public! @@ -55,12 +55,12 @@ class ResourceHandler(BaseHandler): fn: callbacks.ResourceHandlerFn # type clarification reason: Optional[causation.Reason] field: Optional[dicts.FieldPath] - initial: Optional[bool] = None - deleted: Optional[bool] = None # used for mixed-in (initial==True) @on.resume handlers only. - labels: Optional[bodies.Labels] = None - annotations: Optional[bodies.Annotations] = None - when: Optional[callbacks.WhenHandlerFn] = None - requires_finalizer: Optional[bool] = None + initial: Optional[bool] + deleted: Optional[bool] # used for mixed-in (initial==True) @on.resume handlers only. + labels: Optional[bodies.Labels] + annotations: Optional[bodies.Annotations] + when: Optional[callbacks.WhenHandlerFn] + requires_finalizer: Optional[bool] @property def event(self) -> Optional[causation.Reason]: diff --git a/kopf/reactor/handling.py b/kopf/reactor/handling.py index d46c9702..65d4f259 100644 --- a/kopf/reactor/handling.py +++ b/kopf/reactor/handling.py @@ -104,22 +104,38 @@ async def execute( raise TypeError("Only one of the fns, handlers, registry can be passed. Got more.") elif fns is not None and isinstance(fns, collections.abc.Mapping): - subregistry = registries.ResourceChangingRegistry(prefix=parent_prefix) + subregistry = registries.ResourceChangingRegistry() for id, fn in fns.items(): - subregistry.register(fn=fn, id=id) + real_id = registries.generate_id(fn=fn, id=id, prefix=parent_prefix) + handler = handlers_.ResourceHandler( + fn=fn, id=real_id, + errors=None, timeout=None, retries=None, backoff=None, cooldown=None, + labels=None, annotations=None, when=None, + initial=None, deleted=None, requires_finalizer=None, + reason=None, field=None, + ) + subregistry.append(handler) elif fns is not None and isinstance(fns, collections.abc.Iterable): - subregistry = registries.ResourceChangingRegistry(prefix=parent_prefix) + subregistry = registries.ResourceChangingRegistry() for fn in fns: - subregistry.register(fn=fn) + real_id = registries.generate_id(fn=fn, id=None, prefix=parent_prefix) + handler = handlers_.ResourceHandler( + fn=fn, id=real_id, + errors=None, timeout=None, retries=None, backoff=None, cooldown=None, + labels=None, annotations=None, when=None, + initial=None, deleted=None, requires_finalizer=None, + reason=None, field=None, + ) + subregistry.append(handler) elif fns is not None: raise ValueError(f"fns must be a mapping or an iterable, got {fns.__class__}.") elif handlers is not None: - subregistry = registries.ResourceChangingRegistry(prefix=parent_prefix) + subregistry = registries.ResourceChangingRegistry() for handler in handlers: - subregistry.append(handler=handler) + subregistry.append(handler) # Use the registry as is; assume that the caller knows what they do. elif registry is not None: @@ -350,7 +366,7 @@ async def invoke_handler( # This replaces the multiple kwargs passing through the whole call stack (easy to forget). with invocation.context([ (sublifecycle_var, lifecycle), - (subregistry_var, registries.ResourceChangingRegistry(prefix=handler.id)), + (subregistry_var, registries.ResourceChangingRegistry()), (subexecuted_var, False), (handler_var, handler), (cause_var, cause), diff --git a/kopf/reactor/processing.py b/kopf/reactor/processing.py index a8a72365..6174a34e 100644 --- a/kopf/reactor/processing.py +++ b/kopf/reactor/processing.py @@ -69,7 +69,7 @@ async def process_resource_event( await memories.forget(body) # Invoke all silent spies. No causation, no progress storage is performed. - if registry.has_resource_watching_handlers(resource=resource): + if registry.resource_watching_handlers[resource]: resource_watching_cause = causation.detect_resource_watching_cause( event=event, resource=resource, @@ -86,8 +86,8 @@ async def process_resource_event( # Object patch accumulator. Populated by the methods. Applied in the end of the handler. # Detect the cause and handle it (or at least log this happened). - if registry.has_resource_changing_handlers(resource=resource): - extra_fields = registry.get_extra_fields(resource=resource) + if registry.resource_changing_handlers[resource]: + extra_fields = registry.resource_changing_handlers[resource].get_extra_fields() old, new, diff = lastseen.get_essential_diffs(body=body, extra_fields=extra_fields) resource_changing_cause = causation.detect_resource_changing_cause( event=event, @@ -154,7 +154,7 @@ async def process_resource_watching_cause( Note: K8s-event posting is skipped for `kopf.on.event` handlers, as they should be silent. Still, the messages are logged normally. """ - handlers = registry.get_resource_watching_handlers(cause=cause) + handlers = registry.resource_watching_handlers[cause.resource].get_handlers(cause=cause) outcomes = await handling.execute_handlers_once( lifecycle=lifecycle, handlers=handlers, @@ -183,7 +183,8 @@ async def process_resource_changing_cause( done = None skip = None - requires_finalizer = registry.requires_finalizer(resource=cause.resource, cause=cause) + resource_changing_handlers = registry.resource_changing_handlers[cause.resource] + requires_finalizer = resource_changing_handlers.requires_finalizer(cause=cause) has_finalizer = finalizers.has_finalizers(body=cause.body) if requires_finalizer and not has_finalizer: @@ -203,7 +204,7 @@ async def process_resource_changing_cause( if cause.diff and cause.old is not None and cause.new is not None: logger.debug(f"{title.capitalize()} diff: %r", cause.diff) - handlers = registry.get_resource_changing_handlers(cause=cause) + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause=cause) state = states.State.from_body(body=cause.body, handlers=handlers) if handlers: outcomes = await handling.execute_handlers_once( @@ -227,7 +228,7 @@ async def process_resource_changing_cause( # Regular causes also do some implicit post-handling when all handlers are done. if done or skip: - extra_fields = registry.get_extra_fields(resource=cause.resource) + extra_fields = registry.resource_changing_handlers[cause.resource].get_extra_fields() lastseen.refresh_essence(body=body, patch=patch, extra_fields=extra_fields) if cause.reason == causation.Reason.DELETE: logger.debug("Removing the finalizer, thus allowing the actual deletion.") diff --git a/kopf/reactor/registries.py b/kopf/reactor/registries.py index 7363deba..ea2dda2e 100644 --- a/kopf/reactor/registries.py +++ b/kopf/reactor/registries.py @@ -14,6 +14,7 @@ import abc import collections import functools +import warnings from types import FunctionType, MethodType from typing import (Any, MutableMapping, Optional, Sequence, Collection, Iterable, Iterator, List, Set, FrozenSet, Mapping, Callable, cast, Generic, TypeVar) @@ -38,9 +39,8 @@ class GenericRegistry(Generic[HandlerT, HandlerFnT]): """ A generic base class of a simple registry (with no handler getters). """ _handlers: List[HandlerT] - def __init__(self, prefix: Optional[str] = None) -> None: + def __init__(self) -> None: super().__init__() - self.prefix = prefix self._handlers = [] def __bool__(self) -> bool: @@ -66,7 +66,10 @@ def register( activity: Optional[causation.Activity] = None, _fallback: bool = False, ) -> callbacks.ActivityHandlerFn: - real_id = generate_id(fn=fn, id=id, prefix=self.prefix) + warnings.warn("registry.register() is deprecated; " + "use @kopf.on... decorators with registry= kwarg.", + DeprecationWarning) + real_id = generate_id(fn=fn, id=id) handler = handlers.ActivityHandler( id=real_id, fn=fn, activity=activity, errors=errors, timeout=timeout, retries=retries, backoff=backoff, cooldown=cooldown, @@ -123,11 +126,15 @@ def register( annotations: Optional[bodies.Annotations] = None, when: Optional[callbacks.WhenHandlerFn] = None, ) -> callbacks.ResourceHandlerFn: + warnings.warn("registry.register() is deprecated; " + "use @kopf.on... decorators with registry= kwarg.", + DeprecationWarning) + if reason is None and event is not None: reason = causation.Reason(event) real_field = dicts.parse_field(field) or None # to not store tuple() as a no-field case. - real_id = generate_id(fn=fn, id=id, prefix=self.prefix, suffix=".".join(real_field or [])) + real_id = generate_id(fn=fn, id=id, suffix=".".join(real_field or [])) handler = handlers.ResourceHandler( id=real_id, fn=fn, reason=reason, field=real_field, errors=errors, timeout=timeout, retries=retries, backoff=backoff, cooldown=cooldown, @@ -167,6 +174,9 @@ def requires_finalizer( self, cause: causation.ResourceCause, ) -> bool: + """ + Check whether a finalizer should be added to the given resource or not. + """ # check whether the body matches a deletion handler for handler in self._handlers: if handler.requires_finalizer and match(handler=handler, cause=cause): @@ -210,20 +220,25 @@ class OperatorRegistry: It is usually populated by the ``@kopf.on...`` decorators, but can also be explicitly created and used in the embedded operators. """ - _activity_handlers: ActivityRegistry - _resource_watching_handlers: MutableMapping[resources_.Resource, ResourceWatchingRegistry] - _resource_changing_handlers: MutableMapping[resources_.Resource, ResourceChangingRegistry] + activity_handlers: ActivityRegistry + resource_watching_handlers: MutableMapping[resources_.Resource, ResourceWatchingRegistry] + resource_changing_handlers: MutableMapping[resources_.Resource, ResourceChangingRegistry] def __init__(self) -> None: super().__init__() - self._activity_handlers = ActivityRegistry() - self._resource_watching_handlers = collections.defaultdict(ResourceWatchingRegistry) - self._resource_changing_handlers = collections.defaultdict(ResourceChangingRegistry) + self.activity_handlers = ActivityRegistry() + self.resource_watching_handlers = collections.defaultdict(ResourceWatchingRegistry) + self.resource_changing_handlers = collections.defaultdict(ResourceChangingRegistry) @property def resources(self) -> FrozenSet[resources_.Resource]: """ All known resources in the registry. """ - return frozenset(self._resource_watching_handlers) | frozenset(self._resource_changing_handlers) + return (frozenset(self.resource_watching_handlers) | + frozenset(self.resource_changing_handlers)) + + # + # Everything below is deprecated and will be removed in the next major release. + # def register_activity_handler( self, @@ -238,7 +253,10 @@ def register_activity_handler( activity: Optional[causation.Activity] = None, _fallback: bool = False, ) -> callbacks.ActivityHandlerFn: - return self._activity_handlers.register( + warnings.warn("registry.register_activity_handler() is deprecated; " + "use @kopf.on... decorators with registry= kwarg.", + DeprecationWarning) + return self.activity_handlers.register( fn=fn, id=id, activity=activity, errors=errors, timeout=timeout, retries=retries, backoff=backoff, cooldown=cooldown, _fallback=_fallback, @@ -258,8 +276,11 @@ def register_resource_watching_handler( """ Register an additional handler function for low-level events. """ + warnings.warn("registry.register_resource_watching_handler() is deprecated; " + "use @kopf.on... decorators with registry= kwarg.", + DeprecationWarning) resource = resources_.Resource(group, version, plural) - return self._resource_watching_handlers[resource].register( + return self.resource_watching_handlers[resource].register( fn=fn, id=id, labels=labels, annotations=annotations, when=when, ) @@ -289,8 +310,11 @@ def register_resource_changing_handler( """ Register an additional handler function for the specific resource and specific reason. """ + warnings.warn("registry.register_resource_changing_handler() is deprecated; " + "use @kopf.on... decorators with registry= kwarg.", + DeprecationWarning) resource = resources_.Resource(group, version, plural) - return self._resource_changing_handlers[resource].register( + return self.resource_changing_handlers[resource].register( reason=reason, event=event, field=field, fn=fn, id=id, errors=errors, timeout=timeout, retries=retries, backoff=backoff, cooldown=cooldown, initial=initial, deleted=deleted, requires_finalizer=requires_finalizer, @@ -300,47 +324,66 @@ def register_resource_changing_handler( def has_activity_handlers( self, ) -> bool: - return bool(self._activity_handlers) + warnings.warn("registry.has_activity_handlers() is deprecated; " + "use registry.activity_handlers directly.", + DeprecationWarning) + return bool(self.activity_handlers) def has_resource_watching_handlers( self, resource: resources_.Resource, ) -> bool: - return (resource in self._resource_watching_handlers and - bool(self._resource_watching_handlers[resource])) + warnings.warn("registry.has_resource_watching_handlers() is deprecated; " + "use registry.resource_watching_handlers[resource] directly.", + DeprecationWarning) + return bool(self.resource_watching_handlers[resource]) def has_resource_changing_handlers( self, resource: resources_.Resource, ) -> bool: - return (resource in self._resource_changing_handlers and - bool(self._resource_changing_handlers[resource])) + warnings.warn("registry.has_resource_changing_handlers() is deprecated; " + "use registry.resource_changing_handlers[resource] directly.", + DeprecationWarning) + return bool(self.resource_changing_handlers[resource]) def get_activity_handlers( self, *, activity: causation.Activity, ) -> Sequence[handlers.ActivityHandler]: - return list(_deduplicated(self.iter_activity_handlers(activity=activity))) + warnings.warn("registry.get_activity_handlers() is deprecated; " + "use registry.activity_handlers.get_handlers().", + DeprecationWarning) + return self.activity_handlers.get_handlers(activity=activity) def get_resource_watching_handlers( self, cause: causation.ResourceWatchingCause, ) -> Sequence[handlers.ResourceHandler]: - return list(_deduplicated(self.iter_resource_watching_handlers(cause=cause))) + warnings.warn("registry.get_resource_watching_handlers() is deprecated; " + "use registry.resource_watching_handlers[resource].get_handlers().", + DeprecationWarning) + return self.resource_watching_handlers[cause.resource].get_handlers(cause=cause) def get_resource_changing_handlers( self, cause: causation.ResourceChangingCause, ) -> Sequence[handlers.ResourceHandler]: - return list(_deduplicated(self.iter_resource_changing_handlers(cause=cause))) + warnings.warn("registry.get_resource_changing_handlers() is deprecated; " + "use registry.resource_changing_handlers[resource].get_handlers().", + DeprecationWarning) + return self.resource_changing_handlers[cause.resource].get_handlers(cause=cause) def iter_activity_handlers( self, *, activity: causation.Activity, ) -> Iterator[handlers.ActivityHandler]: - yield from self._activity_handlers.iter_handlers(activity=activity) + warnings.warn("registry.iter_activity_handlers() is deprecated; " + "use registry.activity_handlers.iter_handlers().", + DeprecationWarning) + yield from self.activity_handlers.iter_handlers(activity=activity) def iter_resource_watching_handlers( self, @@ -349,8 +392,10 @@ def iter_resource_watching_handlers( """ Iterate all handlers for the low-level events. """ - if cause.resource in self._resource_watching_handlers: - yield from self._resource_watching_handlers[cause.resource].iter_handlers(cause=cause) + warnings.warn("registry.iter_resource_watching_handlers() is deprecated; " + "use registry.resource_watching_handlers[resource].iter_handlers().", + DeprecationWarning) + yield from self.resource_watching_handlers[cause.resource].iter_handlers(cause=cause) def iter_resource_changing_handlers( self, @@ -359,21 +404,28 @@ def iter_resource_changing_handlers( """ Iterate all handlers that match this cause/event, in the order they were registered (even if mixed). """ - if cause.resource in self._resource_changing_handlers: - yield from self._resource_changing_handlers[cause.resource].iter_handlers(cause=cause) + warnings.warn("registry.iter_resource_changing_handlers() is deprecated; " + "use registry.resource_changing_handlers[resource].iter_handlers().", + DeprecationWarning) + yield from self.resource_changing_handlers[cause.resource].iter_handlers(cause=cause) def get_extra_fields( self, resource: resources_.Resource, ) -> Set[dicts.FieldPath]: - return set(self.iter_extra_fields(resource=resource)) + warnings.warn("registry.get_extra_fields() is deprecated; " + "use registry.resource_changing_handlers[resource].get_extra_fields().", + DeprecationWarning) + return self.resource_changing_handlers[resource].get_extra_fields() def iter_extra_fields( self, resource: resources_.Resource, ) -> Iterator[dicts.FieldPath]: - if resource in self._resource_changing_handlers: - yield from self._resource_changing_handlers[resource].iter_extra_fields() + warnings.warn("registry.iter_extra_fields() is deprecated; " + "use registry.resource_changing_handlers[resource].iter_extra_fields().", + DeprecationWarning) + yield from self.resource_changing_handlers[resource].iter_extra_fields() def requires_finalizer( self, @@ -383,8 +435,10 @@ def requires_finalizer( """ Check whether a finalizer should be added to the given resource or not. """ - return (resource in self._resource_changing_handlers and - self._resource_changing_handlers[resource].requires_finalizer(cause=cause)) + warnings.warn("registry.requires_finalizer() is deprecated; " + "use registry.resource_changing_handlers[resource].requires_finalizer().", + DeprecationWarning) + return self.resource_changing_handlers[resource].requires_finalizer(cause=cause) class SmartOperatorRegistry(OperatorRegistry): @@ -397,26 +451,27 @@ def __init__(self) -> None: except ImportError: pass else: - self.register_activity_handler( - id='login_via_pykube', + self.activity_handlers.append(handlers.ActivityHandler( + id=handlers.HandlerId('login_via_pykube'), fn=cast(callbacks.ActivityHandlerFn, piggybacking.login_via_pykube), activity=causation.Activity.AUTHENTICATION, errors=errors_.ErrorsMode.IGNORED, + timeout=None, retries=None, backoff=None, cooldown=None, _fallback=True, - ) - + )) try: import kubernetes except ImportError: pass else: - self.register_activity_handler( - id='login_via_client', + self.activity_handlers.append(handlers.ActivityHandler( + id=handlers.HandlerId('login_via_client'), fn=cast(callbacks.ActivityHandlerFn, piggybacking.login_via_client), activity=causation.Activity.AUTHENTICATION, errors=errors_.ErrorsMode.IGNORED, + timeout=None, retries=None, backoff=None, cooldown=None, _fallback=True, - ) + )) def generate_id( diff --git a/tests/authentication/test_authentication.py b/tests/authentication/test_authentication.py index ef7a5dae..3ad0e03e 100644 --- a/tests/authentication/test_authentication.py +++ b/tests/authentication/test_authentication.py @@ -1,7 +1,8 @@ import pytest -from kopf.reactor.causation import Activity from kopf.reactor.activities import authenticate +from kopf.reactor.causation import Activity +from kopf.reactor.handlers import ActivityHandler from kopf.reactor.registries import OperatorRegistry from kopf.structs.credentials import Vault, ConnectionInfo, LoginError @@ -28,11 +29,11 @@ async def test_noreturn_handler_produces_no_credentials(): def login_fn(**_): pass - registry.register_activity_handler( - fn=login_fn, - id='login_fn', # auto-detection does not work, as it is local to the test function. - activity=Activity.AUTHENTICATION, - ) + # NB: id auto-detection does not work, as it is local to the test function. + registry.activity_handlers.append(ActivityHandler( + fn=login_fn, id='login_fn', activity=Activity.AUTHENTICATION, + errors=None, timeout=None, retries=None, backoff=None, cooldown=None, + )) await authenticate( registry=registry, @@ -53,11 +54,11 @@ async def test_single_credentials_provided_to_vault(): def login_fn(**_): return info - registry.register_activity_handler( - fn=login_fn, - id='login_fn', # auto-detection does not work, as it is local to the test function. - activity=Activity.AUTHENTICATION, - ) + # NB: id auto-detection does not work, as it is local to the test function. + registry.activity_handlers.append(ActivityHandler( + fn=login_fn, id='login_fn', activity=Activity.AUTHENTICATION, + errors=None, timeout=None, retries=None, backoff=None, cooldown=None, + )) await authenticate( registry=registry, diff --git a/tests/basic-structs/test_handlers.py b/tests/basic-structs/test_handlers.py index 56437c5c..793dee9d 100644 --- a/tests/basic-structs/test_handlers.py +++ b/tests/basic-structs/test_handlers.py @@ -46,8 +46,10 @@ def test_resource_handler_with_all_args(mocker): retries = mocker.Mock() backoff = mocker.Mock() initial = mocker.Mock() + deleted = mocker.Mock() labels = mocker.Mock() annotations = mocker.Mock() + when = mocker.Mock() requires_finalizer = mocker.Mock() handler = ResourceHandler( fn=fn, @@ -60,8 +62,10 @@ def test_resource_handler_with_all_args(mocker): backoff=backoff, cooldown=None, # deprecated, but still required initial=initial, + deleted=deleted, labels=labels, annotations=annotations, + when=when, requires_finalizer=requires_finalizer, ) assert handler.fn is fn @@ -73,8 +77,10 @@ def test_resource_handler_with_all_args(mocker): assert handler.retries is retries assert handler.backoff is backoff assert handler.initial is initial + assert handler.deleted is deleted assert handler.labels is labels assert handler.annotations is annotations + assert handler.when is when assert handler.requires_finalizer is requires_finalizer with pytest.deprecated_call(match=r"use handler.reason"): diff --git a/tests/basic-structs/test_handlers_deprecated_cooldown.py b/tests/basic-structs/test_handlers_deprecated_cooldown.py index b9dc5a96..e534569f 100644 --- a/tests/basic-structs/test_handlers_deprecated_cooldown.py +++ b/tests/basic-structs/test_handlers_deprecated_cooldown.py @@ -47,8 +47,10 @@ def test_resource_handler_with_deprecated_cooldown_instead_of_backoff(mocker): retries = mocker.Mock() backoff = mocker.Mock() initial = mocker.Mock() + deleted = mocker.Mock() labels = mocker.Mock() annotations = mocker.Mock() + when = mocker.Mock() requires_finalizer = mocker.Mock() with pytest.deprecated_call(match=r"use backoff="): @@ -63,8 +65,10 @@ def test_resource_handler_with_deprecated_cooldown_instead_of_backoff(mocker): backoff=None, cooldown=backoff, # deprecated, but still required initial=initial, + deleted=deleted, labels=labels, annotations=annotations, + when=when, requires_finalizer=requires_finalizer, ) @@ -77,6 +81,8 @@ def test_resource_handler_with_deprecated_cooldown_instead_of_backoff(mocker): assert handler.retries is retries assert handler.backoff is backoff assert handler.initial is initial + assert handler.deleted is deleted assert handler.labels is labels assert handler.annotations is annotations + assert handler.when is when assert handler.requires_finalizer is requires_finalizer diff --git a/tests/cli/test_preloading.py b/tests/cli/test_preloading.py index 5707dfc5..a1465f77 100644 --- a/tests/cli/test_preloading.py +++ b/tests/cli/test_preloading.py @@ -16,7 +16,7 @@ def test_one_file(invoke, real_run): registry = kopf.get_default_registry() assert len(registry.resources) == 1 resource = list(registry.resources)[0] - handlers = registry._resource_changing_handlers[resource]._handlers + handlers = registry.resource_changing_handlers[resource]._handlers assert len(handlers) == 1 assert handlers[0].id == 'create_fn' @@ -28,7 +28,7 @@ def test_two_files(invoke, real_run): registry = kopf.get_default_registry() assert len(registry.resources) == 1 resource = list(registry.resources)[0] - handlers = registry._resource_changing_handlers[resource]._handlers + handlers = registry.resource_changing_handlers[resource]._handlers assert len(handlers) == 2 assert handlers[0].id == 'create_fn' assert handlers[1].id == 'update_fn' @@ -41,7 +41,7 @@ def test_one_module(invoke, real_run): registry = kopf.get_default_registry() assert len(registry.resources) == 1 resource = list(registry.resources)[0] - handlers = registry._resource_changing_handlers[resource]._handlers + handlers = registry.resource_changing_handlers[resource]._handlers assert len(handlers) == 1 assert handlers[0].id == 'create_fn' @@ -53,7 +53,7 @@ def test_two_modules(invoke, real_run): registry = kopf.get_default_registry() assert len(registry.resources) == 1 resource = list(registry.resources)[0] - handlers = registry._resource_changing_handlers[resource]._handlers + handlers = registry.resource_changing_handlers[resource]._handlers assert len(handlers) == 2 assert handlers[0].id == 'create_fn' assert handlers[1].id == 'update_fn' @@ -66,7 +66,7 @@ def test_mixed_sources(invoke, real_run): registry = kopf.get_default_registry() assert len(registry.resources) == 1 resource = list(registry.resources)[0] - handlers = registry._resource_changing_handlers[resource]._handlers + handlers = registry.resource_changing_handlers[resource]._handlers assert len(handlers) == 2 assert handlers[0].id == 'create_fn' assert handlers[1].id == 'update_fn' diff --git a/tests/handling/test_activity_triggering.py b/tests/handling/test_activity_triggering.py index 98d47342..1f92945b 100644 --- a/tests/handling/test_activity_triggering.py +++ b/tests/handling/test_activity_triggering.py @@ -5,7 +5,7 @@ from kopf.reactor.activities import ActivityError, run_activity from kopf.reactor.causation import Activity -from kopf.reactor.handlers import HandlerId +from kopf.reactor.handlers import HandlerId, ActivityHandler from kopf.reactor.handling import PermanentError, TemporaryError from kopf.reactor.lifecycles import all_at_once from kopf.reactor.registries import OperatorRegistry @@ -31,8 +31,14 @@ def sample_fn2(**_): return 456 registry = OperatorRegistry() - registry.register_activity_handler(fn=sample_fn1, id='id1', activity=activity) - registry.register_activity_handler(fn=sample_fn2, id='id2', activity=activity) + registry.activity_handlers.append(ActivityHandler( + fn=sample_fn1, id='id1', activity=activity, + errors=None, timeout=None, retries=None, backoff=None, cooldown=None, + )) + registry.activity_handlers.append(ActivityHandler( + fn=sample_fn2, id='id2', activity=activity, + errors=None, timeout=None, retries=None, backoff=None, cooldown=None, + )) results = await run_activity( registry=registry, @@ -55,8 +61,14 @@ def sample_fn2(**_): raise PermanentError("boo!456") registry = OperatorRegistry() - registry.register_activity_handler(fn=sample_fn1, id='id1', activity=activity) - registry.register_activity_handler(fn=sample_fn2, id='id2', activity=activity) + registry.activity_handlers.append(ActivityHandler( + fn=sample_fn1, id='id1', activity=activity, + errors=None, timeout=None, retries=None, backoff=None, cooldown=None, + )) + registry.activity_handlers.append(ActivityHandler( + fn=sample_fn2, id='id2', activity=activity, + errors=None, timeout=None, retries=None, backoff=None, cooldown=None, + )) with pytest.raises(ActivityError) as e: await run_activity( @@ -85,7 +97,10 @@ def sample_fn(**_): raise PermanentError("boo!") registry = OperatorRegistry() - registry.register_activity_handler(fn=sample_fn, id='id', activity=activity) + registry.activity_handlers.append(ActivityHandler( + fn=sample_fn, id='id', activity=activity, + errors=None, timeout=None, retries=None, backoff=None, cooldown=None, + )) with pytest.raises(ActivityError) as e: await run_activity( @@ -108,7 +123,10 @@ def sample_fn(**_): raise TemporaryError('to be retried', delay=0) registry = OperatorRegistry() - registry.register_activity_handler(fn=sample_fn, id='id', activity=activity, retries=3) + registry.activity_handlers.append(ActivityHandler( + fn=sample_fn, id='id', activity=activity, + errors=None, timeout=None, retries=3, backoff=None, cooldown=None, + )) with pytest.raises(ActivityError) as e: await run_activity( @@ -128,7 +146,10 @@ def sample_fn(**_): raise TemporaryError('to be retried', delay=123) registry = OperatorRegistry() - registry.register_activity_handler(fn=sample_fn, id='id', activity=activity, retries=3) + registry.activity_handlers.append(ActivityHandler( + fn=sample_fn, id='id', activity=activity, + errors=None, timeout=None, retries=3, backoff=None, cooldown=None, + )) with freezegun.freeze_time() as frozen: diff --git a/tests/handling/test_no_handlers.py b/tests/handling/test_no_handlers.py index 9c0c3898..5131f8f1 100644 --- a/tests/handling/test_no_handlers.py +++ b/tests/handling/test_no_handlers.py @@ -5,6 +5,7 @@ import kopf from kopf.reactor.causation import HANDLER_REASONS +from kopf.reactor.handlers import ResourceHandler from kopf.reactor.processing import process_resource_event from kopf.structs.containers import ResourceMemories from kopf.structs.lastseen import LAST_SEEN_ANNOTATION @@ -18,15 +19,14 @@ async def test_skipped_with_no_handlers( cause_mock.reason = cause_type cause_mock.body['metadata']['finalizers'] = [] - assert not registry.has_resource_changing_handlers(resource=resource) # prerequisite - registry.register_resource_changing_handler( - group=resource.group, - version=resource.version, - plural=resource.plural, + assert not registry.resource_changing_handlers[resource] # prerequisite + registry.resource_changing_handlers[resource].append(ResourceHandler( reason='a-non-existent-cause-type', - fn=lambda **_: None, - ) - assert registry.has_resource_changing_handlers(resource=resource) # prerequisite + fn=lambda **_: None, id='id', + errors=None, timeout=None, retries=None, backoff=None, cooldown=None, + annotations=None, labels=None, when=None, field=None, + deleted=None, initial=None, requires_finalizer=None, + )) await process_resource_event( lifecycle=kopf.lifecycles.all_at_once, diff --git a/tests/registries/conftest.py b/tests/registries/conftest.py index 0b61ccec..c87b346a 100644 --- a/tests/registries/conftest.py +++ b/tests/registries/conftest.py @@ -1,9 +1,10 @@ import pytest from kopf import ActivityRegistry -from kopf import ResourceWatchingRegistry, ResourceChangingRegistry from kopf import OperatorRegistry +from kopf import ResourceWatchingRegistry, ResourceChangingRegistry from kopf import SimpleRegistry, GlobalRegistry # deprecated, but tested +from kopf.reactor.handlers import HandlerId, ResourceHandler @pytest.fixture(params=[ @@ -38,3 +39,18 @@ def resource_registry_cls(request): ]) def operator_registry_cls(request): return request.param + + +@pytest.fixture() +def parent_handler(): + + def parent_fn(**_): + pass + + return ResourceHandler( + fn=parent_fn, id=HandlerId('parent_fn'), + errors=None, retries=None, timeout=None, backoff=None, cooldown=None, + labels=None, annotations=None, when=None, + initial=None, deleted=None, requires_finalizer=None, + reason=None, field=None, + ) diff --git a/tests/registries/legacy/test_legacy_creation.py b/tests/registries/legacy-1/test_legacy1_creation.py similarity index 50% rename from tests/registries/legacy/test_legacy_creation.py rename to tests/registries/legacy-1/test_legacy1_creation.py index 55896f48..3777f1dc 100644 --- a/tests/registries/legacy/test_legacy_creation.py +++ b/tests/registries/legacy-1/test_legacy1_creation.py @@ -1,21 +1,10 @@ from kopf import BaseRegistry, SimpleRegistry, GlobalRegistry -def test_creation_of_simple_no_prefix(): +def test_creation_of_simple(): registry = SimpleRegistry() assert isinstance(registry, BaseRegistry) assert isinstance(registry, SimpleRegistry) - assert registry.prefix is None - - -def test_creation_of_simple_with_prefix_argument(): - registry = SimpleRegistry('hello') - assert registry.prefix == 'hello' - - -def test_creation_of_simple_with_prefix_keyword(): - registry = SimpleRegistry(prefix='hello') - assert registry.prefix == 'hello' def test_creation_of_global(): diff --git a/tests/registries/legacy/test_legacy_decorators.py b/tests/registries/legacy-1/test_legacy1_decorators.py similarity index 91% rename from tests/registries/legacy/test_legacy_decorators.py rename to tests/registries/legacy-1/test_legacy1_decorators.py index 590c55ca..348c0292 100644 --- a/tests/registries/legacy/test_legacy_decorators.py +++ b/tests/registries/legacy-1/test_legacy1_decorators.py @@ -3,7 +3,9 @@ import kopf from kopf import SimpleRegistry, GlobalRegistry from kopf.reactor.causation import Reason +from kopf.reactor.handling import handler_var from kopf.reactor.handling import subregistry_var +from kopf.reactor.invocation import context from kopf.structs.resources import Resource @@ -227,15 +229,32 @@ def fn(**_): assert handlers[0].when == when -def test_subhandler_declaratively(mocker): +def test_subhandler_fails_with_no_parent_handler(): + + registry = SimpleRegistry() + subregistry_var.set(registry) + + # Check if the contextvar is indeed not set (as a prerequisite). + with pytest.raises(LookupError): + handler_var.get() + + # Check the actual behaviour of the decorator. + with pytest.raises(LookupError): + @kopf.on.this() + def fn(**_): + pass + + +def test_subhandler_declaratively(mocker, parent_handler): cause = mocker.MagicMock(reason=Reason.UPDATE, diff=None) registry = SimpleRegistry() subregistry_var.set(registry) - @kopf.on.this() - def fn(**_): - pass + with context([(handler_var, parent_handler)]): + @kopf.on.this() + def fn(**_): + pass with pytest.deprecated_call(match=r"use ResourceChangingRegistry.get_handlers\(\)"): handlers = registry.get_cause_handlers(cause) @@ -244,7 +263,7 @@ def fn(**_): assert handlers[0].fn is fn -def test_subhandler_imperatively(mocker): +def test_subhandler_imperatively(mocker, parent_handler): cause = mocker.MagicMock(reason=Reason.UPDATE, diff=None) registry = SimpleRegistry() @@ -252,7 +271,9 @@ def test_subhandler_imperatively(mocker): def fn(**_): pass - kopf.register(fn) + + with context([(handler_var, parent_handler)]): + kopf.register(fn) with pytest.deprecated_call(match=r"use ResourceChangingRegistry.get_handlers\(\)"): handlers = registry.get_cause_handlers(cause) diff --git a/tests/registries/legacy/test_legacy_default_registry.py b/tests/registries/legacy-1/test_legacy1_default_registry.py similarity index 100% rename from tests/registries/legacy/test_legacy_default_registry.py rename to tests/registries/legacy-1/test_legacy1_default_registry.py diff --git a/tests/registries/legacy/test_legacy_global_registry.py b/tests/registries/legacy-1/test_legacy1_global_registry.py similarity index 100% rename from tests/registries/legacy/test_legacy_global_registry.py rename to tests/registries/legacy-1/test_legacy1_global_registry.py diff --git a/tests/registries/legacy/test_legacy_handler_matching.py b/tests/registries/legacy-1/test_legacy1_handler_matching.py similarity index 99% rename from tests/registries/legacy/test_legacy_handler_matching.py rename to tests/registries/legacy-1/test_legacy1_handler_matching.py index b7e9e93a..744bd607 100644 --- a/tests/registries/legacy/test_legacy_handler_matching.py +++ b/tests/registries/legacy-1/test_legacy1_handler_matching.py @@ -22,7 +22,8 @@ def registry(request): @pytest.fixture() def register_fn(registry, resource): if isinstance(registry, SimpleRegistry): - yield registry.register + with pytest.deprecated_call(match=r"registry.register\(\) is deprecated"): + yield registry.register elif isinstance(registry, GlobalRegistry): with pytest.deprecated_call(match=r"GlobalRegistry.register_cause_handler\(\) is deprecated"): yield functools.partial(registry.register_cause_handler, resource.group, resource.version, resource.plural) diff --git a/tests/registries/legacy/test_legacy_id_detection.py b/tests/registries/legacy-1/test_legacy1_id_detection.py similarity index 81% rename from tests/registries/legacy/test_legacy_id_detection.py rename to tests/registries/legacy-1/test_legacy1_id_detection.py index 2b49b9ea..e26fbda0 100644 --- a/tests/registries/legacy/test_legacy_id_detection.py +++ b/tests/registries/legacy-1/test_legacy1_id_detection.py @@ -75,7 +75,8 @@ def test_with_no_hints(mocker): get_fn_id = mocker.patch('kopf.reactor.registries.get_callable_id', return_value='some-id') registry = SimpleRegistry() - registry.register(some_fn) + with pytest.deprecated_call(match=r"registry.register\(\) is deprecated"): + registry.register(some_fn) with pytest.deprecated_call(match=r"get_cause_handlers\(\) is deprecated"): handlers = registry.get_cause_handlers(mocker.MagicMock()) @@ -86,11 +87,13 @@ def test_with_no_hints(mocker): assert handlers[0].id == 'some-id' +@pytest.mark.skip("Prefixes are removed from the registries, even from the legacy ones.") def test_with_prefix(mocker): get_fn_id = mocker.patch('kopf.reactor.registries.get_callable_id', return_value='some-id') - registry = SimpleRegistry(prefix='some-prefix') - registry.register(some_fn) + registry = SimpleRegistry() + with pytest.deprecated_call(match=r"registry.register\(\) is deprecated"): + registry.register(some_fn) with pytest.deprecated_call(match=r"get_cause_handlers\(\) is deprecated"): handlers = registry.get_cause_handlers(mocker.MagicMock()) @@ -106,7 +109,8 @@ def test_with_suffix(mocker, field): diff = [('add', ('some-field', 'sub-field'), 'old', 'new')] registry = SimpleRegistry() - registry.register(some_fn, field=field) + with pytest.deprecated_call(match=r"registry.register\(\) is deprecated"): + registry.register(some_fn, field=field) with pytest.deprecated_call(match=r"get_cause_handlers\(\) is deprecated"): handlers = registry.get_cause_handlers(mocker.MagicMock(diff=diff)) @@ -117,12 +121,14 @@ def test_with_suffix(mocker, field): assert handlers[0].id == 'some-id/some-field.sub-field' +@pytest.mark.skip("Prefixes are removed from the registries, even from the legacy ones.") def test_with_prefix_and_suffix(mocker, field): get_fn_id = mocker.patch('kopf.reactor.registries.get_callable_id', return_value='some-id') diff = [('add', ('some-field', 'sub-field'), 'old', 'new')] registry = SimpleRegistry(prefix='some-prefix') - registry.register(some_fn, field=field) + with pytest.deprecated_call(match=r"registry.register\(\) is deprecated"): + registry.register(some_fn, field=field) with pytest.deprecated_call(match=r"get_cause_handlers\(\) is deprecated"): handlers = registry.get_cause_handlers(mocker.MagicMock(diff=diff)) @@ -133,12 +139,14 @@ def test_with_prefix_and_suffix(mocker, field): assert handlers[0].id == 'some-prefix/some-id/some-field.sub-field' +@pytest.mark.skip("Prefixes are removed from the registries, even from the legacy ones.") def test_with_explicit_id_and_prefix_and_suffix(mocker, field): get_fn_id = mocker.patch('kopf.reactor.registries.get_callable_id', return_value='some-id') diff = [('add', ('some-field', 'sub-field'), 'old', 'new')] registry = SimpleRegistry(prefix='some-prefix') - registry.register(some_fn, id='explicit-id', field=field) + with pytest.deprecated_call(match=r"registry.register\(\) is deprecated"): + registry.register(some_fn, id='explicit-id', field=field) with pytest.deprecated_call(match=r"get_cause_handlers\(\) is deprecated"): handlers = registry.get_cause_handlers(mocker.MagicMock(diff=diff)) diff --git a/tests/registries/legacy/test_legacy_registering.py b/tests/registries/legacy-1/test_legacy1_registering.py similarity index 96% rename from tests/registries/legacy/test_legacy_registering.py rename to tests/registries/legacy-1/test_legacy1_registering.py index d7810cef..fe7fb4ab 100644 --- a/tests/registries/legacy/test_legacy_registering.py +++ b/tests/registries/legacy-1/test_legacy1_registering.py @@ -43,7 +43,8 @@ def test_simple_registry_with_minimal_signature(mocker): cause = mocker.Mock(event=None, diff=None) registry = SimpleRegistry() - registry.register(some_fn) + with pytest.deprecated_call(match=r"registry.register\(\) is deprecated"): + registry.register(some_fn) with pytest.deprecated_call(match=r"use ResourceChangingRegistry.get_handlers\(\)"): handlers = registry.get_cause_handlers(cause) diff --git a/tests/registries/legacy/test_legacy_requires_finalizer.py b/tests/registries/legacy-1/test_legacy1_requires_finalizer.py similarity index 79% rename from tests/registries/legacy/test_legacy_requires_finalizer.py rename to tests/registries/legacy-1/test_legacy1_requires_finalizer.py index bd15167c..4bf16c2e 100644 --- a/tests/registries/legacy/test_legacy_requires_finalizer.py +++ b/tests/registries/legacy-1/test_legacy1_requires_finalizer.py @@ -40,7 +40,8 @@ def test_requires_finalizer_deletion_handler(optional, expected): def fn(**_): pass - requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) assert requires_finalizer == expected @@ -62,7 +63,8 @@ def fn1(**_): def fn2(**_): pass - requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) assert requires_finalizer == expected @@ -75,7 +77,8 @@ def test_requires_finalizer_no_deletion_handler(): def fn1(**_): pass - requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) assert requires_finalizer is False @@ -97,7 +100,8 @@ def test_requires_finalizer_deletion_handler_matches_labels(labels, optional, ex def fn(**_): pass - requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) assert requires_finalizer == expected @@ -119,7 +123,8 @@ def test_requires_finalizer_deletion_handler_mismatches_labels(labels, optional, def fn(**_): pass - requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) assert requires_finalizer == expected @@ -141,7 +146,8 @@ def test_requires_finalizer_deletion_handler_matches_annotations(annotations, op def fn(**_): pass - requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) assert requires_finalizer == expected @@ -163,5 +169,6 @@ def test_requires_finalizer_deletion_handler_mismatches_annotations(annotations, def fn(**_): pass - requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) assert requires_finalizer == expected diff --git a/tests/registries/legacy-2/test_legacy2_decorators.py b/tests/registries/legacy-2/test_legacy2_decorators.py new file mode 100644 index 00000000..cde495e4 --- /dev/null +++ b/tests/registries/legacy-2/test_legacy2_decorators.py @@ -0,0 +1,496 @@ +import pytest + +import kopf +from kopf.reactor.causation import Reason, Activity, HANDLER_REASONS +from kopf.reactor.errors import ErrorsMode +from kopf.reactor.handling import subregistry_var, handler_var +from kopf.reactor.invocation import context +from kopf.reactor.registries import OperatorRegistry, ResourceChangingRegistry +from kopf.structs.resources import Resource + + +def test_on_startup_minimal(): + registry = kopf.get_default_registry() + + @kopf.on.startup() + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.activity_handlers"): + handlers = registry.get_activity_handlers(activity=Activity.STARTUP) + + assert len(handlers) == 1 + assert handlers[0].fn is fn + assert handlers[0].activity == Activity.STARTUP + assert handlers[0].errors is None + assert handlers[0].timeout is None + assert handlers[0].retries is None + assert handlers[0].backoff is None + + +def test_on_cleanup_minimal(): + registry = kopf.get_default_registry() + + @kopf.on.cleanup() + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.activity_handlers"): + handlers = registry.get_activity_handlers(activity=Activity.CLEANUP) + + assert len(handlers) == 1 + assert handlers[0].fn is fn + assert handlers[0].activity == Activity.CLEANUP + assert handlers[0].errors is None + assert handlers[0].timeout is None + assert handlers[0].retries is None + assert handlers[0].backoff is None + + +def test_on_probe_minimal(): + registry = kopf.get_default_registry() + + @kopf.on.probe() + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.activity_handlers"): + handlers = registry.get_activity_handlers(activity=Activity.PROBE) + + assert len(handlers) == 1 + assert handlers[0].fn is fn + assert handlers[0].activity == Activity.PROBE + assert handlers[0].errors is None + assert handlers[0].timeout is None + assert handlers[0].retries is None + assert handlers[0].backoff is None + + +# Resume handlers are mixed-in into all resource-changing reactions with initial listing. +@pytest.mark.parametrize('reason', HANDLER_REASONS) +def test_on_resume_minimal(mocker, reason): + registry = kopf.get_default_registry() + resource = Resource('group', 'version', 'plural') + cause = mocker.MagicMock(resource=resource, reason=reason, initial=True, deleted=False) + + @kopf.on.resume('group', 'version', 'plural') + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + + assert len(handlers) == 1 + assert handlers[0].fn is fn + assert handlers[0].reason is None + assert handlers[0].field is None + assert handlers[0].errors is None + assert handlers[0].timeout is None + assert handlers[0].retries is None + assert handlers[0].backoff is None + assert handlers[0].labels is None + assert handlers[0].annotations is None + assert handlers[0].when is None + + +def test_on_create_minimal(mocker): + registry = kopf.get_default_registry() + resource = Resource('group', 'version', 'plural') + cause = mocker.MagicMock(resource=resource, reason=Reason.CREATE) + + @kopf.on.create('group', 'version', 'plural') + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + + assert len(handlers) == 1 + assert handlers[0].fn is fn + assert handlers[0].reason == Reason.CREATE + assert handlers[0].field is None + assert handlers[0].errors is None + assert handlers[0].timeout is None + assert handlers[0].retries is None + assert handlers[0].backoff is None + assert handlers[0].labels is None + assert handlers[0].annotations is None + assert handlers[0].when is None + + +def test_on_update_minimal(mocker): + registry = kopf.get_default_registry() + resource = Resource('group', 'version', 'plural') + cause = mocker.MagicMock(resource=resource, reason=Reason.UPDATE) + + @kopf.on.update('group', 'version', 'plural') + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + + assert len(handlers) == 1 + assert handlers[0].fn is fn + assert handlers[0].reason == Reason.UPDATE + assert handlers[0].field is None + assert handlers[0].errors is None + assert handlers[0].timeout is None + assert handlers[0].retries is None + assert handlers[0].backoff is None + assert handlers[0].labels is None + assert handlers[0].annotations is None + assert handlers[0].when is None + + +def test_on_delete_minimal(mocker): + registry = kopf.get_default_registry() + resource = Resource('group', 'version', 'plural') + cause = mocker.MagicMock(resource=resource, reason=Reason.DELETE) + + @kopf.on.delete('group', 'version', 'plural') + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + + assert len(handlers) == 1 + assert handlers[0].fn is fn + assert handlers[0].reason == Reason.DELETE + assert handlers[0].field is None + assert handlers[0].errors is None + assert handlers[0].timeout is None + assert handlers[0].retries is None + assert handlers[0].backoff is None + assert handlers[0].labels is None + assert handlers[0].annotations is None + assert handlers[0].when is None + + +def test_on_field_minimal(mocker): + registry = kopf.get_default_registry() + resource = Resource('group', 'version', 'plural') + diff = [('op', ('field', 'subfield'), 'old', 'new')] + cause = mocker.MagicMock(resource=resource, reason=Reason.UPDATE, diff=diff) + + @kopf.on.field('group', 'version', 'plural', 'field.subfield') + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + + assert len(handlers) == 1 + assert handlers[0].fn is fn + assert handlers[0].reason is None + assert handlers[0].field == ('field', 'subfield') + assert handlers[0].errors is None + assert handlers[0].timeout is None + assert handlers[0].retries is None + assert handlers[0].backoff is None + assert handlers[0].labels is None + assert handlers[0].annotations is None + assert handlers[0].when is None + + +def test_on_field_fails_without_field(): + with pytest.raises(TypeError): + @kopf.on.field('group', 'version', 'plural') + def fn(**_): + pass + + +def test_on_startup_with_all_kwargs(mocker): + registry = OperatorRegistry() + + @kopf.on.startup( + id='id', registry=registry, + errors=ErrorsMode.PERMANENT, timeout=123, retries=456, backoff=78) + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.activity_handlers"): + handlers = registry.get_activity_handlers(activity=Activity.STARTUP) + + assert len(handlers) == 1 + assert handlers[0].fn is fn + assert handlers[0].activity == Activity.STARTUP + assert handlers[0].id == 'id' + assert handlers[0].errors == ErrorsMode.PERMANENT + assert handlers[0].timeout == 123 + assert handlers[0].retries == 456 + assert handlers[0].backoff == 78 + + +def test_on_cleanup_with_all_kwargs(mocker): + registry = OperatorRegistry() + + @kopf.on.cleanup( + id='id', registry=registry, + errors=ErrorsMode.PERMANENT, timeout=123, retries=456, backoff=78) + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.activity_handlers"): + handlers = registry.get_activity_handlers(activity=Activity.CLEANUP) + + assert len(handlers) == 1 + assert handlers[0].fn is fn + assert handlers[0].activity == Activity.CLEANUP + assert handlers[0].id == 'id' + assert handlers[0].errors == ErrorsMode.PERMANENT + assert handlers[0].timeout == 123 + assert handlers[0].retries == 456 + assert handlers[0].backoff == 78 + + +def test_on_probe_with_all_kwargs(mocker): + registry = OperatorRegistry() + + @kopf.on.probe( + id='id', registry=registry, + errors=ErrorsMode.PERMANENT, timeout=123, retries=456, backoff=78) + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.activity_handlers"): + handlers = registry.get_activity_handlers(activity=Activity.PROBE) + + assert len(handlers) == 1 + assert handlers[0].fn is fn + assert handlers[0].activity == Activity.PROBE + assert handlers[0].id == 'id' + assert handlers[0].errors == ErrorsMode.PERMANENT + assert handlers[0].timeout == 123 + assert handlers[0].retries == 456 + assert handlers[0].backoff == 78 + + +# Resume handlers are mixed-in into all resource-changing reactions with initial listing. +@pytest.mark.parametrize('reason', HANDLER_REASONS) +def test_on_resume_with_all_kwargs(mocker, reason): + registry = OperatorRegistry() + resource = Resource('group', 'version', 'plural') + cause = mocker.MagicMock(resource=resource, reason=reason, initial=True, deleted=False) + mocker.patch('kopf.reactor.registries.match', return_value=True) + + when = lambda **_: False + + @kopf.on.resume('group', 'version', 'plural', + id='id', registry=registry, + errors=ErrorsMode.PERMANENT, timeout=123, retries=456, backoff=78, + deleted=True, + labels={'somelabel': 'somevalue'}, + annotations={'someanno': 'somevalue'}, + when=when) + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + + assert len(handlers) == 1 + assert handlers[0].fn is fn + assert handlers[0].reason is None + assert handlers[0].field is None + assert handlers[0].id == 'id' + assert handlers[0].errors == ErrorsMode.PERMANENT + assert handlers[0].timeout == 123 + assert handlers[0].retries == 456 + assert handlers[0].backoff == 78 + assert handlers[0].deleted == True + assert handlers[0].labels == {'somelabel': 'somevalue'} + assert handlers[0].annotations == {'someanno': 'somevalue'} + assert handlers[0].when == when + + +def test_on_create_with_all_kwargs(mocker): + registry = OperatorRegistry() + resource = Resource('group', 'version', 'plural') + cause = mocker.MagicMock(resource=resource, reason=Reason.CREATE) + mocker.patch('kopf.reactor.registries.match', return_value=True) + + when = lambda **_: False + + @kopf.on.create('group', 'version', 'plural', + id='id', registry=registry, + errors=ErrorsMode.PERMANENT, timeout=123, retries=456, backoff=78, + labels={'somelabel': 'somevalue'}, + annotations={'someanno': 'somevalue'}, + when=when) + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + + assert len(handlers) == 1 + assert handlers[0].fn is fn + assert handlers[0].reason == Reason.CREATE + assert handlers[0].field is None + assert handlers[0].id == 'id' + assert handlers[0].errors == ErrorsMode.PERMANENT + assert handlers[0].timeout == 123 + assert handlers[0].retries == 456 + assert handlers[0].backoff == 78 + assert handlers[0].labels == {'somelabel': 'somevalue'} + assert handlers[0].annotations == {'someanno': 'somevalue'} + assert handlers[0].when == when + + +def test_on_update_with_all_kwargs(mocker): + registry = OperatorRegistry() + resource = Resource('group', 'version', 'plural') + cause = mocker.MagicMock(resource=resource, reason=Reason.UPDATE) + mocker.patch('kopf.reactor.registries.match', return_value=True) + + when = lambda **_: False + + @kopf.on.update('group', 'version', 'plural', + id='id', registry=registry, + errors=ErrorsMode.PERMANENT, timeout=123, retries=456, backoff=78, + labels={'somelabel': 'somevalue'}, + annotations={'someanno': 'somevalue'}, + when=when) + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + + assert len(handlers) == 1 + assert handlers[0].fn is fn + assert handlers[0].reason == Reason.UPDATE + assert handlers[0].field is None + assert handlers[0].id == 'id' + assert handlers[0].errors == ErrorsMode.PERMANENT + assert handlers[0].timeout == 123 + assert handlers[0].retries == 456 + assert handlers[0].backoff == 78 + assert handlers[0].labels == {'somelabel': 'somevalue'} + assert handlers[0].annotations == {'someanno': 'somevalue'} + assert handlers[0].when == when + + +@pytest.mark.parametrize('optional', [ + pytest.param(True, id='optional'), + pytest.param(False, id='mandatory'), +]) +def test_on_delete_with_all_kwargs(mocker, optional): + registry = OperatorRegistry() + resource = Resource('group', 'version', 'plural') + cause = mocker.MagicMock(resource=resource, reason=Reason.DELETE) + mocker.patch('kopf.reactor.registries.match', return_value=True) + + when = lambda **_: False + + @kopf.on.delete('group', 'version', 'plural', + id='id', registry=registry, + errors=ErrorsMode.PERMANENT, timeout=123, retries=456, backoff=78, + optional=optional, + labels={'somelabel': 'somevalue'}, + annotations={'someanno': 'somevalue'}, + when=when) + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + + assert len(handlers) == 1 + assert handlers[0].fn is fn + assert handlers[0].reason == Reason.DELETE + assert handlers[0].field is None + assert handlers[0].id == 'id' + assert handlers[0].errors == ErrorsMode.PERMANENT + assert handlers[0].timeout == 123 + assert handlers[0].retries == 456 + assert handlers[0].backoff == 78 + assert handlers[0].labels == {'somelabel': 'somevalue'} + assert handlers[0].annotations == {'someanno': 'somevalue'} + assert handlers[0].when == when + + +def test_on_field_with_all_kwargs(mocker): + registry = OperatorRegistry() + resource = Resource('group', 'version', 'plural') + diff = [('op', ('field', 'subfield'), 'old', 'new')] + cause = mocker.MagicMock(resource=resource, reason=Reason.UPDATE, diff=diff) + mocker.patch('kopf.reactor.registries.match', return_value=True) + + when = lambda **_: False + + @kopf.on.field('group', 'version', 'plural', 'field.subfield', + id='id', registry=registry, + errors=ErrorsMode.PERMANENT, timeout=123, retries=456, backoff=78, + labels={'somelabel': 'somevalue'}, + annotations={'someanno': 'somevalue'}, + when=when) + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + + assert len(handlers) == 1 + assert handlers[0].fn is fn + assert handlers[0].reason is None + assert handlers[0].field ==('field', 'subfield') + assert handlers[0].id == 'id/field.subfield' + assert handlers[0].errors == ErrorsMode.PERMANENT + assert handlers[0].timeout == 123 + assert handlers[0].retries == 456 + assert handlers[0].backoff == 78 + assert handlers[0].labels == {'somelabel': 'somevalue'} + assert handlers[0].annotations == {'someanno': 'somevalue'} + assert handlers[0].when == when + + +def test_subhandler_fails_with_no_parent_handler(): + + registry = ResourceChangingRegistry() + subregistry_var.set(registry) + + # Check if the contextvar is indeed not set (as a prerequisite). + with pytest.raises(LookupError): + handler_var.get() + + # Check the actual behaviour of the decorator. + with pytest.raises(LookupError): + @kopf.on.this() + def fn(**_): + pass + + +def test_subhandler_declaratively(mocker, parent_handler): + cause = mocker.MagicMock(reason=Reason.UPDATE, diff=None) + + registry = ResourceChangingRegistry() + subregistry_var.set(registry) + + with context([(handler_var, parent_handler)]): + @kopf.on.this() + def fn(**_): + pass + + handlers = registry.get_handlers(cause) + assert len(handlers) == 1 + assert handlers[0].fn is fn + + +def test_subhandler_imperatively(mocker, parent_handler): + cause = mocker.MagicMock(reason=Reason.UPDATE, diff=None) + + registry = ResourceChangingRegistry() + subregistry_var.set(registry) + + def fn(**_): + pass + + with context([(handler_var, parent_handler)]): + kopf.register(fn) + + handlers = registry.get_handlers(cause) + assert len(handlers) == 1 + assert handlers[0].fn is fn diff --git a/tests/registries/legacy-2/test_legacy2_handler_matching.py b/tests/registries/legacy-2/test_legacy2_handler_matching.py new file mode 100644 index 00000000..ed7bfdba --- /dev/null +++ b/tests/registries/legacy-2/test_legacy2_handler_matching.py @@ -0,0 +1,466 @@ +import functools +from unittest.mock import Mock + +import pytest + +from kopf import OperatorRegistry +from kopf.reactor.causation import ResourceChangingCause + + +# Used in the tests. Must be global-scoped, or its qualname will be affected. +def some_fn(x=None): + pass + + +@pytest.fixture(params=[ + pytest.param(OperatorRegistry, id='in-global-registry'), +]) +def registry(request): + return request.param() + + +@pytest.fixture() +def register_fn(registry, resource): + if isinstance(registry, OperatorRegistry): + with pytest.deprecated_call(match=r"register_resource_changing_handler\(\) is deprecated"): + yield functools.partial(registry.register_resource_changing_handler, resource.group, resource.version, resource.plural) + else: + raise Exception(f"Unsupported registry type: {registry}") + + +@pytest.fixture(params=[ + pytest.param(None, id='without-diff'), + pytest.param([], id='with-empty-diff'), +]) +def cause_no_diff(request, resource): + body = {'metadata': {'labels': {'somelabel': 'somevalue'}, 'annotations': {'someannotation': 'somevalue'}}} + return Mock(resource=resource, reason='some-reason', diff=request.param, body=body) + + +@pytest.fixture(params=[ + pytest.param([('op', ('some-field',), 'old', 'new')], id='with-field-diff'), +]) +def cause_with_diff(resource): + body = {'metadata': {'labels': {'somelabel': 'somevalue'}, 'annotations': {'someannotation': 'somevalue'}}} + diff = [('op', ('some-field',), 'old', 'new')] + return Mock(resource=resource, reason='some-reason', diff=diff, body=body) + + +@pytest.fixture(params=[ + pytest.param(None, id='without-diff'), + pytest.param([], id='with-empty-diff'), + pytest.param([('op', ('some-field',), 'old', 'new')], id='with-field-diff'), +]) +def cause_any_diff(resource, request): + body = {'metadata': {'labels': {'somelabel': 'somevalue'}, 'annotations': {'someannotation': 'somevalue'}}} + return Mock(resource=resource, reason='some-reason', diff=request.param, body=body) + + +# +# "Catch-all" handlers are those with event == None. +# + +def test_catchall_handlers_without_field_found(cause_any_diff, registry, register_fn): + register_fn(some_fn, reason=None, field=None) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_any_diff) + assert handlers + + +def test_catchall_handlers_with_field_found(cause_with_diff, registry, register_fn): + register_fn(some_fn, reason=None, field='some-field') + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_with_diff) + assert handlers + + +def test_catchall_handlers_with_field_ignored(cause_no_diff, registry, register_fn): + register_fn(some_fn, reason=None, field='some-field') + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_no_diff) + assert not handlers + + +@pytest.mark.parametrize('labels', [ + pytest.param({'somelabel': 'somevalue'}, id='with-label'), + pytest.param({'somelabel': 'somevalue', 'otherlabel': 'othervalue'}, id='with-extra-label'), +]) +def test_catchall_handlers_with_labels_satisfied(registry, register_fn, resource, labels): + cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'labels': labels}}) + register_fn(some_fn, reason=None, field=None, labels={'somelabel': 'somevalue'}) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + assert handlers + + +@pytest.mark.parametrize('labels', [ + pytest.param({}, id='without-label'), + pytest.param({'somelabel': 'othervalue'}, id='with-other-value'), + pytest.param({'otherlabel': 'othervalue'}, id='with-other-label'), +]) +def test_catchall_handlers_with_labels_not_satisfied(registry, register_fn, resource, labels): + cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'labels': labels}}) + register_fn(some_fn, reason=None, field=None, labels={'somelabel': 'somevalue'}) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + assert not handlers + + +@pytest.mark.parametrize('labels', [ + pytest.param({'somelabel': 'somevalue'}, id='with-label'), + pytest.param({'somelabel': 'othervalue'}, id='with-other-value'), +]) +def test_catchall_handlers_with_labels_exist(registry, register_fn, resource, labels): + cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'labels': labels}}) + register_fn(some_fn, reason=None, field=None, labels={'somelabel': None}) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + assert handlers + + +@pytest.mark.parametrize('labels', [ + pytest.param({}, id='without-label'), + pytest.param({'otherlabel': 'othervalue'}, id='with-other-label'), +]) +def test_catchall_handlers_with_labels_not_exist(registry, register_fn, resource, labels): + cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'labels': labels}}) + register_fn(some_fn, reason=None, field=None, labels={'somelabel': None}) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + assert not handlers + + +@pytest.mark.parametrize('labels', [ + pytest.param({}, id='without-label'), + pytest.param({'somelabel': 'somevalue'}, id='with-label'), + pytest.param({'somelabel': 'othervalue'}, id='with-other-value'), + pytest.param({'otherlabel': 'othervalue'}, id='with-other-label'), + pytest.param({'somelabel': 'somevalue', 'otherlabel': 'othervalue'}, id='with-extra-label'), +]) +def test_catchall_handlers_without_labels(registry, register_fn, resource, labels): + cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'labels': labels}}) + register_fn(some_fn, reason=None, field=None, labels=None) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + assert handlers + + +@pytest.mark.parametrize('annotations', [ + pytest.param({'someannotation': 'somevalue'}, id='with-annotation'), + pytest.param({'someannotation': 'somevalue', 'otherannotation': 'othervalue'}, id='with-extra-annotation'), +]) +def test_catchall_handlers_with_annotations_satisfied(registry, register_fn, resource, annotations): + cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'annotations': annotations}}) + register_fn(some_fn, reason=None, field=None, annotations={'someannotation': 'somevalue'}) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + assert handlers + + +@pytest.mark.parametrize('annotations', [ + pytest.param({}, id='without-annotation'), + pytest.param({'someannotation': 'othervalue'}, id='with-other-value'), + pytest.param({'otherannotation': 'othervalue'}, id='with-other-annotation'), +]) +def test_catchall_handlers_with_annotations_not_satisfied(registry, register_fn, resource, annotations): + cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'annotations': annotations}}) + register_fn(some_fn, reason=None, field=None, annotations={'someannotation': 'somevalue'}) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + assert not handlers + + +@pytest.mark.parametrize('annotations', [ + pytest.param({'someannotation': 'somevalue'}, id='with-annotation'), + pytest.param({'someannotation': 'othervalue'}, id='with-other-value'), +]) +def test_catchall_handlers_with_annotations_exist(registry, register_fn, resource, annotations): + cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'annotations': annotations}}) + register_fn(some_fn, reason=None, field=None, annotations={'someannotation': None}) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + assert handlers + + +@pytest.mark.parametrize('annotations', [ + pytest.param({}, id='without-annotation'), + pytest.param({'otherannotation': 'othervalue'}, id='with-other-annotation'), +]) +def test_catchall_handlers_with_annotations_not_exist(registry, register_fn, resource, annotations): + cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'annotations': annotations}}) + register_fn(some_fn, reason=None, field=None, annotations={'someannotation': None}) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + assert not handlers + + +@pytest.mark.parametrize('annotations', [ + pytest.param({}, id='without-annotation'), + pytest.param({'someannotation': 'somevalue'}, id='with-annotation'), + pytest.param({'someannotation': 'othervalue'}, id='with-other-value'), + pytest.param({'otherannotation': 'othervalue'}, id='with-other-annotation'), + pytest.param({'someannotation': 'somevalue', 'otherannotation': 'othervalue'}, id='with-extra-annotation'), +]) +def test_catchall_handlers_without_annotations(registry, register_fn, resource, annotations): + cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'annotations': annotations}}) + register_fn(some_fn, reason=None, field=None, annotations=None) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + assert handlers + + +@pytest.mark.parametrize('labels, annotations', [ + pytest.param({'somelabel': 'somevalue'}, {'someannotation': 'somevalue'}, id='with-label-annotation'), + pytest.param({'somelabel': 'somevalue', 'otherlabel': 'othervalue'}, {'someannotation': 'somevalue'}, id='with-extra-label-annotation'), + pytest.param({'somelabel': 'somevalue'}, {'someannotation': 'somevalue', 'otherannotation': 'othervalue'}, id='with-label-extra-annotation'), + pytest.param({'somelabel': 'somevalue', 'otherlabel': 'othervalue'}, {'someannotation': 'somevalue', 'otherannotation': 'othervalue'}, id='with-extra-label-extra-annotation'), +]) +def test_catchall_handlers_with_labels_and_annotations_satisfied(registry, register_fn, resource, labels, annotations): + cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'labels': labels, 'annotations': annotations}}) + register_fn(some_fn, reason=None, field=None, labels={'somelabel': 'somevalue'}, annotations={'someannotation': 'somevalue'}) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + assert handlers + + +@pytest.mark.parametrize('labels', [ + pytest.param({}, id='without-label'), + pytest.param({'somelabel': 'somevalue'}, id='with-label'), + pytest.param({'somelabel': 'othervalue'}, id='with-other-value'), + pytest.param({'otherlabel': 'othervalue'}, id='with-other-label'), + pytest.param({'somelabel': 'somevalue', 'otherlabel': 'othervalue'}, id='with-extra-label'), +]) +def test_catchall_handlers_with_labels_and_annotations_not_satisfied(registry, register_fn, resource, labels): + cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'labels': labels}}) + register_fn(some_fn, reason=None, field=None, labels={'somelabel': 'somevalue'}, annotations={'someannotation': 'somevalue'}) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + assert not handlers + + +@pytest.mark.parametrize('when', [ + pytest.param(None, id='without-when'), + pytest.param(lambda body=None, **_: body['spec']['name'] == 'test', id='with-when'), + pytest.param(lambda **_: True, id='with-other-when'), +]) +def test_catchall_handlers_with_when_match(registry, register_fn, resource, when): + cause = ResourceChangingCause( + resource=resource, + reason='some-reason', + diff=None, + body={'spec': {'name': 'test'}}, + logger=None, + patch=None, + memo=None, + initial=None + ) + register_fn(some_fn, reason=None, field=None, when=when) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + assert handlers + + +@pytest.mark.parametrize('when', [ + pytest.param(lambda body=None, **_: body['spec']['name'] != "test", id='with-when'), + pytest.param(lambda **_: False, id='with-other-when'), +]) +def test_catchall_handlers_with_when_not_match(registry, register_fn, resource, when): + cause = ResourceChangingCause( + resource=resource, + reason='some-reason', + diff=None, + body={'spec': {'name': 'test'}}, + logger=None, + patch=None, + memo=None, + initial=None + ) + register_fn(some_fn, reason=None, field=None, when=when) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + assert not handlers + + +# +# Relevant handlers are those with event == 'some-reason' (but not 'another-reason'). +# In the per-field handlers, also with field == 'some-field' (not 'another-field'). +# In the label filtered handlers, the relevant handlers are those that ask for 'somelabel'. +# In the annotation filtered handlers, the relevant handlers are those that ask for 'someannotation'. +# + +def test_relevant_handlers_without_field_found(cause_any_diff, registry, register_fn): + register_fn(some_fn, reason='some-reason') + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_any_diff) + assert handlers + + +def test_relevant_handlers_with_field_found(cause_with_diff, registry, register_fn): + register_fn(some_fn, reason='some-reason', field='some-field') + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_with_diff) + assert handlers + + +def test_relevant_handlers_with_field_ignored(cause_no_diff, registry, register_fn): + register_fn(some_fn, reason='some-reason', field='some-field') + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_no_diff) + assert not handlers + + +def test_relevant_handlers_with_labels_satisfied(cause_any_diff, registry, register_fn): + register_fn(some_fn, reason='some-reason', labels={'somelabel': None}) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_any_diff) + assert handlers + + +def test_relevant_handlers_with_labels_not_satisfied(cause_any_diff, registry, register_fn): + register_fn(some_fn, reason='some-reason', labels={'otherlabel': None}) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_any_diff) + assert not handlers + + +def test_relevant_handlers_with_annotations_satisfied(cause_any_diff, registry, register_fn): + register_fn(some_fn, reason='some-reason', annotations={'someannotation': None}) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_any_diff) + assert handlers + + +def test_relevant_handlers_with_annotations_not_satisfied(cause_any_diff, registry, register_fn): + register_fn(some_fn, reason='some-reason', annotations={'otherannotation': None}) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_any_diff) + assert not handlers + + +def test_relevant_handlers_with_filter_satisfied(cause_any_diff, registry, register_fn): + register_fn(some_fn, reason='some-reason', when=lambda *_: True) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_any_diff) + assert handlers + + +def test_relevant_handlers_with_filter_not_satisfied(cause_any_diff, registry, register_fn): + register_fn(some_fn, reason='some-reason', when=lambda *_: False) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_any_diff) + assert not handlers + + +def test_irrelevant_handlers_without_field_ignored(cause_any_diff, registry, register_fn): + register_fn(some_fn, reason='another-reason') + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_any_diff) + assert not handlers + + +def test_irrelevant_handlers_with_field_ignored(cause_any_diff, registry, register_fn): + register_fn(some_fn, reason='another-reason', field='another-field') + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_any_diff) + assert not handlers + + +def test_irrelevant_handlers_with_labels_satisfied(cause_any_diff, registry, register_fn): + register_fn(some_fn, reason='another-reason', labels={'somelabel': None}) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_any_diff) + assert not handlers + + +def test_irrelevant_handlers_with_labels_not_satisfied(cause_any_diff, registry, register_fn): + register_fn(some_fn, reason='another-reason', labels={'otherlabel': None}) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_any_diff) + assert not handlers + + +def test_irrelevant_handlers_with_annotations_satisfied(cause_any_diff, registry, register_fn): + register_fn(some_fn, reason='another-reason', annotations={'someannotation': None}) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_any_diff) + assert not handlers + + +def test_irrelevant_handlers_with_annotations_not_satisfied(cause_any_diff, registry, register_fn): + register_fn(some_fn, reason='another-reason', annotations={'otherannotation': None}) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_any_diff) + assert not handlers + + +def test_irrelevant_handlers_with_when_satisfied(cause_any_diff, registry, register_fn): + register_fn(some_fn, reason='another-reason', when=lambda *_: True) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_any_diff) + assert not handlers + + +def test_irrelevant_handlers_with_when_not_satisfied(cause_any_diff, registry, register_fn): + register_fn(some_fn, reason='another-reason', when=lambda *_: False) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_any_diff) + assert not handlers + +# +# The handlers must be returned in order of registration, +# even if they are mixed with-/without- * -event/-field handlers. +# + +def test_order_persisted_a(cause_with_diff, registry, register_fn): + register_fn(functools.partial(some_fn, 1), reason=None) + register_fn(functools.partial(some_fn, 2), reason='some-reason') + register_fn(functools.partial(some_fn, 3), reason='filtered-out-reason') + register_fn(functools.partial(some_fn, 4), reason=None, field='filtered-out-reason') + register_fn(functools.partial(some_fn, 5), reason=None, field='some-field') + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_with_diff) + + # Order must be preserved -- same as registered. + assert len(handlers) == 3 + assert handlers[0].reason is None + assert handlers[0].field is None + assert handlers[1].reason == 'some-reason' + assert handlers[1].field is None + assert handlers[2].reason is None + assert handlers[2].field == ('some-field',) + + +def test_order_persisted_b(cause_with_diff, registry, register_fn): + register_fn(functools.partial(some_fn, 1), reason=None, field='some-field') + register_fn(functools.partial(some_fn, 2), reason=None, field='filtered-out-field') + register_fn(functools.partial(some_fn, 3), reason='filtered-out-reason') + register_fn(functools.partial(some_fn, 4), reason='some-reason') + register_fn(functools.partial(some_fn, 5), reason=None) + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_with_diff) + + # Order must be preserved -- same as registered. + assert len(handlers) == 3 + assert handlers[0].reason is None + assert handlers[0].field == ('some-field',) + assert handlers[1].reason == 'some-reason' + assert handlers[1].field is None + assert handlers[2].reason is None + assert handlers[2].field is None + +# +# Same function should not be returned twice for the same event/cause. +# Only actual for the cases when the event/cause can match multiple handlers. +# + +def test_deduplicated(cause_with_diff, registry, register_fn): + register_fn(some_fn, reason=None, id='a') + register_fn(some_fn, reason=None, id='b') + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause_with_diff) + + assert len(handlers) == 1 + assert handlers[0].id == 'a' # the first found one is returned diff --git a/tests/registries/legacy-2/test_legacy2_operator_resources.py b/tests/registries/legacy-2/test_legacy2_operator_resources.py new file mode 100644 index 00000000..27d40294 --- /dev/null +++ b/tests/registries/legacy-2/test_legacy2_operator_resources.py @@ -0,0 +1,34 @@ +import collections + +import pytest + +from kopf.reactor.registries import OperatorRegistry +from kopf.structs.resources import Resource + + +# Used in the tests. Must be global-scoped, or its qualname will be affected. +def some_fn(): + pass + + +def test_resources(): + registry = OperatorRegistry() + + with pytest.deprecated_call(match=r"use @kopf.on"): + registry.register_resource_watching_handler('group1', 'version1', 'plural1', some_fn) + with pytest.deprecated_call(match=r"use @kopf.on"): + registry.register_resource_changing_handler('group2', 'version2', 'plural2', some_fn) + with pytest.deprecated_call(match=r"use @kopf.on"): + registry.register_resource_watching_handler('group2', 'version2', 'plural2', some_fn) + with pytest.deprecated_call(match=r"use @kopf.on"): + registry.register_resource_changing_handler('group1', 'version1', 'plural1', some_fn) + + resources = registry.resources + + assert isinstance(resources, collections.abc.Collection) + assert len(resources) == 2 + + resource1 = Resource('group1', 'version1', 'plural1') + resource2 = Resource('group2', 'version2', 'plural2') + assert resource1 in resources + assert resource2 in resources diff --git a/tests/registries/test_registering.py b/tests/registries/legacy-2/test_legacy2_registering.py similarity index 74% rename from tests/registries/test_registering.py rename to tests/registries/legacy-2/test_legacy2_registering.py index 149fde03..d026ea85 100644 --- a/tests/registries/test_registering.py +++ b/tests/registries/legacy-2/test_legacy2_registering.py @@ -41,7 +41,8 @@ def test_generic_registry_with_minimal_signature(mocker, generic_registry_cls): cause = mocker.Mock(event=None, diff=None) registry = generic_registry_cls() - registry.register(some_fn) + with pytest.deprecated_call(match=r"use @kopf.on"): + registry.register(some_fn) handlers = registry.get_handlers(cause) assert len(handlers) == 1 @@ -60,7 +61,8 @@ def test_operator_registry_with_activity_via_iter( assert not isinstance(iterator, collections.abc.Container) assert not isinstance(iterator, (list, tuple)) - handlers = list(iterator) + with pytest.deprecated_call(match=r"use registry.activity_handlers"): + handlers = list(iterator) assert not handlers @@ -76,7 +78,8 @@ def test_operator_registry_with_resource_watching_via_iter( assert not isinstance(iterator, collections.abc.Container) assert not isinstance(iterator, (list, tuple)) - handlers = list(iterator) + with pytest.deprecated_call(match=r"use registry.resource_watching_handlers"): + handlers = list(iterator) assert not handlers @@ -92,7 +95,8 @@ def test_operator_registry_with_resource_changing_via_iter( assert not isinstance(iterator, collections.abc.Container) assert not isinstance(iterator, (list, tuple)) - handlers = list(iterator) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = list(iterator) assert not handlers @@ -101,7 +105,8 @@ def test_operator_registry_with_activity_via_list( operator_registry_cls, activity): registry = operator_registry_cls() - handlers = registry.get_activity_handlers(activity=activity) + with pytest.deprecated_call(match=r"use registry.activity_handlers"): + handlers = registry.get_activity_handlers(activity=activity) assert isinstance(handlers, collections.abc.Iterable) assert isinstance(handlers, collections.abc.Container) @@ -114,7 +119,8 @@ def test_operator_registry_with_resource_watching_via_list( cause = mocker.Mock(resource=resource, event=None, diff=None) registry = operator_registry_cls() - handlers = registry.get_resource_watching_handlers(cause) + with pytest.deprecated_call(match=r"use registry.resource_watching_handlers"): + handlers = registry.get_resource_watching_handlers(cause) assert isinstance(handlers, collections.abc.Iterable) assert isinstance(handlers, collections.abc.Container) @@ -127,7 +133,8 @@ def test_operator_registry_with_resource_changing_via_list( cause = mocker.Mock(resource=resource, event=None, diff=None) registry = operator_registry_cls() - handlers = registry.get_resource_changing_handlers(cause) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) assert isinstance(handlers, collections.abc.Iterable) assert isinstance(handlers, collections.abc.Container) @@ -140,8 +147,10 @@ def test_operator_registry_with_activity_with_minimal_signature( operator_registry_cls, activity): registry = operator_registry_cls() - registry.register_activity_handler(some_fn) - handlers = registry.get_activity_handlers(activity=activity) + with pytest.deprecated_call(match=r"use @kopf.on"): + registry.register_activity_handler(some_fn) + with pytest.deprecated_call(match=r"use registry.activity_handlers"): + handlers = registry.get_activity_handlers(activity=activity) assert len(handlers) == 1 assert handlers[0].fn is some_fn @@ -152,8 +161,10 @@ def test_operator_registry_with_resource_watching_with_minimal_signature( cause = mocker.Mock(resource=resource, event=None, diff=None) registry = operator_registry_cls() - registry.register_resource_watching_handler(resource.group, resource.version, resource.plural, some_fn) - handlers = registry.get_resource_watching_handlers(cause) + with pytest.deprecated_call(match=r"use @kopf.on"): + registry.register_resource_watching_handler(resource.group, resource.version, resource.plural, some_fn) + with pytest.deprecated_call(match=r"use registry.resource_watching_handlers"): + handlers = registry.get_resource_watching_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is some_fn @@ -164,8 +175,10 @@ def test_operator_registry_with_resource_changing_with_minimal_signature( cause = mocker.Mock(resource=resource, event=None, diff=None) registry = operator_registry_cls() - registry.register_resource_changing_handler(resource.group, resource.version, resource.plural, some_fn) - handlers = registry.get_resource_changing_handlers(cause) + with pytest.deprecated_call(match=r"use @kopf.on"): + registry.register_resource_changing_handler(resource.group, resource.version, resource.plural, some_fn) + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is some_fn diff --git a/tests/registries/legacy-2/test_legacy2_requires_finalizer.py b/tests/registries/legacy-2/test_legacy2_requires_finalizer.py new file mode 100644 index 00000000..906a9b2a --- /dev/null +++ b/tests/registries/legacy-2/test_legacy2_requires_finalizer.py @@ -0,0 +1,174 @@ +import pytest + +import kopf +from kopf.reactor.registries import OperatorRegistry +from kopf.structs.resources import Resource +from kopf.reactor.causation import ResourceCause + +OBJECT_BODY = { + 'apiVersion': 'group/version', + 'kind': 'singular', + 'metadata': { + 'name': 'test', + 'labels': { + 'key': 'value', + }, + 'annotations': { + 'key': 'value', + } + } +} + +CAUSE = ResourceCause( + logger=None, + resource=None, + patch=None, + body=OBJECT_BODY, + memo=None +) + +@pytest.mark.parametrize('optional, expected', [ + pytest.param(True, False, id='optional'), + pytest.param(False, True, id='mandatory'), +]) +def test_requires_finalizer_deletion_handler(optional, expected): + registry = OperatorRegistry() + resource = Resource('group', 'version', 'plural') + + @kopf.on.delete('group', 'version', 'plural', + registry=registry, optional=optional) + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + assert requires_finalizer == expected + + +@pytest.mark.parametrize('optional, expected', [ + pytest.param(True, False, id='optional'), + pytest.param(False, True, id='mandatory'), +]) +def test_requires_finalizer_multiple_handlers(optional, expected): + registry = OperatorRegistry() + resource = Resource('group', 'version', 'plural') + + @kopf.on.create('group', 'version', 'plural', + registry=registry) + def fn1(**_): + pass + + @kopf.on.delete('group', 'version', 'plural', + registry=registry, optional=optional) + def fn2(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + assert requires_finalizer == expected + + +def test_requires_finalizer_no_deletion_handler(): + registry = OperatorRegistry() + resource = Resource('group', 'version', 'plural') + + @kopf.on.create('group', 'version', 'plural', + registry=registry) + def fn1(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + assert requires_finalizer is False + + +@pytest.mark.parametrize('optional, expected', [ + pytest.param(True, False, id='optional'), + pytest.param(False, True, id='mandatory'), +]) +@pytest.mark.parametrize('labels', [ + pytest.param({'key': 'value'}, id='value-matches'), + pytest.param({'key': None}, id='key-exists'), +]) +def test_requires_finalizer_deletion_handler_matches_labels(labels, optional, expected): + registry = OperatorRegistry() + resource = Resource('group', 'version', 'plural') + + @kopf.on.delete('group', 'version', 'plural', + labels=labels, + registry=registry, optional=optional) + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + assert requires_finalizer == expected + + +@pytest.mark.parametrize('optional, expected', [ + pytest.param(True, False, id='optional'), + pytest.param(False, False, id='mandatory'), +]) +@pytest.mark.parametrize('labels', [ + pytest.param({'key': 'othervalue'}, id='value-mismatch'), + pytest.param({'otherkey': None}, id='key-doesnt-exist'), +]) +def test_requires_finalizer_deletion_handler_mismatches_labels(labels, optional, expected): + registry = OperatorRegistry() + resource = Resource('group', 'version', 'plural') + + @kopf.on.delete('group', 'version', 'plural', + labels=labels, + registry=registry, optional=optional) + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + assert requires_finalizer == expected + + +@pytest.mark.parametrize('optional, expected', [ + pytest.param(True, False, id='optional'), + pytest.param(False, True, id='mandatory'), +]) +@pytest.mark.parametrize('annotations', [ + pytest.param({'key': 'value'}, id='value-matches'), + pytest.param({'key': None}, id='key-exists'), +]) +def test_requires_finalizer_deletion_handler_matches_annotations(annotations, optional, expected): + registry = OperatorRegistry() + resource = Resource('group', 'version', 'plural') + + @kopf.on.delete('group', 'version', 'plural', + annotations=annotations, + registry=registry, optional=optional) + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + assert requires_finalizer == expected + + +@pytest.mark.parametrize('optional, expected', [ + pytest.param(True, False, id='optional'), + pytest.param(False, False, id='mandatory'), +]) +@pytest.mark.parametrize('annotations', [ + pytest.param({'key': 'othervalue'}, id='value-mismatch'), + pytest.param({'otherkey': None}, id='key-doesnt-exist'), +]) +def test_requires_finalizer_deletion_handler_mismatches_annotations(annotations, optional, expected): + registry = OperatorRegistry() + resource = Resource('group', 'version', 'plural') + + @kopf.on.delete('group', 'version', 'plural', + annotations=annotations, + registry=registry, optional=optional) + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + assert requires_finalizer == expected diff --git a/tests/registries/legacy-2/test_legacy2_resumes_mixed_in.py b/tests/registries/legacy-2/test_legacy2_resumes_mixed_in.py new file mode 100644 index 00000000..c25d3340 --- /dev/null +++ b/tests/registries/legacy-2/test_legacy2_resumes_mixed_in.py @@ -0,0 +1,68 @@ +import pytest + +import kopf +from kopf.reactor.causation import Reason, HANDLER_REASONS +from kopf.structs.resources import Resource + + +@pytest.mark.parametrize('deleted', [True, False, None]) +@pytest.mark.parametrize('reason', HANDLER_REASONS) +def test_resumes_ignored_for_non_initial_causes(mocker, reason, deleted): + registry = kopf.get_default_registry() + resource = Resource('group', 'version', 'plural') + cause = mocker.MagicMock(resource=resource, reason=reason, initial=False, deleted=deleted) + + @kopf.on.resume('group', 'version', 'plural') + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + assert len(handlers) == 0 + + +@pytest.mark.parametrize('reason', list(set(HANDLER_REASONS) - {Reason.DELETE})) +def test_resumes_selected_for_initial_non_deletions(mocker, reason): + registry = kopf.get_default_registry() + resource = Resource('group', 'version', 'plural') + cause = mocker.MagicMock(resource=resource, reason=reason, initial=True, deleted=False) + + @kopf.on.resume('group', 'version', 'plural') + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + assert len(handlers) == 1 + assert handlers[0].fn is fn + + +@pytest.mark.parametrize('reason', [Reason.DELETE]) +def test_resumes_ignored_for_initial_deletions_by_default(mocker, reason): + registry = kopf.get_default_registry() + resource = Resource('group', 'version', 'plural') + cause = mocker.MagicMock(resource=resource, reason=reason, initial=True, deleted=True) + + @kopf.on.resume('group', 'version', 'plural') + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + assert len(handlers) == 0 + + +@pytest.mark.parametrize('reason', [Reason.DELETE]) +def test_resumes_selected_for_initial_deletions_when_explicitly_marked(mocker, reason): + registry = kopf.get_default_registry() + resource = Resource('group', 'version', 'plural') + cause = mocker.MagicMock(resource=resource, reason=reason, initial=True, deleted=True) + + @kopf.on.resume('group', 'version', 'plural', deleted=True) + def fn(**_): + pass + + with pytest.deprecated_call(match=r"use registry.resource_changing_handlers"): + handlers = registry.get_resource_changing_handlers(cause) + assert len(handlers) == 1 + assert handlers[0].fn is fn diff --git a/tests/registries/test_creation.py b/tests/registries/test_creation.py index 88514a8e..9d649571 100644 --- a/tests/registries/test_creation.py +++ b/tests/registries/test_creation.py @@ -1,26 +1,14 @@ from kopf import ActivityRegistry, ResourceRegistry, OperatorRegistry -def test_activity_registry_with_no_prefix(activity_registry_cls): +def test_activity_registry(activity_registry_cls): registry = activity_registry_cls() assert isinstance(registry, ActivityRegistry) - assert registry.prefix is None -def test_resource_registry_with_no_prefix(resource_registry_cls): +def test_resource_registry(resource_registry_cls): registry = resource_registry_cls() assert isinstance(registry, ResourceRegistry) - assert registry.prefix is None - - -def test_resource_registry_with_prefix_argument(resource_registry_cls): - registry = resource_registry_cls('hello') - assert registry.prefix == 'hello' - - -def test_resource_registry_with_prefix_keyword(resource_registry_cls): - registry = resource_registry_cls(prefix='hello') - assert registry.prefix == 'hello' def test_operator_registry(operator_registry_cls): diff --git a/tests/registries/test_decorators.py b/tests/registries/test_decorators.py index 4591300a..082cbd67 100644 --- a/tests/registries/test_decorators.py +++ b/tests/registries/test_decorators.py @@ -3,7 +3,8 @@ import kopf from kopf.reactor.causation import Reason, Activity, HANDLER_REASONS from kopf.reactor.errors import ErrorsMode -from kopf.reactor.handling import subregistry_var +from kopf.reactor.handling import subregistry_var, handler_var +from kopf.reactor.invocation import context from kopf.reactor.registries import OperatorRegistry, ResourceChangingRegistry from kopf.structs.resources import Resource @@ -15,7 +16,7 @@ def test_on_startup_minimal(): def fn(**_): pass - handlers = registry.get_activity_handlers(activity=Activity.STARTUP) + handlers = registry.activity_handlers.get_handlers(activity=Activity.STARTUP) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].activity == Activity.STARTUP @@ -32,7 +33,7 @@ def test_on_cleanup_minimal(): def fn(**_): pass - handlers = registry.get_activity_handlers(activity=Activity.CLEANUP) + handlers = registry.activity_handlers.get_handlers(activity=Activity.CLEANUP) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].activity == Activity.CLEANUP @@ -49,7 +50,7 @@ def test_on_probe_minimal(): def fn(**_): pass - handlers = registry.get_activity_handlers(activity=Activity.PROBE) + handlers = registry.activity_handlers.get_handlers(activity=Activity.PROBE) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].activity == Activity.PROBE @@ -70,7 +71,7 @@ def test_on_resume_minimal(mocker, reason): def fn(**_): pass - handlers = registry.get_resource_changing_handlers(cause) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].reason is None @@ -93,7 +94,7 @@ def test_on_create_minimal(mocker): def fn(**_): pass - handlers = registry.get_resource_changing_handlers(cause) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].reason == Reason.CREATE @@ -116,7 +117,7 @@ def test_on_update_minimal(mocker): def fn(**_): pass - handlers = registry.get_resource_changing_handlers(cause) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].reason == Reason.UPDATE @@ -139,7 +140,7 @@ def test_on_delete_minimal(mocker): def fn(**_): pass - handlers = registry.get_resource_changing_handlers(cause) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].reason == Reason.DELETE @@ -163,7 +164,7 @@ def test_on_field_minimal(mocker): def fn(**_): pass - handlers = registry.get_resource_changing_handlers(cause) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].reason is None @@ -193,7 +194,7 @@ def test_on_startup_with_all_kwargs(mocker): def fn(**_): pass - handlers = registry.get_activity_handlers(activity=Activity.STARTUP) + handlers = registry.activity_handlers.get_handlers(activity=Activity.STARTUP) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].activity == Activity.STARTUP @@ -213,7 +214,7 @@ def test_on_cleanup_with_all_kwargs(mocker): def fn(**_): pass - handlers = registry.get_activity_handlers(activity=Activity.CLEANUP) + handlers = registry.activity_handlers.get_handlers(activity=Activity.CLEANUP) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].activity == Activity.CLEANUP @@ -233,7 +234,7 @@ def test_on_probe_with_all_kwargs(mocker): def fn(**_): pass - handlers = registry.get_activity_handlers(activity=Activity.PROBE) + handlers = registry.activity_handlers.get_handlers(activity=Activity.PROBE) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].activity == Activity.PROBE @@ -264,7 +265,7 @@ def test_on_resume_with_all_kwargs(mocker, reason): def fn(**_): pass - handlers = registry.get_resource_changing_handlers(cause) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].reason is None @@ -297,7 +298,7 @@ def test_on_create_with_all_kwargs(mocker): def fn(**_): pass - handlers = registry.get_resource_changing_handlers(cause) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].reason == Reason.CREATE @@ -329,7 +330,7 @@ def test_on_update_with_all_kwargs(mocker): def fn(**_): pass - handlers = registry.get_resource_changing_handlers(cause) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].reason == Reason.UPDATE @@ -366,7 +367,7 @@ def test_on_delete_with_all_kwargs(mocker, optional): def fn(**_): pass - handlers = registry.get_resource_changing_handlers(cause) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].reason == Reason.DELETE @@ -399,7 +400,7 @@ def test_on_field_with_all_kwargs(mocker): def fn(**_): pass - handlers = registry.get_resource_changing_handlers(cause) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].reason is None @@ -414,22 +415,39 @@ def fn(**_): assert handlers[0].when == when -def test_subhandler_declaratively(mocker): +def test_subhandler_fails_with_no_parent_handler(): + + registry = ResourceChangingRegistry() + subregistry_var.set(registry) + + # Check if the contextvar is indeed not set (as a prerequisite). + with pytest.raises(LookupError): + handler_var.get() + + # Check the actual behaviour of the decorator. + with pytest.raises(LookupError): + @kopf.on.this() + def fn(**_): + pass + + +def test_subhandler_declaratively(mocker, parent_handler): cause = mocker.MagicMock(reason=Reason.UPDATE, diff=None) registry = ResourceChangingRegistry() subregistry_var.set(registry) - @kopf.on.this() - def fn(**_): - pass + with context([(handler_var, parent_handler)]): + @kopf.on.this() + def fn(**_): + pass handlers = registry.get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn -def test_subhandler_imperatively(mocker): +def test_subhandler_imperatively(mocker, parent_handler): cause = mocker.MagicMock(reason=Reason.UPDATE, diff=None) registry = ResourceChangingRegistry() @@ -437,7 +455,9 @@ def test_subhandler_imperatively(mocker): def fn(**_): pass - kopf.register(fn) + + with context([(handler_var, parent_handler)]): + kopf.register(fn) handlers = registry.get_handlers(cause) assert len(handlers) == 1 diff --git a/tests/registries/test_decorators_deprecated_cooldown.py b/tests/registries/test_decorators_deprecated_cooldown.py index 6f921225..4db6fb37 100644 --- a/tests/registries/test_decorators_deprecated_cooldown.py +++ b/tests/registries/test_decorators_deprecated_cooldown.py @@ -13,7 +13,7 @@ def test_on_startup_with_cooldown(): def fn(**_): pass - handlers = registry.get_activity_handlers(activity=Activity.STARTUP) + handlers = registry.activity_handlers.get_handlers(activity=Activity.STARTUP) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].backoff == 78 @@ -30,7 +30,7 @@ def test_on_cleanup_with_cooldown(): def fn(**_): pass - handlers = registry.get_activity_handlers(activity=Activity.CLEANUP) + handlers = registry.activity_handlers.get_handlers(activity=Activity.CLEANUP) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].backoff == 78 @@ -47,7 +47,7 @@ def test_on_probe_with_cooldown(): def fn(**_): pass - handlers = registry.get_activity_handlers(activity=Activity.PROBE) + handlers = registry.activity_handlers.get_handlers(activity=Activity.PROBE) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].backoff == 78 @@ -69,7 +69,7 @@ def test_on_resume_with_cooldown(mocker, reason): def fn(**_): pass - handlers = registry.get_resource_changing_handlers(cause) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].backoff == 78 @@ -89,7 +89,7 @@ def test_on_create_with_cooldown(mocker): def fn(**_): pass - handlers = registry.get_resource_changing_handlers(cause) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].backoff == 78 @@ -109,7 +109,7 @@ def test_on_update_with_cooldown(mocker): def fn(**_): pass - handlers = registry.get_resource_changing_handlers(cause) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].backoff == 78 @@ -133,7 +133,7 @@ def test_on_delete_with_cooldown(mocker, optional): def fn(**_): pass - handlers = registry.get_resource_changing_handlers(cause) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].backoff == 78 @@ -154,7 +154,7 @@ def test_on_field_with_cooldown(mocker): def fn(**_): pass - handlers = registry.get_resource_changing_handlers(cause) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn assert handlers[0].backoff == 78 diff --git a/tests/registries/test_handler_getting.py b/tests/registries/test_handler_getting.py new file mode 100644 index 00000000..64229940 --- /dev/null +++ b/tests/registries/test_handler_getting.py @@ -0,0 +1,124 @@ +import collections.abc + +import pytest + +from kopf.reactor.causation import Activity + + +# Used in the tests. Must be global-scoped, or its qualname will be affected. +def some_fn(): + pass + + +def test_generic_registry_via_iter(mocker, generic_registry_cls): + cause = mocker.Mock(event=None, diff=None) + + registry = generic_registry_cls() + iterator = registry.iter_handlers(cause) + + assert isinstance(iterator, collections.abc.Iterator) + assert not isinstance(iterator, collections.abc.Collection) + assert not isinstance(iterator, collections.abc.Container) + assert not isinstance(iterator, (list, tuple)) + + handlers = list(iterator) + assert not handlers + + +def test_generic_registry_via_list(mocker, generic_registry_cls): + cause = mocker.Mock(event=None, diff=None) + + registry = generic_registry_cls() + handlers = registry.get_handlers(cause) + + assert isinstance(handlers, collections.abc.Iterable) + assert isinstance(handlers, collections.abc.Container) + assert isinstance(handlers, collections.abc.Collection) + assert not handlers + + +@pytest.mark.parametrize('activity', list(Activity)) +def test_operator_registry_with_activity_via_iter( + operator_registry_cls, activity): + + registry = operator_registry_cls() + iterator = registry.activity_handlers.iter_handlers(activity=activity) + + assert isinstance(iterator, collections.abc.Iterator) + assert not isinstance(iterator, collections.abc.Collection) + assert not isinstance(iterator, collections.abc.Container) + assert not isinstance(iterator, (list, tuple)) + + handlers = list(iterator) + assert not handlers + + +def test_operator_registry_with_resource_watching_via_iter( + mocker, operator_registry_cls, resource): + cause = mocker.Mock(resource=resource, event=None, diff=None) + + registry = operator_registry_cls() + iterator = registry.resource_watching_handlers[resource].iter_handlers(cause) + + assert isinstance(iterator, collections.abc.Iterator) + assert not isinstance(iterator, collections.abc.Collection) + assert not isinstance(iterator, collections.abc.Container) + assert not isinstance(iterator, (list, tuple)) + + handlers = list(iterator) + assert not handlers + + +def test_operator_registry_with_resource_changing_via_iter( + mocker, operator_registry_cls, resource): + cause = mocker.Mock(resource=resource, event=None, diff=None) + + registry = operator_registry_cls() + iterator = registry.resource_changing_handlers[resource].iter_handlers(cause) + + assert isinstance(iterator, collections.abc.Iterator) + assert not isinstance(iterator, collections.abc.Collection) + assert not isinstance(iterator, collections.abc.Container) + assert not isinstance(iterator, (list, tuple)) + + handlers = list(iterator) + assert not handlers + + +@pytest.mark.parametrize('activity', list(Activity)) +def test_operator_registry_with_activity_via_list( + operator_registry_cls, activity): + + registry = operator_registry_cls() + handlers = registry.activity_handlers.get_handlers(activity=activity) + + assert isinstance(handlers, collections.abc.Iterable) + assert isinstance(handlers, collections.abc.Container) + assert isinstance(handlers, collections.abc.Collection) + assert not handlers + + +def test_operator_registry_with_resource_watching_via_list( + mocker, operator_registry_cls, resource): + cause = mocker.Mock(resource=resource, event=None, diff=None) + + registry = operator_registry_cls() + handlers = registry.resource_watching_handlers[resource].get_handlers(cause) + + assert isinstance(handlers, collections.abc.Iterable) + assert isinstance(handlers, collections.abc.Container) + assert isinstance(handlers, collections.abc.Collection) + assert not handlers + + +def test_operator_registry_with_resource_changing_via_list( + mocker, operator_registry_cls, resource): + cause = mocker.Mock(resource=resource, event=None, diff=None) + + registry = operator_registry_cls() + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) + + assert isinstance(handlers, collections.abc.Iterable) + assert isinstance(handlers, collections.abc.Container) + assert isinstance(handlers, collections.abc.Collection) + assert not handlers diff --git a/tests/registries/test_handler_matching.py b/tests/registries/test_handler_matching.py index f66e9817..bfa3bc2b 100644 --- a/tests/registries/test_handler_matching.py +++ b/tests/registries/test_handler_matching.py @@ -1,10 +1,12 @@ -import functools from unittest.mock import Mock import pytest -from kopf import ResourceRegistry, OperatorRegistry -from kopf.reactor.causation import ResourceChangingCause +import kopf +from kopf import OperatorRegistry +from kopf.reactor.causation import ResourceChangingCause, Reason, ALL_REASONS +from kopf.reactor.handlers import ResourceHandler +from kopf.structs.dicts import parse_field # Used in the tests. Must be global-scoped, or its qualname will be affected. @@ -12,21 +14,54 @@ def some_fn(x=None): pass -@pytest.fixture(params=[ - # pytest.param(ResourceRegistry, id='in-simple-registry'), - pytest.param(OperatorRegistry, id='in-global-registry'), +def _never(**_): + return False + + +def _always(**_): + return True + + +matching_reason_and_decorator = pytest.mark.parametrize('reason, decorator', [ + (Reason.CREATE, kopf.on.create), + (Reason.UPDATE, kopf.on.update), + (Reason.DELETE, kopf.on.delete), +]) + +matching_reason_and_decorator_with_field = pytest.mark.parametrize('reason, decorator', [ + (Reason.CREATE, kopf.on.field), + (Reason.UPDATE, kopf.on.field), + (Reason.DELETE, kopf.on.field), ]) -def registry(request): - return request.param() + +mismatching_reason_and_decorator = pytest.mark.parametrize('reason, decorator', [ + (Reason.CREATE, kopf.on.update), + (Reason.CREATE, kopf.on.delete), + (Reason.UPDATE, kopf.on.create), + (Reason.UPDATE, kopf.on.delete), + (Reason.DELETE, kopf.on.create), + (Reason.DELETE, kopf.on.update), +]) + + +@pytest.fixture() +def registry(): + return OperatorRegistry() @pytest.fixture() -def register_fn(registry, resource): - # if isinstance(registry, ResourceRegistry): - # return registry.register - if isinstance(registry, OperatorRegistry): - return functools.partial(registry.register_resource_changing_handler, resource.group, resource.version, resource.plural) - raise Exception(f"Unsupported registry type: {registry}") +def handler_factory(registry, resource): + def factory(**kwargs): + handler = ResourceHandler(**dict(dict( + fn=some_fn, id='a', + errors=None, timeout=None, retries=None, backoff=None, cooldown=None, + initial=None, deleted=None, requires_finalizer=None, + annotations=None, labels=None, when=None, field=None, + reason=None, + ), **kwargs)) + registry.resource_changing_handlers[resource].append(handler) + return handler + return factory @pytest.fixture(params=[ @@ -61,21 +96,27 @@ def cause_any_diff(resource, request): # "Catch-all" handlers are those with event == None. # -def test_catchall_handlers_without_field_found(cause_any_diff, registry, register_fn): - register_fn(some_fn, reason=None, field=None) - handlers = registry.get_resource_changing_handlers(cause_any_diff) +def test_catchall_handlers_without_field_found( + cause_any_diff, registry, handler_factory): + cause = cause_any_diff + handler_factory(reason=None, field=None) + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert handlers -def test_catchall_handlers_with_field_found(cause_with_diff, registry, register_fn): - register_fn(some_fn, reason=None, field='some-field') - handlers = registry.get_resource_changing_handlers(cause_with_diff) +def test_catchall_handlers_with_field_found( + cause_with_diff, registry, handler_factory): + cause = cause_with_diff + handler_factory(reason=None, field=parse_field('some-field')) + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert handlers -def test_catchall_handlers_with_field_ignored(cause_no_diff, registry, register_fn): - register_fn(some_fn, reason=None, field='some-field') - handlers = registry.get_resource_changing_handlers(cause_no_diff) +def test_catchall_handlers_with_field_ignored( + cause_no_diff, registry, handler_factory): + cause = cause_no_diff + handler_factory(reason=None, field=parse_field('some-field')) + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert not handlers @@ -83,10 +124,11 @@ def test_catchall_handlers_with_field_ignored(cause_no_diff, registry, register_ pytest.param({'somelabel': 'somevalue'}, id='with-label'), pytest.param({'somelabel': 'somevalue', 'otherlabel': 'othervalue'}, id='with-extra-label'), ]) -def test_catchall_handlers_with_labels_satisfied(registry, register_fn, resource, labels): +def test_catchall_handlers_with_labels_satisfied( + registry, handler_factory, resource, labels): cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'labels': labels}}) - register_fn(some_fn, reason=None, field=None, labels={'somelabel': 'somevalue'}) - handlers = registry.get_resource_changing_handlers(cause) + handler_factory(reason=None, labels={'somelabel': 'somevalue'}) + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert handlers @@ -95,10 +137,11 @@ def test_catchall_handlers_with_labels_satisfied(registry, register_fn, resource pytest.param({'somelabel': 'othervalue'}, id='with-other-value'), pytest.param({'otherlabel': 'othervalue'}, id='with-other-label'), ]) -def test_catchall_handlers_with_labels_not_satisfied(registry, register_fn, resource, labels): +def test_catchall_handlers_with_labels_not_satisfied( + registry, handler_factory, resource, labels): cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'labels': labels}}) - register_fn(some_fn, reason=None, field=None, labels={'somelabel': 'somevalue'}) - handlers = registry.get_resource_changing_handlers(cause) + handler_factory(reason=None, labels={'somelabel': 'somevalue'}) + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert not handlers @@ -106,10 +149,11 @@ def test_catchall_handlers_with_labels_not_satisfied(registry, register_fn, reso pytest.param({'somelabel': 'somevalue'}, id='with-label'), pytest.param({'somelabel': 'othervalue'}, id='with-other-value'), ]) -def test_catchall_handlers_with_labels_exist(registry, register_fn, resource, labels): +def test_catchall_handlers_with_labels_exist( + registry, handler_factory, resource, labels): cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'labels': labels}}) - register_fn(some_fn, reason=None, field=None, labels={'somelabel': None}) - handlers = registry.get_resource_changing_handlers(cause) + handler_factory(reason=None, labels={'somelabel': None}) + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert handlers @@ -117,10 +161,11 @@ def test_catchall_handlers_with_labels_exist(registry, register_fn, resource, la pytest.param({}, id='without-label'), pytest.param({'otherlabel': 'othervalue'}, id='with-other-label'), ]) -def test_catchall_handlers_with_labels_not_exist(registry, register_fn, resource, labels): +def test_catchall_handlers_with_labels_not_exist( + registry, handler_factory, resource, labels): cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'labels': labels}}) - register_fn(some_fn, reason=None, field=None, labels={'somelabel': None}) - handlers = registry.get_resource_changing_handlers(cause) + handler_factory(reason=None, labels={'somelabel': None}) + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert not handlers @@ -131,10 +176,11 @@ def test_catchall_handlers_with_labels_not_exist(registry, register_fn, resource pytest.param({'otherlabel': 'othervalue'}, id='with-other-label'), pytest.param({'somelabel': 'somevalue', 'otherlabel': 'othervalue'}, id='with-extra-label'), ]) -def test_catchall_handlers_without_labels(registry, register_fn, resource, labels): +def test_catchall_handlers_without_labels( + registry, handler_factory, resource, labels): cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'labels': labels}}) - register_fn(some_fn, reason=None, field=None, labels=None) - handlers = registry.get_resource_changing_handlers(cause) + handler_factory(reason=None, labels=None) + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert handlers @@ -142,10 +188,11 @@ def test_catchall_handlers_without_labels(registry, register_fn, resource, label pytest.param({'someannotation': 'somevalue'}, id='with-annotation'), pytest.param({'someannotation': 'somevalue', 'otherannotation': 'othervalue'}, id='with-extra-annotation'), ]) -def test_catchall_handlers_with_annotations_satisfied(registry, register_fn, resource, annotations): +def test_catchall_handlers_with_annotations_satisfied( + registry, handler_factory, resource, annotations): cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'annotations': annotations}}) - register_fn(some_fn, reason=None, field=None, annotations={'someannotation': 'somevalue'}) - handlers = registry.get_resource_changing_handlers(cause) + handler_factory(reason=None, annotations={'someannotation': 'somevalue'}) + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert handlers @@ -154,10 +201,11 @@ def test_catchall_handlers_with_annotations_satisfied(registry, register_fn, res pytest.param({'someannotation': 'othervalue'}, id='with-other-value'), pytest.param({'otherannotation': 'othervalue'}, id='with-other-annotation'), ]) -def test_catchall_handlers_with_annotations_not_satisfied(registry, register_fn, resource, annotations): +def test_catchall_handlers_with_annotations_not_satisfied( + registry, handler_factory, resource, annotations): cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'annotations': annotations}}) - register_fn(some_fn, reason=None, field=None, annotations={'someannotation': 'somevalue'}) - handlers = registry.get_resource_changing_handlers(cause) + handler_factory(reason=None, annotations={'someannotation': 'somevalue'}) + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert not handlers @@ -165,10 +213,11 @@ def test_catchall_handlers_with_annotations_not_satisfied(registry, register_fn, pytest.param({'someannotation': 'somevalue'}, id='with-annotation'), pytest.param({'someannotation': 'othervalue'}, id='with-other-value'), ]) -def test_catchall_handlers_with_annotations_exist(registry, register_fn, resource, annotations): +def test_catchall_handlers_with_annotations_exist( + registry, handler_factory, resource, annotations): cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'annotations': annotations}}) - register_fn(some_fn, reason=None, field=None, annotations={'someannotation': None}) - handlers = registry.get_resource_changing_handlers(cause) + handler_factory(reason=None, annotations={'someannotation': None}) + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert handlers @@ -176,10 +225,11 @@ def test_catchall_handlers_with_annotations_exist(registry, register_fn, resourc pytest.param({}, id='without-annotation'), pytest.param({'otherannotation': 'othervalue'}, id='with-other-annotation'), ]) -def test_catchall_handlers_with_annotations_not_exist(registry, register_fn, resource, annotations): +def test_catchall_handlers_with_annotations_not_exist( + registry, handler_factory, resource, annotations): cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'annotations': annotations}}) - register_fn(some_fn, reason=None, field=None, annotations={'someannotation': None}) - handlers = registry.get_resource_changing_handlers(cause) + handler_factory(reason=None, annotations={'someannotation': None}) + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert not handlers @@ -190,10 +240,11 @@ def test_catchall_handlers_with_annotations_not_exist(registry, register_fn, res pytest.param({'otherannotation': 'othervalue'}, id='with-other-annotation'), pytest.param({'someannotation': 'somevalue', 'otherannotation': 'othervalue'}, id='with-extra-annotation'), ]) -def test_catchall_handlers_without_annotations(registry, register_fn, resource, annotations): +def test_catchall_handlers_without_annotations( + registry, handler_factory, resource, annotations): cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'annotations': annotations}}) - register_fn(some_fn, reason=None, field=None, annotations=None) - handlers = registry.get_resource_changing_handlers(cause) + handler_factory(reason=None) + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert handlers @@ -203,10 +254,11 @@ def test_catchall_handlers_without_annotations(registry, register_fn, resource, pytest.param({'somelabel': 'somevalue'}, {'someannotation': 'somevalue', 'otherannotation': 'othervalue'}, id='with-label-extra-annotation'), pytest.param({'somelabel': 'somevalue', 'otherlabel': 'othervalue'}, {'someannotation': 'somevalue', 'otherannotation': 'othervalue'}, id='with-extra-label-extra-annotation'), ]) -def test_catchall_handlers_with_labels_and_annotations_satisfied(registry, register_fn, resource, labels, annotations): +def test_catchall_handlers_with_labels_and_annotations_satisfied( + registry, handler_factory, resource, labels, annotations): cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'labels': labels, 'annotations': annotations}}) - register_fn(some_fn, reason=None, field=None, labels={'somelabel': 'somevalue'}, annotations={'someannotation': 'somevalue'}) - handlers = registry.get_resource_changing_handlers(cause) + handler_factory(reason=None, labels={'somelabel': 'somevalue'}, annotations={'someannotation': 'somevalue'}) + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert handlers @@ -217,19 +269,22 @@ def test_catchall_handlers_with_labels_and_annotations_satisfied(registry, regis pytest.param({'otherlabel': 'othervalue'}, id='with-other-label'), pytest.param({'somelabel': 'somevalue', 'otherlabel': 'othervalue'}, id='with-extra-label'), ]) -def test_catchall_handlers_with_labels_and_annotations_not_satisfied(registry, register_fn, resource, labels): +def test_catchall_handlers_with_labels_and_annotations_not_satisfied( + registry, handler_factory, resource, labels): cause = Mock(resource=resource, reason='some-reason', diff=None, body={'metadata': {'labels': labels}}) - register_fn(some_fn, reason=None, field=None, labels={'somelabel': 'somevalue'}, annotations={'someannotation': 'somevalue'}) - handlers = registry.get_resource_changing_handlers(cause) + handler_factory(reason=None, labels={'somelabel': 'somevalue'}, annotations={'someannotation': 'somevalue'}) + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert not handlers +@pytest.mark.parametrize('reason', ALL_REASONS) @pytest.mark.parametrize('when', [ pytest.param(None, id='without-when'), pytest.param(lambda body=None, **_: body['spec']['name'] == 'test', id='with-when'), pytest.param(lambda **_: True, id='with-other-when'), ]) -def test_catchall_handlers_with_when_match(registry, register_fn, resource, when): +def test_catchall_handlers_with_when_match( + registry, handler_factory, resource, reason, when): cause = ResourceChangingCause( resource=resource, reason='some-reason', @@ -240,8 +295,8 @@ def test_catchall_handlers_with_when_match(registry, register_fn, resource, when memo=None, initial=None ) - register_fn(some_fn, reason=None, field=None, when=when) - handlers = registry.get_resource_changing_handlers(cause) + handler_factory(reason=None, when=when) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert handlers @@ -249,7 +304,8 @@ def test_catchall_handlers_with_when_match(registry, register_fn, resource, when pytest.param(lambda body=None, **_: body['spec']['name'] != "test", id='with-when'), pytest.param(lambda **_: False, id='with-other-when'), ]) -def test_catchall_handlers_with_when_not_match(registry, register_fn, resource, when): +def test_catchall_handlers_with_when_not_match( + registry, handler_factory, resource, when): cause = ResourceChangingCause( resource=resource, reason='some-reason', @@ -260,116 +316,253 @@ def test_catchall_handlers_with_when_not_match(registry, register_fn, resource, memo=None, initial=None ) - register_fn(some_fn, reason=None, field=None, when=when) - handlers = registry.get_resource_changing_handlers(cause) + handler_factory(reason=None, when=when) + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert not handlers # -# Relevant handlers are those with event == 'some-reason' (but not 'another-reason'). +# Relevant handlers are those with reason matching the cause's reason. # In the per-field handlers, also with field == 'some-field' (not 'another-field'). # In the label filtered handlers, the relevant handlers are those that ask for 'somelabel'. # In the annotation filtered handlers, the relevant handlers are those that ask for 'someannotation'. # -def test_relevant_handlers_without_field_found(cause_any_diff, registry, register_fn): - register_fn(some_fn, reason='some-reason') - handlers = registry.get_resource_changing_handlers(cause_any_diff) + +@matching_reason_and_decorator_with_field +def test_relevant_handlers_without_field_found( + cause_any_diff, registry, resource, reason, decorator): + + @decorator(resource.group, resource.version, resource.plural, registry=registry, + field=None) + def some_fn(**_): ... + + cause = cause_any_diff + cause.reason = reason + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert handlers -def test_relevant_handlers_with_field_found(cause_with_diff, registry, register_fn): - register_fn(some_fn, reason='some-reason', field='some-field') - handlers = registry.get_resource_changing_handlers(cause_with_diff) +@matching_reason_and_decorator_with_field +def test_relevant_handlers_with_field_found( + cause_with_diff, registry, resource, reason, decorator): + + @decorator(resource.group, resource.version, resource.plural, registry=registry, + field='some-field') + def some_fn(**_): ... + + cause = cause_with_diff + cause.reason = reason + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert handlers -def test_relevant_handlers_with_field_ignored(cause_no_diff, registry, register_fn): - register_fn(some_fn, reason='some-reason', field='some-field') - handlers = registry.get_resource_changing_handlers(cause_no_diff) +@matching_reason_and_decorator_with_field +def test_relevant_handlers_with_field_ignored( + cause_no_diff, registry, resource, reason, decorator): + + @decorator(resource.group, resource.version, resource.plural, registry=registry, + field='some-field') + def some_fn(**_): ... + + cause = cause_no_diff + cause.reason = reason + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert not handlers -def test_relevant_handlers_with_labels_satisfied(cause_any_diff, registry, register_fn): - register_fn(some_fn, reason='some-reason', labels={'somelabel': None}) - handlers = registry.get_resource_changing_handlers(cause_any_diff) +@matching_reason_and_decorator +def test_relevant_handlers_with_labels_satisfied( + cause_any_diff, registry, resource, reason, decorator): + + @decorator(resource.group, resource.version, resource.plural, registry=registry, + labels={'somelabel': None}) + def some_fn(**_): ... + + cause = cause_any_diff + cause.reason = reason + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert handlers -def test_relevant_handlers_with_labels_not_satisfied(cause_any_diff, registry, register_fn): - register_fn(some_fn, reason='some-reason', labels={'otherlabel': None}) - handlers = registry.get_resource_changing_handlers(cause_any_diff) +@matching_reason_and_decorator +def test_relevant_handlers_with_labels_not_satisfied( + cause_any_diff, registry, resource, reason, decorator): + + @decorator(resource.group, resource.version, resource.plural, registry=registry, + labels={'otherlabel': None}) + def some_fn(**_): ... + + cause = cause_any_diff + cause.reason = reason + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert not handlers -def test_relevant_handlers_with_annotations_satisfied(cause_any_diff, registry, register_fn): - register_fn(some_fn, reason='some-reason', annotations={'someannotation': None}) - handlers = registry.get_resource_changing_handlers(cause_any_diff) +@matching_reason_and_decorator +def test_relevant_handlers_with_annotations_satisfied( + cause_any_diff, registry, resource, reason, decorator): + + @decorator(resource.group, resource.version, resource.plural, registry=registry, + annotations={'someannotation': None}) + def some_fn(**_): ... + + cause = cause_any_diff + cause.reason = reason + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert handlers -def test_relevant_handlers_with_annotations_not_satisfied(cause_any_diff, registry, register_fn): - register_fn(some_fn, reason='some-reason', annotations={'otherannotation': None}) - handlers = registry.get_resource_changing_handlers(cause_any_diff) +@matching_reason_and_decorator +def test_relevant_handlers_with_annotations_not_satisfied( + cause_any_diff, registry, resource, reason, decorator): + + @decorator(resource.group, resource.version, resource.plural, registry=registry, + annotations={'otherannotation': None}) + def some_fn(**_): ... + + cause = cause_any_diff + cause.reason = reason + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert not handlers -def test_relevant_handlers_with_filter_satisfied(cause_any_diff, registry, register_fn): - register_fn(some_fn, reason='some-reason', when=lambda *_: True) - handlers = registry.get_resource_changing_handlers(cause_any_diff) +@matching_reason_and_decorator +def test_relevant_handlers_with_filter_satisfied( + cause_any_diff, registry, resource, reason, decorator): + + @decorator(resource.group, resource.version, resource.plural, registry=registry, + when=_always) + def some_fn(**_): ... + + cause = cause_any_diff + cause.reason = reason + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert handlers -def test_relevant_handlers_with_filter_not_satisfied(cause_any_diff, registry, register_fn): - register_fn(some_fn, reason='some-reason', when=lambda *_: False) - handlers = registry.get_resource_changing_handlers(cause_any_diff) +@matching_reason_and_decorator +def test_relevant_handlers_with_filter_not_satisfied( + cause_any_diff, registry, resource, reason, decorator): + + @decorator(resource.group, resource.version, resource.plural, registry=registry, + when=_never) + def some_fn(**_): ... + + cause = cause_any_diff + cause.reason = reason + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert not handlers -def test_irrelevant_handlers_without_field_ignored(cause_any_diff, registry, register_fn): - register_fn(some_fn, reason='another-reason') - handlers = registry.get_resource_changing_handlers(cause_any_diff) +@mismatching_reason_and_decorator +def test_irrelevant_handlers_without_field_ignored( + cause_any_diff, registry, resource, reason, decorator): + + @decorator(resource.group, resource.version, resource.plural, registry=registry) + def some_fn(**_): ... + + cause = cause_any_diff + cause.reason = reason + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert not handlers -def test_irrelevant_handlers_with_field_ignored(cause_any_diff, registry, register_fn): - register_fn(some_fn, reason='another-reason', field='another-field') - handlers = registry.get_resource_changing_handlers(cause_any_diff) +@matching_reason_and_decorator_with_field +def test_irrelevant_handlers_with_field_ignored( + cause_any_diff, registry, resource, reason, decorator): + + @decorator(resource.group, resource.version, resource.plural, registry=registry, + field='another-field') + def some_fn(**_): ... + + cause = cause_any_diff + cause.reason = reason + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert not handlers -def test_irrelevant_handlers_with_labels_satisfied(cause_any_diff, registry, register_fn): - register_fn(some_fn, reason='another-reason', labels={'somelabel': None}) - handlers = registry.get_resource_changing_handlers(cause_any_diff) + +@mismatching_reason_and_decorator +def test_irrelevant_handlers_with_labels_satisfied( + cause_any_diff, registry, resource, reason, decorator): + + @decorator(resource.group, resource.version, resource.plural, registry=registry, + labels={'somelabel': None}) + def some_fn(**_): ... + + cause = cause_any_diff + cause.reason = reason + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert not handlers -def test_irrelevant_handlers_with_labels_not_satisfied(cause_any_diff, registry, register_fn): - register_fn(some_fn, reason='another-reason', labels={'otherlabel': None}) - handlers = registry.get_resource_changing_handlers(cause_any_diff) +@mismatching_reason_and_decorator +def test_irrelevant_handlers_with_labels_not_satisfied( + cause_any_diff, registry, resource, reason, decorator): + + @decorator(resource.group, resource.version, resource.plural, registry=registry, + labels={'otherlabel': None}) + def some_fn(**_): ... + + cause = cause_any_diff + cause.reason = reason + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert not handlers -def test_irrelevant_handlers_with_annotations_satisfied(cause_any_diff, registry, register_fn): - register_fn(some_fn, reason='another-reason', annotations={'someannotation': None}) - handlers = registry.get_resource_changing_handlers(cause_any_diff) +@mismatching_reason_and_decorator +def test_irrelevant_handlers_with_annotations_satisfied( + cause_any_diff, registry, resource, reason, decorator): + + @decorator(resource.group, resource.version, resource.plural, registry=registry, + annotations={'someannotation': None}) + def some_fn(**_): ... + + cause = cause_any_diff + cause.reason = reason + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert not handlers -def test_irrelevant_handlers_with_annotations_not_satisfied(cause_any_diff, registry, register_fn): - register_fn(some_fn, reason='another-reason', annotations={'otherannotation': None}) - handlers = registry.get_resource_changing_handlers(cause_any_diff) +@mismatching_reason_and_decorator +def test_irrelevant_handlers_with_annotations_not_satisfied( + cause_any_diff, registry, resource, reason, decorator): + + @decorator(resource.group, resource.version, resource.plural, registry=registry, + annotations={'otherannotation': None}) + def some_fn(**_): ... + + cause = cause_any_diff + cause.reason = reason + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert not handlers -def test_irrelevant_handlers_with_when_satisfied(cause_any_diff, registry, register_fn): - register_fn(some_fn, reason='another-reason', when=lambda *_: True) - handlers = registry.get_resource_changing_handlers(cause_any_diff) +@mismatching_reason_and_decorator +def test_irrelevant_handlers_with_when_satisfied( + cause_any_diff, registry, resource, reason, decorator): + + @decorator(resource.group, resource.version, resource.plural, registry=registry, + when=_always) + def some_fn(**_): ... + + cause = cause_any_diff + cause.reason = reason + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert not handlers -def test_irrelevant_handlers_with_when_not_satisfied(cause_any_diff, registry, register_fn): - register_fn(some_fn, reason='another-reason', when=lambda *_: False) - handlers = registry.get_resource_changing_handlers(cause_any_diff) +@mismatching_reason_and_decorator +def test_irrelevant_handlers_with_when_not_satisfied( + cause_any_diff, registry, resource, reason, decorator): + + @decorator(resource.group, resource.version, resource.plural, registry=registry, + when=_never) + def some_fn(**_): ... + + cause = cause_any_diff + cause.reason = reason + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert not handlers # @@ -377,53 +570,81 @@ def test_irrelevant_handlers_with_when_not_satisfied(cause_any_diff, registry, r # even if they are mixed with-/without- * -event/-field handlers. # -def test_order_persisted_a(cause_with_diff, registry, register_fn): - register_fn(functools.partial(some_fn, 1), reason=None) - register_fn(functools.partial(some_fn, 2), reason='some-reason') - register_fn(functools.partial(some_fn, 3), reason='filtered-out-reason') - register_fn(functools.partial(some_fn, 4), reason=None, field='filtered-out-reason') - register_fn(functools.partial(some_fn, 5), reason=None, field='some-field') +def test_order_persisted_a(cause_with_diff, registry, resource): + + @kopf.on.create(resource.group, resource.version, resource.plural, registry=registry) + def some_fn_1(**_): ... # used + + @kopf.on.update(resource.group, resource.version, resource.plural, registry=registry) + def some_fn_2(**_): ... # filtered out + + @kopf.on.create(resource.group, resource.version, resource.plural, registry=registry) + def some_fn_3(**_): ... # used + + @kopf.on.field(resource.group, resource.version, resource.plural, registry=registry, field='filtered-out-field') + def some_fn_4(**_): ... # filtered out - handlers = registry.get_resource_changing_handlers(cause_with_diff) + @kopf.on.field(resource.group, resource.version, resource.plural, registry=registry, field='some-field') + def some_fn_5(**_): ... # used + + cause = cause_with_diff + cause.reason = Reason.CREATE + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) # Order must be preserved -- same as registered. assert len(handlers) == 3 - assert handlers[0].reason is None - assert handlers[0].field is None - assert handlers[1].reason == 'some-reason' - assert handlers[1].field is None - assert handlers[2].reason is None - assert handlers[2].field == ('some-field',) + assert handlers[0].fn is some_fn_1 + assert handlers[1].fn is some_fn_3 + assert handlers[2].fn is some_fn_5 + + +def test_order_persisted_b(cause_with_diff, registry, resource): + + # TODO: add registering by just `resource` or `resource.name` + # TODO: remake it to `registry.on.field(...)`, and make `kopf.on` decorators as aliases for a default registry. + # @registry.on.field(resource.group, resource.version, resource.plural, field='some-field') + @kopf.on.field(resource.group, resource.version, resource.plural, registry=registry, field='some-field') + def some_fn_1(**_): ... # used + @kopf.on.field(resource.group, resource.version, resource.plural, registry=registry, field='filtered-out-field') + def some_fn_2(**_): ... # filtered out -def test_order_persisted_b(cause_with_diff, registry, register_fn): - register_fn(functools.partial(some_fn, 1), reason=None, field='some-field') - register_fn(functools.partial(some_fn, 2), reason=None, field='filtered-out-field') - register_fn(functools.partial(some_fn, 3), reason='filtered-out-reason') - register_fn(functools.partial(some_fn, 4), reason='some-reason') - register_fn(functools.partial(some_fn, 5), reason=None) + @kopf.on.create(resource.group, resource.version, resource.plural, registry=registry) + def some_fn_3(**_): ... # used - handlers = registry.get_resource_changing_handlers(cause_with_diff) + @kopf.on.update(resource.group, resource.version, resource.plural, registry=registry) + def some_fn_4(**_): ... # filtered out + + @kopf.on.create(resource.group, resource.version, resource.plural, registry=registry) + def some_fn_5(**_): ... # used + + cause = cause_with_diff + cause.reason = Reason.CREATE + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) # Order must be preserved -- same as registered. assert len(handlers) == 3 - assert handlers[0].reason is None - assert handlers[0].field == ('some-field',) - assert handlers[1].reason == 'some-reason' - assert handlers[1].field is None - assert handlers[2].reason is None - assert handlers[2].field is None + assert handlers[0].fn is some_fn_1 + assert handlers[1].fn is some_fn_3 + assert handlers[2].fn is some_fn_5 # # Same function should not be returned twice for the same event/cause. # Only actual for the cases when the event/cause can match multiple handlers. # -def test_deduplicated(cause_with_diff, registry, register_fn): - register_fn(some_fn, reason=None, id='a') - register_fn(some_fn, reason=None, id='b') +@matching_reason_and_decorator +def test_deduplicated( + cause_with_diff, registry, resource, reason, decorator): + + # Note: the decorators are applied bottom-up -- hence, the order of ids: + @decorator(resource.group, resource.version, resource.plural, registry=registry, id='b') + @decorator(resource.group, resource.version, resource.plural, registry=registry, id='a') + def some_fn(**_): ... - handlers = registry.get_resource_changing_handlers(cause_with_diff) + cause = cause_with_diff + cause.reason = reason + handlers = registry.resource_changing_handlers[cause.resource].get_handlers(cause) assert len(handlers) == 1 assert handlers[0].id == 'a' # the first found one is returned diff --git a/tests/registries/test_id_detection.py b/tests/registries/test_id_detection.py index 5512111a..324bf459 100644 --- a/tests/registries/test_id_detection.py +++ b/tests/registries/test_id_detection.py @@ -1,7 +1,5 @@ import functools -import pytest - from kopf.reactor.registries import get_callable_id @@ -10,15 +8,6 @@ def some_fn(): pass -@pytest.fixture(params=[ - 'some-field.sub-field', - ['some-field', 'sub-field'], - ('some-field', 'sub-field'), -], ids=['str', 'list', 'tuple']) -def field(request): - return request.param - - def test_id_of_simple_function(): fn_id = get_callable_id(some_fn) assert fn_id == 'some_fn' @@ -68,86 +57,3 @@ def test_id_of_lambda(): fn_id = get_callable_id(some_lambda) assert fn_id.startswith(f'lambda:{__file__}:') - - -def test_with_no_hints( - mocker, resource_registry_cls): - - get_fn_id = mocker.patch('kopf.reactor.registries.get_callable_id', return_value='some-id') - - registry = resource_registry_cls() - registry.register(some_fn) - handlers = registry.get_handlers(mocker.MagicMock()) - - assert get_fn_id.called - - assert len(handlers) == 1 - assert handlers[0].fn is some_fn - assert handlers[0].id == 'some-id' - - -def test_with_prefix( - mocker, resource_registry_cls): - - get_fn_id = mocker.patch('kopf.reactor.registries.get_callable_id', return_value='some-id') - - registry = resource_registry_cls(prefix='some-prefix') - registry.register(some_fn) - handlers = registry.get_handlers(mocker.MagicMock()) - - assert get_fn_id.called - - assert len(handlers) == 1 - assert handlers[0].fn is some_fn - assert handlers[0].id == 'some-prefix/some-id' - - -def test_with_suffix( - mocker, field, resource_registry_cls): - - get_fn_id = mocker.patch('kopf.reactor.registries.get_callable_id', return_value='some-id') - diff = [('add', ('some-field', 'sub-field'), 'old', 'new')] - - registry = resource_registry_cls() - registry.register(some_fn, field=field) - handlers = registry.get_handlers(mocker.MagicMock(diff=diff)) - - assert get_fn_id.called - - assert len(handlers) == 1 - assert handlers[0].fn is some_fn - assert handlers[0].id == 'some-id/some-field.sub-field' - - -def test_with_prefix_and_suffix( - mocker, field, resource_registry_cls): - - get_fn_id = mocker.patch('kopf.reactor.registries.get_callable_id', return_value='some-id') - diff = [('add', ('some-field', 'sub-field'), 'old', 'new')] - - registry = resource_registry_cls(prefix='some-prefix') - registry.register(some_fn, field=field) - handlers = registry.get_handlers(mocker.MagicMock(diff=diff)) - - assert get_fn_id.called - - assert len(handlers) == 1 - assert handlers[0].fn is some_fn - assert handlers[0].id == 'some-prefix/some-id/some-field.sub-field' - - -def test_with_explicit_id_and_prefix_and_suffix( - mocker, field, resource_registry_cls): - - get_fn_id = mocker.patch('kopf.reactor.registries.get_callable_id', return_value='some-id') - diff = [('add', ('some-field', 'sub-field'), 'old', 'new')] - - registry = resource_registry_cls(prefix='some-prefix') - registry.register(some_fn, id='explicit-id', field=field) - handlers = registry.get_handlers(mocker.MagicMock(diff=diff)) - - assert not get_fn_id.called - - assert len(handlers) == 1 - assert handlers[0].fn is some_fn - assert handlers[0].id == 'some-prefix/explicit-id/some-field.sub-field' diff --git a/tests/registries/test_operator_resources.py b/tests/registries/test_operator_resources.py index 7de8c582..988c90c2 100644 --- a/tests/registries/test_operator_resources.py +++ b/tests/registries/test_operator_resources.py @@ -1,27 +1,26 @@ import collections +from unittest.mock import Mock from kopf.reactor.registries import OperatorRegistry from kopf.structs.resources import Resource -# Used in the tests. Must be global-scoped, or its qualname will be affected. -def some_fn(): - pass +def test_resources(): + handler = Mock() + resource1 = Resource('group1', 'version1', 'plural1') + resource2 = Resource('group2', 'version2', 'plural2') -def test_resources(): registry = OperatorRegistry() - registry.register_resource_watching_handler('group1', 'version1', 'plural1', some_fn) - registry.register_resource_changing_handler('group2', 'version2', 'plural2', some_fn) - registry.register_resource_watching_handler('group2', 'version2', 'plural2', some_fn) - registry.register_resource_changing_handler('group1', 'version1', 'plural1', some_fn) + registry.resource_watching_handlers[resource1].append(handler) + registry.resource_changing_handlers[resource2].append(handler) + registry.resource_watching_handlers[resource2].append(handler) + registry.resource_changing_handlers[resource1].append(handler) resources = registry.resources assert isinstance(resources, collections.abc.Collection) assert len(resources) == 2 - resource1 = Resource('group1', 'version1', 'plural1') - resource2 = Resource('group2', 'version2', 'plural2') assert resource1 in resources assert resource2 in resources diff --git a/tests/registries/test_requires_finalizer.py b/tests/registries/test_requires_finalizer.py index 10d77a24..b75b420b 100644 --- a/tests/registries/test_requires_finalizer.py +++ b/tests/registries/test_requires_finalizer.py @@ -40,7 +40,7 @@ def test_requires_finalizer_deletion_handler(optional, expected): def fn(**_): pass - requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + requires_finalizer = registry.resource_changing_handlers[resource].requires_finalizer(CAUSE) assert requires_finalizer == expected @@ -62,7 +62,7 @@ def fn1(**_): def fn2(**_): pass - requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + requires_finalizer = registry.resource_changing_handlers[resource].requires_finalizer(CAUSE) assert requires_finalizer == expected @@ -75,7 +75,7 @@ def test_requires_finalizer_no_deletion_handler(): def fn1(**_): pass - requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + requires_finalizer = registry.resource_changing_handlers[resource].requires_finalizer(CAUSE) assert requires_finalizer is False @@ -97,7 +97,7 @@ def test_requires_finalizer_deletion_handler_matches_labels(labels, optional, ex def fn(**_): pass - requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + requires_finalizer = registry.resource_changing_handlers[resource].requires_finalizer(CAUSE) assert requires_finalizer == expected @@ -119,7 +119,7 @@ def test_requires_finalizer_deletion_handler_mismatches_labels(labels, optional, def fn(**_): pass - requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + requires_finalizer = registry.resource_changing_handlers[resource].requires_finalizer(CAUSE) assert requires_finalizer == expected @@ -141,7 +141,7 @@ def test_requires_finalizer_deletion_handler_matches_annotations(annotations, op def fn(**_): pass - requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + requires_finalizer = registry.resource_changing_handlers[resource].requires_finalizer(CAUSE) assert requires_finalizer == expected @@ -163,5 +163,5 @@ def test_requires_finalizer_deletion_handler_mismatches_annotations(annotations, def fn(**_): pass - requires_finalizer = registry.requires_finalizer(resource=resource, cause=CAUSE) + requires_finalizer = registry.resource_changing_handlers[resource].requires_finalizer(CAUSE) assert requires_finalizer == expected diff --git a/tests/registries/test_resumes_mixed_in.py b/tests/registries/test_resumes_mixed_in.py index bf813e1d..4994b4b1 100644 --- a/tests/registries/test_resumes_mixed_in.py +++ b/tests/registries/test_resumes_mixed_in.py @@ -16,7 +16,7 @@ def test_resumes_ignored_for_non_initial_causes(mocker, reason, deleted): def fn(**_): pass - handlers = registry.get_resource_changing_handlers(cause) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert len(handlers) == 0 @@ -30,7 +30,7 @@ def test_resumes_selected_for_initial_non_deletions(mocker, reason): def fn(**_): pass - handlers = registry.get_resource_changing_handlers(cause) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn @@ -45,7 +45,7 @@ def test_resumes_ignored_for_initial_deletions_by_default(mocker, reason): def fn(**_): pass - handlers = registry.get_resource_changing_handlers(cause) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert len(handlers) == 0 @@ -59,6 +59,6 @@ def test_resumes_selected_for_initial_deletions_when_explicitly_marked(mocker, r def fn(**_): pass - handlers = registry.get_resource_changing_handlers(cause) + handlers = registry.resource_changing_handlers[resource].get_handlers(cause) assert len(handlers) == 1 assert handlers[0].fn is fn diff --git a/tests/registries/test_subhandlers_ids.py b/tests/registries/test_subhandlers_ids.py new file mode 100644 index 00000000..18990649 --- /dev/null +++ b/tests/registries/test_subhandlers_ids.py @@ -0,0 +1,36 @@ +import kopf +from kopf.reactor.handling import handler_var +from kopf.reactor.invocation import context + + +# Used in the tests. Must be global-scoped, or its qualname will be affected. +def child_fn(**_): + pass + + +def test_with_no_parent( + mocker, resource_registry_cls): + + registry = resource_registry_cls() + + with context([(handler_var, None)]): + kopf.on.this(registry=registry)(child_fn) + + handlers = registry.get_handlers(mocker.MagicMock()) + assert len(handlers) == 1 + assert handlers[0].fn is child_fn + assert handlers[0].id == 'child_fn' + + +def test_with_parent( + mocker, parent_handler, resource_registry_cls): + + registry = resource_registry_cls() + + with context([(handler_var, parent_handler)]): + kopf.on.this(registry=registry)(child_fn) + + handlers = registry.get_handlers(mocker.MagicMock()) + assert len(handlers) == 1 + assert handlers[0].fn is child_fn + assert handlers[0].id == 'parent_fn/child_fn' diff --git a/tests/test_liveness.py b/tests/test_liveness.py index 89970819..9db4b565 100644 --- a/tests/test_liveness.py +++ b/tests/test_liveness.py @@ -5,6 +5,7 @@ from kopf.engines.probing import health_reporter from kopf.reactor.causation import Activity +from kopf.reactor.handlers import ActivityHandler from kopf.reactor.registries import OperatorRegistry @@ -54,8 +55,14 @@ def fn1(**kwargs): def fn2(**kwargs): return {'y': '200'} - liveness_registry.register_activity_handler(fn=fn1, id='id1', activity=Activity.PROBE) - liveness_registry.register_activity_handler(fn=fn2, id='id2', activity=Activity.PROBE) + liveness_registry.activity_handlers.append(ActivityHandler( + fn=fn1, id='id1', activity=Activity.PROBE, + errors=None, timeout=None, retries=None, backoff=None, cooldown=None, + )) + liveness_registry.activity_handlers.append(ActivityHandler( + fn=fn2, id='id2', activity=Activity.PROBE, + errors=None, timeout=None, retries=None, backoff=None, cooldown=None, + )) async with aiohttp.ClientSession() as session: async with session.get(liveness_url) as response: @@ -72,7 +79,10 @@ def fn1(**kwargs): counter += 1 return {'counter': counter} - liveness_registry.register_activity_handler(fn=fn1, id='id1', activity=Activity.PROBE) + liveness_registry.activity_handlers.append(ActivityHandler( + fn=fn1, id='id1', activity=Activity.PROBE, + errors=None, timeout=None, retries=None, backoff=None, cooldown=None, + )) async with aiohttp.ClientSession() as session: async with session.get(liveness_url) as response: