Skip to content

Commit 20355c5

Browse files
committed
Initial commit
1 parent 9429c7b commit 20355c5

2 files changed

Lines changed: 157 additions & 0 deletions

File tree

src/build/build_binaries.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Build dotnet FracturedJson binaries and copy to Python package.""" # noqa: INP001
2+
3+
import json
4+
import shutil
5+
import subprocess
6+
from os import chdir
7+
from pathlib import Path
8+
from sys import exit
9+
10+
ROOT_DIR = Path(__file__).parent.parent.parent.absolute()
11+
BUILD_DIR = ROOT_DIR / "FracturedJson" / "FracturedJsonCli"
12+
SRC_DIR = ROOT_DIR / "src" / "fractured_json"
13+
TEST_DIR = ROOT_DIR / "tests" / "bin"
14+
VERSION_FILE = SRC_DIR / "_version.py"
15+
16+
17+
def build_binaries() -> None:
18+
"""Build the DLL and CLI as a new .NET project."""
19+
chdir(BUILD_DIR)
20+
shutil.rmtree(BUILD_DIR / "bin", ignore_errors=True)
21+
shutil.rmtree(BUILD_DIR / "obj", ignore_errors=True)
22+
23+
cmd = ["dotnet", "publish", "-c", "Release"]
24+
try:
25+
subprocess.run(cmd, check=True, capture_output=True, text=True) # noqa: S603
26+
except subprocess.CalledProcessError as e:
27+
print(e.output)
28+
exit(1)
29+
30+
print("✅ Built CLI project and DLLs")
31+
32+
33+
def parse_assets_json() -> tuple[Path, str]:
34+
"""Parse project.assets.json for FracturedJson version."""
35+
assets_filename = BUILD_DIR / "obj" / "project.assets.json"
36+
if not assets_filename.exists():
37+
msg = f"Run 'dotnet restore' first: {assets_filename}"
38+
raise FileNotFoundError(msg)
39+
40+
assets = json.load(assets_filename.open())
41+
framework = next(iter(assets["projectFileDependencyGroups"].keys()))
42+
library_version = assets["project"]["version"]
43+
44+
print(f"✅ Latest version is {library_version}")
45+
return (BUILD_DIR / "bin" / "Release" / framework / "publish", library_version)
46+
47+
48+
def create_version_file(version: str) -> None:
49+
"""Create _version.pywith latest FracturedJson version."""
50+
with open(VERSION_FILE, "w") as f: # noqa: PTH123
51+
print('__version__ = "5.0.0"', file=f)
52+
print(f"✅ Created _version.py with version {version}")
53+
54+
55+
def copy_binary(bin_dir: Path, file: str, target_dir: Path) -> None:
56+
"""Copy built binaries into Python folders."""
57+
target_dir.mkdir(parents=True, exist_ok=True)
58+
shutil.copy2(bin_dir / file, target_dir / file)
59+
60+
61+
def main() -> None:
62+
"""Build all the .NET dependencies and copy to the Python package."""
63+
build_binaries()
64+
65+
bin_dir, version = parse_assets_json()
66+
create_version_file(version)
67+
copy_binary(bin_dir, "FracturedJson.dll", SRC_DIR)
68+
copy_binary(bin_dir, "Mono.Options.dll", TEST_DIR)
69+
copy_binary(bin_dir, "FracturedJson.dll", TEST_DIR)
70+
copy_binary(bin_dir, "FracturedJsonCli", TEST_DIR)
71+
copy_binary(bin_dir, "FracturedJsonCli.runtimeconfig.json", TEST_DIR)
72+
copy_binary(bin_dir, "FracturedJsonCli.dll", TEST_DIR)
73+
print("✅ Copied binaries to test folder")
74+
75+
76+
if __name__ == "__main__":
77+
main()

src/build/generate_options_desc.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Generate options descriptions from the FracturedJson wiki.""" # noqa: INP001
2+
3+
import re
4+
5+
import requests
6+
from bs4 import BeautifulSoup
7+
8+
WIKI_OPTIONS = "https://github.com/j-brooke/FracturedJson/wiki/Options"
9+
10+
11+
def to_snake_case(name: str, upper: bool = False) -> str:
12+
"""Convert PascalCase or camelCase to SNAKE_CASE or snake_case."""
13+
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
14+
s2 = re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1)
15+
return s2.upper() if upper else s2.lower()
16+
17+
18+
def fetch_options() -> dict[str, str]:
19+
# Fetch the page
20+
resp = requests.get(WIKI_OPTIONS) # noqa: S113
21+
resp.raise_for_status()
22+
23+
# Parse HTML
24+
soup = BeautifulSoup(resp.text, "html.parser")
25+
26+
options: dict[str, str] = {}
27+
option_name_map: dict[str, str] = {}
28+
started = False
29+
30+
# Each option name is in an H2 inside a DIV.markdown-heading
31+
for heading_div in soup.select("div.markdown-heading > h2"):
32+
option_name = heading_div.get_text(strip=True)
33+
34+
# Some H2 tags are misinterpreted before this first option
35+
if not started and option_name != "MaxTotalLineLength":
36+
continue
37+
started = True
38+
39+
# Walk forward to find the first <p> after this heading
40+
desc = None
41+
for sib in heading_div.parent.next_siblings:
42+
# Skip non-tag items (whitespace, etc.)
43+
if not getattr(sib, "name", None):
44+
continue
45+
# Stop if we hit another heading section
46+
if sib.name in {"h1", "h2", "h3"} or (
47+
sib.name == "div" and "markdown-heading" in sib.get("class", [])
48+
):
49+
break
50+
# First paragraph is the description
51+
p = sib.find("p") if sib.name != "p" else sib
52+
if p:
53+
# Preserve spaces around <code> tags
54+
desc = p.get_text(" ", strip=True)
55+
break
56+
57+
if desc:
58+
desc = re.sub(r"\s+\(.*", "", desc)
59+
desc = re.sub(r"\s+", " ", desc)
60+
desc = re.sub(r"The default.*", "", desc)
61+
py_option_name = to_snake_case(option_name, upper=False)
62+
option_name_map[py_option_name] = option_name
63+
options[py_option_name] = desc
64+
65+
# Descriptions may refer to .NET names; convert to Pythonic names
66+
for py_name, desc in options.items():
67+
for py_name_2, dotnet_name in option_name_map.items():
68+
desc = desc.replace(dotnet_name, py_name_2) # noqa: PLW2901
69+
desc = desc.replace("_", "-") # noqa: PLW2901
70+
options[py_name] = desc
71+
return options
72+
73+
74+
if __name__ == "__main__":
75+
options = fetch_options()
76+
with open("src/fractured_json/generated/option_descriptions.py", "w") as f:
77+
f.write("# Auto-generated file; do not edit.\n")
78+
f.write("FLAG_DESCRIPTIONS = {\n")
79+
f.writelines(f' "{name}": "{desc}",\n' for name, desc in sorted(options.items()))
80+
f.write("}\n")

0 commit comments

Comments
 (0)