spp_template/tools/new_module.py

225 lines
7.6 KiB
Python

import json
import logging
import os
import shutil
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Literal, Any, Callable
sys.path.append(os.path.dirname(__file__))
from common import exec_checked, prompt, prompt_and_validate, prompt_choices, run_script, escape_filename, prompt_yesno
from common.jinja import is_jinja_installed, generate_file, render_string
from new_source import run_new_source
class _TemplateCustomizable: ...
@dataclass
class _TemplateOption:
name: str
message: str
@staticmethod
def from_json(type: str, **kwargs) -> '_TemplateOption':
tp = {
'choices': _TemplateOptionChoices
}[type]
return tp.from_json(**kwargs)
@dataclass
class _TemplateOptionChoices(_TemplateOption):
choices: list[str]
@staticmethod
def from_json(**kwargs) -> '_TemplateOptionChoices':
return _TemplateOptionChoices(**kwargs)
@dataclass
class _TemplateNewSource(_TemplateCustomizable):
header: Literal['public', 'private', 'no']
source: bool
path: str
@staticmethod
def from_json(**kwargs) -> '_TemplateNewSource':
return _TemplateNewSource(**kwargs)
@dataclass
class _TemplateConfig(_TemplateCustomizable):
options: list[_TemplateOption]
new_sources: list[_TemplateNewSource]
@staticmethod
def from_json(data: dict) -> '_TemplateConfig':
return _TemplateConfig(
options=[_TemplateOption.from_json(**ele) for ele in data.get('options', [])],
new_sources=[_TemplateNewSource.from_json(**ele) for ele in data.get('new_sources', [])]
)
_PRIVATE_PATH = Path('private')
_PUBLIC_PATH = Path('public')
_TEMPLATE_BASE_PATH = Path(__file__).parent / 'templates/module'
_logger = logging.getLogger(__name__)
_module_folder_name: str
_module_name: str
_template_folder: Path
_template_config: _TemplateConfig
_template_options: dict[str, Any] = {}
def verify_tools() -> None:
success = True
if not is_jinja_installed():
_logger.error('Python module Jinja2 is not installed.')
success = False
if not success:
raise RuntimeError('one or more required tools could not be found')
def query_common_params() -> None:
global _module_folder_name, _module_name, _template_folder
def _validate_name(name: str) -> bool:
return name.strip() != ''
_module_name = prompt_and_validate('Enter module name.', _validate_name).strip()
_module_folder_name = escape_filename(_module_name.lower())
while True:
print(f'Please enter a folder name for the module. Leave empty for "{_module_folder_name}". Anything but [A-Za-z0-9_] will be replaced with underscores.')
new_name = prompt().strip()
if new_name != '':
_module_folder_name = escape_filename(new_name) # just enforce nice names
module_path = _PRIVATE_PATH / _module_folder_name
if not module_path.exists():
break
print(f'Module folder at {module_path} already exists.')
template_choices = {}
for subpath in sorted(_TEMPLATE_BASE_PATH.iterdir()):
if not subpath.is_dir():
continue
template_choices[subpath.name] = subpath
_template_folder = prompt_choices('Please select a module template.', template_choices)
# sanity check
if not (_template_folder / 'private/SModule.jinja').exists():
raise RuntimeError('module template appears to be invalid')
def load_template_config() -> None:
global _template_config
config_file = _template_folder / 'config.json'
if not config_file.exists():
_template_config = _TemplateConfig(
options=[],
new_sources=[]
)
return
with config_file.open('r') as f:
try:
_template_config = _TemplateConfig.from_json(json.load(f))
except Exception as e:
raise RuntimeError(f'Error while parsing template config: {e}')
def query_template_params() -> None:
def _query_choices(option: _TemplateOptionChoices) -> str:
return prompt_choices(option.message, {ele: ele for ele in option.choices})
_QUERY_FUNCS : dict[type[_TemplateOption], Callable] = {
_TemplateOptionChoices: _query_choices
}
for option in _template_config.options:
_template_options[option.name] = _QUERY_FUNCS[option.__class__](option)
def generate_module() -> None:
# firest prepare the template context
context = {
'name': _module_name,
'target_name': _module_folder_name,
'folder_name': _module_folder_name,
**_template_options
}
# next parse templates in the config
def _replace_options(obj: _TemplateCustomizable) -> None:
def _process_value(value: Any, setter: Optional[Callable[[str], None]]) -> None:
if isinstance(value, str):
setter(render_string(value, context))
elif isinstance(value, list):
for idx in range(len(value)):
def __set(v: str) -> None:
value[idx] = v
_process_value(value[idx], __set)
elif isinstance(value, dict):
for key in list(value.keys()):
def __set(v: str) -> None:
value[key] = v
_process_value(value[key], __set)
elif isinstance(value, _TemplateCustomizable):
_process_value(value.__dict__, setter)
_process_value(obj.__dict__, None)
_replace_options(_template_config)
# then copy the files over
def _copy_template(src: Path, dst: Path) -> None:
dst.mkdir(parents=True, exist_ok=True)
for ele in src.iterdir():
dstele = dst / ele.name
if ele.is_dir():
_copy_template(ele, dstele)
continue
if ele.suffix == '.jinja':
generate_file(str(ele.relative_to(_template_folder)), dstele.with_suffix(''), context, templates_path=_template_folder)
else:
shutil.copyfile(ele, dstele)
template_private = _template_folder / 'private'
template_public = _template_folder / 'public'
_copy_template(template_private, _PRIVATE_PATH / _module_folder_name)
if template_public.exists():
_copy_template(template_public, _PUBLIC_PATH / _module_folder_name)
# and then generate sources
for new_source in _template_config.new_sources:
run_new_source(new_source.header, new_source.source, Path(new_source.path))
def append_module() -> None:
print('Appending module to SConstruct file. You might want to adjust it.')
sconstruct_file = Path('SConstruct')
bak_file = sconstruct_file.with_suffix('.bak')
shutil.copyfile(sconstruct_file, bak_file)
with bak_file.open('r') as fin, sconstruct_file.open('w') as fout:
for line in fin:
if line.strip() == 'env.Finalize()':
fout.write(f'# {_module_name}\n')
fout.write(f"env = env.Module('{_PRIVATE_PATH / _module_folder_name}/SModule')\n")
fout.write('\n\n')
fout.write(line)
def refresh_projects() -> None:
if Path('.idea').exists():
if prompt_yesno('CLion project found. Regenerate it?'):
exec_checked(('scons', '--generate_project=clion'))
if Path('.vscode').exists():
if prompt_yesno('VS Code project found. Regenerate it?'):
exec_checked(('scons', '--generate_project=vscode'))
def script_main() -> None:
verify_tools()
query_common_params()
load_template_config()
query_template_params()
generate_module()
append_module()
refresh_projects()
if __name__ == '__main__':
run_script(script_main)