Skip to content

Commit 3f4b4b5

Browse files
author
Bernhard Kaindl
committed
tools/generate-settings-dataclasses.py: Generate attr docstrings(1/2)
Prepare to generate docstrings for the attributes of settings_classes. Even though they are not parsed by python itself, they can be used to generate documentation for the settings dataclasses. To not require manual edits after code generation, it was needed to implement sorting of required attributes like connection_id, -type and uuid before other attributes. The new functions for this are derived from the existing sort function for the settings_classes where we need the connection as first attribute of the profile because likewise, it is not an optional attribute. The switch to enable generating the attribute docstrings is deferred to the next commit, to make reviewing this change above (which does not change the established code) and the minor change below easier: There is a minor change in the established code but only regarding sdbus_async/networkmanager/settings/bond.py whose attributes are not actually required and should be optional. Making them required was a quick hack around some issue I don't recall the exact details of, and should be adressed at its source anyway. This is the only code change as result of running the updated tool, and the next commit will enable the switch to generate the docstrings. To aid in understanding and (in the future, reworking) the code for use in the new upcoming generator based on Jinja2, this also adds a few comments and docstrings for navigating the code. I guess this may help be a step towards adressing PR #31, for documenting new Settings helpers classes.
1 parent ed2d179 commit 3f4b4b5

File tree

2 files changed

+68
-15
lines changed

2 files changed

+68
-15
lines changed

sdbus_async/networkmanager/settings/bond.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111
class BondSettings(NetworkManagerSettingsMixin):
1212
"""Bonding Settings"""
1313

14-
interface_name: str = field(
14+
interface_name: Optional[str] = field(
1515
metadata={'dbus_name': 'interface-name', 'dbus_type': 's'},
16+
default=None,
1617
)
17-
options: Dict[str, str] = field(
18+
options: Optional[Dict[str, str]] = field(
1819
metadata={'dbus_name': 'options', 'dbus_type': 'a{ss}'},
20+
default={'Mode': 'Balance-Rr'},
1921
)

tools/generate-settings-dataclasses.py

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env python
2+
# Based on NetworkManager-1.39.2/tools/generate-docs-nm-settings-docs-merge.py
23
# SPDX-License-Identifier: LGPL-2.1-or-later
34
import collections
45
import textwrap
@@ -57,6 +58,7 @@ def write(msg: Any) -> None:
5758

5859
###############################################################################
5960

61+
# Order of the connection types(settings_classes) in the profile dataclass:
6062
_setting_name_order = [
6163
"connection",
6264
"ipv4",
@@ -112,6 +114,7 @@ def write(msg: Any) -> None:
112114
"wpan",
113115
]
114116

117+
# List of modules for which we must generate an import of typing.List
115118
list_modules = [
116119
"bridge",
117120
"bridge_port",
@@ -134,22 +137,50 @@ def write(msg: Any) -> None:
134137
"wireless_security",
135138
]
136139

140+
# Order of special properties which must be first because they are not optional
141+
_property_name_order = [
142+
"connection.id",
143+
"connection.type",
144+
"connection.uuid",
145+
]
146+
147+
def _property_name_order_idx(name: str) -> int:
148+
"""Return the sort index for the given connection setting property"""
149+
try:
150+
return _property_name_order.index(name)
151+
except ValueError:
152+
# Properties not in _property_name_order are sorted last and then by their name
153+
return len(_property_name_order)
154+
155+
156+
def key_fcn_property_name(n1: str) -> Tuple[int, str]:
157+
"""key function for sorted(), used to sort connection properties"""
158+
return (_property_name_order_idx(n1), n1)
159+
137160

138161
def _setting_name_order_idx(name: str) -> int:
162+
"""Return the sort index for the given connection type(settings_class)"""
139163
try:
140164
return _setting_name_order.index(name)
141165
except ValueError:
142166
return len(_setting_name_order)
143167

144168

145169
def key_fcn_setting_name(n1: str) -> Tuple[int, str]:
170+
"""key function for sorted(), used to sort the settings_classes(connection types)"""
146171
return (_setting_name_order_idx(n1), n1)
147172

148173

149174
def iter_keys_of_dicts(
150-
dicts: List[Dict[str, Any]], key: Optional[Any] = None
175+
dicts: List[Dict[str, Any]], key: Optional[Any] = None, prefix: Optional[str] = ""
151176
) -> List[str]:
152-
keys = {k for d in dicts for k in d.keys()}
177+
"""Return a sorted list of settings_classes or connection properties
178+
179+
To support sorting the required properites of settings_classes first,
180+
the settingsname can be passed ad prefix argument to let the key function
181+
return the correct sort index for the given settingsname property.
182+
"""
183+
keys = {f'{prefix}{k}' for d in dicts for k in d.keys()}
153184
return sorted(keys, key=key)
154185

155186

@@ -185,6 +216,8 @@ def find_first_not_none(itr: List[Any]) -> Optional[Any]:
185216
return next((i for i in itr if i is not None), None)
186217

187218

219+
# The code quality of this function is poor(Sourcery says 5%), needs refactoring,
220+
# also see the rework in tools/generate-settings-dataclasses-jinja.py
188221
def main(settings_xml_path: Path) -> None:
189222
gl_input_files = [settings_xml_path]
190223

@@ -194,25 +227,30 @@ def main(settings_xml_path: Path) -> None:
194227
for root in xml_roots]
195228

196229
root_node = ElementTree.Element("nm-setting-docs")
197-
# print("")
230+
231+
# Generate the file header
198232
license = "SPDX-License-Identifier: LGPL-2.1-or-later"
199233
script = ("This file was generated by "
200234
"tools/generate-settings-dataclasses.py")
201235
header = f"""# {license}\n# {script},
202-
# if possible, please make changes by also updating the script.
203-
"""
236+
# if possible, please make changes by also updating the script.\n"""
204237
i = open("sdbus_async/networkmanager/settings/__init__.py", mode="w")
205238
p = open("sdbus_async/networkmanager/settings/profile.py", mode="r")
206239
profile_py = open("sdbus_async/networkmanager/settings/profile.py").read()
240+
241+
# define start and end markers for generating part of settings/profile.py:
207242
start_string = "# start of the generated list of settings classes\n"
208243
start_index = profile_py.index(start_string) + len(start_string)
209-
# end_string = " def to_dbus"
210244
end_string = " # end of the generated list of settings classes\n"
211245
end_index = profile_py.index(end_string)
212246
p = open("sdbus_async/networkmanager/settings/profile.py", mode="w")
247+
248+
# write the file headers
213249
i.write(header)
214250
p.write(profile_py[:start_index])
215251
classes = []
252+
253+
# generate the connection type settings classes:
216254
for settingname in iter_keys_of_dicts(settings_roots,
217255
key_fcn_setting_name):
218256
settings = [d.get(settingname) for d in settings_roots]
@@ -235,6 +273,8 @@ def main(settings_xml_path: Path) -> None:
235273
i.write(f"from .{module} import {classname}\n")
236274
classes.append(classname)
237275
f = open(f"sdbus_async/networkmanager/settings/{module}.py", mode="w")
276+
277+
# Generate module type headers with the needed import statements
238278
f.write(header)
239279
f.write("from __future__ import annotations\n")
240280
f.write("from dataclasses import dataclass, field\n")
@@ -255,6 +295,7 @@ def main(settings_xml_path: Path) -> None:
255295
f.write("from .datatypes import WireguardPeers as Peers\n")
256296
f.write("\n\n")
257297

298+
# Generate the settings_class and it's entry in profile.py:
258299
setting_node = ElementTree.SubElement(root_node, "setting")
259300
if module != "connection":
260301
p.write(f" {module}: Optional[{classname}] = field(\n")
@@ -265,10 +306,15 @@ def main(settings_xml_path: Path) -> None:
265306
f.write("@dataclass\n")
266307
f.write(f"class {classname}(NetworkManagerSettingsMixin):\n")
267308
setting_node.set("name", settingname)
309+
310+
# generate the docstring of the new settings_class
268311
desc = node_get_attr(settings, "description")
269312
f.write(' """' + desc + '"""\n\n')
270-
# node_set_attr(setting_node, "alias", settings)
271-
for property_name in iter_keys_of_dicts(properties):
313+
314+
# Generate the attributes of the settings_class for this profile type:
315+
316+
for property in iter_keys_of_dicts(properties, key_fcn_property_name, f'{settingname}.'):
317+
property_name = property[len(settingname)+1:]
272318
properties_attrs = [p.get(property_name) for p in properties]
273319
property_node = ElementTree.SubElement(setting_node, "property")
274320
property_node.set("name", property_name)
@@ -306,7 +352,7 @@ def main(settings_xml_path: Path) -> None:
306352
f.write(f" default={str(default).title()},\n )\n")
307353
else:
308354
attribute_type = dbus_type_name_map[dbustype]
309-
optional = module != "bond"
355+
optional = property not in _property_name_order
310356
if optional:
311357
f.write(f" {attribute}: Optional[{attribute_type}]")
312358
else:
@@ -329,21 +375,26 @@ def main(settings_xml_path: Path) -> None:
329375
if optional:
330376
f.write(f" default={str(default).title()},\n")
331377
f.write(" )\n")
378+
379+
# Generate docstrings for attributes: Not stored by python,
380+
# but is parsed for generating documentation and be red by
381+
# developers when they lookup the attribute declaration:
332382
generate_descriptions_for_attributes = False
333383
if generate_descriptions_for_attributes:
334384
desc = node_get_attr(properties_attrs, "description")
335385
wrapper = textwrap.TextWrapper(
336-
width=74,
386+
width=82,
337387
initial_indent=" ",
338388
subsequent_indent=" ",
339389
)
340-
lines = wrapper.wrap(text=f'"""{desc}')
390+
lines = wrapper.wrap(text=f'"""{desc}"""')
341391
if len(lines) == 1:
342392
print(lines[0] + '"""')
343393
else:
344394
for line in lines:
345-
f.write(line)
346-
f.write(' """')
395+
f.write(f'{line}\n')
396+
# If the closing """ shall be on a new line, use:
397+
# f.write(' """\n')
347398
f.write("")
348399
i.write('\n__all__ = (\n')
349400
for cls in classes:

0 commit comments

Comments
 (0)