Skip to content

Feature: str.format accepting !r, !s and !a #1027

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

Merged
merged 4 commits into from
Jun 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions tests/snippets/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,27 @@
assert not '123'.isidentifier()

# String Formatting
assert "{} {}".format(1,2) == "1 2"
assert "{0} {1}".format(2,3) == "2 3"
assert "{} {}".format(1, 2) == "1 2"
assert "{0} {1}".format(2, 3) == "2 3"
assert "--{:s>4}--".format(1) == "--sss1--"
assert "{keyword} {0}".format(1, keyword=2) == "2 1"
assert "repr() shows quotes: {!r}; str() doesn't: {!s}".format(
'test1', 'test2'
) == "repr() shows quotes: 'test1'; str() doesn't: test2", 'Output: {!r}, {!s}'.format('test1', 'test2')


class Foo:
def __str__(self):
return 'str(Foo)'

def __repr__(self):
return 'repr(Foo)'


f = Foo()
assert "{} {!s} {!r} {!a}".format(f, f, f, f) == 'str(Foo) str(Foo) repr(Foo) repr(Foo)'
assert "{foo} {foo!s} {foo!r} {foo!a}".format(foo=f) == 'str(Foo) str(Foo) repr(Foo) repr(Foo)'
# assert '{} {!r} {:10} {!r:10} {foo!r:10} {foo!r} {foo}'.format('txt1', 'txt2', 'txt3', 'txt4', 'txt5', foo='bar')

assert 'a' < 'b'
assert 'a' <= 'b'
Expand Down
67 changes: 66 additions & 1 deletion vm/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,49 @@ use num_traits::Signed;
use std::cmp;
use std::str::FromStr;

#[derive(Debug, Copy, Clone, PartialEq)]
pub enum FormatPreconversor {
Str,
Repr,
Ascii,
}

impl FormatPreconversor {
pub fn from_char(c: char) -> Option<FormatPreconversor> {
match c {
's' => Some(FormatPreconversor::Str),
'r' => Some(FormatPreconversor::Repr),
'a' => Some(FormatPreconversor::Ascii),
_ => None,
}
}

pub fn from_str(text: &str) -> Option<FormatPreconversor> {
let mut chars = text.chars();
if chars.next() != Some('!') {
return None;
}

match chars.next() {
None => None, // Should fail instead?
Some(c) => FormatPreconversor::from_char(c),
Copy link
Member

Choose a reason for hiding this comment

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

This can just be chars.next().map(FormatPreconversor::from_char)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hum... this still returns None if chars.next() is None?

Asking because FormatPreconversor::from_char is not prepared to handle None as input.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Another aspect: // Should fail instead?

I do not know how to raise exceptions. Ideally, {!x} should fail, but this code will pass for now:

# CPy 3.6
>>> '{!x}'.format(123)
Traceback (most recent call last):
  File "<ipython-input-2-b294f18f6498>", line 1, in <module>
    '{!x}'.format(123)
ValueError: Unknown conversion specifier x
# RustPython
>>>>> '{!x}'.format(123)
'123'

Copy link
Contributor

@silmeth silmeth Jun 11, 2019

Choose a reason for hiding this comment

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

next().map(FormatPreconversor::from_char) would just map the inner value of the Option if present – would do exactly what you did (pass the inner value to from_char() if it exists, or return None if the Option is None).

To raise an exception, you’d need to signal an error somehow (eg. return a Result<Option<FormatPreconversor>, String> instead of an Option<FormatPreconversor>) and then check for the result in call_object_format() and in case of an error, return a new vm error there (eg. by return Err(vm.new_value_error(…))). Or just by mapping the error:

let (preconversor, new_format_spec) = FormatPreconversor::parse_and_consume(format_spec);
let preconversor = preconversor.map_err(|msg| vm.new_value_error(msg))?;
// …

But then you’d probably want to return Result<(Option<FormatPreconversor>, &str), String> rather than (Result<Option<FormatPreconversor>, String>, &str), so that it’d look like:

let (preconversor, new_format_spec) = FormatPreconversor::parse_and_consume(format_spec)
    .map_err(|msg| vm.new_value_error(msg))?;

See https://doc.rust-lang.org/std/result/

}
}

pub fn parse_and_consume(text: &str) -> (Option<FormatPreconversor>, &str) {
let preconversor = FormatPreconversor::from_str(text);
match preconversor {
None => (None, text),
Some(_) => {
let mut chars = text.chars();
chars.next(); // Consume the bang
chars.next(); // Consume one r,s,a char
(preconversor, chars.as_str())
}
}
}
}

#[derive(Debug, Copy, Clone, PartialEq)]
pub enum FormatAlign {
Left,
Expand Down Expand Up @@ -56,6 +99,7 @@ pub enum FormatType {

#[derive(Debug, PartialEq)]
pub struct FormatSpec {
preconversor: Option<FormatPreconversor>,
fill: Option<char>,
align: Option<FormatAlign>,
sign: Option<FormatSign>,
Expand All @@ -75,6 +119,10 @@ fn get_num_digits(text: &str) -> usize {
text.len()
}

fn parse_preconversor(text: &str) -> (Option<FormatPreconversor>, &str) {
FormatPreconversor::parse_and_consume(text)
}

fn parse_align(text: &str) -> (Option<FormatAlign>, &str) {
let mut chars = text.chars();
let maybe_align = chars.next().and_then(FormatAlign::from_char);
Expand Down Expand Up @@ -186,7 +234,8 @@ fn parse_format_type(text: &str) -> (Option<FormatType>, &str) {
}

fn parse_format_spec(text: &str) -> FormatSpec {
let (fill, align, after_align) = parse_fill_and_align(text);
let (preconversor, after_preconversor) = parse_preconversor(text);
let (fill, align, after_align) = parse_fill_and_align(after_preconversor);
let (sign, after_sign) = parse_sign(after_align);
let (alternate_form, after_alternate_form) = parse_alternate_form(after_sign);
let after_zero = parse_zero(after_alternate_form);
Expand All @@ -196,6 +245,7 @@ fn parse_format_spec(text: &str) -> FormatSpec {
let (format_type, _) = parse_format_type(after_precision);

FormatSpec {
preconversor,
fill,
align,
sign,
Expand Down Expand Up @@ -467,6 +517,18 @@ impl FormatString {
String::new()
};

// On parts[0] can still be the preconversor (!r, !s, !a)
let parts: Vec<&str> = arg_part.splitn(2, '!').collect();
// before the bang is a keyword or arg index, after the comma is maybe a conversor spec.
let arg_part = parts[0];

let preconversor_spec = if parts.len() > 1 {
"!".to_string() + parts[1]
} else {
String::new()
};
let format_spec = preconversor_spec + &format_spec;

if arg_part.is_empty() {
return Ok(FormatPart::AutoSpec(format_spec));
}
Expand Down Expand Up @@ -551,6 +613,7 @@ mod tests {
#[test]
fn test_width_only() {
let expected = FormatSpec {
preconversor: None,
fill: None,
align: None,
sign: None,
Expand All @@ -566,6 +629,7 @@ mod tests {
#[test]
fn test_fill_and_width() {
let expected = FormatSpec {
preconversor: None,
fill: Some('<'),
align: Some(FormatAlign::Right),
sign: None,
Expand All @@ -581,6 +645,7 @@ mod tests {
#[test]
fn test_all() {
let expected = FormatSpec {
preconversor: None,
fill: Some('<'),
align: Some(FormatAlign::Right),
sign: Some(FormatSign::Minus),
Expand Down
12 changes: 10 additions & 2 deletions vm/src/obj/objstr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use unicode_casing::CharExt;
use unicode_segmentation::UnicodeSegmentation;
use unicode_xid::UnicodeXID;

use crate::format::{FormatParseError, FormatPart, FormatString};
use crate::format::{FormatParseError, FormatPart, FormatPreconversor, FormatString};
use crate::function::{single_or_tuple_any, OptionalArg, PyFuncArgs};
use crate::pyhash;
use crate::pyobject::{
Expand Down Expand Up @@ -1026,7 +1026,15 @@ fn count_char(s: &str, c: char) -> usize {
}

fn call_object_format(vm: &VirtualMachine, argument: PyObjectRef, format_spec: &str) -> PyResult {
let returned_type = vm.ctx.new_str(format_spec.to_string());
let (preconversor, new_format_spec) = FormatPreconversor::parse_and_consume(format_spec);
let argument = match preconversor {
Some(FormatPreconversor::Str) => vm.call_method(&argument, "__str__", vec![])?,
Some(FormatPreconversor::Repr) => vm.call_method(&argument, "__repr__", vec![])?,
Some(FormatPreconversor::Ascii) => vm.call_method(&argument, "__repr__", vec![])?,
None => argument,
};
let returned_type = vm.ctx.new_str(new_format_spec.to_string());

let result = vm.call_method(&argument, "__format__", vec![returned_type])?;
if !objtype::isinstance(&result, &vm.ctx.str_type()) {
let result_type = result.class();
Expand Down