Skip to content

Commit b165fe3

Browse files
author
Bernhard Kaindl
committed
Latest update of the code-generation tool for settings-dataclasses.
Not perfect yet, but the most common cases shoult be covered now. It needs the man/nm-settings-docs-dbus.xml from a build of NM. It can be improved to the point where the settings sub-package is fully generated.
1 parent 36ccf18 commit b165fe3

File tree

1 file changed

+285
-0
lines changed

1 file changed

+285
-0
lines changed
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
#!/usr/bin/env python
2+
# SPDX-License-Identifier: LGPL-2.1-or-later
3+
import collections
4+
from typing import Any, Dict, List, OrderedDict, Optional, Tuple
5+
import xml.etree.ElementTree as ElementTree
6+
import textwrap
7+
8+
9+
def dbg(msg: Any) -> None:
10+
print(f"{msg}")
11+
12+
13+
def write(msg: Any) -> None:
14+
print(f"{msg}")
15+
16+
17+
dbus_type_name_map = {
18+
"b": "bool",
19+
"s": "str",
20+
"i": "int",
21+
"u": "int",
22+
"t": "int",
23+
"x": "int",
24+
"y": "Byte",
25+
"as": "List[str]",
26+
"au": "List[int]",
27+
"ay": "bytes",
28+
"a{ss}": "Dict[str, str]",
29+
"a{sv}": "vardict",
30+
"aa{sv}": "List[Tuple[str, Any]]",
31+
"aau": "List[List[int]]",
32+
"aay": "List[bytes]",
33+
# Legacy types:
34+
"a(ayuay)": "array of legacy IPv6 address struct",
35+
"a(ayuayu)": "array of legacy IPv6 route struct",
36+
}
37+
dbus_name_type_map = {
38+
'array of array of uint32': 'aau',
39+
'array of byte array': 'aay',
40+
'array of legacy IPv6 address struct': 'a(ayuay)',
41+
'array of legacy IPv6 route struct': 'a(ayuayu)',
42+
'array of string': 'as',
43+
'array of uint32': 'au',
44+
'array of vardict': 'aa{sv}',
45+
'boolean': 'b',
46+
'byte': 'y',
47+
'byte array': 'ay',
48+
'dict of string to string': 'a{ss}',
49+
'int32': 'i',
50+
'int64': 'x',
51+
'string': 's',
52+
'uint32': 'u',
53+
'uint64': 't',
54+
'vardict': 'a{sv}',
55+
}
56+
57+
###############################################################################
58+
59+
_setting_name_order = [
60+
"connection",
61+
"ipv4",
62+
"ipv6",
63+
"generic",
64+
"802-3-ethernet",
65+
"802-11-wireless",
66+
"802-11-wireless-security",
67+
"gsm",
68+
"ethtool",
69+
"6lowpan",
70+
"802-1x",
71+
"adsl",
72+
"bluetooth",
73+
"bond",
74+
"bridge",
75+
"bridge-port",
76+
"cdma",
77+
"dcb",
78+
"dummy",
79+
"infiniband",
80+
"ip-tunnel",
81+
"macsec",
82+
"macvlan",
83+
"match",
84+
"802-11-olpc-mesh",
85+
"ovs-bridge",
86+
"ovs-dpdk",
87+
"ovs-interface",
88+
"ovs-patch",
89+
"ovs-port",
90+
"ppp",
91+
"pppoe",
92+
"proxy",
93+
"serial",
94+
"sriov",
95+
"tc",
96+
"team",
97+
"team-port",
98+
"tun",
99+
"user",
100+
"vlan",
101+
"vpn",
102+
"vrf",
103+
"vxlan",
104+
"wifi-p2p",
105+
"wimax",
106+
"wireguard",
107+
"wpan",
108+
]
109+
110+
111+
def _setting_name_order_idx(name: str) -> int:
112+
try:
113+
return _setting_name_order.index(name)
114+
except ValueError:
115+
return len(_setting_name_order)
116+
117+
118+
def key_fcn_setting_name(n1: str) -> Tuple[int, str]:
119+
return (_setting_name_order_idx(n1), n1)
120+
121+
122+
def iter_keys_of_dicts(
123+
dicts: List[Dict[str, Any]], key: Optional[Any] = None
124+
) -> List[str]:
125+
keys = {k for d in dicts for k in d.keys()}
126+
return sorted(keys, key=key)
127+
128+
129+
def node_to_dict(node: Any, tag: str, key_attr: str) -> Dict[str, Any]:
130+
dictionary = collections.OrderedDict()
131+
if node is not None:
132+
for n in node.iter(tag):
133+
k = n.get(key_attr)
134+
assert k is not None
135+
dictionary[k] = n
136+
return dictionary
137+
138+
139+
def node_get_attr(nodes: List[Optional[Any]], name: str) -> Any:
140+
for n in nodes:
141+
if n is None:
142+
continue
143+
x = n.get(name, None)
144+
if x:
145+
return x
146+
return None
147+
148+
149+
def node_set_attr(
150+
dst_node: Any, name: str, nodes: List[Optional[Any]]
151+
) -> None:
152+
x = node_get_attr(nodes, name)
153+
if x:
154+
dst_node.set(name, x)
155+
156+
157+
def find_first_not_none(itr: List[Any]) -> Optional[Any]:
158+
return next((i for i in itr if i is not None), None)
159+
160+
161+
###############################################################################
162+
gl_input_files = ["man/nm-settings-docs-dbus.xml"]
163+
164+
xml_roots = [ElementTree.parse(f).getroot() for f in gl_input_files]
165+
assert all(root.tag == "nm-setting-docs" for root in xml_roots)
166+
settings_roots = [node_to_dict(root, "setting", "name") for root in xml_roots]
167+
168+
root_node = ElementTree.Element("nm-setting-docs")
169+
print("")
170+
script = "This file was generated by tools/generate-settings-dataclasses.py"
171+
license_and_this_file_was_generated_by_script = f"""# {script},
172+
# if possible, please make changes by also updating the script.
173+
# SPDX-License-Identifier: LGPL-2.1-or-later
174+
"""
175+
i = open("sdbus_async/networkmanager/settings/__init__.py", mode="w")
176+
i.write(license_and_this_file_was_generated_by_script)
177+
classes = []
178+
for settingname in iter_keys_of_dicts(settings_roots, key_fcn_setting_name):
179+
settings = [d.get(settingname) for d in settings_roots]
180+
properties = [node_to_dict(s, "property", "name") for s in settings]
181+
if properties == [OrderedDict()]:
182+
continue
183+
module = settingname.replace('-', '_')
184+
for prefix in ["6", "802_11_", "802_1", "802_3_"]:
185+
if module.startswith(prefix):
186+
module = module.replace(prefix, "")
187+
break
188+
classname = module.replace("_", "").title() + "Settings"
189+
i.write(f"from .{module} import {classname}\n")
190+
classes.append(classname)
191+
f = open(f"sdbus_async/networkmanager/settings/{module}.py", mode="w")
192+
f.write(license_and_this_file_was_generated_by_script)
193+
f.write("from dataclasses import dataclass, field\n")
194+
f.write("from typing import List, Optional\n")
195+
f.write("from .base import NetworkManagerSettingsMixin\n")
196+
if settingname.startswith("ipv"):
197+
f.write("from .datatypes import AddressData, RouteData\n")
198+
f.write("\n\n")
199+
200+
setting_node = ElementTree.SubElement(root_node, "setting")
201+
print(f" {module}: Optional[{classname}] = field(")
202+
print(f" metadata={{'dbus_name': '{settingname}',")
203+
print(f" 'settings_class': {classname}}},")
204+
print(" default=None,")
205+
print(" )")
206+
print("")
207+
f.write("@dataclass\n")
208+
f.write(f"class {classname}(NetworkManagerSettingsMixin):\n")
209+
setting_node.set("name", settingname)
210+
desc = node_get_attr(settings, "description")
211+
f.write(' """' + desc + '"""\n\n')
212+
# node_set_attr(setting_node, "alias", settings)
213+
for property_name in iter_keys_of_dicts(properties):
214+
properties_attrs = [p.get(property_name) for p in properties]
215+
property_node = ElementTree.SubElement(setting_node, "property")
216+
property_node.set("name", property_name)
217+
t = node_get_attr(properties_attrs, "type")
218+
attribute = property_name.replace('-', '_')
219+
for builtin in ["id", "type"]:
220+
if attribute == builtin:
221+
attribute = f"{module}_{attribute}"
222+
223+
if not t:
224+
t = "string"
225+
if t.startswith("array of legacy"):
226+
print(" # Deprecated - TODO: ignore")
227+
continue
228+
if t not in dbus_name_type_map:
229+
print(f" # {settingname}.{property_name} type [{t}] not found")
230+
ty = t.replace(")", "")[-5:]
231+
if ty in dbus_name_type_map:
232+
t = ty
233+
if t in ["{sv}'"]:
234+
t = "{sv}"
235+
if t in ["array of 'a{sv}'"]:
236+
if f"{settingname}.{property_name}" == "wireguard.peers":
237+
print("# Has special class in settings.py")
238+
continue
239+
dbustype = dbus_name_type_map[t]
240+
if t == "array of vardict":
241+
default = node_get_attr(properties_attrs, "default")
242+
inner_cls = (
243+
property_name.title().replace("-", "").replace("data", "Data")
244+
)
245+
f.write(f" {attribute}: Optional[List[{inner_cls}]] = field(\n")
246+
f.write(f" metadata={{'dbus_name': '{property_name}',\n")
247+
f.write(f" 'dbus_type': '{dbustype}',\n")
248+
f.write(f" 'dbus_inner_class': {inner_cls}}},\n")
249+
f.write(f" default={str(default).title()},\n )\n")
250+
else:
251+
attribute_type = dbus_type_name_map[dbustype]
252+
f.write(f" {attribute}: Optional[{attribute_type}] = field(\n")
253+
meta = f"'dbus_name': '{property_name}', 'dbus_type':@'{dbustype}'"
254+
line = "metadata={" + meta + "},"
255+
wrapper = textwrap.TextWrapper(
256+
width=80,
257+
initial_indent=" ",
258+
subsequent_indent=" ",
259+
)
260+
word_list = wrapper.wrap(text=line)
261+
for element in word_list:
262+
f.write(element.replace(":@", ": ") + '\n')
263+
default = node_get_attr(properties_attrs, "default")
264+
if default in ["{}", "0", "-1"]:
265+
default = "None"
266+
f.write(f" default={str(default).title()},\n )\n")
267+
generate_descriptions_for_attributes = False
268+
if generate_descriptions_for_attributes:
269+
desc = node_get_attr(properties_attrs, "description")
270+
wrapper = textwrap.TextWrapper(
271+
width=74, initial_indent=" ", subsequent_indent=" "
272+
)
273+
word_list = wrapper.wrap(text=f'"""{desc}')
274+
if len(word_list) == 1:
275+
print(word_list[0] + '"""')
276+
else:
277+
for element in word_list:
278+
f.write(element)
279+
f.write(' """')
280+
f.write("")
281+
# node_set_attr(property_node, "alias", properties_attrs)
282+
i.write('__all__ = (')
283+
for dclass in classes:
284+
i.write(f"'{dclass}', ")
285+
i.write(")\n")

0 commit comments

Comments
 (0)