) -> Self {
+impl<'l> MiddlewareChain<'l> {
+ pub fn new(middlewares: &'l [Middleware]) -> Self {
Self { middlewares }
}
From d6bf8bcbecf4a46819bfebb64cf84d281b76b405 Mon Sep 17 00:00:00 2001
From: FITAHIANA Nomeniavo Joe <24nomeniavo@gmail.com>
Date: Wed, 29 Jul 2026 19:51:17 +0300
Subject: [PATCH 3/6] feat: wraper & registe function to template
- remove cors
- remplace catcher with wrappe function
- allows user to inject function in template engine
- only tera is available
---
Cargo.lock | 19 +-------
Cargo.toml | 3 +-
README.md | 2 +-
build.sh | 2 +-
oxapy/__init__.py | 2 +-
src/lib.rs | 6 +--
src/request.rs | 2 -
src/response.rs | 9 +---
src/templating/minijinja.rs | 65 -------------------------
src/templating/mod.rs | 96 +++++++++++++++++++++++++++----------
src/templating/tera.rs | 46 ------------------
11 files changed, 79 insertions(+), 173 deletions(-)
delete mode 100644 src/templating/minijinja.rs
delete mode 100644 src/templating/tera.rs
diff --git a/Cargo.lock b/Cargo.lock
index 0f5b130..b80038e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1247,12 +1247,6 @@ version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
-[[package]]
-name = "memo-map"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b"
-
[[package]]
name = "micromap"
version = "0.3.0"
@@ -1265,16 +1259,6 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
-[[package]]
-name = "minijinja"
-version = "2.21.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb3d648e68cea56d9858d535ee28f9538404e2dd8cb08ed0bd05dca379477f39"
-dependencies = [
- "memo-map",
- "serde",
-]
-
[[package]]
name = "mio"
version = "1.2.2"
@@ -1485,7 +1469,7 @@ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
[[package]]
name = "oxapy"
-version = "0.9.1"
+version = "0.10.0"
dependencies = [
"ahash",
"ctrlc",
@@ -1497,7 +1481,6 @@ dependencies = [
"jsonschema",
"jsonwebtoken",
"matchit",
- "minijinja",
"multer",
"pyo3",
"pyo3-async-runtimes",
diff --git a/Cargo.toml b/Cargo.toml
index 1fdb745..c30e2ca 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "oxapy"
-version = "0.9.1"
+version = "0.10.0"
edition = "2024"
authors = ["FITAHIANA Nomeniavo Joe <24nomeniavo@gmail.com>"]
repository = "https://github.com/j03-dev/oxapy"
@@ -44,7 +44,6 @@ serde_json = "1.0.145"
jsonschema = { version = "0.46.4", default-features = false }
# Template engines
-minijinja = "2.14.0"
tera = "1.20"
# Utilities
diff --git a/README.md b/README.md
index a928ead..2240c59 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
OxAPY is Python HTTP server library build in Rust - a fast, safe and featureementation.
-
+
diff --git a/build.sh b/build.sh
index 77cd63f..ef58702 100755
--- a/build.sh
+++ b/build.sh
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
-cargo run --bin stub_gen
+cargo run --bin stub_gen --features stub_gen
./.venv/bin/maturin dev --release
diff --git a/oxapy/__init__.py b/oxapy/__init__.py
index 3aec8f7..6268c64 100644
--- a/oxapy/__init__.py
+++ b/oxapy/__init__.py
@@ -216,7 +216,7 @@ def _session_middleware(request, next, secret, max_age, **kwargs):
request.session = session_data
initial_state = json.dumps(session_data)
- response = convert_to_response(next(request, **kwargs)) # ty:ignore
+ response = convert_to_response(next(request, **kwargs))
current_state = json.dumps(request.session)
if current_state != initial_state:
diff --git a/src/lib.rs b/src/lib.rs
index de30e11..da5cbeb 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -44,7 +44,6 @@ pyo3_stub_gen::export_verbatim!("oxapy", "from typing_extensions import Self");
pyo3_stub_gen::define_stub_info_gatherer!(stub_info);
struct ProcessRequest {
- cors: Option>,
wrapper: Option>>,
router: Option>,
match_route: Option>,
@@ -57,7 +56,6 @@ struct RequestContext {
app_data: Option>>,
wrapper: Option>>,
channel_capacity: usize,
- cors: Option>,
routers: Vec>,
request_sender: Sender,
template: Option>,
@@ -451,7 +449,6 @@ impl HttpServer {
app_data: self.app_data.clone(),
wrapper: self.wrapper.clone(),
channel_capacity: self.channel_capacity,
- cors: self.cors.clone(),
routers: self.routers.clone(),
request_sender: tx,
template: self.template.clone(),
@@ -524,8 +521,7 @@ impl HttpServer {
call_python_handler(&req.router, &req.match_route, &req.request, self.is_async)
.await
.unwrap_or_else(Response::from)
- .call_wrapper(&req)
- .apply_cors(&req.cors)?;
+ .call_wrapper(&req);
let _ = req.tx.send(response).await;
Ok(())
}
diff --git a/src/request.rs b/src/request.rs
index f583edd..a8c4a6b 100644
--- a/src/request.rs
+++ b/src/request.rs
@@ -284,7 +284,6 @@ impl Request {
router: Some(router),
match_route: Some(transmutate_route),
tx,
- cors: ctx.cors.clone(),
wrapper: ctx.wrapper.clone(),
};
@@ -302,7 +301,6 @@ impl Request {
router: None,
match_route: None,
tx,
- cors: ctx.cors.clone(),
wrapper: ctx.wrapper.clone(),
};
diff --git a/src/response.rs b/src/response.rs
index 95b4d45..15f7392 100644
--- a/src/response.rs
+++ b/src/response.rs
@@ -17,7 +17,7 @@ use pyo3::prelude::*;
use pyo3::types::{PyBytes, PyString};
use pyo3_stub_gen::derive::*;
-use crate::{Cors, IntoPyException, ProcessRequest, Status, convert_to_response, json};
+use crate::{IntoPyException, ProcessRequest, Status, convert_to_response, json};
pub type Body = BoxBody;
@@ -242,13 +242,6 @@ impl Response {
}
self
}
-
- pub(crate) fn apply_cors(mut self, cors: &Option>) -> PyResult {
- if let Some(cors) = cors {
- self = cors.apply_to_response(self)?;
- }
- Ok(self)
- }
}
/// HTTP redirect response.
diff --git a/src/templating/minijinja.rs b/src/templating/minijinja.rs
deleted file mode 100644
index 0234b72..0000000
--- a/src/templating/minijinja.rs
+++ /dev/null
@@ -1,65 +0,0 @@
-use std::sync::Arc;
-
-use ahash::HashMap;
-use minijinja::Environment;
-use pyo3::{prelude::*, types::PyDict};
-use pyo3_stub_gen::derive::*;
-
-use crate::IntoPyException;
-use crate::json;
-
-#[gen_stub_pyclass]
-#[pyclass(from_py_object, module = "oxapy.templating")]
-#[derive(Debug, Clone)]
-pub struct Jinja {
- engine: Arc>,
-}
-
-#[gen_stub_pymethods]
-#[pymethods]
-impl Jinja {
- #[new]
- #[gen_stub(override_return_type(type_repr = "typing_extensions.Self", imports = ("typing_extensions",)))]
- pub fn new(dir: String) -> PyResult {
- let mut env = Environment::new();
-
- let paths = glob::glob(&dir).into_py_exception()?;
-
- for entry in paths {
- let path = entry.into_py_exception()?;
- if path.is_file() {
- let name = {
- let full_path = path.to_str().unwrap().to_string();
- let name = full_path.split("/").skip(1);
- name.collect::>().join("/")
- };
- let content = std::fs::read_to_string(&path)?;
- let name = Box::leak(name.into_boxed_str());
- let content = Box::leak(content.into_boxed_str());
- env.add_template(name, content).into_py_exception()?;
- }
- }
-
- Ok(Self {
- engine: Arc::new(env),
- })
- }
-
- #[pyo3(signature=(template_name, context=None))]
- pub fn render(
- &self,
- template_name: String,
- context: Option>,
- ) -> PyResult {
- let template = self
- .engine
- .get_template(&template_name)
- .into_py_exception()?;
- let mut ctx_values: HashMap = HashMap::default();
- if let Some(context) = context {
- let value = json::from_pydict2rstruct(&context)?;
- ctx_values = value;
- }
- template.render(ctx_values).into_py_exception()
- }
-}
diff --git a/src/templating/mod.rs b/src/templating/mod.rs
index aa8183c..dcd56c1 100644
--- a/src/templating/mod.rs
+++ b/src/templating/mod.rs
@@ -1,20 +1,47 @@
+use std::sync::Arc;
+
+use ahash::HashMap;
use hyper::{HeaderMap, header::CONTENT_TYPE};
use pyo3::{
Bound, PyResult,
- exceptions::{PyException, PyValueError},
+ exceptions::PyValueError,
prelude::*,
types::{PyDict, PyModule, PyModuleMethods},
};
use pyo3_stub_gen::derive::*;
+use tera::{Function, Result as TeraResult, Value};
use crate::{
+ exceptions::IntoPyException,
+ json,
request::Request,
response::{Response, ResponseBody},
status::Status,
};
-mod minijinja;
-mod tera;
+struct PyTeraFunction {
+ callable: Py,
+}
+
+impl Function for PyTeraFunction {
+ fn call(&self, args: &std::collections::HashMap) -> TeraResult {
+ Python::attach(|py| {
+ let py_kwargs = json::from_rstruct2pydict(args, py)
+ .map_err(tera::Error::msg)?
+ .into_bound(py);
+ let result = self
+ .callable
+ .call(py, (), Some(&py_kwargs))
+ .map_err(tera::Error::msg)?;
+
+ Ok(Value::String(result.to_string()))
+ })
+ }
+
+ fn is_safe(&self) -> bool {
+ true
+ }
+}
/// Template engine for rendering HTML templates.
///
@@ -43,12 +70,11 @@ mod tera;
/// # Or use Tera with custom template directory
/// app.template(templating.Template("./views/**/*.html", "tera"))
/// ```
-#[gen_stub_pyclass_enum]
#[pyclass(from_py_object, module = "oxapy.templating")]
+#[gen_stub_pyclass]
#[derive(Clone, Debug)]
-pub enum Template {
- Jinja(minijinja::Jinja),
- Tera(tera::Tera),
+pub struct Template {
+ engine: Arc,
}
#[gen_stub_pymethods]
@@ -58,7 +84,6 @@ impl Template {
///
/// Args:
/// dir (str, optional): Directory pattern to search for templates (default: "./templates/**/*.html").
- /// engine (str, optional): Template engine to use, either "jinja" or "tera" (default: "jinja").
///
/// Returns:
/// Template: A new template engine instance.
@@ -74,18 +99,46 @@ impl Template {
/// template = templating.Template()
///
/// # Use Tera with custom template directory
- /// template = templating.Template("./views/**/*.html", "tera")
+ /// template = templating.Template("./views/**/*.html")
/// ```
#[new]
- #[pyo3(signature=(dir="./templates/**/*.html", engine="jinja"))]
+ #[pyo3(signature=(dir="./templates/**/*.html"))]
#[gen_stub(override_return_type(type_repr = "typing_extensions.Self", imports = ("typing_extensions",)))]
- fn new(dir: &str, engine: &str) -> PyResult {
- match engine {
- "jinja" => Ok(Template::Jinja(minijinja::Jinja::new(dir.to_string())?)),
- "tera" => Ok(Template::Tera(tera::Tera::new(dir.to_string())?)),
- e => Err(PyException::new_err(format!(
- "Invalid engine type '{e}'. Valid options are 'jinja' or 'tera'.",
- ))),
+ pub fn new(dir: &str) -> PyResult {
+ let tera = tera::Tera::new(dir).into_py_exception()?;
+ Ok(Self {
+ engine: Arc::new(tera),
+ })
+ }
+
+ #[pyo3(signature=(template_name, context=None))]
+ pub fn render(
+ &self,
+ template_name: &str,
+ context: Option>,
+ ) -> PyResult {
+ let mut tera_context = tera::Context::new();
+ if let Some(context) = context {
+ let map: HashMap = json::from_pydict2rstruct(&context)?;
+ for (key, value) in map {
+ tera_context.insert(key, &value);
+ }
+ }
+
+ self.engine
+ .render(template_name, &tera_context)
+ .into_py_exception()
+ }
+
+ pub fn register_function(&mut self, name: &str, callable: Py) -> PyResult<()> {
+ if let Some(tera) = Arc::get_mut(&mut self.engine) {
+ let py_func = PyTeraFunction { callable };
+ tera.register_function(name, py_func);
+ Ok(())
+ } else {
+ Err(PyErr::new::(
+ "Cannot register function: Tera engine is already shared cloned copies",
+ ))
}
}
}
@@ -120,7 +173,7 @@ impl Template {
#[pyo3(signature=(request, name, context=None))]
fn render(
request: Request,
- name: String,
+ name: &str,
context: Option>,
py: Python<'_>,
) -> PyResult {
@@ -135,10 +188,7 @@ fn render(
ctx.set_item("session", session.clone_ref(py))?;
}
- let body = match template.as_ref() {
- Template::Jinja(engine) => engine.render(name, Some(ctx))?,
- Template::Tera(engine) => engine.render(name, Some(ctx))?,
- };
+ let body = template.render(name, Some(ctx))?;
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, "text/html".parse().unwrap());
@@ -152,8 +202,6 @@ fn render(
pub fn templating_submodule(m: &Bound<'_, PyModule>) -> PyResult<()> {
let templating = PyModule::new(m.py(), "templating")?;
templating.add_class::()?;
- templating.add_class::()?;
- templating.add_class::()?;
m.add_function(wrap_pyfunction!(render, m)?)?;
m.add_submodule(&templating)
}
diff --git a/src/templating/tera.rs b/src/templating/tera.rs
deleted file mode 100644
index f45efe7..0000000
--- a/src/templating/tera.rs
+++ /dev/null
@@ -1,46 +0,0 @@
-use std::sync::Arc;
-
-use ahash::HashMap;
-use pyo3::{prelude::*, types::PyDict};
-use pyo3_stub_gen::derive::*;
-
-use crate::IntoPyException;
-use crate::json;
-
-#[gen_stub_pyclass]
-#[pyclass(from_py_object, module = "oxapy.templating")]
-#[derive(Debug, Clone)]
-pub struct Tera {
- engine: Arc,
-}
-
-#[gen_stub_pymethods]
-#[pymethods]
-impl Tera {
- #[new]
- #[gen_stub(override_return_type(type_repr = "typing_extensions.Self", imports = ("typing_extensions",)))]
- pub fn new(dir: String) -> PyResult {
- Ok(Self {
- engine: Arc::new(tera::Tera::new(&dir).into_py_exception()?),
- })
- }
-
- #[pyo3(signature=(template_name, context=None))]
- pub fn render(
- &self,
- template_name: String,
- context: Option>,
- ) -> PyResult {
- let mut tera_context = tera::Context::new();
- if let Some(context) = context {
- let map: HashMap = json::from_pydict2rstruct(&context)?;
- for (key, value) in map {
- tera_context.insert(key, &value);
- }
- }
-
- self.engine
- .render(&template_name, &tera_context)
- .into_py_exception()
- }
-}
From 0b5587f914a5315fd90cc4681fe3dad7d78cfedd Mon Sep 17 00:00:00 2001
From: FITAHIANA Nomeniavo Joe <24nomeniavo@gmail.com>
Date: Wed, 29 Jul 2026 21:26:10 +0300
Subject: [PATCH 4/6] chore: return value
---
src/json.rs | 2 +-
src/templating/mod.rs | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/json.rs b/src/json.rs
index 59c5a15..f2fd488 100644
--- a/src/json.rs
+++ b/src/json.rs
@@ -22,7 +22,7 @@ pub fn loads(data: &str, py: Python<'_>) -> PyResult> {
Ok(deserialized_data.extract(py)?)
}
-pub fn from_pydict2rstruct(dict: &Bound<'_, PyDict>) -> PyResult
+pub fn from_pydict2rstruct(dict: &Bound<'_, PyAny>) -> PyResult
where
T: for<'de> Deserialize<'de>,
{
diff --git a/src/templating/mod.rs b/src/templating/mod.rs
index dcd56c1..c75424e 100644
--- a/src/templating/mod.rs
+++ b/src/templating/mod.rs
@@ -32,9 +32,9 @@ impl Function for PyTeraFunction {
let result = self
.callable
.call(py, (), Some(&py_kwargs))
- .map_err(tera::Error::msg)?;
-
- Ok(Value::String(result.to_string()))
+ .map_err(tera::Error::msg)?
+ .into_bound(py);
+ json::from_pydict2rstruct(&result).map_err(tera::Error::msg)
})
}
From b6b0cb3294af26f99ddb1338f2e5901380596b12 Mon Sep 17 00:00:00 2001
From: FITAHIANA Nomeniavo Joe <24nomeniavo@gmail.com>
Date: Wed, 29 Jul 2026 22:03:28 +0300
Subject: [PATCH 5/6] chore: update docs string
---
oxapy/__init__.pyi | 167 ++++++++++++++++++++++--------------------
src/templating/mod.rs | 21 ++++++
2 files changed, 110 insertions(+), 78 deletions(-)
diff --git a/oxapy/__init__.pyi b/oxapy/__init__.pyi
index 1445823..68a6150 100644
--- a/oxapy/__init__.pyi
+++ b/oxapy/__init__.pyi
@@ -8,10 +8,7 @@ import typing_extensions
from . import exceptions
from . import jwt
from . import serializer
-from . import templating
__all__ = [
- "Catcher",
- "CatcherBuilder",
"Cors",
"File",
"FileStreaming",
@@ -24,7 +21,7 @@ __all__ = [
"Router",
"Session",
"Status",
- "catcher",
+ "Template",
"convert_to_response",
"delete",
"exceptions",
@@ -40,51 +37,8 @@ __all__ = [
"send_file",
"serializer",
"static_file",
- "templating",
]
-@typing.final
-class Catcher:
- r"""
- A catcher for handling specific HTTP status codes.
-
- Catchers allow you to provide custom responses for specific HTTP status codes.
- They are typically created using the `catcher` decorator function.
-
- Args:
- status (Status): The HTTP status code this catcher will handle.
- handler (callable): The handler function that will be called when this status occurs.
-
- Example:
- ```python
- from oxapy import catcher, Status
-
- @catcher(Status.NOT_FOUND)
- def handle_not_found(request, response):
- return Response("Custom 404 Page
", content_type="text/html")
- ```
- """
- ...
-
-@typing.final
-class CatcherBuilder:
- r"""
- Internal builder class for creating catchers.
-
- This class is returned by the `catcher` function and is used to create
- a Catcher when called with a handler function.
- """
- def __call__(self, handler: typing.Any) -> Catcher:
- r"""
- Create a Catcher when called with a handler function.
-
- Args:
- handler (callable): The handler function to call when the status occurs.
-
- Returns:
- Catcher: A new catcher for the specified status.
- """
-
@typing.final
class Cors:
r"""
@@ -491,7 +445,7 @@ class HttpServer:
server.attach(router)
```
"""
- def template(self, template: templating.Template) -> HttpServer:
+ def template(self, template: Template) -> HttpServer:
r"""
Enable template rendering for the server.
@@ -558,7 +512,7 @@ class HttpServer:
server.channel_capacity(200)
```
"""
- def catchers(self, catchers: typing.Sequence[Catcher]) -> HttpServer:
+ def wrap(self, wrapper: typing.Any) -> HttpServer:
r"""
Add status code catchers to the server.
@@ -570,11 +524,12 @@ class HttpServer:
Example:
```python
- @catcher(Status.NOT_FOUND)
- def not_found(request, response):
- return Response("Page Not Found
", content_type="text/html")
+ def global_middleware(request, response):
+ if response.status.code == 200:
+ return Response("Page Not Found
", content_type="text/html")
+ return response
- server.catchers([not_found])
+ server.wrap(global_middleware)
```
"""
def async_mode(self) -> HttpServer:
@@ -1147,6 +1102,87 @@ class Router:
"""
def __repr__(self) -> builtins.str: ...
+@typing.final
+class Template:
+ r"""
+ Template engine for rendering HTML templates.
+
+ This class provides a unified interface for different template engines,
+ currently supporting both Jinja and Tera templates.
+
+ Args:
+ dir (str, optional): Directory pattern to search for templates (default: "./templates/**/*.html").
+ engine (str, optional): Template engine to use, either "jinja" or "tera" (default: "jinja").
+
+ Returns:
+ Template: A new template engine instance.
+
+ Raises:
+ PyException: If an invalid engine type is specified.
+
+ Example:
+ ```python
+ from oxapy import HttpServer, templating
+
+ app = HttpServer(("127.0.0.1", 8000))
+
+ # Configure templates with default settings (Jinja)
+ app.template(templating.Template())
+
+ # Or use Tera with custom template directory
+ app.template(templating.Template("./views/**/*.html", "tera"))
+ ```
+ """
+ def __new__(cls, dir: builtins.str = './templates/**/*.html') -> typing_extensions.Self:
+ r"""
+ Create a new Template instance.
+
+ Args:
+ dir (str, optional): Directory pattern to search for templates (default: "./templates/**/*.html").
+
+ Returns:
+ Template: A new template engine instance.
+
+ Raises:
+ PyException: If an invalid engine type is specified.
+
+ Example:
+ ```python
+ from oxapy import templating
+
+ # Use Jinja with default template directory
+ template = templating.Template()
+
+ # Use Tera with custom template directory
+ template = templating.Template("./views/**/*.html")
+ ```
+ """
+ def render(self, template_name: builtins.str, context: typing.Optional[dict] = None) -> builtins.str: ...
+ def register_function(self, name: builtins.str, callable: typing.Any) -> None:
+ r"""
+ Register a Python function as a custom template function.
+
+ This method allows you to expose Python callables to be used within Tera templates.
+ The function will receive keyword arguments from the template call and should return
+ a value that can be serialized to JSON.
+
+ Args:
+ name (str): The name used to call the function from templates (e.g., `{{ my_function(key=value) }}`).
+ callable (Callable): A Python callable that accepts keyword arguments and returns a value.
+
+ Returns:
+ None: This method does not return a value.
+
+ Raises:
+ RuntimeError: If the template engine has been cloned and is shared across multiple references.
+
+ Example:
+ ```python
+ template.register_function("add", lambda a, b: a + b)
+ # In template: {{ add(a=1, b=2) }} -> 3
+ ```
+ """
+
@typing.final
class Status(enum.Enum):
r"""
@@ -1451,31 +1487,6 @@ class Status(enum.Enum):
def Session(secret: bytes, max_age: builtins.int = 604800) -> Response: ...
-def catcher(status: Status) -> CatcherBuilder:
- r"""
- Decorator for creating status code catchers.
-
- A catcher allows you to provide custom responses for specific HTTP status codes.
-
- Args:
- status (Status): The HTTP status code to catch.
-
- Returns:
- CatcherBuilder: A builder that creates a Catcher when called with a handler function.
-
- Example:
- ```python
- from oxapy import catcher, Status, Response
-
- @catcher(Status.NOT_FOUND)
- def handle_404(request, response):
- return Response("Page Not Found
", content_type="text/html")
-
- # Add the catcher to your server
- app.catchers([handle_404])
- ```
- """
-
def convert_to_response(result: typing.Any) -> Response:
r"""
Convert a Python object into an OxAPY `Response`.
diff --git a/src/templating/mod.rs b/src/templating/mod.rs
index c75424e..20087db 100644
--- a/src/templating/mod.rs
+++ b/src/templating/mod.rs
@@ -130,6 +130,27 @@ impl Template {
.into_py_exception()
}
+ /// Register a Python function as a custom template function.
+ ///
+ /// This method allows you to expose Python callables to be used within Tera templates.
+ /// The function will receive keyword arguments from the template call and should return
+ /// a value that can be serialized to JSON.
+ ///
+ /// Args:
+ /// name (str): The name used to call the function from templates (e.g., `{{ my_function(key=value) }}`).
+ /// callable (Callable): A Python callable that accepts keyword arguments and returns a value.
+ ///
+ /// Returns:
+ /// None: This method does not return a value.
+ ///
+ /// Raises:
+ /// RuntimeError: If the template engine has been cloned and is shared across multiple references.
+ ///
+ /// Example:
+ /// ```python
+ /// template.register_function("add", lambda a, b: a + b)
+ /// # In template: {{ add(a=1, b=2) }} -> 3
+ /// ```
pub fn register_function(&mut self, name: &str, callable: Py) -> PyResult<()> {
if let Some(tera) = Arc::get_mut(&mut self.engine) {
let py_func = PyTeraFunction { callable };
From 2e0f7fe1520be8d26da6ce50c1f5ba7a017aaf12 Mon Sep 17 00:00:00 2001
From: FITAHIANA Nomeniavo Joe <24nomeniavo@gmail.com>
Date: Wed, 29 Jul 2026 22:18:19 +0300
Subject: [PATCH 6/6] chore: move templating to single file
---
src/{templating/mod.rs => templating.rs} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename src/{templating/mod.rs => templating.rs} (100%)
diff --git a/src/templating/mod.rs b/src/templating.rs
similarity index 100%
rename from src/templating/mod.rs
rename to src/templating.rs