Skip to content

Commit 1139ef5

Browse files
committed
string.Formatter: unnumbered key/attributes access
Fixes: python#27307
1 parent e018317 commit 1139ef5

File tree

2 files changed

+63
-21
lines changed

2 files changed

+63
-21
lines changed

Lib/string.py

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -186,19 +186,41 @@ def get_identifiers(self):
186186
# The field name parser is implemented in _string.formatter_field_name_split
187187

188188
class Formatter:
189+
190+
class AutoNumber:
191+
def __init__(self):
192+
self.field_number = 0
193+
194+
def __next__(self):
195+
if self.field_number is False:
196+
raise ValueError('cannot switch from manual field '
197+
'specification to automatic field '
198+
'numbering')
199+
self.field_number += 1
200+
return self.field_number - 1
201+
202+
def set_manual(self):
203+
if self.field_number:
204+
raise ValueError('cannot switch from automatic '
205+
'field numbering to manual '
206+
'field specification')
207+
self.field_number = False
208+
189209
def format(self, format_string, /, *args, **kwargs):
190210
return self.vformat(format_string, args, kwargs)
191211

192212
def vformat(self, format_string, args, kwargs):
193213
used_args = set()
194-
result, _ = self._vformat(format_string, args, kwargs, used_args, 2)
214+
result = self._vformat(format_string, args, kwargs, used_args, 2)
195215
self.check_unused_args(used_args, args, kwargs)
196216
return result
197217

198218
def _vformat(self, format_string, args, kwargs, used_args, recursion_depth,
199-
auto_arg_index=0):
219+
auto_number=None):
200220
if recursion_depth < 0:
201221
raise ValueError('Max string recursion exceeded')
222+
if not auto_number:
223+
auto_number = Formatter.AutoNumber()
202224
result = []
203225
for literal_text, field_name, format_spec, conversion in \
204226
self.parse(format_string):
@@ -212,22 +234,8 @@ def _vformat(self, format_string, args, kwargs, used_args, recursion_depth,
212234
# this is some markup, find the object and do
213235
# the formatting
214236

215-
# handle arg indexing when empty field_names are given.
216-
if field_name == '':
217-
if auto_arg_index is False:
218-
raise ValueError('cannot switch from manual field '
219-
'specification to automatic field '
220-
'numbering')
221-
field_name = str(auto_arg_index)
222-
auto_arg_index += 1
223-
elif field_name.isdigit():
224-
if auto_arg_index:
225-
raise ValueError('cannot switch from manual field '
226-
'specification to automatic field '
227-
'numbering')
228-
# disable auto arg incrementing, if it gets
229-
# used later on, then an exception will be raised
230-
auto_arg_index = False
237+
field_name = self.ensure_reference(field_name,
238+
auto_number)
231239

232240
# given the field_name, find the object it references
233241
# and the argument it came from
@@ -238,15 +246,15 @@ def _vformat(self, format_string, args, kwargs, used_args, recursion_depth,
238246
obj = self.convert_field(obj, conversion)
239247

240248
# expand the format spec, if needed
241-
format_spec, auto_arg_index = self._vformat(
249+
format_spec = self._vformat(
242250
format_spec, args, kwargs,
243251
used_args, recursion_depth-1,
244-
auto_arg_index=auto_arg_index)
252+
auto_number)
245253

246254
# format the object and append to the result
247255
result.append(self.format_field(obj, format_spec))
248256

249-
return ''.join(result), auto_arg_index
257+
return ''.join(result)
250258

251259

252260
def get_value(self, key, args, kwargs):
@@ -288,6 +296,23 @@ def parse(self, format_string):
288296
return _string.formatter_parser(format_string)
289297

290298

299+
# given a field_name and auto_number, return a version
300+
# starting with a name or index, taken from auto_number
301+
# if necessary; calls auto_number.set_manual()
302+
# if field_name already contains index
303+
# field_name: the field being checked, e.g. "",
304+
# ".name" or "[3]"
305+
# auto_number numbering source
306+
def ensure_reference(self, field_name, auto_number):
307+
first, _ = _string.formatter_field_name_split(field_name)
308+
if first == '':
309+
first = str(next(auto_number))
310+
field_name = first + field_name
311+
elif isinstance(first, int):
312+
auto_number.set_manual()
313+
return field_name
314+
315+
291316
# given a field_name, find the object it references.
292317
# field_name: the field being looked up, e.g. "0.name"
293318
# or "lookup[3]"

Lib/test/test_string.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import unittest
22
import string
33
from string import Template
4+
import types
45

56

67
class ModuleTest(unittest.TestCase):
@@ -101,6 +102,22 @@ def test_index_lookup(self):
101102
with self.assertRaises(KeyError):
102103
fmt.format("{0[2]}{0[0]}", {})
103104

105+
def test_auto_numbering_lookup(self):
106+
fmt = string.Formatter()
107+
namespace = types.SimpleNamespace(foo=types.SimpleNamespace(bar='baz'))
108+
widths = [None, types.SimpleNamespace(qux=4)]
109+
self.assertEqual(
110+
fmt.format("{.foo.bar:{[1].qux}}", namespace, widths), 'baz ')
111+
112+
def test_auto_numbering_reenterability(self):
113+
class ReenteringFormatter(string.Formatter):
114+
def format_field(self, value, format_spec):
115+
return (self.format(' {:{}} ', value, int(format_spec) - 1)
116+
if format_spec.isdigit() and int(format_spec) > 0
117+
else super().format_field(value, format_spec))
118+
fmt = ReenteringFormatter()
119+
self.assertEqual(fmt.format("{:{}}", 42, 5), ' 42 ')
120+
104121
def test_override_get_value(self):
105122
class NamespaceFormatter(string.Formatter):
106123
def __init__(self, namespace={}):

0 commit comments

Comments
 (0)