288 lines
7.4 KiB
Python
288 lines
7.4 KiB
Python
|
import collections
|
||
|
|
||
|
|
||
|
def get_named_tuple(name, dict):
|
||
|
return collections.namedtuple(name, dict.keys())(*dict.values())
|
||
|
|
||
|
|
||
|
Align = get_named_tuple(
|
||
|
"align",
|
||
|
{"default": "", "left": "<", "right": ">", "center": "^", "right_sign": "="},
|
||
|
)
|
||
|
|
||
|
Group = get_named_tuple("group", {"no": "", "yes": ","})
|
||
|
|
||
|
Padding = get_named_tuple("padding", {"no": "", "yes": "0"})
|
||
|
|
||
|
Prefix = get_named_tuple(
|
||
|
"prefix",
|
||
|
{
|
||
|
"yocto": 10**-24,
|
||
|
"zepto": 10**-21,
|
||
|
"atto": 10**-18,
|
||
|
"femto": 10**-15,
|
||
|
"pico": 10**-12,
|
||
|
"nano": 10**-9,
|
||
|
"micro": 10**-6,
|
||
|
"milli": 10**-3,
|
||
|
"none": None,
|
||
|
"kilo": 10**3,
|
||
|
"mega": 10**6,
|
||
|
"giga": 10**9,
|
||
|
"tera": 10**12,
|
||
|
"peta": 10**15,
|
||
|
"exa": 10**18,
|
||
|
"zetta": 10**21,
|
||
|
"yotta": 10**24,
|
||
|
},
|
||
|
)
|
||
|
|
||
|
Scheme = get_named_tuple(
|
||
|
"scheme",
|
||
|
{
|
||
|
"default": "",
|
||
|
"decimal": "r",
|
||
|
"decimal_integer": "d",
|
||
|
"decimal_or_exponent": "g",
|
||
|
"decimal_si_prefix": "s",
|
||
|
"exponent": "e",
|
||
|
"fixed": "f",
|
||
|
"percentage": "%",
|
||
|
"percentage_rounded": "p",
|
||
|
"binary": "b",
|
||
|
"octal": "o",
|
||
|
"lower_case_hex": "x",
|
||
|
"upper_case_hex": "X",
|
||
|
"unicode": "c",
|
||
|
},
|
||
|
)
|
||
|
|
||
|
Sign = get_named_tuple(
|
||
|
"sign",
|
||
|
{"default": "", "negative": "-", "positive": "+", "parantheses": "(", "space": " "},
|
||
|
)
|
||
|
|
||
|
Symbol = get_named_tuple(
|
||
|
"symbol", {"no": "", "yes": "$", "binary": "#b", "octal": "#o", "hex": "#x"}
|
||
|
)
|
||
|
|
||
|
Trim = get_named_tuple("trim", {"no": "", "yes": "~"})
|
||
|
|
||
|
|
||
|
class Format:
|
||
|
def __init__(self, **kwargs):
|
||
|
self._locale = {}
|
||
|
self._nully = ""
|
||
|
self._prefix = Prefix.none
|
||
|
self._specifier = {
|
||
|
"align": Align.default,
|
||
|
"fill": "",
|
||
|
"group": Group.no,
|
||
|
"width": "",
|
||
|
"padding": Padding.no,
|
||
|
"precision": "",
|
||
|
"sign": Sign.default,
|
||
|
"symbol": Symbol.no,
|
||
|
"trim": Trim.no,
|
||
|
"type": Scheme.default,
|
||
|
}
|
||
|
|
||
|
valid_methods = [
|
||
|
m for m in dir(self.__class__) if m[0] != "_" and m != "to_plotly_json"
|
||
|
]
|
||
|
|
||
|
for kw, val in kwargs.items():
|
||
|
if kw not in valid_methods:
|
||
|
raise TypeError(
|
||
|
"{0} is not a format method. Expected one of".format(kw),
|
||
|
str(list(valid_methods)),
|
||
|
)
|
||
|
|
||
|
getattr(self, kw)(val)
|
||
|
|
||
|
def _validate_char(self, value):
|
||
|
self._validate_string(value)
|
||
|
|
||
|
if len(value) != 1:
|
||
|
raise ValueError("expected value to a string of length one")
|
||
|
|
||
|
def _validate_non_negative_integer_or_none(self, value):
|
||
|
if value is None:
|
||
|
return
|
||
|
|
||
|
if not isinstance(value, int):
|
||
|
raise TypeError("expected value to be an integer")
|
||
|
|
||
|
if value < 0:
|
||
|
raise ValueError("expected value to be non-negative", str(value))
|
||
|
|
||
|
def _validate_named(self, value, named_values):
|
||
|
if value not in named_values:
|
||
|
raise TypeError("expected value to be one of", str(list(named_values)))
|
||
|
|
||
|
def _validate_string(self, value):
|
||
|
if not isinstance(value, (str, "".__class__)):
|
||
|
raise TypeError("expected value to be a string")
|
||
|
|
||
|
# Specifier
|
||
|
def align(self, value):
|
||
|
self._validate_named(value, Align)
|
||
|
|
||
|
self._specifier["align"] = value
|
||
|
return self
|
||
|
|
||
|
def fill(self, value):
|
||
|
self._validate_char(value)
|
||
|
|
||
|
self._specifier["fill"] = value
|
||
|
return self
|
||
|
|
||
|
def group(self, value):
|
||
|
if isinstance(value, bool):
|
||
|
value = Group.yes if value else Group.no
|
||
|
|
||
|
self._validate_named(value, Group)
|
||
|
|
||
|
self._specifier["group"] = value
|
||
|
return self
|
||
|
|
||
|
def padding(self, value):
|
||
|
if isinstance(value, bool):
|
||
|
value = Padding.yes if value else Padding.no
|
||
|
|
||
|
self._validate_named(value, Padding)
|
||
|
|
||
|
self._specifier["padding"] = value
|
||
|
return self
|
||
|
|
||
|
def padding_width(self, value):
|
||
|
self._validate_non_negative_integer_or_none(value)
|
||
|
|
||
|
self._specifier["width"] = value if value is not None else ""
|
||
|
return self
|
||
|
|
||
|
def precision(self, value):
|
||
|
self._validate_non_negative_integer_or_none(value)
|
||
|
|
||
|
self._specifier["precision"] = ".{0}".format(value) if value is not None else ""
|
||
|
return self
|
||
|
|
||
|
def scheme(self, value):
|
||
|
self._validate_named(value, Scheme)
|
||
|
|
||
|
self._specifier["type"] = value
|
||
|
return self
|
||
|
|
||
|
def sign(self, value):
|
||
|
self._validate_named(value, Sign)
|
||
|
|
||
|
self._specifier["sign"] = value
|
||
|
return self
|
||
|
|
||
|
def symbol(self, value):
|
||
|
self._validate_named(value, Symbol)
|
||
|
|
||
|
self._specifier["symbol"] = value
|
||
|
return self
|
||
|
|
||
|
def trim(self, value):
|
||
|
if isinstance(value, bool):
|
||
|
value = Trim.yes if value else Trim.no
|
||
|
|
||
|
self._validate_named(value, Trim)
|
||
|
|
||
|
self._specifier["trim"] = value
|
||
|
return self
|
||
|
|
||
|
# Locale
|
||
|
def symbol_prefix(self, value):
|
||
|
self._validate_string(value)
|
||
|
|
||
|
if "symbol" not in self._locale:
|
||
|
self._locale["symbol"] = [value, ""]
|
||
|
else:
|
||
|
self._locale["symbol"][0] = value
|
||
|
|
||
|
return self
|
||
|
|
||
|
def symbol_suffix(self, value):
|
||
|
self._validate_string(value)
|
||
|
|
||
|
if "symbol" not in self._locale:
|
||
|
self._locale["symbol"] = ["", value]
|
||
|
else:
|
||
|
self._locale["symbol"][1] = value
|
||
|
|
||
|
return self
|
||
|
|
||
|
def decimal_delimiter(self, value):
|
||
|
self._validate_char(value)
|
||
|
|
||
|
self._locale["decimal"] = value
|
||
|
return self
|
||
|
|
||
|
def group_delimiter(self, value):
|
||
|
self._validate_char(value)
|
||
|
|
||
|
self._locale["group"] = value
|
||
|
return self
|
||
|
|
||
|
def groups(self, groups):
|
||
|
groups = (
|
||
|
groups
|
||
|
if isinstance(groups, list)
|
||
|
else [groups]
|
||
|
if isinstance(groups, int)
|
||
|
else None
|
||
|
)
|
||
|
|
||
|
if not isinstance(groups, list):
|
||
|
raise TypeError("expected groups to be an integer or a list of integers")
|
||
|
if len(groups) == 0:
|
||
|
raise ValueError(
|
||
|
"expected groups to be an integer or a list of " "one or more integers"
|
||
|
)
|
||
|
|
||
|
for group in groups:
|
||
|
if not isinstance(group, int):
|
||
|
raise TypeError("expected entry to be an integer")
|
||
|
|
||
|
if group <= 0:
|
||
|
raise ValueError("expected entry to be a non-negative integer")
|
||
|
|
||
|
self._locale["grouping"] = groups
|
||
|
return self
|
||
|
|
||
|
# Nully
|
||
|
def nully(self, value):
|
||
|
self._nully = value
|
||
|
return self
|
||
|
|
||
|
# Prefix
|
||
|
def si_prefix(self, value):
|
||
|
self._validate_named(value, Prefix)
|
||
|
|
||
|
self._prefix = value
|
||
|
return self
|
||
|
|
||
|
def to_plotly_json(self):
|
||
|
f = {}
|
||
|
f["locale"] = self._locale.copy()
|
||
|
f["nully"] = self._nully
|
||
|
f["prefix"] = self._prefix
|
||
|
aligned = self._specifier["align"] != Align.default
|
||
|
f["specifier"] = "{}{}{}{}{}{}{}{}{}{}".format(
|
||
|
self._specifier["fill"] if aligned else "",
|
||
|
self._specifier["align"],
|
||
|
self._specifier["sign"],
|
||
|
self._specifier["symbol"],
|
||
|
self._specifier["padding"],
|
||
|
self._specifier["width"],
|
||
|
self._specifier["group"],
|
||
|
self._specifier["precision"],
|
||
|
self._specifier["trim"],
|
||
|
self._specifier["type"],
|
||
|
)
|
||
|
|
||
|
return f
|