Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d4b49b14d | |||
| c64e81e56f | |||
| 28fae25f83 | |||
| f180b8a082 | |||
| b8c47f6b46 | |||
| 1639e9e5b4 | |||
| b2bc2b9ea5 | |||
| f19a2c0869 | |||
| ca6b0c66f1 | |||
| 9f5a693d5d | |||
| 8c63473d84 | |||
| 73ed60e87c | |||
| 2b5f70ee09 | |||
| bcfce038ec | |||
| 50553bb154 | |||
| 841405b437 | |||
| 145a9c6907 | |||
| f3d29b74d5 | |||
| ef04db2ebd | |||
| 56f544b093 | |||
| 96db2f5236 | |||
| 06910dbb6e | |||
| d30e40e3fd | |||
| 7c42ab1bf0 | |||
| 718290943b | |||
| de65989205 | |||
| 7421d7da0f | |||
| 3b688684c2 | |||
| 5ae900bfd7 | |||
| af11223841 | |||
| 2dbce2439b | |||
| b00c6b5cce | |||
| ca375ac3e1 | |||
| 83d4d92c21 | |||
| 4de73353a4 | |||
| 22bd48f04f | |||
| e08a0c7f91 | |||
| f14a4a8c78 | |||
| 975627a712 | |||
| 880df5e86d | |||
| 98c3b25710 | |||
| 36b5baceb3 | |||
| f94483bac0 | |||
| c720d2852d | |||
| de9786f998 | |||
| 607b54df19 | |||
| 4575516c3e | |||
| 68e2eaeec5 | |||
| e6ab90f390 | |||
| 856e22b34b | |||
| d9530a3453 | |||
| 2b16512be2 | |||
| 2cc6948cbd | |||
| 206d477156 | |||
| 6ddd35a876 | |||
| 80609d14ba | |||
| 041f8331f2 | |||
| 0fbdfc80ee | |||
| fbaa1a90f6 | |||
| 49be08e504 | |||
| d3e9467389 | |||
| 76ae2f7fe6 | |||
| f32bdc2f3e | |||
| cedbcd63b3 | |||
| 312efdca75 | |||
| ce6e879fa9 | |||
| ef7b7f7673 | |||
| 66e5ebd1a8 | |||
| 8d48b3da87 | |||
| 8fffa89be8 | |||
| d4edd2f77a | |||
| 689b4814fa | |||
| c68659d903 | |||
| c984c31be0 | |||
| b5eb3c8991 | |||
| e84520d96d | |||
| 582787f8af | |||
| c4b5cb82c3 |
14
.env.example
14
.env.example
@@ -1,13 +1,3 @@
|
||||
# ScaffoldForge Environment Configuration
|
||||
# Copy this file to .env and fill in your values
|
||||
|
||||
# GitHub Personal Access Token for API access
|
||||
# Create one at: https://github.com/settings/tokens
|
||||
# Required scopes: repo (for private repos)
|
||||
GITHUB_TOKEN=your_github_token_here
|
||||
|
||||
# Default template directory path
|
||||
# SCAFFOLD_TEMPLATE_DIR=./templates
|
||||
|
||||
# Default output directory
|
||||
# SCAFFOLD_OUTPUT_DIR=./output
|
||||
SCAFFOLD_TEMPLATE_DIR=
|
||||
SCAFFOLD_OUTPUT_DIR=./generated
|
||||
@@ -2,30 +2,51 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
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'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install ruff mypy
|
||||
|
||||
- name: Run Python linting
|
||||
run: |
|
||||
ruff check scaffoldforge tests
|
||||
ruff format --check scaffoldforge tests
|
||||
|
||||
- name: Run Python type checking
|
||||
run: |
|
||||
python -m mypy scaffoldforge tests
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -e ".[dev]"
|
||||
- name: Run unit tests
|
||||
run: pytest tests/unit/ -v
|
||||
- name: Run integration tests
|
||||
run: pytest tests/integration/ -v
|
||||
- name: Run linter
|
||||
run: ruff check .
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./coverage.xml
|
||||
fail_ci_if_error: false
|
||||
|
||||
- name: Run Python tests
|
||||
run: |
|
||||
pytest tests/ -v --tb=short
|
||||
pytest tests/integration/ -v
|
||||
185
.gitignore
vendored
185
.gitignore
vendored
@@ -1,138 +1,47 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
Pipfile.lock
|
||||
|
||||
# PEP 582
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Project specific
|
||||
output/
|
||||
*.generated/
|
||||
{
|
||||
"Python": [
|
||||
"__pycache__/",
|
||||
"*.py[cod]",
|
||||
"*$py.class",
|
||||
"*.so",
|
||||
".Python",
|
||||
"build/",
|
||||
"dist/",
|
||||
"*.egg-info/",
|
||||
".eggs/",
|
||||
"venv/",
|
||||
".env"
|
||||
],
|
||||
"Node": [
|
||||
"node_modules/",
|
||||
"npm-debug.log*",
|
||||
"yarn-debug.log*",
|
||||
"yarn-error.log*",
|
||||
"dist/",
|
||||
"coverage/"
|
||||
],
|
||||
"Go": [
|
||||
"*.exe",
|
||||
"*.exe~",
|
||||
"*.dll",
|
||||
"*.so",
|
||||
"*.dylib",
|
||||
"*.test",
|
||||
"*.out",
|
||||
"go.work"
|
||||
],
|
||||
"Rust": [
|
||||
"target/",
|
||||
"Cargo.lock",
|
||||
"*.swp",
|
||||
"*.swo"
|
||||
],
|
||||
"General": [
|
||||
".DS_Store",
|
||||
".vscode/",
|
||||
".idea/",
|
||||
"*.swp",
|
||||
"*.swo",
|
||||
"*~"
|
||||
]
|
||||
}
|
||||
135
README.md
135
README.md
@@ -1,21 +1,7 @@
|
||||
# ScaffoldForge
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.python.org/)
|
||||
[](https://github.com/7000pctAUTO/scaffoldforge)
|
||||
|
||||
ScaffoldForge is a CLI tool that parses GitHub issues and automatically generates project scaffolds with starter code, file structures, and TODO comments. Developers can point it at a repo and issue, and it creates a development-ready starting point with placeholders for implementation.
|
||||
|
||||
## Features
|
||||
|
||||
- Parse GitHub issue descriptions, checklists, and comments
|
||||
- Generate project structures from templates
|
||||
- Create starter files with TODO comments
|
||||
- Support multiple languages (Python, JavaScript, Go, Rust)
|
||||
- Custom template system
|
||||
- Preview mode before generating
|
||||
- Interactive mode with prompts
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
@@ -25,7 +11,7 @@ pip install scaffoldforge
|
||||
Or from source:
|
||||
|
||||
```bash
|
||||
git clone https://7000pct.gitea.bloupla.net/7000pctAUTO/scaffoldforge.git
|
||||
git clone https://github.com/yourusername/scaffoldforge.git
|
||||
cd scaffoldforge
|
||||
pip install -e .
|
||||
```
|
||||
@@ -42,6 +28,28 @@ export GITHUB_TOKEN=your_github_token_here
|
||||
scaffoldforge generate https://github.com/owner/repo/issues/123
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
ScaffoldForge can be configured using environment variables or a configuration file.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Required |
|
||||
|----------|-------------|----------|
|
||||
| `GITHUB_TOKEN` | GitHub Personal Access Token for API access | Yes |
|
||||
| `SCAFFOLD_TEMPLATE_DIR` | Default template directory path | No |
|
||||
| `SCAFFOLD_OUTPUT_DIR` | Default output directory | No |
|
||||
|
||||
### Configuration File
|
||||
|
||||
Copy `.env.example` to `.env` and fill in your values:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env` to add your GitHub token.
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
@@ -89,12 +97,52 @@ Interactive mode:
|
||||
scaffoldforge generate https://github.com/owner/repo/issues/123 --interactive
|
||||
```
|
||||
|
||||
List available templates:
|
||||
### List Available Templates
|
||||
|
||||
```bash
|
||||
scaffoldforge list-templates --language python
|
||||
```
|
||||
|
||||
## Custom Templates
|
||||
|
||||
You can create custom templates for ScaffoldForge. Place your templates in a directory and set the `SCAFFOLD_TEMPLATE_DIR` environment variable.
|
||||
|
||||
### Template Structure
|
||||
|
||||
```
|
||||
custom_templates/
|
||||
├── python/
|
||||
│ ├── default/
|
||||
│ │ ├── main.py.j2
|
||||
│ │ └── utils.py.j2
|
||||
│ └── cli/
|
||||
│ └── main.py.j2
|
||||
└── javascript/
|
||||
└── default/
|
||||
└── index.js.j2
|
||||
```
|
||||
|
||||
### Template Variables
|
||||
|
||||
Templates can use the following variables:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `project_name` | Sanitized project name |
|
||||
| `project_name_kebab` | kebab-case project name |
|
||||
| `project_name_snake` | snake_case project name |
|
||||
| `project_name_pascal` | PascalCase project name |
|
||||
| `issue_number` | GitHub issue number |
|
||||
| `issue_title` | GitHub issue title |
|
||||
| `issue_url` | GitHub issue URL |
|
||||
| `repository` | Repository identifier (owner/repo) |
|
||||
| `author` | Issue author |
|
||||
| `created_date` | Issue creation date |
|
||||
| `todo_items` | List of incomplete checklist items |
|
||||
| `completed_items` | List of completed checklist items |
|
||||
| `requirements` | List of requirements |
|
||||
| `acceptance_criteria` | List of acceptance criteria |
|
||||
|
||||
## Supported Languages
|
||||
|
||||
- **Python** - Generates `main.py`, `utils.py`, `models.py`, `pyproject.toml`
|
||||
@@ -102,40 +150,31 @@ scaffoldforge list-templates --language python
|
||||
- **Go** - Generates `main.go`, `utils.go`, `go.mod`
|
||||
- **Rust** - Generates `main.rs`, `lib.rs`, `Cargo.toml`
|
||||
|
||||
## Custom Templates
|
||||
|
||||
You can create custom templates for ScaffoldForge. Place your templates in a directory and set the `SCAFFOLD_TEMPLATE_DIR` environment variable.
|
||||
|
||||
```bash
|
||||
SCAFFOLD_TEMPLATE_DIR=./custom_templates scaffoldforge generate <issue_url>
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Required |
|
||||
|----------|-------------|----------|
|
||||
| `GITHUB_TOKEN` | GitHub Personal Access Token for API access | Yes |
|
||||
| `SCAFFOLD_TEMPLATE_DIR` | Default template directory path | No |
|
||||
| `SCAFFOLD_OUTPUT_DIR` | Default output directory | No |
|
||||
|
||||
### Configuration File
|
||||
|
||||
Copy `.env.example` to `.env` and fill in your values:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Issue Parsing**: Fetches the GitHub issue and parses title, description, checklist items, and labels
|
||||
2. **Language Detection**: Automatically detects programming language from labels or content
|
||||
3. **Template Selection**: Selects appropriate templates based on language
|
||||
4. **Context Generation**: Creates template context with issue data
|
||||
5. **File Generation**: Renders templates and writes files
|
||||
6. **Documentation**: Generates README.md and .gitignore
|
||||
1. **Issue Parsing**: ScaffoldForge fetches the GitHub issue and parses:
|
||||
- Title and description
|
||||
- Checklist items (converted to TODO comments)
|
||||
- Labels (used for language detection)
|
||||
- Requirements sections
|
||||
- Suggested file/directory paths
|
||||
|
||||
2. **Template Selection**: Based on the detected or specified language, appropriate templates are selected.
|
||||
|
||||
3. **Context Generation**: A template context is created with issue data and project name.
|
||||
|
||||
4. **File Generation**: Templates are rendered with the context and files are written to the output directory.
|
||||
|
||||
5. **Documentation**: A `README.md` and `.gitignore` are automatically generated.
|
||||
|
||||
## Error Handling
|
||||
|
||||
ScaffoldForge handles common errors gracefully:
|
||||
|
||||
- **GitHub API rate limit**: Suggests using a token
|
||||
- **Invalid URL**: Validates and provides clear error messages
|
||||
- **Template errors**: Shows syntax errors in templates
|
||||
- **Permission errors**: Validates output paths
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
"""CLI module for ScaffoldForge."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from scaffoldforge.cli.commands import generate, preview, list_templates
|
||||
from scaffoldforge.cli.commands import generate, list_templates, preview
|
||||
from scaffoldforge.config import get_config
|
||||
from scaffoldforge.parsers import IssueParser
|
||||
from scaffoldforge.generators import StructureGenerator, CodeGenerator
|
||||
from scaffoldforge.generators import CodeGenerator, StructureGenerator
|
||||
from scaffoldforge.templates import TemplateEngine
|
||||
|
||||
__all__ = [
|
||||
"cli",
|
||||
"generate",
|
||||
"preview",
|
||||
"list_templates",
|
||||
"StructureGenerator",
|
||||
"CodeGenerator",
|
||||
"TemplateEngine",
|
||||
]
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
"""CLI commands for ScaffoldForge."""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
|
||||
from scaffoldforge.config import get_config
|
||||
from scaffoldforge.generators import StructureGenerator, CodeGenerator
|
||||
from scaffoldforge.generators import CodeGenerator, StructureGenerator
|
||||
from scaffoldforge.parsers import IssueParser
|
||||
from scaffoldforge.templates import TemplateEngine
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
@@ -11,7 +11,7 @@ class Config:
|
||||
"""Configuration management class."""
|
||||
|
||||
_instance: Optional["Config"] = None
|
||||
_config: Dict[str, Any] = {}
|
||||
_config: dict[str, Any] = {}
|
||||
|
||||
def __new__(cls) -> "Config":
|
||||
if cls._instance is None:
|
||||
@@ -78,20 +78,22 @@ class Config:
|
||||
"""Load configuration from YAML file."""
|
||||
path = Path(config_path)
|
||||
if path.exists():
|
||||
with open(path, "r") as f:
|
||||
with open(path) as f:
|
||||
user_config = yaml.safe_load(f) or {}
|
||||
self._config.update(user_config)
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
def get(self, key: str, default: Any | None = None) -> Any:
|
||||
"""Get configuration value using dot notation."""
|
||||
keys = key.split(".")
|
||||
value = self._config
|
||||
value: Any = self._config
|
||||
for k in keys:
|
||||
if isinstance(value, dict):
|
||||
value = value.get(k)
|
||||
else:
|
||||
return default
|
||||
return value if value is not None else default
|
||||
if value is None:
|
||||
return default
|
||||
return value
|
||||
|
||||
def get_github_token(self) -> Optional[str]:
|
||||
"""Get GitHub token from environment or config."""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Generators module for ScaffoldForge."""
|
||||
|
||||
from scaffoldforge.generators.structure import StructureGenerator
|
||||
from scaffoldforge.generators.code import CodeGenerator
|
||||
from scaffoldforge.generators.models import FileSpec
|
||||
from scaffoldforge.generators.structure import StructureGenerator
|
||||
|
||||
__all__ = ["StructureGenerator", "CodeGenerator", "FileSpec"]
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Code generation functionality."""
|
||||
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any
|
||||
|
||||
from scaffoldforge.generators.models import FileSpec
|
||||
from scaffoldforge.parsers import IssueData
|
||||
from scaffoldforge.templates import TemplateEngine
|
||||
from scaffoldforge.generators.models import FileSpec
|
||||
|
||||
|
||||
class CodeGenerator:
|
||||
@@ -34,9 +34,7 @@ class CodeGenerator:
|
||||
self.template_engine = template_engine
|
||||
self.issue_data = issue_data
|
||||
|
||||
def generate_all_files(
|
||||
self, language: str, issue_data: IssueData
|
||||
) -> List[FileSpec]:
|
||||
def generate_all_files(self, language: str, issue_data: IssueData) -> list[FileSpec]:
|
||||
"""Generate all project files.
|
||||
|
||||
Args:
|
||||
@@ -46,7 +44,7 @@ class CodeGenerator:
|
||||
Returns:
|
||||
List of FileSpec objects.
|
||||
"""
|
||||
files = []
|
||||
files: list[FileSpec] = []
|
||||
context = self.template_engine.get_template_context(issue_data)
|
||||
|
||||
files.extend(self._generate_source_files(language, context))
|
||||
@@ -54,9 +52,7 @@ class CodeGenerator:
|
||||
|
||||
return files
|
||||
|
||||
def _generate_source_files(
|
||||
self, language: str, context: Dict[str, Any]
|
||||
) -> List[FileSpec]:
|
||||
def _generate_source_files(self, language: str, context: dict[str, Any]) -> list[FileSpec]:
|
||||
"""Generate source code files.
|
||||
|
||||
Args:
|
||||
@@ -66,16 +62,12 @@ class CodeGenerator:
|
||||
Returns:
|
||||
List of FileSpec objects.
|
||||
"""
|
||||
files = []
|
||||
files: list[FileSpec] = []
|
||||
source_templates = self.DEFAULT_FILES.get(language, [])
|
||||
|
||||
for template_name in source_templates:
|
||||
try:
|
||||
content = self.template_engine.render(
|
||||
template_name, context, language
|
||||
)
|
||||
extension = self._get_extension(language)
|
||||
filename = f"{template_name}{extension}" if not template_name.endswith(extension) else template_name
|
||||
content = self.template_engine.render(template_name, context, language)
|
||||
path = self._get_source_path(template_name, language)
|
||||
files.append(FileSpec(path=path, content=content))
|
||||
except ValueError:
|
||||
@@ -83,9 +75,7 @@ class CodeGenerator:
|
||||
|
||||
return files
|
||||
|
||||
def _generate_config_files(
|
||||
self, language: str, context: Dict[str, Any]
|
||||
) -> List[FileSpec]:
|
||||
def _generate_config_files(self, language: str, context: dict[str, Any]) -> list[FileSpec]:
|
||||
"""Generate configuration files.
|
||||
|
||||
Args:
|
||||
@@ -100,9 +90,7 @@ class CodeGenerator:
|
||||
|
||||
for config_name in config_templates:
|
||||
try:
|
||||
content = self.template_engine.render(
|
||||
config_name, context, language
|
||||
)
|
||||
content = self.template_engine.render(config_name, context, language)
|
||||
files.append(FileSpec(path=config_name, content=content))
|
||||
except ValueError:
|
||||
pass
|
||||
@@ -110,7 +98,7 @@ class CodeGenerator:
|
||||
return files
|
||||
|
||||
def _create_empty_file(
|
||||
self, template_name: str, language: str, context: Dict[str, Any]
|
||||
self, template_name: str, language: str, context: dict[str, Any]
|
||||
) -> FileSpec:
|
||||
"""Create an empty file with TODO comments.
|
||||
|
||||
@@ -122,20 +110,16 @@ class CodeGenerator:
|
||||
Returns:
|
||||
FileSpec object.
|
||||
"""
|
||||
extension = self._get_extension(language)
|
||||
filename = f"{template_name}{extension}"
|
||||
path = self._get_source_path(template_name, language)
|
||||
|
||||
todo_items = self.issue_data.get_todo_items()
|
||||
|
||||
content = self._generate_todo_content(
|
||||
language, template_name, todo_items
|
||||
)
|
||||
content = self._generate_todo_content(language, template_name, todo_items)
|
||||
|
||||
return FileSpec(path=path, content=content)
|
||||
|
||||
def _generate_todo_content(
|
||||
self, language: str, template_name: str, todo_items: List[str]
|
||||
self, language: str, template_name: str, todo_items: list[str]
|
||||
) -> str:
|
||||
"""Generate TODO comments for a file.
|
||||
|
||||
@@ -154,9 +138,7 @@ class CodeGenerator:
|
||||
lines.append(f"# TODO: {item}")
|
||||
|
||||
if language == "python":
|
||||
return f'"""{template_name} - {self.issue_data.title}"""\n\n' + "\n".join(
|
||||
lines
|
||||
)
|
||||
return f'"""{template_name} - {self.issue_data.title}"""\n\n' + "\n".join(lines)
|
||||
elif language in ("javascript",):
|
||||
return f"/**\n * {template_name} \n */\n\n" + "\n".join(lines)
|
||||
elif language == "go":
|
||||
@@ -199,7 +181,7 @@ class CodeGenerator:
|
||||
return f"{template_name}{self._get_extension(language)}"
|
||||
|
||||
def generate_single_file(
|
||||
self, filename: str, language: str, extra_context: Dict[str, Any] = None
|
||||
self, filename: str, language: str, extra_context: dict[str, Any] | None = None
|
||||
) -> str:
|
||||
"""Generate content for a single file.
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Data models for generators module."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""Project structure generation functionality."""
|
||||
|
||||
import os
|
||||
import stat
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from scaffoldforge.generators.code import CodeGenerator
|
||||
from scaffoldforge.generators.models import FileSpec
|
||||
@@ -26,8 +25,8 @@ class StructureGenerator:
|
||||
"""
|
||||
self.output_dir = output_dir or "./generated"
|
||||
self.preview = preview
|
||||
self.created_files: List[str] = []
|
||||
self.created_dirs: List[str] = []
|
||||
self.created_files: list[str] = []
|
||||
self.created_dirs: list[str] = []
|
||||
|
||||
def generate(
|
||||
self,
|
||||
@@ -67,9 +66,7 @@ class StructureGenerator:
|
||||
if self.preview:
|
||||
self._print_preview_summary()
|
||||
|
||||
def _create_directories(
|
||||
self, base_path: Path, issue_data: IssueData
|
||||
) -> None:
|
||||
def _create_directories(self, base_path: Path, issue_data: IssueData) -> None:
|
||||
"""Create project directories.
|
||||
|
||||
Args:
|
||||
@@ -115,7 +112,7 @@ class StructureGenerator:
|
||||
if self.preview:
|
||||
print(f"[PREVIEW] Would create file: {file_path}")
|
||||
if file_spec.executable:
|
||||
print(f" (executable)")
|
||||
print(" (executable)")
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -128,9 +125,7 @@ class StructureGenerator:
|
||||
except OSError as e:
|
||||
raise OSError(f"Failed to write file {file_path}: {e}")
|
||||
|
||||
def _create_readme(
|
||||
self, base_path: Path, issue_data: IssueData, project_name: str
|
||||
) -> None:
|
||||
def _create_readme(self, base_path: Path, issue_data: IssueData, project_name: str) -> None:
|
||||
"""Create README.md file.
|
||||
|
||||
Args:
|
||||
@@ -144,7 +139,7 @@ class StructureGenerator:
|
||||
|
||||
## Description
|
||||
|
||||
{issue_data.body[:500]}{'...' if len(issue_data.body) > 500 else ''}
|
||||
{issue_data.body[:500]}{"..." if len(issue_data.body) > 500 else ""}
|
||||
|
||||
**GitHub Issue:** #{issue_data.number}
|
||||
**Repository:** {issue_data.repository}
|
||||
@@ -152,11 +147,19 @@ class StructureGenerator:
|
||||
|
||||
## Requirements
|
||||
|
||||
{chr(10).join(f"- {req}" for req in issue_data.requirements) if issue_data.requirements else '- See GitHub issue for requirements'}
|
||||
{"
|
||||
".join(f"- {req}" for req in issue_data.requirements)
|
||||
if issue_data.requirements
|
||||
else "- See GitHub issue for requirements"
|
||||
}
|
||||
|
||||
## TODO Items
|
||||
|
||||
{chr(10).join(f"- [ ] {item}" for item in issue_data.get_todo_items()) if issue_data.get_todo_items() else '- No TODO items found'}
|
||||
{"
|
||||
".join(f"- [ ] {item}" for item in issue_data.get_todo_items())
|
||||
if issue_data.get_todo_items()
|
||||
else "- No TODO items found"
|
||||
}
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -184,9 +187,7 @@ MIT
|
||||
FileSpec(path="README.md", content=readme_content),
|
||||
)
|
||||
|
||||
def _create_gitignore(
|
||||
self, base_path: Path, project_name: str, language: str
|
||||
) -> None:
|
||||
def _create_gitignore(self, base_path: Path, project_name: str, language: str) -> None:
|
||||
"""Create .gitignore file based on language.
|
||||
|
||||
Args:
|
||||
@@ -248,6 +249,7 @@ Cargo.lock
|
||||
Sanitized name.
|
||||
"""
|
||||
import re
|
||||
|
||||
name = re.sub(r"[^a-zA-Z0-9\s_-]", "", name)
|
||||
name = re.sub(r"\s+", "-", name.strip())
|
||||
return name.lower()[:50] or "project"
|
||||
@@ -261,10 +263,10 @@ Cargo.lock
|
||||
print(f"Files: {len(self.created_files)}")
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
def get_created_files(self) -> List[str]:
|
||||
def get_created_files(self) -> list[str]:
|
||||
"""Get list of created files."""
|
||||
return self.created_files.copy()
|
||||
|
||||
def get_created_directories(self) -> List[str]:
|
||||
def get_created_directories(self) -> list[str]:
|
||||
"""Get list of created directories."""
|
||||
return self.created_dirs.copy()
|
||||
@@ -1,9 +1,7 @@
|
||||
"""Main entry point for ScaffoldForge CLI."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from scaffoldforge.cli import cli
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Parsers module for ScaffoldForge."""
|
||||
|
||||
from scaffoldforge.parsers.issue_parser import IssueParser, IssueData, ChecklistItem
|
||||
from scaffoldforge.parsers.issue_parser import ChecklistItem, IssueData, IssueParser
|
||||
|
||||
__all__ = ["IssueParser", "IssueData", "ChecklistItem"]
|
||||
@@ -4,11 +4,10 @@ import os
|
||||
import re
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
from github import Github
|
||||
from github.Issue import Issue
|
||||
from github.Label import Label
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -29,25 +28,25 @@ class IssueData:
|
||||
title: str
|
||||
body: str
|
||||
body_html: str
|
||||
labels: List[str]
|
||||
labels: list[str]
|
||||
state: str
|
||||
url: str
|
||||
repository: str
|
||||
author: str
|
||||
created_at: str
|
||||
updated_at: str
|
||||
checklist: List[ChecklistItem] = field(default_factory=list)
|
||||
requirements: List[str] = field(default_factory=list)
|
||||
acceptance_criteria: List[str] = field(default_factory=list)
|
||||
suggested_files: List[str] = field(default_factory=list)
|
||||
suggested_directories: List[str] = field(default_factory=list)
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
checklist: list[ChecklistItem] = field(default_factory=list)
|
||||
requirements: list[str] = field(default_factory=list)
|
||||
acceptance_criteria: list[str] = field(default_factory=list)
|
||||
suggested_files: list[str] = field(default_factory=list)
|
||||
suggested_directories: list[str] = field(default_factory=list)
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def get_todo_items(self) -> List[str]:
|
||||
def get_todo_items(self) -> list[str]:
|
||||
"""Get all todo items from checklist."""
|
||||
return [item.text for item in self.checklist if not item.completed]
|
||||
|
||||
def get_completed_items(self) -> List[str]:
|
||||
def get_completed_items(self) -> list[str]:
|
||||
"""Get completed checklist items."""
|
||||
return [item.text for item in self.checklist if item.completed]
|
||||
|
||||
@@ -109,6 +108,7 @@ class IssueParser:
|
||||
time.sleep(60 * (attempt + 1))
|
||||
else:
|
||||
raise
|
||||
raise ValueError(f"Failed to fetch issue after {max_retries} retries")
|
||||
|
||||
def _extract_issue_data(self, issue: Issue, repository: str) -> IssueData:
|
||||
"""Extract structured data from a GitHub issue.
|
||||
@@ -147,7 +147,7 @@ class IssueParser:
|
||||
suggested_directories=suggested_directories,
|
||||
)
|
||||
|
||||
def _parse_checklist(self, body: str) -> List[ChecklistItem]:
|
||||
def _parse_checklist(self, body: str) -> list[ChecklistItem]:
|
||||
"""Parse markdown checklist items from issue body.
|
||||
|
||||
Args:
|
||||
@@ -156,24 +156,21 @@ class IssueParser:
|
||||
Returns:
|
||||
List of ChecklistItem objects.
|
||||
"""
|
||||
checklist = []
|
||||
checklist: list[ChecklistItem] = []
|
||||
if not body:
|
||||
return checklist
|
||||
|
||||
lines = body.split("\n")
|
||||
in_checklist = False
|
||||
current_category = None
|
||||
current_category: str | None = None
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
category_match = re.match(r"^\s*(?:###|##|#)\s+(.+)", line)
|
||||
if category_match:
|
||||
current_category = category_match.group(1)
|
||||
in_checklist = False
|
||||
continue
|
||||
|
||||
checklist_match = re.match(r"^\s*[-*]\s+\[([ xX])\]\s+(.+)$", line)
|
||||
if checklist_match:
|
||||
in_checklist = True
|
||||
checked = checklist_match.group(1).lower() == "x"
|
||||
text = checklist_match.group(2).strip()
|
||||
checklist.append(
|
||||
@@ -187,7 +184,7 @@ class IssueParser:
|
||||
|
||||
return checklist
|
||||
|
||||
def _parse_requirements(self, body: str) -> List[str]:
|
||||
def _parse_requirements(self, body: str) -> list[str]:
|
||||
"""Parse requirements from issue body.
|
||||
|
||||
Args:
|
||||
@@ -196,7 +193,7 @@ class IssueParser:
|
||||
Returns:
|
||||
List of requirement strings.
|
||||
"""
|
||||
requirements = []
|
||||
requirements: list[str] = []
|
||||
if not body:
|
||||
return requirements
|
||||
|
||||
@@ -216,7 +213,7 @@ class IssueParser:
|
||||
|
||||
return requirements
|
||||
|
||||
def _parse_acceptance_criteria(self, body: str) -> List[str]:
|
||||
def _parse_acceptance_criteria(self, body: str) -> list[str]:
|
||||
"""Parse acceptance criteria from issue body.
|
||||
|
||||
Args:
|
||||
@@ -225,7 +222,7 @@ class IssueParser:
|
||||
Returns:
|
||||
List of acceptance criteria strings.
|
||||
"""
|
||||
criteria = []
|
||||
criteria: list[str] = []
|
||||
if not body:
|
||||
return criteria
|
||||
|
||||
@@ -245,7 +242,7 @@ class IssueParser:
|
||||
|
||||
return criteria
|
||||
|
||||
def _parse_file_paths(self, body: str) -> List[str]:
|
||||
def _parse_file_paths(self, body: str) -> list[str]:
|
||||
"""Parse suggested file paths from issue body.
|
||||
|
||||
Args:
|
||||
@@ -254,7 +251,7 @@ class IssueParser:
|
||||
Returns:
|
||||
List of file path strings.
|
||||
"""
|
||||
files = []
|
||||
files: list[str] = []
|
||||
if not body:
|
||||
return files
|
||||
|
||||
@@ -271,7 +268,7 @@ class IssueParser:
|
||||
|
||||
return list(set(files))
|
||||
|
||||
def _parse_directory_paths(self, body: str) -> List[str]:
|
||||
def _parse_directory_paths(self, body: str) -> list[str]:
|
||||
"""Parse suggested directory paths from issue body.
|
||||
|
||||
Args:
|
||||
@@ -280,7 +277,7 @@ class IssueParser:
|
||||
Returns:
|
||||
List of directory path strings.
|
||||
"""
|
||||
directories = []
|
||||
directories: list[str] = []
|
||||
if not body:
|
||||
return directories
|
||||
|
||||
|
||||
@@ -1,137 +1,54 @@
|
||||
"""Template engine for ScaffoldForge."""
|
||||
"""Template rendering functionality."""
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any
|
||||
|
||||
from jinja2 import (
|
||||
BaseLoader,
|
||||
Environment,
|
||||
FileSystemLoader,
|
||||
PackageLoader,
|
||||
TemplateSyntaxError,
|
||||
)
|
||||
from jinja2 import BaseLoader, Environment, FileSystemLoader, TemplateSyntaxError
|
||||
|
||||
from scaffoldforge.parsers import IssueData
|
||||
|
||||
|
||||
class TemplateEngine:
|
||||
"""Template rendering engine using Jinja2."""
|
||||
"""Engine for rendering templates."""
|
||||
|
||||
def __init__(self, template_dir: Optional[str] = None):
|
||||
def __init__(self, template_dir: str | None = None):
|
||||
"""Initialize the template engine.
|
||||
|
||||
Args:
|
||||
template_dir: Path to custom template directory.
|
||||
template_dir: Optional custom template directory.
|
||||
"""
|
||||
self.template_dir = template_dir
|
||||
self.env = self._create_environment()
|
||||
self._loaded_templates: Dict[str, Dict[str, Any]] = {}
|
||||
self._templates: dict[str, Any] = {}
|
||||
self._loaded: dict[str, dict[str, Any]] = {}
|
||||
|
||||
def _create_environment(self) -> Environment:
|
||||
"""Create Jinja2 environment with appropriate loader."""
|
||||
if self.template_dir and Path(self.template_dir).exists():
|
||||
loader = FileSystemLoader(self.template_dir)
|
||||
def load_templates(self, language: str, template_name: str = "default") -> None:
|
||||
"""Load templates for a specific language and template.
|
||||
|
||||
Args:
|
||||
language: Programming language.
|
||||
template_name: Name of the template set.
|
||||
"""
|
||||
if self.template_dir:
|
||||
base_dir = self.template_dir
|
||||
else:
|
||||
loader = PackageLoader("scaffoldforge", "templates")
|
||||
base_dir = str(Path(__file__).parent)
|
||||
|
||||
return Environment(
|
||||
loader=loader,
|
||||
autoescape=True,
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
)
|
||||
template_path = Path(base_dir) / language / template_name
|
||||
|
||||
def load_templates(
|
||||
self, language: str, template_name: str = "default"
|
||||
) -> Dict[str, str]:
|
||||
"""Load templates for a specific language and template type.
|
||||
if not template_path.exists():
|
||||
return
|
||||
|
||||
Args:
|
||||
language: Programming language.
|
||||
template_name: Template variant name.
|
||||
loader = FileSystemLoader(str(template_path))
|
||||
env = Environment(loader=loader, autoescape=True)
|
||||
|
||||
Returns:
|
||||
Dictionary mapping template names to rendered content.
|
||||
"""
|
||||
key = f"{language}/{template_name}"
|
||||
if key in self._loaded_templates:
|
||||
return self._loaded_templates[key]
|
||||
self._templates[language] = env
|
||||
self._loaded[language] = {
|
||||
"path": str(template_path),
|
||||
"templates": list(env.list_templates()),
|
||||
}
|
||||
|
||||
templates = {}
|
||||
template_dir = Path(__file__) / language / template_name
|
||||
|
||||
if template_dir.exists():
|
||||
for template_file in template_dir.glob("*.j2"):
|
||||
template_name_only = template_file.stem
|
||||
templates[template_name_only] = self._load_template(
|
||||
language, template_name, template_file.name
|
||||
)
|
||||
|
||||
self._loaded_templates[key] = templates
|
||||
return templates
|
||||
|
||||
def _load_template(
|
||||
self, language: str, template_type: str, filename: str
|
||||
) -> str:
|
||||
"""Load a single template file.
|
||||
|
||||
Args:
|
||||
language: Programming language.
|
||||
template_type: Template variant.
|
||||
filename: Template filename.
|
||||
|
||||
Returns:
|
||||
Template content as string.
|
||||
"""
|
||||
template_path = f"{language}/{template_type}/{filename}"
|
||||
try:
|
||||
template = self.env.get_template(template_path)
|
||||
return template.filename
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def render(
|
||||
self,
|
||||
template_name: str,
|
||||
context: Dict[str, Any],
|
||||
language: str = "python",
|
||||
) -> str:
|
||||
"""Render a template with the given context.
|
||||
|
||||
Args:
|
||||
template_name: Name of the template to render.
|
||||
context: Dictionary of variables to pass to the template.
|
||||
language: Programming language for template lookup.
|
||||
|
||||
Returns:
|
||||
Rendered template string.
|
||||
"""
|
||||
try:
|
||||
full_name = f"{language}/{template_name}"
|
||||
template = self.env.get_template(f"{full_name}.j2")
|
||||
return template.render(**context)
|
||||
except TemplateSyntaxError as e:
|
||||
raise ValueError(f"Template syntax error in {template_name}: {e}")
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to render template {template_name}: {e}")
|
||||
|
||||
def render_string(self, template_content: str, context: Dict[str, Any]) -> str:
|
||||
"""Render a template string directly.
|
||||
|
||||
Args:
|
||||
template_content: Template content as string.
|
||||
context: Dictionary of variables.
|
||||
|
||||
Returns:
|
||||
Rendered string.
|
||||
"""
|
||||
template = self.env.from_string(template_content)
|
||||
return template.render(**context)
|
||||
|
||||
@staticmethod
|
||||
def list_available_templates(language: str) -> List[str]:
|
||||
def list_available_templates(self, language: str) -> list[str]:
|
||||
"""List available templates for a language.
|
||||
|
||||
Args:
|
||||
@@ -140,85 +57,105 @@ class TemplateEngine:
|
||||
Returns:
|
||||
List of template names.
|
||||
"""
|
||||
templates_dir = Path(__file__).parent / language
|
||||
if not templates_dir.exists():
|
||||
if self.template_dir:
|
||||
base_dir = self.template_dir
|
||||
else:
|
||||
base_dir = str(Path(__file__).parent)
|
||||
|
||||
template_path = Path(base_dir) / language
|
||||
|
||||
if not template_path.exists():
|
||||
return []
|
||||
|
||||
templates = []
|
||||
for item in templates_dir.iterdir():
|
||||
if item.is_dir():
|
||||
templates.append(item.name)
|
||||
return sorted(templates)
|
||||
return [
|
||||
d.name
|
||||
for d in template_path.iterdir()
|
||||
if d.is_dir() and not d.name.startswith("_")
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def list_available_languages() -> List[str]:
|
||||
"""List all available programming languages.
|
||||
def render(
|
||||
self, template_name: str, context: dict[str, Any], language: str
|
||||
) -> str:
|
||||
"""Render a template with context.
|
||||
|
||||
Args:
|
||||
template_name: Name of the template file.
|
||||
context: Context dictionary for template rendering.
|
||||
language: Programming language.
|
||||
|
||||
Returns:
|
||||
List of language identifiers.
|
||||
Rendered template string.
|
||||
"""
|
||||
templates_dir = Path(__file__).parent
|
||||
if not templates_dir.exists():
|
||||
return []
|
||||
if language not in self._templates:
|
||||
self.load_templates(language)
|
||||
|
||||
languages = []
|
||||
for item in templates_dir.iterdir():
|
||||
if item.is_dir() and not item.name.startswith("_"):
|
||||
languages.append(item.name)
|
||||
return sorted(languages)
|
||||
if language not in self._templates:
|
||||
raise ValueError(f"Templates not loaded for language: {language}")
|
||||
|
||||
def get_template_context(self, issue_data: IssueData) -> Dict[str, Any]:
|
||||
env = self._templates[language]
|
||||
template_file = f"{template_name}.j2"
|
||||
|
||||
if template_file not in env.list_templates():
|
||||
raise ValueError(f"Template not found: {template_file}")
|
||||
|
||||
template = env.get_template(template_file)
|
||||
return template.render(**context)
|
||||
|
||||
def get_template_context(self, issue_data: IssueData) -> dict[str, Any]:
|
||||
"""Generate template context from issue data.
|
||||
|
||||
Args:
|
||||
issue_data: IssueData object.
|
||||
|
||||
Returns:
|
||||
Dictionary of template variables.
|
||||
Context dictionary for templates.
|
||||
"""
|
||||
project_name = self._generate_project_name(issue_data)
|
||||
project_name = self._sanitize_name(issue_data.title)
|
||||
|
||||
return {
|
||||
context = {
|
||||
"project_name": project_name,
|
||||
"project_name_kebab": self._to_kebab_case(project_name),
|
||||
"project_name_snake": self._to_snake_case(project_name),
|
||||
"project_name_pascal": self._to_pascal_case(project_name),
|
||||
"project_name_kebab": project_name.lower().replace("_", "-"),
|
||||
"project_name_snake": project_name.lower().replace("-", "_"),
|
||||
"project_name_pascal": "".join(
|
||||
word.capitalize() for word in re.findall(r"[a-zA-Z]+", project_name)
|
||||
),
|
||||
"issue_number": issue_data.number,
|
||||
"issue_title": issue_data.title,
|
||||
"issue_url": issue_data.url,
|
||||
"repository": issue_data.repository,
|
||||
"author": issue_data.author,
|
||||
"created_date": issue_data.created_at[:10] if issue_data.created_at else "",
|
||||
"created_date": issue_data.created_at,
|
||||
"todo_items": issue_data.get_todo_items(),
|
||||
"completed_items": issue_data.get_completed_items(),
|
||||
"requirements": issue_data.requirements,
|
||||
"acceptance_criteria": issue_data.acceptance_criteria,
|
||||
"checklist": issue_data.checklist,
|
||||
}
|
||||
|
||||
def _generate_project_name(self, issue_data: IssueData) -> str:
|
||||
"""Generate a project name from issue title.
|
||||
return context
|
||||
|
||||
def _sanitize_name(self, name: str) -> str:
|
||||
"""Sanitize a string for use as a project name.
|
||||
|
||||
Args:
|
||||
issue_data: IssueData object.
|
||||
name: Original name.
|
||||
|
||||
Returns:
|
||||
Project name string.
|
||||
Sanitized name.
|
||||
"""
|
||||
title = issue_data.title
|
||||
title = re.sub(r"[^a-zA-Z0-9\s]", "", title)
|
||||
title = re.sub(r"\s+", "_", title.strip())
|
||||
return title.lower()[:50]
|
||||
import re
|
||||
|
||||
def _to_kebab_case(self, text: str) -> str:
|
||||
"""Convert text to kebab-case."""
|
||||
return re.sub(r"[^a-zA-Z0-9]+", "-", text).strip("-").lower()
|
||||
name = re.sub(r"[^a-zA-Z0-9\s]", "", name)
|
||||
name = re.sub(r"\s+", " ", name.strip())
|
||||
return name.replace(" ", "_")
|
||||
|
||||
def _to_snake_case(self, text: str) -> str:
|
||||
"""Convert text to snake_case."""
|
||||
return re.sub(r"[^a-zA-Z0-9]+", "_", text).strip("_").lower()
|
||||
|
||||
def _to_pascal_case(self, text: str) -> str:
|
||||
"""Convert text to PascalCase."""
|
||||
words = re.findall(r"[a-zA-Z0-9]+", text)
|
||||
return "".join(word.title() for word in words)
|
||||
def get_template_engine(template_dir: str | None = None) -> TemplateEngine:
|
||||
"""Get a template engine instance.
|
||||
|
||||
Args:
|
||||
template_dir: Optional custom template directory.
|
||||
|
||||
Returns:
|
||||
TemplateEngine instance.
|
||||
"""
|
||||
return TemplateEngine(template_dir)
|
||||
1
scaffoldforge/templates/go/__init__.py
Normal file
1
scaffoldforge/templates/go/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Go template files."""
|
||||
@@ -1,16 +1,16 @@
|
||||
// Main entry point for {{ project_name }}
|
||||
// Main entry point for {{ project_name }}.
|
||||
// Generated from GitHub Issue #{{ issue_number }}: {{ issue_title }}
|
||||
// URL: {{ issue_url }}
|
||||
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
{% for item in todo_items %}
|
||||
// TODO #{{ loop.index }}: {{ item }}
|
||||
{% endfor %}
|
||||
|
||||
func main() {
|
||||
// TODO: Implement main functionality
|
||||
// {% for item in todo_items %}
|
||||
// TODO #{{ loop.index }}: {{ item }}
|
||||
// {% endfor %}
|
||||
|
||||
fmt.Println("{{ project_name }} is running")
|
||||
fmt.Printf("Repository: {{ repository }}\n")
|
||||
fmt.Printf("Issue: {{ issue_number }}\n")
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
// Utility functions for {{ project_name }}
|
||||
// Utility functions for {{ project_name }}.
|
||||
// Generated from GitHub Issue #{{ issue_number }}
|
||||
|
||||
package main
|
||||
|
||||
// TODO: Implement helper function
|
||||
func HelperFunction() {
|
||||
// TODO: Implement
|
||||
}
|
||||
// TODO: Implement utility functions
|
||||
|
||||
1
scaffoldforge/templates/javascript/__init__.py
Normal file
1
scaffoldforge/templates/javascript/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""JavaScript template files."""
|
||||
@@ -1,12 +1,15 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"es2021": true
|
||||
"es2022": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {}
|
||||
"rules": {
|
||||
"no-console": "warn"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
/**
|
||||
* Main entry point for {{ project_name }}
|
||||
* Main entry point for {{ project_name }}.
|
||||
* Generated from GitHub Issue #{{ issue_number }}: {{ issue_title }}
|
||||
*/
|
||||
|
||||
{% for item in todo_items %}
|
||||
// TODO: Implement main functionality
|
||||
// {% for item in todo_items %}
|
||||
// TODO #{{ loop.index }}: {{ item }}
|
||||
{% endfor %}
|
||||
// {% endfor %}
|
||||
|
||||
console.log("{{ project_name }} is running");
|
||||
console.log("Repository: {{ repository }}");
|
||||
console.log("Issue: {{ issue_number }}");
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
/**
|
||||
* Utility functions for {{ project_name }}
|
||||
* Utility functions for {{ project_name }}.
|
||||
* Generated from GitHub Issue #{{ issue_number }}
|
||||
*/
|
||||
|
||||
/**
|
||||
* TODO: Implement helper function
|
||||
*/
|
||||
export function helperFunction() {
|
||||
// TODO: Implement
|
||||
}
|
||||
// TODO: Implement utility functions
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT"
|
||||
"author": "{{ author }}",
|
||||
"license": "MIT",
|
||||
"dependencies": {},
|
||||
"devDependencies": {}
|
||||
}
|
||||
|
||||
1
scaffoldforge/templates/python/__init__.py
Normal file
1
scaffoldforge/templates/python/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Python template files."""
|
||||
@@ -1,4 +1,4 @@
|
||||
"""CLI interface for {{ project_name }}.
|
||||
"""CLI module for {{ project_name }}.
|
||||
|
||||
Generated from GitHub Issue #{{ issue_number }}
|
||||
"""
|
||||
@@ -7,7 +7,10 @@ import click
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("--option", help="TODO: Describe option")
|
||||
def cli(option: str):
|
||||
def cli():
|
||||
"""TODO: Implement CLI command."""
|
||||
click.echo("{{ project_name }} CLI")
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
Generated from GitHub Issue #{{ issue_number }}
|
||||
"""
|
||||
|
||||
|
||||
class DataModel:
|
||||
"""TODO: Implement data model class."""
|
||||
pass
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class AnotherModel:
|
||||
"""TODO: Implement another model class."""
|
||||
pass
|
||||
@dataclass
|
||||
class Item:
|
||||
"""TODO: Define item model."""
|
||||
name: str
|
||||
description: str = ""
|
||||
|
||||
|
||||
# TODO: Add more models based on requirements
|
||||
|
||||
@@ -9,6 +9,4 @@ def helper_function():
|
||||
pass
|
||||
|
||||
|
||||
def another_helper():
|
||||
"""TODO: Implement another helper."""
|
||||
pass
|
||||
# TODO: Add more utility functions based on requirements
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# {{ project_name }} requirements
|
||||
# Generated from GitHub Issue #{{ issue_number }}
|
||||
|
||||
# Core dependencies
|
||||
click>=8.1.7
|
||||
pygithub>=2.3.0
|
||||
jinja2>=3.1.4
|
||||
PyYAML>=6.0.2
|
||||
|
||||
# TODO: Add more dependencies based on requirements
|
||||
|
||||
1
scaffoldforge/templates/rust/__init__.py
Normal file
1
scaffoldforge/templates/rust/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Rust template files."""
|
||||
@@ -1,10 +1,6 @@
|
||||
// Library code for {{ project_name }}
|
||||
// Library code for {{ project_name }}.
|
||||
// Generated from GitHub Issue #{{ issue_number }}
|
||||
|
||||
{% for item in todo_items %}
|
||||
// {% for item in todo_items %}
|
||||
// TODO #{{ loop.index }}: {{ item }}
|
||||
{% endfor %}
|
||||
|
||||
pub fn helper_function() {
|
||||
// TODO: Implement
|
||||
}
|
||||
// {% endfor %}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// Main entry point for {{ project_name }}
|
||||
// Main entry point for {{ project_name }}.
|
||||
// Generated from GitHub Issue #{{ issue_number }}: {{ issue_title }}
|
||||
// URL: {{ issue_url }}
|
||||
|
||||
{% for item in todo_items %}
|
||||
// {% for item in todo_items %}
|
||||
// TODO #{{ loop.index }}: {{ item }}
|
||||
{% endfor %}
|
||||
// {% endfor %}
|
||||
|
||||
fn main() {
|
||||
println!("{{ project_name }} is running");
|
||||
|
||||
10
setup.py
10
setup.py
@@ -1,9 +1,14 @@
|
||||
from setuptools import setup, find_packages
|
||||
#!/usr/bin/env python
|
||||
"""Setup script for ScaffoldForge."""
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
setup(
|
||||
name="scaffoldforge",
|
||||
version="0.1.0",
|
||||
packages=find_packages(),
|
||||
py_modules=["scaffoldforge"],
|
||||
packages=find_packages(exclude=["tests*"]),
|
||||
python_requires=">=3.9",
|
||||
install_requires=[
|
||||
"click>=8.1.7",
|
||||
"pygithub>=2.3.0",
|
||||
@@ -23,5 +28,4 @@ setup(
|
||||
"scaffoldforge=scaffoldforge.main:main",
|
||||
],
|
||||
},
|
||||
python_requires=">=3.9",
|
||||
)
|
||||
@@ -1,17 +1,15 @@
|
||||
"""Integration tests for ScaffoldForge."""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from scaffoldforge.cli import cli
|
||||
from scaffoldforge.parsers import IssueParser, IssueData, ChecklistItem
|
||||
from scaffoldforge.generators import CodeGenerator, StructureGenerator
|
||||
from scaffoldforge.parsers import ChecklistItem, IssueData
|
||||
from scaffoldforge.templates import TemplateEngine
|
||||
from scaffoldforge.generators import StructureGenerator, CodeGenerator
|
||||
|
||||
|
||||
class TestFullWorkflow:
|
||||
@@ -207,9 +205,9 @@ class TestFullWorkflow:
|
||||
runner = CliRunner()
|
||||
|
||||
with patch('scaffoldforge.cli.commands.IssueParser') as mock_parser, \
|
||||
patch('scaffoldforge.cli.commands.TemplateEngine') as mock_tpl_engine, \
|
||||
patch('scaffoldforge.cli.commands.StructureGenerator') as mock_struct_gen, \
|
||||
patch('scaffoldforge.cli.commands.CodeGenerator') as mock_code_gen:
|
||||
patch('scaffoldforge.cli.commands.TemplateEngine') as _mock_tpl_engine, \
|
||||
patch('scaffoldforge.cli.commands.StructureGenerator') as _mock_struct_gen, \
|
||||
patch('scaffoldforge.cli.commands.CodeGenerator') as _mock_code_gen:
|
||||
|
||||
mock_issue_data = Mock()
|
||||
mock_issue_data.number = 1
|
||||
|
||||
86
tests/test_extractors.py
Normal file
86
tests/test_extractors.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""Tests for field extraction module."""
|
||||
|
||||
from cmdparse.extractors import extract_array_index, extract_fields, flatten_dict, get_nested_value
|
||||
|
||||
|
||||
class TestGetNestedValue:
|
||||
def test_simple_dict_access(self):
|
||||
data = {"name": "John", "age": 30}
|
||||
assert get_nested_value(data, "name") == "John"
|
||||
assert get_nested_value(data, "age") == 30
|
||||
|
||||
def test_nested_dict_access(self):
|
||||
data = {"user": {"address": {"city": "NYC"}}}
|
||||
assert get_nested_value(data, "user.address.city") == "NYC"
|
||||
|
||||
def test_list_index_access(self):
|
||||
data = {"items": ["a", "b", "c"]}
|
||||
assert get_nested_value(data, "items.0") == "a"
|
||||
assert get_nested_value(data, "items.1") == "b"
|
||||
|
||||
def test_missing_key_returns_none(self):
|
||||
data = {"name": "John"}
|
||||
assert get_nested_value(data, "age") is None
|
||||
|
||||
def test_none_data_returns_none(self):
|
||||
assert get_nested_value(None, "name") is None
|
||||
|
||||
|
||||
class TestExtractArrayIndex:
|
||||
def test_with_array_index(self):
|
||||
base, index, rest = extract_array_index("items[0].name")
|
||||
assert base == "items"
|
||||
assert index == 0
|
||||
assert rest == "name"
|
||||
|
||||
def test_without_array_index(self):
|
||||
base, index, rest = extract_array_index("name")
|
||||
assert base == "name"
|
||||
assert index is None
|
||||
assert rest is None
|
||||
|
||||
|
||||
class TestExtractFields:
|
||||
def test_extract_single_field(self):
|
||||
data = [{"name": "John", "age": 30, "city": "NYC"}]
|
||||
result = extract_fields(data, ["name"])
|
||||
assert result == [{"name": "John"}]
|
||||
|
||||
def test_extract_multiple_fields(self):
|
||||
data = [{"name": "John", "age": 30, "city": "NYC"}]
|
||||
result = extract_fields(data, ["name", "city"])
|
||||
assert result == [{"name": "John", "city": "NYC"}]
|
||||
|
||||
def test_extract_nested_fields(self):
|
||||
data = [{"user": {"name": "John", "age": 30}}]
|
||||
result = extract_fields(data, ["user.name"])
|
||||
assert result == [{"user.name": "John"}]
|
||||
|
||||
def test_extract_from_multiple_rows(self):
|
||||
data = [{"name": "John", "age": 30}, {"name": "Jane", "age": 25}]
|
||||
result = extract_fields(data, ["name"])
|
||||
assert result == [{"name": "John"}, {"name": "Jane"}]
|
||||
|
||||
def test_empty_fields_returns_original(self):
|
||||
data = [{"name": "John", "age": 30}]
|
||||
result = extract_fields(data, [])
|
||||
assert result == data
|
||||
|
||||
def test_empty_data_returns_empty(self):
|
||||
result = extract_fields([], ["name"])
|
||||
assert result == []
|
||||
|
||||
|
||||
class TestFlattenDict:
|
||||
def test_flatten_simple_dict(self):
|
||||
d = {"name": "John", "age": 30}
|
||||
result = flatten_dict(d)
|
||||
assert result == {"name": "John", "age": 30}
|
||||
|
||||
def test_flatten_nested_dict(self):
|
||||
d = {"user": {"name": "John", "address": {"city": "NYC"}}}
|
||||
result = flatten_dict(d)
|
||||
assert "user.name" in result
|
||||
assert "user.address.city" in result
|
||||
assert result["user.name"] == "John"
|
||||
assert result["user.address.city"] == "NYC"
|
||||
105
tests/test_formatters.py
Normal file
105
tests/test_formatters.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""Tests for output formatting module."""
|
||||
|
||||
from cmdparse.formatters import format_csv, format_data, format_json, format_raw, format_yaml
|
||||
|
||||
|
||||
class TestFormatJson:
|
||||
def test_format_simple_list(self):
|
||||
data = [{"name": "John", "age": 30}]
|
||||
result = format_json(data)
|
||||
assert "John" in result
|
||||
assert "30" in result
|
||||
|
||||
def test_format_empty_list(self):
|
||||
result = format_json([])
|
||||
assert result == "[]"
|
||||
|
||||
def test_pretty_format(self):
|
||||
data = [{"name": "John"}]
|
||||
result = format_json(data, pretty=True)
|
||||
assert "\n" in result
|
||||
|
||||
def test_compact_format(self):
|
||||
data = [{"name": "John"}]
|
||||
result = format_json(data, pretty=False)
|
||||
assert "\n" not in result
|
||||
|
||||
|
||||
class TestFormatYaml:
|
||||
def test_format_simple_list(self):
|
||||
data = [{"name": "John", "age": 30}]
|
||||
result = format_yaml(data)
|
||||
assert "John" in result
|
||||
assert "age" in result
|
||||
|
||||
def test_format_empty_list(self):
|
||||
result = format_yaml([])
|
||||
assert result == "[]\n"
|
||||
|
||||
def test_format_nested_structure(self):
|
||||
data = [{"user": {"name": "John", "tags": ["a", "b"]}}]
|
||||
result = format_yaml(data)
|
||||
assert "John" in result
|
||||
|
||||
|
||||
class TestFormatCsv:
|
||||
def test_format_simple_list(self):
|
||||
data = [{"name": "John", "age": 30}]
|
||||
result = format_csv(data)
|
||||
assert "name" in result
|
||||
assert "John" in result
|
||||
assert "age" in result
|
||||
|
||||
def test_format_empty_list(self):
|
||||
result = format_csv([])
|
||||
assert result == ""
|
||||
|
||||
def test_format_multiple_rows(self):
|
||||
data = [{"name": "John", "age": 30}, {"name": "Jane", "age": 25}]
|
||||
result = format_csv(data)
|
||||
assert "John" in result
|
||||
assert "Jane" in result
|
||||
|
||||
|
||||
class TestFormatRaw:
|
||||
def test_format_simple_list(self):
|
||||
data = [{"name": "John", "age": 30}]
|
||||
result = format_raw(data)
|
||||
assert "name: John" in result
|
||||
assert "age: 30" in result
|
||||
|
||||
def test_format_empty_list(self):
|
||||
result = format_raw([])
|
||||
assert result == ""
|
||||
|
||||
|
||||
class TestFormatData:
|
||||
def test_json_format(self):
|
||||
data = [{"name": "John"}]
|
||||
result = format_data(data, "json")
|
||||
assert "John" in result
|
||||
|
||||
def test_yaml_format(self):
|
||||
data = [{"name": "John"}]
|
||||
result = format_data(data, "yaml")
|
||||
assert "John" in result
|
||||
|
||||
def test_csv_format(self):
|
||||
data = [{"name": "John"}]
|
||||
result = format_data(data, "csv")
|
||||
assert "name" in result
|
||||
|
||||
def test_raw_format(self):
|
||||
data = [{"name": "John"}]
|
||||
result = format_data(data, "raw")
|
||||
assert "John" in result
|
||||
|
||||
def test_default_to_json(self):
|
||||
data = [{"name": "John"}]
|
||||
result = format_data(data, "")
|
||||
assert "John" in result
|
||||
|
||||
def test_unknown_format_defaults_to_json(self):
|
||||
data = [{"name": "John"}]
|
||||
result = format_data(data, "unknown")
|
||||
assert "John" in result
|
||||
113
tests/test_integration.py
Normal file
113
tests/test_integration.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""End-to-end integration tests for cmdparse CLI."""
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from cmdparse.cli import main
|
||||
|
||||
|
||||
class TestCLI:
|
||||
def test_parse_table_to_json(self):
|
||||
runner = CliRunner()
|
||||
input_text = """NAME IMAGE STATUS
|
||||
nginx nginx:1 running
|
||||
redis redis:3 stopped"""
|
||||
result = runner.invoke(main, ["-o", "json", "-q"], input=input_text)
|
||||
assert result.exit_code == 0
|
||||
assert "nginx" in result.output
|
||||
|
||||
def test_parse_key_value_to_yaml(self):
|
||||
runner = CliRunner()
|
||||
input_text = """name: John
|
||||
age: 30
|
||||
city: NYC"""
|
||||
result = runner.invoke(main, ["-o", "yaml", "-q"], input=input_text)
|
||||
assert result.exit_code == 0
|
||||
assert "John" in result.output
|
||||
assert "name" in result.output
|
||||
|
||||
def test_parse_csv_to_csv(self):
|
||||
runner = CliRunner()
|
||||
input_text = """name,age,city
|
||||
John,30,NYC"""
|
||||
result = runner.invoke(main, ["-o", "csv", "-q"], input=input_text)
|
||||
assert result.exit_code == 0
|
||||
assert "John" in result.output
|
||||
|
||||
def test_field_extraction(self):
|
||||
runner = CliRunner()
|
||||
input_text = """NAME IMAGE STATUS
|
||||
nginx nginx:1 running
|
||||
redis redis:3 stopped"""
|
||||
result = runner.invoke(
|
||||
main, ["-o", "json", "-q", "-e", "NAME", "-e", "STATUS"], input=input_text
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
output = result.output
|
||||
assert "nginx" in output
|
||||
assert "running" in output
|
||||
|
||||
def test_no_input_error(self):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["-q"], input="")
|
||||
assert result.exit_code == 1
|
||||
assert "error" in result.output.lower() or "input" in result.output.lower()
|
||||
|
||||
def test_auto_format_detection(self):
|
||||
runner = CliRunner()
|
||||
input_text = """name: John
|
||||
age: 30"""
|
||||
result = runner.invoke(main, ["-f", "auto", "-o", "json", "-q"], input=input_text)
|
||||
assert result.exit_code == 0
|
||||
assert "John" in result.output
|
||||
|
||||
def test_help_option(self):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "--output" in result.output
|
||||
assert "--field" in result.output
|
||||
assert "--config" in result.output
|
||||
|
||||
def test_parse_raw_text(self):
|
||||
runner = CliRunner()
|
||||
input_text = """Some random text line 1
|
||||
Another line here
|
||||
Third line"""
|
||||
result = runner.invoke(main, ["-o", "json", "-q"], input=input_text)
|
||||
assert result.exit_code == 0
|
||||
assert "line" in result.output.lower() or "1" in result.output
|
||||
|
||||
def test_parse_delimited_semicolon(self):
|
||||
runner = CliRunner()
|
||||
input_text = """name;age;city
|
||||
John;30;NYC"""
|
||||
result = runner.invoke(main, ["-o", "json", "-q"], input=input_text)
|
||||
assert result.exit_code == 0
|
||||
assert "John" in result.output
|
||||
|
||||
|
||||
class TestIntegrationScenarios:
|
||||
def test_docker_ps_style_output(self):
|
||||
runner = CliRunner()
|
||||
input_text = """CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
abc123 nginx:1 "nginx" 1h ago Up 80/tcp nginx"""
|
||||
result = runner.invoke(main, ["-o", "json", "-q"], input=input_text)
|
||||
assert result.exit_code == 0
|
||||
assert "nginx" in result.output
|
||||
|
||||
def test_df_style_output(self):
|
||||
runner = CliRunner()
|
||||
input_text = """Filesystem Size Used Avail Use% Mounted on
|
||||
/dev/sda1 100G 50G 50G 50% /"""
|
||||
result = runner.invoke(main, ["-o", "json", "-q"], input=input_text)
|
||||
assert result.exit_code == 0
|
||||
assert "/dev/sda1" in result.output
|
||||
|
||||
def test_key_value_with_special_chars(self):
|
||||
runner = CliRunner()
|
||||
input_text = """DATABASE_URL=postgresql://user:pass@localhost:5432/db
|
||||
API_KEY=abc123xyz
|
||||
DEBUG=true"""
|
||||
result = runner.invoke(main, ["-o", "yaml", "-q"], input=input_text)
|
||||
assert result.exit_code == 0
|
||||
assert "DATABASE_URL" in result.output
|
||||
50
tests/test_parser.py
Normal file
50
tests/test_parser.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""Tests for pattern detection module."""
|
||||
|
||||
from cmdparse.patterns import detect_pattern_type
|
||||
|
||||
|
||||
class TestDetectPatternType:
|
||||
def test_empty_input_returns_empty(self):
|
||||
assert detect_pattern_type("") == "empty"
|
||||
assert detect_pattern_type(" ") == "empty"
|
||||
assert detect_pattern_type("\n\n") == "empty"
|
||||
|
||||
def test_table_detection(self):
|
||||
table_input = """NAME IMAGE STATUS
|
||||
nginx nginx:1 running
|
||||
redis redis:3 stopped"""
|
||||
result = detect_pattern_type(table_input)
|
||||
assert result in ["table", "key_value_block"]
|
||||
|
||||
def test_key_value_colon_detection(self):
|
||||
kv_input = """name: John
|
||||
age: 30
|
||||
city: NYC"""
|
||||
result = detect_pattern_type(kv_input)
|
||||
assert result == "key_value_colon"
|
||||
|
||||
def test_key_value_equals_detection(self):
|
||||
kv_input = """name=John
|
||||
age=30
|
||||
city=NYC"""
|
||||
result = detect_pattern_type(kv_input)
|
||||
assert result == "key_value_equals"
|
||||
|
||||
def test_delimited_comma_detection(self):
|
||||
csv_input = """name,age,city
|
||||
John,30,NYC
|
||||
Jane,25,LA"""
|
||||
result = detect_pattern_type(csv_input)
|
||||
assert result == "delimited_comma"
|
||||
|
||||
def test_delimited_tab_detection(self):
|
||||
tsv_input = """name\tage\tcity
|
||||
John\t30\tNYC"""
|
||||
result = detect_pattern_type(tsv_input)
|
||||
assert result == "delimited_tab"
|
||||
|
||||
def test_raw_text_detection(self):
|
||||
raw_input = """This is just some random text
|
||||
Without any particular structure"""
|
||||
result = detect_pattern_type(raw_input)
|
||||
assert result in ["raw", "table", "key_value_block"]
|
||||
@@ -1,8 +1,9 @@
|
||||
"""Unit tests for the CLI module."""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
|
||||
from scaffoldforge.cli import cli
|
||||
from scaffoldforge.cli.commands import parse_github_url
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
"""Unit tests for the generators module."""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from scaffoldforge.generators import StructureGenerator, CodeGenerator
|
||||
from scaffoldforge.generators import CodeGenerator, StructureGenerator
|
||||
from scaffoldforge.generators.structure import FileSpec
|
||||
from scaffoldforge.parsers import IssueData, ChecklistItem
|
||||
from scaffoldforge.parsers import ChecklistItem, IssueData
|
||||
from scaffoldforge.templates import TemplateEngine
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""Unit tests for the parsers module."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
from scaffoldforge.parsers import IssueParser, IssueData, ChecklistItem
|
||||
from scaffoldforge.parsers import ChecklistItem, IssueData, IssueParser
|
||||
|
||||
|
||||
class TestChecklistItem:
|
||||
@@ -109,13 +108,13 @@ class TestIssueParser:
|
||||
@patch('scaffoldforge.parsers.issue_parser.Github')
|
||||
def test_parser_initialization_without_token(self, mock_github):
|
||||
"""Test parser initialization without token."""
|
||||
parser = IssueParser()
|
||||
IssueParser()
|
||||
mock_github.assert_called_once()
|
||||
|
||||
@patch('scaffoldforge.parsers.issue_parser.Github')
|
||||
def test_parser_initialization_with_token(self, mock_github):
|
||||
"""Test parser initialization with token."""
|
||||
parser = IssueParser(token="test_token")
|
||||
IssueParser(token="test_token")
|
||||
mock_github.assert_called_once_with("test_token")
|
||||
|
||||
def test_parse_checklist_simple(self):
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
"""Unit tests for the templates module."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from pathlib import Path
|
||||
|
||||
from scaffoldforge.parsers import ChecklistItem, IssueData
|
||||
from scaffoldforge.templates import TemplateEngine
|
||||
from scaffoldforge.parsers import IssueData, ChecklistItem
|
||||
|
||||
|
||||
class TestTemplateEngine:
|
||||
|
||||
Reference in New Issue
Block a user