msra-codegen 0.1.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 (68) hide show
  1. msra_codegen/README.md +23 -0
  2. msra_codegen/__init__.py +6 -0
  3. msra_codegen/__main__.py +5 -0
  4. msra_codegen/bridge.py +29 -0
  5. msra_codegen/cli.py +105 -0
  6. msra_codegen/codegen_context.py +1690 -0
  7. msra_codegen/config.toml +164 -0
  8. msra_codegen/core_naming.py +155 -0
  9. msra_codegen/docs_generator.py +346 -0
  10. msra_codegen/file_utils.py +8 -0
  11. msra_codegen/funcresult.py +156 -0
  12. msra_codegen/generator.py +6 -0
  13. msra_codegen/generator_config.py +35 -0
  14. msra_codegen/github_workflows.py +129 -0
  15. msra_codegen/gitignore.py +31 -0
  16. msra_codegen/issue_templates.py +100 -0
  17. msra_codegen/logo_assets.py +99 -0
  18. msra_codegen/msra_serializer.py +205 -0
  19. msra_codegen/node_export.js +296 -0
  20. msra_codegen/package_metadata.py +306 -0
  21. msra_codegen/package_writer.py +175 -0
  22. msra_codegen/project_model.py +490 -0
  23. msra_codegen/python_formatting.py +88 -0
  24. msra_codegen/python_render.py +242 -0
  25. msra_codegen/readme_pipeline.py +519 -0
  26. msra_codegen/requirements.txt +5 -0
  27. msra_codegen/template_engine.py +26 -0
  28. msra_codegen/templates/Makefile.tpl +44 -0
  29. msra_codegen/templates/README.md.tpl +55 -0
  30. msra_codegen/templates/abstraction/__init__.py.tpl +188 -0
  31. msra_codegen/templates/abstraction/regexes.py.tpl +25 -0
  32. msra_codegen/templates/docs/requirements.txt.tpl +3 -0
  33. msra_codegen/templates/docs/source/Makefile.tpl +20 -0
  34. msra_codegen/templates/docs/source/api.rst.tpl +9 -0
  35. msra_codegen/templates/docs/source/conf.py.tpl +88 -0
  36. msra_codegen/templates/docs/source/index.rst.tpl +14 -0
  37. msra_codegen/templates/docs/source/module.rst.tpl +34 -0
  38. msra_codegen/templates/docs/source/quick_start.rst.tpl +19 -0
  39. msra_codegen/templates/endpoints_init.py.tpl +15 -0
  40. msra_codegen/templates/example.py.tpl +1 -0
  41. msra_codegen/templates/function.py.tpl +364 -0
  42. msra_codegen/templates/github/issue_templates/bug_report.yml.tpl +55 -0
  43. msra_codegen/templates/github/issue_templates/config.yml.tpl +8 -0
  44. msra_codegen/templates/github/issue_templates/documentation_issue.yml.tpl +33 -0
  45. msra_codegen/templates/github/issue_templates/feature_request.yml.tpl +36 -0
  46. msra_codegen/templates/github/workflows/publish.yml.tpl +100 -0
  47. msra_codegen/templates/github/workflows/source-sync.yml.tpl +177 -0
  48. msra_codegen/templates/github/workflows/tests.yml.tpl +69 -0
  49. msra_codegen/templates/gitignore.tpl +3 -0
  50. msra_codegen/templates/group.py.tpl +56 -0
  51. msra_codegen/templates/group_init.py.tpl +14 -0
  52. msra_codegen/templates/init.py.tpl +4 -0
  53. msra_codegen/templates/licenses/GPL-3.0-or-later.txt.tpl +674 -0
  54. msra_codegen/templates/licenses/MIT.txt.tpl +21 -0
  55. msra_codegen/templates/manager.py.tpl +257 -0
  56. msra_codegen/templates/pyproject.toml.tpl +38 -0
  57. msra_codegen/templates/tests/api_test.py.tpl +49 -0
  58. msra_codegen/templates/tests/conftest.py.tpl +21 -0
  59. msra_codegen/templates/variable.py.tpl +54 -0
  60. msra_codegen/tests_generator.py +988 -0
  61. msra_codegen/typespec.py +275 -0
  62. msra_codegen/validation.py +118 -0
  63. msra_codegen-0.1.0.dist-info/METADATA +47 -0
  64. msra_codegen-0.1.0.dist-info/RECORD +68 -0
  65. msra_codegen-0.1.0.dist-info/WHEEL +5 -0
  66. msra_codegen-0.1.0.dist-info/entry_points.txt +2 -0
  67. msra_codegen-0.1.0.dist-info/licenses/LICENSE +674 -0
  68. msra_codegen-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,490 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from .core_naming import group_path_from_expr, parse_script_reference
7
+ from .python_render import get_plain_value
8
+ from .typespec import extract_variable_match
9
+
10
+
11
+ def build_group_tree(project: dict[str, Any]) -> dict[str, Any]:
12
+ root: dict[str, Any] = {"children": {}, "functions": [], "path": []}
13
+ for group in project.get("groups", []):
14
+ node = root
15
+ for segment in group.get("path", []):
16
+ node = node["children"].setdefault(
17
+ segment,
18
+ {
19
+ "name": segment,
20
+ "path": node["path"] + [segment],
21
+ "description": "",
22
+ "children": {},
23
+ "functions": [],
24
+ },
25
+ )
26
+ node["path"] = node["path"]
27
+ node["description"] = group.get("description", "")
28
+ for func in project.get("functions", []):
29
+ path = [part for part in str(func.get("group", "")).split(".") if part]
30
+ node = root
31
+ for segment in path:
32
+ node = node["children"].setdefault(
33
+ segment,
34
+ {
35
+ "name": segment,
36
+ "path": node["path"] + [segment],
37
+ "description": "",
38
+ "children": {},
39
+ "functions": [],
40
+ },
41
+ )
42
+ node["functions"].append(func)
43
+ return root
44
+
45
+
46
+ def iter_group_nodes(tree: dict[str, Any]) -> list[dict[str, Any]]:
47
+ nodes: list[dict[str, Any]] = []
48
+
49
+ def walk(node: dict[str, Any]) -> None:
50
+ for child in node.get("children", {}).values():
51
+ nodes.append(child)
52
+ walk(child)
53
+
54
+ walk(tree)
55
+ return nodes
56
+
57
+
58
+ def top_level_groups(tree: dict[str, Any]) -> list[dict[str, Any]]:
59
+ return list(tree.get("children", {}).values())
60
+
61
+
62
+ def build_project(ast: dict[str, Any], msra_path: Path) -> dict[str, Any]:
63
+ tables = [table for table in ast.get("tables", [])]
64
+ table_index: dict[tuple[str, ...], list[dict[str, Any]]] = {}
65
+ for table in tables:
66
+ table_index.setdefault(tuple(table["path"]), []).append(table)
67
+ examples_by_function: dict[str, list[dict[str, Any]]] = {}
68
+
69
+ def get_table(path: list[str] | tuple[str, ...]) -> dict[str, Any] | None:
70
+ entries = table_index.get(tuple(path))
71
+ if not entries:
72
+ return None
73
+ return entries[0]
74
+
75
+ def get_tables(path: list[str] | tuple[str, ...]) -> list[dict[str, Any]]:
76
+ return list(table_index.get(tuple(path), []))
77
+
78
+ def get_assignment_entry(table: dict[str, Any] | None, key: str) -> dict[str, Any] | None:
79
+ if not table:
80
+ return None
81
+ for assignment in table.get("assignments", []):
82
+ if assignment.get("key") == key:
83
+ return assignment
84
+ return None
85
+
86
+ def get_assignment(table: dict[str, Any] | None, key: str, default: Any = None) -> Any:
87
+ entry = get_assignment_entry(table, key)
88
+ if entry is None:
89
+ return default
90
+ return entry.get("value")
91
+
92
+ app_table = get_table(["app"])
93
+ app = {
94
+ "name": str(get_plain_value(get_assignment(app_table, "name", "GeneratedAPI"))),
95
+ "package_name": str(get_plain_value(get_assignment(app_table, "package_name", ""))),
96
+ "package_owner": str(get_plain_value(get_assignment(app_table, "package_owner", ""))),
97
+ "social": get_plain_value(get_assignment(app_table, "social", {})),
98
+ "authors": get_plain_value(get_assignment(app_table, "authors", [])),
99
+ "logo": str(get_plain_value(get_assignment(app_table, "logo", ""))).strip(),
100
+ "license": str(get_plain_value(get_assignment(app_table, "license", "MIT"))),
101
+ "keywords": get_plain_value(get_assignment(app_table, "keywords", [])),
102
+ "min_required_python": str(get_plain_value(get_assignment(app_table, "min_required_python", "3.10"))),
103
+ "description": str(get_plain_value(get_assignment(app_table, "description", ""))),
104
+ "version": str(get_plain_value(get_assignment(app_table, "version", "0.1.0"))),
105
+ "timeout_ms": int(get_plain_value(get_assignment(app_table, "timeout_ms", 35000))),
106
+ "browser": str(get_plain_value(get_assignment(app_table, "browser", "camoufox"))),
107
+ "humanize": get_plain_value(get_assignment(app_table, "humanize", False)),
108
+ "block_images": bool(get_plain_value(get_assignment(app_table, "block_images", False))),
109
+ "disallow_headless": bool(get_plain_value(get_assignment(app_table, "disallow_headless", False))),
110
+ "sync": {},
111
+ "abstractions": [],
112
+ }
113
+ abstractions_value = get_plain_value(get_assignment(app_table, "abstractions", []))
114
+ if not isinstance(abstractions_value, list):
115
+ raise TypeError("app.abstractions must be a list of strings.")
116
+ for item in abstractions_value:
117
+ if not isinstance(item, str):
118
+ raise TypeError("app.abstractions entries must be strings.")
119
+ text = item.strip()
120
+ if text:
121
+ app["abstractions"].append(text)
122
+
123
+ prefixes_table = get_table(["app", "prefixes"])
124
+ prefixes = {
125
+ assignment["key"]: get_plain_value(assignment["value"])
126
+ for assignment in (prefixes_table or {}).get("assignments", [])
127
+ }
128
+
129
+ regex_tables = [
130
+ table
131
+ for table in tables
132
+ if len(table["path"]) == 3 and table["path"][0] == "app" and table["path"][1] == "regexes"
133
+ ]
134
+ regexes: list[dict[str, Any]] = []
135
+ for table in regex_tables:
136
+ regexes.append(
137
+ {
138
+ "name": table["path"][2],
139
+ "regex": get_plain_value(get_assignment(table, "regex", "")),
140
+ "raise": get_plain_value(get_assignment(table, "raise", "")),
141
+ "description": get_plain_value(get_assignment(table, "description", "")),
142
+ }
143
+ )
144
+
145
+ groups_tables = [
146
+ table
147
+ for table in tables
148
+ if len(table["path"]) >= 3 and table["path"][0] == "app" and table["path"][1] == "groups"
149
+ ]
150
+ groups = []
151
+ for table in groups_tables:
152
+ groups.append(
153
+ {
154
+ "path": table["path"][2:],
155
+ "name": ".".join(table["path"][2:]),
156
+ "description": get_plain_value(get_assignment(table, "description", "")),
157
+ }
158
+ )
159
+
160
+ headers_table = get_table(["app", "defaults", "func", "headers"])
161
+ headers_spec = build_headers_spec(headers_table, get_assignment)
162
+
163
+ warmup_table = get_table(["app", "warmup"])
164
+ warmup_spec = None
165
+ if warmup_table:
166
+ warmup_script = parse_script_reference(get_assignment(warmup_table, "warmup"))
167
+ warmup_spec = {
168
+ "headers_sniffer": bool(get_plain_value(get_assignment(warmup_table, "headers_sniffer", False))),
169
+ "on_error_screenshot_path": get_plain_value(get_assignment(warmup_table, "on_error_screenshot_path", "")),
170
+ "script": warmup_script,
171
+ }
172
+
173
+ sync_table = get_table(["app", "sync"])
174
+ if sync_table:
175
+ preserved_target_paths_value = get_plain_value(
176
+ get_assignment(sync_table, "preserved_target_paths", [])
177
+ )
178
+ if not isinstance(preserved_target_paths_value, list):
179
+ raise TypeError("app.sync.preserved_target_paths must be a list of strings.")
180
+ preserved_target_paths: list[str] = []
181
+ for item in preserved_target_paths_value:
182
+ if not isinstance(item, str):
183
+ raise TypeError("app.sync.preserved_target_paths entries must be strings.")
184
+ text = item.strip()
185
+ if text:
186
+ preserved_target_paths.append(text)
187
+ ignored_generated_patterns_value = get_plain_value(
188
+ get_assignment(sync_table, "ignored_generated_patterns", [])
189
+ )
190
+ if not isinstance(ignored_generated_patterns_value, list):
191
+ raise TypeError("app.sync.ignored_generated_patterns must be a list of strings.")
192
+ ignored_generated_patterns: list[str] = []
193
+ for item in ignored_generated_patterns_value:
194
+ if not isinstance(item, str):
195
+ raise TypeError("app.sync.ignored_generated_patterns entries must be strings.")
196
+ text = item.strip()
197
+ if text:
198
+ ignored_generated_patterns.append(text)
199
+ app["sync"] = {
200
+ "preserved_target_paths": preserved_target_paths,
201
+ "ignored_generated_patterns": ignored_generated_patterns,
202
+ }
203
+
204
+ issue_templates_table = get_table(["app", "issue_templates"])
205
+ issue_templates_child_tables = [
206
+ table
207
+ for table in tables
208
+ if len(table["path"]) >= 3 and table["path"][0] == "app" and table["path"][1] == "issue_templates"
209
+ and len(table["path"]) > 2
210
+ ]
211
+ if issue_templates_child_tables:
212
+ raise RuntimeError("app.issue_templates only supports the assignee field and no child tables.")
213
+ if issue_templates_table:
214
+ app["issue_templates"] = build_issue_templates_spec(issue_templates_table, get_assignment)
215
+
216
+ variable_tables = [
217
+ table
218
+ for table in tables
219
+ if len(table["path"]) == 3 and table["path"][0] == "app" and table["path"][1] == "variables"
220
+ ]
221
+ variables = []
222
+ for table in variable_tables:
223
+ types_expr = get_assignment(table, "types")
224
+ variables.append(
225
+ {
226
+ "name": table["path"][2],
227
+ "types": types_expr,
228
+ "match": extract_variable_match(types_expr),
229
+ "read_only": bool(get_plain_value(get_assignment(table, "read_only", False))),
230
+ "nullable": bool(get_plain_value(get_assignment(table, "nullable", False))),
231
+ "from": get_assignment(table, "from"),
232
+ "description": get_plain_value(get_assignment(table, "description", "")),
233
+ }
234
+ )
235
+
236
+ functions: list[dict[str, Any]] = []
237
+ for table in tables:
238
+ path = table["path"]
239
+ if len(path) == 5 and path[0] == "app" and path[1] == "func" and path[3] == "examples":
240
+ func_id = path[2]
241
+ docs_assignment = get_assignment_entry(table, "docs")
242
+ examples_by_function.setdefault(func_id, []).append(
243
+ {
244
+ "name": path[4],
245
+ "docs": bool(docs_assignment and bool(get_plain_value(docs_assignment.get("value")))),
246
+ "test": bool(get_plain_value(get_assignment(table, "test", False))),
247
+ "type": str(get_plain_value(get_assignment(table, "type", "json"))).strip().lower(),
248
+ "description": str(get_plain_value(get_assignment(table, "description", ""))),
249
+ "inputs": get_assignment(table, "inputs"),
250
+ "print": extract_docs_print_value(docs_assignment),
251
+ }
252
+ )
253
+ continue
254
+ if len(path) != 3 or path[0] != "app" or path[1] != "func":
255
+ continue
256
+ func_id = path[2]
257
+
258
+ root = table
259
+ group = group_path_from_expr(get_assignment(root, "group", ""))
260
+ transport = str(get_plain_value(get_assignment(root, "transport", "fetch")))
261
+ method = str(get_plain_value(get_assignment(root, "method", "GET")))
262
+ functions.append(
263
+ {
264
+ "id": func_id,
265
+ "name": str(get_plain_value(get_assignment(root, "name", func_id.lower()))),
266
+ "group": group,
267
+ "transport": transport,
268
+ "method": method,
269
+ "description": str(get_plain_value(get_assignment(root, "description", ""))),
270
+ "root_table": root,
271
+ "inputs": [],
272
+ "url": None,
273
+ "body": None,
274
+ "headers": None,
275
+ "extractor": None,
276
+ "examples": examples_by_function.get(func_id, []),
277
+ }
278
+ )
279
+
280
+ for func in functions:
281
+ func_id = func["id"]
282
+ prefix = ["app", "func", func_id]
283
+ input_tables = [
284
+ table
285
+ for table in tables
286
+ if len(table["path"]) == 5
287
+ and table["path"][:4] == prefix + ["input"]
288
+ ]
289
+ function_overload_tables = [
290
+ table
291
+ for table in tables
292
+ if len(table["path"]) == 5
293
+ and table["path"][:4] == prefix + ["overload"]
294
+ ]
295
+ overload_names = [str(table["path"][-1]) for table in function_overload_tables]
296
+
297
+ input_contexts = []
298
+ for table in input_tables:
299
+ input_name = str(table["path"][-1])
300
+ input_overload_tables = [
301
+ overload_table
302
+ for overload_table in tables
303
+ if len(overload_table["path"]) == 7
304
+ and overload_table["path"][:5] == prefix + ["input", input_name]
305
+ and overload_table["path"][5] == "overload"
306
+ ]
307
+ overload_specs = {
308
+ str(overload_table["path"][-1]): build_input_spec(
309
+ overload_table,
310
+ get_assignment,
311
+ get_assignment_entry,
312
+ explicit_only=True,
313
+ )
314
+ for overload_table in input_overload_tables
315
+ }
316
+ overload_names.extend(name for name in overload_specs.keys() if name not in overload_names)
317
+ input_context = build_input_spec(table, get_assignment, get_assignment_entry)
318
+ input_context["overloads"] = overload_specs
319
+ input_contexts.append(input_context)
320
+ func["inputs"] = input_contexts
321
+ func["overload_names"] = list(dict.fromkeys(overload_names))
322
+
323
+ url_tables = get_tables(prefix + ["url"])
324
+ if url_tables:
325
+ param_tables = [
326
+ table
327
+ for table in tables
328
+ if len(table["path"]) == 6
329
+ and table["path"][:5] == prefix + ["url", "params"]
330
+ ]
331
+ url_entries = []
332
+ for url_table in url_tables:
333
+ url_entries.append(
334
+ {
335
+ "base": get_assignment(url_table, "base"),
336
+ "priority": get_plain_value(get_assignment(url_table, "priority", 0)),
337
+ "wants": get_assignment(url_table, "wants"),
338
+ }
339
+ )
340
+ func["url"] = {
341
+ "base": url_entries[0]["base"] if url_entries else None,
342
+ "entries": url_entries,
343
+ "params": [build_url_param_spec(table, get_assignment) for table in param_tables],
344
+ }
345
+ else:
346
+ func["url"] = None
347
+
348
+ body_table = get_table(prefix + ["body"])
349
+ if body_table:
350
+ func["body"] = {
351
+ "type": str(get_plain_value(get_assignment(body_table, "type", "application/json"))),
352
+ "from": get_assignment(body_table, "from"),
353
+ }
354
+
355
+ func_headers_table = get_table(prefix + ["headers"])
356
+ if func_headers_table:
357
+ func["headers"] = build_headers_spec(func_headers_table, get_assignment)
358
+
359
+ extractor_table = get_table(prefix + ["extractor"])
360
+ if extractor_table:
361
+ goto_pipeline = parse_script_reference(get_assignment(extractor_table, "goto_pipeline"))
362
+ func["extractor"] = {
363
+ "render_html": bool(get_plain_value(get_assignment(extractor_table, "render_html", False))),
364
+ "script": get_plain_value(get_assignment(extractor_table, "script", "")),
365
+ "goto_pipeline": goto_pipeline,
366
+ }
367
+
368
+ for func in functions:
369
+ func["examples"] = examples_by_function.get(func["id"], [])
370
+
371
+ return {
372
+ "source_path": str(msra_path.resolve()),
373
+ "app": app,
374
+ "prefixes": prefixes,
375
+ "regexes": regexes,
376
+ "groups": groups,
377
+ "variables": variables,
378
+ "headers": headers_spec,
379
+ "warmup": warmup_spec,
380
+ "functions": functions,
381
+ }
382
+
383
+
384
+ def extract_docs_print_value(docs_assignment: dict[str, Any] | None) -> Any:
385
+ if not docs_assignment or not bool(docs_assignment.get("annotation")):
386
+ return None
387
+ if str(docs_assignment.get("annotationName") or "").lower() != "docs":
388
+ return None
389
+ args = docs_assignment.get("annotationArgs")
390
+ if not isinstance(args, list):
391
+ return None
392
+ for arg in args:
393
+ if isinstance(arg, dict) and str(arg.get("name") or "") == "print":
394
+ return arg.get("value")
395
+ return None
396
+
397
+
398
+ def build_input_spec(
399
+ table: dict[str, Any],
400
+ get_assignment,
401
+ get_assignment_entry=None,
402
+ *,
403
+ explicit_only: bool = False,
404
+ ) -> dict[str, Any]:
405
+ spec: dict[str, Any] = {
406
+ "name": table["path"][-1],
407
+ }
408
+
409
+ def assignment_value(key: str, default: Any = None) -> Any:
410
+ if get_assignment_entry is None:
411
+ if explicit_only:
412
+ return default
413
+ return get_assignment(table, key, default)
414
+ entry = get_assignment_entry(table, key)
415
+ if entry is None:
416
+ if explicit_only:
417
+ return None
418
+ return get_assignment(table, key, default)
419
+ return entry.get("value")
420
+
421
+ def include(key: str, value: Any) -> None:
422
+ if explicit_only and value is None:
423
+ return
424
+ spec[key] = value
425
+
426
+ include("type", assignment_value("type"))
427
+ include("default", assignment_value("default"))
428
+ if not explicit_only:
429
+ include("required", bool(get_plain_value(assignment_value("required", False))))
430
+ else:
431
+ required_value = assignment_value("required")
432
+ if required_value is not None:
433
+ include("required", bool(get_plain_value(required_value)))
434
+ include("const", assignment_value("const"))
435
+ include("values", assignment_value("values"))
436
+ include("match", assignment_value("match"))
437
+ if not explicit_only:
438
+ include("read_only", bool(get_plain_value(assignment_value("read_only", False))))
439
+ else:
440
+ read_only_value = assignment_value("read_only")
441
+ if read_only_value is not None:
442
+ include("read_only", bool(get_plain_value(read_only_value)))
443
+ include("from", assignment_value("from"))
444
+ description_value = assignment_value("description")
445
+ if description_value is None:
446
+ if not explicit_only:
447
+ spec["description"] = ""
448
+ else:
449
+ spec["description"] = str(get_plain_value(description_value))
450
+ return spec
451
+
452
+
453
+ def build_headers_spec(table: dict[str, Any] | None, get_assignment) -> dict[str, Any] | None:
454
+ if not table:
455
+ return None
456
+ return {
457
+ "referrer": get_assignment(table, "referrer"),
458
+ "cors_mode": get_assignment(table, "cors_mode"),
459
+ "credentials": get_assignment(table, "credentials"),
460
+ "headers": get_assignment(table, "headers"),
461
+ }
462
+
463
+
464
+ def build_issue_templates_spec(root_table: dict[str, Any], get_assignment) -> dict[str, Any]:
465
+ assignee = str(get_plain_value(get_assignment(root_table, "assignee", ""))).strip()
466
+ if not assignee:
467
+ raise RuntimeError('app.issue_templates.assignee is required and must be a non-empty string.')
468
+ return {
469
+ "assignee": assignee,
470
+ }
471
+
472
+
473
+ def build_url_param_spec(table: dict[str, Any], get_assignment) -> dict[str, Any]:
474
+ list_style = get_plain_value(get_assignment(table, "list_style"))
475
+ if not isinstance(list_style, dict):
476
+ list_style = {}
477
+ return {
478
+ "name": table["path"][-1],
479
+ "sub_url": bool(get_plain_value(get_assignment(table, "sub_url", False))),
480
+ "list": bool(get_plain_value(get_assignment(table, "list", False))),
481
+ "list_style": {
482
+ "style": str(list_style.get("style", "repeat") or "repeat").strip().lower(),
483
+ "delimiter": str(list_style.get("delimiter", ",") or ","),
484
+ "indexed": bool(list_style.get("indexed", False)),
485
+ },
486
+ "from": get_assignment(table, "from"),
487
+ "const": get_assignment(table, "const"),
488
+ "values": get_assignment(table, "values"),
489
+ "description": str(get_plain_value(get_assignment(table, "description", ""))),
490
+ }
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ import sys
5
+ import tempfile
6
+ from pathlib import Path
7
+ from typing import Iterable
8
+
9
+ from .generator_config import config_section
10
+
11
+
12
+ def get_python_line_length() -> int:
13
+ ruff_config = config_section("ruff")
14
+ value = ruff_config.get("line_length")
15
+ if not isinstance(value, int):
16
+ raise RuntimeError("ruff.line_length must be an integer.")
17
+ return value
18
+
19
+
20
+ def format_python_source(source: str, *, line_length: int | None = None) -> str:
21
+ with tempfile.TemporaryDirectory(prefix="msra-python-format-") as tmp_dir:
22
+ temp_path = Path(tmp_dir) / "snippet.py"
23
+ temp_path.write_text(source, encoding="utf-8")
24
+ format_python_files([temp_path], line_length=line_length)
25
+ return temp_path.read_text(encoding="utf-8")
26
+
27
+
28
+ def format_python_files(paths: Iterable[Path], *, line_length: int | None = None) -> None:
29
+ path_list = [Path(path).resolve() for path in paths]
30
+ if not path_list:
31
+ return
32
+
33
+ effective_line_length = line_length if line_length is not None else get_python_line_length()
34
+ file_args = [str(path) for path in path_list]
35
+ run_ruff_tool(
36
+ [
37
+ "-m",
38
+ "ruff",
39
+ "check",
40
+ "--select",
41
+ "I",
42
+ "--fix",
43
+ "--line-length",
44
+ str(effective_line_length),
45
+ *file_args,
46
+ ]
47
+ )
48
+ run_ruff_tool(
49
+ [
50
+ "-m",
51
+ "ruff",
52
+ "format",
53
+ "--line-length",
54
+ str(effective_line_length),
55
+ *file_args,
56
+ ]
57
+ )
58
+
59
+
60
+ def format_python_tree(root: Path, *, line_length: int | None = None) -> None:
61
+ python_files = [
62
+ path
63
+ for path in root.rglob("*.py")
64
+ if "__pycache__" not in path.parts and path.is_file()
65
+ ]
66
+ format_python_files(python_files, line_length=line_length)
67
+
68
+
69
+ def run_ruff_tool(arguments: list[str]) -> None:
70
+ process = subprocess.run(
71
+ [sys.executable, *arguments],
72
+ capture_output=True,
73
+ text=True,
74
+ encoding="utf-8",
75
+ errors="replace",
76
+ check=False,
77
+ )
78
+ if process.returncode == 0:
79
+ return
80
+
81
+ stderr = process.stderr.strip()
82
+ stdout = process.stdout.strip()
83
+ details = stderr or stdout or "ruff formatting command failed without output"
84
+ raise RuntimeError(
85
+ f"Formatting command failed with exit code {process.returncode}.\n"
86
+ f"Command: {sys.executable} {' '.join(arguments)}\n"
87
+ f"{details}"
88
+ )