Skip to content

Commit 740e838

Browse files
authored
Merge pull request #1027 from alanjds/format-bang
Feature: str.format accepting !r, !s and !a
2 parents 68011df + 854bacf commit 740e838

File tree

3 files changed

+95
-5
lines changed

3 files changed

+95
-5
lines changed

tests/snippets/strings.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,10 +182,27 @@
182182
assert not '123'.isidentifier()
183183

184184
# String Formatting
185-
assert "{} {}".format(1,2) == "1 2"
186-
assert "{0} {1}".format(2,3) == "2 3"
185+
assert "{} {}".format(1, 2) == "1 2"
186+
assert "{0} {1}".format(2, 3) == "2 3"
187187
assert "--{:s>4}--".format(1) == "--sss1--"
188188
assert "{keyword} {0}".format(1, keyword=2) == "2 1"
189+
assert "repr() shows quotes: {!r}; str() doesn't: {!s}".format(
190+
'test1', 'test2'
191+
) == "repr() shows quotes: 'test1'; str() doesn't: test2", 'Output: {!r}, {!s}'.format('test1', 'test2')
192+
193+
194+
class Foo:
195+
def __str__(self):
196+
return 'str(Foo)'
197+
198+
def __repr__(self):
199+
return 'repr(Foo)'
200+
201+
202+
f = Foo()
203+
assert "{} {!s} {!r} {!a}".format(f, f, f, f) == 'str(Foo) str(Foo) repr(Foo) repr(Foo)'
204+
assert "{foo} {foo!s} {foo!r} {foo!a}".format(foo=f) == 'str(Foo) str(Foo) repr(Foo) repr(Foo)'
205+
# assert '{} {!r} {:10} {!r:10} {foo!r:10} {foo!r} {foo}'.format('txt1', 'txt2', 'txt3', 'txt4', 'txt5', foo='bar')
189206

190207
assert 'a' < 'b'
191208
assert 'a' <= 'b'

vm/src/format.rs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,49 @@ use num_traits::Signed;
33
use std::cmp;
44
use std::str::FromStr;
55

6+
#[derive(Debug, Copy, Clone, PartialEq)]
7+
pub enum FormatPreconversor {
8+
Str,
9+
Repr,
10+
Ascii,
11+
}
12+
13+
impl FormatPreconversor {
14+
pub fn from_char(c: char) -> Option<FormatPreconversor> {
15+
match c {
16+
's' => Some(FormatPreconversor::Str),
17+
'r' => Some(FormatPreconversor::Repr),
18+
'a' => Some(FormatPreconversor::Ascii),
19+
_ => None,
20+
}
21+
}
22+
23+
pub fn from_str(text: &str) -> Option<FormatPreconversor> {
24+
let mut chars = text.chars();
25+
if chars.next() != Some('!') {
26+
return None;
27+
}
28+
29+
match chars.next() {
30+
None => None, // Should fail instead?
31+
Some(c) => FormatPreconversor::from_char(c),
32+
}
33+
}
34+
35+
pub fn parse_and_consume(text: &str) -> (Option<FormatPreconversor>, &str) {
36+
let preconversor = FormatPreconversor::from_str(text);
37+
match preconversor {
38+
None => (None, text),
39+
Some(_) => {
40+
let mut chars = text.chars();
41+
chars.next(); // Consume the bang
42+
chars.next(); // Consume one r,s,a char
43+
(preconversor, chars.as_str())
44+
}
45+
}
46+
}
47+
}
48+
649
#[derive(Debug, Copy, Clone, PartialEq)]
750
pub enum FormatAlign {
851
Left,
@@ -56,6 +99,7 @@ pub enum FormatType {
5699

57100
#[derive(Debug, PartialEq)]
58101
pub struct FormatSpec {
102+
preconversor: Option<FormatPreconversor>,
59103
fill: Option<char>,
60104
align: Option<FormatAlign>,
61105
sign: Option<FormatSign>,
@@ -75,6 +119,10 @@ fn get_num_digits(text: &str) -> usize {
75119
text.len()
76120
}
77121

122+
fn parse_preconversor(text: &str) -> (Option<FormatPreconversor>, &str) {
123+
FormatPreconversor::parse_and_consume(text)
124+
}
125+
78126
fn parse_align(text: &str) -> (Option<FormatAlign>, &str) {
79127
let mut chars = text.chars();
80128
let maybe_align = chars.next().and_then(FormatAlign::from_char);
@@ -186,7 +234,8 @@ fn parse_format_type(text: &str) -> (Option<FormatType>, &str) {
186234
}
187235

188236
fn parse_format_spec(text: &str) -> FormatSpec {
189-
let (fill, align, after_align) = parse_fill_and_align(text);
237+
let (preconversor, after_preconversor) = parse_preconversor(text);
238+
let (fill, align, after_align) = parse_fill_and_align(after_preconversor);
190239
let (sign, after_sign) = parse_sign(after_align);
191240
let (alternate_form, after_alternate_form) = parse_alternate_form(after_sign);
192241
let after_zero = parse_zero(after_alternate_form);
@@ -196,6 +245,7 @@ fn parse_format_spec(text: &str) -> FormatSpec {
196245
let (format_type, _) = parse_format_type(after_precision);
197246

198247
FormatSpec {
248+
preconversor,
199249
fill,
200250
align,
201251
sign,
@@ -467,6 +517,18 @@ impl FormatString {
467517
String::new()
468518
};
469519

520+
// On parts[0] can still be the preconversor (!r, !s, !a)
521+
let parts: Vec<&str> = arg_part.splitn(2, '!').collect();
522+
// before the bang is a keyword or arg index, after the comma is maybe a conversor spec.
523+
let arg_part = parts[0];
524+
525+
let preconversor_spec = if parts.len() > 1 {
526+
"!".to_string() + parts[1]
527+
} else {
528+
String::new()
529+
};
530+
let format_spec = preconversor_spec + &format_spec;
531+
470532
if arg_part.is_empty() {
471533
return Ok(FormatPart::AutoSpec(format_spec));
472534
}
@@ -551,6 +613,7 @@ mod tests {
551613
#[test]
552614
fn test_width_only() {
553615
let expected = FormatSpec {
616+
preconversor: None,
554617
fill: None,
555618
align: None,
556619
sign: None,
@@ -566,6 +629,7 @@ mod tests {
566629
#[test]
567630
fn test_fill_and_width() {
568631
let expected = FormatSpec {
632+
preconversor: None,
569633
fill: Some('<'),
570634
align: Some(FormatAlign::Right),
571635
sign: None,
@@ -581,6 +645,7 @@ mod tests {
581645
#[test]
582646
fn test_all() {
583647
let expected = FormatSpec {
648+
preconversor: None,
584649
fill: Some('<'),
585650
align: Some(FormatAlign::Right),
586651
sign: Some(FormatSign::Minus),

vm/src/obj/objstr.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use unicode_casing::CharExt;
1111
use unicode_segmentation::UnicodeSegmentation;
1212
use unicode_xid::UnicodeXID;
1313

14-
use crate::format::{FormatParseError, FormatPart, FormatString};
14+
use crate::format::{FormatParseError, FormatPart, FormatPreconversor, FormatString};
1515
use crate::function::{single_or_tuple_any, OptionalArg, PyFuncArgs};
1616
use crate::pyhash;
1717
use crate::pyobject::{
@@ -1052,7 +1052,15 @@ fn count_char(s: &str, c: char) -> usize {
10521052
}
10531053

10541054
fn call_object_format(vm: &VirtualMachine, argument: PyObjectRef, format_spec: &str) -> PyResult {
1055-
let returned_type = vm.ctx.new_str(format_spec.to_string());
1055+
let (preconversor, new_format_spec) = FormatPreconversor::parse_and_consume(format_spec);
1056+
let argument = match preconversor {
1057+
Some(FormatPreconversor::Str) => vm.call_method(&argument, "__str__", vec![])?,
1058+
Some(FormatPreconversor::Repr) => vm.call_method(&argument, "__repr__", vec![])?,
1059+
Some(FormatPreconversor::Ascii) => vm.call_method(&argument, "__repr__", vec![])?,
1060+
None => argument,
1061+
};
1062+
let returned_type = vm.ctx.new_str(new_format_spec.to_string());
1063+
10561064
let result = vm.call_method(&argument, "__format__", vec![returned_type])?;
10571065
if !objtype::isinstance(&result, &vm.ctx.str_type()) {
10581066
let result_type = result.class();

0 commit comments

Comments
 (0)