File: _access.py

package info (click to toggle)
ansible-core 2.19.0~beta6-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 32,628 kB
  • sloc: python: 180,313; cs: 4,929; sh: 4,601; xml: 34; makefile: 21
file content (86 lines) | stat: -rw-r--r-- 3,467 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
from __future__ import annotations

import abc
import typing as t

from contextvars import ContextVar

from ansible.module_utils._internal._datatag import AnsibleTagHelper


class NotifiableAccessContextBase(metaclass=abc.ABCMeta):
    """Base class for a context manager that, when active, receives notification of managed access for types/tags in which it has registered an interest."""

    _type_interest: t.FrozenSet[type] = frozenset()
    """Set of types (including tag types) for which this context will be notified upon access."""

    _mask: t.ClassVar[bool] = False
    """When true, only the innermost (most recently created) context of this type will be notified."""

    def __enter__(self):
        # noinspection PyProtectedMember
        AnsibleAccessContext.current()._register_interest(self)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
        # noinspection PyProtectedMember
        AnsibleAccessContext.current()._unregister_interest(self)
        return None

    @abc.abstractmethod
    def _notify(self, o: t.Any) -> t.Any:
        """Derived classes implement custom notification behavior when a registered type or tag is accessed."""


class AnsibleAccessContext:
    """
    Broker object for managed access registration and notification.
    Each thread or other logical callstack has a dedicated `AnsibleAccessContext` object with which `NotifiableAccessContext` objects can register interest.
    When a managed access occurs on an object, each active `NotifiableAccessContext` within the current callstack that has registered interest in that
    object's type or a tag present on it will be notified.
    """

    _contextvar: t.ClassVar[ContextVar[AnsibleAccessContext]] = ContextVar('AnsibleAccessContext')

    @staticmethod
    def current() -> AnsibleAccessContext:
        """Creates or retrieves an `AnsibleAccessContext` for the current logical callstack."""
        try:
            ctx: AnsibleAccessContext = AnsibleAccessContext._contextvar.get()
        except LookupError:
            # didn't exist; create it
            ctx = AnsibleAccessContext()
            AnsibleAccessContext._contextvar.set(ctx)  # we ignore the token, since this should live for the life of the thread/async ctx

        return ctx

    def __init__(self) -> None:
        self._notify_contexts: list[NotifiableAccessContextBase] = []

    def _register_interest(self, context: NotifiableAccessContextBase) -> None:
        self._notify_contexts.append(context)

    def _unregister_interest(self, context: NotifiableAccessContextBase) -> None:
        ctx = self._notify_contexts.pop()

        if ctx is not context:
            raise RuntimeError(f'Out-of-order context deactivation detected. Found {ctx} instead of {context}.')

    def access(self, value: t.Any) -> None:
        """Notify all contexts which have registered interest in the given value that it is being accessed."""
        if not self._notify_contexts:
            return

        value_types = AnsibleTagHelper.tag_types(value) | frozenset((type(value),))
        masked: set[type] = set()

        for ctx in reversed(self._notify_contexts):
            if ctx._mask:
                if (ctx_type := type(ctx)) in masked:
                    continue

                masked.add(ctx_type)

            # noinspection PyProtectedMember
            if ctx._type_interest.intersection(value_types):
                ctx._notify(value)