FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Feature: str.format accepting !r, !s and !a by alanjds · Pull Request #1027 · RustPython/RustPython · GitHub

Repository navigation

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .py  (1) .rs  (2) All 2 file types selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
21 changes: 19 additions & 2 deletions tests/snippets/strings.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
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
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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'

silmeth Jun 11, 2019 •
edited
Loading

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
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

Back | FazBrowse Home | New Git URL