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
|