root/livinglogic.python.xist/src/ll/stringformat.py @ 3440:e9732c5765c6

Revision 3440:e9732c5765c6, 14.5 KB (checked in by Walter Doerwald <walter@…>, 11 years ago)

Implement format for the rest of the types by reusing a module from the Python SVN repo.

Line 
1# Python string formatting
2
3from math import log
4try:
5    import locale
6except:
7    locale = None
8try:
9    import fpformat
10except:
11    fpformat = None
12
13# Except for errors in the format string.
14class FormatError(StandardError):
15    pass
16
17strict_format_errors = False
18
19class ConversionTypes:
20    Binary      = 'b'   # Base-2
21    Character   = 'c'   # Print as character
22    Decimal     = 'd'   # Decimal integer
23    Exponent    = 'e'   # Exponential notation
24    ExponentUC  = 'E'   # Exponential notation with upper case 'E'
25    Fixed       = 'f'   # Fixed-point
26    FixedUC     = 'F'   # Fixed-point with upper case
27    General     = 'g'   # General number notation
28    GeneralUC   = 'G'   # General number notation with upper case 'E'
29    Number      = 'n'   # Number in locale-specific format
30    Octal       = 'o'   # Octal
31    Repr        = 'r'   # In repr() format
32    String      = 's'   # Convert using str()
33    Hex         = 'x'   # Base 16
34    HexUC       = 'X'   # Base 16 upper case
35    Percentage  = '%'   # As percentage
36
37ConversionTypes.All = set(ConversionTypes.__dict__.values())
38
39# Parse the standard conversion spec. Note that I don't use
40# regex here because I'm trying to eliminate external dependencies
41# as much as possible.
42def parse_std_conversion(spec):
43    length = None
44    precision = None
45    ctype = None
46    align = None
47    fill_char = None
48    sign = None
49
50    index = 0
51    spec_len = len(spec)
52
53    # If the second char is an alignment token,
54    # then parse the fill char
55    if spec_len >=2 and spec[ 1 ] in '<>=^':
56        fill_char = spec[ 0 ]
57        align = spec[ 1 ]
58        index = 2
59    # Otherwise, parse the alignment token
60    elif spec_len >= 1 and spec[ 0 ] in '<>=^':
61        align = spec[ 0 ]
62        index = 1
63
64    # Parse the various sign options
65    if index < spec_len and spec[ index ] in ' +-(':
66        sign = spec[ index ]
67        index += 1
68        if index < spec_len and spec[ index ] == ')':
69            index += 1
70
71    # The special case for 0-padding (backwards compat)
72    if fill_char == None and index < spec_len and spec[ index ] == '0':
73        fill_char = '0'
74        if align == None:
75            align = '='
76        index += 1
77
78    # Parse field width
79    saveindex = index
80    while index < spec_len and spec[index].isdigit():
81        index += 1
82
83    if index > saveindex:
84        length = int(spec[saveindex : index])
85
86    # Parse field precision
87    if index < spec_len and spec[index] == '.':
88        index += 1
89        saveindex = index
90        while index < spec_len and spec[index].isdigit():
91            index += 1
92        if index > saveindex:
93            precision = int(spec[saveindex:index])
94
95    # Finally, parse the type field
96    remaining = spec_len - index
97    if remaining > 1:
98        return None     # Invalid conversion spec
99
100    if remaining == 1:
101        ctype = spec[index]
102        if ctype not in ConversionTypes.All:
103            return None
104
105    return (fill_char, align, sign, length, precision, ctype)
106
107# Convert to int, and split into sign part and magnitude part
108def to_int(val):
109    val = int(val)
110    if val < 0: return '-', -val
111    return '+', val
112
113# Convert to float, and split into sign part and magnitude part
114def to_float(val):
115    val = float(val)
116    if val < 0: return '-', -val
117    return '+', val
118
119# Pure python implementation of the C printf 'e' format specificer
120def sci(val,precision,letter='e'):
121    # Split into sign and magnitude (not really needed for formatting
122    # since we already did this part. Mainly here in case 'sci'
123    # ever gets split out as an independent function.)
124    sign = ''
125    if val < 0:
126        sign = '-'
127        val = -val
128
129    # Calculate the exponent
130    exp = int(floor(log(val,10)))
131
132    # Normalize the value
133    val *= 10**-exp
134
135    # If the value is exactly an integer, then we don't want to
136    # print *any* decimal digits, regardless of precision
137    if val == floor(val):
138        val = int(val)
139    else:
140        # Otherwise, round it based on precision
141        val = round(val,precision)
142        # The rounding operation might have increased the
143        # number to where it is no longer normalized, if so
144        # then adjust the exponent.
145        if val >= 10.0:
146            exp += 1
147            val = val * 0.1
148
149    # Convert the exponent to a string using only str().
150    # The existing C printf always prints at least 2 digits.
151    esign = '+'
152    if exp < 0:
153        exp = -exp
154        esign = '-'
155    if exp < 10: exp = '0' + str(exp)
156    else: exp = str(exp)
157
158    # The final result
159    return sign + str(val) + letter + esign + exp
160
161# The standard formatter
162def format_builtin_type(value, spec):
163
164    # Parse the conversion spec
165    conversion = parse_std_conversion(spec)
166    if conversion is None:
167        raise FormatError("Invalid conversion spec: " + spec)
168
169    # Unpack the conversion spec
170    fill_char, align, sign_char, length, precision, ctype = conversion
171
172    # Choose a default conversion type
173    if ctype == None:
174        if isinstance(value, int) or isinstance(value, long):
175            ctype = ConversionTypes.Decimal
176        elif isinstance(value, float):
177            ctype = ConversionTypes.General
178        else:
179            ctype = ConversionTypes.String
180
181    sign = None
182
183    # Conversion types that resolve to other types
184    if ctype == ConversionTypes.Percentage:
185        ctype = ConversionTypes.Fixed
186        value = float(value) * 100.0
187
188    if ctype == ConversionTypes.Binary:
189        result = ''
190        sign, value = to_int(value)
191        while value:
192            if value & 1: result = '1' + result
193            else: result = '0' + result
194            value >>= 1
195        if len(result) == 0:
196            result = '0'
197    elif ctype == ConversionTypes.Octal:
198        sign, value = to_int(value)
199        result = oct(value)
200    elif ctype == ConversionTypes.Hex:
201        sign, value = to_int(value)
202        result = hex(value)
203    elif ctype == ConversionTypes.HexUC:
204        sign, value = to_int(value)
205        result = hex(value).upper()
206    elif ctype == ConversionTypes.Character:
207        result = chr(int( value) )
208    elif ctype == ConversionTypes.Decimal:
209        sign, value = to_int(value)
210        result = str(value)
211    elif ctype == ConversionTypes.Fixed or ctype == ConversionTypes.FixedUC:
212        sign, value = to_float(value)
213        if fpformat and precision is not None:
214            result = fpformat.fix(value, precision)
215        else:
216            result = str(value)
217    elif ctype == ConversionTypes.General or ctype == ConversionTypes.GeneralUC:
218        #Same as "e" if exponent is less than -4 or greater than precision, "f" otherwise.
219        sign, value = to_float(value)
220        if fpformat and precision is not None:
221            if value < 0.0001 or value > 10**precision:
222                result = fpformat.sci(value, precision)
223            else:
224                result = fpformat.fix(value, precision)
225            if ctype == ConversionTypes.GeneralUC:
226                result = result.upper()
227        else:
228            result = str(value)
229    elif ctype == ConversionTypes.Exponent or ctype == ConversionTypes.ExponentUC:
230        sign, value = to_float(value)
231        if precision is None: precision = 5 # Duh, I dunno
232        result = sci(value, precision, ctype)
233    elif ctype == ConversionTypes.Number:
234        sign, value = to_float(value)
235        if locale:
236            # For some reason, this is not working the way I would
237            # expect
238            result = locale.format("%f", float( value) )
239        else:
240            result = str(value)
241    elif ctype == ConversionTypes.String:
242        result = str(value)
243    elif ctype == ConversionTypes.Repr:
244        result = repr(value)
245
246    # Handle the sign logic
247    prefix = ''
248    suffix = ''
249    if sign == '-':
250        if sign_char == '(': prefix, suffix = '(', ')'
251        else: prefix = '-'
252    elif sign == '+':
253        if sign_char == '+': prefix = '+'
254        elif sign_char == ' ': prefix = ' '
255
256    # Handle the padding logic
257    if length is not None:
258        padding = length - len(result) - len(prefix) - len(suffix)
259        if fill_char is None:
260            fill_char = ' '
261        if padding > 0:
262            if align == '>' or align == '^':
263                return fill_char * padding + prefix + result + suffix
264            elif align == '=':
265                return prefix + fill_char * padding + result + suffix
266            else:
267                return prefix + result + suffix + fill_char * padding
268
269    return prefix + result + suffix
270
271def cformat(template, format_hook, args, kwargs):
272    # Using array types since we're going to be growing
273    # a lot.
274    from array import array
275    array_type = 'c'
276
277    # Use unicode array if the original string is unicode.
278    if isinstance(template, unicode): array_type = 'u'
279    buffer = array(array_type)
280
281    # Track which arguments actuallly got used
282    unused_args = set(kwargs.keys())
283    unused_args.update(range(0, len(args)))
284
285    # Inner function to format a field from a value and
286    # conversion spec. Most details missing.
287    def format_field(value, cspec):
288
289        # See if there's a hook
290        if format_hook:
291            v = format_hook(value, cspec)
292            if v is not None:
293                return str(v)
294
295        # See if there's a __format__ method
296        elif hasattr(value, '__format__'):
297            return value.__format__(cspec)
298
299        # Default formatting
300        return format_builtin_type(value, cspec)
301
302    # Parse a field specification. Returns True if it was a valid
303    # field, False if it was merely an escaped brace. (We do it
304    # this way to avoid lookahead.)
305    def parse_field(buffer):
306
307        # A separate array for the field spec.
308        fieldspec = array(array_type)
309
310        # Consume from the template iterator.
311        for index, ch in template_iter:
312            # A sub-field. We just interpret it like a normal field,
313            # and append to the fieldspec.
314            if ch == '{':
315                # If the very first character is an open brace, then
316                # assume its an escaped (doubled) brace.
317                if len(fieldspec) == 0:
318                    return False
319
320                # Here's where we catch that doubled brace
321                if not parse_field(fieldspec):
322                    buffer.extend('{')
323                    return True
324
325            # End of field. Now interpret it.
326            elif ch == '}':
327                # Convert the array to string or uni
328                if array_type == 'u':
329                  fieldspec = fieldspec.tosunicode()
330                else:
331                  fieldspec = fieldspec.tostring()
332
333                # Check for conversion spec
334                name = fieldspec
335                conversion = ''
336                parts = fieldspec.split(':', 1)
337                if len(parts) > 1:
338                    name, conversion = parts
339
340                try:
341                    first_time = True
342                    # Split the field name into subfields
343                    for namepart in name.split('.'):
344                        # Split that part by open bracket chars
345                        keyparts = namepart.split('[')
346                        # The first part is just a bare name
347                        key = keyparts[0]
348
349                        # Empty strings are not allowed as field names
350                        if key == '':
351                            raise FormatError("empty field name at char " + str(index))
352
353                        # The first name in the sequence is used to index
354                        # the args/kwargs arrays. Subsequent names are used
355                        # on the result of the previous operation.
356                        if first_time:
357                            first_time = False
358
359                            # Attempt to coerce key to integer
360                            try:
361                                key = int(key)
362                                value = args[key]
363                            except ValueError:
364                                # Keyword args are strings, not uni (so far)
365                                value = kwargs[key]
366
367                            # If we got no exception, then remove from
368                            # unused args
369                            unused_args.remove(key)
370                        else:
371                            # This is not the first time, so get
372                            # an attribute
373                            value = getattr(value, key)
374
375                        # Now process any bracket expressions which followed
376                        # the first part.
377                        for key in keyparts[1:]:
378                            endbracket = key.find(']')
379                            if endbracket < 0 or endbracket != len(key) - 1:
380                                raise FormatError("Invalid field syntax at position " + str(index))
381
382                            # Strip off the closing bracket and try to coerce to int
383                            key = key[:-1]
384                            try:
385                                key = int(key)
386                            except ValueError:
387                                pass
388
389                            # Get the attribute
390                            value = value[key]
391
392                except (AttributeError,KeyError,IndexError), e:
393                    if strict_format_errors: raise
394                    buffer.extend('?' + e.__class__.__name__ + '?')
395                    return True
396
397                buffer.extend(format_field(value, conversion))
398                return True
399            else:
400                fieldspec.append(ch)
401
402        raise FormatError("unmatched open brace at position " + str(index))
403
404    # Construct an iterator from the template
405    template_iter = enumerate(template)
406    prev = None
407    for index, ch in template_iter:
408        if prev == '}':
409            if ch != '}':
410                raise FormatError("unmatched close brace")
411            else:
412                buffer.append('}')
413                prev = None
414                continue
415
416        if ch == '{':
417            # It's a field
418            if not parse_field(buffer):
419                buffer.extend('{')
420        elif ch != '}':
421            buffer.append(ch)
422        prev = ch
423
424    if prev == '}':
425        raise FormatError("unmatched close brace")
426
427    # Complain about unused args
428    if unused_args and strict_format_errors:
429        raise FormatError(
430            "Unused arguments: "
431            + ",".join(str(x) for x in unused_args))
432
433    # Convert the array to its proper type
434    if isinstance(template, unicode):
435        return buffer.tounicode()
436    else:
437        return buffer.tostring()
438
439def format(template, *args, **kwargs):
440    return cformat(template, None, args, kwargs)
Note: See TracBrowser for help on using the browser.