Skip to content

Commit

Permalink
Adds more string methods to pycompat (#522)
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko authored Jun 11, 2024
1 parent 3a15159 commit de5dff9
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 0 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to MiniJinja are documented here.

## 2.0.3

- Added new methods to pycompat: `str.endswith`, `str.rfind`,
`str.isalnum`, `str.isalpha`, `str.isascii`, `str.isdigit`,
`str.isnumeric`, `str.join`, `str.startswith`. #522

## 2.0.2

- Implemented sequence (+ some iterator) and string repeating with the `*`
Expand Down
104 changes: 104 additions & 0 deletions minijinja-contrib/src/pycompat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,24 @@ use minijinja::{Error, ErrorKind, State, Value};
/// * `list.count`
/// * `str.capitalize`
/// * `str.count`
/// * `str.endswith`
/// * `str.find`
/// * `str.isalnum`
/// * `str.isalpha`
/// * `str.isascii`
/// * `str.isdigit`
/// * `str.islower`
/// * `str.isnumeric`
/// * `str.isupper`
/// * `str.join`
/// * `str.lower`
/// * `str.lstrip`
/// * `str.replace`
/// * `str.rfind`
/// * `str.rstrip`
/// * `str.split`
/// * `str.splitlines`
/// * `str.startswith`
/// * `str.strip`
/// * `str.title`
/// * `str.upper`
Expand Down Expand Up @@ -77,6 +86,24 @@ fn string_methods(value: &Value, method: &str, args: &[Value]) -> Result<Value,
from_args(args)?;
Ok(Value::from(s.chars().all(|x| x.is_whitespace())))
}
"isdigit" | "isnumeric" => {
// this is not a perfect mapping to what Python does, but
// close enough for most uses in templates.
from_args(args)?;
Ok(Value::from(s.chars().all(|x| x.is_numeric())))
}
"isalnum" => {
from_args(args)?;
Ok(Value::from(s.chars().all(|x| x.is_alphanumeric())))
}
"isalpha" => {
from_args(args)?;
Ok(Value::from(s.chars().all(|x| x.is_alphabetic())))
}
"isascii" => {
from_args(args)?;
Ok(Value::from(s.chars().all(|x| x.is_ascii())))
}
"strip" => {
let (chars,): (Option<&str>,) = from_args(args)?;
Ok(Value::from(if let Some(chars) = chars {
Expand Down Expand Up @@ -164,6 +191,83 @@ fn string_methods(value: &Value, method: &str, args: &[Value]) -> Result<Value,
None => -1,
}))
}
"rfind" => {
let (what,): (&str,) = from_args(args)?;
Ok(Value::from(match s.rfind(what) {
Some(x) => x as i64,
None => -1,
}))
}
"startswith" => {
let (prefix,): (&Value,) = from_args(args)?;
if let Some(prefix) = prefix.as_str() {
Ok(Value::from(s.starts_with(prefix)))
} else if matches!(prefix.kind(), ValueKind::Iterable | ValueKind::Seq) {
for prefix in prefix.try_iter()? {
if s.starts_with(prefix.as_str().ok_or_else(|| {
Error::new(
ErrorKind::InvalidOperation,
format!(
"tuple for startswith must contain only strings, not {}",
prefix.kind()
),
)
})?) {
return Ok(Value::from(true));
}
}
Ok(Value::from(false))
} else {
Err(Error::new(
ErrorKind::InvalidOperation,
format!(
"startswith argument must be string or a tuple of strings, not {}",
prefix.kind()
),
))
}
}
"endswith" => {
let (suffix,): (&Value,) = from_args(args)?;
if let Some(suffix) = suffix.as_str() {
Ok(Value::from(s.ends_with(suffix)))
} else if matches!(suffix.kind(), ValueKind::Iterable | ValueKind::Seq) {
for suffix in suffix.try_iter()? {
if s.ends_with(suffix.as_str().ok_or_else(|| {
Error::new(
ErrorKind::InvalidOperation,
format!(
"tuple for endswith must contain only strings, not {}",
suffix.kind()
),
)
})?) {
return Ok(Value::from(true));
}
}
Ok(Value::from(false))
} else {
Err(Error::new(
ErrorKind::InvalidOperation,
format!(
"endswith argument must be string or a tuple of strings, not {}",
suffix.kind()
),
))
}
}
"join" => {
use std::fmt::Write;
let (values,): (&Value,) = from_args(args)?;
let mut rv = String::new();
for (idx, value) in values.try_iter()?.enumerate() {
if idx > 0 {
rv.push_str(s);
}
write!(rv, "{}", value).ok();
}
Ok(Value::from(rv))
}
_ => Err(Error::from(ErrorKind::UnknownMethod)),
}
}
Expand Down
11 changes: 11 additions & 0 deletions minijinja-contrib/tests/pycompat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ fn test_string_methods() {
assert!(eval_expr("'foobar'.islower()").is_true());
assert!(eval_expr("'FOOBAR'.isupper()").is_true());
assert!(eval_expr("' \\n'.isspace()").is_true());
assert!(eval_expr("'abc'.isalpha()").is_true());
assert!(eval_expr("'abc123'.isalnum()").is_true());
assert!(eval_expr("'abc%@#'.isascii()").is_true());
assert_eq!(
eval_expr("'foobar'.replace('o', 'x')").as_str(),
Some("fxxbar")
Expand All @@ -41,12 +44,20 @@ fn test_string_methods() {
);
assert_eq!(eval_expr("'foo barooo'.count('oo')").as_usize(), Some(2));
assert_eq!(eval_expr("'foo barooo'.find('oo')").as_usize(), Some(1));
assert_eq!(eval_expr("'foo barooo'.rfind('oo')").as_usize(), Some(8));
assert!(eval_expr("'a b c'.split() == ['a', 'b', 'c']").is_true());
assert!(eval_expr("'a b c'.split() == ['a', 'b', 'c']").is_true());
assert!(eval_expr("'a b c'.split(none, 1) == ['a', 'b c']").is_true());
assert!(eval_expr("'abcbd'.split('b', 1) == ['a', 'cbd']").is_true());
assert!(eval_expr("'a\\nb\\r\\nc'.splitlines() == ['a', 'b', 'c']").is_true());
assert!(eval_expr("'a\\nb\\r\\nc'.splitlines(true) == ['a\\n', 'b\\r\\n', 'c']").is_true());
assert!(eval_expr("'foobarbaz'.startswith('foo')").is_true());
assert!(eval_expr("'foobarbaz'.startswith(('foo', 'bar'))").is_true());
assert!(!eval_expr("'barfoobaz'.startswith(('foo', 'baz'))").is_true());
assert!(eval_expr("'foobarbaz'.endswith('baz')").is_true());
assert!(eval_expr("'foobarbaz'.endswith(('baz', 'bar'))").is_true());
assert!(!eval_expr("'foobarbazblah'.endswith(('baz', 'bar'))").is_true());
assert_eq!(eval_expr("'|'.join([1, 2, 3])").as_str(), Some("1|2|3"));
}

#[test]
Expand Down

0 comments on commit de5dff9

Please sign in to comment.