pct2fmt.py (original) (raw)

# Copyright (C) 2009 Vinay Sajip.

# Licensed under the PSF licence.

# Exploratory code to convert %-format strings to {}-format.

import re

PATTERN = re.compile(r'''

% # specifier start

(\((?P[^)]+)\))? # optional mapping key

(?P[#+0 -]*) # conversion flags

(?P<min_width>(\*|([1-9][0-9]*)))? # minimum width

(\.(?P(\*|([1-9][0-9]*))))? # precision

(?P<len_modifier>[hlL])? # length modifier

(?P[diouxXeEfFgGcrs%]) # conversion

''', re.VERBOSE)

class PercentConversion:

'''Represents a single conversion group.'''

def __init__(self, converter, match):

'''

Initialize using the regex match information.

The converter is passed because we need to keep track of the

positional argument index, so that we can support minimum width

and precision values of '*'.

'''

self.source = match.group(0)

d = match.groupdict()

self.span = match.span()

s = d['key']

if s is None:

self.key = converter.next_index()

else:

self.key = s

s = d['min_width']

if s == '*':

#TODO {} representation is hard-wired, could generalise this

#if needed.

self.min_width = '{%s}' % converter.next_index()

else:

self.min_width = s

s = d['precision']

if s == '*':

self.precision = '{%s}' % converter.next_index()

else:

self.precision = s

#len_modifier not used

#self.len_modifier = d['len_modifier']

self.flags = d['flags']

self.conversion = d['conversion']

def as_brace(self):

'''Return representation of this conversion group for {}-formatting.'''

#TODO: '#', ' ' flags not handled

conversion = self.conversion

if conversion == '%':

result = '%'

else:

key = self.key

flags = self.flags

if self.min_width is None:

align = ''

elif '-' in flags:

align = '' # could use '<', but as it's the default...

elif '0' in flags and conversion in 'diouxX':

align = '='

else:

align = '>'

sign = ''

if '+' in flags:

sign = '+'

elif ' ' in flags:

sign = ' '

alt = ''

fill = ''

if '0' in flags and '-' not in flags and conversion not in 'crs':

fill = '0'

if '#' in flags and conversion in 'diuxX': # exclude 'o'

alt = '#'

transform = ''

if conversion in 'iu':

conversion = 'd'

#%s is interpreted as calling str() on the operand, so

#we specify !s in the {}-format. If we don't do this, then

#we can't convert the case where %s is used to print e.g. integers

#or floats.

elif conversion in 'rs':

transform = '!' + conversion

conversion = ''

prefix = '%s%s' % (key, transform)

suffix = '%s%s%s%s' % (fill, align, sign, alt)

if self.min_width:

suffix += self.min_width

if self.precision:

suffix += '.' + self.precision

suffix += conversion

result = prefix

if suffix:

result += ':' + suffix

result = '{%s}' % result

return result

class PercentToBraceConverter:

'''Utility class to convert a %-format string to a {}-format string.'''

def __init__(self, percent_format):

self.initial = percent_format

self.argindex = 0

def next_index(self):

'''Get the next argument index.'''

result = '%d' % self.argindex

self.argindex += 1

return result

def convert(self):

'''Perform the conversion, and return its result.'''

s = self.initial.replace('{', '{{').replace('}', '}}')

result = [c for c in s] # convert to mutable form

matches = [m for m in PATTERN.finditer(s)]

conversions = []

if matches:

for m in matches:

conversions.append(PercentConversion(self, m))

#Go backwards so that span values remain valid as we go

for conv in reversed(conversions):

s, e = conv.span

result[s:e] = conv.as_brace()

return ''.join(result), conversions

def main():

'''Quick-n-dirty test harness for PercentToBraceConverter.'''

import sys

from random import choice

if sys.version_info[0] == 3:

get_str = input

else:

get_str = raw_input

class Dummy:

def __str__(self):

return "I'm a dummy at %#x" % id(self)

INSTANCE_VALUE = Dummy()

INTEGER_VALUE = 0x1234

NEGATIVE_VALUE = -0x5678

FLOAT_VALUE = 3.14159265358979323846

LARGE_FLOAT_VALUE = 31415926535323846e108

NEGATIVE_FLOAT_VALUE = -2.71828182845904523536

LARGE_NEGATIVE_FLOAT_VALUE = -271828182845904523536e108

INT_VALUES = (INTEGER_VALUE, NEGATIVE_VALUE)

FLOAT_VALUES = (FLOAT_VALUE, NEGATIVE_FLOAT_VALUE, LARGE_FLOAT_VALUE, LARGE_NEGATIVE_FLOAT_VALUE)

STRING_VALUE = 'abcd'

STRING_VALUES = (INTEGER_VALUE, NEGATIVE_VALUE, FLOAT_VALUE, LARGE_FLOAT_VALUE, NEGATIVE_FLOAT_VALUE, LARGE_NEGATIVE_FLOAT_VALUE, STRING_VALUE, INSTANCE_VALUE)

VALUE_MAP = {

'd' : INT_VALUES,

'i' : INT_VALUES,

'o' : INT_VALUES,

'u' : INT_VALUES,

'x' : INT_VALUES,

'X' : INT_VALUES,

'e' : FLOAT_VALUES,

'E' : FLOAT_VALUES,

'f' : FLOAT_VALUES,

'F' : FLOAT_VALUES,

'g' : FLOAT_VALUES,

'G' : FLOAT_VALUES,

'c' : (INTEGER_VALUE,),

'r' : STRING_VALUES,

's' : STRING_VALUES,

}

while True:

s = get_str("Enter a %-format string:")

if not s:

break

f, conversions = PercentToBraceConverter(s).convert()

print('%s -> %s' % (s, f))

if conversions:

values = []

for c in conversions:

vals = VALUE_MAP.get(c.conversion, None)

if vals:

values.append(choice(vals))

values = tuple(values)

print("%-20s: %s" % ('Example raw values', ', '.join([repr(v) for v in values])))

print("%-20s: %s" % ('Output using %', s % values))

print("%-20s: %s" % ('Output using {}', f.format(*values)))

if __name__ == "__main__":

main()