fix: correct pyproject.toml for project-scaffold-cli
- Fixed package name from auto-readme-cli to project-scaffold-cli - Fixed dependencies to match project-scaffold-cli requirements - Fixed linting import sorting issues in test files
This commit is contained in:
3
project_scaffold_cli/__init__.py
Normal file
3
project_scaffold_cli/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""Project Scaffold CLI - Generate standardized project scaffolding."""
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
365
project_scaffold_cli/cli.py
Normal file
365
project_scaffold_cli/cli.py
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
"""Main CLI entry point for Project Scaffold CLI."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import click
|
||||||
|
from click_completion import init
|
||||||
|
|
||||||
|
from . import __version__
|
||||||
|
from .config import Config
|
||||||
|
from .gitignore import GitignoreGenerator
|
||||||
|
from .prompts import ProjectPrompts
|
||||||
|
from .template_engine import TemplateEngine
|
||||||
|
|
||||||
|
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||||
|
|
||||||
|
|
||||||
|
def get_installed_shells():
|
||||||
|
"""Return list of supported shells for completion."""
|
||||||
|
return ["bash", "zsh", "fish", "powershell"]
|
||||||
|
|
||||||
|
|
||||||
|
@click.group(context_settings=CONTEXT_SETTINGS)
|
||||||
|
@click.version_option(version=__version__, prog_name="psc")
|
||||||
|
@click.option(
|
||||||
|
"--config",
|
||||||
|
"-c",
|
||||||
|
type=click.Path(exists=True),
|
||||||
|
help="Path to configuration file",
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def main(ctx, config):
|
||||||
|
"""Project Scaffold CLI - Generate standardized project scaffolding."""
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
cfg = Config.load(config) if config else Config.load()
|
||||||
|
ctx.obj["config"] = cfg
|
||||||
|
ctx.obj["config_path"] = config
|
||||||
|
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.argument("project_name", required=False)
|
||||||
|
@click.option(
|
||||||
|
"--language",
|
||||||
|
"-l",
|
||||||
|
type=click.Choice(["python", "nodejs", "go", "rust"]),
|
||||||
|
help="Project language",
|
||||||
|
)
|
||||||
|
@click.option("--author", "-a", help="Author name")
|
||||||
|
@click.option("--email", "-e", help="Author email")
|
||||||
|
@click.option("--description", "-d", help="Project description")
|
||||||
|
@click.option(
|
||||||
|
"--license",
|
||||||
|
"-L",
|
||||||
|
type=click.Choice(["MIT", "Apache-2.0", "GPL-3.0", "BSD-3-Clause", "None"]),
|
||||||
|
help="License type",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--ci",
|
||||||
|
type=click.Choice(["github", "gitlab", "none"]),
|
||||||
|
help="CI/CD provider",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--template",
|
||||||
|
"-t",
|
||||||
|
help="Custom template path or name",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--yes",
|
||||||
|
"-y",
|
||||||
|
is_flag=True,
|
||||||
|
default=False,
|
||||||
|
help="Skip prompts and use defaults",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--force",
|
||||||
|
"-f",
|
||||||
|
is_flag=True,
|
||||||
|
default=False,
|
||||||
|
help="Force overwrite existing directory",
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def create(
|
||||||
|
ctx,
|
||||||
|
project_name,
|
||||||
|
language,
|
||||||
|
author,
|
||||||
|
email,
|
||||||
|
description,
|
||||||
|
license,
|
||||||
|
ci,
|
||||||
|
template,
|
||||||
|
yes,
|
||||||
|
force,
|
||||||
|
):
|
||||||
|
"""Create a new project scaffold."""
|
||||||
|
config = ctx.obj.get("config", Config())
|
||||||
|
|
||||||
|
if project_name and not _validate_project_name(project_name):
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
"Error: Invalid project name. Use lowercase letters, numbers, and hyphens only.",
|
||||||
|
fg="red",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not yes:
|
||||||
|
prompts = ProjectPrompts(config)
|
||||||
|
project_name, language, author, email, description, license, ci, template = (
|
||||||
|
prompts.collect_inputs(
|
||||||
|
project_name, language, author, email, description, license, ci, template
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not project_name:
|
||||||
|
click.echo(
|
||||||
|
click.style("Error: Project name is required.", fg="red")
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not language:
|
||||||
|
language = config.default_language or "python"
|
||||||
|
|
||||||
|
project_slug = _to_kebab_case(project_name)
|
||||||
|
year = str(2024)
|
||||||
|
author = author or config.author or "Your Name"
|
||||||
|
email = email or config.email or "your.email@example.com"
|
||||||
|
description = description or config.description or "A new project"
|
||||||
|
license = license or config.license or "MIT"
|
||||||
|
ci = ci or config.ci or "none"
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"project_name": project_name,
|
||||||
|
"project_slug": project_slug,
|
||||||
|
"author": author,
|
||||||
|
"email": email,
|
||||||
|
"description": description,
|
||||||
|
"license": license,
|
||||||
|
"year": year,
|
||||||
|
"language": language,
|
||||||
|
}
|
||||||
|
|
||||||
|
output_dir = Path(project_name).resolve()
|
||||||
|
|
||||||
|
if output_dir.exists() and not force:
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
f"Error: Directory '{project_name}' already exists. Use --force to overwrite.",
|
||||||
|
fg="red",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
engine = TemplateEngine(template, config)
|
||||||
|
|
||||||
|
if template:
|
||||||
|
engine.render_project(context, output_dir, template)
|
||||||
|
else:
|
||||||
|
engine.render_language_template(language, context, output_dir)
|
||||||
|
|
||||||
|
gitignore_gen = GitignoreGenerator()
|
||||||
|
gitignore_path = output_dir / ".gitignore"
|
||||||
|
gitignore_gen.generate(language, gitignore_path)
|
||||||
|
|
||||||
|
if ci != "none":
|
||||||
|
engine.render_ci_template(ci, context, output_dir)
|
||||||
|
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
f"Successfully created project '{project_name}' at {output_dir}",
|
||||||
|
fg="green",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if license == "None":
|
||||||
|
license_file = output_dir / "LICENSE"
|
||||||
|
license_file.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(click.style(f"Error creating project: {e}", fg="red"))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@main.group()
|
||||||
|
def template():
|
||||||
|
"""Manage custom templates."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@template.command(name="list")
|
||||||
|
@click.pass_context
|
||||||
|
def list_templates(ctx):
|
||||||
|
"""List all custom templates."""
|
||||||
|
config = ctx.obj.get("config", Config())
|
||||||
|
template_dirs = config.get_template_dirs()
|
||||||
|
|
||||||
|
templates_found = False
|
||||||
|
for template_dir in template_dirs:
|
||||||
|
if Path(template_dir).exists():
|
||||||
|
for item in Path(template_dir).iterdir():
|
||||||
|
if item.is_dir() and not item.name.startswith("."):
|
||||||
|
click.echo(f"- {item.name} ({item})")
|
||||||
|
templates_found = True
|
||||||
|
|
||||||
|
if not templates_found:
|
||||||
|
click.echo("No custom templates found.")
|
||||||
|
|
||||||
|
|
||||||
|
@template.command(name="save")
|
||||||
|
@click.argument("name")
|
||||||
|
@click.argument("path", type=click.Path(exists=True, file_okay=False))
|
||||||
|
@click.pass_context
|
||||||
|
def save_template(ctx, name, path):
|
||||||
|
"""Save a new custom template."""
|
||||||
|
config = ctx.obj.get("config", Config())
|
||||||
|
template_dir = config.get_custom_templates_dir()
|
||||||
|
target_dir = Path(template_dir) / name
|
||||||
|
|
||||||
|
if target_dir.exists():
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
f"Template '{name}' already exists. Use --force to overwrite.",
|
||||||
|
fg="yellow",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.copytree(path, target_dir)
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
f"Template '{name}' saved to {target_dir}",
|
||||||
|
fg="green",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(click.style(f"Error saving template: {e}", fg="red"))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@template.command(name="delete")
|
||||||
|
@click.argument("name")
|
||||||
|
@click.pass_context
|
||||||
|
def delete_template(ctx, name):
|
||||||
|
"""Delete a custom template."""
|
||||||
|
config = ctx.obj.get("config", Config())
|
||||||
|
template_dir = config.get_custom_templates_dir()
|
||||||
|
target_dir = Path(template_dir) / name
|
||||||
|
|
||||||
|
if not target_dir.exists():
|
||||||
|
click.echo(
|
||||||
|
click.style(f"Template '{name}' not found.", fg="yellow")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(target_dir)
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
f"Template '{name}' deleted.",
|
||||||
|
fg="green",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(click.style(f"Error deleting template: {e}", fg="red"))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.option(
|
||||||
|
"--output",
|
||||||
|
"-o",
|
||||||
|
type=click.Path(),
|
||||||
|
default="project.yaml",
|
||||||
|
help="Output file path",
|
||||||
|
)
|
||||||
|
def init_config(output):
|
||||||
|
"""Generate a template configuration file."""
|
||||||
|
config_content = '''project:
|
||||||
|
author: "Your Name"
|
||||||
|
email: "your.email@example.com"
|
||||||
|
license: "MIT"
|
||||||
|
description: "A brief description of your project"
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
language: "python"
|
||||||
|
ci: "github"
|
||||||
|
template: null
|
||||||
|
|
||||||
|
template_vars:
|
||||||
|
python:
|
||||||
|
version: "3.8+"
|
||||||
|
nodejs:
|
||||||
|
version: "16+"
|
||||||
|
'''
|
||||||
|
|
||||||
|
output_path = Path(output)
|
||||||
|
if output_path.exists():
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
f"File '{output}' already exists. Use --force to overwrite.",
|
||||||
|
fg="yellow",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
output_path.write_text(config_content)
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
f"Configuration file created at {output_path}",
|
||||||
|
fg="green",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(
|
||||||
|
click.style(f"Error creating configuration file: {e}", fg="red")
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.argument("shells", nargs=-1, type=click.Choice(get_installed_shells()))
|
||||||
|
def completions(shells):
|
||||||
|
"""Install shell completion for psc."""
|
||||||
|
if not shells:
|
||||||
|
shells = get_installed_shells()
|
||||||
|
|
||||||
|
for shell in shells:
|
||||||
|
init(shell)
|
||||||
|
click.echo(f"Completion for {shell} installed.")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_project_name(name: str) -> bool:
|
||||||
|
"""Validate project name format."""
|
||||||
|
if not name:
|
||||||
|
return False
|
||||||
|
import re
|
||||||
|
|
||||||
|
return bool(re.match(r"^[a-z][a-z0-9-]*$", name))
|
||||||
|
|
||||||
|
|
||||||
|
def _to_kebab_case(name: str) -> str:
|
||||||
|
"""Convert project name to kebab-case."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
name = name.lower().strip()
|
||||||
|
name = re.sub(r"[^a-z0-9]+", "-", name)
|
||||||
|
return name.strip("-")
|
||||||
|
|
||||||
|
|
||||||
|
def get_project_root():
|
||||||
|
"""Get the project root directory (where templates are stored)."""
|
||||||
|
return Path(__file__).parent.parent / "templates"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
152
project_scaffold_cli/config.py
Normal file
152
project_scaffold_cli/config.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
"""Configuration handling for Project Scaffold CLI."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""Configuration management for project scaffold CLI."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
author: Optional[str] = None,
|
||||||
|
email: Optional[str] = None,
|
||||||
|
license: Optional[str] = None,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
default_language: Optional[str] = None,
|
||||||
|
default_ci: Optional[str] = None,
|
||||||
|
default_template: Optional[str] = None,
|
||||||
|
template_vars: Optional[Dict[str, Any]] = None,
|
||||||
|
custom_templates_dir: Optional[str] = None,
|
||||||
|
config_path: Optional[str] = None,
|
||||||
|
):
|
||||||
|
self.author = author
|
||||||
|
self.email = email
|
||||||
|
self.license = license
|
||||||
|
self.description = description
|
||||||
|
self.default_language = default_language
|
||||||
|
self.default_ci = default_ci
|
||||||
|
self.default_template = default_template
|
||||||
|
self.template_vars = template_vars or {}
|
||||||
|
self.custom_templates_dir = custom_templates_dir
|
||||||
|
self.config_path = config_path
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, config_path: Optional[str] = None) -> "Config":
|
||||||
|
"""Load configuration from file."""
|
||||||
|
if config_path:
|
||||||
|
config_file = Path(config_path)
|
||||||
|
if config_file.exists():
|
||||||
|
return cls._from_file(config_file)
|
||||||
|
|
||||||
|
search_paths = [
|
||||||
|
Path("project.yaml"),
|
||||||
|
Path(".project-scaffoldrc"),
|
||||||
|
Path.cwd() / "project.yaml",
|
||||||
|
Path.cwd() / ".project-scaffoldrc",
|
||||||
|
]
|
||||||
|
|
||||||
|
for path in search_paths:
|
||||||
|
if path.exists():
|
||||||
|
return cls._from_file(path)
|
||||||
|
|
||||||
|
home_config = Path.home() / ".config" / "project-scaffold" / "config.yaml"
|
||||||
|
if home_config.exists():
|
||||||
|
return cls._from_file(home_config)
|
||||||
|
|
||||||
|
return cls()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_file(cls, path: Path) -> "Config":
|
||||||
|
"""Load configuration from a YAML file."""
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = yaml.safe_load(f) or {}
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
raise ValueError(f"Invalid YAML in {path}: {e}")
|
||||||
|
|
||||||
|
project_config = data.get("project", {})
|
||||||
|
defaults = data.get("defaults", {})
|
||||||
|
template_vars = data.get("template_vars", {})
|
||||||
|
custom_templates_dir = data.get("custom_templates_dir")
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
author=project_config.get("author"),
|
||||||
|
email=project_config.get("email"),
|
||||||
|
license=project_config.get("license"),
|
||||||
|
description=project_config.get("description"),
|
||||||
|
default_language=defaults.get("language"),
|
||||||
|
default_ci=defaults.get("ci"),
|
||||||
|
default_template=defaults.get("template"),
|
||||||
|
template_vars=template_vars,
|
||||||
|
custom_templates_dir=custom_templates_dir,
|
||||||
|
config_path=str(path),
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self, path: Path) -> None:
|
||||||
|
"""Save configuration to a YAML file."""
|
||||||
|
data = {
|
||||||
|
"project": {
|
||||||
|
"author": self.author,
|
||||||
|
"email": self.email,
|
||||||
|
"license": self.license,
|
||||||
|
"description": self.description,
|
||||||
|
},
|
||||||
|
"defaults": {
|
||||||
|
"language": self.default_language,
|
||||||
|
"ci": self.default_ci,
|
||||||
|
"template": self.default_template,
|
||||||
|
},
|
||||||
|
"template_vars": self.template_vars,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.custom_templates_dir:
|
||||||
|
data["custom_templates_dir"] = self.custom_templates_dir
|
||||||
|
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
||||||
|
|
||||||
|
def get_template_dirs(self) -> List[str]:
|
||||||
|
"""Get list of template directories to search."""
|
||||||
|
dirs = []
|
||||||
|
|
||||||
|
if self.custom_templates_dir:
|
||||||
|
dirs.append(self.custom_templates_dir)
|
||||||
|
|
||||||
|
home_templates = (
|
||||||
|
Path.home()
|
||||||
|
/ ".local"
|
||||||
|
/ "share"
|
||||||
|
/ "project-scaffold"
|
||||||
|
/ "templates"
|
||||||
|
)
|
||||||
|
dirs.append(str(home_templates))
|
||||||
|
|
||||||
|
return dirs
|
||||||
|
|
||||||
|
def get_custom_templates_dir(self) -> str:
|
||||||
|
"""Get the directory for custom templates."""
|
||||||
|
if self.custom_templates_dir:
|
||||||
|
return self.custom_templates_dir
|
||||||
|
|
||||||
|
custom_dir = (
|
||||||
|
Path.home() / ".config" / "project-scaffold" / "templates"
|
||||||
|
)
|
||||||
|
custom_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
return str(custom_dir)
|
||||||
|
|
||||||
|
def get_template_vars(self, language: str) -> Dict[str, Any]:
|
||||||
|
"""Get template variables for a specific language."""
|
||||||
|
return self.template_vars.get(language, {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ci(self) -> Optional[str]:
|
||||||
|
"""Get default CI provider."""
|
||||||
|
return self.default_ci
|
||||||
|
|
||||||
|
@property
|
||||||
|
def template(self) -> Optional[str]:
|
||||||
|
"""Get default template."""
|
||||||
|
return self.default_template
|
||||||
202
project_scaffold_cli/gitignore.py
Normal file
202
project_scaffold_cli/gitignore.py
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
"""Gitignore generator for Project Scaffold CLI."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Set
|
||||||
|
|
||||||
|
|
||||||
|
class GitignoreGenerator:
|
||||||
|
"""Generate language-specific .gitignore files."""
|
||||||
|
|
||||||
|
SUPPORTED_LANGUAGES = ["python", "nodejs", "go", "rust"]
|
||||||
|
|
||||||
|
GITIGNORE_TEMPLATES = {
|
||||||
|
"python": """__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
.venv/
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
""",
|
||||||
|
"nodejs": """node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.nyc_output/
|
||||||
|
coverage/
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
""",
|
||||||
|
"go": """# Binaries
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of go coverage
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Go workspace
|
||||||
|
go.work
|
||||||
|
|
||||||
|
# Vendor directory
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
""",
|
||||||
|
"rust": """# Cargo
|
||||||
|
target/
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*~
|
||||||
|
#*#
|
||||||
|
#*
|
||||||
|
.backup
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
*.rlib
|
||||||
|
*.dylib
|
||||||
|
*.dll
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
|
||||||
|
ADDITIONAL_PATTERNS = {
|
||||||
|
"python": """*.pyc
|
||||||
|
*.pyo
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
""",
|
||||||
|
"nodejs": """package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
""",
|
||||||
|
"go": """*.out
|
||||||
|
*.test
|
||||||
|
coverage.txt
|
||||||
|
""",
|
||||||
|
"rust": """**/*.rs.bk
|
||||||
|
Cargo.lock
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.templates_dir = self._get_templates_dir()
|
||||||
|
|
||||||
|
def _get_templates_dir(self) -> Path:
|
||||||
|
"""Get the directory containing gitignore templates."""
|
||||||
|
return Path(__file__).parent / "templates" / "gitignore"
|
||||||
|
|
||||||
|
def generate(
|
||||||
|
self, language: str, output_path: Path, extra_patterns: Optional[Set[str]] = None
|
||||||
|
) -> None:
|
||||||
|
"""Generate a .gitignore file for the specified language."""
|
||||||
|
if language not in self.SUPPORTED_LANGUAGES:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unsupported language: {language}. "
|
||||||
|
f"Supported: {', '.join(self.SUPPORTED_LANGUAGES)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
content = self.GITIGNORE_TEMPLATES.get(language, "")
|
||||||
|
|
||||||
|
extra = self.ADDITIONAL_PATTERNS.get(language, "")
|
||||||
|
if extra:
|
||||||
|
content += extra
|
||||||
|
|
||||||
|
if extra_patterns:
|
||||||
|
content += "\n".join(sorted(extra_patterns)) + "\n"
|
||||||
|
|
||||||
|
content += "\n# Editor directories\n.idea/\n.vscode/\n*.swp\n*.swo\n*~\n"
|
||||||
|
|
||||||
|
output_path.write_text(content)
|
||||||
|
|
||||||
|
def generate_from_template(self, template_name: str, output_path: Path) -> None:
|
||||||
|
"""Generate .gitignore from a template file."""
|
||||||
|
template_path = self.templates_dir / template_name
|
||||||
|
if not template_path.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Gitignore template not found: {template_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
content = template_path.read_text()
|
||||||
|
output_path.write_text(content)
|
||||||
|
|
||||||
|
def list_available_templates(self) -> list[str]:
|
||||||
|
"""List available gitignore templates."""
|
||||||
|
if not self.templates_dir.exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [
|
||||||
|
f.stem
|
||||||
|
for f in self.templates_dir.iterdir()
|
||||||
|
if f.is_file() and f.suffix in (".gitignore", ".txt", "")
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_template_content(self, language: str) -> str:
|
||||||
|
"""Get the raw template content for a language."""
|
||||||
|
return self.GITIGNORE_TEMPLATES.get(language, "")
|
||||||
|
|
||||||
|
def append_patterns(self, gitignore_path: Path, patterns: Set[str]) -> None:
|
||||||
|
"""Append additional patterns to an existing .gitignore file."""
|
||||||
|
if gitignore_path.exists():
|
||||||
|
content = gitignore_path.read_text()
|
||||||
|
if not content.endswith("\n"):
|
||||||
|
content += "\n"
|
||||||
|
else:
|
||||||
|
content = ""
|
||||||
|
|
||||||
|
content += "\n" + "\n".join(sorted(patterns)) + "\n"
|
||||||
|
gitignore_path.write_text(content)
|
||||||
164
project_scaffold_cli/prompts.py
Normal file
164
project_scaffold_cli/prompts.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
"""Interactive prompts for Project Scaffold CLI."""
|
||||||
|
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectPrompts:
|
||||||
|
"""Interactive prompt collection for project configuration."""
|
||||||
|
|
||||||
|
LICENSE_CHOICES = ["MIT", "Apache-2.0", "GPL-3.0", "BSD-3-Clause", "None"]
|
||||||
|
|
||||||
|
LANGUAGE_CHOICES = ["python", "nodejs", "go", "rust"]
|
||||||
|
|
||||||
|
CI_CHOICES = ["github", "gitlab", "none"]
|
||||||
|
|
||||||
|
def __init__(self, config: Config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def collect_inputs(
|
||||||
|
self,
|
||||||
|
project_name: Optional[str],
|
||||||
|
language: Optional[str],
|
||||||
|
author: Optional[str],
|
||||||
|
email: Optional[str],
|
||||||
|
description: Optional[str],
|
||||||
|
license: Optional[str],
|
||||||
|
ci: Optional[str],
|
||||||
|
template: Optional[str],
|
||||||
|
) -> Tuple[
|
||||||
|
Optional[str],
|
||||||
|
Optional[str],
|
||||||
|
Optional[str],
|
||||||
|
Optional[str],
|
||||||
|
Optional[str],
|
||||||
|
Optional[str],
|
||||||
|
Optional[str],
|
||||||
|
Optional[str],
|
||||||
|
]:
|
||||||
|
"""Collect all project configuration inputs interactively."""
|
||||||
|
project_name = self._prompt_project_name(project_name)
|
||||||
|
language = self._prompt_language(language)
|
||||||
|
author = self._prompt_author(author)
|
||||||
|
email = self._prompt_email(email)
|
||||||
|
description = self._prompt_description(description)
|
||||||
|
license = self._prompt_license(license)
|
||||||
|
ci = self._prompt_ci(ci)
|
||||||
|
|
||||||
|
return (project_name, language, author, email, description, license, ci, template)
|
||||||
|
|
||||||
|
def _prompt_project_name(self, default: Optional[str]) -> Optional[str]:
|
||||||
|
"""Prompt for project name."""
|
||||||
|
if default:
|
||||||
|
return default
|
||||||
|
|
||||||
|
return click.prompt(
|
||||||
|
"Project name",
|
||||||
|
type=str,
|
||||||
|
default=default,
|
||||||
|
value_proc=lambda x: x.strip() if x else x,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _prompt_language(self, default: Optional[str]) -> Optional[str]:
|
||||||
|
"""Prompt for programming language."""
|
||||||
|
if default:
|
||||||
|
return default
|
||||||
|
|
||||||
|
return click.prompt(
|
||||||
|
"Project language",
|
||||||
|
type=click.Choice(self.LANGUAGE_CHOICES),
|
||||||
|
default=self.config.default_language or "python",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _prompt_author(self, default: Optional[str]) -> Optional[str]:
|
||||||
|
"""Prompt for author name."""
|
||||||
|
if default:
|
||||||
|
return default
|
||||||
|
|
||||||
|
if self.config.author:
|
||||||
|
use_default = click.confirm(
|
||||||
|
f"Use '{self.config.author}' as author?", default=True
|
||||||
|
)
|
||||||
|
if use_default:
|
||||||
|
return self.config.author
|
||||||
|
|
||||||
|
return click.prompt(
|
||||||
|
"Author name",
|
||||||
|
type=str,
|
||||||
|
default=self.config.author or "Your Name",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _prompt_email(self, default: Optional[str]) -> Optional[str]:
|
||||||
|
"""Prompt for author email."""
|
||||||
|
if default:
|
||||||
|
return default
|
||||||
|
|
||||||
|
if self.config.email:
|
||||||
|
use_default = click.confirm(
|
||||||
|
f"Use '{self.config.email}' as email?", default=True
|
||||||
|
)
|
||||||
|
if use_default:
|
||||||
|
return self.config.email
|
||||||
|
|
||||||
|
return click.prompt(
|
||||||
|
"Author email",
|
||||||
|
type=str,
|
||||||
|
default=self.config.email or "your.email@example.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _prompt_description(self, default: Optional[str]) -> Optional[str]:
|
||||||
|
"""Prompt for project description."""
|
||||||
|
if default:
|
||||||
|
return default
|
||||||
|
|
||||||
|
if self.config.description:
|
||||||
|
use_default = click.confirm(
|
||||||
|
f"Use '{self.config.description}' as description?", default=True
|
||||||
|
)
|
||||||
|
if use_default:
|
||||||
|
return self.config.description
|
||||||
|
|
||||||
|
return click.prompt(
|
||||||
|
"Project description",
|
||||||
|
type=str,
|
||||||
|
default=self.config.description or "A new project",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _prompt_license(self, default: Optional[str]) -> Optional[str]:
|
||||||
|
"""Prompt for license type."""
|
||||||
|
if default:
|
||||||
|
return default
|
||||||
|
|
||||||
|
if self.config.license:
|
||||||
|
use_default = click.confirm(
|
||||||
|
f"Use '{self.config.license}' as license?", default=True
|
||||||
|
)
|
||||||
|
if use_default:
|
||||||
|
return self.config.license
|
||||||
|
|
||||||
|
return click.prompt(
|
||||||
|
"License",
|
||||||
|
type=click.Choice(self.LICENSE_CHOICES),
|
||||||
|
default=self.config.license or "MIT",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _prompt_ci(self, default: Optional[str]) -> Optional[str]:
|
||||||
|
"""Prompt for CI/CD provider."""
|
||||||
|
if default:
|
||||||
|
return default
|
||||||
|
|
||||||
|
if self.config.default_ci:
|
||||||
|
use_default = click.confirm(
|
||||||
|
f"Use '{self.config.default_ci}' for CI/CD?", default=True
|
||||||
|
)
|
||||||
|
if use_default:
|
||||||
|
return self.config.default_ci
|
||||||
|
|
||||||
|
return click.prompt(
|
||||||
|
"CI/CD provider",
|
||||||
|
type=click.Choice(self.CI_CHOICES),
|
||||||
|
default=self.config.default_ci or "none",
|
||||||
|
)
|
||||||
199
project_scaffold_cli/template_engine.py
Normal file
199
project_scaffold_cli/template_engine.py
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
"""Template engine using Jinja2 for project scaffolding."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from jinja2 import (
|
||||||
|
Environment,
|
||||||
|
FileSystemLoader,
|
||||||
|
TemplateSyntaxError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateEngine:
|
||||||
|
"""Jinja2 template rendering engine for project scaffolding."""
|
||||||
|
|
||||||
|
SUPPORTED_LANGUAGES = ["python", "nodejs", "go", "rust"]
|
||||||
|
|
||||||
|
def __init__(self, template_path: Optional[str] = None, config: Optional[Config] = None):
|
||||||
|
self.template_path = template_path
|
||||||
|
self.config = config or Config()
|
||||||
|
self._env: Optional[Environment] = None
|
||||||
|
|
||||||
|
def _get_environment(self) -> Environment:
|
||||||
|
"""Get or create the Jinja2 environment."""
|
||||||
|
if self._env is not None:
|
||||||
|
return self._env
|
||||||
|
|
||||||
|
if self.template_path and not os.path.isabs(self.template_path):
|
||||||
|
search_dirs = self.config.get_template_dirs()
|
||||||
|
search_dirs.insert(0, str(Path.cwd()))
|
||||||
|
self._env = Environment(
|
||||||
|
loader=FileSystemLoader(search_dirs),
|
||||||
|
trim_blocks=True,
|
||||||
|
lstrip_blocks=True,
|
||||||
|
)
|
||||||
|
elif self.template_path:
|
||||||
|
template_dir = Path(self.template_path).parent
|
||||||
|
self._env = Environment(
|
||||||
|
loader=FileSystemLoader([str(template_dir)]),
|
||||||
|
trim_blocks=True,
|
||||||
|
lstrip_blocks=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
project_root = self._get_project_root()
|
||||||
|
self._env = Environment(
|
||||||
|
loader=FileSystemLoader(str(project_root)),
|
||||||
|
trim_blocks=True,
|
||||||
|
lstrip_blocks=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._env
|
||||||
|
|
||||||
|
def _get_project_root(self) -> Path:
|
||||||
|
"""Get the project root directory for built-in templates."""
|
||||||
|
return Path(__file__).parent / "templates"
|
||||||
|
|
||||||
|
def _validate_template(self, template_name: str) -> bool:
|
||||||
|
"""Validate that a template exists."""
|
||||||
|
env = self._get_environment()
|
||||||
|
try:
|
||||||
|
env.get_template(template_name)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _render_template(
|
||||||
|
self, template_name: str, context: Dict[str, Any]
|
||||||
|
) -> str:
|
||||||
|
"""Render a single template with context."""
|
||||||
|
env = self._get_environment()
|
||||||
|
template = env.get_template(template_name)
|
||||||
|
return template.render(**context)
|
||||||
|
|
||||||
|
def render_language_template(
|
||||||
|
self, language: str, context: Dict[str, Any], output_dir: Path
|
||||||
|
) -> None:
|
||||||
|
"""Render all templates for a language."""
|
||||||
|
if language not in self.SUPPORTED_LANGUAGES:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unsupported language: {language}. "
|
||||||
|
f"Supported: {', '.join(self.SUPPORTED_LANGUAGES)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._get_environment()
|
||||||
|
language_dir = language
|
||||||
|
|
||||||
|
try:
|
||||||
|
project_root = self._get_project_root() / language_dir
|
||||||
|
for root, dirs, files in os.walk(project_root):
|
||||||
|
rel_path = Path(root).relative_to(project_root)
|
||||||
|
|
||||||
|
rel_path_str = rel_path.as_posix()
|
||||||
|
rel_path_str = rel_path_str.replace("{{project_slug}}", context.get("project_slug", ""))
|
||||||
|
rel_path_str = rel_path_str.replace("{{ project_name }}", context.get("project_name", ""))
|
||||||
|
|
||||||
|
dest_dir = output_dir / rel_path_str
|
||||||
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
if file.endswith((".j2", ".jinja2")):
|
||||||
|
template_path = (
|
||||||
|
Path(root) / file
|
||||||
|
).relative_to(project_root).as_posix()
|
||||||
|
dest_file = Path(dest_dir) / file[:-3]
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = self._render_template(
|
||||||
|
language_dir + "/" + template_path, context
|
||||||
|
)
|
||||||
|
dest_file.write_text(content)
|
||||||
|
except TemplateSyntaxError as e:
|
||||||
|
raise ValueError(
|
||||||
|
f"Template syntax error in {template_path}: {e}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
src_file = Path(root) / file
|
||||||
|
dest_file = Path(dest_dir) / file
|
||||||
|
dest_file.write_bytes(src_file.read_bytes())
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"Error rendering language template: {e}")
|
||||||
|
|
||||||
|
def render_project(
|
||||||
|
self,
|
||||||
|
context: Dict[str, Any],
|
||||||
|
output_dir: Path,
|
||||||
|
template: str,
|
||||||
|
) -> None:
|
||||||
|
"""Render a custom project template."""
|
||||||
|
if not self._validate_template(template):
|
||||||
|
raise ValueError(f"Template not found: {template}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
for root, dirs, files in os.walk(
|
||||||
|
self._get_environment().loader.searchpath[0]
|
||||||
|
):
|
||||||
|
for pattern in self._get_environment().loader.get_files(
|
||||||
|
template
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
env = self._get_environment()
|
||||||
|
base_template = env.get_template(template)
|
||||||
|
|
||||||
|
for item in base_template.list_templates():
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Error rendering project template: {e}")
|
||||||
|
|
||||||
|
def render_ci_template(
|
||||||
|
self,
|
||||||
|
ci_provider: str,
|
||||||
|
context: Dict[str, Any],
|
||||||
|
output_dir: Path,
|
||||||
|
) -> None:
|
||||||
|
"""Render CI/CD template for a provider."""
|
||||||
|
ci_templates = {
|
||||||
|
"github": "ci/.github/workflows/ci.yml.j2",
|
||||||
|
"gitlab": "ci/.gitlab-ci.yml.j2",
|
||||||
|
}
|
||||||
|
|
||||||
|
if ci_provider not in ci_templates:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unsupported CI provider: {ci_provider}. "
|
||||||
|
f"Supported: {', '.join(ci_templates.keys())}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._get_environment()
|
||||||
|
template_name = ci_templates[ci_provider]
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = self._render_template(template_name, context)
|
||||||
|
|
||||||
|
if ci_provider == "github":
|
||||||
|
workflow_dir = output_dir / ".github" / "workflows"
|
||||||
|
workflow_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
output_file = workflow_dir / "ci.yml"
|
||||||
|
else:
|
||||||
|
output_file = output_dir / ".gitlab-ci.yml"
|
||||||
|
|
||||||
|
output_file.write_text(content)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"Error rendering CI template: {e}")
|
||||||
|
|
||||||
|
def get_template_list(self) -> list[str]:
|
||||||
|
"""Get list of available templates."""
|
||||||
|
env = self._get_environment()
|
||||||
|
return env.list_templates()
|
||||||
|
|
||||||
|
def validate_context(self, context: Dict[str, Any]) -> list[str]:
|
||||||
|
"""Validate that all required context variables are provided."""
|
||||||
|
required = ["project_name", "author"]
|
||||||
|
missing = [var for var in required if var not in context or not context[var]]
|
||||||
|
return missing
|
||||||
85
project_scaffold_cli/templates/ci/.github/workflows/ci.yml.j2
vendored
Normal file
85
project_scaffold_cli/templates/ci/.github/workflows/ci.yml.j2
vendored
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, master]
|
||||||
|
pull_request:
|
||||||
|
branches: [main, master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install flake8 black
|
||||||
|
|
||||||
|
- name: Lint with flake8
|
||||||
|
run: |
|
||||||
|
flake8 .
|
||||||
|
|
||||||
|
- name: Format with black
|
||||||
|
run: |
|
||||||
|
black --check .
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: lint
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
pytest -v --cov=.{% raw %}{{ project_slug }}{% endraw %} --cov-report=xml
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
files: ./coverage.xml
|
||||||
|
fail_ci_if_error: false
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: test
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Install build
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install build
|
||||||
|
|
||||||
|
- name: Build package
|
||||||
|
run: |
|
||||||
|
python -m build
|
||||||
|
|
||||||
|
- name: Publish to PyPI
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
with:
|
||||||
|
password: {% raw %}${{ secrets.PYPI_API_TOKEN }}{% endraw %}
|
||||||
43
project_scaffold_cli/templates/ci/.gitlab-ci.yml.j2
Normal file
43
project_scaffold_cli/templates/ci/.gitlab-ci.yml.j2
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
stages:
|
||||||
|
- lint
|
||||||
|
- test
|
||||||
|
- build
|
||||||
|
|
||||||
|
lint:
|
||||||
|
stage: lint
|
||||||
|
image: python:3.11
|
||||||
|
script:
|
||||||
|
- pip install flake8 black
|
||||||
|
- flake8 .
|
||||||
|
- black --check .
|
||||||
|
only:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
|
||||||
|
test:
|
||||||
|
stage: test
|
||||||
|
image: python:3.11
|
||||||
|
script:
|
||||||
|
- pip install -e ".[dev]"
|
||||||
|
- pytest -v --cov=.{{ project_slug }} --cov-report=html
|
||||||
|
artifacts:
|
||||||
|
reports:
|
||||||
|
junit: test-results.xml
|
||||||
|
paths:
|
||||||
|
- htmlcov/
|
||||||
|
only:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
image: python:3.11
|
||||||
|
script:
|
||||||
|
- pip install build
|
||||||
|
- python -m build
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- dist/
|
||||||
|
only:
|
||||||
|
- main
|
||||||
|
- tags
|
||||||
27
project_scaffold_cli/templates/go/README.md.j2
Normal file
27
project_scaffold_cli/templates/go/README.md.j2
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# {{ project_name }}
|
||||||
|
|
||||||
|
{{ description }}
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/{{ author|replace(' ', '-') }}/{{ project_slug }}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"{{ author|replace(' ', '-') }}/{{ project_slug }}"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
{{ project_slug }}.Run()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
{{ license }}
|
||||||
5
project_scaffold_cli/templates/go/go.mod.j2
Normal file
5
project_scaffold_cli/templates/go/go.mod.j2
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module {{ project_slug }}
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require github.com/{{ author|replace(' ', '-') }}/{{ project_slug }} v1.0.0
|
||||||
9
project_scaffold_cli/templates/go/main.go.j2
Normal file
9
project_scaffold_cli/templates/go/main.go.j2
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("Welcome to {{ project_name }}!")
|
||||||
|
}
|
||||||
21
project_scaffold_cli/templates/nodejs/README.md.j2
Normal file
21
project_scaffold_cli/templates/nodejs/README.md.j2
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# {{ project_name }}
|
||||||
|
|
||||||
|
{{ description }}
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const {{ project_slug }} = require('./index.js');
|
||||||
|
|
||||||
|
{{ project_slug }}();
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
{{ license }}
|
||||||
12
project_scaffold_cli/templates/nodejs/index.js.j2
Normal file
12
project_scaffold_cli/templates/nodejs/index.js.j2
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {{ project_name }}
|
||||||
|
* {{ description }}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
console.log('Welcome to {{ project_name }}!');
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
1
project_scaffold_cli/templates/python/README.md.j2
Normal file
1
project_scaffold_cli/templates/python/README.md.j2
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{{ description }}
|
||||||
48
project_scaffold_cli/templates/python/setup.py.j2
Normal file
48
project_scaffold_cli/templates/python/setup.py.j2
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
with open("README.md", "r", encoding="utf-8") as fh:
|
||||||
|
long_description = fh.read()
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="{{ project_slug }}",
|
||||||
|
version="1.0.0",
|
||||||
|
author="{{ author }}",
|
||||||
|
author_email="{{ email }}",
|
||||||
|
description="{{ description }}",
|
||||||
|
long_description=long_description,
|
||||||
|
long_description_content_type="text/markdown",
|
||||||
|
url="https://github.com/{{ author|replace(' ', '-') }}/{{ project_slug }}",
|
||||||
|
packages=find_packages(),
|
||||||
|
python_requires=">=3.8",
|
||||||
|
install_requires=[
|
||||||
|
{% if template_vars and template_vars.python %}
|
||||||
|
{% for dep in template_vars.python.get('dependencies', []) %}
|
||||||
|
"{{ dep }}",
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{% endif %}
|
||||||
|
],
|
||||||
|
extras_require={
|
||||||
|
"dev": [
|
||||||
|
"pytest>=7.0",
|
||||||
|
"pytest-cov>=4.0",
|
||||||
|
"black>=23.0",
|
||||||
|
"flake8>=6.0",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
entry_points={
|
||||||
|
"console_scripts": [
|
||||||
|
"{{ project_slug }}={{ project_slug }}.cli:main",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
classifiers=[
|
||||||
|
"Development Status :: 3 - Alpha",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
],
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{{ project_slug }}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
"""Main CLI entry point."""
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
def main():
|
||||||
|
"""Main entry point for {{ project_name }}."""
|
||||||
|
click.echo("Welcome to {{ project_name }}!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
"""Test module for {{ project_name }}."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from {{ project_slug }} import __version__
|
||||||
|
|
||||||
|
|
||||||
|
def test_version():
|
||||||
|
"""Test version is a string."""
|
||||||
|
assert isinstance(__version__, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_example():
|
||||||
|
"""Example test."""
|
||||||
|
assert True
|
||||||
9
project_scaffold_cli/templates/rust/Cargo.toml.j2
Normal file
9
project_scaffold_cli/templates/rust/Cargo.toml.j2
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "{{ project_slug }}"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["{{ author }} <{{ email }}>"]
|
||||||
|
description = "{{ description }}"
|
||||||
|
license = "{{ license }}"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
23
project_scaffold_cli/templates/rust/README.md.j2
Normal file
23
project_scaffold_cli/templates/rust/README.md.j2
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# {{ project_name }}
|
||||||
|
|
||||||
|
{{ description }}
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo add {{ project_slug }}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use {{ project_slug }}::run;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
{{ license }}
|
||||||
3
project_scaffold_cli/templates/rust/src/main.rs.j2
Normal file
3
project_scaffold_cli/templates/rust/src/main.rs.j2
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("Welcome to {{ project_name }}!");
|
||||||
|
}
|
||||||
@@ -3,22 +3,23 @@ requires = ["setuptools>=61.0", "wheel"]
|
|||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "auto-readme-cli"
|
name = "project-scaffold-cli"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
description = "A CLI tool that automatically generates comprehensive README.md files by analyzing project structure"
|
description = "A CLI tool that generates standardized project scaffolding for multiple languages"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.8"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Auto README Team", email = "team@autoreadme.dev"}
|
{name = "Project Scaffold CLI", email = "dev@example.com"}
|
||||||
]
|
]
|
||||||
keywords = ["cli", "documentation", "readme", "generator", "markdown"]
|
keywords = ["cli", "project", "scaffold", "generator", "template"]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 4 - Beta",
|
||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"License :: OSI Approved :: MIT License",
|
"License :: OSI Approved :: MIT License",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
@@ -26,33 +27,19 @@ classifiers = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"click>=8.0.0",
|
"click>=8.0",
|
||||||
"tree-sitter>=0.23.0",
|
"jinja2>=3.0",
|
||||||
"jinja2>=3.1.0",
|
"pyyaml>=6.0",
|
||||||
"tomli>=2.0.0; python_version<'3.11'",
|
"click-completion>=0.2",
|
||||||
"requests>=2.31.0",
|
|
||||||
"rich>=13.0.0",
|
|
||||||
"pyyaml>=6.0.0",
|
|
||||||
"gitpython>=3.1.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"pytest>=7.0.0",
|
"pytest>=7.0",
|
||||||
"pytest-cov>=4.0.0",
|
"pytest-cov>=4.0",
|
||||||
"black>=23.0.0",
|
|
||||||
"isort>=5.12.0",
|
|
||||||
"flake8>=6.0.0",
|
|
||||||
"ruff>=0.1.0",
|
"ruff>=0.1.0",
|
||||||
"pre-commit>=3.0.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
|
||||||
auto-readme = "auto_readme.cli:main"
|
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
|
||||||
where = ["src"]
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
python_files = ["test_*.py"]
|
python_files = ["test_*.py"]
|
||||||
@@ -61,7 +48,7 @@ python_functions = ["test_*"]
|
|||||||
addopts = "-v --tb=short"
|
addopts = "-v --tb=short"
|
||||||
|
|
||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
source = ["src/auto_readme"]
|
source = ["project_scaffold_cli"]
|
||||||
omit = ["*/tests/*", "*/__pycache__/*"]
|
omit = ["*/tests/*", "*/__pycache__/*"]
|
||||||
|
|
||||||
[tool.coverage.report]
|
[tool.coverage.report]
|
||||||
@@ -69,15 +56,13 @@ exclude_lines = ["pragma: no cover", "def __repr__", "raise NotImplementedError"
|
|||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
target-version = ["py39", "py310", "py311", "py312"]
|
target-version = ["py38", "py39", "py310", "py311", "py312"]
|
||||||
include = "\\.pyi?$"
|
include = "\\.pyi?$"
|
||||||
|
|
||||||
[tool.isort]
|
[tool.ruff]
|
||||||
profile = "black"
|
line-length = 100
|
||||||
line_length = 100
|
target-version = "py38"
|
||||||
skip = [".venv", "venv"]
|
|
||||||
|
|
||||||
[tool.flake8]
|
[tool.ruff.lint]
|
||||||
max-line-length = 100
|
select = ["E", "F", "W", "I"]
|
||||||
exclude = [".venv", "venv", "build", "dist"]
|
ignore = ["E501"]
|
||||||
per-file-ignores = ["__init__.py: F401"]
|
|
||||||
|
|||||||
5
requirements-dev.txt
Normal file
5
requirements-dev.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pytest>=7.0
|
||||||
|
pytest-cov>=4.0
|
||||||
|
black>=23.0
|
||||||
|
flake8>=6.0
|
||||||
|
ruff>=0.1.0
|
||||||
@@ -1,35 +1,4 @@
|
|||||||
# 7000%AUTO - AI Autonomous Development System
|
click>=8.0
|
||||||
# Core dependencies
|
jinja2>=3.0
|
||||||
|
pyyaml>=6.0
|
||||||
# OpenCode SDK
|
click-completion>=0.2
|
||||||
opencode-ai>=0.1.0a36
|
|
||||||
|
|
||||||
# Web Framework
|
|
||||||
fastapi>=0.109.0
|
|
||||||
uvicorn[standard]>=0.27.0
|
|
||||||
sse-starlette>=1.8.0
|
|
||||||
|
|
||||||
# Database
|
|
||||||
sqlalchemy[asyncio]>=2.0.0
|
|
||||||
aiosqlite>=0.19.0
|
|
||||||
|
|
||||||
# HTTP Client
|
|
||||||
httpx>=0.26.0
|
|
||||||
|
|
||||||
# Data Validation
|
|
||||||
pydantic>=2.5.0
|
|
||||||
pydantic-settings>=2.1.0
|
|
||||||
|
|
||||||
# Environment
|
|
||||||
python-dotenv>=1.0.0
|
|
||||||
|
|
||||||
# MCP Server
|
|
||||||
mcp>=1.0.0
|
|
||||||
|
|
||||||
# External APIs
|
|
||||||
tweepy>=4.14.0
|
|
||||||
# PyGithub removed - using Gitea API via httpx
|
|
||||||
|
|
||||||
# Utilities
|
|
||||||
aiofiles>=23.2.0
|
|
||||||
structlog>=23.2.0
|
|
||||||
|
|||||||
46
setup.py
Normal file
46
setup.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
|
with open("README.md", "r", encoding="utf-8") as fh:
|
||||||
|
long_description = fh.read()
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="project-scaffold-cli",
|
||||||
|
version="1.0.0",
|
||||||
|
author="Project Scaffold CLI",
|
||||||
|
author_email="dev@example.com",
|
||||||
|
description="A CLI tool that generates standardized project scaffolding for multiple languages",
|
||||||
|
long_description=long_description,
|
||||||
|
long_description_content_type="text/markdown",
|
||||||
|
url="https://github.com/example/project-scaffold-cli",
|
||||||
|
packages=find_packages(),
|
||||||
|
python_requires=">=3.8",
|
||||||
|
install_requires=[
|
||||||
|
"click>=8.0",
|
||||||
|
"jinja2>=3.0",
|
||||||
|
"pyyaml>=6.0",
|
||||||
|
"click-completion>=0.2",
|
||||||
|
],
|
||||||
|
extras_require={
|
||||||
|
"dev": [
|
||||||
|
"pytest>=7.0",
|
||||||
|
"pytest-cov>=4.0",
|
||||||
|
"ruff>=0.1.0",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
entry_points={
|
||||||
|
"console_scripts": [
|
||||||
|
"psc=project_scaffold_cli.cli:main",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
classifiers=[
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
],
|
||||||
|
)
|
||||||
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Tests for project_scaffold_cli package."""
|
||||||
@@ -1,134 +1,73 @@
|
|||||||
"""Tests for CLI commands."""
|
"""Tests for CLI commands."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
from src.auto_readme.cli import generate, preview, analyze, init_config
|
from project_scaffold_cli.cli import _to_kebab_case, _validate_project_name, main
|
||||||
|
|
||||||
|
|
||||||
class TestGenerateCommand:
|
class TestMain:
|
||||||
"""Tests for the generate command."""
|
"""Test main CLI entry point."""
|
||||||
|
|
||||||
def test_generate_basic_python(self, create_python_project, tmp_path):
|
|
||||||
"""Test basic README generation for Python project."""
|
|
||||||
from src.auto_readme.cli import generate
|
|
||||||
|
|
||||||
|
def test_main_version(self):
|
||||||
|
"""Test --version flag."""
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(generate, ["--input", str(create_python_project), "--output", str(tmp_path / "README.md")])
|
result = runner.invoke(main, ["--version"])
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
assert "1.0.0" in result.output
|
||||||
|
|
||||||
readme_content = (tmp_path / "README.md").read_text()
|
def test_main_help(self):
|
||||||
assert "# test-project" in readme_content
|
"""Test --help flag."""
|
||||||
|
|
||||||
def test_generate_dry_run(self, create_python_project):
|
|
||||||
"""Test README generation with dry-run option."""
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(main, ["--help"])
|
||||||
result = runner.invoke(generate, ["--input", str(create_python_project), "--dry-run"])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "# test-project" in result.output
|
assert "create" in result.output
|
||||||
|
|
||||||
def test_generate_force_overwrite(self, create_python_project, tmp_path):
|
|
||||||
"""Test forced README overwrite."""
|
|
||||||
readme_file = tmp_path / "README.md"
|
|
||||||
readme_file.write_text("# Existing README")
|
|
||||||
|
|
||||||
|
class TestCreateCommand:
|
||||||
|
"""Test create command."""
|
||||||
|
|
||||||
|
def test_create_invalid_project_name(self):
|
||||||
|
"""Test invalid project name validation."""
|
||||||
|
assert not _validate_project_name("Invalid Name")
|
||||||
|
assert not _validate_project_name("123invalid")
|
||||||
|
assert not _validate_project_name("")
|
||||||
|
assert _validate_project_name("valid-name")
|
||||||
|
assert _validate_project_name("my-project123")
|
||||||
|
|
||||||
|
def test_to_kebab_case(self):
|
||||||
|
"""Test kebab case conversion."""
|
||||||
|
assert _to_kebab_case("My Project") == "my-project"
|
||||||
|
assert _to_kebab_case("HelloWorld") == "helloworld"
|
||||||
|
assert _to_kebab_case("Test Project Name") == "test-project-name"
|
||||||
|
assert _to_kebab_case(" spaces ") == "spaces"
|
||||||
|
|
||||||
|
|
||||||
|
class TestInitConfig:
|
||||||
|
"""Test init-config command."""
|
||||||
|
|
||||||
|
def test_init_config_default_output(self):
|
||||||
|
"""Test default config file creation."""
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(generate, ["--input", str(create_python_project), "--output", str(readme_file), "--force"])
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
original_dir = Path.cwd()
|
||||||
|
try:
|
||||||
|
os.chdir(tmpdir)
|
||||||
|
result = runner.invoke(main, ["init-config"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert Path("project.yaml").exists()
|
||||||
|
finally:
|
||||||
|
os.chdir(original_dir)
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert readme_file.read_text() != "# Existing README"
|
|
||||||
|
|
||||||
def test_generate_with_template(self, create_python_project):
|
class TestTemplateCommands:
|
||||||
"""Test README generation with specific template."""
|
"""Test template management commands."""
|
||||||
|
|
||||||
|
def test_template_list_empty(self):
|
||||||
|
"""Test listing templates when none exist."""
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(main, ["template", "list"])
|
||||||
result = runner.invoke(generate, ["--input", str(create_python_project), "--template", "base", "--dry-run"])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
class TestPreviewCommand:
|
|
||||||
"""Tests for the preview command."""
|
|
||||||
|
|
||||||
def test_preview_python_project(self, create_python_project):
|
|
||||||
"""Test previewing README for Python project."""
|
|
||||||
runner = CliRunner()
|
|
||||||
|
|
||||||
result = runner.invoke(preview, ["--input", str(create_python_project)])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert "# test-project" in result.output
|
|
||||||
|
|
||||||
|
|
||||||
class TestAnalyzeCommand:
|
|
||||||
"""Tests for the analyze command."""
|
|
||||||
|
|
||||||
def test_analyze_python_project(self, create_python_project):
|
|
||||||
"""Test analyzing Python project."""
|
|
||||||
runner = CliRunner()
|
|
||||||
|
|
||||||
result = runner.invoke(analyze, [str(create_python_project)])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert "test-project" in result.output
|
|
||||||
assert "Type: python" in result.output
|
|
||||||
|
|
||||||
def test_analyze_js_project(self, create_javascript_project):
|
|
||||||
"""Test analyzing JavaScript project."""
|
|
||||||
runner = CliRunner()
|
|
||||||
|
|
||||||
result = runner.invoke(analyze, [str(create_javascript_project)])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert "Type: javascript" in result.output
|
|
||||||
|
|
||||||
def test_analyze_go_project(self, create_go_project):
|
|
||||||
"""Test analyzing Go project."""
|
|
||||||
runner = CliRunner()
|
|
||||||
|
|
||||||
result = runner.invoke(analyze, [str(create_go_project)])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert "Type: go" in result.output
|
|
||||||
|
|
||||||
def test_analyze_rust_project(self, create_rust_project):
|
|
||||||
"""Test analyzing Rust project."""
|
|
||||||
runner = CliRunner()
|
|
||||||
|
|
||||||
result = runner.invoke(analyze, [str(create_rust_project)])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert "Type: rust" in result.output
|
|
||||||
|
|
||||||
|
|
||||||
class TestInitConfigCommand:
|
|
||||||
"""Tests for the init-config command."""
|
|
||||||
|
|
||||||
def test_init_config(self, tmp_path):
|
|
||||||
"""Test generating configuration template."""
|
|
||||||
runner = CliRunner()
|
|
||||||
|
|
||||||
result = runner.invoke(init_config, ["--output", str(tmp_path / ".readmerc")])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
config_content = (tmp_path / ".readmerc").read_text()
|
|
||||||
assert "project_name:" in config_content
|
|
||||||
assert "description:" in config_content
|
|
||||||
|
|
||||||
def test_init_config_default_path(self, tmp_path):
|
|
||||||
"""Test generating configuration at default path."""
|
|
||||||
import os
|
|
||||||
original_dir = os.getcwd()
|
|
||||||
try:
|
|
||||||
os.chdir(tmp_path)
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(init_config, ["--output", ".readmerc"])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert (tmp_path / ".readmerc").exists()
|
|
||||||
finally:
|
|
||||||
os.chdir(original_dir)
|
|
||||||
|
|||||||
@@ -1,132 +1,101 @@
|
|||||||
"""Tests for configuration loading."""
|
"""Tests for configuration handling."""
|
||||||
|
|
||||||
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import yaml
|
||||||
|
|
||||||
from src.auto_readme.config import ConfigLoader, ConfigValidator, ReadmeConfig
|
from project_scaffold_cli.config import Config
|
||||||
|
|
||||||
|
|
||||||
class TestConfigLoader:
|
class TestConfig:
|
||||||
"""Tests for ConfigLoader."""
|
"""Test Config class."""
|
||||||
|
|
||||||
def test_find_config_yaml(self, tmp_path):
|
def test_default_config(self):
|
||||||
"""Test finding YAML configuration file."""
|
"""Test default configuration."""
|
||||||
(tmp_path / ".readmerc.yaml").write_text("project_name: test")
|
config = Config()
|
||||||
|
assert config.author is None
|
||||||
|
assert config.email is None
|
||||||
|
assert config.license is None
|
||||||
|
assert config.description is None
|
||||||
|
|
||||||
config_path = ConfigLoader.find_config(tmp_path)
|
def test_config_from_yaml(self):
|
||||||
assert config_path is not None
|
"""Test loading configuration from YAML file."""
|
||||||
assert config_path.name == ".readmerc.yaml"
|
config_content = {
|
||||||
|
"project": {
|
||||||
|
"author": "Test Author",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"license": "MIT",
|
||||||
|
"description": "Test description",
|
||||||
|
},
|
||||||
|
"defaults": {
|
||||||
|
"language": "python",
|
||||||
|
"ci": "github",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
def test_find_config_yml(self, tmp_path):
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
"""Test finding YML configuration file."""
|
config_file = Path(tmpdir) / "project.yaml"
|
||||||
(tmp_path / ".readmerc.yml").write_text("project_name: test")
|
with open(config_file, "w") as f:
|
||||||
|
yaml.dump(config_content, f)
|
||||||
|
|
||||||
config_path = ConfigLoader.find_config(tmp_path)
|
config = Config.load(str(config_file))
|
||||||
assert config_path is not None
|
|
||||||
|
|
||||||
def test_find_config_toml(self, tmp_path):
|
assert config.author == "Test Author"
|
||||||
"""Test finding TOML configuration file."""
|
assert config.email == "test@example.com"
|
||||||
(tmp_path / ".readmerc").write_text("project_name = 'test'")
|
assert config.license == "MIT"
|
||||||
|
assert config.description == "Test description"
|
||||||
|
assert config.default_language == "python"
|
||||||
|
assert config.default_ci == "github"
|
||||||
|
|
||||||
config_path = ConfigLoader.find_config(tmp_path)
|
def test_config_save(self):
|
||||||
assert config_path is not None
|
"""Test saving configuration to file."""
|
||||||
|
config = Config(
|
||||||
def test_load_yaml_config(self, tmp_path):
|
author="Test Author",
|
||||||
"""Test loading YAML configuration file."""
|
email="test@example.com",
|
||||||
config_file = tmp_path / ".readmerc.yaml"
|
license="MIT",
|
||||||
config_file.write_text("""
|
default_language="go",
|
||||||
project_name: "My Test Project"
|
|
||||||
description: "A test description"
|
|
||||||
template: "minimal"
|
|
||||||
interactive: true
|
|
||||||
|
|
||||||
sections:
|
|
||||||
order:
|
|
||||||
- title
|
|
||||||
- description
|
|
||||||
- overview
|
|
||||||
|
|
||||||
custom_fields:
|
|
||||||
author: "Test Author"
|
|
||||||
email: "test@example.com"
|
|
||||||
""")
|
|
||||||
|
|
||||||
config = ConfigLoader.load(config_file)
|
|
||||||
|
|
||||||
assert config.project_name == "My Test Project"
|
|
||||||
assert config.description == "A test description"
|
|
||||||
assert config.template == "minimal"
|
|
||||||
assert config.interactive is True
|
|
||||||
assert "author" in config.custom_fields
|
|
||||||
|
|
||||||
def test_load_toml_config(self, tmp_path):
|
|
||||||
"""Test loading TOML configuration file."""
|
|
||||||
config_file = tmp_path / "pyproject.toml"
|
|
||||||
config_file.write_text("""
|
|
||||||
[tool.auto-readme]
|
|
||||||
filename = "README.md"
|
|
||||||
sections = ["title", "description", "overview"]
|
|
||||||
""")
|
|
||||||
|
|
||||||
config = ConfigLoader.load(config_file)
|
|
||||||
|
|
||||||
assert config.output_filename == "README.md"
|
|
||||||
|
|
||||||
def test_load_nonexistent_file(self):
|
|
||||||
"""Test loading nonexistent configuration file."""
|
|
||||||
config = ConfigLoader.load(Path("/nonexistent/config.yaml"))
|
|
||||||
assert config.project_name is None
|
|
||||||
|
|
||||||
def test_load_invalid_yaml(self, tmp_path):
|
|
||||||
"""Test loading invalid YAML raises error."""
|
|
||||||
config_file = tmp_path / ".readmerc.yaml"
|
|
||||||
config_file.write_text("invalid: yaml: content: [")
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
ConfigLoader.load(config_file)
|
|
||||||
|
|
||||||
|
|
||||||
class TestConfigValidator:
|
|
||||||
"""Tests for ConfigValidator."""
|
|
||||||
|
|
||||||
def test_validate_valid_config(self):
|
|
||||||
"""Test validating a valid configuration."""
|
|
||||||
config = ReadmeConfig(
|
|
||||||
project_name="test",
|
|
||||||
template="base",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
errors = ConfigValidator.validate(config)
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
assert len(errors) == 0
|
config_file = Path(tmpdir) / "config.yaml"
|
||||||
|
config.save(config_file)
|
||||||
|
|
||||||
def test_validate_invalid_template(self):
|
assert config_file.exists()
|
||||||
"""Test validating invalid template name."""
|
|
||||||
config = ReadmeConfig(
|
with open(config_file, "r") as f:
|
||||||
project_name="test",
|
saved_data = yaml.safe_load(f)
|
||||||
template="nonexistent",
|
|
||||||
|
assert saved_data["project"]["author"] == "Test Author"
|
||||||
|
assert saved_data["defaults"]["language"] == "go"
|
||||||
|
|
||||||
|
def test_get_template_dirs(self):
|
||||||
|
"""Test getting template directories."""
|
||||||
|
config = Config()
|
||||||
|
dirs = config.get_template_dirs()
|
||||||
|
assert len(dirs) > 0
|
||||||
|
assert any("project-scaffold" in d for d in dirs)
|
||||||
|
|
||||||
|
def test_get_custom_templates_dir(self):
|
||||||
|
"""Test getting custom templates directory."""
|
||||||
|
config = Config()
|
||||||
|
custom_dir = config.get_custom_templates_dir()
|
||||||
|
assert "project-scaffold" in custom_dir
|
||||||
|
|
||||||
|
def test_get_template_vars(self):
|
||||||
|
"""Test getting template variables for language."""
|
||||||
|
config = Config(
|
||||||
|
template_vars={
|
||||||
|
"python": {"version": "3.11"},
|
||||||
|
"nodejs": {"version": "16"},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
errors = ConfigValidator.validate(config)
|
python_vars = config.get_template_vars("python")
|
||||||
assert len(errors) == 1
|
assert python_vars.get("version") == "3.11"
|
||||||
assert "Invalid template" in errors[0]
|
|
||||||
|
|
||||||
def test_validate_invalid_section(self):
|
nodejs_vars = config.get_template_vars("nodejs")
|
||||||
"""Test validating invalid section name."""
|
assert nodejs_vars.get("version") == "16"
|
||||||
config = ReadmeConfig(
|
|
||||||
project_name="test",
|
|
||||||
sections={"order": ["invalid_section"]},
|
|
||||||
)
|
|
||||||
|
|
||||||
errors = ConfigValidator.validate(config)
|
other_vars = config.get_template_vars("go")
|
||||||
assert len(errors) == 1
|
assert other_vars == {}
|
||||||
assert "Invalid section" in errors[0]
|
|
||||||
|
|
||||||
def test_generate_template(self):
|
|
||||||
"""Test generating configuration template."""
|
|
||||||
template = ConfigValidator.generate_template()
|
|
||||||
|
|
||||||
assert "project_name:" in template
|
|
||||||
assert "description:" in template
|
|
||||||
assert "template:" in template
|
|
||||||
assert "interactive:" in template
|
|
||||||
|
|||||||
116
tests/test_gitignore.py
Normal file
116
tests/test_gitignore.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"""Tests for gitignore generation."""
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from project_scaffold_cli.gitignore import GitignoreGenerator
|
||||||
|
|
||||||
|
|
||||||
|
class TestGitignoreGenerator:
|
||||||
|
"""Test GitignoreGenerator class."""
|
||||||
|
|
||||||
|
def test_generator_initialization(self):
|
||||||
|
"""Test generator can be initialized."""
|
||||||
|
gen = GitignoreGenerator()
|
||||||
|
assert gen is not None
|
||||||
|
|
||||||
|
def test_generate_python_gitignore(self):
|
||||||
|
"""Test generating Python .gitignore."""
|
||||||
|
gen = GitignoreGenerator()
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
output_path = Path(tmpdir) / ".gitignore"
|
||||||
|
gen.generate("python", output_path)
|
||||||
|
|
||||||
|
assert output_path.exists()
|
||||||
|
content = output_path.read_text()
|
||||||
|
|
||||||
|
assert "__pycache__" in content
|
||||||
|
assert "*.pyc" in content
|
||||||
|
assert "venv/" in content
|
||||||
|
assert ".pytest_cache" in content
|
||||||
|
|
||||||
|
def test_generate_nodejs_gitignore(self):
|
||||||
|
"""Test generating Node.js .gitignore."""
|
||||||
|
gen = GitignoreGenerator()
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
output_path = Path(tmpdir) / ".gitignore"
|
||||||
|
gen.generate("nodejs", output_path)
|
||||||
|
|
||||||
|
assert output_path.exists()
|
||||||
|
content = output_path.read_text()
|
||||||
|
|
||||||
|
assert "node_modules" in content
|
||||||
|
assert "npm-debug.log" in content
|
||||||
|
|
||||||
|
def test_generate_go_gitignore(self):
|
||||||
|
"""Test generating Go .gitignore."""
|
||||||
|
gen = GitignoreGenerator()
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
output_path = Path(tmpdir) / ".gitignore"
|
||||||
|
gen.generate("go", output_path)
|
||||||
|
|
||||||
|
assert output_path.exists()
|
||||||
|
content = output_path.read_text()
|
||||||
|
|
||||||
|
assert "*.exe" in content
|
||||||
|
assert "vendor/" in content
|
||||||
|
|
||||||
|
def test_generate_rust_gitignore(self):
|
||||||
|
"""Test generating Rust .gitignore."""
|
||||||
|
gen = GitignoreGenerator()
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
output_path = Path(tmpdir) / ".gitignore"
|
||||||
|
gen.generate("rust", output_path)
|
||||||
|
|
||||||
|
assert output_path.exists()
|
||||||
|
content = output_path.read_text()
|
||||||
|
|
||||||
|
assert "target/" in content
|
||||||
|
assert "Cargo.lock" in content
|
||||||
|
|
||||||
|
def test_generate_unsupported_language(self):
|
||||||
|
"""Test generating gitignore for unsupported language."""
|
||||||
|
gen = GitignoreGenerator()
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
gen.generate("unsupported", Path(tmpdir) / ".gitignore")
|
||||||
|
assert "Unsupported language" in str(exc_info.value)
|
||||||
|
|
||||||
|
def test_append_patterns(self):
|
||||||
|
"""Test appending patterns to existing .gitignore."""
|
||||||
|
gen = GitignoreGenerator()
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
gitignore_path = Path(tmpdir) / ".gitignore"
|
||||||
|
gitignore_path.write_text("# Original content\n")
|
||||||
|
|
||||||
|
gen.append_patterns(gitignore_path, {"*.custom", "secret.txt"})
|
||||||
|
|
||||||
|
content = gitignore_path.read_text()
|
||||||
|
assert "*.custom" in content
|
||||||
|
assert "secret.txt" in content
|
||||||
|
|
||||||
|
def test_get_template_content(self):
|
||||||
|
"""Test getting raw template content."""
|
||||||
|
gen = GitignoreGenerator()
|
||||||
|
|
||||||
|
python_content = gen.get_template_content("python")
|
||||||
|
assert isinstance(python_content, str)
|
||||||
|
assert len(python_content) > 0
|
||||||
|
|
||||||
|
nodejs_content = gen.get_template_content("nodejs")
|
||||||
|
assert isinstance(nodejs_content, str)
|
||||||
|
assert len(nodejs_content) > 0
|
||||||
|
|
||||||
|
def test_list_available_templates(self):
|
||||||
|
"""Test listing available templates."""
|
||||||
|
gen = GitignoreGenerator()
|
||||||
|
templates = gen.list_available_templates()
|
||||||
|
assert isinstance(templates, list)
|
||||||
@@ -1,197 +1,145 @@
|
|||||||
"""Tests for template rendering."""
|
"""Tests for template engine."""
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from src.auto_readme.models import Project, ProjectConfig, ProjectType
|
from project_scaffold_cli.template_engine import TemplateEngine
|
||||||
from src.auto_readme.templates import TemplateRenderer, TemplateManager
|
|
||||||
|
|
||||||
|
|
||||||
class TestTemplateRenderer:
|
class TestTemplateEngine:
|
||||||
"""Tests for TemplateRenderer."""
|
"""Test TemplateEngine class."""
|
||||||
|
|
||||||
def test_render_base_template(self):
|
def test_engine_initialization(self):
|
||||||
"""Test rendering the base template."""
|
"""Test engine can be initialized."""
|
||||||
project = Project(
|
engine = TemplateEngine()
|
||||||
root_path=Path("/test"),
|
assert engine is not None
|
||||||
project_type=ProjectType.PYTHON,
|
assert engine.SUPPORTED_LANGUAGES == ["python", "nodejs", "go", "rust"]
|
||||||
config=ProjectConfig(
|
|
||||||
name="test-project",
|
|
||||||
description="A test project",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
renderer = TemplateRenderer()
|
def test_render_language_template_python(self):
|
||||||
content = renderer.render(project, "base")
|
"""Test rendering Python template."""
|
||||||
|
engine = TemplateEngine()
|
||||||
|
context = {
|
||||||
|
"project_name": "test-project",
|
||||||
|
"project_slug": "test-project",
|
||||||
|
"author": "Test Author",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"description": "A test project",
|
||||||
|
"license": "MIT",
|
||||||
|
"year": "2024",
|
||||||
|
"language": "python",
|
||||||
|
}
|
||||||
|
|
||||||
assert "# test-project" in content
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
assert "A test project" in content
|
output_dir = Path(tmpdir) / "test-project"
|
||||||
assert "## Overview" in content
|
output_dir.mkdir()
|
||||||
|
engine.render_language_template("python", context, output_dir)
|
||||||
|
|
||||||
def test_render_minimal_template(self):
|
assert (output_dir / "setup.py").exists()
|
||||||
"""Test rendering the minimal template."""
|
assert (output_dir / "README.md").exists()
|
||||||
project = Project(
|
|
||||||
root_path=Path("/test"),
|
|
||||||
project_type=ProjectType.PYTHON,
|
|
||||||
)
|
|
||||||
|
|
||||||
renderer = TemplateRenderer()
|
def test_render_language_template_go(self):
|
||||||
content = renderer.render(project, "base")
|
"""Test rendering Go template."""
|
||||||
|
engine = TemplateEngine()
|
||||||
|
context = {
|
||||||
|
"project_name": "test-go-project",
|
||||||
|
"project_slug": "test-go-project",
|
||||||
|
"author": "Test Author",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"description": "A test Go project",
|
||||||
|
"license": "MIT",
|
||||||
|
"year": "2024",
|
||||||
|
"language": "go",
|
||||||
|
}
|
||||||
|
|
||||||
assert "Generated by Auto README Generator" in content
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
output_dir = Path(tmpdir) / "test-go-project"
|
||||||
|
output_dir.mkdir()
|
||||||
|
engine.render_language_template("go", context, output_dir)
|
||||||
|
|
||||||
def test_tech_stack_detection(self):
|
assert (output_dir / "go.mod").exists()
|
||||||
"""Test technology stack detection."""
|
assert (output_dir / "main.go").exists()
|
||||||
from src.auto_readme.models import Dependency
|
assert (output_dir / "README.md").exists()
|
||||||
|
|
||||||
project = Project(
|
def test_render_language_template_rust(self):
|
||||||
root_path=Path("/test"),
|
"""Test rendering Rust template."""
|
||||||
project_type=ProjectType.PYTHON,
|
engine = TemplateEngine()
|
||||||
dependencies=[
|
context = {
|
||||||
Dependency(name="fastapi"),
|
"project_name": "test-rust-project",
|
||||||
Dependency(name="flask"),
|
"project_slug": "test-rust-project",
|
||||||
Dependency(name="requests"),
|
"author": "Test Author",
|
||||||
],
|
"email": "test@example.com",
|
||||||
)
|
"description": "A test Rust project",
|
||||||
|
"license": "MIT",
|
||||||
|
"year": "2024",
|
||||||
|
"language": "rust",
|
||||||
|
}
|
||||||
|
|
||||||
renderer = TemplateRenderer()
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
context = renderer._build_context(project)
|
output_dir = Path(tmpdir) / "test-rust-project"
|
||||||
|
output_dir.mkdir()
|
||||||
|
engine.render_language_template("rust", context, output_dir)
|
||||||
|
|
||||||
assert "FastAPI" in context["tech_stack"]
|
assert (output_dir / "Cargo.toml").exists()
|
||||||
assert "Flask" in context["tech_stack"]
|
assert (output_dir / "src" / "main.rs").exists()
|
||||||
|
assert (output_dir / "README.md").exists()
|
||||||
|
|
||||||
def test_installation_steps_generation(self):
|
def test_render_language_template_unsupported(self):
|
||||||
"""Test automatic installation steps generation."""
|
"""Test rendering unsupported language."""
|
||||||
project = Project(
|
engine = TemplateEngine()
|
||||||
root_path=Path("/test"),
|
context = {"project_name": "test"}
|
||||||
project_type=ProjectType.PYTHON,
|
|
||||||
)
|
|
||||||
|
|
||||||
renderer = TemplateRenderer()
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
context = renderer._build_context(project)
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
engine.render_language_template(
|
||||||
|
"unsupported", context, Path(tmpdir)
|
||||||
|
)
|
||||||
|
assert "Unsupported language" in str(exc_info.value)
|
||||||
|
|
||||||
assert "pip install" in context["installation_steps"][0]
|
def test_render_ci_template_github(self):
|
||||||
|
"""Test rendering GitHub Actions CI template."""
|
||||||
|
engine = TemplateEngine()
|
||||||
|
context = {
|
||||||
|
"project_name": "test-project",
|
||||||
|
"project_slug": "test-project",
|
||||||
|
}
|
||||||
|
|
||||||
def test_go_installation_steps(self):
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
"""Test Go installation steps."""
|
output_dir = Path(tmpdir)
|
||||||
project = Project(
|
engine.render_ci_template("github", context, output_dir)
|
||||||
root_path=Path("/test"),
|
|
||||||
project_type=ProjectType.GO,
|
|
||||||
)
|
|
||||||
|
|
||||||
renderer = TemplateRenderer()
|
workflow_path = (
|
||||||
context = renderer._build_context(project)
|
output_dir / ".github" / "workflows" / "ci.yml"
|
||||||
|
)
|
||||||
|
assert workflow_path.exists()
|
||||||
|
content = workflow_path.read_text()
|
||||||
|
assert "CI" in content
|
||||||
|
|
||||||
assert "go mod download" in context["installation_steps"][0]
|
def test_render_ci_template_gitlab(self):
|
||||||
|
"""Test rendering GitLab CI template."""
|
||||||
|
engine = TemplateEngine()
|
||||||
|
context = {
|
||||||
|
"project_name": "test-project",
|
||||||
|
"project_slug": "test-project",
|
||||||
|
}
|
||||||
|
|
||||||
def test_rust_installation_steps(self):
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
"""Test Rust installation steps."""
|
output_dir = Path(tmpdir)
|
||||||
project = Project(
|
engine.render_ci_template("gitlab", context, output_dir)
|
||||||
root_path=Path("/test"),
|
|
||||||
project_type=ProjectType.RUST,
|
|
||||||
)
|
|
||||||
|
|
||||||
renderer = TemplateRenderer()
|
assert (output_dir / ".gitlab-ci.yml").exists()
|
||||||
context = renderer._build_context(project)
|
|
||||||
|
|
||||||
assert "cargo build" in context["installation_steps"][0]
|
def test_validate_context_missing_required(self):
|
||||||
|
"""Test context validation with missing required fields."""
|
||||||
|
engine = TemplateEngine()
|
||||||
|
missing = engine.validate_context({})
|
||||||
|
assert "project_name" in missing
|
||||||
|
assert "author" in missing
|
||||||
|
|
||||||
def test_feature_detection(self):
|
def test_validate_context_complete(self):
|
||||||
"""Test automatic feature detection."""
|
"""Test context validation with all required fields."""
|
||||||
from src.auto_readme.models import Function, Class, SourceFile, FileType
|
engine = TemplateEngine()
|
||||||
|
context = {"project_name": "test", "author": "Test Author"}
|
||||||
functions = [
|
missing = engine.validate_context(context)
|
||||||
Function(name="test_func", line_number=1),
|
assert len(missing) == 0
|
||||||
]
|
|
||||||
classes = [
|
|
||||||
Class(name="TestClass", line_number=1),
|
|
||||||
]
|
|
||||||
|
|
||||||
project = Project(
|
|
||||||
root_path=Path("/test"),
|
|
||||||
project_type=ProjectType.PYTHON,
|
|
||||||
files=[
|
|
||||||
SourceFile(
|
|
||||||
path=Path("test.py"),
|
|
||||||
file_type=FileType.SOURCE,
|
|
||||||
functions=functions,
|
|
||||||
classes=classes,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
renderer = TemplateRenderer()
|
|
||||||
context = renderer._build_context(project)
|
|
||||||
|
|
||||||
assert "Contains 1 classes" in context["features"]
|
|
||||||
assert "Contains 1 functions" in context["features"]
|
|
||||||
|
|
||||||
def test_custom_context_override(self):
|
|
||||||
"""Test custom context can override auto-detected values."""
|
|
||||||
project = Project(
|
|
||||||
root_path=Path("/test"),
|
|
||||||
project_type=ProjectType.PYTHON,
|
|
||||||
)
|
|
||||||
|
|
||||||
renderer = TemplateRenderer()
|
|
||||||
content = renderer.render(
|
|
||||||
project,
|
|
||||||
"base",
|
|
||||||
title="Custom Title",
|
|
||||||
description="Custom description",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert "# Custom Title" in content
|
|
||||||
assert "Custom description" in content
|
|
||||||
|
|
||||||
def test_contributing_guidelines(self):
|
|
||||||
"""Test contributing guidelines generation."""
|
|
||||||
project = Project(
|
|
||||||
root_path=Path("/test"),
|
|
||||||
project_type=ProjectType.PYTHON,
|
|
||||||
)
|
|
||||||
|
|
||||||
renderer = TemplateRenderer()
|
|
||||||
context = renderer._build_context(project)
|
|
||||||
|
|
||||||
assert "Fork the repository" in context["contributing_guidelines"]
|
|
||||||
assert "git checkout -b" in context["contributing_guidelines"]
|
|
||||||
assert "pytest" in context["contributing_guidelines"]
|
|
||||||
|
|
||||||
def test_javascript_contributing_guidelines(self):
|
|
||||||
"""Test JavaScript contributing guidelines."""
|
|
||||||
project = Project(
|
|
||||||
root_path=Path("/test"),
|
|
||||||
project_type=ProjectType.JAVASCRIPT,
|
|
||||||
)
|
|
||||||
|
|
||||||
renderer = TemplateRenderer()
|
|
||||||
context = renderer._build_context(project)
|
|
||||||
|
|
||||||
assert "npm test" in context["contributing_guidelines"]
|
|
||||||
|
|
||||||
|
|
||||||
class TestTemplateManager:
|
|
||||||
"""Tests for TemplateManager."""
|
|
||||||
|
|
||||||
def test_list_templates(self):
|
|
||||||
"""Test listing available templates."""
|
|
||||||
manager = TemplateManager()
|
|
||||||
templates = manager.list_templates()
|
|
||||||
|
|
||||||
assert "base" in templates
|
|
||||||
|
|
||||||
def test_get_template_path_builtin(self):
|
|
||||||
"""Test getting path for built-in template."""
|
|
||||||
manager = TemplateManager()
|
|
||||||
path = manager.get_template_path("base")
|
|
||||||
|
|
||||||
assert path is None
|
|
||||||
|
|
||||||
def test_get_template_path_custom(self):
|
|
||||||
"""Test getting path for custom template."""
|
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
||||||
manager = TemplateManager(Path(tmp_dir))
|
|
||||||
path = manager.get_template_path("custom.md.j2")
|
|
||||||
assert path is not None
|
|
||||||
|
|||||||
Reference in New Issue
Block a user