#!/usr/bin/env python3
"""
Main driver module of the skaff program.
"""
# ------------------------------- MODULE INFO ---------------------------------
__all__ = ["skaff_drive"]
# ------------------------------- MODULE INFO ---------------------------------
# --------------------------------- MODULES -----------------------------------
import collections
import os
import re
import shutil
import subprocess
import tempfile
from datetime import datetime
from distutils import spawn
from skaff.clitools import (
timed_key_get,
ANSIColor
)
from skaff.config import SkaffConfig
# --------------------------------- MODULES -----------------------------------
# -------------------------------- FUNCTIONS ----------------------------------
[docs]def skaff_drive(config: SkaffConfig) -> None:
"""
Creates all the necessary subdirectories in addition to the project root.
"""
if not isinstance(config, SkaffConfig):
raise ValueError("'config' argument must be of 'SkaffConfig' type")
for base_dir in config.directories_get():
os.makedirs(base_dir)
for sub_dir in config.subdirectories_get():
os.makedirs(base_dir + sub_dir)
# Create parent directory if it does not exist
os.makedirs("{0}include{1}{2}".format(base_dir,
os.sep,
os.path.basename(base_dir[:-1])))
_license_sign(base_dir, config)
_conf_doc_prompt(base_dir, config)
def _arguments_check(directory, config):
"""
Performs 3 separate checks for the input 'directory' and 'config':
1. Whether 'directory' actually exist in the physical file system.
2. Whether 'config' is a (sub)class instance of 'SkaffConfig'.
3. Whether 'directory' can be obtained by 'directories_get' member function
call.
"""
if not os.path.isdir(directory):
raise ValueError("'directory' must already exist")
if not isinstance(config, SkaffConfig):
raise ValueError("'config' argument must be of 'SkaffConfig' type")
if directory not in config.directories_get():
raise ValueError(("'directory' argument must appear in the result of "
"'directories_get()' member function invocation"))
def _conf_doc_prompt(directory, config):
"""
Prints interactive prompt related to the current 'directory' if 'quiet' is
False.
Calls '_conf_spawn' and '_doc_create()' with the arguments given
afterwards.
"""
_arguments_check(directory, config)
terminal_info = shutil.get_terminal_size()
hints = list()
hints.append("Upcoming Configuration Editing for {0}{1}{2}".format(
ANSIColor.KHAKI, directory, ANSIColor.RESET))
hints.append("The editing will start after [{0}{1}{2}].".format(
ANSIColor.BLUE, "5 seconds", ANSIColor.RESET))
hints.append("Press [{0}c{1}] to continue the editing.".format(
ANSIColor.PURPLE, ANSIColor.RESET))
hints.append("Press [{0}k{1}] to skip the upcoming directory.".format(
ANSIColor.PURPLE, ANSIColor.RESET))
hints.append("Press [{0}a{1}] to skip all the rest.".format(
ANSIColor.PURPLE, ANSIColor.RESET))
key = str()
quiet = config.quiet_get()
if not quiet:
if "posix" == os.name:
os.system("clear")
elif "nt" == os.name:
os.system("cls")
print("-" * terminal_info.columns + "\n")
for line in hints:
print(line.center(terminal_info.columns))
print("\n" + "-" * terminal_info.columns)
try:
while "c" != key.lower():
key = timed_key_get(5)
if "a" == key.lower() or "k" == key.lower():
config.quiet_set(True)
break
except TimeoutError:
pass
if "posix" == os.name:
os.system("clear")
elif "nt" == os.name:
os.system("cls")
_conf_spawn(directory, config)
_doc_create(directory, config)
# Revert the changes if only the current 'directory' is affected
# by the 'quiet' setting
if "k" == key.lower():
config.quiet_set(False)
def _conf_edit(directory, conf_files):
"""
Edits all the 'conf_files' under 'directory' interactively.
By default the environment variable 'EDITOR' is used; if it is empty,
fall back to either 'vim' or 'vi'.
"""
if not directory or not os.path.isdir(directory):
raise ValueError("'directory' must already exist")
if not directory.endswith(os.sep):
directory += os.sep
if not isinstance(conf_files, collections.Iterable):
raise ValueError("'conf_files' argument must be of iterable type")
elif 0 == len(conf_files):
raise ValueError("'conf_files' argument must not be empty")
# Default to 'vi' or 'vim' if the environment variable is not set.
default_editor = None
editor_candidates = ("vim", "vi", "notepad")
for candidate in editor_candidates:
if spawn.find_executable(candidate):
default_editor = candidate
break
else:
raise RuntimeError("editors not found")
editor = os.environ.get("EDITOR", default_editor)
for conf_file in conf_files:
subprocess.call([editor, directory + conf_file])
def _conf_spawn(directory, config):
"""
Spawns configuration files under the project root directory.
The spawned configuration files in the project root include:
{
".editorconfig", ".gdbinit", ".gitattributes",
".gitignore", ".travis.yml", "CMakeLists.txt"
}
An additional "CMakeLists.txt" will also be spawned in 'src' subdirectory
if it exists.
"""
_arguments_check(directory, config)
language = config.language_get()
quiet = config.quiet_get()
cmake_file = "CMakeLists.txt"
cmake_source_prefix = SkaffConfig.basepath_fetch() +\
"config" + os.sep +\
"template" + os.sep +\
language + os.sep
sample_source_file = "main." + language
shutil.copy(cmake_source_prefix + cmake_file, directory)
if os.path.isdir(directory + "src"):
shutil.copy(cmake_source_prefix + "src" + os.sep + cmake_file,
directory + "src" + os.sep)
shutil.copy(cmake_source_prefix + "src" + os.sep + sample_source_file,
directory + "src" + os.sep)
# Again, "figuring out where the configuration resides" may belong to the
# responsibility of 'SkaffConfig' class; this responsibiltiy will be
# moved to 'SkaffConfig' after "ini-parsing" functionality is implemented.
conf_files = ("editorconfig", "gdbinit", "gitattributes", "gitignore")
conf_source_prefix = SkaffConfig.basepath_fetch() +\
"config" + os.sep +\
"template" + os.sep
conf_target_prefix = directory + "."
travis_file = "travis.yml"
travis_source_file = conf_source_prefix + travis_file
travis_target_file = conf_target_prefix + travis_file
language_header = "language: {0}\n".format(language)
for configuration in conf_files:
shutil.copy(conf_source_prefix + configuration + ".txt",
conf_target_prefix + configuration)
with open(travis_source_file, "r", encoding="utf-8") as travis_source:
travis_text = travis_source.read()
with open(travis_target_file, "w", encoding="utf-8") as travis_target:
travis_target.write(language_header)
travis_target.write(travis_text)
if not quiet:
_conf_edit(directory, [cmake_file])
def _doc_create(directory, config):
"""
Creates 'CHANGELOG.md', 'Doxyfile', and 'README.md' template.
Launches $EDITOR or vim on the 'Doxyfile' upon completion, can be turned
off by setting quiet to True.
"""
_arguments_check(directory, config)
changelog_header = (
"# Change Log\n"
"This document records all notable changes to {0}. \n"
"This project adheres to [Semantic Versioning](http://semver.org/).\n"
"\n## 0.1 (Upcoming)\n"
"* New feature here\n"
).format(directory[:-1].title())
readme_header = (
"\n"
"\n## Overview\n"
"\n## License\n"
).format(directory[:-1], os.sep)
changelog_text = directory + "CHANGELOG.md"
copyright_line = "Copyright © {year} {authors}\n".format(
year=datetime.now().year,
authors=", ".join(config.authors_get())
)
license_text = SkaffConfig.basepath_fetch() +\
"config" + os.sep +\
"license" + os.sep +\
config.license_get() + ".md"
readme_text = directory + "README.md"
with open(license_text, "r", encoding="utf-8") as license_file:
license_markdown = license_file.read()
with open(readme_text, "w", encoding="utf-8") as readme_file:
readme_file.write(readme_header)
readme_file.write(copyright_line)
readme_file.write(license_markdown)
with open(changelog_text, "w", encoding="utf-8") as changelog_file:
changelog_file.write(changelog_header)
_doxyfile_generate(directory, config)
def _doxyfile_attr_match(project_name, line):
"""
Determines whether there is any 'Doxyfile' options available in 'line'.
Return the updated version if 'line' contains options that need to be
changed; otherwise return None.
"""
arguments = (project_name, line)
if not all(argument for argument in arguments):
raise ValueError(("Both 'project_name' and 'line' "
"have to be non-empty 'str' type"))
if not all(isinstance(argument, str) for argument in arguments):
raise ValueError(("Both 'project_name' and 'line' "
"have to be of 'str' type"))
# Gets rid of the trailing separator character
if project_name.endswith(os.sep):
project_name = project_name[:-1]
# Tests whether the length of 'project_name' become 0 after truncation
if not project_name:
raise ValueError("'project_name' cannot be a single slash character")
attr_dict = {"PROJECT_NAME": "\"" + project_name.title() + "\"",
"OUTPUT_DIRECTORY": "." + os.sep + "doc",
"TAB_SIZE": 8,
"EXTRACT_ALL": "YES",
"EXTRACT_STATIC": "YES",
"RECURSIVE": "YES",
"EXCLUDE": "build",
"HAVE_DOT": "YES",
"UML_LOOK": "YES",
"TEMPLATE_RELATIONS": "YES",
"CALL_GRAPH": "YES",
"DOT_IMAGE_FORMAT": "svg",
"INTERACTIVE_SVG": "YES"}
line = line.lstrip()
# If the line is solely composed of whitespace or is a comment
if not line or line.startswith("#"):
return None
for attr in attr_dict:
# '\s' stands for whitespace characters
match = re.match(R"\s*" + attr + R"\s*=", line)
if match:
split_index = match.string.find("=") + 1
return match.string[:split_index] + " " +\
str(attr_dict[attr]) + "\n"
return None
def _doxyfile_generate(directory, config):
"""
Generates or uses existing template 'Doxyfile' within 'directory'.
Launches $EDITOR or vim afterwards if 'quiet' is set to False.
"""
_arguments_check(directory, config)
doxyfile = "Doxyfile"
doxyfile_source_prefix = SkaffConfig.basepath_fetch() +\
"config" + os.sep +\
"template" + os.sep
doxyfile_target_prefix = directory
doxygen_cmd = ["doxygen", "-g", doxyfile_target_prefix + doxyfile]
quiet = config.quiet_get()
if spawn.find_executable("doxygen"):
# Redirects the terminal output of 'doxygen' to null device
with open(os.devnull, "w") as null_device:
subprocess.call(doxygen_cmd, stdout=null_device)
with tempfile.TemporaryFile("w+") as tmp_file:
with open(doxyfile_target_prefix + doxyfile, "r+") as output_file:
for line in output_file:
match = _doxyfile_attr_match(directory, line)
tmp_file.write(line if not match else match)
tmp_file.seek(0)
output_file.seek(0)
output_file.truncate()
shutil.copyfileobj(tmp_file, output_file)
else:
shutil.copy(doxyfile_source_prefix + doxyfile,
doxyfile_target_prefix + doxyfile)
if not quiet:
_conf_edit(directory, [doxyfile])
def _license_sign(directory, config):
"""
Copies the license chosen by authors to the 'directory', signs it
with authors and current year prepended if applicable; 'directory' must
already exist.
Note only licenses in {"bsd2", "bsd3", "mit"} will be signed by names in
authors.
"""
_arguments_check(directory, config)
copyright_line = "Copyright (c) {year}, {authors}\n".format(
year=datetime.now().year,
authors=", ".join(config.authors_get())
)
# Note "figuring out where the source license resides" may belong to the
# responsibility of 'SkaffConfig' class; this responsibiltiy will be
# moved to 'SkaffConfig' after "ini-parsing" functionality is implemented.
license_source = SkaffConfig.basepath_fetch() +\
"config" + os.sep +\
"license" + os.sep +\
config.license_get() + ".txt"
license_target = directory + "LICENSE.txt"
if config.license_get() in frozenset(("bsd2", "bsd3", "mit")):
with open(license_source, "r", encoding="utf-8") as from_file:
vanilla_license_text = from_file.read()
with open(license_target, "w", encoding="utf-8") as to_file:
to_file.write(copyright_line)
to_file.write(vanilla_license_text)
else:
shutil.copy(license_source, license_target)
# -------------------------------- FUNCTIONS ----------------------------------