Skip to content

Instantly share code, notes, and snippets.

@seblin
Created August 28, 2025 15:16
Show Gist options
  • Save seblin/2ce44e57a02e8f381ec96d52fb343ded to your computer and use it in GitHub Desktop.
Save seblin/2ce44e57a02e8f381ec96d52fb343ded to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
import dataclasses as dc
from enum import StrEnum
import re
import typing as tp
class Align(StrEnum):
LEFT = "<"
RIGHT = ">"
CENTER = "^"
PAD_AFTER_SIGN = "="
class Sign(StrEnum):
POS_AND_NEG = "+"
NEG_ONLY = "-"
SPACE_IF_POS = " "
class Grouping(StrEnum):
COMMA = ","
UNDERSCORE = "_"
class Type(StrEnum):
BINARY = "b"
CHARACTER = "c"
DECIMAL = "d"
SCIENTIFIC = "e"
SCIENTIFIC_UPPER = "E"
FIXED_POINT = "f"
FIXED_POINT_UPPER = "F"
GENERAL = "g"
GENERAL_UPPER = "G"
NUMBER = "n"
OCTAL = "o"
STRING = "s"
HEX = "x"
HEX_UPPER = "X"
PERCENTAGE = "%"
RE_FORMAT_SPEC = re.compile(
r'(?:(?P<fill>[\s\S])?(?P<align>[<>=^]))?'
r'(?P<sign>[- +])?'
r'(?P<pos_zero>z)?'
r'(?P<alt>#)?'
r'(?P<zero_padding>0)?'
r'(?P<width>\d+)?'
r'(?P<grouping>[_,])?'
r'(?:\.(?P<precision>\d+))?'
r'(?P<type>[bcdeEfFgGnosxX%])?'
)
SYMBOL_TABLE = {"pos_zero": "z", "alt": "#", "zero_padding": "0"}
@dc.dataclass
class FormatSpec:
# Based on https://stackoverflow.com/a/78351366
fill: str | None = None
align: Align | None = None
sign: Sign | None = None
pos_zero: bool = False
alt: bool = False
zero_padding: bool = False
width: int | None = None
grouping: Grouping | None = None
precision: int | None = None
type: Type | None = None
@staticmethod
def _get_type(field: dc.Field) -> tp.Any:
type_args = tp.get_args(field.type)
return type_args[0] if type_args else field.type
def _get_string(self, field: dc.Field) -> str:
if (value := getattr(self, field.name)) is None:
return ""
if issubclass(self._get_type(field), bool):
symbol = SYMBOL_TABLE[field.name]
return symbol if value else ""
if field.name == "precision":
return f".{value}"
return str(value)
def __str__(self) -> str:
strings = map(self._get_string, dc.fields(self))
return "".join(strings)
@classmethod
def _as_field_type(cls, field: dc.Field, value: tp.Any) -> tp.Any:
field_type = cls._get_type(field)
if issubclass(field_type, bool):
return value == SYMBOL_TABLE[field.name]
if value is not None:
return field_type(value)
return value
@classmethod
def from_string(cls, spec: str) -> tp.Self:
if not (match := RE_FORMAT_SPEC.fullmatch(spec)):
raise ValueError(f"Invalid format specifier: {spec!r}")
config = {
field.name: cls._as_field_type(field, match.group(field.name))
for field in dc.fields(cls)
}
return cls(**config)
def main():
x = 0.12345
fmt = FormatSpec.from_string("~^11.3f")
print(fmt) # ~^11.3f
print(f"My Number: '{x:{fmt}}'") # My Number: '~~~0.123~~~'
fmt.width -= 5
fmt.precision -= 1
fmt.fill = " "
print(fmt) # ^6.2f
print(f"My Number: '{x:{fmt}}'") # My Number: ' 0.12 '
print(repr(fmt))
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment