-
Notifications
You must be signed in to change notification settings - Fork 17
Implement the 'first' filter #100
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
284297f
d75bb0f
26204b2
959d160
efd740c
15d356a
f9f63bb
632c758
2cf99a1
d1ccf7d
d36981f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,5 +1,6 @@ | ||||||
| use std::sync::Arc; | ||||||
|
|
||||||
| use miette::SourceSpan; | ||||||
| use pyo3::prelude::*; | ||||||
|
|
||||||
| use crate::types::Argument; | ||||||
|
|
@@ -13,6 +14,7 @@ pub enum FilterType { | |||||
| Default(DefaultFilter), | ||||||
| Escape(EscapeFilter), | ||||||
| External(ExternalFilter), | ||||||
| First(FirstFilter), | ||||||
| Lower(LowerFilter), | ||||||
| Safe(SafeFilter), | ||||||
| Slugify(SlugifyFilter), | ||||||
|
|
@@ -67,6 +69,17 @@ pub struct ExternalFilter { | |||||
| pub argument: Option<Argument>, | ||||||
| } | ||||||
|
|
||||||
| #[derive(Clone, Debug, PartialEq)] | ||||||
| pub struct FirstFilter { | ||||||
| pub at: SourceSpan, | ||||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn't really matter, but I'd keep
Suggested change
The reason is basically just to avoid using the third party type (
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, I think this change will be necessary to use |
||||||
| } | ||||||
|
|
||||||
| impl FirstFilter { | ||||||
| pub fn new(at: SourceSpan) -> Self { | ||||||
| Self { at } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| impl ExternalFilter { | ||||||
| pub fn new(filter: Py<PyAny>, argument: Option<Argument>) -> Self { | ||||||
| Self { | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,14 +4,15 @@ use std::sync::LazyLock; | |||||||||||||||||||||||||||||||||||||||||||||||||||
| use html_escape::encode_quoted_attribute_to_string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| use num_bigint::{BigInt, ToBigInt}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| use num_traits::ToPrimitive; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| use pyo3::exceptions::PyIndexError; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| use pyo3::prelude::*; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| use pyo3::sync::GILOnceCell; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| use pyo3::types::PyType; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| use crate::error::RenderError; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| use crate::filters::{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| AddFilter, AddSlashesFilter, CapfirstFilter, CenterFilter, DefaultFilter, EscapeFilter, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ExternalFilter, FilterType, LowerFilter, SafeFilter, SlugifyFilter, UpperFilter, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ExternalFilter, FilterType, FirstFilter, LowerFilter, SafeFilter, SlugifyFilter, UpperFilter, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| use crate::parse::Filter; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| use crate::render::types::{AsBorrowedContent, Content, ContentString, Context, IntoOwnedContent}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -47,6 +48,7 @@ impl Resolve for Filter { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| FilterType::Default(filter) => filter.resolve(left, py, template, context), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| FilterType::Escape(filter) => filter.resolve(left, py, template, context), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| FilterType::External(filter) => filter.resolve(left, py, template, context), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| FilterType::First(filter) => filter.resolve(left, py, template, context), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| FilterType::Lower(filter) => filter.resolve(left, py, template, context), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| FilterType::Safe(filter) => filter.resolve(left, py, template, context), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| FilterType::Slugify(filter) => filter.resolve(left, py, template, context), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -308,6 +310,89 @@ impl ResolveFilter for ExternalFilter { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| impl ResolveFilter for FirstFilter { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn resolve<'t, 'py>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| &self, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| variable: Option<Content<'t, 'py>>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| py: Python<'py>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| _template: TemplateString<'t>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| _context: &mut Context, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> ResolveResult<'t, 'py> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let content = match variable { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Some(content) => content, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| None => return Ok(Some("".as_content())), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| match content { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Content::Py(obj) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Try to get the first item using Python's indexing | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Django only catches IndexError, not TypeError | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| match obj.get_item(0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Ok(first) => Ok(Some(Content::Py(first))), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Err(e) if e.is_instance_of::<PyIndexError>(py) => Ok(Some("".as_content())), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Err(e) if e.is_instance_of::<pyo3::exceptions::PyTypeError>(py) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if this is the standard "'type' object is not subscriptable" error | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let error_msg = e.to_string(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if error_msg.contains("is not subscriptable") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Standard not subscriptable error - provide better formatting | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let type_name = obj | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .get_type() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .name() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map(|n| n.to_string()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .unwrap_or_else(|_| "unknown".to_string()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Err(RenderError::NotSubscriptable { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| type_name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| at: self.at, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .into()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Custom TypeError - preserve the original message | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Err(e.into()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Err(e) => Err(e.into()), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+333
to
+353
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since my last review, I've added a
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Content::String(s) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // For strings, get the first character | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Skip the empty string always present at the start after splitting | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut chars = s.as_raw().split("").skip(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| match chars.next() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| None => Ok(Some("".as_content())), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Some(c) => Ok(Some(c.to_string().into_content())), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Content::Int(_) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Numbers are not sequences, should raise TypeError | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Match Django's behavior exactly | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Err(RenderError::NotSubscriptable { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| type_name: "int".to_string(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| at: self.at, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .into()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Content::Float(_) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Floats are not sequences, should raise TypeError | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Match Django's behavior exactly | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Err(RenderError::NotSubscriptable { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| type_name: "float".to_string(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| at: self.at, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .into()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Content::Bool(_) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Booleans are not sequences, should raise TypeError | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Match Django's behavior exactly | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Err(RenderError::NotSubscriptable { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| type_name: "bool".to_string(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| at: self.at, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .into()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+356
to
+392
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need tests for these. |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| impl ResolveFilter for LowerFilter { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn resolve<'t, 'py>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| &self, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -981,4 +1066,84 @@ mod tests { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(rendered, ""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn test_render_filter_first_list() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| pyo3::prepare_freethreaded_python(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Python::with_gil(|py| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let engine = EngineData::empty(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let template_string = "{{ items|first }}".to_string(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let context = PyDict::new(py); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let items = vec!["a", "b", "c"]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| context.set_item("items", items).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let template = Template::new_from_string(py, template_string, &engine).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let result = template.render(py, Some(context), None).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(result, "a"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn test_render_filter_first_empty_list() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| pyo3::prepare_freethreaded_python(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Python::with_gil(|py| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let engine = EngineData::empty(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let template_string = "{{ items|first }}".to_string(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let context = PyDict::new(py); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let items: Vec<&str> = vec![]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| context.set_item("items", items).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let template = Template::new_from_string(py, template_string, &engine).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let result = template.render(py, Some(context), None).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(result, ""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn test_render_filter_first_string() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| pyo3::prepare_freethreaded_python(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Python::with_gil(|py| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let engine = EngineData::empty(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let template_string = "{{ text|first }}".to_string(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let context = PyDict::new(py); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| context.set_item("text", "hello").unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let template = Template::new_from_string(py, template_string, &engine).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let result = template.render(py, Some(context), None).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(result, "h"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn test_render_filter_first_empty_string() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| pyo3::prepare_freethreaded_python(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Python::with_gil(|py| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let engine = EngineData::empty(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let template_string = "{{ text|first }}".to_string(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let context = PyDict::new(py); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| context.set_item("text", "").unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let template = Template::new_from_string(py, template_string, &engine).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let result = template.render(py, Some(context), None).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(result, ""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn test_render_filter_first_no_argument() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| pyo3::prepare_freethreaded_python(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Python::with_gil(|py| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let engine = EngineData::empty(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let template_string = "{{ items|first:'arg' }}".to_string(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let error = Template::new_from_string(py, template_string, &engine).unwrap_err(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| let error_string = format!("{error}"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert!(error_string.contains("first filter does not take an argument")); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we can use
&'static strhere to avoid an allocation:Not a problem to leave it as is if this doesn't compile.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I checked, and we can do this alongside the
PyErr::annotatechange.