5

Following a remark here: How to define a new string formatter, I tried subclassing string.Formatter. Here is what I've done. Unfortunately I seem to have broken it in the process

import string
from math import floor, log10

class CustFormatter(string.Formatter):
    "Defines special formatting"
    def __init__(self):
        super(CustFormatter, self).__init__()

    def powerise10(self, x):
        if x == 0: return 0, 0
        Neg = x < 0
        if Neg: x = -x
        a = 1.0 * x / 10**(floor(log10(x)))
        b = int(floor(log10(x)))
        if Neg: a = -a
        return a, b

    def eng(self, x):
        a, b = self.powerise10(x)
        if -3 < b < 3: return "%.4g" % x
        a = a * 10**(b%3)
        b = b - b%3
        return "%.4g*10^%s" % (a, b)

    def format_field(self, value, format_string):
      # handle an invalid format
      if format_string == "i":
          return self.eng(value)
      else:
          return super(CustFormatter,self).format_field(value, format_string)

fmt = CustFormatter()
print('{}'.format(0.055412))
print(fmt.format("{0:i} ", 55654654231654))
print(fmt.format("{} ", 0.00254641))

As if as in the last line, I don't refer to the variables by position, I get a KeyError. It is obviously expecting a key which is optional in the original class but I don't understand why and I am not sure what I've done wrong.

dreftymac
  • 31,404
  • 26
  • 119
  • 182
Cambium
  • 169
  • 7

2 Answers2

5

str.format does auto numbering, while string.Formatter does not.

Modifying __init__ and overriding get_value will do the trick.

def __init__(self):
    super(CustFormatter, self).__init__()
    self.last_number = 0

def get_value(self, key, args, kwargs):
    if key == '':
        key = self.last_number
        self.last_number += 1
    return super(CustFormatter, self).get_value(key, args, kwargs)

BTW, above code does not strictly mimic str.format behavior. str.format complains if we mix auto numbering with manual number, but above does not.

>>> '{} {1}'.format(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: cannot switch from automatic field numbering to manual field specification
>>> '{0} {}'.format(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: cannot switch from manual field specification to automatic field numbering
falsetru
  • 357,413
  • 63
  • 732
  • 636
  • 1
    Btw it seems auto numbering has been added natively to `string.Formatter` since Python 3.6, see this commit: [`7ce9074`](https://github.com/python/cpython/commit/7ce90743a1948680f409bcc02f56f4f100df1a04) – Delgan Jan 11 '20 at 16:46
1

Good news: you have not done anything wrong. Bad news: that's how string.Formatter behaves, it does not support {}-like positional format. So, the last call will fail even without any subclassing. Good news: that's can be fixed by overriding the parse method:

import string

class CF(string.Formatter):
    def parse(self, s):
        position = 0
        for lit, name, spec, conv in super(CF, self).parse(s):
            if not name:
                name = str(position)
                position += 1
            yield lit, name, spec, conv

Bad news... Ah, no, that's basically it:

>>> CF().format('{} {}!', 'Hello', 'world')
'Hello world!'
bereal
  • 32,519
  • 6
  • 58
  • 104
  • Thank you, do you know what are the + and - of overriding parse or get_value as offered by the previous poster? Or is it equivalent? – Cambium Feb 09 '14 at 20:51
  • @Cambium falsetru's version looks more readable to me, besides, you need to override one method less (though, have to override the constructor and introduce an attribute). His commend about mixing the styles also applies to my code (though, you can extend either version to fix that). – bereal Feb 09 '14 at 21:00