This repository was archived by the owner on Sep 12, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjonf.py
More file actions
151 lines (115 loc) · 4.1 KB
/
jonf.py
File metadata and controls
151 lines (115 loc) · 4.1 KB
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""JONF parser/formatter in Python"""
__version__ = "0.0.6"
__jonf_format_version__ = "0.0.13"
import json
from typing import Any, Callable, Tuple
BASIC_TYPES = (bool, int, float, str)
ARRAYS = (list, tuple)
OBJECTS = (dict,)
COLLECTIONS = ARRAYS + OBJECTS
EMPTIES: Tuple[Any, Any, Any, Any] = ("", [], (), {})
INDENT = " "
SPACES = " \t\r\n"
INNER_COMMENT_MARKS = tuple(space + "#" for space in SPACES)
OBJECT_KEY_MARKS_TO_QUOTE = SPACES + '"-='
def parse(text: str, json_parse: Callable[[str], Any] = json.loads) -> Any:
"""Parses JONF text to Python data
Args:
text: JONF text
json_parse: JSON parser to use, default is `json.loads`
Returns:
Python data, parsed from this text
Example::
text = textwrap.dedent(
'''\
compare =
- true
= true
'''
).rstrip()
assert jonf.parse(text) == {"compare": ["true", True]}
"""
raise NotImplementedError
def format(data: Any, json_format: Callable[[Any], str] = json.dumps) -> str:
"""Formats Python data to JONF text
Args:
data: Any data that can be converted to JSON
json_format: JSON formatter to use, default is `json.dumps`
Returns:
JONF text, representing this data
Example::
text = textwrap.dedent(
'''\
compare =
- true
= true
'''
).rstrip()
assert jonf.format({"compare": ["true", True]}) == text
"""
if _is_unquoted_string(data):
_, space, text = _format_item(data, is_unquoted_string=True)
if space == "\n":
return text
if data is None or isinstance(data, BASIC_TYPES) or data in EMPTIES:
return json_format(data)
if isinstance(data, ARRAYS):
return "\n".join("".join(_format_item(item)) for item in data)
if isinstance(data, OBJECTS):
chunks = []
for key, value in data.items():
if not isinstance(key, str):
raise TypeError(f"Object key should be string, found: {repr(key)}")
if any(
mark in key for mark in OBJECT_KEY_MARKS_TO_QUOTE
) or not _is_unquoted_string(key):
key = json_format(key)
marker, space, value = _format_item(value)
chunks.append(f"{key} {marker}{space}{value}")
return "\n".join(chunks)
# This will probably raise `TypeError: Object of type ... is not JSON serializable`
# unless custom JSON formatter knows how to serialize it
return json_format(data)
def _format_item(item: Any, is_unquoted_string: bool = False) -> Tuple[str, str, str]:
"""
Formats Python nested item to JONF marker, space, text
Args:
item: Any data that can be converted to JSON
is_unquoted_string: If we already found that `_is_unquoted_string(item) is True`
Returns:
marker: Either "-" or "="
space: Either " " or "\n"
text: JONF text, representing this item
"""
if is_unquoted_string or _is_unquoted_string(item):
marker = "-"
text = item
else:
marker = "="
text = format(item)
lines = text.splitlines()
assert lines
if len(lines) == 1 and (not isinstance(item, COLLECTIONS) or item in EMPTIES):
space = " "
else:
space = "\n"
text = "\n".join(((INDENT + line) if line else "") for line in lines)
return marker, space, text
def _is_unquoted_string(data: Any) -> bool:
"""
Finds if this data should be represented as a JONF unquoted string
"""
return bool(
isinstance(data, str)
and data
and data.strip() == data
and not _has_comment(data)
)
def _has_comment(data: str) -> bool:
"""
Finds if this string data should be formatted as a quoted JSON string,
because otherwise a part of this data will become a JONF comment
"""
return data.startswith("#") or any(mark in data for mark in INNER_COMMENT_MARKS)
# TODO: Compare performance with regex
# TODO: Full coverage by tests, at the moment only the examples from docs are tested