Option C Tools
Loading...
Searching...
No Matches
new_python_file.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oct/generator/new_python_file.py
4
5"""
6File Generator
7==============
8
9Purpose
10-------
11Create new Option C-compliant Python files with the mandatory 3-line header
12block and a complete module docstring skeleton, ready for immediate development.
13
14Responsibilities
15----------------
16- Accept a target path relative to or absolute from the project root.
17- Write the mandatory 3-line header block (shebang, encoding, file identity).
18- Append the ``from oc_diagnostics import _dbg`` import so the generated file
19 is immediately linter-compliant for ``_dbg`` usage checks.
20- Append the Option C docstring template including the Diagnostics section.
21- Refuse to overwrite existing files unless ``--force`` is explicitly requested.
22
23Diagnostics
24-----------
25Domain: OCT-GENERATOR
26Levels:
27 L2 — lifecycle
28 L3 — semantic details
29 L4 — deep tracing
30
31Contracts
32---------
33- Must not overwrite existing files unless ``force=True``.
34- Generated file must pass the OCT linter header and docstring checks immediately.
35- Target path may be absolute or relative to the project root.
36"""
37
38from pathlib import Path
39import click
40from oct.core.project_root import find_project_root
41
42SHEBANG = "#!/usr/bin/env python3"
43ENCODING = "# -*- coding: utf-8 -*-"
44
45# Import block placed after the header so generated files are immediately
46# linter-compliant for _dbg usage checks (oc_diagnostics is the source of _dbg).
47IMPORT_BLOCK = "from oc_diagnostics import _dbg\n\n"
48
49DOCSTRING_TEMPLATE = '''"""
50Purpose
51-------
52Describe the purpose of this module.
53
54Responsibilities
55----------------
56- List the responsibilities of this module.
57
58Diagnostics
59-----------
60Domain: <DOMAIN>
61Levels:
62 L2 — lifecycle
63 L3 — semantic details
64 L4 — deep tracing
65
66Contracts
67---------
68- State the contracts and guarantees of this module.
69"""
70'''
71
72TEST_DOCSTRING_TEMPLATE = '''"""
73Purpose
74-------
75Unit tests for {module_name}.
76
77Responsibilities
78----------------
79- Verify the public API of {module_name}.
80
81Diagnostics
82-----------
83Domain: TESTS
84Levels:
85 L2 — lifecycle
86 L3 — semantic details
87 L4 — deep tracing
88
89Contracts
90---------
91- Must not modify external state.
92- Must be deterministic and repeatable.
93"""
94'''
95
96TEST_BODY_TEMPLATE = '''
97{import_line}
98
99
100def test_{stem}_placeholder():
101 """Placeholder test — replace with real tests."""
102 assert True
103'''
104
105
106def _make_header(rel_posix: str) -> str:
107 """Build the 4-line Option C header block (shebang, encoding, identity, blank)."""
108 return f"{SHEBANG}\n{ENCODING}\n# {rel_posix}\n\n"
109
110
111def _derive_import_path(rel_posix: str) -> str:
112 """Convert a relative posix path to a dotted Python import path (without .py)."""
113 no_ext = rel_posix.replace(".py", "")
114 return no_ext.replace("/", ".")
115
116
117def _print_content(label: str, content: str) -> None:
118 """Print generated file content with a visual frame."""
119 print(f"\n--- {label} ---")
120 print(content, end="")
121 print("--- end ---")
122
123
125 project_root: Path, path: str, force: bool,
126 dry_run: bool = False, test: bool = False,
127 verbose: bool = False,
128):
129 target = Path(path)
130 if not target.is_absolute():
131 target = project_root / target
132
133 if target.suffix != ".py":
134 raise click.ClickException(
135 f"oct new only creates Python (.py) files. Got: '{target.suffix or '(no extension)'}'"
136 )
137
138 # Ensure the target is inside the project root.
139 try:
140 target.resolve().relative_to(project_root.resolve())
141 except ValueError:
142 raise click.ClickException(
143 f"Target path {target} is outside the project root ({project_root}). "
144 "Use a relative path or a path inside the project."
145 )
146
147 if target.exists() and not force:
148 raise click.ClickException(f"File already exists: {target}")
149
150 # Derive test file path
151 test_target = None
152 if test:
153 test_target = project_root / "tests" / f"test_{target.stem}.py"
154 if test_target.exists() and not force:
155 raise click.ClickException(f"Test file already exists: {test_target}")
156
157 # Build content (needed for both dry-run verbose preview and actual write)
158 rel = target.relative_to(project_root).as_posix()
159 header = _make_header(rel)
160 main_content = header + IMPORT_BLOCK + DOCSTRING_TEMPLATE
161
162 test_content = None
163 if test_target:
164 test_rel = test_target.relative_to(project_root).as_posix()
165 test_header = _make_header(test_rel)
166 import_path = _derive_import_path(rel)
167 test_docstring = TEST_DOCSTRING_TEMPLATE.format(module_name=target.stem)
168 test_body = TEST_BODY_TEMPLATE.format(
169 import_line=f"from {import_path} import *",
170 stem=target.stem,
171 )
172 test_content = test_header + test_docstring + test_body
173
174 if dry_run:
175 if not target.parent.exists():
176 print(f"[DRY RUN] Would create directory: {target.parent}")
177 # Report __init__.py files that would be created
178 current = target.parent
179 while current != project_root and current != current.parent:
180 try:
181 current.relative_to(project_root)
182 except ValueError:
183 break
184 init_file = current / "__init__.py"
185 if not init_file.exists():
186 print(f"[DRY RUN] Would create package marker: {init_file.relative_to(project_root)}")
187 current = current.parent
188 print(f"[DRY RUN] Would write file: {target}")
189 if test_target:
190 print(f"[DRY RUN] Would write test file: {test_target}")
191 if verbose:
192 _print_content(rel, main_content)
193 if test_content:
194 _print_content(test_rel, test_content)
195 return
196
197 target.parent.mkdir(parents=True, exist_ok=True)
198
199 # Ensure __init__.py exists in all new package directories up to project root
200 current = target.parent
201 while current != project_root and current != current.parent:
202 try:
203 current.relative_to(project_root)
204 except ValueError:
205 break
206 init_file = current / "__init__.py"
207 if not init_file.exists():
208 init_file.write_text("", encoding="utf-8")
209 print(f" Created package marker: {init_file.relative_to(project_root)}")
210 current = current.parent
211
212 target.write_text(main_content, encoding="utf-8")
213 print(f"Created Option C-compliant file: {target}")
214 if verbose:
215 _print_content(rel, main_content)
216
217 # Generate companion test file
218 if test_target and test_content:
219 test_target.parent.mkdir(parents=True, exist_ok=True)
220 test_target.write_text(test_content, encoding="utf-8")
221 print(f"Created companion test file: {test_target}")
222 if verbose:
223 _print_content(test_rel, test_content)
create_new_file(Path project_root, str path, bool force, bool dry_run=False, bool test=False, bool verbose=False)
str _make_header(str rel_posix)
str _derive_import_path(str rel_posix)
None _print_content(str label, str content)