Coverage for scripts/init_scaffold.py: 100%

45 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-31 06:18 +0000

1#!/usr/bin/env python3 

2"""Write a rhiza-managed repo's one non-synced file: `.rhiza/template.yml`. 

3 

4Bundled with this plugin so `/rhiza:init` can point a repo at a template without 

5the `rhiza` CLI. That pointer is the only file `/init` writes itself — everything 

6else is another step's job: 

7 

8 project skeleton the skeleton procedure (scripts/init_skeleton.py) 

9 Makefile, CI, docs the template sync, via `/rhiza:update` 

10 license the license procedure (scripts/set_license.py) 

11 Python version the python-version procedure (scripts/set_python_version.py) 

12 README / mkdocs.yml `/rhiza:docs` 

13 first module + test the user's own 

14 

15The file is created **only if absent** — an existing `template.yml` is left 

16untouched (bumping one is `/rhiza:update`'s job). 

17 

18Usage: 

19 uv run --python 3.12 --no-project python \ 

20 scripts/init_scaffold.py [TARGET] \ 

21 [--host github|gitlab] [--template-host github|gitlab] \ 

22 [--language python|go] \ 

23 [--template-repo owner/repo] [--ref TAG] [--json] 

24""" 

25 

26from __future__ import annotations 

27 

28import argparse 

29import json 

30import sys 

31from pathlib import Path 

32from typing import Any 

33 

34DEFAULT_TEMPLATE_REPO = {"python": "jebel-quant/rhiza", "go": "jebel-quant/rhiza-go"} 

35 

36 

37def profile_for_host(host: str) -> str: 

38 """Return the sync profile matching the git hosting platform.""" 

39 return "gitlab-project" if host == "gitlab" else "github-project" 

40 

41 

42def render_template_yml( 

43 repo: str, ref: str, host: str, language: str, template_host: str | None = None 

44) -> str: 

45 """Render `.rhiza/template.yml` (mirrors template.yml.jinja2). 

46 

47 Two independent facts, which must not be conflated: 

48 

49 * *host* — where **this repo** lives. It selects the ``profiles:`` entry, so a 

50 GitLab repo gets ``gitlab-project`` and its CI, not GitHub's. 

51 * *template_host* — where the **template** lives. It becomes ``template-host:``, 

52 which `sync.py` turns into the clone URL. 

53 

54 They are usually different: a GitLab-hosted project following ``jebel-quant/rhiza`` 

55 is on GitLab, but the template is on GitHub. Deriving one from the other emitted 

56 ``template-host: gitlab`` for every GitLab repo, so the first sync tried to clone 

57 the template from gitlab.com and died with "could not read Username". Default 

58 *template_host* to GitHub — where the rhiza templates are — not to *host*. 

59 """ 

60 lines = [f'repository: "{repo}"', f'ref: "{ref}"'] 

61 if (template_host or "github") == "gitlab": 

62 lines.append("template-host: gitlab") 

63 if language != "python": 

64 lines.append(f"language: {language}") 

65 lines += ["", "profiles:", f" - {profile_for_host(host)}", ""] 

66 return "\n".join(lines) 

67 

68 

69def scaffold( 

70 target: Path, 

71 *, 

72 host: str, 

73 language: str, 

74 template_repo: str, 

75 ref: str, 

76 template_host: str | None = None, 

77) -> dict[str, Any]: 

78 """Write `.rhiza/template.yml` if absent; return a summary dict.""" 

79 path = target / ".rhiza" / "template.yml" 

80 created: list[str] = [] 

81 skipped: list[str] = [] 

82 

83 if path.exists(): 

84 skipped.append(".rhiza/template.yml") 

85 else: 

86 path.parent.mkdir(parents=True, exist_ok=True) 

87 path.write_text(render_template_yml(template_repo, ref, host, language, template_host)) 

88 created.append(".rhiza/template.yml") 

89 

90 return { 

91 "target": str(target), 

92 "language": language, 

93 "template_repository": template_repo, 

94 "ref": ref, 

95 "profile": profile_for_host(host), 

96 "template_host": template_host or "github", 

97 "created": created, 

98 "skipped": skipped, 

99 } 

100 

101 

102def main(argv: list[str] | None = None) -> int: 

103 """Entry point: parse args, write the pointer, and return an exit code.""" 

104 parser = argparse.ArgumentParser( 

105 description="Write a rhiza-managed repo's .rhiza/template.yml pointer.", 

106 ) 

107 parser.add_argument( 

108 "target", nargs="?", default=".", help="Repository root (default: current directory)." 

109 ) 

110 parser.add_argument( 

111 "--host", 

112 choices=("github", "gitlab"), 

113 default="github", 

114 help="Where THIS repo lives; selects the profile (and its CI).", 

115 ) 

116 parser.add_argument( 

117 "--template-host", 

118 choices=("github", "gitlab"), 

119 default="github", 

120 help="Where the TEMPLATE lives; sets the clone URL. Usually github, even for " 

121 "a GitLab-hosted repo, because the rhiza templates are on GitHub.", 

122 ) 

123 parser.add_argument( 

124 "--language", choices=("python", "go"), default="python", help="Project language." 

125 ) 

126 parser.add_argument( 

127 "--template-repo", help="Template repository owner/repo (default: by language)." 

128 ) 

129 parser.add_argument("--ref", default="main", help="Template branch/tag (default: main).") 

130 parser.add_argument( 

131 "--json", dest="json_output", action="store_true", help="Emit the summary as JSON." 

132 ) 

133 args = parser.parse_args(argv) 

134 

135 summary = scaffold( 

136 Path(args.target).resolve(), 

137 host=args.host, 

138 language=args.language, 

139 template_repo=args.template_repo or DEFAULT_TEMPLATE_REPO[args.language], 

140 ref=args.ref, 

141 template_host=args.template_host, 

142 ) 

143 

144 if args.json_output: 

145 print(json.dumps(summary, indent=2)) 

146 else: 

147 for path in summary["created"]: 

148 print(f"created {path}") 

149 for path in summary["skipped"]: 

150 print(f"skipped {path} (already exists)", file=sys.stderr) 

151 return 0 

152 

153 

154if __name__ == "__main__": 

155 raise SystemExit(main())