Skip to content

Latest commit

 

History

15 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

jinject

jinject is a Jinja2 layout preprocessor designed to enable inclusion of templates with Python packages and dynamic injection of Python utility functions at runtime.

Installation

In order to use jinject with compatible installed system packages, you can run:

uv tool install ./jinject

This works well for packages installed to your base global environment (such as via apt, pip --break-system-packages, or a sourced ROS 2 workspace).

Creating Jinjectable Python Packages

Packages can expose both templates and executable Python helper macros directly to the jinject processor via standard entry points.

Registering template directories

Registered templates must reside inside your Python package directory structure (e.g., src/my_asset_package/templates/) and added as a "jinject.templates" entry-point. Here is an example of a pyproject.toml:

[project]
name = "my_asset_package"
version = "1.0.0"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project.entry-points."jinject.templates"]
# Maps the prefix "core_layouts/" to the package's templates/ folder
core_layouts = "my_asset_package:templates"

# Ensure Hatchling sweeps up non-Python (e.g. *.jinja) files
[tool.hatch.build.targets.wheel]
packages = ["src/my_asset_package"]

Downstream files can now import these assets explicitly using the defined namespace key:
{% import 'core_layouts/base.html' as theme %}

Registering functions

Functions must behave as pure interfaces—accepting standard primitives (str, int, float) and returning structures Jinja can cleanly process or unpack (such as a generic dict or list or string).

# located inside: src/my_asset_package/core.py

def metadata_combiner(prefix: str, count: int) -> dict:
    """Computes layout tag strings and parity flags."""
    return {
        "id_tag": f"{prefix}_{count}",
        "is_even": count % 2 == 0
    }

Map this function path under the jinject.plugins entry point namespace inside your pyproject.toml:

[project.entry-points."jinject.plugins"]
get_meta = "my_asset_package.core:metadata_combiner"

Register the Entry Point

Map the python method path within the distribution pyproject.toml configuration block:

[project.entry-points."jinject.plugins"]
get_meta = "my_asset_package.core:metadata_combiner"

Writing Templates

Python functions in the template

jinject automatically wires get_meta() into the global execution block, making it instantly executable within layout templates:

{# Inside template context #}
{% set layout_state = my_asset_package.get_meta("node", 4) %}
<div id="{{ layout_state.id_tag }}">Parity Event Status: {{ layout_state.is_even }}</div>

Shell Command Execution in the template

jinject provides built-in shell() and shell_json() functions available in all templates. These allow calling external command-line tools during template rendering — no entry points required.

shell(*args, **kwargs)str

Executes a command and returns its stdout as a stripped string. Raises on non-zero exit.

Positional arguments form the command tokens. Keyword arguments are appended as --key=value flags (underscores in kwarg names become hyphens). Boolean True kwargs emit the flag alone; False kwargs are omitted.

{# Simple command #}
{{ shell("echo", "hello") }}

{# Command with flags via kwargs #}
{% set version = shell("uv", "run", "my_tool", config="/etc/app.yaml") %}
{# Equivalent to: uv run my_tool --config=/etc/app.yaml #}

{# Boolean flags #}
{{ shell("ros2", "param", "get", "/node", "use_sim_time", verbose=True) }}
{# Equivalent to: ros2 param get /node use_sim_time --verbose #}

shell_json(*args, **kwargs)dict | list | ...

Same calling convention as shell(), but parses stdout as JSON and returns the resulting Python object for structured access in templates.

{% set info = shell_json("uv", "run", "get_sensor_config", output_format="json") %}
<sensor name="{{ info.name }}" rate="{{ info.rate_hz }}" />

Error Handling

  • Non-zero exit code: Raises subprocess.CalledProcessError, halting template rendering with a clear error message.
  • Invalid JSON (for shell_json): Raises json.JSONDecodeError.
  • Command not found: Raises FileNotFoundError.

These are intentionally loud failures — jinject is a build-time tool, and silent corruption is worse than a failed build.

Template Discovery via Environment Variable

For packages that cannot use Python entry points, jinject supports the JINJECT_TEMPLATE_PATH environment variable as a template discovery mechanism.

Format

JINJECT_TEMPLATE_PATH is a path-separator-delimited list (: on Unix, ; on Windows) of entries. Each entry takes one of two forms:

# Explicit namespace mapping
JINJECT_TEMPLATE_PATH="calibration_description=/opt/share/calibration_description/templates"

# Basename inference (directory basename becomes the namespace)
JINJECT_TEMPLATE_PATH="/opt/share/calibration_description"
# → namespace = "calibration_description"

Multiple entries can be combined:

export JINJECT_TEMPLATE_PATH="calibration_description=/opt/share/calibration_description/templates:/opt/share/robot_description"

Behavior

  • Each entry registers a directory under a namespace. Templates are then resolvable as namespace/path/to/template.jinja.
  • Non-existent directories are silently skipped.
  • If the same namespace appears multiple times, the first entry wins.
  • Precedence: Environment variable paths are searched before entry-point-registered packages. If both provide the same namespace, the env var wins.
  • Entry points remain fully supported — this is additive.

Example: CMake-Installed Templates

A CMake package installs templates to a known prefix:

install(DIRECTORY templates/ DESTINATION share/${PROJECT_NAME}/templates)

At runtime, set the path to make them discoverable:

export JINJECT_TEMPLATE_PATH="calibration_description=/opt/ros/install/share/calibration_description/templates"

Then any Jinja template can import from that namespace:

{% from "calibration_description/velo2cam.sdf.jinja" import velo2cam_model %}
{{ velo2cam_model("lidar_front") }}

Background

jinject was originally created to generate highly parameterized simulation environments for Gazebo. The workflow maps directly to a modular robotic ecosystem:

  • World Containers: ROS 2 packages define overall Gazebo world environments as top-level Jinja files.
  • Asset Containers: Independent hardware/robot packages bundle parameterized SDF templates alongside python procedural helpers (e.g., generating dynamic collision hulls, specific wheel configurations, or customized meshes).

About

A jinja-based template preprocessor with runtime process execution

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages