pc-init 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.
- gpc_init/__init__.py +1 -0
- gpc_init/about.py +3 -0
- gpc_init/cli.py +204 -0
- gpc_init/exceptions.py +49 -0
- gpc_init/fetcher.py +63 -0
- gpc_init/framework/bevy/preset.yaml +13 -0
- gpc_init/framework/django/preset.yaml +8 -0
- gpc_init/framework/react/preset.yaml +15 -0
- gpc_init/lang/common/default.yaml +23 -0
- gpc_init/lang/go/baseline.yaml +5 -0
- gpc_init/lang/js/baseline.yaml +14 -0
- gpc_init/lang/md/baseline.yaml +9 -0
- gpc_init/lang/py/baseline.yaml +38 -0
- gpc_init/lang/ru/baseline.yaml +28 -0
- gpc_init/lang/toml/baseline.yaml +0 -0
- gpc_init/lang/yaml/baseline.yaml +6 -0
- gpc_init/loader.py +93 -0
- gpc_init/merger.py +148 -0
- gpc_init/profiles.py +63 -0
- gpc_init/renderer.py +28 -0
- gpc_init/resolver.py +155 -0
- pc_init-0.1.0.dist-info/METADATA +743 -0
- pc_init-0.1.0.dist-info/RECORD +27 -0
- pc_init-0.1.0.dist-info/WHEEL +5 -0
- pc_init-0.1.0.dist-info/entry_points.txt +2 -0
- pc_init-0.1.0.dist-info/licenses/LICENSE +674 -0
- pc_init-0.1.0.dist-info/top_level.txt +1 -0
gpc_init/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""pc-init: Generate .pre-commit-config.yaml from language and framework presets."""
|
gpc_init/about.py
ADDED
gpc_init/cli.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""pc-init CLI: Generate .pre-commit-config.yaml from language and framework presets."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from gpc_init import fetcher
|
|
9
|
+
from gpc_init.exceptions import (
|
|
10
|
+
PresetFetchError,
|
|
11
|
+
PresetNotFoundError,
|
|
12
|
+
PresetParseError,
|
|
13
|
+
UnsupportedFrameworkError,
|
|
14
|
+
UnsupportedLanguageError,
|
|
15
|
+
)
|
|
16
|
+
from gpc_init.loader import (
|
|
17
|
+
load_common_preset,
|
|
18
|
+
load_framework_preset,
|
|
19
|
+
load_language_preset,
|
|
20
|
+
)
|
|
21
|
+
from gpc_init.merger import merge_presets
|
|
22
|
+
from gpc_init.renderer import render_yaml
|
|
23
|
+
from gpc_init.resolver import (
|
|
24
|
+
deduplicate_preserving_order,
|
|
25
|
+
get_primary_languages_info,
|
|
26
|
+
normalize_framework,
|
|
27
|
+
normalize_lang,
|
|
28
|
+
validate_frameworks,
|
|
29
|
+
validate_langs,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _run(
|
|
34
|
+
langs: list[str],
|
|
35
|
+
frameworks: list[str],
|
|
36
|
+
target: Path,
|
|
37
|
+
base_dir: Path | None,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Validate, load, merge, render, and write the preset config."""
|
|
40
|
+
if base_dir is not None and not (base_dir / "lang").is_dir():
|
|
41
|
+
typer.echo(f"Error: '{base_dir}' must contain a 'lang' subdirectory.", err=True)
|
|
42
|
+
raise typer.Exit(code=1)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
validate_langs(langs, base_dir=base_dir)
|
|
46
|
+
validate_frameworks(frameworks, base_dir=base_dir)
|
|
47
|
+
|
|
48
|
+
common = load_common_preset(base_dir=base_dir)
|
|
49
|
+
lang_presets = [
|
|
50
|
+
load_language_preset(lang_id, base_dir=base_dir) for lang_id in langs
|
|
51
|
+
]
|
|
52
|
+
fw_presets = [
|
|
53
|
+
load_framework_preset(fw_id, base_dir=base_dir) for fw_id in frameworks
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
merged = merge_presets(common, lang_presets, fw_presets)
|
|
57
|
+
content = render_yaml(merged)
|
|
58
|
+
|
|
59
|
+
overwritten = target.exists()
|
|
60
|
+
try:
|
|
61
|
+
target.write_text(content, encoding="utf-8")
|
|
62
|
+
except (PermissionError, OSError) as exc:
|
|
63
|
+
typer.echo(f"Error: cannot write to '{target}': {exc}", err=True)
|
|
64
|
+
raise typer.Exit(code=1) from exc
|
|
65
|
+
|
|
66
|
+
info = get_primary_languages_info(frameworks, fw_presets, langs)
|
|
67
|
+
if info:
|
|
68
|
+
typer.echo(info)
|
|
69
|
+
|
|
70
|
+
lang_str = ", ".join(langs)
|
|
71
|
+
fw_str = (", ".join(frameworks)) if frameworks else "none"
|
|
72
|
+
action = "Overwrote" if overwritten else "Generated"
|
|
73
|
+
typer.echo(
|
|
74
|
+
f"{action} {target} with languages: {lang_str} and frameworks: {fw_str}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
except UnsupportedLanguageError as exc:
|
|
78
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
79
|
+
raise typer.Exit(code=1) from exc
|
|
80
|
+
|
|
81
|
+
except UnsupportedFrameworkError as exc:
|
|
82
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
83
|
+
raise typer.Exit(code=1) from exc
|
|
84
|
+
|
|
85
|
+
except PresetNotFoundError as exc:
|
|
86
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
87
|
+
raise typer.Exit(code=1) from exc
|
|
88
|
+
|
|
89
|
+
except PresetParseError as exc:
|
|
90
|
+
typer.echo(f"Error: failed to parse preset YAML: {exc}", err=True)
|
|
91
|
+
raise typer.Exit(code=1) from exc
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
app = typer.Typer(
|
|
95
|
+
name="pc-init",
|
|
96
|
+
help="Generate a .pre-commit-config.yaml for your project.",
|
|
97
|
+
add_completion=False,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _normalize_langs(raw_langs: list[str]) -> list[str]:
|
|
102
|
+
"""Lowercase, resolve aliases, and deduplicate language values."""
|
|
103
|
+
return deduplicate_preserving_order([normalize_lang(v) for v in raw_langs])
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _normalize_frameworks(raw_frameworks: list[str]) -> list[str]:
|
|
107
|
+
"""Lowercase and deduplicate framework values."""
|
|
108
|
+
return deduplicate_preserving_order(
|
|
109
|
+
[normalize_framework(v) for v in raw_frameworks]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@app.command()
|
|
114
|
+
def main(
|
|
115
|
+
lang: Annotated[
|
|
116
|
+
list[str],
|
|
117
|
+
typer.Option(
|
|
118
|
+
"--lang",
|
|
119
|
+
help="Language preset to include (repeatable). "
|
|
120
|
+
"Run without --lang to see supported values from the active catalog.",
|
|
121
|
+
),
|
|
122
|
+
],
|
|
123
|
+
framework: Annotated[
|
|
124
|
+
list[str] | None,
|
|
125
|
+
typer.Option(
|
|
126
|
+
"--framework",
|
|
127
|
+
help="Framework preset to layer on top of language baselines (repeatable). "
|
|
128
|
+
"Run without --framework to see supported values from the active catalog.",
|
|
129
|
+
),
|
|
130
|
+
] = None,
|
|
131
|
+
force: Annotated[
|
|
132
|
+
bool,
|
|
133
|
+
typer.Option(
|
|
134
|
+
"--force",
|
|
135
|
+
help="Overwrite existing .pre-commit-config.yaml without prompting.",
|
|
136
|
+
),
|
|
137
|
+
] = False,
|
|
138
|
+
output: Annotated[
|
|
139
|
+
str,
|
|
140
|
+
typer.Option(
|
|
141
|
+
"--output",
|
|
142
|
+
help="Output file path. Defaults to .pre-commit-config.yaml.",
|
|
143
|
+
),
|
|
144
|
+
] = ".pre-commit-config.yaml",
|
|
145
|
+
presets: Annotated[
|
|
146
|
+
str | None,
|
|
147
|
+
typer.Option(
|
|
148
|
+
"--presets",
|
|
149
|
+
help=(
|
|
150
|
+
"Preset catalog to use. Accepts a local directory path or a git "
|
|
151
|
+
"repository URL (https://, git@, git://, ssh://). The directory / "
|
|
152
|
+
"repo root must contain lang/ and framework/ subdirectories. "
|
|
153
|
+
"Defaults to the bundled presets."
|
|
154
|
+
),
|
|
155
|
+
),
|
|
156
|
+
] = None,
|
|
157
|
+
) -> None:
|
|
158
|
+
"""
|
|
159
|
+
Generate a .pre-commit-config.yaml from language and optional framework presets.
|
|
160
|
+
|
|
161
|
+
At least one --lang value is required. --framework values are optional.
|
|
162
|
+
Use --force to overwrite an existing config file.
|
|
163
|
+
"""
|
|
164
|
+
raw_frameworks: list[str] = framework or []
|
|
165
|
+
|
|
166
|
+
# Normalize and deduplicate
|
|
167
|
+
langs = _normalize_langs(lang)
|
|
168
|
+
frameworks = _normalize_frameworks(raw_frameworks)
|
|
169
|
+
|
|
170
|
+
# Check existing file before any I/O
|
|
171
|
+
target = Path(output)
|
|
172
|
+
if target.exists() and not force:
|
|
173
|
+
typer.echo(
|
|
174
|
+
f"Error: '{target}' already exists. Use --force to overwrite.", err=True
|
|
175
|
+
)
|
|
176
|
+
raise typer.Exit(code=1)
|
|
177
|
+
|
|
178
|
+
# Resolve preset catalog
|
|
179
|
+
if presets is not None and not fetcher.is_git_url(presets):
|
|
180
|
+
local_dir = Path(presets)
|
|
181
|
+
if not local_dir.is_dir():
|
|
182
|
+
typer.echo(
|
|
183
|
+
f"Error: presets directory '{local_dir}' does not exist.", err=True
|
|
184
|
+
)
|
|
185
|
+
raise typer.Exit(code=1)
|
|
186
|
+
_run(langs, frameworks, target, local_dir)
|
|
187
|
+
elif presets is not None:
|
|
188
|
+
try:
|
|
189
|
+
with fetcher.fetch_preset_repo(presets) as cloned:
|
|
190
|
+
_run(langs, frameworks, target, cloned)
|
|
191
|
+
except PresetFetchError as exc:
|
|
192
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
193
|
+
raise typer.Exit(code=1) from exc
|
|
194
|
+
else:
|
|
195
|
+
_run(langs, frameworks, target, None)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def entry_point() -> None:
|
|
199
|
+
"""Entry point for the pc-init command."""
|
|
200
|
+
app()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__": # pragma: no cover
|
|
204
|
+
app()
|
gpc_init/exceptions.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Custom exceptions for pc-init."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PresetNotFoundError(Exception):
|
|
5
|
+
"""Raised when a preset file is not found on the filesystem."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PresetParseError(Exception):
|
|
9
|
+
"""Raised when a preset file contains invalid YAML or unexpected structure."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UnsupportedLanguageError(Exception):
|
|
13
|
+
"""Raised when a requested language is not in the language catalog."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, lang: str, supported: list[str]) -> None:
|
|
16
|
+
"""Initialize with the unsupported language and supported language list."""
|
|
17
|
+
self.lang = lang
|
|
18
|
+
self.supported = supported
|
|
19
|
+
supported_str = ", ".join(sorted(supported))
|
|
20
|
+
super().__init__(f"Unsupported language '{lang}'. Supported: {supported_str}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class UnsupportedFrameworkError(Exception):
|
|
24
|
+
"""Raised when a requested framework is not in the framework catalog."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, fw: str, supported: list[str]) -> None:
|
|
27
|
+
"""Initialize with the unsupported framework and supported framework list."""
|
|
28
|
+
self.fw = fw
|
|
29
|
+
self.supported = supported
|
|
30
|
+
supported_str = ", ".join(sorted(supported))
|
|
31
|
+
super().__init__(f"Unsupported framework '{fw}'. Supported: {supported_str}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TargetFileExistsError(Exception):
|
|
35
|
+
"""Raised when target file already exists and --force was not provided."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, path: str) -> None:
|
|
38
|
+
"""Initialize with the path that already exists."""
|
|
39
|
+
self.path = path
|
|
40
|
+
super().__init__(f"'{path}' already exists. Use --force to overwrite.")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PresetFetchError(Exception):
|
|
44
|
+
"""Raised when a remote preset repository cannot be fetched."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, url: str, detail: str) -> None:
|
|
47
|
+
"""Initialize with the URL that failed and the reason."""
|
|
48
|
+
self.url = url
|
|
49
|
+
super().__init__(f"Failed to fetch presets from '{url}': {detail}")
|
gpc_init/fetcher.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Fetch preset catalogs from remote git repositories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from gpc_init.exceptions import PresetFetchError
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Generator
|
|
16
|
+
|
|
17
|
+
_GIT_URL_PREFIXES = ("https://", "git@", "git://", "ssh://")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def is_git_url(value: str) -> bool:
|
|
21
|
+
"""Return True if value looks like a git repository URL rather than a local path."""
|
|
22
|
+
if Path(value).exists():
|
|
23
|
+
return False
|
|
24
|
+
return value.startswith(_GIT_URL_PREFIXES) or value.endswith(".git")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@contextmanager
|
|
28
|
+
def fetch_preset_repo(url: str) -> Generator[Path]:
|
|
29
|
+
"""
|
|
30
|
+
Shallow-clone a git repository and yield the path to the clone root.
|
|
31
|
+
|
|
32
|
+
The temporary directory is removed on exit regardless of success or failure.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
url: Git repository URL (https://, git@, git://, or ssh://).
|
|
36
|
+
|
|
37
|
+
Yields:
|
|
38
|
+
Path to the root of the cloned repository.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
PresetFetchError: If git is not installed or the clone fails.
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
git = shutil.which("git")
|
|
45
|
+
if git is None:
|
|
46
|
+
msg = "git is not installed or not on PATH"
|
|
47
|
+
raise PresetFetchError(url, msg)
|
|
48
|
+
|
|
49
|
+
with tempfile.TemporaryDirectory(prefix="gpc-init-presets-") as tmp:
|
|
50
|
+
try:
|
|
51
|
+
subprocess.run( # noqa: S603
|
|
52
|
+
[git, "clone", "--depth=1", "--", url, tmp],
|
|
53
|
+
capture_output=True,
|
|
54
|
+
text=True,
|
|
55
|
+
check=True,
|
|
56
|
+
)
|
|
57
|
+
except subprocess.CalledProcessError as exc:
|
|
58
|
+
detail = (exc.stderr or exc.stdout or "unknown error").strip()
|
|
59
|
+
raise PresetFetchError(url, detail) from exc
|
|
60
|
+
except OSError as exc:
|
|
61
|
+
msg = f"Failed to execute git: {exc}"
|
|
62
|
+
raise PresetFetchError(url, msg) from exc
|
|
63
|
+
yield Path(tmp)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
primary_languages:
|
|
2
|
+
- ru
|
|
3
|
+
repos:
|
|
4
|
+
- repo: https://github.com/nickel-lang/nickel-pre-commit
|
|
5
|
+
rev: v0.1.0
|
|
6
|
+
hooks:
|
|
7
|
+
- id: bevy-lint
|
|
8
|
+
name: bevy-lint
|
|
9
|
+
description: run bevy-lint to check Bevy ECS usage patterns
|
|
10
|
+
entry: bevy-lint
|
|
11
|
+
language: rust
|
|
12
|
+
types:
|
|
13
|
+
- rust
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
primary_languages:
|
|
2
|
+
- js
|
|
3
|
+
repos:
|
|
4
|
+
- repo: local
|
|
5
|
+
hooks:
|
|
6
|
+
- id: eslint-react
|
|
7
|
+
name: eslint (react)
|
|
8
|
+
description: lint JavaScript/React files with eslint and react-hooks plugin
|
|
9
|
+
entry: npx eslint --fix
|
|
10
|
+
files: \.(jsx?|tsx?)$
|
|
11
|
+
language: node
|
|
12
|
+
additional_dependencies:
|
|
13
|
+
- eslint@9.28.0
|
|
14
|
+
- eslint-plugin-react@7.37.5
|
|
15
|
+
- eslint-plugin-react-hooks@5.2.0
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: meta
|
|
3
|
+
hooks:
|
|
4
|
+
- id: check-useless-excludes
|
|
5
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
6
|
+
rev: v6.0.0
|
|
7
|
+
hooks:
|
|
8
|
+
- id: check-added-large-files
|
|
9
|
+
- id: check-docstring-first
|
|
10
|
+
- id: check-json
|
|
11
|
+
- id: check-merge-conflict
|
|
12
|
+
- id: check-toml
|
|
13
|
+
- id: check-vcs-permalinks
|
|
14
|
+
- id: check-xml
|
|
15
|
+
- id: check-yaml
|
|
16
|
+
- id: debug-statements
|
|
17
|
+
- id: end-of-file-fixer
|
|
18
|
+
- id: mixed-line-ending
|
|
19
|
+
- id: name-tests-test
|
|
20
|
+
- id: no-commit-to-branch
|
|
21
|
+
- id: pretty-format-json
|
|
22
|
+
- id: requirements-txt-fixer
|
|
23
|
+
- id: trailing-whitespace
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: local
|
|
3
|
+
hooks:
|
|
4
|
+
- id: prettier
|
|
5
|
+
name: run prettier
|
|
6
|
+
description: format files with prettier
|
|
7
|
+
entry: prettier --write '**/*.js' '**/*.yaml' '**/*.yml'
|
|
8
|
+
files: \.(js|ya?ml)$
|
|
9
|
+
language: node
|
|
10
|
+
additional_dependencies: ['prettier@3.6.2']
|
|
11
|
+
- repo: https://github.com/crate-ci/typos
|
|
12
|
+
rev: v1
|
|
13
|
+
hooks:
|
|
14
|
+
- id: typos
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/MarcoGorelli/absolufy-imports
|
|
3
|
+
rev: v0.3.1
|
|
4
|
+
hooks:
|
|
5
|
+
- id: absolufy-imports
|
|
6
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
7
|
+
rev: v0.15.12
|
|
8
|
+
hooks:
|
|
9
|
+
- id: ruff-check
|
|
10
|
+
- id: ruff-format
|
|
11
|
+
- repo: https://github.com/abravalheri/validate-pyproject
|
|
12
|
+
rev: v0.25
|
|
13
|
+
hooks:
|
|
14
|
+
- id: validate-pyproject
|
|
15
|
+
- repo: https://github.com/kieran-ryan/pyprojectsort
|
|
16
|
+
rev: v0.4.0
|
|
17
|
+
hooks:
|
|
18
|
+
- id: pyprojectsort
|
|
19
|
+
- repo: https://github.com/adamchainz/blacken-docs
|
|
20
|
+
rev: 1.20.0
|
|
21
|
+
hooks:
|
|
22
|
+
- id: blacken-docs
|
|
23
|
+
- repo: https://github.com/crate-ci/typos
|
|
24
|
+
rev: v1
|
|
25
|
+
hooks:
|
|
26
|
+
- id: typos
|
|
27
|
+
- repo: https://github.com/facebook/pyrefly-pre-commit
|
|
28
|
+
rev: v0.42.0
|
|
29
|
+
hooks:
|
|
30
|
+
- id: pyrefly-check
|
|
31
|
+
- repo: https://github.com/astral-sh/ty-pre-commit
|
|
32
|
+
rev: v0.0.51
|
|
33
|
+
hooks:
|
|
34
|
+
- id: ty
|
|
35
|
+
- repo: https://github.com/rohaquinlop/complexipy-pre-commit
|
|
36
|
+
rev: v5.1.0
|
|
37
|
+
hooks:
|
|
38
|
+
- id: complexipy
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/doublify/pre-commit-rust
|
|
3
|
+
rev: v1.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: cargo-check
|
|
6
|
+
args:
|
|
7
|
+
- --workspace
|
|
8
|
+
- id: clippy
|
|
9
|
+
args:
|
|
10
|
+
- --
|
|
11
|
+
- -D
|
|
12
|
+
- warnings
|
|
13
|
+
- id: fmt
|
|
14
|
+
args:
|
|
15
|
+
- --
|
|
16
|
+
- --check
|
|
17
|
+
- repo: https://github.com/nim65s/pre-commit-sort
|
|
18
|
+
rev: v1.0.0
|
|
19
|
+
hooks:
|
|
20
|
+
- id: pre-commit-sort
|
|
21
|
+
- repo: https://github.com/crate-ci/typos
|
|
22
|
+
rev: v1
|
|
23
|
+
hooks:
|
|
24
|
+
- id: typos
|
|
25
|
+
- repo: https://github.com/rvben/rumdl-pre-commit
|
|
26
|
+
rev: v0.2.0
|
|
27
|
+
hooks:
|
|
28
|
+
- id: rumdl-fmt
|
|
File without changes
|
gpc_init/loader.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Load language and framework presets from the filesystem."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from gpc_init.exceptions import PresetNotFoundError, PresetParseError
|
|
9
|
+
|
|
10
|
+
# Base directory containing lang/ and framework/ preset folders.
|
|
11
|
+
# In development the symlinks gpc_init/lang -> ../lang resolve here; when installed
|
|
12
|
+
# from a wheel the real copies are present at the same location.
|
|
13
|
+
_DEFAULT_PRESETS_BASE = Path(__file__).parent
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _resolve_base(base_dir: Path | None) -> Path:
|
|
17
|
+
return base_dir if base_dir is not None else _DEFAULT_PRESETS_BASE
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _load_yaml_file(path: Path) -> dict[str, Any]:
|
|
21
|
+
"""Load and parse a YAML file, raising structured errors on failure."""
|
|
22
|
+
if not path.exists():
|
|
23
|
+
msg = f"Preset file not found: {path}"
|
|
24
|
+
raise PresetNotFoundError(msg)
|
|
25
|
+
try:
|
|
26
|
+
with path.open(encoding="utf-8") as fh:
|
|
27
|
+
data = yaml.safe_load(fh)
|
|
28
|
+
except yaml.YAMLError as exc:
|
|
29
|
+
msg = f"Failed to parse preset YAML '{path}': {exc}"
|
|
30
|
+
raise PresetParseError(msg) from exc
|
|
31
|
+
if data is None:
|
|
32
|
+
data = {}
|
|
33
|
+
if not isinstance(data, dict):
|
|
34
|
+
msg = (
|
|
35
|
+
f"Preset file '{path}' must contain a YAML mapping,"
|
|
36
|
+
f" got {type(data).__name__}"
|
|
37
|
+
)
|
|
38
|
+
raise PresetParseError(msg)
|
|
39
|
+
return data
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_common_preset(base_dir: Path | None = None) -> dict[str, Any]:
|
|
43
|
+
"""
|
|
44
|
+
Load the common baseline preset (lang/common/default.yaml).
|
|
45
|
+
|
|
46
|
+
Returns an empty dict if the file does not exist.
|
|
47
|
+
"""
|
|
48
|
+
path = _resolve_base(base_dir) / "lang" / "common" / "default.yaml"
|
|
49
|
+
if not path.exists():
|
|
50
|
+
return {}
|
|
51
|
+
return _load_yaml_file(path)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_language_preset(lang_id: str, base_dir: Path | None = None) -> dict[str, Any]:
|
|
55
|
+
"""
|
|
56
|
+
Load the baseline preset for a language (lang/<lang_id>/baseline.yaml).
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
lang_id: Canonical language identifier (e.g. 'py', 'js', 'go', 'ru').
|
|
60
|
+
base_dir: Override base directory for presets (used in tests).
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Parsed preset as a dictionary.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
PresetNotFoundError: If the preset file does not exist.
|
|
67
|
+
PresetParseError: If the YAML is invalid or not a mapping.
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
return _load_yaml_file(_resolve_base(base_dir) / "lang" / lang_id / "baseline.yaml")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def load_framework_preset(
|
|
74
|
+
framework_id: str, base_dir: Path | None = None
|
|
75
|
+
) -> dict[str, Any]:
|
|
76
|
+
"""
|
|
77
|
+
Load the preset for a framework (framework/<framework_id>/preset.yaml).
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
framework_id: Canonical framework identifier (e.g. 'react', 'bevy').
|
|
81
|
+
base_dir: Override base directory for presets (used in tests).
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Parsed preset as a dictionary.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
PresetNotFoundError: If the preset file does not exist.
|
|
88
|
+
PresetParseError: If the YAML is invalid or not a mapping.
|
|
89
|
+
|
|
90
|
+
"""
|
|
91
|
+
return _load_yaml_file(
|
|
92
|
+
_resolve_base(base_dir) / "framework" / framework_id / "preset.yaml"
|
|
93
|
+
)
|