Source code for hbllmutils.template.env

"""
This module provides utilities for enhancing Jinja2 environments with Python builtins and additional settings.

It includes functions to add Python built-in functions to Jinja2 environments as filters, tests, and globals,
as well as adding environment variables and custom text processing functions.
"""

import builtins
import inspect
import os
import pathlib
import textwrap

import jinja2
from hbutils.string import plural_word, ordinalize, titleize
from jinja2 import StrictUndefined, Undefined


[docs] def add_builtins_to_env(env: jinja2.Environment) -> jinja2.Environment: """ Mount Python built-in functions to a Jinja2 environment. This function adds Python's built-in functions to the specified Jinja2 environment as filters, tests, or global functions based on their characteristics. Functions are categorized and mounted appropriately: - Filters: Functions that can process data (e.g., str, len, sorted) - Tests: Boolean-returning functions for conditional checks (e.g., isinstance, callable) - Globals: All built-in functions available as global functions in templates :param env: A Jinja2 Environment instance to be enhanced :type env: jinja2.Environment :return: The Jinja2 Environment with Python builtins mounted :rtype: jinja2.Environment Example:: >>> env = jinja2.Environment() >>> env = add_builtins_to_env(env) >>> # Now Python builtins like len, str, etc. are available in templates >>> template = env.from_string("{{ items | len }}") >>> template.render(items=[1, 2, 3]) '3' """ # Existing built-in filters, tests and global functions in Jinja2 existing_filters = set(env.filters.keys()) existing_tests = set(env.tests.keys()) existing_globals = set(env.globals.keys()) # Get all Python built-in functions builtin_items = [(name, obj) for name, obj in inspect.getmembers(builtins) if not name.startswith('_')] # Exclude internal functions starting with underscore # Categorize functions for appropriate mounting positions for name, func in builtin_items: # Skip non-function objects if not callable(func): continue # Determine if the function is suitable as a filter is_filter_candidate = ( # Filters typically accept one main argument and may have other optional parameters inspect.isfunction(func) or inspect.isbuiltin(func) ) # Determine if the function is suitable as a tester is_test_candidate = ( # Test functions typically return boolean values, like isinstance, issubclass, etc. name.startswith('is') or name in ('all', 'any', 'callable', 'hasattr') ) # Mount as a filter (if suitable and no conflict) 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) # Mount as a tester (if suitable and no conflict) test_name = name if name.startswith('is'): # For functions starting with 'is', the prefix can be removed as the tester name test_name = name[2:].lower() if is_test_candidate and test_name not in existing_tests: env.tests[test_name] = func # Mount as a global function (if no conflict) if name not in existing_globals: env.globals[name] = func return env
[docs] def add_settings_for_env(env: jinja2.Environment) -> jinja2.Environment: """ Add additional settings and functions to a Jinja2 environment. This function enhances a Jinja2 environment by: 1. Adding Python built-in functions via :func:`add_builtins_to_env` 2. Adding custom text processing filters: - indent: Text indentation using textwrap.indent - plural: Pluralize words using hbutils.string.plural_word - ordinalize: Convert numbers to ordinal form (1st, 2nd, etc.) - titleize: Convert text to title case - read_file_text: Read text content from a file path 3. Adding environment variables as global variables for template access :param env: The Jinja2 environment to enhance :type env: jinja2.Environment :return: The enhanced Jinja2 environment :rtype: jinja2.Environment Example:: >>> env = jinja2.Environment() >>> env = add_settings_for_env(env) >>> # Now the environment has additional filters and globals >>> template = env.from_string("{{ 'word' | plural }}") >>> template.render() 'words' """ env = add_builtins_to_env(env) env.globals['indent'] = env.filters['indent'] = textwrap.indent env.globals['plural_word'] = env.filters['plural'] = plural_word env.globals['ordinalize'] = env.filters['ordinalize'] = ordinalize env.globals['titleize'] = env.filters['titleize'] = titleize env.globals['read_file_text'] = env.filters['read_file_text'] = lambda x: pathlib.Path(x).read_text() for key, value in os.environ.items(): if key not in env.globals: env.globals[key] = value return env
[docs] def create_env(strict_undefined: bool = True) -> jinja2.Environment: """ Create a new Jinja2 environment with enhanced settings. This function creates a new Jinja2 environment and applies all enhancements including Python builtins, custom filters, and environment variables via :func:`add_settings_for_env`. This is a convenience function that provides a fully configured environment ready for template rendering. :param strict_undefined: If True, use StrictUndefined to raise errors for undefined variables; if False, use default Undefined behavior :type strict_undefined: bool :return: A fully configured Jinja2 environment with all enhancements :rtype: jinja2.Environment Example:: >>> env = create_env() >>> # Use the environment to render templates with enhanced features >>> template = env.from_string("{{ 'hello' | upper }}") >>> template.render() 'HELLO' >>> template = env.from_string("{{ 3 | ordinalize }}") >>> template.render() '3rd' """ env = jinja2.Environment(undefined=StrictUndefined if strict_undefined else Undefined) env = add_settings_for_env(env) return env