doxysip 0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
doxysip/__init__.py ADDED
@@ -0,0 +1,53 @@
1
+ # SPDX-License-Identifier: BSD-3-Clause
2
+
3
+ # Copyright (c) 2026 C. Savergne <csavergne@yahoo.com>
4
+
5
+
6
+ from sphinx.domains import Domain
7
+
8
+ from .directives import (
9
+ SIPSpecificationDirective,
10
+ SIPModuleDirective,
11
+ SIPClassDirective,
12
+ SIPNamespaceDirective,
13
+ SIPEnumDirective,
14
+ SIPMethodDirective,
15
+ SIPPropertyDirective,
16
+ SIPVariableDirective,
17
+ SIPMacroConstantDirective,
18
+ )
19
+
20
+
21
+ __version__ = "0.0.1"
22
+
23
+
24
+ class SIPDomain(Domain):
25
+ name = 'sip'
26
+ label = 'Bindings specification language'
27
+ directives = {
28
+ 'specification': SIPSpecificationDirective,
29
+ 'module' : SIPModuleDirective,
30
+ 'class': SIPClassDirective,
31
+ 'namespace': SIPNamespaceDirective,
32
+ 'enum': SIPEnumDirective,
33
+ 'method': SIPMethodDirective,
34
+ 'property': SIPPropertyDirective,
35
+ 'variable': SIPVariableDirective,
36
+ 'macro-constant': SIPMacroConstantDirective,
37
+ }
38
+ initial_data = {
39
+ 'sip_specs': {},
40
+ 'sip_dox_index': None,
41
+ 'sip_project_loaded': False,
42
+ 'sip_current_spec': '',
43
+ }
44
+
45
+
46
+ def setup(app):
47
+ app.add_domain(SIPDomain)
48
+ app.add_config_value('sip_toml_project', '.', 'env', str)
49
+ app.add_config_value('sip_doxygen_project', '.', 'env', str)
50
+
51
+ return {
52
+ "version": __version__
53
+ }
doxysip/combiner.py ADDED
@@ -0,0 +1,456 @@
1
+ # SPDX-License-Identifier: BSD-3-Clause
2
+
3
+ # Copyright (c) 2026 C. Savergne <csavergne@yahoo.com>
4
+
5
+ import dataclasses
6
+
7
+ from . import sip_struct
8
+ from . import dox_struct
9
+
10
+
11
+ def _get_dox_index(domain):
12
+ dox_dir = domain.env.app.config.sip_doxygen_project
13
+ dox_dir = os.path.abspath(dox_dir)
14
+
15
+ dox_index = domain.data['sip_dox_index']
16
+ if dox_index is None:
17
+ index_path = os.path.join(dox_dir, 'index.xml')
18
+ dox_index = xml_parse(index_path)
19
+ domain.data['sip_dox_index'] = dox_index
20
+
21
+ return dox_struct.DoxElement(dox_index.documentElement)
22
+
23
+
24
+ def find_dox_for_sip_class(spec, domain, sip_klass):
25
+ dox_index = _get_dox_index(domain)
26
+
27
+ if sip_klass.scope is None:
28
+ for node in root.find_children('compound'):
29
+ if sip_klass.class_key is None:
30
+ if node.attr('kind') != 'namespace': continue
31
+ else:
32
+ if node.attr('kind') not in ('class', 'struct'): continue
33
+
34
+ sip_scoped_name = sip_klass.iface_file.fq_cpp_name
35
+
36
+
37
+ refid = node.attr('refid')
38
+ fn = os.path.join(dox_dir, refid + '.xml')
39
+ klass_xmldoc = xml_parse(fn)
40
+
41
+ element = DoxElement(klass_xmldoc.documentElement)
42
+ return element.find_child('compounddef')
43
+
44
+ return None
45
+
46
+
47
+ def _find_dox_member_by_kind(dox_klass, kind, predicate=None):
48
+ member = None
49
+ if dox_klass is not None:
50
+ for m in dox_klass.getElementsByTagName('memberdef'):
51
+ if m.attr('kind') != kind: continue
52
+ if predicate is not None and not predicate(m): continue
53
+ #if member is not None:
54
+ # raise ValueError('Multiple member match')
55
+ member = m
56
+
57
+ return member
58
+
59
+
60
+
61
+ SLOT_MAPPING = {
62
+ 'or': '|',
63
+ 'and': '&',
64
+ 'xor': '^',
65
+ 'invert': '~',
66
+ 'add': '+',
67
+ 'sub': '-',
68
+ 'mul': '*',
69
+ 'truediv': '/',
70
+ 'mod': '%',
71
+ 'pos': '+',
72
+ 'neg': '-',
73
+ 'lshift': '<<',
74
+ 'rshift': '>>',
75
+ 'ior': '|=',
76
+ 'iand': '&=',
77
+ 'ixor': '^=',
78
+ 'iadd': '+=',
79
+ 'isub': '-=',
80
+ 'imul': '*=',
81
+ 'itruediv': '/=',
82
+ 'imod': '%=',
83
+ 'ilshift': '<<=',
84
+ 'irshift': '>>=',
85
+ 'call': '()',
86
+ 'getitem': '[]',
87
+ 'eq': '==',
88
+ 'ne': '!=',
89
+ 'lt': '<',
90
+ 'le': '<=',
91
+ 'gt': '>',
92
+ 'ge': '>=',
93
+ }
94
+
95
+ def _py_slot_to_cpp_name(py_slot_name):
96
+ py_name = py_slot_name.strip('_')
97
+ cpp_op = SLOT_MAPPING.get(py_name, None)
98
+ if cpp_op is None:
99
+ return None
100
+ return 'operator' + cpp_op
101
+
102
+
103
+ _TAG_MARKER = '\\dox:'
104
+
105
+ def _parse_docstring(docstring):
106
+ '''Parse the docstring and extract the marker if present and
107
+ leave the rest of the docstring unchanged
108
+ '''
109
+
110
+ if docstring is None:
111
+ return '', None
112
+
113
+ if not docstring.text:
114
+ return '', []
115
+
116
+ tag = ''
117
+ ds_lines = docstring.text.strip(' \t\r\n').split('\n')
118
+ headlines = True
119
+ textlines = []
120
+ for line in ds_lines:
121
+ if headlines and line.startswith(_TAG_MARKER):
122
+ tag = line.replace(_TAG_MARKER, '').lstrip()
123
+ else:
124
+ headlines = False
125
+ textlines.append(line)
126
+
127
+ return tag, textlines
128
+
129
+
130
+ def merge_description(sip_docstring, dox_node):
131
+ tag, sip_desc = _parse_docstring(sip_docstring)
132
+
133
+ if dox_node is None:
134
+ dox_desc = ''
135
+ else:
136
+ dox_desc = dox_struct.extract_description(dox_node)
137
+
138
+ if sip_desc is None:
139
+ if dox_desc:
140
+ merged_desc = dox_desc
141
+ else:
142
+ merged_desc = None
143
+ else:
144
+ if dox_desc:
145
+ if tag == 'prepend':
146
+ merged_desc = dox_desc + [''] + sip_desc
147
+ elif tag == 'append':
148
+ merged_desc = sip_desc + [''] + dox_desc
149
+ else: #discard
150
+ merged_desc = sip_desc
151
+ else:
152
+ merged_desc = sip_desc
153
+
154
+ return tag, merged_desc
155
+
156
+
157
+ #==============================================================================
158
+
159
+ @dataclasses.dataclass
160
+ class FunctionDeclaration:
161
+
162
+ name: str|None = None #not for constructors
163
+ signature: str = ''
164
+ description: str|None = None
165
+ arguments: list[tuple[str, str|None]] = dataclasses.field(default_factory=list)
166
+ result: str|None = None
167
+ default: bool = False #for constructors only
168
+
169
+ @property
170
+ def documented(self):
171
+ return self.description is not None or \
172
+ any(a[1] is not None for a in self.arguments) or \
173
+ self.result is not None
174
+
175
+
176
+ def _combine_arguments(sip_signature, dox_member, discard_no_desc):
177
+ #Extract the argument declared names from the param nodes
178
+ if dox_member is not None:
179
+ dox_arg_declnames = [ n.child_text('declname') for n in dox_member.find_children('param')]
180
+
181
+ #Extract the argument name and description node from the \param tags in the comment
182
+ dox_arg_desc_map = {}
183
+ for n_arg_list in dox_member.getElementsByTagName('parameterlist'):
184
+ if n_arg_list.attr('kind') != 'param': continue
185
+ for n_arg_name, n_arg_desc in zip(n_arg_list.getElementsByTagName('parameternamelist'),
186
+ n_arg_list.getElementsByTagName('parameterdescription'),
187
+ strict=True):
188
+ name = n_arg_name.child_text('parametername')
189
+ dox_arg_desc_map[name] = n_arg_desc
190
+
191
+ else:
192
+ dox_arg_declnames = []
193
+ dox_arg_desc_map = {}
194
+
195
+ #Combine the argument metadata from sip and dox into arg_descriptions
196
+ arg_descriptions = []
197
+ for ix, sip_arg in enumerate(sip_signature.args):
198
+ #Replace the argument name in SIP if it's empty
199
+ if sip_arg.name is not None:
200
+ arg_name = sip_arg.name.name
201
+ elif ix < len(dox_arg_declnames):
202
+ arg_name = dox_arg_declnames[ix]
203
+ if arg_name is not None:
204
+ #Modify the signature to let the name appear
205
+ sip_arg.name = sip_struct.CachedName(arg_name)
206
+ else:
207
+ arg_name = None
208
+
209
+ if arg_name is not None:
210
+ dox_arg_desc_node = dox_arg_desc_map.get(arg_name, None)
211
+ if dox_arg_desc_node is not None:
212
+ arg_desc = dox_struct.extract_single_line_description(dox_arg_desc_node)
213
+ arg_descriptions.append((arg_name, arg_desc))
214
+ elif not discard_no_desc:
215
+ arg_descriptions.append((arg_name, None))
216
+
217
+ return arg_descriptions
218
+
219
+
220
+ def combine_constructors(sip_spec, sip_klass, dox_klass):
221
+
222
+ klass_name = sip_klass.py_name.name
223
+
224
+ def ctor_match(ctor, m):
225
+
226
+ if m.child_text('name') != dox_klass.child_text('compoundname'):
227
+ return False
228
+
229
+ dox_arg_sig = dox_struct.get_fct_arg_signature(m)
230
+ if ctor.cpp_arg_signature != dox_arg_sig:
231
+ return False
232
+
233
+ return True
234
+
235
+ ctor_decl_list = []
236
+ for sip_ctor in sip_klass.ctors:
237
+ if sip_ctor.access_specifier is sip_struct.AccessSpecifier.PRIVATE: continue
238
+
239
+ ctor = sip_struct.SIP_Constructor(sip_spec, sip_klass, sip_ctor)
240
+
241
+ ctor_decl = FunctionDeclaration()
242
+
243
+ #Find the corresponding member node
244
+ dox_ctor = _find_dox_member_by_kind(dox_klass, 'function', lambda m: ctor_match(ctor, m))
245
+
246
+ #Description for the overload
247
+ tag, merged_description = merge_description(ctor.docstring, dox_ctor)
248
+ ctor_decl.description = merged_description
249
+
250
+ #Try to fill in the missing argument name and description using Doxygen
251
+ #Skip it if there is a docstring with a 'discard' tag
252
+ ctor_decl.arguments = _combine_arguments(ctor.py_signature,
253
+ dox_ctor if tag != 'discard' else None,
254
+ ctor.docstring is not None)
255
+
256
+ arg_sig = sip_struct.fmt_signature_as_type_hint(sip_spec,
257
+ ctor.py_signature,
258
+ need_self=False,
259
+ defined=None)
260
+ ctor_decl.signature = f'{klass_name}{arg_sig}'
261
+ ctor_decl.default = ctor.is_default
262
+ ctor_decl_list.append(ctor_decl)
263
+
264
+ return ctor_decl_list
265
+
266
+
267
+ def combine_overload(sip_spec, overload, dox_klass):
268
+
269
+ if overload.py_slot:
270
+ sip_cpp_name = _py_slot_to_cpp_name(overload.py_name)
271
+ else:
272
+ sip_cpp_name = overload.cpp_name
273
+
274
+
275
+ def overload_match(m):
276
+
277
+ dox_name = m.child_text('name')
278
+ if sip_cpp_name != dox_name:
279
+ return False
280
+ if overload.is_const != (m.attr('const') == 'yes'):
281
+ return False
282
+ if overload.is_static != (m.attr('static') == 'yes'):
283
+ return False
284
+
285
+ dox_result_sig = dox_struct.get_type_signature(m.find_child('type'))
286
+ if overload.result_signature != dox_result_sig:
287
+ return False
288
+
289
+ dox_arg_sig = dox_struct.get_fct_arg_signature(m)
290
+ if overload.cpp_arg_signature != dox_arg_sig:
291
+ return False
292
+
293
+ return True
294
+
295
+ #Find the corresponding member node
296
+ dox_member = _find_dox_member_by_kind(dox_klass, 'function', overload_match)
297
+
298
+ fct_decl = FunctionDeclaration(name=overload.py_name)
299
+
300
+ #Description for the overload
301
+ tag, overload_description = merge_description(overload.docstring, dox_member)
302
+ fct_decl.description = overload_description
303
+
304
+ #Find the argument and result descriptions. Skip if the tag is 'discard'
305
+ if tag != 'discard':
306
+ fct_decl.arguments = _combine_arguments(overload.py_signature,
307
+ dox_member,
308
+ overload.docstring is not None)
309
+ if dox_member is not None:
310
+ #Find the description of the return value
311
+ for n in dox_member.getElementsByTagName('simplesect'):
312
+ if n.attr('kind') != 'return': continue
313
+ fct_decl.result = dox_struct.extract_single_line_description(n)
314
+ break
315
+ else:
316
+ fct_decl.arguments = _combine_arguments(overload.py_signature, None, True)
317
+
318
+ arg_sig = sip_struct.fmt_signature_as_type_hint(sip_spec,
319
+ overload.py_signature,
320
+ need_self=False,
321
+ defined=None)
322
+ fct_decl.signature = f'{overload.py_name}{arg_sig}'
323
+
324
+ return fct_decl
325
+
326
+
327
+ #==============================================================================
328
+
329
+ @dataclasses.dataclass
330
+ class EnumDeclaration:
331
+ signature: str
332
+ description: str|None
333
+ members: list[tuple[str, str|None]]|None
334
+
335
+
336
+ def combine_enum(sip_enum, dox_enum):
337
+ superclass = 'int'
338
+ if sip_enum.base_type is sip_struct.EnumBaseType.ENUM:
339
+ superclass = 'enum.Enum'
340
+ elif sip_enum.base_type is sip_struct.EnumBaseType.FLAG:
341
+ superclass = 'enum.Flag'
342
+ elif sip_enum.base_type in (sip_struct.EnumBaseType.INT_ENUM, sip_struct.EnumBaseType.UINT_ENUM):
343
+ superclass = 'enum.IntEnum'
344
+ elif sip_enum.base_type is sip_struct.EnumBaseType.INT_FLAG:
345
+ superclass = 'enum.IntFlag'
346
+
347
+ sig = f'{sip_enum.py_name} ({superclass})'
348
+
349
+ if dox_enum is None:
350
+ enum_desc = None
351
+ else:
352
+ enum_desc = dox_struct.extract_description(dox_enum)
353
+
354
+ members_decl = []
355
+ for member in sip_enum.members:
356
+ desc = None
357
+ if dox_enum is not None:
358
+ for dox_enum_value in dox_enum.find_children('enumvalue'):
359
+ if dox_enum_value.child_text('name') == member.cpp_name:
360
+ desc = dox_struct.extract_description(dox_enum_value)
361
+ break
362
+ members_decl.append((member.py_name.name, desc))
363
+
364
+ return EnumDeclaration(sig, enum_desc, members_decl)
365
+
366
+
367
+ def combine_class_enum(sip_enum, dox_klass):
368
+ enum_cpp_name = sip_enum.fq_cpp_name.base_name
369
+ enum_match = lambda m : m.child_text('name') == enum_cpp_name
370
+ dox_enum = _find_dox_member_by_kind(dox_klass, 'enum', enum_match)
371
+ return combine_enum(sip_enum, dox_enum)
372
+
373
+
374
+ #==============================================================================
375
+
376
+ @dataclasses.dataclass
377
+ class PropertyDeclaration:
378
+ name: str
379
+ type: str|None
380
+ description: str|None
381
+
382
+
383
+ def combine_property(sip_spec, sip_klass, sip_prop):
384
+ getter_member = None
385
+ for m in sip_klass.members:
386
+ if m.py_name.name == sip_prop.getter:
387
+ getter_member = m
388
+ break
389
+
390
+ #Infer the property type from the getter result type
391
+ result_type = None
392
+ if getter_member:
393
+ c = sip_struct.SIP_Callable(sip_spec, sip_klass, getter_member)
394
+ for ovr in c.overloads:
395
+ if not len(ovr.py_signature.args):
396
+ result_type = sip_struct.signature_as_result_type_hint(sip_spec, ovr.py_signature)
397
+ break
398
+
399
+ _, description = _parse_docstring(sip_prop.docstring)
400
+
401
+ return PropertyDeclaration(sip_prop.name.name, result_type, description)
402
+
403
+
404
+ #==============================================================================
405
+
406
+ @dataclasses.dataclass
407
+ class VariableDeclaration:
408
+ name: str
409
+ type_: str
410
+ const: bool
411
+ qualifiers : list[str]
412
+ description : str|None
413
+
414
+
415
+ def combine_variable(sip_spec, sip_var, dox_var):
416
+ var_type = sip_struct.fmt_argument_as_type_hint(sip_spec, sip_var.type, None, None)
417
+
418
+ if dox_var is not None:
419
+ description = dox_struct.extract_description(dox_var)
420
+ else:
421
+ description = None
422
+
423
+ has_const_type = sip_var.type.is_const# and len(sip_var.type.derefs) == 0
424
+ is_const = has_const_type and (sip_var.is_static or sip_var.scope is None)
425
+ qualifiers = []
426
+ if sip_var.no_setter or has_const_type:
427
+ qualifiers.append('readonly')
428
+ if sip_var.is_static:
429
+ qualifiers.append('static')
430
+
431
+ var_decl = VariableDeclaration(sip_var.py_name.name,
432
+ var_type,
433
+ is_const,
434
+ qualifiers,
435
+ description)
436
+
437
+ return var_decl
438
+
439
+
440
+ def combine_class_variable(sip_spec, sip_var, dox_klass):
441
+ var_cpp_name = sip_var.fq_cpp_name.base_name
442
+ dox_var = _find_dox_member_by_kind(dox_klass, 'variable', lambda m: m.child_text('name') == var_cpp_name)
443
+ return combine_variable(sip_spec, sip_var, dox_var)
444
+
445
+
446
+ def combine_class_variables(sip_spec, sip_klass, dox_klass):
447
+ var_decl_list = []
448
+ for v in sip_spec.variables:
449
+ if v.module is not sip_spec.module: continue
450
+ if v.scope is not sip_klass: continue
451
+
452
+ var_decl = combine_class_variable(sip_spec, v, dox_klass)
453
+ var_decl_list.append(var_decl)
454
+
455
+ return var_decl_list
456
+