Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/python-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@prosdevlab/dev-agent': minor
---

Python language support

- Index Python codebases: functions, classes, methods, imports, decorators, type hints, docstrings
- `__all__` controls export detection, `_` prefix convention as fallback
- Async function detection, callee extraction, code snippets
- Pattern analysis: try/except, import style, type coverage via tree-sitter queries
- Skip generated files (_pb2.py, migrations)
- `isTestFile()` refactored to language-aware pattern map (test_*.py, *_test.py, conftest.py)
- All MCP tools (dev_search, dev_refs, dev_map, dev_patterns, dev_status) work with Python automatically
9 changes: 9 additions & 0 deletions .claude/da-plans/core/phase-4-python-support/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@ def get_user(user_id: int) -> User
| [4.3](./4.3-pattern-rules.md) | Add Python-specific pattern rules for dev_patterns | Low — S-expression constants |
| [4.4](./4.4-test-fixtures.md) | Test fixtures, integration tests, documentation | Low — validation |

### Commit strategy

| # | Commit | Risk | What changes |
|---|--------|------|-------------|
| 1 | `feat(core): bundle tree-sitter-python WASM and define extraction queries` | Low | Add `'python'` to languages, `PYTHON_QUERIES` constants, validate against grammar |
| 2 | `feat(core): implement PythonScanner with full extraction` | **Medium** | Scanner class, `isTestFile()` refactor, registry registration. All extraction logic. **Risk is concentrated here.** |
| 3 | `feat(core): add Python pattern rules for dev_patterns` | Low | Python S-expression rules, `WasmPatternMatcher` update, `QUERIES_BY_LANGUAGE` map refactor |
| 4 | `feat(core): add Python test fixtures, integration tests, and docs` | Low | Fixtures (FastAPI, pytest, dataclass, `__init__.py`), parity test, changeset, docs |

---

## Decisions
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Everything runs on your machine. No data leaves.

```
packages/
core/ # Scanner (ts-morph, tree-sitter), vector storage (Antfly), services
core/ # Scanner (ts-morph, tree-sitter for Python/Go), vector storage (Antfly), services
cli/ # Commander.js CLI — dev index, dev mcp install, etc.
mcp-server/ # MCP server with 5 built-in adapters
subagents/ # Coordinator, explorer, planner, PR agents
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,15 @@ Indexing status, document counts, Antfly stats, file watcher state, and health c
| Language | Scanner | Features |
|----------|---------|----------|
| TypeScript/JavaScript | ts-morph | Functions, classes, interfaces, types, arrow functions, hooks |
| Python | tree-sitter | Functions, classes, methods, decorators, type hints, docstrings |
| Go | tree-sitter | Functions, methods, structs, interfaces, generics |
| Markdown | remark | Documentation sections |

## Technology

- **[Antfly](https://antfly.io)** — Hybrid search (BM25 + vector + RRF), local embeddings via Termite (ONNX)
- **ts-morph** — TypeScript/JavaScript AST analysis
- **tree-sitter** — Go analysis (WASM, extensible to Python/Rust)
- **tree-sitter** — Python and Go analysis (WASM)
- **@parcel/watcher** — File change detection for auto-reindexing
- **MCP** — Model Context Protocol for AI tool integration

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ function App() {
});

it('returns empty map for unsupported language', async () => {
const results = await matcher.match('def hello(): pass', 'python', ERROR_HANDLING_QUERIES);
const results = await matcher.match('fn main() {}', 'rust', ERROR_HANDLING_QUERIES);
expect(results.size).toBe(0);
});
});
Expand Down Expand Up @@ -347,7 +347,7 @@ describe('resolveLanguage', () => {
});

it('returns undefined for unsupported extensions', () => {
expect(resolveLanguage('main.py')).toBeUndefined();
expect(resolveLanguage('main.py')).toBe('python');
expect(resolveLanguage('main.go')).toBeUndefined(); // Go has scanner, not pattern matcher
expect(resolveLanguage('README.md')).toBeUndefined();
});
Expand Down Expand Up @@ -443,7 +443,7 @@ describe('extractErrorHandlingWithAst', () => {

it('unsupported extension → runAllAstQueries returns empty → regex', async () => {
const source = 'throw new Error("bad");';
const ast = await runAllAstQueries(source, 'test.py', matcher);
const ast = await runAllAstQueries(source, 'test.rs', matcher);
expect(ast.size).toBe(0); // unsupported language
expect(extractErrorHandlingWithAst(source, ast)).toEqual(
extractErrorHandlingFromContent(source)
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/pattern-matcher/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@
*/

export {
ALL_PYTHON_QUERIES,
ALL_QUERIES,
ERROR_HANDLING_QUERIES,
IMPORT_STYLE_QUERIES,
PYTHON_ERROR_HANDLING_QUERIES,
PYTHON_IMPORT_QUERIES,
PYTHON_TYPE_QUERIES,
TYPE_COVERAGE_QUERIES,
} from './rules.js';
export {
Expand Down
76 changes: 76 additions & 0 deletions packages/core/src/pattern-matcher/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,79 @@ export const ALL_QUERIES: PatternMatchRule[] = [
...IMPORT_STYLE_QUERIES,
...TYPE_COVERAGE_QUERIES,
];

// ============================================================================
// Python Error Handling (3 rules)
// ============================================================================

export const PYTHON_ERROR_HANDLING_QUERIES: PatternMatchRule[] = [
{
id: 'try-except',
category: 'error-handling',
query: '(try_statement) @match',
},
{
id: 'raise',
category: 'error-handling',
query: '(raise_statement) @match',
},
{
id: 'except-clause',
category: 'error-handling',
query: '(except_clause) @match',
},
];

// ============================================================================
// Python Import Style (3 rules)
// ============================================================================

export const PYTHON_IMPORT_QUERIES: PatternMatchRule[] = [
{
id: 'import-module',
category: 'import-style',
query: '(import_statement) @match',
},
{
id: 'from-import',
category: 'import-style',
query: '(import_from_statement) @match',
},
{
id: 'relative-import',
category: 'import-style',
query: '(import_from_statement module_name: (relative_import)) @match',
},
];

// ============================================================================
// Python Type Coverage (3 rules)
// ============================================================================

export const PYTHON_TYPE_QUERIES: PatternMatchRule[] = [
{
id: 'typed-parameter',
category: 'type-coverage',
query: '(typed_parameter) @match',
},
{
id: 'py-function-return-type',
category: 'type-coverage',
query: '(function_definition return_type: (type)) @match',
},
{
id: 'py-function-total',
category: 'type-coverage',
query: '(function_definition) @match',
},
];

// ============================================================================
// All Python rules combined
// ============================================================================

export const ALL_PYTHON_QUERIES: PatternMatchRule[] = [
...PYTHON_ERROR_HANDLING_QUERIES,
...PYTHON_IMPORT_QUERIES,
...PYTHON_TYPE_QUERIES,
];
3 changes: 2 additions & 1 deletion packages/core/src/pattern-matcher/wasm-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const EXTENSION_TO_LANGUAGE: Record<string, TreeSitterLanguage> = {
'.tsx': 'tsx',
'.js': 'javascript',
'.jsx': 'javascript',
'.py': 'python',
};

/**
Expand All @@ -61,7 +62,7 @@ class WasmPatternMatcher implements PatternMatcher {
queries: PatternMatchRule[]
): Promise<Map<string, number>> {
// Validate language is supported
const supportedLanguages = new Set<string>(['typescript', 'tsx', 'javascript', 'go']);
const supportedLanguages = new Set<string>(['typescript', 'tsx', 'javascript', 'go', 'python']);
if (!supportedLanguages.has(language)) {
return new Map();
}
Expand Down
30 changes: 30 additions & 0 deletions packages/core/src/scanner/__fixtures__/fastapi-app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""FastAPI application for user management."""

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional

app = FastAPI()

MAX_USERS = 1000


class User(BaseModel):
"""User data model."""
name: str
email: str
age: Optional[int] = None


@app.get("/users/{user_id}")
async def get_user(user_id: int) -> User:
"""Fetch a user by ID."""
user = await db.get(user_id)
if not user:
raise HTTPException(status_code=404)
return user


def _validate_email(email: str) -> bool:
"""Private helper for email validation."""
return "@" in email
71 changes: 71 additions & 0 deletions packages/core/src/scanner/__fixtures__/python-service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""User service with realistic Python patterns."""

from typing import Optional


class UserService:
"""Service for managing users.

Provides CRUD operations for the user database.
Handles authentication and authorization checks.

Attributes:
db: Database connection instance.
cache_ttl: Cache time-to-live in seconds.
"""

DEFAULT_CACHE_TTL = 300

def __init__(self, db, cache_ttl: int = 300):
"""Initialize the user service.

Args:
db: Database connection.
cache_ttl: Cache TTL in seconds.
"""
self.db = db
self.cache_ttl = cache_ttl

@property
def is_connected(self) -> bool:
"""Check if the database connection is active."""
return self.db.is_alive()

@classmethod
def from_config(cls, config: dict) -> "UserService":
"""Create a UserService from a configuration dictionary.

Args:
config: Dictionary with 'db_url' and optional 'cache_ttl'.

Returns:
Configured UserService instance.
"""
db = connect(config["db_url"])
return cls(db, cache_ttl=config.get("cache_ttl", 300))

@staticmethod
def validate_email(email: str) -> bool:
"""Validate an email address format."""
return "@" in email and "." in email

async def get_user(self, user_id: int) -> Optional[dict]:
"""Fetch a user by ID.

Args:
user_id: The user's unique identifier.

Returns:
User dictionary if found, None otherwise.

Raises:
ConnectionError: If database is unavailable.
"""
cached = self._check_cache(user_id)
if cached:
return cached
return await self.db.query("SELECT * FROM users WHERE id = ?", user_id)

def _check_cache(self, user_id: int) -> Optional[dict]:
"""Private: check the in-memory cache."""
return None
20 changes: 20 additions & 0 deletions packages/core/src/scanner/__fixtures__/python-utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Utility functions."""

__all__ = ["parse_date", "format_currency"]

MAX_RETRIES = 3
_INTERNAL_CACHE = {}


def parse_date(date_str: str):
"""Parse a date string."""
return date_str


def format_currency(amount: float) -> str:
return f"${amount:.2f}"


def _internal_helper():
"""Private helper — not in __all__."""
pass
Loading
Loading