proj-flow 0.18.0__py3-none-any.whl → 0.20.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 (33) hide show
  1. proj_flow/__init__.py +1 -1
  2. proj_flow/api/makefile.py +1 -1
  3. proj_flow/ext/tools/__init__.py +13 -0
  4. proj_flow/ext/tools/pragma_once.py +40 -0
  5. proj_flow/ext/tools/run_linter.py +228 -0
  6. proj_flow/ext/webidl/__init__.py +12 -0
  7. proj_flow/ext/webidl/base/__init__.py +2 -0
  8. proj_flow/ext/webidl/base/config.py +425 -0
  9. proj_flow/ext/webidl/cli/__init__.py +10 -0
  10. proj_flow/ext/webidl/cli/cmake.py +82 -0
  11. proj_flow/ext/webidl/cli/depfile.py +83 -0
  12. proj_flow/ext/webidl/cli/gen.py +157 -0
  13. proj_flow/ext/webidl/cli/init.py +26 -0
  14. proj_flow/ext/webidl/cli/root.py +12 -0
  15. proj_flow/ext/webidl/cli/updater.py +20 -0
  16. proj_flow/ext/webidl/data/init/flow_webidl.cmake +26 -0
  17. proj_flow/ext/webidl/data/templates/cmake.mustache +45 -0
  18. proj_flow/ext/webidl/data/templates/depfile.mustache +6 -0
  19. proj_flow/ext/webidl/data/templates/partials/cxx/attribute-decl.mustache +1 -0
  20. proj_flow/ext/webidl/data/templates/partials/cxx/in-out.mustache +2 -0
  21. proj_flow/ext/webidl/data/templates/partials/cxx/includes.mustache +6 -0
  22. proj_flow/ext/webidl/data/templates/partials/cxx/operation-decl.mustache +12 -0
  23. proj_flow/ext/webidl/data/templates/partials/cxx/type.mustache +6 -0
  24. proj_flow/ext/webidl/data/types/cxx.json +47 -0
  25. proj_flow/ext/webidl/model/__init__.py +2 -0
  26. proj_flow/ext/webidl/model/ast.py +586 -0
  27. proj_flow/ext/webidl/model/builders.py +230 -0
  28. proj_flow/ext/webidl/registry.py +23 -0
  29. {proj_flow-0.18.0.dist-info → proj_flow-0.20.0.dist-info}/METADATA +2 -1
  30. {proj_flow-0.18.0.dist-info → proj_flow-0.20.0.dist-info}/RECORD +33 -7
  31. {proj_flow-0.18.0.dist-info → proj_flow-0.20.0.dist-info}/WHEEL +0 -0
  32. {proj_flow-0.18.0.dist-info → proj_flow-0.20.0.dist-info}/entry_points.txt +0 -0
  33. {proj_flow-0.18.0.dist-info → proj_flow-0.20.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,425 @@
1
+ # Copyright (c) 2026 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+
5
+ import copy
6
+ import json
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any, Callable, cast
10
+
11
+ import chevron
12
+
13
+ from proj_flow.ext.webidl.model.builders import ExtAttrsContextBuilders, TypeReplacement
14
+
15
+ package_root = Path(__file__).parent.parent
16
+ templates_dir = package_root / "data" / "templates"
17
+
18
+
19
+ def load_package_template(name: str):
20
+ path = templates_dir / f"{name}.mustache"
21
+ return path.read_text(encoding="UTF-8")
22
+
23
+
24
+ @dataclass
25
+ class CMakeDirs:
26
+ base_dir: Path
27
+ project_source_dir: Path
28
+ project_binary_dir: Path
29
+ current_source_dir: Path
30
+ current_binary_dir: Path
31
+
32
+ @staticmethod
33
+ def from_config_path(config: Path, project_binary_dir: Path):
34
+ config = config.absolute()
35
+ project_binary_dir = project_binary_dir.absolute()
36
+ base_dir = config.parent
37
+ project_source_dir = Path().absolute()
38
+ current_source_dir = project_source_dir
39
+
40
+ for parent in config.parents:
41
+ cmake_lists = parent / "CMakeLists.txt"
42
+ if cmake_lists.exists():
43
+ current_source_dir = parent
44
+ break
45
+
46
+ current_binary_dir = project_binary_dir / current_source_dir.relative_to(
47
+ project_source_dir
48
+ )
49
+
50
+ return CMakeDirs(
51
+ base_dir=base_dir,
52
+ project_source_dir=project_source_dir,
53
+ project_binary_dir=project_binary_dir,
54
+ current_source_dir=current_source_dir,
55
+ current_binary_dir=current_binary_dir,
56
+ )
57
+
58
+ def filename_from_config(self, config: Path, ext: str):
59
+ in_source = config.absolute().relative_to(self.current_source_dir)
60
+ in_build = self.current_binary_dir / in_source
61
+ return Path(in_build.as_posix() + ext)
62
+
63
+ def dependency_from_config(self, config: Path):
64
+ return self.filename_from_config(config, ".deps")
65
+
66
+ def cmake_from_config(self, config: Path):
67
+ return self.filename_from_config(config, ".cmake")
68
+
69
+ def my_defines(self):
70
+ return {
71
+ "PROJECT_SOURCE_DIR": self.project_source_dir.as_posix(),
72
+ "PROJECT_BINARY_DIR": self.project_binary_dir.as_posix(),
73
+ "CMAKE_CURRENT_SOURCE_DIR": self.current_source_dir.as_posix(),
74
+ "CMAKE_CURRENT_BINARY_DIR": self.current_binary_dir.as_posix(),
75
+ }
76
+
77
+ def prefix(self, filename: str):
78
+ if "${" in filename:
79
+ return Path(filename)
80
+
81
+ full = self.base_dir / filename
82
+
83
+ if full.is_relative_to(self.current_binary_dir):
84
+ return Path("${CMAKE_CURRENT_BINARY_DIR}") / full.relative_to(
85
+ self.current_binary_dir
86
+ )
87
+ if full.is_relative_to(self.project_binary_dir):
88
+ return Path("${PROJECT_BINARY_DIR}") / full.relative_to(
89
+ self.project_binary_dir
90
+ )
91
+ if full.is_relative_to(self.current_source_dir):
92
+ return Path("${CMAKE_CURRENT_SOURCE_DIR}") / full.relative_to(
93
+ self.current_source_dir
94
+ )
95
+ if full.is_relative_to(self.project_source_dir):
96
+ return Path("${PROJECT_SOURCE_DIR}") / full.relative_to(
97
+ self.project_source_dir
98
+ )
99
+
100
+ return Path(filename)
101
+
102
+ def make_abs_path(self):
103
+ def impl(filename: str):
104
+ return self.prefix(filename)
105
+
106
+ return impl
107
+
108
+
109
+ def _relative(path: Path, basedir: Path | None):
110
+ if not basedir:
111
+ return path
112
+ return path.relative_to(basedir, walk_up=True)
113
+
114
+
115
+ class Rebuild:
116
+ @staticmethod
117
+ def __rebuild_map(data: dict, global_ctx: dict[str, str]):
118
+ for key, value in data.items():
119
+ data[key] = Rebuild.__rebuild_any(value, global_ctx)
120
+ return data
121
+
122
+ @staticmethod
123
+ def __rebuild_list(data: list, global_ctx: dict[str, str]):
124
+ for index, value in enumerate(data):
125
+ data[index] = Rebuild.__rebuild_any(value, global_ctx)
126
+ return data
127
+
128
+ @staticmethod
129
+ def __rebuild_str(data: str, global_ctx: dict[str, str]):
130
+ return chevron.render(data, global_ctx)
131
+
132
+ @staticmethod
133
+ def __rebuild_any(data, global_ctx: dict[str, str]):
134
+ if isinstance(data, str):
135
+ return Rebuild.__rebuild_str(data, global_ctx)
136
+ if isinstance(data, list):
137
+ return Rebuild.__rebuild_list(data, global_ctx)
138
+ if isinstance(data, dict):
139
+ return Rebuild.__rebuild_map(data, global_ctx)
140
+
141
+ return data
142
+
143
+ @staticmethod
144
+ def rebuild(initial_context: dict, global_ctx: dict[str, str]):
145
+ return Rebuild.__rebuild_map(initial_context, global_ctx)
146
+
147
+
148
+ @dataclass
149
+ class ConfigOutput:
150
+ output_name: str
151
+ mustache_template: str | None
152
+ output_template: str | None
153
+ lang: str | None
154
+ types: str | None
155
+ partials: str | None
156
+ initial_context: dict
157
+ debug: bool
158
+ ndebug: bool
159
+
160
+ @staticmethod
161
+ def from_config(
162
+ data: dict[str, str | dict[str, Any]], root_default: "ConfigOutput"
163
+ ):
164
+ result = [ConfigOutput.from_dict(key, item) for key, item in data.items()]
165
+
166
+ for index in range(len(result)):
167
+ default = result[index]
168
+ if default.output_name != "":
169
+ continue
170
+
171
+ del result[index]
172
+ for item in result:
173
+ item.update_from(default)
174
+
175
+ break
176
+
177
+ for item in result:
178
+ item.update_from(root_default)
179
+
180
+ return result
181
+
182
+ @staticmethod
183
+ def from_dict(
184
+ output_name: str, data: str | dict[str, str | dict]
185
+ ) -> "ConfigOutput":
186
+ if isinstance(data, str):
187
+ return ConfigOutput(
188
+ output_name=output_name,
189
+ mustache_template=data,
190
+ output_template=None,
191
+ lang=None,
192
+ types=None,
193
+ partials=None,
194
+ initial_context={},
195
+ debug=False,
196
+ ndebug=False,
197
+ )
198
+
199
+ mustache_template = cast(str | None, data.get("template", None))
200
+ output_template = cast(str | None, data.get("path", None))
201
+ lang = cast(str | None, data.get("lang", None))
202
+ types = cast(str | None, data.get("types", None))
203
+ partials = cast(str | None, data.get("partials", None))
204
+ initial_context = copy.deepcopy(cast(dict, data.get("context", {})))
205
+ debug = not not data.get("debug", {})
206
+ ndebug = not not data.get("ndebug", {})
207
+
208
+ return ConfigOutput(
209
+ output_name=output_name,
210
+ mustache_template=mustache_template,
211
+ output_template=output_template,
212
+ lang=lang,
213
+ types=types,
214
+ partials=partials,
215
+ initial_context=initial_context,
216
+ debug=debug,
217
+ ndebug=ndebug,
218
+ )
219
+
220
+ def update_from(self, default: "ConfigOutput"):
221
+ self.mustache_template = self.mustache_template or default.mustache_template
222
+ self.output_template = self.output_template or default.output_template
223
+ self.lang = self.lang or default.lang
224
+ self.types = self.types or default.types
225
+ self.partials = self.partials or default.partials
226
+ self.initial_context = {
227
+ **copy.deepcopy(self.initial_context),
228
+ **copy.deepcopy(default.initial_context),
229
+ }
230
+ self.debug = self.debug or default.debug
231
+ self.ndebug = self.ndebug or default.ndebug
232
+
233
+
234
+ def _file_props(path: Path):
235
+ suffixes = "".join(path.suffixes)
236
+ name = path.name
237
+ shortest = name[: -len(suffixes)]
238
+ return {
239
+ "filename": path.as_posix(),
240
+ "dirname": path.parent.as_posix(),
241
+ "basename": path.name,
242
+ "stem": path.stem,
243
+ "suffix": path.suffix,
244
+ "suffixes": suffixes,
245
+ "shortest_stem": shortest,
246
+ }
247
+
248
+
249
+ def _rel_file_props(path: Path, root: Path):
250
+ props: dict = _file_props(path)
251
+ if path.is_relative_to(root):
252
+ rel = path.relative_to(root)
253
+ props["relative"] = {
254
+ "filename": rel.as_posix(),
255
+ "dirname": rel.parent.as_posix(),
256
+ }
257
+ return props
258
+
259
+
260
+ @dataclass
261
+ class Output:
262
+ output: Path
263
+ mustache_template: Path | None
264
+ lang: str | None
265
+ types: Path | None
266
+ partials: Path | None
267
+ initial_context: dict
268
+ debug: bool
269
+
270
+ @staticmethod
271
+ def filename_context(filename: str, root_dir: Path):
272
+ return _rel_file_props(Path(filename), root_dir)
273
+
274
+ @staticmethod
275
+ def abs_path(basedir: Path, path: str):
276
+ return basedir / path
277
+
278
+ @staticmethod
279
+ def from_config(
280
+ context: dict[str, str],
281
+ root_default: ConfigOutput,
282
+ data: dict[str, str | dict[str, Any]],
283
+ abs_path: Callable[[str], Path],
284
+ ):
285
+ expand_path = lambda text: chevron.render(template=text, data=context)
286
+
287
+ result: list[Output] = []
288
+
289
+ for output in ConfigOutput.from_config(data, root_default):
290
+ output_name = expand_path(output.output_name)
291
+ context["PATH"] = str(output_name)
292
+
293
+ if output.output_template:
294
+ output_name = expand_path(output.output_template)
295
+ output_name = abs_path(output_name)
296
+
297
+ mustache_template = None
298
+ if output.mustache_template:
299
+ mustache_template = abs_path(expand_path(output.mustache_template))
300
+
301
+ types = None
302
+ if output.types:
303
+ types = abs_path(expand_path(output.types))
304
+
305
+ partials = None
306
+ if output.partials:
307
+ partials = abs_path(expand_path(output.partials))
308
+
309
+ result.append(
310
+ Output(
311
+ output=output_name,
312
+ mustache_template=mustache_template,
313
+ lang=output.lang,
314
+ types=types,
315
+ partials=partials,
316
+ initial_context=Rebuild.rebuild(output.initial_context, context),
317
+ debug=output.debug and not output.ndebug,
318
+ )
319
+ )
320
+
321
+ return result
322
+
323
+ def get_type_replacements(self):
324
+ return TypeReplacement.load_config(self.lang, self.types)
325
+
326
+
327
+ @dataclass
328
+ class TemplateRule:
329
+ inputs: list[Path]
330
+ outputs: list[Output]
331
+
332
+ @staticmethod
333
+ def from_config(
334
+ global_ctx: dict, source_dir: Path, data: dict, abs_path: Callable[[str], Path]
335
+ ):
336
+ templates = cast(dict[str, dict[str, str | dict]], data.get("templates", {}))
337
+ inputs = cast(list[dict[str, str]], data.get("inputs", []))
338
+ return cast(
339
+ list[TemplateRule],
340
+ list(
341
+ filter(
342
+ lambda v: v is not None,
343
+ [
344
+ TemplateRule.__get_rule(
345
+ cast(list[str] | str, input.get("idl", [])),
346
+ cast(str | None, input.get("template")),
347
+ global_ctx,
348
+ source_dir,
349
+ templates,
350
+ abs_path,
351
+ )
352
+ for input in inputs
353
+ ],
354
+ )
355
+ ),
356
+ )
357
+
358
+ @staticmethod
359
+ def __get_rule(
360
+ input_names: str | list[str],
361
+ suite_id: str | None,
362
+ global_ctx: dict,
363
+ source_dir: Path,
364
+ templates: dict[str, dict[str, str | dict[str, Any]]],
365
+ abs_path: Callable[[str], Path],
366
+ ):
367
+ if suite_id is None:
368
+ return None
369
+
370
+ names = [input_names] if isinstance(input_names, str) else input_names
371
+ context = {**global_ctx, "input": Output.filename_context(names[0], source_dir)}
372
+
373
+ inputs = [abs_path(name) for name in names]
374
+ root_config = ConfigOutput.from_dict("", templates.get("", {}).get("", {}))
375
+ outputs = Output.from_config(
376
+ context, root_config, templates.get(suite_id, {}), abs_path
377
+ )
378
+
379
+ return TemplateRule(inputs=inputs, outputs=outputs)
380
+
381
+ def get_dependencies(
382
+ self, CMAKE_CURRENT_BINARY_DIR: Path | None, *additional_inputs: Path
383
+ ) -> dict[str, list[str]]:
384
+ def rel(path: Path):
385
+ return _relative(path, CMAKE_CURRENT_BINARY_DIR)
386
+
387
+ result: dict[str, list[str]] = {}
388
+ for out in self.outputs:
389
+ current_inputs: list[Path] = [*self.inputs, *additional_inputs]
390
+ if out.mustache_template:
391
+ current_inputs.append(out.mustache_template)
392
+ result[rel(out.output).as_posix()] = [
393
+ path.as_posix() for path in current_inputs
394
+ ]
395
+ return result
396
+
397
+
398
+ @dataclass
399
+ class TemplateConfig:
400
+ version: int
401
+ rules: list[TemplateRule]
402
+ ext_attrs: ExtAttrsContextBuilders
403
+
404
+ @staticmethod
405
+ def load_config(
406
+ config: Path,
407
+ context: dict,
408
+ source_dir: Path,
409
+ abs_path: Callable[[str], Path] | None = None,
410
+ ):
411
+ with config.open(encoding="UTF-8") as json_file:
412
+ data = cast(dict, json.load(json_file))
413
+
414
+ config_dep = config.absolute()
415
+ basedir = config_dep.parent
416
+
417
+ return TemplateConfig(
418
+ version=cast(int, data.get("version", 1)),
419
+ rules=TemplateRule.from_config(
420
+ context, source_dir, data, abs_path or (lambda path: basedir / path)
421
+ ),
422
+ ext_attrs=ExtAttrsContextBuilders.builtin().merge(
423
+ ExtAttrsContextBuilders.from_config(data)
424
+ ),
425
+ )
@@ -0,0 +1,10 @@
1
+ # Copyright (c) 2026 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+ from proj_flow.ext.webidl.cli.cmake import cmake
5
+ from proj_flow.ext.webidl.cli.depfile import depfile
6
+ from proj_flow.ext.webidl.cli.gen import gen
7
+ from proj_flow.ext.webidl.cli.init import init
8
+ from proj_flow.ext.webidl.cli.root import webidl
9
+
10
+ __all__ = ["webidl", "init", "cmake", "depfile", "gen"]
@@ -0,0 +1,82 @@
1
+ # Copyright (c) 2026 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+ import typing
5
+ from pathlib import Path
6
+
7
+ import chevron
8
+
9
+ from proj_flow.api import arg, env
10
+ from proj_flow.ext.webidl.base.config import (
11
+ CMakeDirs,
12
+ TemplateConfig,
13
+ load_package_template,
14
+ )
15
+ from proj_flow.ext.webidl.cli.updater import update_file_if_needed
16
+
17
+
18
+ @arg.command("webidl", "cmake")
19
+ def cmake(
20
+ config_path: typing.Annotated[
21
+ str, arg.Argument(help="Input configuration", names=["--cfg"], meta="json-file")
22
+ ],
23
+ binary_dir: typing.Annotated[
24
+ str,
25
+ arg.Argument(
26
+ help="Project binary dir", names=["--binary-dir"], meta="project-binary-dir"
27
+ ),
28
+ ],
29
+ target: typing.Annotated[
30
+ str | None,
31
+ arg.Argument(
32
+ help="Name of the target to add output files to", meta="name", opt=True
33
+ ),
34
+ ],
35
+ rt: env.Runtime,
36
+ ):
37
+ """Write the CMake configuration based on given config"""
38
+
39
+ global_ctx = {
40
+ key: f"${{{key}}}"
41
+ for key in [
42
+ "CMAKE_CURRENT_SOURCE_DIR",
43
+ "CMAKE_CURRENT_BINARY_DIR",
44
+ "PROJECT_SOURCE_DIR",
45
+ "PROJECT_BINARY_DIR",
46
+ ]
47
+ }
48
+ config_filename = Path(config_path)
49
+ cmake_dirs = CMakeDirs.from_config_path(
50
+ config_filename, project_binary_dir=Path(binary_dir)
51
+ )
52
+
53
+ abs_path = cmake_dirs.make_abs_path()
54
+
55
+ depfile_ref = cmake_dirs.prefix(
56
+ str(cmake_dirs.dependency_from_config(config_filename))
57
+ )
58
+ cmake_filename = cmake_dirs.cmake_from_config(config_filename)
59
+ config_noext = cmake_dirs.filename_from_config(config_filename, "")
60
+
61
+ config = TemplateConfig.load_config(
62
+ config_filename.absolute(), global_ctx, cmake_dirs.project_source_dir, abs_path
63
+ )
64
+
65
+ context = {
66
+ "config": {
67
+ "binary": cmake_dirs.prefix(str(config_noext)).as_posix(),
68
+ "source": cmake_dirs.prefix(str(config_filename.absolute())).as_posix(),
69
+ "basename": config_filename.name,
70
+ },
71
+ "output": [
72
+ out.output.as_posix() for rule in config.rules for out in rule.outputs
73
+ ],
74
+ "target": target,
75
+ "depfile": depfile_ref.as_posix(),
76
+ }
77
+
78
+ text = load_package_template("cmake")
79
+ text = chevron.render(text, data=context, partials_path=None)
80
+ update_file_if_needed(
81
+ cmake_filename, text, f"Generating {cmake_filename.as_posix()}"
82
+ )
@@ -0,0 +1,83 @@
1
+ # Copyright (c) 2026 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+ import typing
5
+ from pathlib import Path
6
+
7
+ import chevron
8
+
9
+ from proj_flow.api import arg, env
10
+ from proj_flow.ext.webidl.base.config import (
11
+ CMakeDirs,
12
+ TemplateConfig,
13
+ load_package_template,
14
+ )
15
+ from proj_flow.ext.webidl.cli.updater import update_file_if_needed
16
+
17
+
18
+ @arg.command("webidl", "depfile")
19
+ def depfile(
20
+ config_path: typing.Annotated[
21
+ str, arg.Argument(help="Input configuration", names=["--cfg"], meta="json-file")
22
+ ],
23
+ binary_dir: typing.Annotated[
24
+ str,
25
+ arg.Argument(
26
+ help="Project binary dir", names=["--binary-dir"], meta="project-binary-dir"
27
+ ),
28
+ ],
29
+ output_path: typing.Annotated[
30
+ str | None,
31
+ arg.Argument(
32
+ help="Output path", names=["--out"], meta="dependency-file", opt=True
33
+ ),
34
+ ],
35
+ rt: env.Runtime,
36
+ ):
37
+ """Write the dependency file for CMake generators to use"""
38
+
39
+ cmake_dirs = CMakeDirs.from_config_path(
40
+ Path(config_path), project_binary_dir=Path(binary_dir)
41
+ )
42
+ global_ctx = cmake_dirs.my_defines()
43
+
44
+ config_filename = Path(config_path).absolute()
45
+ config = TemplateConfig.load_config(
46
+ config_filename, global_ctx, cmake_dirs.project_source_dir
47
+ )
48
+
49
+ output = (
50
+ Path(output_path)
51
+ if output_path
52
+ else cmake_dirs.dependency_from_config(Path(config_path))
53
+ )
54
+
55
+ flat_deps: dict[str, set[str]] = {}
56
+
57
+ for rule in config.rules:
58
+ for outname, inputs in rule.get_dependencies(None, config_filename).items():
59
+ try:
60
+ flat_deps[outname].update(inputs)
61
+ except KeyError:
62
+ flat_deps[outname] = set(inputs)
63
+ reverse: dict[str, set[str]] = {}
64
+ for key, value in flat_deps.items():
65
+ val = ";".join(sorted(value))
66
+ try:
67
+ reverse[val].update(key)
68
+ except KeyError:
69
+ reverse[val] = {key}
70
+
71
+ rules: list[dict] = []
72
+ for deps, tgts in reverse.items():
73
+ dependency = deps.split(";")
74
+ target = [{"sep": " \\\n", "pathname": pathname} for pathname in tgts]
75
+ target[0]["sep"] = ""
76
+ rules.append({"dependency": dependency, "target": target})
77
+
78
+ text = load_package_template("depfile")
79
+ update_file_if_needed(
80
+ output,
81
+ chevron.render(text, {"rule": rules}),
82
+ f"Generating {output.as_posix()}",
83
+ )