1"""Generate a wrapper class from DBus introspection data"""
2import argparse
3from textwrap import indent
4import xml.etree.ElementTree as ET
5
6from jeepney.wrappers import Introspectable
7from jeepney.io.blocking import open_dbus_connection, Proxy
8from jeepney import __version__
9
10class Method:
11    def __init__(self, xml_node):
12        self.name = xml_node.attrib['name']
13        self.in_args = []
14        self.signature = []
15        for arg in xml_node.findall("arg[@direction='in']"):
16            try:
17                name = arg.attrib['name']
18            except KeyError:
19                name = 'arg{}'.format(len(self.in_args))
20            self.in_args.append(name)
21            self.signature.append(arg.attrib['type'])
22
23    def _make_code_noargs(self):
24        return ("def {name}(self):\n"
25                "    return new_method_call(self, '{name}')\n").format(
26            name=self.name)
27
28    def make_code(self):
29        if not self.in_args:
30            return self._make_code_noargs()
31
32        args = ', '.join(self.in_args)
33        signature = ''.join(self.signature)
34        tuple = ('({},)' if len(self.in_args) == 1 else '({})').format(args)
35        return ("def {name}(self, {args}):\n"
36                "    return new_method_call(self, '{name}', '{signature}',\n"
37                "                           {tuple})\n").format(
38            name=self.name, args=args, signature=signature, tuple=tuple
39        )
40
41INTERFACE_CLASS_TEMPLATE = """
42class {cls_name}(MessageGenerator):
43    interface = {interface!r}
44
45    def __init__(self, object_path={path!r},
46                 bus_name={bus_name!r}):
47        super().__init__(object_path=object_path, bus_name=bus_name)
48"""
49
50class Interface:
51    def __init__(self, xml_node, path, bus_name):
52        self.name = xml_node.attrib['name']
53        self.path = path
54        self.bus_name = bus_name
55        self.methods = [Method(node) for node in xml_node.findall('method')]
56
57    def make_code(self):
58        cls_name = self.name.split('.')[-1]
59        chunks = [INTERFACE_CLASS_TEMPLATE.format(cls_name=cls_name,
60              interface=self.name, path=self.path, bus_name=self.bus_name)]
61        for method in self.methods:
62            chunks.append(indent(method.make_code(), ' ' * 4))
63        return '\n'.join(chunks)
64
65MODULE_TEMPLATE = '''\
66"""Auto-generated DBus bindings
67
68Generated by jeepney version {version}
69
70Object path: {path}
71Bus name   : {bus_name}
72"""
73
74from jeepney.wrappers import MessageGenerator, new_method_call
75
76'''
77
78# Jeepney already includes bindings for these common interfaces
79IGNORE_INTERFACES = {
80    'org.freedesktop.DBus.Introspectable',
81    'org.freedesktop.DBus.Properties',
82    'org.freedesktop.DBus.Peer',
83}
84
85def code_from_xml(xml, path, bus_name, fh):
86    if isinstance(fh, (bytes, str)):
87        with open(fh, 'w') as f:
88            return code_from_xml(xml, path, bus_name, f)
89
90    root = ET.fromstring(xml)
91    fh.write(MODULE_TEMPLATE.format(version=__version__, path=path,
92                                    bus_name=bus_name))
93
94    i = 0
95    for interface_node in root.findall('interface'):
96        if interface_node.attrib['name'] in IGNORE_INTERFACES:
97            continue
98        fh.write(Interface(interface_node, path, bus_name).make_code())
99        i += 1
100
101    return i
102
103def generate(path, name, output_file, bus='SESSION'):
104    conn = open_dbus_connection(bus)
105    introspectable = Proxy(Introspectable(path, name), conn)
106    xml, = introspectable.Introspect()
107    # print(xml)
108
109    n_interfaces = code_from_xml(xml, path, name, output_file)
110    print("Written {} interface wrappers to {}".format(n_interfaces, output_file))
111
112def main():
113    ap = argparse.ArgumentParser()
114    ap.add_argument('-n', '--name', required=True)
115    ap.add_argument('-p', '--path', required=True)
116    ap.add_argument('--bus', default='SESSION')
117    ap.add_argument('-o', '--output')
118    args = ap.parse_args()
119
120    output = args.output or (args.path[1:].replace('/', '_') + '.py')
121
122    generate(args.path, args.name, output, args.bus)
123
124
125if __name__ == '__main__':
126    main()
127