minijinja-2.5.0/0000775000175000017500000000000014745076366013372 5ustar carstencarstenminijinja-2.5.0/LICENSE0000664000175000017500000002513714714136652014376 0ustar carstencarsten Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. minijinja-2.5.0/src/0000775000175000017500000000000014714136652014150 5ustar carstencarstenminijinja-2.5.0/src/typeconv.rs0000664000175000017500000002337014714136652016372 0ustar carstencarstenuse std::collections::BTreeMap; use std::fmt; use std::sync::{Arc, Mutex}; use minijinja::value::{Enumerator, Object, ObjectRepr, Value, ValueKind}; use minijinja::{AutoEscape, Error, State}; use once_cell::sync::OnceCell; use pyo3::prelude::*; use pyo3::pybacked::PyBackedStr; use pyo3::types::{PyDict, PyList, PySequence, PyTuple}; use crate::error_support::{to_minijinja_error, to_py_error}; use crate::state::{bind_state, StateRef}; static AUTO_ESCAPE_CACHE: Mutex> = Mutex::new(BTreeMap::new()); static MARK_SAFE: OnceCell> = OnceCell::new(); fn is_safe_attr(name: &str) -> bool { !name.starts_with('_') } fn is_dictish(val: &Bound<'_, PyAny>) -> bool { val.hasattr("__getitem__").unwrap_or(false) && val.hasattr("items").unwrap_or(false) } pub struct DynamicObject { pub inner: Py, } impl DynamicObject { pub fn new(inner: Py) -> DynamicObject { DynamicObject { inner } } } impl fmt::Debug for DynamicObject { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Python::with_gil(|py| write!(f, "{}", self.inner.bind(py))) } } impl Object for DynamicObject { fn repr(self: &Arc) -> ObjectRepr { Python::with_gil(|py| { let inner = self.inner.bind(py); if inner.downcast::().is_ok() { ObjectRepr::Seq } else if is_dictish(inner) { ObjectRepr::Map } else if inner.iter().is_ok() { ObjectRepr::Iterable } else { ObjectRepr::Plain } }) } fn render(self: &Arc, f: &mut fmt::Formatter<'_>) -> fmt::Result where Self: Sized + 'static, { Python::with_gil(|py| write!(f, "{}", self.inner.bind(py))) } fn call(self: &Arc, state: &State, args: &[Value]) -> Result { Python::with_gil(|py| -> Result { bind_state(state, || { let inner = self.inner.bind(py); let (py_args, py_kwargs) = to_python_args(py, inner, args).map_err(to_minijinja_error)?; Ok(to_minijinja_value( &inner .call(py_args, py_kwargs.as_ref()) .map_err(to_minijinja_error)?, )) }) }) } fn call_method( self: &Arc, state: &State, name: &str, args: &[Value], ) -> Result { if !is_safe_attr(name) { return Err(Error::new( minijinja::ErrorKind::InvalidOperation, "insecure method call", )); } Python::with_gil(|py| -> Result { bind_state(state, || { let inner = self.inner.bind(py); let (py_args, py_kwargs) = to_python_args(py, inner, args).map_err(to_minijinja_error)?; Ok(to_minijinja_value( &inner .call_method(name, py_args, py_kwargs.as_ref()) .map_err(to_minijinja_error)?, )) }) }) } fn get_value(self: &Arc, key: &Value) -> Option { Python::with_gil(|py| { let inner = self.inner.bind(py); match inner.get_item(to_python_value_impl(py, key.clone()).ok()?) { Ok(value) => Some(to_minijinja_value(&value)), Err(_) => { if let Some(attr) = key.as_str() { if is_safe_attr(attr) { if let Ok(rv) = inner.getattr(attr) { return Some(to_minijinja_value(&rv)); } } } None } } }) } fn enumerate(self: &Arc) -> Enumerator { Python::with_gil(|py| { let inner = self.inner.bind(py); if inner.downcast::().is_ok() { Enumerator::Seq(inner.len().unwrap_or(0)) } else if let Ok(iter) = inner.iter() { Enumerator::Values( iter.filter_map(|x| match x { Ok(x) => Some(to_minijinja_value(&x)), Err(_) => None, }) .collect(), ) } else { Enumerator::NonEnumerable } }) } } pub fn to_minijinja_value(value: &Bound<'_, PyAny>) -> Value { if value.is_none() { Value::from(()) } else if let Ok(val) = value.extract::() { Value::from(val) } else if let Ok(val) = value.extract::() { Value::from(val) } else if let Ok(val) = value.extract::() { Value::from(val) } else if let Ok(val) = value.extract::() { if let Ok(to_html) = value.getattr("__html__") { if to_html.is_callable() { // TODO: if to_minijinja_value returns results we could // report the swallowed error of __html__. if let Ok(html) = to_html.call0() { if let Ok(val) = html.extract::() { return Value::from_safe_string(val.to_string()); } } } } Value::from(val.to_string()) } else { Value::from_object(DynamicObject::new(value.clone().unbind())) } } pub fn to_python_value(value: Value) -> PyResult> { Python::with_gil(|py| to_python_value_impl(py, value)) } fn mark_string_safe(py: Python<'_>, value: &str) -> PyResult> { let mark_safe: &Py = MARK_SAFE.get_or_try_init::<_, PyErr>(|| { let module = py.import_bound("minijinja._internal")?; Ok(module.getattr("mark_safe")?.into()) })?; mark_safe.call1(py, PyTuple::new_bound(py, [value])) } fn to_python_value_impl(py: Python<'_>, value: Value) -> PyResult> { // if we are holding a true dynamic object, we want to allow bidirectional // conversion. That means that when passing the object back to Python we // extract the retained raw Python reference. if let Some(pyobj) = value.downcast_object_ref::() { return Ok(pyobj.inner.clone_ref(py)); } if let Some(obj) = value.as_object() { match obj.repr() { ObjectRepr::Plain => return Ok(obj.to_string().into_py(py)), ObjectRepr::Map => { let rv = PyDict::new_bound(py); if let Some(pair_iter) = obj.try_iter_pairs() { for (key, value) in pair_iter { rv.set_item( to_python_value_impl(py, key)?, to_python_value_impl(py, value)?, )?; } } return Ok(rv.into()); } ObjectRepr::Seq | ObjectRepr::Iterable => { let rv = PyList::empty_bound(py); if let Some(iter) = obj.try_iter() { for value in iter { rv.append(to_python_value_impl(py, value)?)?; } } return Ok(rv.into()); } _ => {} } } match value.kind() { ValueKind::Undefined | ValueKind::None => Ok(().into_py(py)), ValueKind::Bool => Ok(value.is_true().into_py(py)), ValueKind::Number => { if let Ok(rv) = TryInto::::try_into(value.clone()) { Ok(rv.into_py(py)) } else if let Ok(rv) = TryInto::::try_into(value.clone()) { Ok(rv.into_py(py)) } else if let Ok(rv) = TryInto::::try_into(value) { Ok(rv.into_py(py)) } else { unreachable!() } } ValueKind::String => { if value.is_safe() { Ok(mark_string_safe(py, value.as_str().unwrap())?) } else { Ok(value.as_str().unwrap().into_py(py)) } } ValueKind::Bytes => Ok(value.as_bytes().unwrap().into_py(py)), kind => Err(to_py_error(minijinja::Error::new( minijinja::ErrorKind::InvalidOperation, format!("object {} cannot roundtrip", kind), ))), } } pub fn to_python_args<'py>( py: Python<'py>, callback: &Bound<'_, PyAny>, args: &[Value], ) -> PyResult<(Bound<'py, PyTuple>, Option>)> { let mut py_args = Vec::new(); let mut py_kwargs = None; if callback .getattr("__minijinja_pass_state__") .map_or(false, |x| x.is_truthy().unwrap_or(false)) { py_args.push(Bound::new(py, StateRef)?.to_object(py)); } for arg in args { if arg.is_kwargs() { let kwargs = py_kwargs.get_or_insert_with(|| PyDict::new_bound(py)); if let Ok(iter) = arg.try_iter() { for k in iter { if let Ok(v) = arg.get_item(&k) { kwargs .set_item(to_python_value_impl(py, k)?, to_python_value_impl(py, v)?)?; } } } } else { py_args.push(to_python_value_impl(py, arg.clone())?); } } let py_args = PyTuple::new_bound(py, py_args); Ok((py_args, py_kwargs)) } pub fn get_custom_autoescape(value: &str) -> AutoEscape { let mut cache = AUTO_ESCAPE_CACHE.lock().unwrap(); if let Some(rv) = cache.get(value).copied() { return rv; } let val = AutoEscape::Custom(Box::leak(value.to_string().into_boxed_str())); cache.insert(value.to_string(), val); val } minijinja-2.5.0/src/environment.rs0000664000175000017500000006220214714136652017064 0ustar carstencarstenuse std::borrow::Cow; use std::ffi::c_void; use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; use std::sync::Mutex; use minijinja::syntax::SyntaxConfig; use minijinja::value::{Rest, Value}; use minijinja::{context, escape_formatter, AutoEscape, Error, State, UndefinedBehavior}; use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; use pyo3::pybacked::PyBackedStr; use pyo3::types::{PyDict, PyTuple}; use crate::error_support::{report_unraisable, to_minijinja_error, to_py_error}; use crate::state::bind_state; use crate::typeconv::{ get_custom_autoescape, to_minijinja_value, to_python_args, to_python_value, DynamicObject, }; thread_local! { static CURRENT_ENV: AtomicPtr = const { AtomicPtr::new(std::ptr::null_mut()) }; } struct Syntax { block_start: String, block_end: String, variable_start: String, variable_end: String, comment_start: String, comment_end: String, line_statement_prefix: String, line_comment_prefix: String, } impl Default for Syntax { fn default() -> Self { Self { block_start: "{%".into(), block_end: "%}".into(), variable_start: "{{".into(), variable_end: "}}".into(), comment_start: "{#".into(), comment_end: "#}".into(), line_statement_prefix: "".into(), line_comment_prefix: "".into(), } } } impl Syntax { fn compile(&self) -> Result { SyntaxConfig::builder() .block_delimiters(self.block_start.clone(), self.block_end.clone()) .variable_delimiters(self.variable_start.clone(), self.variable_end.clone()) .comment_delimiters(self.comment_start.clone(), self.comment_end.clone()) .line_statement_prefix(self.line_statement_prefix.clone()) .line_comment_prefix(self.line_comment_prefix.clone()) .build() } } macro_rules! syntax_setter { ($slf:expr, $value:expr, $field:ident, $default:expr) => {{ let value = $value; let mut inner = $slf.inner.lock().unwrap(); if inner.syntax.is_none() { if value == $default { return Ok(()); } inner.syntax = Some(Syntax::default()); } if let Some(ref mut syntax) = inner.syntax { if syntax.$field != value { syntax.$field = value.into(); let syntax_config = syntax.compile().map_err(to_py_error)?; inner.env.set_syntax(syntax_config); } } Ok(()) }}; } macro_rules! syntax_getter { ($slf:expr, $field:ident, $default:expr) => {{ $slf.inner .lock() .unwrap() .syntax .as_ref() .map_or($default, |x| &x.$field) .into() }}; } struct Inner { env: minijinja::Environment<'static>, loader: Option>, auto_escape_callback: Option>, finalizer_callback: Option>, path_join_callback: Option>, syntax: Option, } /// Represents a MiniJinja environment. #[pyclass(subclass, module = "minijinja._lowlevel")] pub struct Environment { inner: Mutex, reload_before_render: AtomicBool, } #[pymethods] impl Environment { #[new] fn py_new() -> PyResult { Ok(Environment { inner: Mutex::new(Inner { env: minijinja::Environment::new(), loader: None, auto_escape_callback: None, finalizer_callback: None, path_join_callback: None, syntax: None, }), reload_before_render: AtomicBool::new(false), }) } /// Enables or disables debug mode. #[setter] pub fn set_debug(&self, value: bool) -> PyResult<()> { let mut inner = self.inner.lock().unwrap(); inner.env.set_debug(value); Ok(()) } /// Enables or disables debug mode. #[getter] pub fn get_debug(&self) -> PyResult { let inner = self.inner.lock().unwrap(); Ok(inner.env.debug()) } /// Sets the undefined behavior. #[setter] pub fn set_undefined_behavior(&self, value: &str) -> PyResult<()> { let mut inner = self.inner.lock().unwrap(); inner.env.set_undefined_behavior(match value { "strict" => UndefinedBehavior::Strict, "lenient" => UndefinedBehavior::Lenient, "chainable" => UndefinedBehavior::Chainable, _ => { return Err(PyRuntimeError::new_err( "invalid value for undefined behavior", )) } }); Ok(()) } /// Gets the undefined behavior. #[getter] pub fn get_undefined_behavior(&self) -> PyResult<&'static str> { let inner = self.inner.lock().unwrap(); Ok(match inner.env.undefined_behavior() { UndefinedBehavior::Lenient => "lenient", UndefinedBehavior::Chainable => "chainable", UndefinedBehavior::Strict => "strict", _ => { return Err(PyRuntimeError::new_err( "invalid value for undefined behavior", )) } }) } /// Sets fuel #[setter] pub fn set_fuel(&self, value: Option) -> PyResult<()> { let mut inner = self.inner.lock().unwrap(); inner.env.set_fuel(value); Ok(()) } /// Enables or disables debug mode. #[getter] pub fn get_fuel(&self) -> PyResult> { let inner = self.inner.lock().unwrap(); Ok(inner.env.fuel()) } /// Registers a filter function. #[pyo3(text_signature = "(self, name, callback)")] pub fn add_filter(&self, name: &str, callback: &Bound<'_, PyAny>) -> PyResult<()> { if !callback.is_callable() { return Err(PyRuntimeError::new_err("expected callback")); } let callback: Py = callback.clone().unbind(); self.inner.lock().unwrap().env.add_filter( name.to_string(), move |state: &State, args: Rest| -> Result { Python::with_gil(|py| { bind_state(state, || { let (py_args, py_kwargs) = to_python_args(py, callback.bind(py), &args) .map_err(to_minijinja_error)?; let rv = callback .call_bound(py, py_args, py_kwargs.as_ref()) .map_err(to_minijinja_error)?; Ok(to_minijinja_value(rv.bind(py))) }) }) }, ); Ok(()) } /// Removes a filter function. #[pyo3(text_signature = "(self, name)")] pub fn remove_filter(&self, name: &str) -> PyResult<()> { self.inner.lock().unwrap().env.remove_filter(name); Ok(()) } /// Registers a test function. #[pyo3(text_signature = "(self, name, callback)")] pub fn add_test(&self, name: &str, callback: &Bound<'_, PyAny>) -> PyResult<()> { if !callback.is_callable() { return Err(PyRuntimeError::new_err("expected callback")); } let callback: Py = callback.clone().unbind(); self.inner.lock().unwrap().env.add_test( name.to_string(), move |state: &State, args: Rest| -> Result { Python::with_gil(|py| { bind_state(state, || { let (py_args, py_kwargs) = to_python_args(py, callback.bind(py), &args) .map_err(to_minijinja_error)?; let rv = callback .call_bound(py, py_args, py_kwargs.as_ref()) .map_err(to_minijinja_error)?; Ok(to_minijinja_value(rv.bind(py)).is_true()) }) }) }, ); Ok(()) } /// Removes a test function. #[pyo3(text_signature = "(self, name)")] pub fn remove_test(&self, name: &str) -> PyResult<()> { self.inner.lock().unwrap().env.remove_test(name); Ok(()) } fn add_function(&self, name: &str, callback: &Bound<'_, PyAny>) -> PyResult<()> { let callback: Py = callback.clone().unbind(); self.inner.lock().unwrap().env.add_function( name.to_string(), move |state: &State, args: Rest| -> Result { Python::with_gil(|py| { bind_state(state, || { let (py_args, py_kwargs) = to_python_args(py, callback.bind(py), &args) .map_err(to_minijinja_error)?; let rv = callback .call_bound(py, py_args, py_kwargs.as_ref()) .map_err(to_minijinja_error)?; Ok(to_minijinja_value(rv.bind(py))) }) }) }, ); Ok(()) } /// Registers a global #[pyo3(text_signature = "(self, name, value)")] pub fn add_global(&self, name: &str, value: &Bound<'_, PyAny>) -> PyResult<()> { if value.is_callable() { self.add_function(name, value) } else { self.inner .lock() .unwrap() .env .add_global(name.to_string(), to_minijinja_value(value)); Ok(()) } } /// Removes a global #[pyo3(text_signature = "(self, name)")] pub fn remove_global(&self, name: &str) -> PyResult<()> { self.inner.lock().unwrap().env.remove_global(name); Ok(()) } /// Sets an auto escape callback. /// /// Note that because this interface in MiniJinja is infallible, the callback is /// not able to raise an error. #[setter] pub fn set_auto_escape_callback(&self, callback: &Bound<'_, PyAny>) -> PyResult<()> { if !callback.is_callable() { return Err(PyRuntimeError::new_err("expected callback")); } let callback: Py = callback.clone().unbind(); let mut inner = self.inner.lock().unwrap(); inner.auto_escape_callback = Python::with_gil(|py| Some(callback.clone_ref(py))); inner .env .set_auto_escape_callback(move |name: &str| -> AutoEscape { Python::with_gil(|py| { let py_args = PyTuple::new_bound(py, [name]); let rv = match callback.call_bound(py, py_args, None) { Ok(value) => value, Err(err) => { report_unraisable(py, err); return AutoEscape::None; } }; let rv = rv.bind(py); if rv.is_none() { return AutoEscape::None; } if let Ok(value) = rv.extract::() { match &value as &str { "html" => AutoEscape::Html, "json" => AutoEscape::Json, other => get_custom_autoescape(other), } } else if let Ok(value) = rv.extract::() { match value { true => AutoEscape::Html, false => AutoEscape::None, } } else { AutoEscape::None } }) }); Ok(()) } #[getter] pub fn get_auto_escape_callback(&self, py: Python<'_>) -> PyResult>> { Ok(self .inner .lock() .unwrap() .auto_escape_callback .as_ref() .map(|x| x.clone_ref(py))) } /// Sets a finalizer. /// /// A finalizer is called before a value is rendered to customize it. #[setter] pub fn set_finalizer(&self, callback: &Bound<'_, PyAny>) -> PyResult<()> { if !callback.is_callable() { return Err(PyRuntimeError::new_err("expected callback")); } let callback: Py = callback.clone().unbind(); let mut inner = self.inner.lock().unwrap(); Python::with_gil(|py| { inner.finalizer_callback = Some(callback.clone_ref(py)); }); inner.env.set_formatter(move |output, state, value| { Python::with_gil(|py| -> Result<(), Error> { let maybe_new_value = bind_state(state, || -> Result<_, Error> { let args = std::slice::from_ref(value); let (py_args, py_kwargs) = to_python_args(py, callback.bind(py), args).map_err(to_minijinja_error)?; let rv = callback .call_bound(py, py_args, py_kwargs.as_ref()) .map_err(to_minijinja_error)?; if rv.is(&py.NotImplemented()) { Ok(None) } else { Ok(Some(to_minijinja_value(rv.bind(py)))) } })?; let value = match maybe_new_value { Some(ref new_value) => new_value, None => value, }; escape_formatter(output, state, value) }) }); Ok(()) } #[getter] pub fn get_finalizer(&self, py: Python<'_>) -> PyResult>> { Ok(self .inner .lock() .unwrap() .finalizer_callback .as_ref() .map(|x| x.clone_ref(py))) } /// Sets a loader function for the environment. /// /// The loader function is invoked with the name of the template to load. If the /// template exists the source code of the template should be returned a string, /// otherwise `None` can be used to indicate that the template does not exist. #[setter] pub fn set_loader(&self, callback: Option<&Bound<'_, PyAny>>) -> PyResult<()> { let callback = match callback { None => None, Some(callback) => { if !callback.is_callable() { return Err(PyRuntimeError::new_err("expected callback")); } Some(callback.clone().unbind()) } }; let mut inner = self.inner.lock().unwrap(); Python::with_gil(|py| { inner.loader = callback.as_ref().map(|x| x.clone_ref(py)); }); if let Some(callback) = callback { inner.env.set_loader(move |name| { Python::with_gil(|py| { let callback = callback.bind(py); let rv = callback .call1(PyTuple::new_bound(py, [name])) .map_err(to_minijinja_error)?; if rv.is_none() { Ok(None) } else { Ok(Some(rv.to_string())) } }) }) } Ok(()) } /// Returns the current loader. #[getter] pub fn get_loader(&self, py: Python<'_>) -> Option> { self.inner .lock() .unwrap() .loader .as_ref() .map(|x| x.clone_ref(py)) } /// Sets a new path join callback. #[setter] pub fn set_path_join_callback(&self, callback: &Bound<'_, PyAny>) -> PyResult<()> { if !callback.is_callable() { return Err(PyRuntimeError::new_err("expected callback")); } let callback: Py = callback.clone().unbind(); let mut inner = self.inner.lock().unwrap(); Python::with_gil(|py| { inner.path_join_callback = Some(callback.clone_ref(py)); }); inner.env.set_path_join_callback(move |name, parent| { Python::with_gil(|py| { let callback = callback.bind(py); match callback.call1(PyTuple::new_bound(py, [name, parent])) { Ok(rv) => Cow::Owned(rv.to_string()), Err(err) => { report_unraisable(py, err); Cow::Borrowed(name) } } }) }); Ok(()) } /// Returns the current path join callback. #[getter] pub fn get_path_join_callback(&self, py: Python<'_>) -> Option> { self.inner .lock() .unwrap() .path_join_callback .as_ref() .map(|x| x.clone_ref(py)) } /// Triggers a reload of the templates. pub fn reload(&self, py: Python<'_>) -> PyResult<()> { let mut inner = self.inner.lock().unwrap(); let loader = inner.loader.as_ref().map(|x| x.clone_ref(py)); if loader.is_some() { inner.env.clear_templates(); } Ok(()) } /// Can be used to instruct the environment to automatically reload templates /// before each render. #[setter] pub fn set_reload_before_render(&self, yes: bool) { self.reload_before_render.store(yes, Ordering::Relaxed); } #[getter] pub fn get_reload_before_render(&self) -> bool { self.reload_before_render.load(Ordering::Relaxed) } #[setter] pub fn set_variable_start_string(&self, value: String) -> PyResult<()> { syntax_setter!(self, value, variable_start, "{{") } #[getter] pub fn get_variable_start_string(&self) -> String { syntax_getter!(self, variable_start, "{{") } #[setter] pub fn set_block_start_string(&self, value: String) -> PyResult<()> { syntax_setter!(self, value, block_start, "{%") } #[getter] pub fn get_block_start_string(&self) -> String { syntax_getter!(self, block_start, "{%") } #[setter] pub fn set_comment_start_string(&self, value: String) -> PyResult<()> { syntax_setter!(self, value, comment_start, "{#") } #[getter] pub fn get_comment_start_string(&self) -> String { syntax_getter!(self, comment_start, "{#") } #[setter] pub fn set_variable_end_string(&self, value: String) -> PyResult<()> { syntax_setter!(self, value, variable_end, "}}") } #[getter] pub fn get_variable_end_string(&self) -> String { syntax_getter!(self, variable_end, "}}") } #[setter] pub fn set_block_end_string(&self, value: String) -> PyResult<()> { syntax_setter!(self, value, block_end, "%}") } #[getter] pub fn get_block_end_string(&self) -> String { syntax_getter!(self, block_end, "%}") } #[setter] pub fn set_comment_end_string(&self, value: String) -> PyResult<()> { syntax_setter!(self, value, comment_end, "#}") } #[getter] pub fn get_comment_end_string(&self) -> String { syntax_getter!(self, comment_end, "#}") } #[setter] pub fn set_line_statement_prefix(&self, value: Option) -> PyResult<()> { syntax_setter!(self, value.unwrap_or_default(), line_statement_prefix, "") } #[getter] pub fn get_line_statement_prefix(&self) -> Option { let rv: String = syntax_getter!(self, line_statement_prefix, ""); (!rv.is_empty()).then_some(rv) } #[setter] pub fn set_line_comment_prefix(&self, value: Option) -> PyResult<()> { syntax_setter!(self, value.unwrap_or_default(), line_comment_prefix, "") } #[getter] pub fn get_line_comment_prefix(&self) -> Option { let rv: String = syntax_getter!(self, line_comment_prefix, ""); (!rv.is_empty()).then_some(rv) } /// Configures the trailing newline trimming feature. #[setter] pub fn set_keep_trailing_newline(&self, yes: bool) -> PyResult<()> { self.inner .lock() .unwrap() .env .set_keep_trailing_newline(yes); Ok(()) } /// Returns the current value of the trailing newline trimming flag. #[getter] pub fn get_keep_trailing_newline(&self) -> PyResult { Ok(self.inner.lock().unwrap().env.keep_trailing_newline()) } /// Configures the trim blocks feature. #[setter] pub fn set_trim_blocks(&self, yes: bool) -> PyResult<()> { self.inner.lock().unwrap().env.set_trim_blocks(yes); Ok(()) } /// Returns the current value of the trim blocks flag. #[getter] pub fn get_trim_blocks(&self) -> PyResult { Ok(self.inner.lock().unwrap().env.trim_blocks()) } /// Configures the lstrip blocks feature. #[setter] pub fn set_lstrip_blocks(&self, yes: bool) -> PyResult<()> { self.inner.lock().unwrap().env.set_lstrip_blocks(yes); Ok(()) } /// Returns the current value of the lstrip blocks flag. #[getter] pub fn get_lstrip_blocks(&self) -> PyResult { Ok(self.inner.lock().unwrap().env.lstrip_blocks()) } /// Manually adds a template to the environment. pub fn add_template(&self, name: String, source: String) -> PyResult<()> { let mut inner = self.inner.lock().unwrap(); inner .env .add_template_owned(name, source) .map_err(to_py_error) } /// Removes a loaded template. pub fn remove_template(&self, name: &str) { self.inner.lock().unwrap().env.remove_template(name); } /// Clears all loaded templates. pub fn clear_templates(&self) { self.inner.lock().unwrap().env.clear_templates(); } /// Renders a template looked up from the loader. /// /// The first argument is the name of the template, all other arguments must be passed /// as keyword arguments and are pass as render context of the template. #[pyo3(signature = (template_name, /, **ctx))] pub fn render_template( slf: PyRef<'_, Self>, py: Python<'_>, template_name: &str, ctx: Option<&Bound<'_, PyDict>>, ) -> PyResult { if slf.reload_before_render.load(Ordering::Relaxed) { slf.reload(py)?; } bind_environment(slf.as_ptr(), || { let inner = slf.inner.lock().unwrap(); let tmpl = inner.env.get_template(template_name).map_err(to_py_error)?; let ctx = ctx .map(|ctx| Value::from_object(DynamicObject::new(ctx.as_any().clone().unbind()))) .unwrap_or_else(|| context!()); tmpl.render(ctx).map_err(to_py_error) }) } /// Renders a template from a string /// /// The first argument is the source of the template, all other arguments must be passed /// as keyword arguments and are pass as render context of the template. #[pyo3(signature = (source, name=None, /, **ctx))] pub fn render_str( slf: PyRef<'_, Self>, source: &str, name: Option<&str>, ctx: Option<&Bound<'_, PyDict>>, ) -> PyResult { bind_environment(slf.as_ptr(), || { let ctx = ctx .map(|ctx| Value::from_object(DynamicObject::new(ctx.as_any().clone().unbind()))) .unwrap_or_else(|| context!()); slf.inner .lock() .unwrap() .env .render_named_str(name.unwrap_or(""), source, ctx) .map_err(to_py_error) }) } /// Evaluates an expression with a given context. #[pyo3(signature = (expression, /, **ctx))] pub fn eval_expr( slf: PyRef<'_, Self>, expression: &str, ctx: Option<&Bound<'_, PyDict>>, ) -> PyResult> { bind_environment(slf.as_ptr(), || { let inner = slf.inner.lock().unwrap(); let expr = inner .env .compile_expression(expression) .map_err(to_py_error)?; let ctx = ctx .map(|ctx| Value::from_object(DynamicObject::new(ctx.as_any().clone().unbind()))) .unwrap_or_else(|| context!()); to_python_value(expr.eval(ctx).map_err(to_py_error)?) }) } } pub fn with_environment) -> PyResult>(f: F) -> PyResult { Python::with_gil(|py| { CURRENT_ENV.with(|handle| { let ptr = handle.load(Ordering::Relaxed) as *mut _; match unsafe { Py::::from_borrowed_ptr_or_opt(py, ptr) } { Some(env) => f(env), None => Err(PyRuntimeError::new_err( "environment cannot be used outside of template render", )), } }) }) } /// Invokes a function with the state stashed away. pub fn bind_environment R>(envptr: *mut pyo3::ffi::PyObject, f: F) -> R { let old_handle = CURRENT_ENV .with(|handle| handle.swap(envptr as *const _ as *mut c_void, Ordering::Relaxed)); let rv = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)); CURRENT_ENV.with(|handle| handle.store(old_handle, Ordering::Relaxed)); match rv { Ok(rv) => rv, Err(payload) => std::panic::resume_unwind(payload), } } minijinja-2.5.0/src/state.rs0000664000175000017500000000530314714136652015637 0ustar carstencarstenuse minijinja::{AutoEscape, State}; use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; use std::ffi::c_void; use std::sync::atomic::{AtomicPtr, Ordering}; use crate::environment::{with_environment, Environment}; use crate::typeconv::to_python_value; thread_local! { static CURRENT_STATE: AtomicPtr = const { AtomicPtr::new(std::ptr::null_mut()) }; } /// A reference to the current state. #[pyclass(subclass, module = "minijinja._lowlevel", name = "State")] pub struct StateRef; #[pymethods] impl StateRef { /// Returns a reference to the environment. #[getter] pub fn get_env(&self) -> PyResult> { with_environment(Ok) } /// Returns the name of the template. #[getter] pub fn get_name(&self) -> PyResult { with_state(|state| Ok(state.name().to_string())) } /// Returns the current auto escape flag #[getter] pub fn get_auto_escape(&self) -> PyResult> { with_state(|state| { Ok(match state.auto_escape() { AutoEscape::None => None, AutoEscape::Html => Some("html"), AutoEscape::Json => Some("json"), AutoEscape::Custom(custom) => Some(custom), _ => None, }) }) } /// Returns the current block #[getter] pub fn get_current_block(&self) -> PyResult> { with_state(|state| Ok(state.current_block().map(|x| x.into()))) } /// Looks up a variable in the context #[pyo3(text_signature = "(self, name)")] pub fn lookup(&self, name: &str) -> PyResult> { with_state(|state| { state .lookup(name) .map(to_python_value) .unwrap_or_else(|| Ok(Python::with_gil(|py| py.None()))) }) } } pub fn with_state PyResult>(f: F) -> PyResult { CURRENT_STATE.with(|handle| { match unsafe { (handle.load(Ordering::Relaxed) as *const State).as_ref() } { Some(state) => f(state), None => Err(PyRuntimeError::new_err( "state cannot be used outside of template render", )), } }) } /// Invokes a function with the state stashed away. pub fn bind_state R>(state: &State, f: F) -> R { let old_handle = CURRENT_STATE .with(|handle| handle.swap(state as *const _ as *mut c_void, Ordering::Relaxed)); let rv = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)); CURRENT_STATE.with(|handle| handle.store(old_handle, Ordering::Relaxed)); match rv { Ok(rv) => rv, Err(payload) => std::panic::resume_unwind(payload), } } minijinja-2.5.0/src/lib.rs0000664000175000017500000000047214714136652015267 0ustar carstencarstenuse pyo3::prelude::*; mod environment; mod error_support; mod state; mod typeconv; #[pymodule] fn _lowlevel(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; Ok(()) } minijinja-2.5.0/src/error_support.rs0000664000175000017500000000532514714136652017450 0ustar carstencarstenuse std::cell::RefCell; use minijinja::{Error, ErrorKind}; use once_cell::sync::OnceCell; use pyo3::ffi::PyErr_WriteUnraisable; use pyo3::prelude::*; use pyo3::types::PyTuple; static TEMPLATE_ERROR: OnceCell> = OnceCell::new(); thread_local! { static STASHED_ERROR: RefCell> = const { RefCell::new(None) }; } /// Provides information about a template error from the runtime. #[pyclass(subclass, module = "minijinja._lowlevel", name = "ErrorInfo")] pub struct ErrorInfo { err: minijinja::Error, } #[pymethods] impl ErrorInfo { #[getter] pub fn get_kind(&self) -> String { format!("{:?}", self.err.kind()) } #[getter] pub fn get_name(&self) -> Option { self.err.name().map(|x| x.into()) } #[getter] pub fn get_line(&self) -> Option { self.err.line() } #[getter] pub fn get_range(&self) -> Option<(usize, usize)> { self.err.range().map(|x| (x.start, x.end)) } #[getter] pub fn get_template_source(&self) -> Option<&str> { self.err.template_source() } #[getter] pub fn get_description(&self) -> String { format!("{}", self.err) } #[getter] pub fn get_detail(&self) -> Option<&str> { self.err.detail() } #[getter] pub fn get_full_description(&self) -> String { use std::fmt::Write; let mut rv = format!("{:#}", self.err); let mut err = &self.err as &dyn std::error::Error; while let Some(next_err) = err.source() { rv.push('\n'); writeln!(&mut rv, "caused by: {next_err:#}").unwrap(); err = next_err; } rv } } pub fn to_minijinja_error(err: PyErr) -> Error { let msg = err.to_string(); STASHED_ERROR.with(|stash| { *stash.borrow_mut() = Some(err); }); Error::new(ErrorKind::TemplateNotFound, msg) } pub fn to_py_error(original_err: Error) -> PyErr { STASHED_ERROR.with(|stash| { stash .borrow_mut() .take() .unwrap_or_else(|| make_error(original_err)) }) } pub fn report_unraisable(py: Python<'_>, err: PyErr) { err.restore(py); unsafe { PyErr_WriteUnraisable(std::ptr::null_mut()); } } fn make_error(err: Error) -> PyErr { Python::with_gil(|py| { let template_error: &Py = TEMPLATE_ERROR.get_or_init(|| { let module = py.import_bound("minijinja._internal").unwrap(); let err = module.getattr("make_error").unwrap(); err.into() }); let args = PyTuple::new_bound(py, [Bound::new(py, ErrorInfo { err }).unwrap()]); PyErr::from_value_bound(template_error.call1(py, args).unwrap().bind(py).clone()) }) } minijinja-2.5.0/Cargo.toml0000664000175000017500000000075714714136652015322 0ustar carstencarsten[package] name = "minijinja-py" version = "2.5.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] name = "minijinja_py" crate-type = ["cdylib"] [dependencies] minijinja = { version = "2.5.0", path = "../minijinja", features = ["loader", "json", "urlencode", "fuel", "preserve_order", "speedups", "custom_syntax"] } once_cell = "1.17.0" pyo3 = { version = "0.22.6", features = ["extension-module", "serde", "abi3-py38"] } minijinja-2.5.0/README.md0000664000175000017500000001715614714136652014652 0ustar carstencarsten

MiniJinja for Python: a powerful template engine for Rust and Python

[![Build Status](https://github.com/mitsuhiko/minijinja/workflows/Tests/badge.svg?branch=main)](https://github.com/mitsuhiko/minijinja/actions?query=workflow%3ATests) [![License](https://img.shields.io/github/license/mitsuhiko/minijinja)](https://github.com/mitsuhiko/minijinja/blob/main/LICENSE) [![Crates.io](https://img.shields.io/crates/d/minijinja.svg)](https://crates.io/crates/minijinja) [![rustc 1.63.0](https://img.shields.io/badge/rust-1.63%2B-orange.svg)](https://img.shields.io/badge/rust-1.63%2B-orange.svg) [![Documentation](https://docs.rs/minijinja/badge.svg)](https://docs.rs/minijinja)
`minijinja-py` is an experimental binding of [MiniJinja](https://github.com/mitsuhiko/minijinja) to Python. It has somewhat limited functionality compared to the Rust version. These bindings use [maturin](https://www.maturin.rs/) and [pyo3](https://pyo3.rs/). You might want to use MiniJinja instead of Jinja2 when the full feature set of Jinja2 is not required and you want to have the same rendering experience of a data set between Rust and Python. With these bindings MiniJinja can render some Python objects and values that are passed to templates, but there are clear limitations with regards to what can be done. To install MiniJinja for Python you can fetch the package [from PyPI](https://pypi.org/project/minijinja/): ``` $ pip install minijinja ``` ## Basic API The basic API is hidden behind the `Environment` object. It behaves almost entirely like in `minijinja` with some Python specific changes. For instance instead of `env.set_debug(True)` you use `env.debug = True`. Additionally instead of using `add_template` or attaching a `source` you either pass a dictionary of templates directly to the environment or a `loader` function. ```python from minijinja import Environment env = Environment(templates={ "template_name": "Template source" }) ``` To render a template you can use the `render_template` method: ```python result = env.render_template('template_name', var1="value 1", var2="value 2") print(result) ``` ## Purpose MiniJinja attempts a reasonably high level of compatibility with Jinja2, but it does not try to achieve this at all costs. As a result you will notice that quite a few templates will refuse to render with MiniJinja despite the fact that they probably look quite innocent. It is however possible to write templates that render to the same results for both Jinja2 and MiniJinja. This raises the question why you might want to use MiniJinja. The main benefit would be to achieve the exact same results in both Rust and Python. Additionally MiniJinja has a stronger sandbox than Jinja2 and might perform ever so slightly better in some situations. However you should be aware that due to the marshalling that needs to happen in either direction there is a certain amount of loss of information. ## Dynamic Template Loading MiniJinja's Python bindings inherit the underlying behavior of how MiniJinja loads templates. Templates are loaded on first use and then cached. The templates are loaded via a loader. To trigger a reload you can call `env.reload()` or alternatively set `env.reload_before_render` to `True`. ```python def my_loader(name): segments = [] for segment in name.split("/"): if "\\" in segment or segment in (".", ".."): return None segments.append(segment) try: with open(os.path.join(TEMPLATES, *segments)) as f: return f.read() except (IOError, OSError): pass env = Environment(loader=my_loader) env.reload_before_render = True print(env.render_template("index.html")) ``` Alternatively templates can manually be loaded and unloaded with `env.add_template` and `env.remove_template`. ## Auto Escaping The default behavior is to use auto escaping file files ending in `.html`. You can customize this behavior by overriding the `auto_escape_callback`: ```python env = Environment(auto_escape_callback=lambda x: x.endswith((".html", ".foo"))) ``` MiniJinja uses [markupsafe](https://github.com/pallets/markupsafe) if it's available on the Python side. It will honor `__html__`. ## Finalizers Instead of custom formatters like in MiniJinja, you can define a finalizer instead which is similar to how it works in Jinja2. It's passed a value (or optional also the state as first argument when `pass_state` is used) and can return a new value. If the special `NotImplemented` value is returned, the original value is rendered without any modification: ``` from minijinja import Environment def finalizer(value): if value is None: return "" return NotImplemented env = Environment(finalizer=finalizer) assert env.render_str("{{ none }}") == "" ``` ## State Access Functions passed to the environment such as filters or global functions can optionally have the template state passed by using the `pass_state` parameter. This is similar to `pass_context` in Jinja2. It can be used to look at the name of the template or to look up variables in the context. ```python from minijinja import pass_state @pass_state def my_filter(state, value): return state.lookup("a_variable") + value env.add_filter("add_a_variable", my_filter) ``` ## Runtime Behavior MiniJinja uses it's own runtime model which is not matching the Python runtime model. As a result there are gaps in behavior between the two but some limited effort is made to bridge them. For instance you will be able to call some methods of types, but for instance builtins such as dicts and lists do not expose their methods on the MiniJinja side in all cases. A natively generated MiniJinja map (such as with the `dict` global function) will not have an `.items()` method, whereas a Python dict passed to MiniJinja will. Here is what this means for some basic types: * Python dictionaries and lists (as well as other objects that behave as sequences) appear in the MiniJinja side very similar to how they do in Python. * Tuples on the MiniJinja side are represented as lists, but will appear again as tuples if passed back to Python. * Python objects are represented in MiniJinja similarly to dicts, but they retain all their meaningful Python APIs. This means they stringify via `__str__` and they allow the MiniJinja code to call their non-underscored methods. Note that there is no extra security layer in use at the moment so take care of what you pass there. * MiniJinja's python binding understand what `__html__` is when it exists on a string subclass. This means that a `markupsafe.Markup` object will appear as safe string in MiniJinja. This information can also flow back to Python again. * Stringification of objects uses `__str__` which is why mixed Python and MiniJinja objects can be a bit confusing at times. * Where in Jinja2 there is a difference between `foo["bar"]` and `foo.bar` which can be used to disambiugate properties and keys, in MiniJinja there is no such difference. However methods are disambiugated so `foo.items()` works and will correctly call the method in all cases. ## Sponsor If you like the project and find it useful you can [become a sponsor](https://github.com/sponsors/mitsuhiko). ## License and Links - [Documentation](https://docs.rs/minijinja/) - [Examples](https://github.com/mitsuhiko/minijinja/tree/main/examples) - [Issue Tracker](https://github.com/mitsuhiko/minijinja/issues) - [MiniJinja Playground](https://mitsuhiko.github.io/minijinja-playground/) - License: [Apache-2.0](https://github.com/mitsuhiko/minijinja/blob/main/LICENSE) minijinja-2.5.0/python/0000775000175000017500000000000014714136652014702 5ustar carstencarstenminijinja-2.5.0/python/minijinja/0000775000175000017500000000000014714136652016652 5ustar carstencarstenminijinja-2.5.0/python/minijinja/__init__.py0000664000175000017500000001240414714136652020764 0ustar carstencarstenfrom . import _lowlevel __all__ = [ "Environment", "TemplateError", "safe", "escape", "render_str", "eval_expr", "pass_state", ] class Environment(_lowlevel.Environment): """Represents a MiniJinja environment""" def __new__(cls, *args, **kwargs): # `_lowlevel.Environment` does not accept any arguments return super().__new__(cls) def __init__( self, loader=None, templates=None, filters=None, tests=None, globals=None, debug=True, fuel=None, undefined_behavior=None, auto_escape_callback=None, path_join_callback=None, keep_trailing_newline=False, trim_blocks=False, lstrip_blocks=False, finalizer=None, reload_before_render=False, block_start_string="{%", block_end_string="%}", variable_start_string="{{", variable_end_string="}}", comment_start_string="{#", comment_end_string="#}", line_statement_prefix=None, line_comment_prefix=None, ): super().__init__() if loader is not None: if templates: raise TypeError("Cannot set loader and templates at the same time") self.loader = loader elif templates is not None: self.loader = dict(templates).get if fuel is not None: self.fuel = fuel if filters: for name, callback in filters.items(): self.add_filter(name, callback) if tests: for name, callback in tests.items(): self.add_test(name, callback) if globals is not None: for name, value in globals.items(): self.add_global(name, value) self.debug = debug if auto_escape_callback is not None: self.auto_escape_callback = auto_escape_callback if path_join_callback is not None: self.path_join_callback = path_join_callback if keep_trailing_newline: self.keep_trailing_newline = True if trim_blocks: self.trim_blocks = True if lstrip_blocks: self.lstrip_blocks = True if finalizer is not None: self.finalizer = finalizer if undefined_behavior is not None: self.undefined_behavior = undefined_behavior self.reload_before_render = reload_before_render # XXX: because this is not an atomic reconfigure if you set one of # the values to a conflicting set, it will immediately error out :( self.block_start_string = block_start_string self.block_end_string = block_end_string self.variable_start_string = variable_start_string self.variable_end_string = variable_end_string self.comment_start_string = comment_start_string self.comment_end_string = comment_end_string self.line_statement_prefix = line_statement_prefix self.line_comment_prefix = line_comment_prefix DEFAULT_ENVIRONMENT = Environment() def render_str(*args, **context): """Shortcut to render a string with the default environment.""" return DEFAULT_ENVIRONMENT.render_str(*args, **context) def eval_expr(*args, **context): """Evaluate an expression with the default environment.""" return DEFAULT_ENVIRONMENT.eval_expr(*args, **context) try: from markupsafe import escape, Markup except ImportError: from html import escape as _escape class Markup(str): def __html__(self): return self def escape(value): callback = getattr(value, "__html__", None) if callback is not None: return callback() return Markup(_escape(str(value))) def safe(s): """Marks a string as safe.""" return Markup(s) def pass_state(f): """Pass the engine state to the function as first argument.""" f.__minijinja_pass_state__ = True return f class TemplateError(RuntimeError): """Represents a runtime error in the template engine.""" def __init__(self, message): super().__init__(message) self._info = None @property def message(self): """The short message of the error.""" return self.args[0] @property def kind(self): """The kind of the error.""" if self._info is None: return "Unknown" else: return self._info.kind @property def name(self): """The name of the template.""" if self._info is not None: return self._info.name @property def detail(self): """The detail error message of the error.""" if self._info is not None: return self._info.detail @property def line(self): """The line of the error.""" if self._info is not None: return self._info.line @property def range(self): """The range of the error.""" if self._info is not None: return self._info.range @property def template_source(self): """The template source of the error.""" if self._info is not None: return self._info.template_source def __str__(self): if self._info is not None: return self._info.full_description return self.message minijinja-2.5.0/python/minijinja/_lowlevel.pyi0000664000175000017500000000035614714136652021371 0ustar carstencarstenfrom typing import Any from . import Environment from typing_extensions import final @final class State: name: str env: Environment current_block: str | None auto_escape: bool def lookup(self, name: str) -> Any: ... minijinja-2.5.0/python/minijinja/__init__.pyi0000664000175000017500000001211414714136652021133 0ustar carstencarstenfrom pathlib import PurePath from typing import ( Any, Callable, Literal, TypeVar, Protocol, overload, ) from typing_extensions import Final, TypeAlias, Self from minijinja._lowlevel import State from collections.abc import Mapping __all__ = [ "Environment", "TemplateError", "safe", "escape", "render_str", "eval_expr", "pass_state", ] _A_contra = TypeVar("_A_contra", contravariant=True) _R_co = TypeVar("_R_co", covariant=True) class _PassesState(Protocol[_A_contra, _R_co]): def __call__(self, state: State, value: _A_contra, /) -> _R_co: ... __minijinja_pass_state__: Literal[True] _StrPath: TypeAlias = PurePath | str _Behavior = Literal["strict", "lenient", "chainable"] DEFAULT_ENVIRONMENT: Final[Environment] def render_str(source: str, name: str | None = None, /, **context: Any) -> str: ... def eval_expr(expression: str, /, **context: Any) -> Any: ... class Environment: loader: Callable[[str], str] | None fuel: int | None debug: bool undefined_behavior: _Behavior auto_escape_callback: Callable[[str], bool] | None path_join_callback: Callable[[str, str], _StrPath] | None keep_trailing_newline: bool trim_blocks: bool lstrip_blocks: bool finalizer: _PassesState[Any, Any] | Callable[[Any], Any] | None reload_before_render: bool block_start_string: str block_end_string: str variable_start_string: str variable_end_string: str comment_start_string: str comment_end_string: str line_statement_prefix: str | None line_comment_prefix: str | None @overload def __init__( self, loader: Callable[[str], str] | None = None, templates: Mapping[str, str] | None = None, filters: Mapping[str, Callable[[Any], Any]] | None = None, tests: Mapping[str, Callable[[Any], bool]] | None = None, globals: Mapping[str, Any] | None = None, debug: bool = True, fuel: int | None = None, undefined_behavior: _Behavior = "lenient", auto_escape_callback: Callable[[str], bool] | None = None, path_join_callback: Callable[[str, str], _StrPath] | None = None, keep_trailing_newline: bool = False, trim_blocks: bool = False, lstrip_blocks: bool = False, finalizer: _PassesState[Any, Any] | None = None, reload_before_render: bool = False, block_start_string: str = "{%", block_end_string: str = "%}", variable_start_string: str = "{{", variable_end_string: str = "}}", comment_start_string: str = "{#", comment_end_string: str = "#}", line_statement_prefix: str | None = None, line_comment_prefix: str | None = None, ) -> None: ... @overload def __init__( self, loader: Callable[[str], str] | None = None, templates: Mapping[str, str] | None = None, filters: Mapping[str, Callable[[Any], Any]] | None = None, tests: Mapping[str, Callable[[Any], bool]] | None = None, globals: Mapping[str, Any] | None = None, debug: bool = True, fuel: int | None = None, undefined_behavior: _Behavior = "lenient", auto_escape_callback: Callable[[str], bool] | None = None, path_join_callback: Callable[[str, str], _StrPath] | None = None, keep_trailing_newline: bool = False, trim_blocks: bool = False, lstrip_blocks: bool = False, finalizer: Callable[[Any], Any] | None = None, reload_before_render: bool = False, block_start_string: str = "{%", block_end_string: str = "%}", variable_start_string: str = "{{", variable_end_string: str = "}}", comment_start_string: str = "{#", comment_end_string: str = "#}", line_statement_prefix: str | None = None, line_comment_prefix: str | None = None, ) -> None: ... def remove_filter(self, name: str) -> None: ... def add_test(self, name: str, test: Callable[[Any], bool]) -> None: ... def remove_test(self, name: str) -> None: ... def add_global(self, name: str, value: Any) -> None: ... def remove_global(self, name: str) -> None: ... def render_template(self, template_name: str, /, **context: Any) -> str: ... def render_str( self, source: str, name: str | None = None, /, **context: Any ) -> str: ... def eval_expr(self, expression: str, /, **context: Any) -> Any: ... class TemplateError(RuntimeError): def __init__(self, message: str) -> None: ... @property def message(self) -> str: ... @property def kind(self) -> str: ... @property def name(self) -> str | None: ... @property def detail(self) -> str | None: ... @property def line(self) -> int | None: ... @property def range(self) -> int | None: ... @property def template_source(self) -> str | None: ... def __str__(self) -> str: ... class Markup(str): def __html__(self) -> Self: ... def safe(value: str) -> str: ... def escape(value: Any) -> str: ... def pass_state( f: Callable[[State, _A_contra], _R_co], ) -> _PassesState[_A_contra, _R_co]: ... minijinja-2.5.0/python/minijinja/_internal.py0000664000175000017500000000105614714136652021201 0ustar carstencarsten# This file contains functions that the rust module imports. from . import TemplateError, safe def make_error(info): # Internal utility function used by the rust binding to create a template error # with info object. We cannot directly create an error on the Rust side because # we want to subclass the runtime error, but on the limited abi it's not possible # to create subclasses (yet!) err = TemplateError(info.description) err._info = info return err # used by the rust runtime to mark something as safe mark_safe = safe minijinja-2.5.0/pyproject.toml0000664000175000017500000000235714714136652016304 0ustar carstencarsten[build-system] requires = ["maturin>=1.5"] build-backend = "maturin" [project] name = "minijinja" version = "2.5.0" description = "An experimental Python binding of the Rust MiniJinja template engine." requires-python = ">=3.8" license = { file = "LICENSE" } authors = [ { name = "Armin Ronacher", email = "armin.ronacher@active-4.com" } ] maintainers = [ { name = "Armin Ronacher", email = "armin.ronacher@active-4.com" } ] keywords = ["jinja", "template-engine"] classifiers = [ "Programming Language :: Rust", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Environment :: Web Environment", "License :: OSI Approved :: Apache Software License", "Topic :: Text Processing :: Markup :: HTML", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", ] [project.urls] Repository = "https://github.com/mitsuhiko/minijinja" "Issue Tracker" = "https://github.com/mitsuhiko/minijinja/issues" "Donate" = "https://github.com/sponsors/mitsuhiko" [tool.maturin] module-name = "minijinja._lowlevel" python-source = "python" strip = true [tool.pyright] include = ["python/**/*.pyi"] exclude = ["python/**/*.py"] typeCheckingMode = "strict" pythonVersion = "3.9" minijinja-2.5.0/hello.py0000664000175000017500000000107314714136652015037 0ustar carstencarstenfrom minijinja import Environment INDEX = """{% extends "layout.html" %} {% block title %}{{ page.title }}{% endblock %} {% block body %}
    {%- for item in items %}
  • {{ item }} {%- endfor %}
{% endblock %} """ LAYOUT = """ {% block title %}{% endblock %} {% block body %}{% endblock %} """ env = Environment(templates={ "index.html": INDEX, "layout.html": LAYOUT, }) print(env.render_template( 'index.html', page={"title": "The Page Title"}, items=["Peter", "Paul", "Mary"] )) minijinja-2.5.0/Makefile0000664000175000017500000000072514714136652015025 0ustar carstencarsten.PHONY: all all: develop test .venv: python3 -mvenv .venv .venv/bin/pip install --upgrade pip .venv/bin/pip install maturin pytest markupsafe black pyright .PHONY: test develop: .venv .venv/bin/maturin develop .PHONY: develop-release develop-release: .venv .venv/bin/maturin develop --release .PHONY: test test: .venv .venv/bin/pytest .PHONY: format format: .venv .venv/bin/black tests python .PHONY: type-check type-check: .venv .venv/bin/pyright python minijinja-2.5.0/tests/0000775000175000017500000000000014714136652014523 5ustar carstencarstenminijinja-2.5.0/tests/test_state.py0000664000175000017500000000440014714136652017252 0ustar carstencarstenfrom minijinja import Environment, safe, pass_state def test_func_state(): env = Environment() @pass_state def my_func(state): assert state.name == "template-name" assert state.auto_escape is None assert state.current_block == "foo" assert state.lookup("bar") == 23 assert state.lookup("aha") is None assert state.lookup("my_func") is my_func assert state.env is env return 42 rv = env.render_str( "{% block foo %}{{ my_func() }}{% endblock %}", "template-name", my_func=my_func, bar=23, ) assert rv == "42" def test_global_func_state(): env = Environment() @pass_state def my_func(state): assert state.name == "template-name" assert state.auto_escape is None assert state.current_block == "foo" assert state.lookup("bar") == 23 assert state.lookup("aha") is None assert state.env is env return 42 env.add_global("my_func", my_func) rv = env.render_str( "{% block foo %}{{ my_func() }}{% endblock %}", "template-name", bar=23, ) assert rv == "42" def test_filter_state(): env = Environment() @pass_state def my_filter(state, value): assert state.name == "template-name" assert state.auto_escape is None assert state.current_block == "foo" assert state.lookup("bar") == 23 assert state.lookup("aha") is None assert state.env is env return value env.add_filter("myfilter", my_filter) rv = env.render_str( "{% block foo %}{{ 42|myfilter }}{% endblock %}", "template-name", bar=23, ) assert rv == "42" def test_test_state(): env = Environment() @pass_state def my_test(state, value): assert state.name == "template-name" assert state.auto_escape is None assert state.current_block == "foo" assert state.lookup("bar") == 23 assert state.lookup("aha") is None assert state.env is env return True env.add_test("mytest", my_test) rv = env.render_str( "{% block foo %}{{ 42 is mytest }}{% endblock %}", "template-name", bar=23, ) assert rv == "true" minijinja-2.5.0/tests/test_security.py0000664000175000017500000000070514714136652020005 0ustar carstencarstenfrom minijinja import Environment def test_private_attrs(): class MyClass: def __init__(self): self.public = 42 self._private = 23 env = Environment() rv = env.eval_expr("[x.public, x._private]", x=MyClass()) assert rv == [42, None] def test_dict_is_always_public(): env = Environment() rv = env.eval_expr("[x.public, x._private]", x={"public": 42, "_private": 23}) assert rv == [42, 23] minijinja-2.5.0/tests/test_basic.py0000664000175000017500000002272114714136652017221 0ustar carstencarstenimport binascii import pytest import posixpath import types from _pytest.unraisableexception import catch_unraisable_exception from minijinja import ( Environment, TemplateError, safe, pass_state, eval_expr, render_str, ) def test_expression(): env = Environment() rv = env.eval_expr("1 + b", b=42) assert rv == 43 rv = env.eval_expr("range(n)", n=10) assert rv == list(range(10)) def test_pass_callable(): def magic(): return [1, 2, 3] env = Environment() rv = env.eval_expr("x()", x=magic) assert rv == [1, 2, 3] def test_callable_attrs(): def hmm(): pass hmm.public_attr = 42 env = Environment() rv = env.eval_expr("[hmm.public_attr, hmm.__module__]", hmm=hmm) assert rv == [42, None] def test_generator(): def hmm(): yield 1 yield 2 yield 3 hmm.public_attr = 42 env = Environment() rv = env.eval_expr("values", values=hmm()) assert isinstance(rv, types.GeneratorType) rv = env.eval_expr("values|list", values=hmm()) assert rv == [1, 2, 3] def test_method_calling(): class MyClass(object): def my_method(self): return 23 def __repr__(self): return "This is X" env = Environment() rv = env.eval_expr("[x ~ '', x.my_method()]", x=MyClass()) assert rv == ["This is X", 23] rv = env.eval_expr("x.items()|list", x={"a": "b"}) assert rv == [("a", "b")] def test_types_passthrough(): tup = (1, 2, 3) assert eval_expr("x", x=tup) == tup assert render_str("{{ x }}", x=tup) == "(1, 2, 3)" assert eval_expr("x is sequence", x=tup) == True assert render_str("{{ x }}", x=(1, True)) == "(1, True)" assert eval_expr("x[0] == 42", x=[42]) == True def test_custom_filter(): def my_filter(value): return "<%s>" % value.upper() env = Environment() env.add_filter("myfilter", my_filter) rv = env.eval_expr("'hello'|myfilter") assert rv == "" def test_custom_filter_kwargs(): def my_filter(value, x): return "<%s %s>" % (value.upper(), x) env = Environment() env.add_filter("myfilter", my_filter) rv = env.eval_expr("'hello'|myfilter(x=42)") assert rv == "" def test_custom_test(): def my_test(value, arg): return value == arg env = Environment() env.add_filter("mytest", my_test) rv = env.eval_expr("'hello'|mytest(arg='hello')") assert rv == True rv = env.eval_expr("'hello'|mytest(arg='hellox')") assert rv == False def test_basic_types(): env = Environment() rv = env.eval_expr("{'a': 42, 'b': 42.5, 'c': 'blah'}") assert rv == {"a": 42, "b": 42.5, "c": "blah"} def test_loader(): called = [] def my_loader(name): called.append(name) return "Hello from " + name env = Environment(loader=my_loader) assert env.render_template("index.html") == "Hello from index.html" assert env.render_template("index.html") == "Hello from index.html" assert env.render_template("other.html") == "Hello from other.html" assert env.loader is my_loader assert called == ["index.html", "other.html"] env.loader = my_loader assert env.render_template("index.html") == "Hello from index.html" assert called == ["index.html", "other.html"] env.reload() assert env.render_template("index.html") == "Hello from index.html" assert called == ["index.html", "other.html", "index.html"] def test_loader_reload(): called = [] def my_loader(name): called.append(name) return "Hello from " + name env = Environment(loader=my_loader) env.reload_before_render = True assert env.render_template("index.html") == "Hello from index.html" assert env.render_template("index.html") == "Hello from index.html" assert env.render_template("other.html") == "Hello from other.html" assert called == ["index.html", "index.html", "other.html"] def test_autoescape(): assert Environment().auto_escape_callback is None def auto_escape(name): assert name == "foo.html" return "html" env = Environment( auto_escape_callback=auto_escape, loader=lambda x: "Hello {{ foo }}", ) assert env.auto_escape_callback is auto_escape rv = env.render_template("foo.html", foo="") assert rv == "Hello <x>" with catch_unraisable_exception() as cm: rv = env.render_template("invalid.html", foo="") assert rv == "Hello " assert cm.unraisable[0] is AssertionError def test_finalizer(): assert Environment().finalizer is None @pass_state def my_finalizer(state, value): assert state.name == "" if value is None: return "" elif isinstance(value, bytes): return binascii.b2a_hex(value).decode("utf-8") return NotImplemented env = Environment(finalizer=my_finalizer) rv = env.render_str("[{{ foo }}]") assert rv == "[]" rv = env.render_str("[{{ foo }}]", foo=None) assert rv == "[]" rv = env.render_str("[{{ foo }}]", foo="test") assert rv == "[test]" rv = env.render_str("[{{ foo }}]", foo=b"test") assert rv == "[74657374]" def raising_finalizer(value): 1 / 0 env = Environment(finalizer=raising_finalizer) with pytest.raises(ZeroDivisionError): env.render_str("{{ whatever }}") def test_globals(): env = Environment(globals={"x": 23, "y": lambda: 42}) rv = env.eval_expr("[x, y(), z]", z=11) assert rv == [23, 42, 11] def test_honor_safe(): env = Environment(auto_escape_callback=lambda x: True) rv = env.render_str("{{ x }} {{ y }}", x=safe(""), y="") assert rv == " <bar>" def test_full_object_transfer(): class X(object): def __init__(self): self.x = 1 self.y = 2 def test_filter(value): assert isinstance(value, X) return value env = Environment(filters=dict(testfilter=test_filter)) rv = env.eval_expr("x|testfilter", x=X()) assert isinstance(rv, X) assert rv.x == 1 assert rv.y == 2 def test_markup_transfer(): env = Environment() rv = env.eval_expr("value", value=safe("")) assert hasattr(rv, "__html__") assert rv.__html__() == "" rv = env.eval_expr("''|escape") assert hasattr(rv, "__html__") assert rv.__html__() == "<test>" def test_error(): env = Environment() try: env.eval_expr("1 +") except TemplateError as e: assert e.name == "" assert "unexpected end of input" in e.message assert "1 > 1 +" not in e.message assert "1 > 1 +" in str(e) assert e.line == 1 assert e.kind == "SyntaxError" assert e.range == (2, 3) assert e.template_source == "1 +" assert "unexpected end of input" in e.detail else: assert False, "expected error" def test_custom_syntax(): env = Environment( block_start_string="[%", block_end_string="%]", variable_start_string="{", variable_end_string="}", comment_start_string="/*", comment_end_string="*/", ) rv = env.render_str("[% if true %]{value}[% endif %]/* nothing */", value=42) assert rv == "42" def test_path_join(): def join_path(name, parent): return posixpath.join(posixpath.dirname(parent), name) env = Environment( path_join_callback=join_path, templates={ "foo/bar.txt": "{% include 'baz.txt' %}", "foo/baz.txt": "I am baz!", }, ) with catch_unraisable_exception() as cm: rv = env.render_template("foo/bar.txt") assert rv == "I am baz!" assert cm.unraisable is None def test_keep_trailing_newline(): env = Environment(keep_trailing_newline=False) assert env.render_str("foo\n") == "foo" env = Environment(keep_trailing_newline=True) assert env.render_str("foo\n") == "foo\n" def test_trim_blocks(): env = Environment(trim_blocks=False) assert env.render_str("{% if true %}\nfoo{% endif %}") == "\nfoo" env = Environment(trim_blocks=True) assert env.render_str("{% if true %}\nfoo{% endif %}") == "foo" def test_lstrip_blocks(): env = Environment(lstrip_blocks=False) assert env.render_str(" {% if true %}\nfoo{% endif %}") == " \nfoo" env = Environment(lstrip_blocks=True) assert env.render_str(" {% if true %}\nfoo{% endif %}") == "\nfoo" def test_trim_and_lstrip_blocks(): env = Environment(lstrip_blocks=False, trim_blocks=False) assert env.render_str(" {% if true %}\nfoo{% endif %}") == " \nfoo" env = Environment(lstrip_blocks=True, trim_blocks=True) assert env.render_str(" {% if true %}\nfoo{% endif %}") == "foo" def test_line_statements(): env = Environment() assert env.line_statement_prefix is None assert env.line_comment_prefix is None env = Environment(line_statement_prefix="#", line_comment_prefix="##") assert env.line_statement_prefix == "#" assert env.line_comment_prefix == "##" rv = env.render_str("# for x in range(3)\n{{ x }}\n# endfor") assert rv == "0\n1\n2\n" def test_custom_delimiters(): env = Environment( variable_start_string="${", variable_end_string="}", block_start_string="<%", block_end_string="%>", comment_start_string="", ) rv = env.render_str("<% if true %>${ value }<% endif %>", value=42) assert rv == "42"