-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconfig_formatter.py
114 lines (100 loc) · 4.58 KB
/
config_formatter.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
"""The `config-parser` module provides utilities to format .ini and .cfg files."""
from typing import Tuple
import configupdater
import configupdater.builder
import configupdater.container
import configupdater.parser
__version__ = "1.2.0"
__all__ = ["ConfigFormatter"]
class ConfigFormatter:
"""A class used to reformat .ini/.cfg configurations."""
def prettify(self, string: str) -> str:
"""Transform the content of a .ini/.cfg file to make it more pleasing to the eye.
It preserves comments and ensures that it stays semantically identical to the input string
(the built-in Python module "configparser" serves as reference).
The accepted entry format is relatively strict, in particular:
- no duplicated section or option are allowed ;
- only "=" and ":" delimiters are considered ;
- only "#" and ";" comment prefixes are considered ;
- comments next to values are left untouched ;
- options without assigned value are not allowed ;
- empty lines in values are allowed but discouraged.
These settings are those used by default in the "ConfigParser" from the standard library.
"""
string = string.strip()
if not string:
return "\n"
base_config, has_dummy_top_section = self._load_config(string)
return self._format_config(base_config, has_dummy_top_section=has_dummy_top_section)
def _load_config(self, string: str) -> Tuple[configupdater.parser.Document, bool]:
"""Load the given string as a configuration document.
It also implements a workaround to handle configs that do not have a top section header.
"""
parser = configupdater.parser.Parser(
strict=True,
delimiters=("=", ":"),
comment_prefixes=("#", ";"),
inline_comment_prefixes=None,
allow_no_value=False,
empty_lines_in_values=True,
)
try:
return parser.read_string(string), False
except configupdater.MissingSectionHeaderError:
i = 1
while True:
dummy_section = f"[config-formatter-dummy-section-name-{i}]"
if dummy_section in string:
i += 1
continue
return parser.read_string(f"{dummy_section}\n{string}"), True
def _format_config(
self, source: configupdater.container.Container, *, has_dummy_top_section: bool
) -> str:
"""Recursively construct a normalized string of the given configuration."""
output = ""
for block in source.iter_blocks():
if isinstance(block, configupdater.Section):
if has_dummy_top_section:
has_dummy_top_section = False
else:
comment = block.raw_comment.strip()
if comment:
output += f"[{block.name}] {comment}\n"
else:
output += f"[{block.name}]\n"
output += self._format_config(block, has_dummy_top_section=False)
elif isinstance(block, configupdater.Comment):
for line in block.lines:
comment = line.strip()
output += f"{comment}\n"
elif isinstance(block, configupdater.Space):
if block.lines:
output += "\n"
elif isinstance(block, configupdater.Option):
key = block.raw_key
value = block.value
if value is None: # Should never happen in theory as "allow_no_value" is disabled.
output += f"{key}\n"
elif "\n" in value:
first, *lines = (line.strip() for line in value.splitlines())
if not first:
output += f"{key} =\n"
indent = 4
else:
output += f"{key} = {first}\n"
indent = len(key) + 3
for line in lines:
if line:
output += f"{' ' * indent}{line}\n"
else:
output += "\n"
else:
value = value.strip()
if value:
output += f"{key} = {value}\n"
else:
output += f"{key} =\n"
else:
raise ValueError("Encountered an unexpected block type: '%s'", type(block).__name__)
return output