overture-schema-codegen 0.1.1.dev0__tar.gz
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.
- overture_schema_codegen-0.1.1.dev0/PKG-INFO +13 -0
- overture_schema_codegen-0.1.1.dev0/pyproject.toml +62 -0
- overture_schema_codegen-0.1.1.dev0/pyproject.toml.orig +46 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/__init__.py +1 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/cli.py +228 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/__init__.py +0 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/docstring.py +46 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/enum_extraction.py +40 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/examples.py +367 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/field.py +172 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/field_constraints.py +185 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/field_walk.py +275 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/length_constraints.py +49 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/literal_alternatives.py +26 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/model_constraints.py +252 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/model_extraction.py +240 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/newtype_extraction.py +73 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/numeric_extraction.py +74 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/pydantic_extraction.py +33 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/specs.py +295 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/type_analyzer.py +693 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/type_registry.py +137 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/union_extraction.py +270 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/layout/__init__.py +0 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/layout/module_layout.py +139 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/layout/type_collection.py +122 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/__init__.py +0 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/link_computation.py +70 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/path_assignment.py +114 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/pipeline.py +198 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/renderer.py +641 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/reverse_references.py +169 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/templates/_used_by.md.jinja2 +10 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/templates/enum.md.jinja2 +13 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/templates/feature.md.jinja2 +45 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/templates/geometric.md.jinja2 +11 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/templates/newtype.md.jinja2 +17 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/templates/numeric.md.jinja2 +27 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/templates/pydantic_type.md.jinja2 +8 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/type_format.py +383 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/py.typed +0 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/__init__.py +1 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/_primitive_fill.py +23 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/_render_common.py +477 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/check_builder.py +961 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/check_ir.py +223 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/constraint_dispatch.py +753 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/pipeline.py +220 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/renderer.py +816 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/schema_builder.py +187 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/templates/_check_function.py.jinja2 +10 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/templates/model_module.py.jinja2 +83 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/templates/test_module.py.jinja2 +129 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/test_data/__init__.py +9 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/test_data/base_row.py +835 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/test_data/constraint_values.py +203 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/test_data/invalid_value.py +105 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/test_data/scaffold.py +390 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/test_renderer.py +708 -0
- overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/spec_discovery.py +66 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: overture-schema-codegen
|
|
3
|
+
Version: 0.1.1.dev0
|
|
4
|
+
Summary: Code generator that produces documentation and code from Pydantic models
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Dist: click>=8.1
|
|
7
|
+
Requires-Dist: jinja2>=3.0
|
|
8
|
+
Requires-Dist: overture-schema-cli>=0.1.1
|
|
9
|
+
Requires-Dist: overture-schema-common>=0.1.1
|
|
10
|
+
Requires-Dist: overture-schema-system>=0.1.1
|
|
11
|
+
Requires-Dist: tomli>=2.0 ; python_full_version < '3.11'
|
|
12
|
+
Requires-Dist: typing-extensions>=4.0
|
|
13
|
+
Requires-Python: >=3.10
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.11.32,<0.13"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
dependencies = [
|
|
7
|
+
"click>=8.1",
|
|
8
|
+
"jinja2>=3.0",
|
|
9
|
+
"overture-schema-cli>=0.1.1",
|
|
10
|
+
"overture-schema-common>=0.1.1",
|
|
11
|
+
"overture-schema-system>=0.1.1",
|
|
12
|
+
"tomli>=2.0; python_version < '3.11'",
|
|
13
|
+
"typing-extensions>=4.0",
|
|
14
|
+
]
|
|
15
|
+
description = "Code generator that produces documentation and code from Pydantic models"
|
|
16
|
+
version = "0.1.1.dev0"
|
|
17
|
+
license = "MIT"
|
|
18
|
+
name = "overture-schema-codegen"
|
|
19
|
+
requires-python = ">=3.10"
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
overture-codegen = "overture.schema.codegen.cli:main"
|
|
23
|
+
|
|
24
|
+
[tool.uv.sources.overture-schema-cli]
|
|
25
|
+
workspace = true
|
|
26
|
+
|
|
27
|
+
[tool.uv.sources.overture-schema-common]
|
|
28
|
+
workspace = true
|
|
29
|
+
|
|
30
|
+
[tool.uv.sources.overture-schema-system]
|
|
31
|
+
workspace = true
|
|
32
|
+
|
|
33
|
+
[tool.uv.sources.overture-schema-theme-addresses]
|
|
34
|
+
workspace = true
|
|
35
|
+
|
|
36
|
+
[tool.uv.sources.overture-schema-theme-base]
|
|
37
|
+
workspace = true
|
|
38
|
+
|
|
39
|
+
[tool.uv.sources.overture-schema-theme-buildings]
|
|
40
|
+
workspace = true
|
|
41
|
+
|
|
42
|
+
[tool.uv.sources.overture-schema-theme-divisions]
|
|
43
|
+
workspace = true
|
|
44
|
+
|
|
45
|
+
[tool.uv.sources.overture-schema-theme-places]
|
|
46
|
+
workspace = true
|
|
47
|
+
|
|
48
|
+
[tool.uv.sources.overture-schema-theme-transportation]
|
|
49
|
+
workspace = true
|
|
50
|
+
|
|
51
|
+
[tool.uv.build-backend]
|
|
52
|
+
module-name = "overture.schema.codegen"
|
|
53
|
+
|
|
54
|
+
[dependency-groups]
|
|
55
|
+
test = [
|
|
56
|
+
"overture-schema-theme-addresses",
|
|
57
|
+
"overture-schema-theme-base",
|
|
58
|
+
"overture-schema-theme-buildings",
|
|
59
|
+
"overture-schema-theme-divisions",
|
|
60
|
+
"overture-schema-theme-places",
|
|
61
|
+
"overture-schema-theme-transportation",
|
|
62
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.11.32,<0.13"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
dependencies = [
|
|
7
|
+
"click>=8.1",
|
|
8
|
+
"jinja2>=3.0",
|
|
9
|
+
"overture-schema-cli>=0.1.1",
|
|
10
|
+
"overture-schema-common>=0.1.1",
|
|
11
|
+
"overture-schema-system>=0.1.1",
|
|
12
|
+
"tomli>=2.0; python_version < '3.11'",
|
|
13
|
+
"typing-extensions>=4.0",
|
|
14
|
+
]
|
|
15
|
+
description = "Code generator that produces documentation and code from Pydantic models"
|
|
16
|
+
version = "0.1.1.dev0"
|
|
17
|
+
license = "MIT"
|
|
18
|
+
name = "overture-schema-codegen"
|
|
19
|
+
requires-python = ">=3.10"
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
overture-codegen = "overture.schema.codegen.cli:main"
|
|
23
|
+
|
|
24
|
+
[tool.uv.sources]
|
|
25
|
+
overture-schema-cli = { workspace = true }
|
|
26
|
+
overture-schema-common = { workspace = true }
|
|
27
|
+
overture-schema-system = { workspace = true }
|
|
28
|
+
overture-schema-theme-addresses = { workspace = true }
|
|
29
|
+
overture-schema-theme-base = { workspace = true }
|
|
30
|
+
overture-schema-theme-buildings = { workspace = true }
|
|
31
|
+
overture-schema-theme-divisions = { workspace = true }
|
|
32
|
+
overture-schema-theme-places = { workspace = true }
|
|
33
|
+
overture-schema-theme-transportation = { workspace = true }
|
|
34
|
+
|
|
35
|
+
[dependency-groups]
|
|
36
|
+
test = [
|
|
37
|
+
"overture-schema-theme-addresses",
|
|
38
|
+
"overture-schema-theme-base",
|
|
39
|
+
"overture-schema-theme-buildings",
|
|
40
|
+
"overture-schema-theme-divisions",
|
|
41
|
+
"overture-schema-theme-places",
|
|
42
|
+
"overture-schema-theme-transportation",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[tool.uv.build-backend]
|
|
46
|
+
module-name = "overture.schema.codegen"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Code generator for Overture Schema Pydantic models."""
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""CLI entrypoint for schema code generation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from overture.schema.cli.tag_options import build_selector, tag_selection_options
|
|
11
|
+
from overture.schema.system.discovery import (
|
|
12
|
+
discover_models,
|
|
13
|
+
filter_models,
|
|
14
|
+
split_entry_point,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .extraction.specs import ModelSpec, SupplementarySpec, TypeIdentity
|
|
18
|
+
from .layout.module_layout import (
|
|
19
|
+
OUTPUT_ROOT,
|
|
20
|
+
compute_schema_root,
|
|
21
|
+
entry_point_module,
|
|
22
|
+
)
|
|
23
|
+
from .markdown.pipeline import generate_markdown_pages
|
|
24
|
+
from .pyspark.pipeline import generate_pyspark_modules
|
|
25
|
+
from .spec_discovery import extract_alias_spec, extract_model_spec
|
|
26
|
+
|
|
27
|
+
log = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
__all__ = ["cli"]
|
|
30
|
+
|
|
31
|
+
_OUTPUT_FORMATS = ("markdown", "pyspark")
|
|
32
|
+
|
|
33
|
+
_FEATURE_FRONTMATTER = "---\nsidebar_position: 1\n---\n\n"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _write_output(
|
|
37
|
+
content: str,
|
|
38
|
+
output_dir: Path | None,
|
|
39
|
+
output_path: PurePosixPath,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Write content to a file under output_dir, or stdout."""
|
|
42
|
+
if output_dir:
|
|
43
|
+
file_path = output_dir / output_path
|
|
44
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
file_path.write_text(content)
|
|
46
|
+
else:
|
|
47
|
+
click.echo(content)
|
|
48
|
+
click.echo() # separate entries with a blank line in stdout mode
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@click.group()
|
|
52
|
+
def cli() -> None:
|
|
53
|
+
"""Overture Schema code generator.
|
|
54
|
+
|
|
55
|
+
Generate documentation and code from Pydantic schema models.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@cli.command("list")
|
|
60
|
+
def list_models() -> None:
|
|
61
|
+
"""List all discovered models."""
|
|
62
|
+
models = discover_models()
|
|
63
|
+
# Name every entry from its entry point, not the loaded object: a
|
|
64
|
+
# discriminated union loads as an `Annotated[...]` alias with no
|
|
65
|
+
# `__name__`, so `str(model)` would print the whole type expression.
|
|
66
|
+
names = []
|
|
67
|
+
for key in models:
|
|
68
|
+
_, class_name = split_entry_point(key.entry_point)
|
|
69
|
+
names.append(class_name)
|
|
70
|
+
for name in sorted(names):
|
|
71
|
+
click.echo(name)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@cli.command()
|
|
75
|
+
@click.option(
|
|
76
|
+
"--format",
|
|
77
|
+
"output_format",
|
|
78
|
+
required=True,
|
|
79
|
+
type=click.Choice(_OUTPUT_FORMATS),
|
|
80
|
+
help="Output format",
|
|
81
|
+
)
|
|
82
|
+
@tag_selection_options
|
|
83
|
+
@click.option(
|
|
84
|
+
"--output-dir",
|
|
85
|
+
type=click.Path(path_type=Path),
|
|
86
|
+
default=None,
|
|
87
|
+
help="Write output files directly into this directory (default: stdout). "
|
|
88
|
+
"For pyspark, writes expression modules (*.py). "
|
|
89
|
+
"For markdown, writes theme subdirectories.",
|
|
90
|
+
)
|
|
91
|
+
@click.option(
|
|
92
|
+
"--test-output-dir",
|
|
93
|
+
type=click.Path(path_type=Path),
|
|
94
|
+
default=None,
|
|
95
|
+
help="Write test modules (test_*.py) into this directory (pyspark only).",
|
|
96
|
+
)
|
|
97
|
+
def generate(
|
|
98
|
+
output_format: str,
|
|
99
|
+
tags: tuple[str, ...],
|
|
100
|
+
filters: tuple[str, ...],
|
|
101
|
+
excludes: tuple[str, ...],
|
|
102
|
+
output_dir: Path | None,
|
|
103
|
+
test_output_dir: Path | None,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Generate code/docs from discovered models."""
|
|
106
|
+
if output_format != "pyspark" and test_output_dir is not None:
|
|
107
|
+
raise click.UsageError("--test-output-dir is only valid with --format pyspark")
|
|
108
|
+
|
|
109
|
+
all_models = discover_models()
|
|
110
|
+
|
|
111
|
+
models = filter_models(all_models, build_selector(tags, filters, excludes))
|
|
112
|
+
|
|
113
|
+
if output_dir:
|
|
114
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
|
|
116
|
+
model_specs: list[ModelSpec] = [
|
|
117
|
+
spec
|
|
118
|
+
for key, entry in models.items()
|
|
119
|
+
if (spec := extract_model_spec(key, entry)) is not None
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
if output_format == "pyspark":
|
|
123
|
+
_generate_pyspark(model_specs, output_dir, test_output_dir)
|
|
124
|
+
else:
|
|
125
|
+
# RootModel entry points yield no ModelSpec, so they document as
|
|
126
|
+
# named aliases -- reachable no other way, since a RootModel field
|
|
127
|
+
# unwraps to its bare shape and names no type.
|
|
128
|
+
external_specs: dict[TypeIdentity, SupplementarySpec] = {
|
|
129
|
+
alias.identity: alias
|
|
130
|
+
for entry in models.values()
|
|
131
|
+
if (alias := extract_alias_spec(entry)) is not None
|
|
132
|
+
}
|
|
133
|
+
module_paths = [entry_point_module(k.entry_point) for k in all_models]
|
|
134
|
+
schema_root = compute_schema_root(module_paths)
|
|
135
|
+
_generate_markdown(model_specs, schema_root, output_dir, external_specs)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _generate_markdown(
|
|
139
|
+
model_specs: list[ModelSpec],
|
|
140
|
+
schema_root: str,
|
|
141
|
+
output_dir: Path | None,
|
|
142
|
+
external_specs: Mapping[TypeIdentity, SupplementarySpec],
|
|
143
|
+
) -> None:
|
|
144
|
+
"""Generate markdown with directory layout and placement-aware links."""
|
|
145
|
+
pages = generate_markdown_pages(
|
|
146
|
+
model_specs, schema_root, external_specs=external_specs
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
for page in pages:
|
|
150
|
+
content = (
|
|
151
|
+
f"{_FEATURE_FRONTMATTER}{page.content}" if page.is_model else page.content
|
|
152
|
+
)
|
|
153
|
+
_write_output(content, output_dir, page.path)
|
|
154
|
+
|
|
155
|
+
if output_dir:
|
|
156
|
+
feature_paths = {page.path for page in pages if page.is_model}
|
|
157
|
+
all_paths = {page.path for page in pages}
|
|
158
|
+
_write_category_files(output_dir, all_paths, feature_paths)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _generate_pyspark(
|
|
162
|
+
model_specs: list[ModelSpec],
|
|
163
|
+
output_dir: Path | None,
|
|
164
|
+
test_output_dir: Path | None = None,
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Generate PySpark validation modules.
|
|
167
|
+
|
|
168
|
+
Output is syntactically valid Python; we assume a code formatter runs
|
|
169
|
+
over the written directories afterwards to match existing conventions.
|
|
170
|
+
"""
|
|
171
|
+
modules = generate_pyspark_modules(model_specs)
|
|
172
|
+
for mod in modules.source:
|
|
173
|
+
_write_output(mod.content, output_dir, mod.path)
|
|
174
|
+
if test_output_dir is not None:
|
|
175
|
+
for mod in modules.test:
|
|
176
|
+
_write_output(mod.content, test_output_dir, mod.path)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _ancestor_dirs(paths: set[PurePosixPath]) -> set[PurePosixPath]:
|
|
180
|
+
"""Collect all ancestor directories for a set of file paths."""
|
|
181
|
+
dirs: set[PurePosixPath] = set()
|
|
182
|
+
for path in paths:
|
|
183
|
+
parent = path.parent
|
|
184
|
+
while parent != OUTPUT_ROOT:
|
|
185
|
+
dirs.add(parent)
|
|
186
|
+
parent = parent.parent
|
|
187
|
+
return dirs
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _top_level_positions(
|
|
191
|
+
dirs: set[PurePosixPath],
|
|
192
|
+
feature_paths: set[PurePosixPath],
|
|
193
|
+
) -> dict[PurePosixPath, int]:
|
|
194
|
+
"""Assign sidebar positions: feature dirs first, then non-feature, both alphabetical."""
|
|
195
|
+
feature_dir_names = {p.parts[0] for p in feature_paths}
|
|
196
|
+
top_level = sorted(d for d in dirs if d.parent == OUTPUT_ROOT)
|
|
197
|
+
feature_dirs = [d for d in top_level if d.name in feature_dir_names]
|
|
198
|
+
non_feature_dirs = [d for d in top_level if d.name not in feature_dir_names]
|
|
199
|
+
return {d: i for i, d in enumerate(feature_dirs + non_feature_dirs, start=1)}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _write_category_files(
|
|
203
|
+
output_dir: Path,
|
|
204
|
+
all_paths: set[PurePosixPath],
|
|
205
|
+
feature_paths: set[PurePosixPath],
|
|
206
|
+
) -> None:
|
|
207
|
+
"""Write _category_.json files for Docusaurus sidebar navigation."""
|
|
208
|
+
dirs = _ancestor_dirs(all_paths)
|
|
209
|
+
positions = _top_level_positions(dirs, feature_paths)
|
|
210
|
+
|
|
211
|
+
for dir_path in sorted(dirs):
|
|
212
|
+
label = dir_path.name.replace("_", " ").title()
|
|
213
|
+
category: dict[str, object] = {"label": label}
|
|
214
|
+
if dir_path in positions:
|
|
215
|
+
category["position"] = positions[dir_path]
|
|
216
|
+
|
|
217
|
+
file_path = output_dir / dir_path / "_category_.json"
|
|
218
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
219
|
+
file_path.write_text(json.dumps(category, indent=2) + "\n")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def main() -> None:
|
|
223
|
+
"""Run the CLI entry point."""
|
|
224
|
+
cli()
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
if __name__ == "__main__":
|
|
228
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Docstring extraction and cleaning utilities."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import NewType
|
|
6
|
+
|
|
7
|
+
__all__ = ["clean_docstring", "first_docstring_line", "is_custom_docstring"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Probe auto-generated docstrings so we can distinguish them from explicit ones.
|
|
11
|
+
# Both Enum and NewType generate default docstrings that vary by Python version;
|
|
12
|
+
# capturing at import time adapts automatically if the format changes.
|
|
13
|
+
class _DocstringProbeEnum(Enum):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_ENUM_DEFAULT_DOCSTRING = _DocstringProbeEnum.__doc__
|
|
18
|
+
del _DocstringProbeEnum
|
|
19
|
+
_NewtypeProbe = NewType("_NewtypeProbe", int)
|
|
20
|
+
_NEWTYPE_DEFAULT_DOCSTRING = _NewtypeProbe.__doc__
|
|
21
|
+
del _NewtypeProbe
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def clean_docstring(doc: str | None) -> str | None:
|
|
25
|
+
"""Return cleaned docstring, or None if absent or whitespace-only."""
|
|
26
|
+
if not doc:
|
|
27
|
+
return None
|
|
28
|
+
cleaned = inspect.cleandoc(doc)
|
|
29
|
+
return cleaned or None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def first_docstring_line(doc: str | None) -> str | None:
|
|
33
|
+
"""Return the first line of a docstring, or None if absent."""
|
|
34
|
+
cleaned = clean_docstring(doc)
|
|
35
|
+
if not cleaned:
|
|
36
|
+
return None
|
|
37
|
+
return cleaned.split("\n")[0]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def is_custom_docstring(doc: str | None, inherited_doc: str | None = None) -> bool:
|
|
41
|
+
"""Check if a docstring was explicitly written, not auto-generated or inherited."""
|
|
42
|
+
return bool(doc) and doc not in (
|
|
43
|
+
_ENUM_DEFAULT_DOCSTRING,
|
|
44
|
+
_NEWTYPE_DEFAULT_DOCSTRING,
|
|
45
|
+
inherited_doc,
|
|
46
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Enum extraction."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from .docstring import clean_docstring, is_custom_docstring
|
|
6
|
+
from .specs import EnumMemberSpec, EnumSpec
|
|
7
|
+
|
|
8
|
+
__all__ = ["extract_enum"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def extract_enum(enum_class: type[Enum]) -> EnumSpec:
|
|
12
|
+
"""Extract enum specification from an Enum class.
|
|
13
|
+
|
|
14
|
+
Handles both simple str Enums and DocumentedEnums where members
|
|
15
|
+
have per-value descriptions via the __doc__ attribute.
|
|
16
|
+
"""
|
|
17
|
+
class_doc = enum_class.__doc__
|
|
18
|
+
description = clean_docstring(class_doc) if is_custom_docstring(class_doc) else None
|
|
19
|
+
|
|
20
|
+
members: list[EnumMemberSpec] = []
|
|
21
|
+
for member in enum_class:
|
|
22
|
+
member_doc = getattr(member, "__doc__", None)
|
|
23
|
+
member_description = (
|
|
24
|
+
member_doc if is_custom_docstring(member_doc, class_doc) else None
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
members.append(
|
|
28
|
+
EnumMemberSpec(
|
|
29
|
+
name=member.name,
|
|
30
|
+
value=str(member.value),
|
|
31
|
+
description=member_description,
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return EnumSpec(
|
|
36
|
+
name=enum_class.__name__,
|
|
37
|
+
description=description,
|
|
38
|
+
members=members,
|
|
39
|
+
source_type=enum_class,
|
|
40
|
+
)
|