Skip to content

slongfield/pyfmt

Repository files navigation

pyfmt

Build Status

pyfmt implements Python's advanced string formatting in Golang.

This is an alternative to the fmt package's Sprintf-style string formatting and mimics the .format() style formatting available in Python >2.6. Under the hood, it still uses the go 'fmt' library, for better compatibility with Go types at the expense of not being as exact of a clone of Python format strings.

Braces {} are used to indicate 'format items', anything outside of braces will be emitted directly into the output string, and anything inside will be used to get values from the other function arguments and format them. The one exception is double braces, '{{' and '}}', which will cause literal '{' and '}' runes to be emitted in the output.

Each format item consists of a 'field name', which indicates which value from the argument list to use, and a 'format specifier', which indicates how to format that item.

Requires Golang 1.5 or newer for formatting, 1.9 or newer to run the tests.

Functions

pyfmt implements three functions, 'Fmt', 'Must', and 'Error'. 'Fmt' formats, but may return an error as detailed below. 'Must' formats, but will panic when 'Fmt' would return an error, and 'Error' acts like 'Fmt', but returns an error type. In the event that there's an error formatting the error, 'Error' includes the format error and as much of the formatted string as possible.

All of them take a format string and arguments to be used in formatting that string.

Getting Values from Field Names

Values can be fetched from field names in two forms: simple names or compound names. All compound names build off of simple names.

Simple field names:

The simplest look up treats the argument list as just a list. There are two possible ways to look up elements from this list. First, by {}, which gets the 'next' item and by {n}, which gets the nth item. Accessing these two ways is independent but cannot be mixed, and

  pyfmt.Must("{} {} {}", ...)

is equivalent to

  pyfmt.Must("{0} {1} {2}", ...)

Accessing an element that's outside the list range will return an error (with Fmt) or panic (with Must).

The first element in the list is treated specially if it's a struct or a map with string keys, allowing the elements from that struct or map can be directly accessed. For instance:

  pyfmt.Must("{test}", map[string]int{"test": 5}) --> "5"

similarly for structs:

  pyfmt.Must("{test}": myStruct{test: 5}) --> "5"

Attempting to read from an undefined key will return an error or panic, depending on if it was accessed with Fmt or Must.

Compound field names:

If the value referenced by the field is itself a List, map[string]interface{}, or struct, it can be further accessed in the format string.

Lists are accessed with square brackets:

  pyfmt.Must("{0[0]}", []string{"test"}) --> "test"

similarly, maps are accessed with square brackets:

  pyfmt.Must("{0[test]}", map[string]interface{}{"test": "42"}) --> "42"

and struct fields are accessed with a period ('.')

  pyfmt.Must("{foo.bar.baz}", MyStruct{foo: Foo{bar: Bar{baz: "test"}}}) --> "test"

Formatting

If after a simple or complex field name, there's a ':', what follows is considered to be the format specifier. If a type satisfies the PyFormat interface (discussed below), the format specifier will be passed to that, but otherwise, it will fall back to the default formatter, which expects the standard format specifier:

  [[fill]align][sign][#][0][minimumwidth][.precision][type]

The optional align feature can be one of the following:

  '<': left-aligned
  '>': right-aligned (this is the default)
  '=': padding after the sign, but before the digits (e.g., for +000042)
  '^': centered

If an align flag is defined, a 'fill' character can also be defined. If undefined, space (' ') will be used.

The optional 'sign' is only valid for numeric types and can be:

  '+': Show sign for both positive and negative numbers
  '-': Show sign only for negative numbers (default)
  ' ': use a leading space for positive numbers

If # is present, when using the binary, octal, or hex types, a '0b', '0o', or '0x' will be prepended, respectively.

The minimumwidth field specifies a minimum width, which is helpful when used with alignment. If preceded with a zero, numbers will be zero-padded.

The precision field specifies a maximum width for non-floating point, non-integer types, and the number of points to show after the decimal point for floating types.

The 'type' format determines what type the value will be formatted as.

For integers:

  'b' - Binary, base 2
  'd' - Decimal, base 10 (default)
  'o' - Octal, base 8
  'x' - Hexadecimal, base 16
  'X' - Hexadecimal, base 16, using upper-case letters

For floats and complex numbers:

  'e' - Scientific notation
  'E' - Similar to e, but uppercase
  'f' - Fixed point, displays the number as a fixed-point number.
  'F' - Same, but uppercase.
  'g' - General format, prints as a fixed point unless it's too large, then switches to scientific
        notation. (default)
  'G' - Similar to g, but uses capital letters
  '%' - Percentage, multiplies the number by 100 and displays it with a '%' sign. Can also be
        applied to integer types.

Special Formatting Types

For some types (most notably structs), the default formatter doesn't quite give enough information to understand the value after its printed, so it's useful to get more accurate Go representations. Additionally, sometimes it's useful to print the type of a variable while formatting it. For these, pyfmt allows for some special formatting types that aren't in the Python format syntax.

  'r' - convert the value to its Go-syntax representation
  't' - convert the value to its Go type
  's' - if printing a struct, print the struct field names

These are equivalent to the %#v, %T and %+v format strings in the "fmt" package, but don't have an exact equivalent in Python.

Custom formatters

Internally, pyfmt uses Go's fmt package, so existing types satisfying its Formatter, GoStringer, or Stringer interfaces will use those implementations as appropriate.

If the type satisfies the PyFormatter interface, the format specifier will be passed to that function, for custom formatting. Due to the limits of Golang reflection, if accessing a struct sub-field that has a custom formatter, the struct field must be exported for pyfmt to access the custom Formatter. This is similar to the default 'fmt' package, which doesn't apply custom Stringer implementations to unexported struct fields.

TODOs

  • Improve performance. Some of the string manipulations allocate more frequency than they need to, causing slowdown relative to the built-in 'fmt' library.