proj-flow 0.17.2__py3-none-any.whl → 0.19.0__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.
Files changed (30) hide show
  1. proj_flow/__init__.py +1 -1
  2. proj_flow/api/step.py +13 -3
  3. proj_flow/ext/webidl/__init__.py +12 -0
  4. proj_flow/ext/webidl/base/__init__.py +2 -0
  5. proj_flow/ext/webidl/base/config.py +425 -0
  6. proj_flow/ext/webidl/cli/__init__.py +10 -0
  7. proj_flow/ext/webidl/cli/cmake.py +82 -0
  8. proj_flow/ext/webidl/cli/depfile.py +83 -0
  9. proj_flow/ext/webidl/cli/gen.py +157 -0
  10. proj_flow/ext/webidl/cli/init.py +26 -0
  11. proj_flow/ext/webidl/cli/root.py +12 -0
  12. proj_flow/ext/webidl/cli/updater.py +20 -0
  13. proj_flow/ext/webidl/data/init/flow_webidl.cmake +26 -0
  14. proj_flow/ext/webidl/data/templates/cmake.mustache +45 -0
  15. proj_flow/ext/webidl/data/templates/depfile.mustache +6 -0
  16. proj_flow/ext/webidl/data/templates/partials/cxx/attribute-decl.mustache +1 -0
  17. proj_flow/ext/webidl/data/templates/partials/cxx/in-out.mustache +2 -0
  18. proj_flow/ext/webidl/data/templates/partials/cxx/includes.mustache +6 -0
  19. proj_flow/ext/webidl/data/templates/partials/cxx/operation-decl.mustache +12 -0
  20. proj_flow/ext/webidl/data/templates/partials/cxx/type.mustache +6 -0
  21. proj_flow/ext/webidl/data/types/cxx.json +47 -0
  22. proj_flow/ext/webidl/model/__init__.py +2 -0
  23. proj_flow/ext/webidl/model/ast.py +586 -0
  24. proj_flow/ext/webidl/model/builders.py +230 -0
  25. proj_flow/ext/webidl/registry.py +23 -0
  26. {proj_flow-0.17.2.dist-info → proj_flow-0.19.0.dist-info}/METADATA +2 -1
  27. {proj_flow-0.17.2.dist-info → proj_flow-0.19.0.dist-info}/RECORD +30 -7
  28. {proj_flow-0.17.2.dist-info → proj_flow-0.19.0.dist-info}/WHEEL +0 -0
  29. {proj_flow-0.17.2.dist-info → proj_flow-0.19.0.dist-info}/entry_points.txt +0 -0
  30. {proj_flow-0.17.2.dist-info → proj_flow-0.19.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,586 @@
1
+ # Copyright (c) 2026 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any, TypeVar, cast
8
+
9
+ from pywebidl2 import expr, parser, raw_parse, validate
10
+
11
+ from proj_flow.ext.webidl.base.config import ExtAttrsContextBuilders, TypeReplacement
12
+
13
+
14
+ def _flatten_union(ast: expr.IdlType) -> expr.IdlType:
15
+ arguments = cast(list[expr.IdlType], ast.idl_type)
16
+ result: list[expr.IdlType] = []
17
+ for argument in arguments:
18
+ if not argument.union:
19
+ result.append(argument)
20
+ continue
21
+ argument = _flatten_union(argument)
22
+ if argument.nullable:
23
+ result.append(argument)
24
+ sub_args = cast(list[expr.IdlType], argument.idl_type)
25
+ result.extend(sub_args)
26
+ ast.idl_type = result
27
+ return ast
28
+
29
+
30
+ @dataclass
31
+ class TypeVisitor:
32
+ known_types: set[str]
33
+ types: dict[str, TypeReplacement]
34
+ local_references: set[str] = field(default_factory=set)
35
+ modules_or_includes: set[str] = field(default_factory=set)
36
+
37
+ def on_type_name(self, type_name: str):
38
+ if type_name in self.known_types:
39
+ self.local_references.add(type_name)
40
+ return type_name
41
+
42
+ replacement = self.types.get(type_name)
43
+ if replacement:
44
+ if replacement.module_or_include:
45
+ self.modules_or_includes.add(replacement.module_or_include)
46
+ return replacement.name
47
+
48
+ return type_name
49
+
50
+ def on_type(self, type: "Type"):
51
+ if type.nullable:
52
+ replacement = self.types.get("optional")
53
+ if replacement and replacement.module_or_include:
54
+ self.modules_or_includes.add(replacement.module_or_include)
55
+
56
+ type.name = self.on_type_name(type.idl_name)
57
+
58
+
59
+ @dataclass
60
+ class TypeArg:
61
+ arg_type: "Type | expr.IdlType"
62
+ first: bool = False
63
+
64
+
65
+ @dataclass
66
+ class Type:
67
+ idl_name: str
68
+ name: str | None = field(default=None)
69
+ arguments: list[TypeArg] = field(default_factory=list)
70
+ nullable: bool = field(default=False)
71
+ generic: bool = field(default=False)
72
+ ext_attrs: dict = field(default_factory=dict)
73
+
74
+ def populate_references(self, visitor: TypeVisitor):
75
+ visitor.on_type(self)
76
+ for arg in self.arguments:
77
+ if isinstance(arg.arg_type, Type):
78
+ arg.arg_type.populate_references(visitor)
79
+
80
+ def __patched_type(self):
81
+ if self.generic and self.idl_name == "sequence":
82
+ if self.ext_attrs.get("unique"):
83
+ self.idl_name = "set"
84
+ elif self.ext_attrs.get("span"):
85
+ self.idl_name = "span"
86
+ elif self.generic and self.idl_name == "record":
87
+ first_arg = (
88
+ self.arguments[0].arg_type
89
+ if self.arguments and isinstance(self.arguments[0].arg_type, Type)
90
+ else None
91
+ )
92
+ if first_arg:
93
+ first_arg.idl_name = self.ext_attrs.get("key") or "string"
94
+ for attr in ["unique", "span", "key"]:
95
+ if attr in self.ext_attrs:
96
+ del self.ext_attrs[attr]
97
+ return self
98
+
99
+ @staticmethod
100
+ def from_idl(
101
+ ast: expr.IdlType,
102
+ builders: ExtAttrsContextBuilders,
103
+ parent_ast_attrs: list[expr.ExtendedAttribute],
104
+ ) -> "Type | expr.IdlType":
105
+ ext_attrs = builders.from_idl(lambda b: b.type, ast.ext_attrs)
106
+ parent_attrs = builders.from_idl(lambda b: b.type, parent_ast_attrs)
107
+ builders.property_fixup(ext_attrs, parent_attrs)
108
+ nullable = ext_attrs.get("nullable", False)
109
+ if "nullable" in ext_attrs:
110
+ del ext_attrs["nullable"]
111
+
112
+ if ast.union:
113
+ ast = _flatten_union(ast)
114
+ args = cast(list[expr.IdlType], ast.idl_type)
115
+ arguments = [TypeArg(Type.from_idl(arg, builders, [])) for arg in args]
116
+ if arguments:
117
+ arguments[0].first = True
118
+ return Type(
119
+ idl_name="union",
120
+ arguments=arguments,
121
+ nullable=ast.nullable or nullable,
122
+ generic=True,
123
+ ext_attrs=ext_attrs,
124
+ ).__patched_type()
125
+
126
+ if isinstance(ast.idl_type, str):
127
+ return Type(
128
+ idl_name=ast.idl_type,
129
+ nullable=ast.nullable or nullable,
130
+ ext_attrs=ext_attrs,
131
+ ).__patched_type()
132
+
133
+ if ast.generic:
134
+ arguments: list[TypeArg] = []
135
+ if isinstance(ast.idl_type, str):
136
+ arguments = [TypeArg(Type(idl_name=ast.idl_type), True)]
137
+ elif isinstance(ast.idl_type, expr.IdlType):
138
+ arguments = [TypeArg(Type.from_idl(ast.idl_type, builders, []), True)]
139
+ else:
140
+ arguments = [
141
+ TypeArg(Type.from_idl(arg, builders, []))
142
+ for arg in cast(list[expr.IdlType], ast.idl_type)
143
+ ]
144
+ if arguments:
145
+ arguments[0].first = True
146
+ return Type(
147
+ idl_name=ast.generic,
148
+ arguments=arguments,
149
+ nullable=ast.nullable or nullable,
150
+ generic=True,
151
+ ext_attrs=ext_attrs,
152
+ ).__patched_type()
153
+
154
+ inner = ast.idl_type
155
+ if isinstance(inner, expr.IdlType):
156
+ inner.nullable = inner.nullable or ast.nullable or nullable
157
+ inner.ext_attrs = [*ast.ext_attrs, *inner.ext_attrs]
158
+ inner.generic = inner.generic or ast.generic
159
+ return Type.from_idl(inner, builders, parent_ast_attrs)
160
+
161
+ print(type(ast.idl_type), ast.generic != "")
162
+ print(ast)
163
+ return ast
164
+
165
+
166
+ @dataclass
167
+ class Attribute:
168
+ name: str
169
+ type: Type | expr.IdlType
170
+ readonly: bool
171
+ static: bool
172
+ ext_attrs: dict
173
+
174
+ def populate_references(self, visitor: TypeVisitor):
175
+ if isinstance(self.type, Type):
176
+ self.type.populate_references(visitor)
177
+
178
+ @staticmethod
179
+ def from_idl(ast: expr.Attribute, builders: ExtAttrsContextBuilders):
180
+ ext_attrs = builders.from_idl(lambda b: b.attribute, ast.ext_attrs)
181
+ type = Type.from_idl(ast.idl_type, builders, ast.ext_attrs)
182
+ return Attribute(
183
+ name=ast.name,
184
+ type=type,
185
+ readonly=ast.readonly,
186
+ static=ast.special == "static",
187
+ ext_attrs=ext_attrs,
188
+ )
189
+
190
+
191
+ @dataclass
192
+ class Constant:
193
+ name: str
194
+ type: Type | expr.IdlType
195
+ value: expr.Expression
196
+ ext_attrs: dict
197
+
198
+ def populate_references(self, visitor: TypeVisitor):
199
+ if isinstance(self.type, Type):
200
+ self.type.populate_references(visitor)
201
+
202
+ @staticmethod
203
+ def from_idl(ast: expr.Const, builders: ExtAttrsContextBuilders):
204
+ ext_attrs = builders.from_idl(lambda b: b.attribute, ast.ext_attrs)
205
+ type = Type.from_idl(ast.idl_type, builders, ast.ext_attrs)
206
+ return Constant(
207
+ name=ast.name,
208
+ type=type,
209
+ value=ast.value,
210
+ ext_attrs=ext_attrs,
211
+ )
212
+
213
+
214
+ @dataclass
215
+ class Argument:
216
+ name: str
217
+ type: Type | expr.IdlType
218
+ ext_attrs: dict
219
+ first: bool = False
220
+
221
+ def populate_references(self, visitor: TypeVisitor):
222
+ if isinstance(self.type, Type):
223
+ self.type.populate_references(visitor)
224
+
225
+ @staticmethod
226
+ def from_idl(ast: expr.Argument, builders: ExtAttrsContextBuilders):
227
+ ext_attrs = builders.from_idl(lambda b: b.argument, ast.ext_attrs)
228
+ type = Type.from_idl(ast.idl_type, builders, ast.ext_attrs)
229
+ return Argument(
230
+ name=ast.name,
231
+ type=type,
232
+ ext_attrs=ext_attrs,
233
+ )
234
+
235
+
236
+ @dataclass
237
+ class Operation:
238
+ name: str
239
+ type: Type | expr.IdlType | None
240
+ arguments: list[Argument]
241
+ static: bool
242
+ ext_attrs: dict
243
+
244
+ def populate_references(self, visitor: TypeVisitor):
245
+ if isinstance(self.type, Type):
246
+ self.type.populate_references(visitor)
247
+ for arg in self.arguments:
248
+ arg.populate_references(visitor)
249
+
250
+ @staticmethod
251
+ def from_idl(ast: expr.Operation, builders: ExtAttrsContextBuilders):
252
+ ext_attrs = builders.from_idl(lambda b: b.operation, ast.ext_attrs)
253
+ type = (
254
+ Type.from_idl(ast.idl_type, builders, ast.ext_attrs)
255
+ if ast.idl_type
256
+ else None
257
+ )
258
+ arguments = [Argument.from_idl(arg, builders) for arg in ast.arguments]
259
+ if arguments:
260
+ arguments[0].first = True
261
+ return Operation(
262
+ name=ast.name,
263
+ type=type,
264
+ arguments=arguments,
265
+ static=ast.special == "static",
266
+ ext_attrs=ext_attrs,
267
+ )
268
+
269
+
270
+ @dataclass
271
+ class MemberContainer:
272
+ constants: list[Constant]
273
+ attributes: list[Attribute]
274
+ operations: list[Operation]
275
+
276
+ @staticmethod
277
+ def from_idl(
278
+ ast: expr.Interface | expr.InterfaceMixin, builders: ExtAttrsContextBuilders
279
+ ):
280
+ constants: list[Constant] = []
281
+ attributes: list[Attribute] = []
282
+ operations: list[Operation] = []
283
+ for member in ast.members:
284
+ if isinstance(member, expr.Const):
285
+ constants.append(Constant.from_idl(member, builders))
286
+ continue
287
+
288
+ if isinstance(member, expr.Attribute):
289
+ attributes.append(Attribute.from_idl(member, builders))
290
+ continue
291
+
292
+ if isinstance(member, expr.Operation):
293
+ operations.append(Operation.from_idl(member, builders))
294
+ continue
295
+
296
+ print(f"warning: unsupported member type `{getattr(member, 'type')}'")
297
+
298
+ return MemberContainer(
299
+ constants=constants,
300
+ attributes=attributes,
301
+ operations=operations,
302
+ )
303
+
304
+
305
+ def update(existing: dict, incoming: dict):
306
+ for key in incoming:
307
+ val = incoming[key]
308
+ if val or key not in existing:
309
+ existing[key] = val
310
+
311
+
312
+ @dataclass
313
+ class Interface:
314
+ name: str
315
+ inheritance: str | None
316
+ partial: bool
317
+ constants: list[Constant]
318
+ attributes: list[Attribute]
319
+ operations: list[Operation]
320
+ ext_attrs: dict
321
+ has_constants: bool = field(default=False)
322
+ has_attributes: bool = field(default=False)
323
+ has_operations: bool = field(default=False)
324
+
325
+ def populate_references(self, visitor: TypeVisitor):
326
+ if self.inheritance:
327
+ visitor.on_type_name(self.inheritance)
328
+ for item in self.constants:
329
+ item.populate_references(visitor)
330
+ for item in self.attributes:
331
+ item.populate_references(visitor)
332
+ for item in self.operations:
333
+ item.populate_references(visitor)
334
+
335
+ @staticmethod
336
+ def from_idl(ast: expr.Interface, builders: ExtAttrsContextBuilders):
337
+ members = MemberContainer.from_idl(ast, builders)
338
+ return Interface(
339
+ name=ast.name,
340
+ inheritance=ast.inheritance,
341
+ partial=ast.partial,
342
+ constants=members.constants,
343
+ attributes=members.attributes,
344
+ operations=members.operations,
345
+ ext_attrs=builders.from_idl(lambda b: b.interface, ast.ext_attrs),
346
+ )
347
+
348
+
349
+ @dataclass
350
+ class InterfaceMixin:
351
+ partial: bool
352
+ constants: list[Constant]
353
+ attributes: list[Attribute]
354
+ operations: list[Operation]
355
+ ext_attrs: dict
356
+
357
+ @staticmethod
358
+ def from_idl(ast: expr.InterfaceMixin, builders: ExtAttrsContextBuilders):
359
+ members = MemberContainer.from_idl(ast, builders)
360
+ return InterfaceMixin(
361
+ partial=ast.partial,
362
+ constants=members.constants,
363
+ attributes=members.attributes,
364
+ operations=members.operations,
365
+ ext_attrs=builders.from_idl(lambda b: b.interface, ast.ext_attrs),
366
+ )
367
+
368
+
369
+ @dataclass
370
+ class EnumInfo:
371
+ name: str
372
+ NAME: str
373
+ items: list[str]
374
+ ext_attrs: dict
375
+
376
+ @staticmethod
377
+ def from_idl(ast: expr.Enum, builders: ExtAttrsContextBuilders):
378
+ return EnumInfo(
379
+ name=ast.name,
380
+ NAME=ast.name.upper(),
381
+ items=[cast(str, value.value) for value in ast.values],
382
+ ext_attrs=builders.from_idl(lambda b: b.enum, ast.ext_attrs),
383
+ )
384
+
385
+
386
+ @dataclass
387
+ class MergedDefinitions:
388
+ interfaces: dict[str, Interface] = field(default_factory=dict)
389
+ enum: list[EnumInfo] = field(default_factory=list)
390
+
391
+ def order(self, types: dict[str, TypeReplacement]):
392
+ dependency_tree: dict[str, set[str]] = {}
393
+ modules_or_includes: set[str] = set()
394
+
395
+ known_names = set[str]()
396
+ known_names.update(item.name for item in self.enum)
397
+ known_names.update(self.interfaces.keys())
398
+
399
+ for name, interface in self.interfaces.items():
400
+ visitor = TypeVisitor(known_names, types)
401
+ interface.populate_references(visitor)
402
+ modules_or_includes.update(visitor.modules_or_includes)
403
+ if name in visitor.local_references:
404
+ visitor.local_references.remove(name)
405
+ dependency_tree[name] = visitor.local_references
406
+
407
+ order = list[str]()
408
+ while dependency_tree:
409
+ layer = [
410
+ name
411
+ for name in sorted(dependency_tree.keys())
412
+ if not dependency_tree[name]
413
+ ]
414
+ order.extend(layer)
415
+
416
+ if not layer:
417
+ for key in dependency_tree:
418
+ layer.append(key)
419
+ break
420
+
421
+ for key in layer:
422
+ del dependency_tree[key]
423
+
424
+ for key in layer:
425
+ for dependencies in dependency_tree.values():
426
+ if key in dependencies:
427
+ dependencies.remove(key)
428
+
429
+ return ([self.interfaces[key] for key in order], modules_or_includes)
430
+
431
+
432
+ P_ = TypeVar("P_")
433
+ AST_ = TypeVar("AST_")
434
+
435
+
436
+ class PartialSetDict[P_, AST_](dict[str, list[P_]]):
437
+ def append(self, name: str, item: AST_, builders: ExtAttrsContextBuilders):
438
+ conv = self.convert(item, builders)
439
+ try:
440
+ self[name].append(conv)
441
+ except KeyError:
442
+ self[name] = [conv]
443
+
444
+ def extend(self, additional: dict[str, list[P_]]):
445
+ for name, partials in additional.items():
446
+ try:
447
+ self[name].extend(partials)
448
+ except KeyError:
449
+ self[name] = [*partials]
450
+
451
+ def convert(self, item: AST_, builders: ExtAttrsContextBuilders) -> P_: ...
452
+
453
+
454
+ class InterfacePartialSet(PartialSetDict[Interface | str, expr.Interface | str]):
455
+ def convert(self, item: expr.Interface | str, builders: ExtAttrsContextBuilders):
456
+ return item if isinstance(item, str) else Interface.from_idl(item, builders)
457
+
458
+
459
+ class MixinPartialSet(PartialSetDict[InterfaceMixin, expr.InterfaceMixin]):
460
+ def convert(self, item: expr.InterfaceMixin, builders: ExtAttrsContextBuilders):
461
+ return InterfaceMixin.from_idl(item, builders)
462
+
463
+
464
+ @dataclass
465
+ class IdlSyntaxError:
466
+ path: str
467
+ error: parser.SyntaxErrorInfo
468
+
469
+
470
+ @dataclass
471
+ class Definitions:
472
+ interfaces: InterfacePartialSet = field(default_factory=InterfacePartialSet)
473
+ mixins: MixinPartialSet = field(default_factory=MixinPartialSet)
474
+ enums: list[EnumInfo] = field(default_factory=list)
475
+
476
+ @staticmethod
477
+ def from_idl(ast: expr.Definitions, builders: ExtAttrsContextBuilders):
478
+ result = Definitions()
479
+
480
+ for definition in ast.definitions:
481
+ if isinstance(definition, expr.Includes):
482
+ result.interfaces.append(
483
+ definition.target, definition.includes, builders
484
+ )
485
+ continue
486
+ if isinstance(definition, expr.Interface):
487
+ result.interfaces.append(definition.name, definition, builders)
488
+ continue
489
+ if isinstance(definition, expr.InterfaceMixin):
490
+ result.mixins.append(definition.name, definition, builders)
491
+ continue
492
+ if isinstance(definition, expr.Enum):
493
+ result.enums.append(EnumInfo.from_idl(definition, builders))
494
+ continue
495
+
496
+ return result
497
+
498
+ @staticmethod
499
+ def merge(*partials: "Definitions"):
500
+ intermediate = Definitions()
501
+ for partial in partials:
502
+ intermediate.interfaces.extend(partial.interfaces)
503
+ intermediate.mixins.extend(partial.mixins)
504
+ intermediate.enums.extend(partial.enums)
505
+
506
+ interfaces: dict[str, Interface] = {}
507
+ mixins: dict[str, Interface] = {}
508
+
509
+ for name, mixin_set in intermediate.mixins.items():
510
+ mixin = Interface(
511
+ name=name,
512
+ inheritance=None,
513
+ partial=True,
514
+ constants=[],
515
+ attributes=[],
516
+ operations=[],
517
+ ext_attrs={},
518
+ )
519
+
520
+ for partial in mixin_set:
521
+ mixin.constants.extend(partial.constants)
522
+ mixin.attributes.extend(partial.attributes)
523
+ mixin.operations.extend(partial.operations)
524
+ update(mixin.ext_attrs, partial.ext_attrs)
525
+
526
+ mixins[name] = mixin
527
+
528
+ for name, interface_set in intermediate.interfaces.items():
529
+ target = Interface(
530
+ name=name,
531
+ inheritance=None,
532
+ partial=False,
533
+ constants=[],
534
+ attributes=[],
535
+ operations=[],
536
+ ext_attrs={},
537
+ )
538
+
539
+ for partial in interface_set:
540
+ if isinstance(partial, str):
541
+ try:
542
+ partial = mixins[partial]
543
+ except KeyError:
544
+ message = f"{name} cannot find {partial} to include"
545
+ raise RuntimeError(message)
546
+
547
+ target.inheritance = target.inheritance or partial.inheritance
548
+ target.constants.extend(partial.constants)
549
+ target.attributes.extend(partial.attributes)
550
+ target.operations.extend(partial.operations)
551
+ update(target.ext_attrs, partial.ext_attrs)
552
+
553
+ target.has_constants = len(target.constants) > 0
554
+ target.has_attributes = len(target.attributes) > 0
555
+ target.has_operations = len(target.operations) > 0
556
+ interfaces[name] = target
557
+
558
+ return MergedDefinitions(
559
+ interfaces=interfaces,
560
+ enum=intermediate.enums,
561
+ )
562
+
563
+ @staticmethod
564
+ def parse_and_merge(
565
+ names: list[str],
566
+ builders: ExtAttrsContextBuilders,
567
+ ):
568
+ partials: list[Definitions] = []
569
+ syntax_errors: list[IdlSyntaxError] = []
570
+
571
+ for name in names:
572
+ path = Path(name)
573
+ text = path.read_text(encoding="UTF-8")
574
+ errors = validate(text)
575
+ for error in errors:
576
+ syntax_errors.append(IdlSyntaxError(path=name, error=error))
577
+ if errors:
578
+ continue
579
+
580
+ ast = cast(expr.Definitions, raw_parse(text))
581
+ definitions = Definitions.from_idl(ast, builders)
582
+ partials.append(definitions)
583
+
584
+ if syntax_errors:
585
+ return syntax_errors
586
+ return Definitions.merge(*partials)