|
| 1 | +#!/usr/bin/env python |
| 2 | +# SPDX-License-Identifier: LGPL-2.1-or-later |
| 3 | +from __future__ import annotations |
| 4 | +from argparse import ArgumentParser |
| 5 | +from pathlib import Path |
| 6 | +from xml.etree.ElementTree import parse, Element |
| 7 | + |
| 8 | +from typing import List, Optional |
| 9 | +from jinja2 import Environment |
| 10 | +import builtins |
| 11 | +import keyword |
| 12 | + |
| 13 | +dbus_to_python_extra_typing_imports = { |
| 14 | + "as": ("List", ), |
| 15 | + "au": ("List", ), |
| 16 | + "a{ss}": ("Dict", ), |
| 17 | + "aa{sv}": ("List", "Tuple", "Any"), |
| 18 | + "aau": ("List", ), |
| 19 | + "aay": ("List", ) |
| 20 | +} |
| 21 | + |
| 22 | +dbus_to_python_type_map = { |
| 23 | + "b": "bool", |
| 24 | + "s": "str", |
| 25 | + "i": "int", |
| 26 | + "u": "int", |
| 27 | + "t": "int", |
| 28 | + "x": "int", |
| 29 | + "y": "int", |
| 30 | + "as": "List[str]", |
| 31 | + "au": "List[int]", |
| 32 | + "ay": "bytes", |
| 33 | + "a{ss}": "Dict[str, str]", |
| 34 | + "aa{sv}": "List[Tuple[str, Any]]", |
| 35 | + "aau": "List[List[int]]", |
| 36 | + "aay": "List[bytes]", |
| 37 | + # Legacy types: |
| 38 | + "a(ayuay)": "array of legacy IPv6 address struct", |
| 39 | + "a(ayuayu)": "array of legacy IPv6 route struct", |
| 40 | +} |
| 41 | + |
| 42 | +dbus_name_type_map = { |
| 43 | + 'array of array of uint32': 'aau', |
| 44 | + 'array of byte array': 'aay', |
| 45 | + 'array of legacy IPv6 address struct': 'a(ayuay)', |
| 46 | + 'array of legacy IPv6 route struct': 'a(ayuayu)', |
| 47 | + 'array of string': 'as', |
| 48 | + 'array of uint32': 'au', |
| 49 | + 'array of vardict': 'aa{sv}', |
| 50 | + "array of 'a{sv}'": 'aa{sv}', # wireguard.peers uses this, fix NM upstream |
| 51 | + 'boolean': 'b', |
| 52 | + 'byte': 'y', |
| 53 | + 'byte array': 'ay', |
| 54 | + 'dict of string to string': 'a{ss}', |
| 55 | + 'int32': 'i', |
| 56 | + 'int64': 'x', |
| 57 | + 'string': 's', |
| 58 | + 'uint32': 'u', |
| 59 | + 'uint64': 't', |
| 60 | +} |
| 61 | + |
| 62 | +python_name_replacements = { |
| 63 | + 'type': 'connection_type', |
| 64 | + 'id': 'pretty_id', |
| 65 | +} |
| 66 | + |
| 67 | + |
| 68 | +def must_replace_name(name: str) -> bool: |
| 69 | + return (keyword.iskeyword(name) |
| 70 | + or keyword.issoftkeyword(name) |
| 71 | + or hasattr(builtins, name)) |
| 72 | + |
| 73 | + |
| 74 | +class NmSettingPropertyIntrospection: |
| 75 | + def __init__(self, name: str, |
| 76 | + description: str, |
| 77 | + name_upper: str, |
| 78 | + dbus_type: str, |
| 79 | + python_type: str, |
| 80 | + parent: NmSettingsIntrospection, |
| 81 | + default: Optional[str] = None, |
| 82 | + ) -> None: |
| 83 | + self.name = name |
| 84 | + self.description = description |
| 85 | + self.name_upper = name_upper |
| 86 | + self.python_name = name_upper.lower() |
| 87 | + self.dbus_type = dbus_type |
| 88 | + self.python_type = python_type |
| 89 | + self.default = default |
| 90 | + |
| 91 | + if must_replace_name(self.python_name): |
| 92 | + self.python_name = (f"{parent.name_upper.lower()}" |
| 93 | + f"_{self.python_name}") |
| 94 | + |
| 95 | + extra_typing = dbus_to_python_extra_typing_imports.get(dbus_type) |
| 96 | + if extra_typing is not None: |
| 97 | + parent.typing_imports.update(extra_typing) |
| 98 | + |
| 99 | + |
| 100 | +class NmSettingsIntrospection: |
| 101 | + def __init__(self, name: str, description: str, name_upper: str, |
| 102 | + ) -> None: |
| 103 | + self.name = name |
| 104 | + self.description = description |
| 105 | + self.name_upper = name_upper |
| 106 | + self.python_class_name = name.capitalize() + 'Settings' |
| 107 | + |
| 108 | + self.typing_imports = {'Optional'} |
| 109 | + |
| 110 | + self.properties: List[NmSettingPropertyIntrospection] = [] |
| 111 | + |
| 112 | + |
| 113 | +def convert_property(node: Element, |
| 114 | + parent: NmSettingsIntrospection |
| 115 | + ) -> NmSettingPropertyIntrospection: |
| 116 | + options = node.attrib |
| 117 | + |
| 118 | + unconverted_type = options.pop('type') |
| 119 | + try: |
| 120 | + dbus_type = dbus_name_type_map[unconverted_type] |
| 121 | + except KeyError: |
| 122 | + dbus_type = dbus_name_type_map[unconverted_type.split('(')[1][:-1]] |
| 123 | + |
| 124 | + options['dbus_type'] = dbus_type |
| 125 | + options['python_type'] = dbus_to_python_type_map[dbus_type] |
| 126 | + |
| 127 | + return NmSettingPropertyIntrospection(**options, parent=parent) |
| 128 | + |
| 129 | + |
| 130 | +def generate_introspection(root: Element) -> List[NmSettingsIntrospection]: |
| 131 | + settings_introspection: List[NmSettingsIntrospection] = [] |
| 132 | + for setting_node in root: |
| 133 | + setting = NmSettingsIntrospection(**setting_node.attrib) |
| 134 | + setting.properties.extend( |
| 135 | + (convert_property(x, setting) for x in setting_node) |
| 136 | + ) |
| 137 | + |
| 138 | + settings_introspection.append(setting) |
| 139 | + |
| 140 | + return settings_introspection |
| 141 | + |
| 142 | + |
| 143 | +setttngs_template_str = """# SPDX-License-Identifier: LGPL-2.1-or-later |
| 144 | +# This file was generated by tools/generate-settings-dataclasses-jinja.py, |
| 145 | +# if possible, please make changes by also updating the script. |
| 146 | +from __future__ import annotations |
| 147 | +from dataclasses import dataclass, field |
| 148 | +from typing import {{ setting.typing_imports|sort|join(', ') }} |
| 149 | +from .base import NetworkManagerSettingsMixin |
| 150 | +
|
| 151 | +
|
| 152 | +@dataclass |
| 153 | +class {{ setting.python_class_name }}(NetworkManagerSettingsMixin): |
| 154 | + \"""{{ setting.description }}\""" |
| 155 | +{% for property in setting.properties %} |
| 156 | + {{ property.python_name }}: Optional[{{ property.python_type }}] = field( |
| 157 | + metadata={ |
| 158 | + 'dbus_name': '{{ property.name }}', |
| 159 | + 'dbus_type': '{{ property.dbus_type }}', |
| 160 | + }, |
| 161 | + default=None, |
| 162 | + ){% endfor %} |
| 163 | +
|
| 164 | +""" |
| 165 | + |
| 166 | +jinja_env = Environment() |
| 167 | +settings_template = jinja_env.from_string(setttngs_template_str) |
| 168 | + |
| 169 | + |
| 170 | +def main(settings_xml_path: Path) -> None: |
| 171 | + tree = parse(settings_xml_path) |
| 172 | + introspection = generate_introspection(tree.getroot()) |
| 173 | + |
| 174 | + settings_dir = Path('sdbus_async/networkmanager/settings/') |
| 175 | + for setting in introspection: |
| 176 | + setting_py_file = settings_dir / (setting.name_upper.lower() + '.py') |
| 177 | + with open(setting_py_file, mode='w') as f: |
| 178 | + f.write(settings_template.render(setting=setting)) |
| 179 | + |
| 180 | + |
| 181 | +if __name__ == '__main__': |
| 182 | + arg_parser = ArgumentParser() |
| 183 | + arg_parser.add_argument( |
| 184 | + 'nm_settings_xml', |
| 185 | + type=Path, |
| 186 | + default=Path('man/nm-settings-docs-dbus.xml'), |
| 187 | + ) |
| 188 | + args = arg_parser.parse_args() |
| 189 | + |
| 190 | + main(args.nm_settings_xml) |
0 commit comments