File: __init__.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 (413 lines) | stat: -rw-r--r-- 17,387 bytes parent folder | download | duplicates (3)
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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
from __future__ import annotations as _annotations

import contextlib as _contextlib
import io as _io
import os as _os
import typing as _t

from jinja2 import environment as _environment

from ansible import _internal
from ansible import errors as _errors
from ansible._internal._datatag import _tags, _wrappers
from ansible._internal._templating import _jinja_bits, _engine, _jinja_common, _template_vars

from ansible.module_utils import datatag as _module_utils_datatag
from ansible.utils.display import Display as _Display

if _t.TYPE_CHECKING:  # pragma: nocover
    import collections as _collections

    from ansible.parsing import dataloader as _dataloader

    _VariableContainer = dict[str, _t.Any] | _collections.ChainMap[str, _t.Any]


_display: _t.Final[_Display] = _Display()
_UNSET = _t.cast(_t.Any, object())
_TTrustable = _t.TypeVar('_TTrustable', bound=str | _io.IOBase | _t.TextIO | _t.BinaryIO)
_TRUSTABLE_TYPES = (str, _io.IOBase)

AnsibleUndefined = _jinja_common.UndefinedMarker
"""Backwards compatibility alias for UndefinedMarker."""


class Templar:
    """Primary public API container for Ansible's template engine."""

    def __init__(
        self,
        loader: _dataloader.DataLoader | None = None,
        variables: _VariableContainer | None = None,
    ) -> None:
        self._engine = _engine.TemplateEngine(loader=loader, variables=variables)
        self._overrides = _jinja_bits.TemplateOverrides.DEFAULT

    @classmethod
    @_internal.experimental
    def _from_template_engine(cls, engine: _engine.TemplateEngine) -> _t.Self:
        """
        EXPERIMENTAL: For internal use within ansible-core only.
        Create a `Templar` instance from the given `TemplateEngine` instance.
        """
        templar = object.__new__(cls)
        templar._engine = engine.copy()
        templar._overrides = _jinja_bits.TemplateOverrides.DEFAULT

        return templar

    def resolve_variable_expression(
        self,
        expression: str,
        *,
        local_variables: dict[str, _t.Any] | None = None,
    ) -> _t.Any:
        """
        Resolve a potentially untrusted string variable expression consisting only of valid identifiers, integers, dots, and indexing containing these.
        Optional local variables may be provided, which can only be referenced directly by the given expression.
        Valid: x, x.y, x[y].z, x[1], 1, x[y.z]
        Error: 'x', x['y'], q('env')
        """
        return self._engine.resolve_variable_expression(expression, local_variables=local_variables)

    def evaluate_expression(
        self,
        expression: str,
        *,
        local_variables: dict[str, _t.Any] | None = None,
        escape_backslashes: bool = True,
    ) -> _t.Any:
        """
        Evaluate a trusted string expression and return its result.
        Optional local variables may be provided, which can only be referenced directly by the given expression.
        """
        return self._engine.evaluate_expression(expression, local_variables=local_variables, escape_backslashes=escape_backslashes)

    def evaluate_conditional(self, conditional: str | bool) -> bool:
        """
        Evaluate a trusted string expression or boolean and return its boolean result. A non-boolean result will raise `AnsibleBrokenConditionalError`.
        The ALLOW_BROKEN_CONDITIONALS configuration option can temporarily relax this requirement, allowing truthy conditionals to succeed.
        The ALLOW_EMBEDDED_TEMPLATES configuration option can temporarily enable inline Jinja template delimiter support (e.g., {{ }}, {% %}).
        """
        return self._engine.evaluate_conditional(conditional)

    @property
    def basedir(self) -> str:
        """The basedir from DataLoader."""
        # DTFIX-FUTURE: come up with a better way to handle this so it can be deprecated
        return self._engine.basedir

    @property
    def available_variables(self) -> _VariableContainer:
        """Available variables this instance will use when templating."""
        return self._engine.available_variables

    @available_variables.setter
    def available_variables(self, variables: _VariableContainer) -> None:
        self._engine.available_variables = variables

    @property
    def _available_variables(self) -> _VariableContainer:
        """Deprecated. Use `available_variables` instead."""
        # Commonly abused by numerous collection lookup plugins and the Ceph Ansible `config_template` action.
        _display.deprecated(
            msg='Direct access to the `_available_variables` internal attribute is deprecated.',
            help_text='Use `available_variables` instead.',
            version='2.23',
        )

        return self.available_variables

    @property
    def _loader(self) -> _dataloader.DataLoader:
        """Deprecated. Use `copy_with_new_env` to create a new instance."""
        # Abused by cloud.common, community.general and felixfontein.tools collections to create a new Templar instance.
        _display.deprecated(
            msg='Direct access to the `_loader` internal attribute is deprecated.',
            help_text='Use `copy_with_new_env` to create a new instance.',
            version='2.23',
        )

        return self._engine._loader

    @property
    def environment(self) -> _environment.Environment:
        """Deprecated."""
        _display.deprecated(
            msg='Direct access to the `environment` attribute is deprecated.',
            help_text='Consider using `copy_with_new_env` or passing `overrides` to `template`.',
            version='2.23',
        )

        return self._engine.environment

    def copy_with_new_env(
        self,
        *,
        searchpath: str | _os.PathLike | _t.Sequence[str | _os.PathLike] | None = None,
        available_variables: _VariableContainer | None = None,
        **context_overrides: _t.Any,
    ) -> Templar:
        """Return a new templar based on the current one with customizations applied."""
        if context_overrides.pop('environment_class', _UNSET) is not _UNSET:
            _display.deprecated(
                msg="The `environment_class` argument is ignored.",
                version='2.23',
            )

        if context_overrides:
            _display.deprecated(
                msg='Passing Jinja environment overrides to `copy_with_new_env` is deprecated.',
                help_text='Pass Jinja environment overrides to individual `template` calls.',
                version='2.23',
            )

        templar = Templar(
            loader=self._engine._loader,
            variables=self._engine._variables if available_variables is None else available_variables,
        )

        # backward compatibility: filter out None values from overrides, even though it is a valid value for some of them
        templar._overrides = self._overrides.merge({key: value for key, value in context_overrides.items() if value is not None})

        if searchpath is not None:
            templar._engine.environment.loader.searchpath = searchpath

        return templar

    @_contextlib.contextmanager
    def set_temporary_context(
        self,
        *,
        searchpath: str | _os.PathLike | _t.Sequence[str | _os.PathLike] | None = None,
        available_variables: _VariableContainer | None = None,
        **context_overrides: _t.Any,
    ) -> _t.Generator[None, None, None]:
        """Context manager used to set temporary templating context, without having to worry about resetting original values afterward."""
        _display.deprecated(
            msg='The `set_temporary_context` method on `Templar` is deprecated.',
            help_text='Use the `copy_with_new_env` method on `Templar` instead.',
            version='2.23',
        )

        targets = dict(
            searchpath=self._engine.environment.loader,
            available_variables=self._engine,
        )

        target_args = dict(
            searchpath=searchpath,
            available_variables=available_variables,
        )

        original: dict[str, _t.Any] = {}
        previous_overrides = self._overrides

        try:
            for key, value in target_args.items():
                if value is not None:
                    target = targets[key]
                    original[key] = getattr(target, key)
                    setattr(target, key, value)

            # backward compatibility: filter out None values from overrides, even though it is a valid value for some of them
            self._overrides = self._overrides.merge({key: value for key, value in context_overrides.items() if value is not None})

            yield
        finally:
            for key, value in original.items():
                setattr(targets[key], key, value)

            self._overrides = previous_overrides

    # noinspection PyUnusedLocal
    def template(
        self,
        variable: _t.Any,
        convert_bare: bool = _UNSET,
        preserve_trailing_newlines: bool = True,
        escape_backslashes: bool = True,
        fail_on_undefined: bool = True,
        overrides: dict[str, _t.Any] | None = None,
        convert_data: bool = _UNSET,
        disable_lookups: bool = _UNSET,
    ) -> _t.Any:
        """Templates (possibly recursively) any given data as input."""
        # DTFIX-FUTURE: offer a public version of TemplateOverrides to support an optional strongly typed `overrides` argument
        if convert_bare is not _UNSET:
            # Skipping a deferred deprecation due to minimal usage outside ansible-core.
            # Use `hasattr(templar, 'evaluate_expression')` to determine if `template` or `evaluate_expression` should be used.
            _display.deprecated(
                msg="Passing `convert_bare` to `template` is deprecated.",
                help_text="Use `evaluate_expression` instead.",
                version="2.23",
            )

            if convert_bare and isinstance(variable, str):
                contains_filters = "|" in variable
                first_part = variable.split("|")[0].split(".")[0].split("[")[0]
                convert_bare = (contains_filters or first_part in self.available_variables) and not self.is_possibly_template(variable, overrides)
            else:
                convert_bare = False
        else:
            convert_bare = False

        if fail_on_undefined is None:
            # The pre-2.19 config fallback is ignored for content portability.
            _display.deprecated(
                msg="Falling back to `True` for `fail_on_undefined`.",
                help_text="Use either `True` or `False` for `fail_on_undefined` when calling `template`.",
                version="2.23",
            )

            fail_on_undefined = True

        if convert_data is not _UNSET:
            # Skipping a deferred deprecation due to minimal usage outside ansible-core.
            # Use `hasattr(templar, 'evaluate_expression')` as a surrogate check to determine if `convert_data` is accepted.
            _display.deprecated(
                msg="Passing `convert_data` to `template` is deprecated.",
                version="2.23",
            )

        if disable_lookups is not _UNSET:
            # Skipping a deferred deprecation due to no known usage outside ansible-core.
            # Use `hasattr(templar, 'evaluate_expression')` as a surrogate check to determine if `disable_lookups` is accepted.
            _display.deprecated(
                msg="Passing `disable_lookups` to `template` is deprecated.",
                version="2.23",
            )

        try:
            if convert_bare:  # pre-2.19 compat
                return self.evaluate_expression(variable, escape_backslashes=escape_backslashes)

            return self._engine.template(
                variable=variable,
                options=_engine.TemplateOptions(
                    preserve_trailing_newlines=preserve_trailing_newlines,
                    escape_backslashes=escape_backslashes,
                    overrides=self._overrides.merge(overrides),
                ),
                mode=_engine.TemplateMode.ALWAYS_FINALIZE,
            )
        except _errors.AnsibleUndefinedVariable:
            if not fail_on_undefined:
                return variable

            raise

    def is_template(self, data: _t.Any) -> bool:
        """
        Evaluate the input data to determine if it contains a template, even if that template is invalid. Containers will be recursively searched.
        Objects subject to template-time transforms that do not yield a template are not considered templates by this method.
        Gating a conditional call to `template` with this method is redundant and inefficient -- request templating unconditionally instead.
        """
        return self._engine.is_template(data, self._overrides)

    def is_possibly_template(
        self,
        data: _t.Any,
        overrides: dict[str, _t.Any] | None = None,
    ) -> bool:
        """
        A lightweight check to determine if the given value is a string that looks like it contains a template, even if that template is invalid.
        Returns `True` if the given value is a string that starts with a Jinja overrides header or if it contains template start strings.
        Gating a conditional call to `template` with this method is redundant and inefficient -- request templating unconditionally instead.
        """
        return isinstance(data, str) and _jinja_bits.is_possibly_template(data, self._overrides.merge(overrides))

    def do_template(
        self,
        data: _t.Any,
        preserve_trailing_newlines: bool = True,
        escape_backslashes: bool = True,
        fail_on_undefined: bool = True,
        overrides: dict[str, _t.Any] | None = None,
        disable_lookups: bool = _UNSET,
        convert_data: bool = _UNSET,
    ) -> _t.Any:
        """Deprecated. Use `template` instead."""
        _display.deprecated(
            msg='The `do_template` method on `Templar` is deprecated.',
            help_text='Use the `template` method on `Templar` instead.',
            version='2.23',
        )

        if not isinstance(data, str):
            return data

        return self.template(
            variable=data,
            preserve_trailing_newlines=preserve_trailing_newlines,
            escape_backslashes=escape_backslashes,
            fail_on_undefined=fail_on_undefined,
            overrides=overrides,
            disable_lookups=disable_lookups,
            convert_data=convert_data,
        )


def generate_ansible_template_vars(
    path: str,
    fullpath: str | None = None,
    dest_path: str | None = None,
) -> dict[str, object]:
    """
    Generate and return a dictionary with variable metadata about the template specified by `fullpath`.
    If `fullpath` is `None`, `path` will be used instead.
    """
    # deprecated description="deprecate `generate_ansible_template_vars`, collections should inline the necessary variables" core_version="2.23"
    return _template_vars.generate_ansible_template_vars(path=path, fullpath=fullpath, dest_path=dest_path, include_ansible_managed=True)


def trust_as_template(value: _TTrustable) -> _TTrustable:
    """
    Returns `value` tagged as trusted for templating.
    Raises a `TypeError` if `value` is not a supported type.
    """
    if isinstance(value, str):
        return _tags.TrustedAsTemplate().tag(value)  # type: ignore[return-value]

    if isinstance(value, _io.IOBase):  # covers TextIO and BinaryIO at runtime, but type checking disagrees
        return _wrappers.TaggedStreamWrapper(value, _tags.TrustedAsTemplate())

    raise TypeError(f"Trust cannot be applied to {_module_utils_datatag.native_type_name(value)}, only to 'str' or 'IOBase'.")


def is_trusted_as_template(value: object) -> bool:
    """
    Returns `True` if `value` is a `str` or `IOBase` marked as trusted for templating, otherwise returns `False`.
    Returns `False` for types which cannot be trusted for templating.
    Containers are not recursed and will always return `False`.
    This function should not be needed for production code, but may be useful in unit tests.
    """
    return isinstance(value, _TRUSTABLE_TYPES) and _tags.TrustedAsTemplate.is_tagged_on(value)


_TCallable = _t.TypeVar('_TCallable', bound=_t.Callable)


def accept_args_markers(plugin: _TCallable) -> _TCallable:
    """
    A decorator to mark a Jinja plugin as capable of handling `Marker` values for its top-level arguments.
    Non-decorated plugin invocation is skipped when a top-level argument is a `Marker`, with the first such value substituted as the plugin result.
    This ensures that only plugins which understand `Marker` instances for top-level arguments will encounter them.
    """
    plugin.accept_args_markers = True

    return plugin


def accept_lazy_markers(plugin: _TCallable) -> _TCallable:
    """
    A decorator to mark a Jinja plugin as capable of handling `Marker` values retrieved from lazy containers.
    Non-decorated plugins will trigger a `MarkerError` exception when attempting to retrieve a `Marker` from a lazy container.
    This ensures that only plugins which understand lazy retrieval of `Marker` instances will encounter them.
    """
    plugin.accept_lazy_markers = True

    return plugin


get_first_marker_arg = _jinja_common.get_first_marker_arg