Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Apply the {!r} on str.format calls
By the Python docs, the `!` forces a conversion of the argument before
applying the normal formating logic described after the `:` marker.

See: https://docs.python.org/3.4/library/string.html#format-string-syntax
  • Loading branch information
alanjds committed Jun 9, 2019
commit f0a2b4c50bc0b638d5257f12bae2e9d3f2966934
18 changes: 16 additions & 2 deletions tests/snippets/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,28 @@
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'
assert 'a' <= 'a'
Expand Down
44 changes: 28 additions & 16 deletions vm/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,39 @@ pub enum FormatPreconversor {
}

impl FormatPreconversor {
fn from_char(c: char) -> Option<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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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)]
Expand Down Expand Up @@ -95,20 +120,7 @@ fn get_num_digits(text: &str) -> usize {
}

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

match chars.next() {
None => (None, text), // Should fail instead?
Some(c) => {
match FormatPreconversor::from_char(c) {
Some(preconversor) => (Some(preconversor), chars.as_str()),
None => (None, text), // Should fail instead?
}
},
}
FormatPreconversor::parse_and_consume(text)
}

fn parse_align(text: &str) -> (Option<FormatAlign>, &str) {
Expand Down Expand Up @@ -506,7 +518,7 @@ impl FormatString {
};

// On parts[0] can still be the preconversor (!r, !s, !a)
let parts: Vec<&str> = arg_part .splitn(2, '!').collect();
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];

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