Created
August 28, 2025 15:16
-
-
Save seblin/2ce44e57a02e8f381ec96d52fb343ded to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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