Source code for scaffold_kit.scaffold
"""Creates project structure from a structured data file.
This module provides the core logic for the scaffolding utility. It can read a
project structure definition from a YAML or JSON file and create the
corresponding folders and files in the current working directory. It handles
common use cases like root-level project folders, nested structures, and
skipping existing files.
Usage:
To run this script, navigate to your project's root directory or parent
directory and execute it as a module:
# Generate from project's root directory:
$ uv run python -m scaffold_kit.scaffold
# Generate project's parent directory:
$ uv run python -m scaffold_kit.scaffold --root
"""
from __future__ import annotations
import os
import json
import argparse
from typing import Any
import yaml
from scaffold_kit.config import (
SCAFFOLD_FILE,
)
[docs]
def read_structure_file(scaffold_file: str) -> dict[str, Any]:
"""Reads a project structure file and returns its content.
This function attempts to load a project structure definition from either
a YAML or JSON file. It prioritizes the file extension provided, but will
raise an error if the file is not found.
Args:
scaffold_file: The name of the file to read (e.g., 'scaffold.yaml').
Returns:
The content of the file as a dictionary.
Raises:
ValueError: If a found file has invalid YAML or JSON syntax.
FileNotFoundError: If the specified `scaffold_file` is not found
in the current working directory.
"""
is_yaml = False
is_json = False
# 1. Check file extension to determine format.
if scaffold_file.endswith((".json")):
is_json = True
else:
is_yaml = True
# 2. Attempt to read and parse the YAML file.
if is_yaml and os.path.exists(scaffold_file):
print(f"Found '{scaffold_file}'. Reading file...")
try:
with open(scaffold_file, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
except yaml.YAMLError as e:
raise ValueError(
f"Error parsing YAML file '{scaffold_file}': {e}"
) from e
# 3. Attempt to read and parse the JSON file.
if is_json and os.path.exists(scaffold_file):
print(f"Found '{scaffold_file}'. Reading file...")
try:
with open(scaffold_file, "r", encoding="utf-8") as f:
return json.load(f)
except json.JSONDecodeError as e:
raise ValueError(
f"Error decoding JSON file '{scaffold_file}': {e}"
) from e
# 4. Raise error if the file was not found.
raise FileNotFoundError(
f"Error: '{scaffold_file}' was not found in the current directory."
)
[docs]
def scaffold_project(
root: bool = False, scaffold_file: str = "scaffold.yaml"
) -> dict[str, Any]:
"""Creates a project structure from a structured data file.
This function reads a project structure definition from a YAML or JSON
file and creates the corresponding folders and files. It supports
creating a top-level root folder and recursively creating nested files
and directories.
Args:
root: If True, creates the top-level folder defined in the structure
file. If False, it scaffolds the contents directly in the current
directory.
scaffold_file: The path to the project structure definition file.
Returns:
The parsed data from the structure file as a dictionary.
Raises:
OSError: If a file or directory cannot be created due to permissions.
ValueError: If a found file is not valid YAML or JSON.
FileNotFoundError: If the specified file is not found.
"""
data: dict[str, Any] = read_structure_file(scaffold_file)
name = data.get("name")
children = data.get("children", [])
# 1. Handle the creation of the top-level root folder.
if root and name:
if not os.path.exists(name):
try:
os.makedirs(name)
print(f"Created folder: {name}")
except OSError as e:
raise OSError(
f"Permission denied: Unable to create folder '{name}'."
) from e
else:
print(f"Folder '{name}' already exists. Skipping.")
base_path = name
else:
base_path = ""
def create_structure(items: list[dict], path: str):
"""Recursively creates folders and files based on the structure.
Args:
items: A list of dictionaries, where each dictionary represents
a folder or file.
path: The base path where the items should be created.
"""
for item in items:
item_name = item.get("name")
item_type = item.get("type", "folder")
# Skip malformed entries without a name.
if item_name is None:
continue
item_path = os.path.join(path, item_name)
# Check if the item already exists to avoid overwriting.
if os.path.exists(item_path):
print(f"Found existing {item_type}: {item_path}")
# Recursively create children for existing folders.
if item_type == "folder" and "children" in item:
create_structure(item["children"], item_path)
continue
# Create the folder or file.
try:
if item_type == "folder":
os.makedirs(item_path)
print(f"Created folder: {item_path}")
if "children" in item:
create_structure(item["children"], item_path)
elif item_type == "file":
with open(item_path, "w", encoding="utf-8") as _:
pass
print(f"Created file: {item_path}")
except OSError as e:
raise OSError(
f"Permission denied: Unable to create '{item_path}'."
) from e
# 2. Start the recursive creation process if children are defined.
if children:
create_structure(children, base_path)
return data
[docs]
def main():
"""Main entry point to run the scaffolding process.
Parses command-line arguments and runs the scaffolding process.
"""
# 1. Create the argument parser.
parser = argparse.ArgumentParser(
description="Scaffold a project from a structured data file."
)
# 2. Add the --root argument.
parser.add_argument(
"-r",
"--root",
action="store_true",
default=False,
help="Create the root folder defined in the structured data file.",
)
args = parser.parse_args()
# 3. Call the main scaffolding function with parsed arguments.
scaffold_project(root=args.root, scaffold_file=SCAFFOLD_FILE)
if __name__ == "__main__":
main()