Source code for pyfcstm.utils.jinja2

"""
Jinja2 environment augmentation utilities.

This module provides helpers for enriching a :class:`jinja2.Environment` with
Python built-ins, text-processing filters, and selected operating system
environment variables. It is intended to simplify template authoring by making
common Python functions available as filters, tests, and globals, while also
adding project-specific text utilities.

The module contains the following public functions:

* :func:`add_builtins_to_env` - Register Python built-ins as filters, tests, and globals
* :func:`add_settings_for_env` - Apply built-ins plus additional filters and globals

.. note::
   The added filters and globals are only attached to the environment instance
   passed to the functions and do not affect other environments.

Example::

    >>> import jinja2
    >>> from pyfcstm.utils.jinja2 import add_settings_for_env
    >>> env = add_settings_for_env(jinja2.Environment())
    >>> template = env.from_string("{{ 'Hello World'|to_identifier }}")
    >>> template.render()
    'Hello_World'
"""

import builtins
import inspect
import os
import textwrap

import jinja2

from .text import normalize, to_identifier, to_c_identifier


[docs] def add_builtins_to_env(env: jinja2.Environment) -> jinja2.Environment: """ Mount Python built-in functions to a Jinja2 environment. This function registers Python built-ins to the provided environment as: - **Filters**: Callable built-ins are added as filters when no naming conflict exists. - **Tests**: Common boolean checks are added as tests, using a simplified name for functions beginning with ``is`` (e.g., ``isinstance`` becomes the ``instance`` test). - **Globals**: All non-conflicting built-ins are added to the global namespace. In addition to these automatic registrations, this function always injects several convenience filters, even if they overwrite existing names in the environment: - ``str``: :class:`str` - ``set``: :class:`set` - ``dict``: :class:`dict` - ``keys``: ``lambda x: x.keys()`` - ``values``: ``lambda x: x.values()`` - ``enumerate``: :func:`enumerate` - ``reversed``: :func:`reversed` - ``filter``: ``lambda x, y: filter(y, x)`` :param env: A Jinja2 environment instance to modify. :type env: jinja2.Environment :return: The same Jinja2 environment with built-ins mounted. :rtype: jinja2.Environment .. warning:: This function may override pre-existing filters named ``str``, ``set``, ``dict``, ``keys``, ``values``, ``enumerate``, ``reversed``, and ``filter``. Example:: >>> import jinja2 >>> env = add_builtins_to_env(jinja2.Environment()) >>> tmpl = env.from_string("{{ [1, 2, 3]|reversed|list }}") >>> tmpl.render() '[3, 2, 1]' """ existing_filters = set(env.filters.keys()) existing_tests = set(env.tests.keys()) existing_globals = set(env.globals.keys()) builtin_items = [(name, obj) for name, obj in inspect.getmembers(builtins) if not name.startswith('_')] for name, func in builtin_items: if not callable(func): continue is_filter_candidate = ( inspect.isfunction(func) or inspect.isbuiltin(func) ) is_test_candidate = ( name.startswith('is') or name in ('all', 'any', 'callable', 'hasattr') ) filter_name = name if is_filter_candidate and filter_name not in existing_filters: env.filters[filter_name] = func env.filters['str'] = str env.filters['set'] = set env.filters['dict'] = dict env.filters['keys'] = lambda x: x.keys() env.filters['values'] = lambda x: x.values() env.filters['enumerate'] = enumerate env.filters['reversed'] = reversed env.filters['filter'] = lambda x, y: filter(y, x) test_name = name if name.startswith('is'): test_name = name[2:].lower() if is_test_candidate and test_name not in existing_tests: env.tests[test_name] = func if name not in existing_globals: env.globals[name] = func return env
[docs] def add_settings_for_env(env: jinja2.Environment) -> jinja2.Environment: """ Add built-ins, text filters, and environment variables to a Jinja2 environment. This function enhances a Jinja2 environment by applying the following steps: 1. Register Python built-ins via :func:`add_builtins_to_env` 2. Add text-processing filters: - ``normalize``: :func:`pyfcstm.utils.text.normalize` - ``to_identifier``: :func:`pyfcstm.utils.text.to_identifier` - ``to_c_identifier``: :func:`pyfcstm.utils.text.to_c_identifier` 3. Add a global helper: - ``indent``: :func:`textwrap.indent` 4. Add operating system environment variables as globals (only if the name does not already exist in the environment). :param env: The Jinja2 environment to enhance. :type env: jinja2.Environment :return: The enhanced Jinja2 environment. :rtype: jinja2.Environment .. note:: Environment variables are only added if their names do not already exist in the environment's global namespace. Example:: >>> import jinja2 >>> env = add_settings_for_env(jinja2.Environment()) >>> template = env.from_string("{{ 'Hello World'|normalize }}") >>> template.render() 'Hello_World' """ env = add_builtins_to_env(env) env.filters['normalize'] = normalize env.filters['to_identifier'] = to_identifier env.filters['to_c_identifier'] = to_c_identifier env.globals['indent'] = textwrap.indent for key, value in os.environ.items(): if key not in env.globals: env.globals[key] = value return env