Skip to content

Conversation

ipmb
Copy link

@ipmb ipmb commented Jul 26, 2025

Full disclosure upfront, this was generated with Claude Code. I'm excited about this project and some of this porting work felt like a good fit for an LLM. If this isn't welcome, feel free to close it. I'm happy to iterate on it if there is feedback.


Implementation Details

The filter returns the first item of a sequence (list, tuple, string) or empty string for empty sequences. It matches Django's behavior, including error handling.

Django References

Changes

  • Added FirstFilter struct and enum variant
  • Implemented parsing for the 'first' filter (no arguments)
  • Implemented ResolveFilter trait with Django-compatible behavior
    • Returns first item for sequences
    • Returns empty string for empty sequences
    • Raises TypeError for None and non-subscriptable types
    • Uses exact Django error messages
  • Added comprehensive test suite porting all Django test cases plus additional edge cases

Testing

The implementation handles:

  • Lists, tuples, strings (including empty)
  • Falsy values (0, False) correctly returned
  • Autoescape on/off behavior
  • Safe strings preservation
  • Type errors for None and integers

@ipmb ipmb force-pushed the implement-first-filter branch from ca856bc to f98bb33 Compare July 26, 2025 19:11
- Add FirstFilter struct and enum variant
- Implement parsing logic for 'first' filter
- Implement ResolveFilter trait with Django-compatible behavior
  - Returns first item of sequences (lists, tuples, strings)
  - Returns empty string for empty sequences
  - Raises TypeError for None and non-subscriptable types
  - Matches Django's exact error messages
- Add comprehensive test suite porting all Django test cases
  - Test with lists, tuples, strings
  - Test empty sequences
  - Test autoescape on/off behavior
  - Test with mark_safe strings
  - Test error conditions (None, integers)
  - Add additional edge case tests

refs LilyFirefly#1
@ipmb ipmb force-pushed the implement-first-filter branch from f98bb33 to 284297f Compare July 26, 2025 19:14
Copy link

codecov bot commented Jul 27, 2025

Codecov Report

❌ Patch coverage is 92.36111% with 22 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/render/filters.rs 80.35% 21 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Owner

@LilyFirefly LilyFirefly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this! I have a few suggestions, but this is looking really good already.

- Added custom RenderError::NotSubscriptable for better error messages
- Used inline isinstance check for PyIndexError
- Implemented horizontal space reduction with early returns
- Used str.split("") for efficient character iteration
- Fixed all clippy warnings and formatting issues
- Moved tests to tests/filters/test_first.py following project structure
- All 18 tests pass with 100% Django compatibility

The filter returns the first item of sequences (lists, tuples, strings)
or empty string for empty sequences, matching Django's behavior exactly
including error handling for non-subscriptable types.

Co-Authored-By: Claude <[email protected]>
ipmb and others added 6 commits August 22, 2025 14:40
Following feedback on the first filter implementation, this commit adds proper
source location tracking to the FirstFilter struct. The location is captured
during parsing and used when reporting NotSubscriptable errors for non-sequence
types (int and float).

Changes:
- Add `at: SourceSpan` field to FirstFilter struct
- Add constructor method `FirstFilter::new(at: SourceSpan)`
- Pass filter location from parser when creating FirstFilter instances
- Use stored location instead of hardcoded (0, 0) in error reporting

This provides more accurate error diagnostics by preserving the template
position where the filter was applied, though the location is not yet
displayed in the Python error messages.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Brings in CenterFilter and Bool content type support
while maintaining FirstFilter location tracking
- Update FirstFilter to return Option<Content> to match new API
- Add handling for Content::Bool variant in FirstFilter
The IntoOwnedContent trait is already imported at module scope,
so the local import in FirstFilter::resolve is unnecessary.
- Inline s.as_raw() call to reduce unnecessary variable
- More concise and readable code
@ipmb ipmb force-pushed the implement-first-filter branch from a5acdf3 to 2cf99a1 Compare August 22, 2025 21:06
ipmb added 2 commits August 22, 2025 15:16
- Implement WithSourceCode trait for PyTypeError to show miette error formatting
- Update NotSubscriptable error handling to use the new method
- Handle Python TypeError in FirstFilter to detect not subscriptable errors
- Preserve custom TypeError messages while formatting standard ones
- Now shows source code context with location indicators for type errors
Ensures complete coverage of all Content type branches
@ipmb
Copy link
Author

ipmb commented Aug 22, 2025

I think I've resolved all the open comments here 👍

Copy link
Owner

@LilyFirefly LilyFirefly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! This is great progress. I've got a few more suggestions:

Comment on lines +333 to +353
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()),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since my last review, I've added a PyErr::annotate method which I think we can use here:

Suggested change
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()),
Err(error) => {
let error = error.annotate(py, self.at, "here", template);
Err(error.into())
}

&self,
variable: Option<Content<'t, 'py>>,
py: Python<'py>,
_template: TemplateString<'t>,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_template: TemplateString<'t>,
template: TemplateString<'t>,


#[derive(Clone, Debug, PartialEq)]
pub struct FirstFilter {
pub at: SourceSpan,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't really matter, but I'd keep at as a (usize, usize) until using it in RenderError:

Suggested change
pub at: SourceSpan,
pub at: (usize, usize),

The reason is basically just to avoid using the third party type (miette::SourceSpan) until it's necessary.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think this change will be necessary to use PyErr::annotate.

},
#[error("'{type_name}' object is not subscriptable")]
NotSubscriptable {
type_name: String,
Copy link
Owner

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 str here to avoid an allocation:

Suggested change
type_name: String,
type_name: &'static str,

Not a problem to leave it as is if this doesn't compile.

Copy link
Owner

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::annotate change.

// Numbers are not sequences, should raise TypeError
// Match Django's behavior exactly
Err(RenderError::NotSubscriptable {
type_name: "int".to_string(),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If type_name can be &'static str, we can avoid the to_string() call:

Suggested change
type_name: "int".to_string(),
type_name: "int",

Comment on lines +81 to +82
assert "'NoneType' object is not subscriptable" in str(django_exc.value)
assert "'NoneType' object is not subscriptable" in str(rusty_exc.value)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +356 to +392
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(),
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())
}
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need tests for these.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants