Skip to content

Commit

Permalink
Merge pull request skulpt#1539 from s-cork/fix/format-errors
Browse files Browse the repository at this point in the history
fix: a couple of bugs in formatting with tests
  • Loading branch information
s-cork authored Feb 9, 2024
2 parents 4d606f0 + e6dacdc commit 4b05db8
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 17 deletions.
60 changes: 43 additions & 17 deletions src/formatting.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,11 +279,14 @@ function formatString(format_spec) {
};

// str.format() implementation
const isDigit = /^\d+$/;

const regex = /{(((?:\d+)|(?:\w+))?((?:\.(\w+))|(?:\[((?:\d+)|(?:\w+))\])?))?(?:\!(.))?(?:\:([^}]*))?}/g;

function format(args, kwargs) {
// following PEP 3101
kwargs = kwargs || [];
const arg_dict = {};
const regex = /{(((?:\d+)|(?:\w+))?((?:\.(\w+))|(?:\[((?:\d+)|(?:\w+))\])?))?(?:\!([rs]))?(?:\:([^}]*))?}/g;
// ex: {o.name!r:*^+#030,.9b}
// Field 1, Field_name, o.name
// Field 2, arg_name, o
Expand All @@ -300,49 +303,72 @@ function format(args, kwargs) {
for (let i = 0; i < kwargs.length; i += 2) {
arg_dict[kwargs[i]] = kwargs[i + 1];
}
for (let i in args) {
arg_dict[i] = args[i];
}
let currentMode;
const manual = "manual field specification";
const auto = "automatic field numbering";
const checkMode = (newMode) => {
if (currentMode === undefined) {
currentMode = newMode;
} else if (currentMode !== newMode) {
throw new Sk.builtin.ValueError(`cannot switch from ${currentMode} to ${newMode}`);
}
};
const getArg = (key) => {
let rv;
if (typeof key === "number") {
checkMode(manual);
rv = args[key];
} else if (isDigit.test(key)) {
checkMode(auto);
rv = args[key];
} else {
rv = arg_dict[key];
if (rv === undefined) {
throw new Sk.builtin.KeyError(key);
}
}
if (rv === undefined) {
throw new Sk.builtin.IndexError(`Replacement index ${key} out of range for positional args tuple`);
}
return rv;
};

let index = 0;
function replFunc (substring, field_name, arg_name, attr_name, attribute_name, element_index, conversion, format_spec, offset, str_whole) {
let value;

if (element_index !== undefined && element_index !== "") {
let container = arg_dict[arg_name];
let container = getArg(arg_name);
if (container.constructor === Array) {
value = container[element_index];
} else if (/^\d+$/.test(element_index)) {
} else if (isDigit.test(element_index)) {
value = Sk.abstr.objectGetItem(container, new Sk.builtin.int_(parseInt(element_index, 10)), false);
} else {
value = Sk.abstr.objectGetItem(container, new Sk.builtin.str(element_index), false);
}
index++;
} else if (attribute_name !== undefined && attribute_name !== "") {
value = Sk.abstr.gattr(arg_dict[arg_name || index++], new Sk.builtin.str(attribute_name));
const arg = getArg(arg_name || index++);
value = Sk.abstr.gattr(arg, new Sk.builtin.str(attribute_name));
} else if (arg_name !== undefined && arg_name !== "") {
value = arg_dict[arg_name];
value = getArg(arg_name);
} else if (field_name === undefined || field_name === "") {
value = arg_dict[index];
value = getArg(index);
index++;
} else if (
field_name instanceof Sk.builtin.int_ ||
field_name instanceof Sk.builtin.float_ ||
field_name instanceof Sk.builtin.lng ||
/^\d+$/.test(field_name)
) {
value = arg_dict[field_name];
} else if (isDigit.test(field_name)) {
value = getArg(field_name);
index++;
}

if (conversion === "s") {
value = new Sk.builtin.str(value);
} else if (conversion === "r") {
value = Sk.builtin.repr(value);
} else if (conversion === "a") {
value = Sk.builtin.ascii(value);
} else if (conversion !== "" && conversion !== undefined) {
throw new Sk.builtin.ValueError("Unknown conversion specifier " + conversion);
}
// TODO "!a" I guess?

return Sk.abstr.objectFormat(value, new Sk.builtin.str(format_spec)).$jsstr();
};
Expand Down
67 changes: 67 additions & 0 deletions test/unit3/test_strformat.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
__author__ = "gerbal"

import unittest
import string

class string_format(unittest.TestCase):
def test_simple_position(self):
Expand Down Expand Up @@ -114,5 +115,71 @@ def test_scientific_notation(self):
self.assertEqual('1.234568e+16', "{:e}".format(12345678987654321))
self.assertEqual('1e+08', "{:.0e}".format(123456789))

def test_basic_formatter(self):
fmt = str
self.assertEqual(fmt.format("foo"), "foo")
self.assertEqual(fmt.format("foo{0}", "bar"), "foobar")
self.assertEqual(fmt.format("foo{1}{0}-{1}", "bar", 6), "foo6bar-6")

def test_format_keyword_arguments(self):
fmt = str
self.assertEqual(fmt.format("-{arg}-", arg='test'), '-test-')
self.assertRaises(KeyError, fmt.format, "-{arg}-")
self.assertEqual(fmt.format("-{self}-", self='test'), '-test-')
self.assertRaises(KeyError, fmt.format, "-{self}-")
self.assertEqual(fmt.format("-{format_string}-", format_string='test'),
'-test-')
self.assertRaises(KeyError, fmt.format, "-{format_string}-")

def test_auto_numbering(self):
fmt = str
self.assertEqual(fmt.format('foo{}{}', 'bar', 6),
'foo{}{}'.format('bar', 6))
self.assertEqual(fmt.format('foo{1}{num}{1}', None, 'bar', num=6),
'foo{1}{num}{1}'.format(None, 'bar', num=6))
# self.assertEqual(fmt.format('{:^{}}', 'bar', 6),
# '{:^{}}'.format('bar', 6))
# self.assertEqual(fmt.format('{:^{}} {}', 'bar', 6, 'X'),
# '{:^{}} {}'.format('bar', 6, 'X'))
# self.assertEqual(fmt.format('{:^{pad}}{}', 'foo', 'bar', pad=6),
# '{:^{pad}}{}'.format('foo', 'bar', pad=6))

with self.assertRaises(ValueError):
fmt.format('foo{1}{}', 'bar', 6)

with self.assertRaises(ValueError):
fmt.format('foo{}{1}', 'bar', 6)

def test_conversion_specifiers(self):
fmt = str
self.assertEqual(fmt.format("-{arg!r}-", arg='test'), "-'test'-")
self.assertEqual(fmt.format("{0!s}", 'test'), 'test')
self.assertRaises(ValueError, fmt.format, "{0!h}", 'test')
# issue13579
self.assertEqual(fmt.format("{0!a}", 42), '42')
self.assertEqual(fmt.format("{0!a}", string.ascii_letters),
"'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'")
self.assertEqual(fmt.format("{0!a}", chr(255)), "'\\xff'")
self.assertEqual(fmt.format("{0!a}", chr(256)), "'\\u0100'")

def test_name_lookup(self):
fmt = str
class AnyAttr:
def __getattr__(self, attr):
return attr
x = AnyAttr()
self.assertEqual(fmt.format("{0.lumber}{0.jack}", x), 'lumberjack')
with self.assertRaises(AttributeError):
fmt.format("{0.lumber}{0.jack}", '')

def test_index_lookup(self):
fmt = str
lookup = ["eggs", "and", "spam"]
self.assertEqual(fmt.format("{0[2]}{0[0]}", lookup), 'spameggs')
with self.assertRaises(IndexError):
fmt.format("{0[2]}{0[0]}", [])
with self.assertRaises(KeyError):
fmt.format("{0[2]}{0[0]}", {})

if __name__ == '__main__':
unittest.main()

0 comments on commit 4b05db8

Please sign in to comment.