File: _display_utils.py

package info (click to toggle)
ansible-core 2.19.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 32,944 kB
  • sloc: python: 181,408; cs: 4,929; sh: 4,661; xml: 34; makefile: 21
file content (145 lines) | stat: -rw-r--r-- 5,651 bytes parent folder | download
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
from __future__ import annotations

import dataclasses

from ansible.module_utils._internal import _ambient_context, _messages
from . import _event_formatting


class DeferredWarningContext(_ambient_context.AmbientContextBase):
    """
    Calls to `Display.warning()` and `Display.deprecated()` within this context will cause the resulting warnings to be captured and not displayed.
    The intended use is for task-initiated warnings to be recorded with the task result, which makes them visible to registered results, callbacks, etc.
    The active display callback is responsible for communicating any warnings to the user.
    """

    # DTFIX-FUTURE: once we start implementing nested scoped contexts for our own bookkeeping, this should be an interface facade that forwards to the nearest
    #               context that actually implements the warnings collection capability

    def __init__(self, *, variables: dict[str, object]) -> None:
        self._variables = variables  # DTFIX-FUTURE: move this to an AmbientContext-derived TaskContext (once it exists)
        self._deprecation_warnings: list[_messages.DeprecationSummary] = []
        self._warnings: list[_messages.WarningSummary] = []
        self._seen: set[_messages.WarningSummary] = set()

    def capture(self, warning: _messages.WarningSummary) -> None:
        """Add the warning/deprecation to the context if it has not already been seen by this context."""
        if warning in self._seen:
            return

        self._seen.add(warning)

        if isinstance(warning, _messages.DeprecationSummary):
            self._deprecation_warnings.append(warning)
        else:
            self._warnings.append(warning)

    def get_warnings(self) -> list[_messages.WarningSummary]:
        """Return a list of the captured non-deprecation warnings."""
        # DTFIX-FUTURE: return a read-only list proxy instead
        return self._warnings

    def get_deprecation_warnings(self) -> list[_messages.DeprecationSummary]:
        """Return a list of the captured deprecation warnings."""
        # DTFIX-FUTURE: return a read-only list proxy instead
        return self._deprecation_warnings


def format_message(summary: _messages.SummaryBase, include_traceback: bool) -> str:
    if isinstance(summary, _messages.DeprecationSummary):
        deprecation_message = get_deprecation_message_with_plugin_info(
            msg=summary.event.msg,
            version=summary.version,
            date=summary.date,
            deprecator=summary.deprecator,
        )

        event = dataclasses.replace(summary.event, msg=deprecation_message)
    else:
        event = summary.event

    return _event_formatting.format_event(event, include_traceback)


def get_deprecation_message_with_plugin_info(
    *,
    msg: str,
    version: str | None,
    removed: bool = False,
    date: str | None,
    deprecator: _messages.PluginInfo | None,
) -> str:
    """Internal use only. Return a deprecation message and help text for display."""
    # DTFIX-FUTURE: the logic for omitting date/version doesn't apply to the payload, so it shows up in vars in some cases when it should not

    if removed:
        removal_fragment = 'This feature was removed'
    else:
        removal_fragment = 'This feature will be removed'

    if not deprecator or not deprecator.type:
        # indeterminate has no resolved_name or type
        # collections have a resolved_name but no type
        collection = deprecator.resolved_name if deprecator else None
        plugin_fragment = ''
    elif deprecator.resolved_name == 'ansible.builtin':
        # core deprecations from base classes (the API) have no plugin name, only 'ansible.builtin'
        plugin_type_name = str(deprecator.type) if deprecator.type is _messages.PluginType.MODULE else f'{deprecator.type} plugin'

        collection = deprecator.resolved_name
        plugin_fragment = f'the {plugin_type_name} API'
    else:
        parts = deprecator.resolved_name.split('.')
        plugin_name = parts[-1]
        plugin_type_name = str(deprecator.type) if deprecator.type is _messages.PluginType.MODULE else f'{deprecator.type} plugin'

        collection = '.'.join(parts[:2]) if len(parts) > 2 else None
        plugin_fragment = f'{plugin_type_name} {plugin_name!r}'

    if collection and plugin_fragment:
        plugin_fragment += ' in'

    if collection == 'ansible.builtin':
        collection_fragment = 'ansible-core'
    elif collection:
        collection_fragment = f'collection {collection!r}'
    else:
        collection_fragment = ''

    if not collection:
        when_fragment = 'in the future' if not removed else ''
    elif date:
        when_fragment = f'in a release after {date}'
    elif version:
        when_fragment = f'version {version}'
    else:
        when_fragment = 'in a future release' if not removed else ''

    if plugin_fragment or collection_fragment:
        from_fragment = 'from'
    else:
        from_fragment = ''

    deprecation_msg = ' '.join(f for f in [removal_fragment, from_fragment, plugin_fragment, collection_fragment, when_fragment] if f) + '.'

    return join_sentences(msg, deprecation_msg)


def join_sentences(first: str | None, second: str | None) -> str:
    """Join two sentences together."""
    first = (first or '').strip()
    second = (second or '').strip()

    if first and first[-1] not in ('!', '?', '.'):
        first += '.'

    if second and second[-1] not in ('!', '?', '.'):
        second += '.'

    if first and not second:
        return first

    if not first and second:
        return second

    return ' '.join((first, second))