- Replace walrus operator (:=) with traditional assignment in config.py - Replace datetime.fromisoformat() with strptime() for Python 3.6 - Replace subprocess capture_output and text params with PIPE and universal_newlines - All tests now pass on Python 3.6.9 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
219 lines
6.8 KiB
Python
219 lines
6.8 KiB
Python
"""
|
|
Workspace management for tddai.
|
|
"""
|
|
|
|
import json
|
|
import shutil
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
from typing import Optional, Dict, Any
|
|
|
|
from .config import get_config
|
|
from .exceptions import WorkspaceError
|
|
|
|
|
|
class WorkspaceStatus(Enum):
|
|
"""Status of workspace."""
|
|
CLEAN = "clean"
|
|
ACTIVE = "active"
|
|
DIRTY = "dirty"
|
|
|
|
|
|
@dataclass
|
|
class Workspace:
|
|
"""Represents a TDD workspace for an issue."""
|
|
|
|
issue_number: int
|
|
issue_title: str
|
|
issue_body: str
|
|
issue_state: str
|
|
created_at: datetime
|
|
workspace_dir: Path
|
|
|
|
@property
|
|
def issue_dir(self) -> Path:
|
|
"""Get the issue-specific directory."""
|
|
return self.workspace_dir / f"issue_{self.issue_number}"
|
|
|
|
@property
|
|
def tests_dir(self) -> Path:
|
|
"""Get the tests directory for this issue."""
|
|
return self.issue_dir / "tests"
|
|
|
|
@property
|
|
def requirements_file(self) -> Path:
|
|
"""Get the requirements file path."""
|
|
return self.issue_dir / "requirements.md"
|
|
|
|
@property
|
|
def test_plan_file(self) -> Path:
|
|
"""Get the test plan file path."""
|
|
return self.issue_dir / "test_plan.md"
|
|
|
|
|
|
class WorkspaceManager:
|
|
"""Manages TDD workspaces for issues."""
|
|
|
|
def __init__(self, config=None):
|
|
self.config = config or get_config()
|
|
|
|
def get_status(self) -> WorkspaceStatus:
|
|
"""Get current workspace status."""
|
|
if not self.config.workspace_dir.exists():
|
|
return WorkspaceStatus.CLEAN
|
|
|
|
if not self.config.current_issue_path.exists():
|
|
return WorkspaceStatus.DIRTY
|
|
|
|
return WorkspaceStatus.ACTIVE
|
|
|
|
def get_current_workspace(self) -> Optional[Workspace]:
|
|
"""Get the currently active workspace."""
|
|
if not self.config.current_issue_path.exists():
|
|
return None
|
|
|
|
try:
|
|
with open(self.config.current_issue_path, 'r') as f:
|
|
issue_data = json.load(f)
|
|
|
|
return Workspace(
|
|
issue_number=issue_data['number'],
|
|
issue_title=issue_data['title'],
|
|
issue_body=issue_data['body'],
|
|
issue_state=issue_data['state'],
|
|
created_at=datetime.strptime(issue_data['created_at'].replace('Z', '').split('.')[0], '%Y-%m-%dT%H:%M:%S'),
|
|
workspace_dir=self.config.workspace_dir
|
|
)
|
|
except (json.JSONDecodeError, KeyError, ValueError) as e:
|
|
raise WorkspaceError(f"Failed to load current workspace: {e}")
|
|
|
|
def create_workspace(self, issue_data: Dict[str, Any]) -> Workspace:
|
|
"""Create a new workspace for an issue."""
|
|
status = self.get_status()
|
|
if status == WorkspaceStatus.ACTIVE:
|
|
current = self.get_current_workspace()
|
|
raise WorkspaceError(
|
|
f"Workspace already active for issue #{current.issue_number}. "
|
|
"Finish current workspace before starting a new one."
|
|
)
|
|
|
|
# Clean up any dirty workspace
|
|
if status == WorkspaceStatus.DIRTY:
|
|
self.cleanup_workspace()
|
|
|
|
# Create workspace structure
|
|
workspace = Workspace(
|
|
issue_number=issue_data['number'],
|
|
issue_title=issue_data['title'],
|
|
issue_body=issue_data['body'],
|
|
issue_state=issue_data['state'],
|
|
created_at=datetime.now(),
|
|
workspace_dir=self.config.workspace_dir
|
|
)
|
|
|
|
# Create directories
|
|
workspace.workspace_dir.mkdir(exist_ok=True)
|
|
workspace.issue_dir.mkdir(exist_ok=True)
|
|
workspace.tests_dir.mkdir(exist_ok=True)
|
|
|
|
# Create metadata files
|
|
self._create_requirements_file(workspace, issue_data)
|
|
self._create_test_plan_file(workspace, issue_data)
|
|
self._save_current_issue(workspace, issue_data)
|
|
|
|
return workspace
|
|
|
|
def cleanup_workspace(self) -> None:
|
|
"""Clean up the current workspace."""
|
|
if self.config.workspace_dir.exists():
|
|
shutil.rmtree(self.config.workspace_dir)
|
|
|
|
def finish_workspace(self) -> Optional[Workspace]:
|
|
"""Finish the current workspace and integrate tests."""
|
|
workspace = self.get_current_workspace()
|
|
if not workspace:
|
|
return None
|
|
|
|
# Move tests to main tests directory
|
|
main_tests_dir = self.config.tests_dir
|
|
main_tests_dir.mkdir(exist_ok=True)
|
|
|
|
if workspace.tests_dir.exists():
|
|
for test_file in workspace.tests_dir.glob("*.py"):
|
|
dest_file = main_tests_dir / test_file.name
|
|
shutil.copy2(test_file, dest_file)
|
|
|
|
# Clean up workspace
|
|
self.cleanup_workspace()
|
|
|
|
return workspace
|
|
|
|
def _create_requirements_file(self, workspace: Workspace, issue_data: Dict[str, Any]) -> None:
|
|
"""Create requirements.md file for the issue."""
|
|
content = f"""# Requirements for Issue #{workspace.issue_number}
|
|
|
|
## Title
|
|
{workspace.issue_title}
|
|
|
|
## Description
|
|
{workspace.issue_body}
|
|
|
|
## Acceptance Criteria
|
|
- [ ] Implementation meets the requirements described above
|
|
- [ ] All tests pass
|
|
- [ ] Code follows project conventions
|
|
- [ ] Documentation is updated if needed
|
|
|
|
## Notes
|
|
Created: {workspace.created_at.strftime('%Y-%m-%d %H:%M:%S')}
|
|
"""
|
|
workspace.requirements_file.write_text(content)
|
|
|
|
def _create_test_plan_file(self, workspace: Workspace, issue_data: Dict[str, Any]) -> None:
|
|
"""Create test_plan.md file for the issue."""
|
|
content = f"""# Test Plan for Issue #{workspace.issue_number}
|
|
|
|
## Overview
|
|
This test plan outlines the testing strategy for implementing: {workspace.issue_title}
|
|
|
|
## Test Categories
|
|
|
|
### Unit Tests
|
|
- [ ] Core functionality tests
|
|
- [ ] Edge case handling
|
|
- [ ] Error condition tests
|
|
|
|
### Integration Tests
|
|
- [ ] Component integration
|
|
- [ ] API integration
|
|
- [ ] End-to-end scenarios
|
|
|
|
### Generated Tests
|
|
Tests generated for this workspace will be listed here as they are created.
|
|
|
|
## Test Execution
|
|
Run tests with: `pytest tests/test_issue_{workspace.issue_number}_*.py`
|
|
|
|
## Notes
|
|
- Follow TDD red-green-refactor cycle
|
|
- Each test should be focused and specific
|
|
- Tests should be readable and maintainable
|
|
"""
|
|
workspace.test_plan_file.write_text(content)
|
|
|
|
def _save_current_issue(self, workspace: Workspace, issue_data: Dict[str, Any]) -> None:
|
|
"""Save current issue metadata."""
|
|
current_issue_data = {
|
|
'number': workspace.issue_number,
|
|
'title': workspace.issue_title,
|
|
'body': workspace.issue_body,
|
|
'state': workspace.issue_state,
|
|
'created_at': workspace.created_at.isoformat(),
|
|
'url': issue_data.get('html_url', ''),
|
|
'assignee': issue_data.get('assignee', {}).get('login', '') if issue_data.get('assignee') else ''
|
|
}
|
|
|
|
with open(self.config.current_issue_path, 'w') as f:
|
|
json.dump(current_issue_data, f, indent=2) |