Files
markitect-main/tddai/workspace.py
Bernd Worsch 7b9d6af5a1 fix: Python 3.6 compatibility for tests
- 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>
2025-09-23 01:40:37 +02:00

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)