diff --git a/tests/snippets/strings.py b/tests/snippets/strings.py index 48534f1744..fc413c5e53 100644 --- a/tests/snippets/strings.py +++ b/tests/snippets/strings.py @@ -1,4 +1,4 @@ -from testutils import assert_raises +from testutils import assert_raises, AssertRaises assert "".__eq__(1) == NotImplemented assert "a" == 'a' @@ -338,3 +338,63 @@ def try_mutate_str(): assert "1a".islower() assert "가나다a".islower() assert "가나다A".isupper() + +# test str.format_map() +# +# The following tests were performed in Python 3.7.5: +# Python 3.7.5 (default, Dec 19 2019, 17:11:32) +# [GCC 5.4.0 20160609] on linux + +# >>> '{x} {y}'.format_map({'x': 1, 'y': 2}) +# '1 2' +assert '{x} {y}'.format_map({'x': 1, 'y': 2}) == '1 2' + +# >>> '{x:04d}'.format_map({'x': 1}) +# '0001' +assert '{x:04d}'.format_map({'x': 1}) == '0001' + +# >>> '{x} {y}'.format_map('foo') +# Traceback (most recent call last): +# File "", line 1, in +# TypeError: string indices must be integers +with AssertRaises(TypeError, None): + '{x} {y}'.format_map('foo') + +# >>> '{x} {y}'.format_map(['foo']) +# Traceback (most recent call last): +# File "", line 1, in +# TypeError: list indices must be integers or slices, not str +with AssertRaises(TypeError, None): + '{x} {y}'.format_map(['foo']) + +# >>> '{x} {y}'.format_map() +# Traceback (most recent call last): +# File "", line 1, in +# TypeError: format_map() takes exactly one argument (0 given) +with AssertRaises(TypeError, msg='TypeError: format_map() takes exactly one argument (0 given)'): + '{x} {y}'.format_map(), + +# >>> '{x} {y}'.format_map('foo', 'bar') +# Traceback (most recent call last): +# File "", line 1, in +# TypeError: format_map() takes exactly one argument (2 given) +with AssertRaises(TypeError, msg='TypeError: format_map() takes exactly one argument (2 given)'): + '{x} {y}'.format_map('foo', 'bar') + +# >>> '{x} {y}'.format_map({'x': 1}) +# Traceback (most recent call last): +# File "", line 1, in +# KeyError: 'y' +with AssertRaises(KeyError, msg="KeyError: 'y'"): + '{x} {y}'.format_map({'x': 1}) + +# >>> '{x} {y}'.format_map({'x': 1, 'z': 2}) +# Traceback (most recent call last): +# File "", line 1, in +# KeyError: 'y' +with AssertRaises(KeyError, msg="KeyError: 'y'"): + '{x} {y}'.format_map({'x': 1, 'z': 2}) + +# >>> '{{literal}}'.format_map('foo') +# '{literal}' +assert '{{literal}}'.format_map('foo') == '{literal}' diff --git a/vm/src/obj/objstr.rs b/vm/src/obj/objstr.rs index dbb188601b..71e39f2059 100644 --- a/vm/src/obj/objstr.rs +++ b/vm/src/obj/objstr.rs @@ -651,6 +651,32 @@ impl PyString { } } + /// S.format_map(mapping) -> str + /// + /// Return a formatted version of S, using substitutions from mapping. + /// The substitutions are identified by braces ('{' and '}'). + #[pymethod] + fn format_map(vm: &VirtualMachine, args: PyFuncArgs) -> PyResult { + if args.args.len() != 2 { + return Err(vm.new_type_error(format!( + "format_map() takes exactly one argument ({} given)", + args.args.len() - 1 + ))); + } + + let zelf = &args.args[0]; + let format_string_text = get_value(zelf); + match FormatString::from_str(format_string_text.as_str()) { + Ok(format_string) => perform_format_map(vm, &format_string, &args.args[1]), + Err(err) => match err { + FormatParseError::UnmatchedBracket => { + Err(vm.new_value_error("expected '}' before end of string".to_string())) + } + _ => Err(vm.new_value_error("Unexpected error parsing format string".to_string())), + }, + } + } + /// Return a titlecased version of the string where words start with an /// uppercase character and the remaining characters are lowercase. #[pymethod] @@ -1590,6 +1616,31 @@ fn perform_format( Ok(vm.ctx.new_str(final_string)) } +fn perform_format_map( + vm: &VirtualMachine, + format_string: &FormatString, + dict: &PyObjectRef, +) -> PyResult { + let mut final_string = String::new(); + for part in &format_string.format_parts { + let result_string: String = match part { + FormatPart::AutoSpec(_) | FormatPart::IndexSpec(_, _) => { + return Err( + vm.new_value_error("Format string contains positional fields".to_string()) + ); + } + FormatPart::KeywordSpec(keyword, format_spec) => { + let argument = dict.get_item(keyword, &vm)?; + let result = call_object_format(vm, argument.clone(), &format_spec)?; + get_value(&result) + } + FormatPart::Literal(literal) => literal.clone(), + }; + final_string.push_str(&result_string); + } + Ok(vm.ctx.new_str(final_string)) +} + impl PySliceableSequence for String { type Sliced = String;