Skip to content

Interpolation

What Loader(interpolate=...) renders with: a tplx engine through TemplateInterpolator, ${a.b} references through VarsInterpolator, or an object of your own with the Interpolator shape. See Templating for interpolation and template files.

anycfg.Interpolator

Bases: Protocol

Renders template text found inside loaded values.

render is called with every string of the document and decides itself whether the text is a template: text it does not render comes back as it is. scope is the document's top-level names (plus env with inject_env); reading a name that is itself a template renders it first.

render(text, scope)

text rendered; plain text as it is; one expression keeps its type.

Source code in src/anycfg/_templating.py
121
122
def render(self, text: str, scope: _ty.Mapping[str, _ty.Any]) -> _ty.Any:
    """*text* rendered; plain text as it is; one expression keeps its type."""

anycfg.TemplateInterpolator(engine='jinja', *, sandbox=True, strict=False)

Interpolation through a tplx engine; sandboxed by default.

engine is a tplx engine's name or class ("jinja" by default, "liquid", "go", "envsubst", ...). sandbox uses tplx's sandbox: an untrusted value cannot reach Python internals and sees plain data — a mapping or list as itself, any other non-plain value (a date) as its text — and an engine tplx cannot sandbox is refused. strict makes an undefined name raise instead of rendering empty. A template can include no file. Compiled templates are cached per instance.

Raises:

Type Description
MissingDependencyError

Without tplx, or the engine's own extra.

TemplatesDisabledError

Under sandbox, for an engine tplx cannot sandbox (mako, Cheetah, ERB, EPP).

ValueError

For a name no tplx engine has.

Source code in src/anycfg/_templating.py
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
def __init__(
    self,
    engine: "_ty.Union[str, type]" = "jinja",
    *,
    sandbox: bool = True,
    strict: bool = False,
) -> None:
    tplx = _tplx()
    self.engine = engine
    self.sandbox = sandbox
    self.strict = strict
    self._lookup = tplx.Loader(
        [_nowhere()],
        undefined="strict" if strict else "lenient",
        sandbox=sandbox,
    ).lookup()
    failure: "_ty.Optional[Exception]" = None
    try:
        # Fails now, not once per value, when the engine's library is missing.
        probe = self._lookup.compile("", engine=engine)
    except tplx.EngineUnavailableError as error:
        hint = (
            '; Jinja comes with the "templates" extra: '
            'pip install "anycfg[templates]"'
            if _engine_name(engine) in ("jinja", "j2", "jinja2")
            else ""
        )
        failure = MissingDependencyError(f"{error}{hint}")
    except tplx.SandboxError:
        failure = TemplatesDisabledError(
            f"refusing to interpolate with {_engine_name(engine)}: the engine "
            "cannot be sandboxed, and the policy's sandbox is on"
        )
    if failure is not None:
        raise failure
    #: The tplx engine class *engine* names.
    self.engine_class: type = type(probe)

engine = engine instance-attribute

engine_class = type(probe) instance-attribute

sandbox = sandbox instance-attribute

strict = strict instance-attribute

render(text, scope)

tplx's eval: plain text back as it is, one expression with its type, anything else rendered to text. The engine is the only syntax check, and the template sees only the names it reads.

Source code in src/anycfg/_templating.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
def render(self, text: str, scope: _ty.Mapping[str, _ty.Any]) -> _ty.Any:
    """tplx's ``eval``: plain text back as it is, one expression with its
    type, anything else rendered to text. The engine is the only syntax
    check, and the template sees only the names it reads."""
    tplx = _tplx()
    names = _ReadNames(scope, self._lookup, text, self.engine_class)
    try:
        return _plain(self._lookup.eval(text, names, engine=self.engine_class))
    except tplx.TplxError as error:
        root = _root(error)
        if isinstance(root, ConfigError):
            # A name it read failed to render (a cycle, say): that error.
            raise root from None
        raise _template_error(error) from root

anycfg.VarsInterpolator(*, strict=False)

${name} and ${a.b.0} references; no expressions, no extra needed.

A reference is a dotted path into the document: each step a mapping key or a list index. A value that is exactly one reference keeps the referenced value's type; inside other text it is written with str (None as nothing). $${ writes a literal ${. An undefined reference renders empty (None alone), or raises TemplateError when strict. It runs no code and reads no attribute, so it needs no sandbox.

Source code in src/anycfg/_templating.py
268
269
def __init__(self, *, strict: bool = False) -> None:
    self.strict = strict

engine = 'vars' class-attribute instance-attribute

strict = strict instance-attribute

render(text, scope)

Source code in src/anycfg/_templating.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def render(self, text: str, scope: _ty.Mapping[str, _ty.Any]) -> _ty.Any:
    if "${" not in text:
        return text
    whole = _VAR.fullmatch(text.rstrip("\r\n"))
    if whole is not None and whole.group(1) is not None:
        return self._lookup(whole.group(1), scope)

    def substitute(match: "re.Match[str]") -> str:
        if match.group(1) is None:
            return "${"
        value = self._lookup(match.group(1), scope)
        return "" if value is None else str(value)

    return _VAR.sub(substitute, text)