devflow-kit 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/README.md +35 -11
- package/dist/cli.js +5 -1
- package/dist/commands/ambient.d.ts +18 -0
- package/dist/commands/ambient.js +136 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +97 -10
- package/dist/commands/memory.d.ts +22 -0
- package/dist/commands/memory.js +175 -0
- package/dist/commands/uninstall.js +72 -5
- package/dist/plugins.js +74 -3
- package/dist/utils/post-install.d.ts +12 -0
- package/dist/utils/post-install.js +82 -1
- package/dist/utils/safe-delete-install.d.ts +7 -0
- package/dist/utils/safe-delete-install.js +40 -5
- package/package.json +2 -1
- package/plugins/devflow-accessibility/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-ambient/.claude-plugin/plugin.json +7 -0
- package/plugins/devflow-ambient/README.md +49 -0
- package/plugins/devflow-ambient/commands/ambient.md +110 -0
- package/plugins/devflow-ambient/skills/ambient-router/SKILL.md +89 -0
- package/plugins/devflow-ambient/skills/ambient-router/references/skill-catalog.md +68 -0
- package/plugins/devflow-audit-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-code-review/.claude-plugin/plugin.json +1 -4
- package/plugins/devflow-code-review/agents/reviewer.md +8 -0
- package/plugins/devflow-code-review/commands/code-review-teams.md +11 -1
- package/plugins/devflow-code-review/commands/code-review.md +12 -2
- package/plugins/devflow-core-skills/.claude-plugin/plugin.json +3 -6
- package/plugins/devflow-core-skills/skills/docs-framework/SKILL.md +10 -6
- package/plugins/devflow-core-skills/skills/test-driven-development/SKILL.md +139 -0
- package/plugins/devflow-core-skills/skills/test-driven-development/references/rationalization-prevention.md +111 -0
- package/plugins/devflow-debug/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-frontend-design/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-go/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-go/skills/go/SKILL.md +187 -0
- package/plugins/devflow-go/skills/go/references/concurrency.md +312 -0
- package/plugins/devflow-go/skills/go/references/detection.md +129 -0
- package/plugins/devflow-go/skills/go/references/patterns.md +232 -0
- package/plugins/devflow-go/skills/go/references/violations.md +205 -0
- package/plugins/devflow-implement/.claude-plugin/plugin.json +1 -3
- package/plugins/devflow-implement/agents/coder.md +11 -6
- package/plugins/devflow-java/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-java/skills/java/SKILL.md +183 -0
- package/plugins/devflow-java/skills/java/references/detection.md +120 -0
- package/plugins/devflow-java/skills/java/references/modern-java.md +270 -0
- package/plugins/devflow-java/skills/java/references/patterns.md +235 -0
- package/plugins/devflow-java/skills/java/references/violations.md +213 -0
- package/plugins/devflow-python/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-python/skills/python/SKILL.md +188 -0
- package/plugins/devflow-python/skills/python/references/async.md +220 -0
- package/plugins/devflow-python/skills/python/references/detection.md +128 -0
- package/plugins/devflow-python/skills/python/references/patterns.md +226 -0
- package/plugins/devflow-python/skills/python/references/violations.md +204 -0
- package/plugins/devflow-react/.claude-plugin/plugin.json +15 -0
- package/plugins/{devflow-core-skills → devflow-react}/skills/react/SKILL.md +1 -1
- package/plugins/{devflow-core-skills → devflow-react}/skills/react/references/patterns.md +3 -3
- package/plugins/devflow-resolve/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-rust/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-rust/skills/rust/SKILL.md +193 -0
- package/plugins/devflow-rust/skills/rust/references/detection.md +131 -0
- package/plugins/devflow-rust/skills/rust/references/ownership.md +242 -0
- package/plugins/devflow-rust/skills/rust/references/patterns.md +210 -0
- package/plugins/devflow-rust/skills/rust/references/violations.md +191 -0
- package/plugins/devflow-self-review/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-specify/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-typescript/.claude-plugin/plugin.json +15 -0
- package/plugins/{devflow-core-skills → devflow-typescript}/skills/typescript/references/patterns.md +3 -3
- package/scripts/hooks/ambient-prompt.sh +48 -0
- package/scripts/hooks/background-memory-update.sh +49 -8
- package/scripts/hooks/ensure-memory-gitignore.sh +17 -0
- package/scripts/hooks/pre-compact-memory.sh +12 -6
- package/scripts/hooks/session-start-memory.sh +50 -8
- package/scripts/hooks/stop-update-memory.sh +10 -6
- package/shared/agents/coder.md +11 -6
- package/shared/agents/reviewer.md +8 -0
- package/shared/skills/ambient-router/SKILL.md +89 -0
- package/shared/skills/ambient-router/references/skill-catalog.md +68 -0
- package/shared/skills/docs-framework/SKILL.md +10 -6
- package/shared/skills/go/SKILL.md +187 -0
- package/shared/skills/go/references/concurrency.md +312 -0
- package/shared/skills/go/references/detection.md +129 -0
- package/shared/skills/go/references/patterns.md +232 -0
- package/shared/skills/go/references/violations.md +205 -0
- package/shared/skills/java/SKILL.md +183 -0
- package/shared/skills/java/references/detection.md +120 -0
- package/shared/skills/java/references/modern-java.md +270 -0
- package/shared/skills/java/references/patterns.md +235 -0
- package/shared/skills/java/references/violations.md +213 -0
- package/shared/skills/python/SKILL.md +188 -0
- package/shared/skills/python/references/async.md +220 -0
- package/shared/skills/python/references/detection.md +128 -0
- package/shared/skills/python/references/patterns.md +226 -0
- package/shared/skills/python/references/violations.md +204 -0
- package/shared/skills/react/SKILL.md +1 -1
- package/shared/skills/react/references/patterns.md +3 -3
- package/shared/skills/rust/SKILL.md +193 -0
- package/shared/skills/rust/references/detection.md +131 -0
- package/shared/skills/rust/references/ownership.md +242 -0
- package/shared/skills/rust/references/patterns.md +210 -0
- package/shared/skills/rust/references/violations.md +191 -0
- package/shared/skills/test-driven-development/SKILL.md +139 -0
- package/shared/skills/test-driven-development/references/rationalization-prevention.md +111 -0
- package/shared/skills/typescript/references/patterns.md +3 -3
- package/src/templates/managed-settings.json +14 -0
- package/plugins/devflow-code-review/skills/react/SKILL.md +0 -276
- package/plugins/devflow-code-review/skills/react/references/patterns.md +0 -1331
- package/plugins/devflow-core-skills/skills/accessibility/SKILL.md +0 -229
- package/plugins/devflow-core-skills/skills/accessibility/references/detection.md +0 -171
- package/plugins/devflow-core-skills/skills/accessibility/references/patterns.md +0 -670
- package/plugins/devflow-core-skills/skills/accessibility/references/violations.md +0 -419
- package/plugins/devflow-core-skills/skills/frontend-design/SKILL.md +0 -254
- package/plugins/devflow-core-skills/skills/frontend-design/references/detection.md +0 -184
- package/plugins/devflow-core-skills/skills/frontend-design/references/patterns.md +0 -511
- package/plugins/devflow-core-skills/skills/frontend-design/references/violations.md +0 -453
- package/plugins/devflow-core-skills/skills/react/references/violations.md +0 -565
- package/plugins/devflow-implement/skills/accessibility/SKILL.md +0 -229
- package/plugins/devflow-implement/skills/accessibility/references/detection.md +0 -171
- package/plugins/devflow-implement/skills/accessibility/references/patterns.md +0 -670
- package/plugins/devflow-implement/skills/accessibility/references/violations.md +0 -419
- package/plugins/devflow-implement/skills/frontend-design/SKILL.md +0 -254
- package/plugins/devflow-implement/skills/frontend-design/references/detection.md +0 -184
- package/plugins/devflow-implement/skills/frontend-design/references/patterns.md +0 -511
- package/plugins/devflow-implement/skills/frontend-design/references/violations.md +0 -453
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/SKILL.md +0 -0
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/detection.md +0 -0
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/patterns.md +0 -0
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/violations.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/SKILL.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/detection.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/patterns.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/violations.md +0 -0
- /package/plugins/{devflow-code-review → devflow-react}/skills/react/references/violations.md +0 -0
- /package/plugins/{devflow-core-skills → devflow-typescript}/skills/typescript/SKILL.md +0 -0
- /package/plugins/{devflow-core-skills → devflow-typescript}/skills/typescript/references/violations.md +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Async Python Patterns
|
|
2
|
+
|
|
3
|
+
Deep-dive on async Python programming. Reference from main SKILL.md.
|
|
4
|
+
|
|
5
|
+
## Core asyncio Patterns
|
|
6
|
+
|
|
7
|
+
### Basic Async Function
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import asyncio
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
async def fetch_user(user_id: str) -> User:
|
|
14
|
+
async with aiohttp.ClientSession() as session:
|
|
15
|
+
async with session.get(f"/api/users/{user_id}") as response:
|
|
16
|
+
data = await response.json()
|
|
17
|
+
return User.model_validate(data)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Concurrent Execution with gather
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
async def load_dashboard(user_id: str) -> Dashboard:
|
|
24
|
+
# Independent fetches run concurrently
|
|
25
|
+
user, orders, preferences = await asyncio.gather(
|
|
26
|
+
fetch_user(user_id),
|
|
27
|
+
fetch_orders(user_id),
|
|
28
|
+
fetch_preferences(user_id),
|
|
29
|
+
)
|
|
30
|
+
return Dashboard(user=user, orders=orders, preferences=preferences)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Structured Concurrency with TaskGroup
|
|
34
|
+
|
|
35
|
+
### TaskGroup for Safe Concurrency (Python 3.11+)
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
async def process_batch(items: list[Item]) -> list[Result]:
|
|
39
|
+
results: list[Result] = []
|
|
40
|
+
|
|
41
|
+
async with asyncio.TaskGroup() as tg:
|
|
42
|
+
for item in items:
|
|
43
|
+
tg.create_task(process_and_collect(item, results))
|
|
44
|
+
|
|
45
|
+
return results
|
|
46
|
+
|
|
47
|
+
async def process_and_collect(item: Item, results: list[Result]) -> None:
|
|
48
|
+
result = await process_item(item)
|
|
49
|
+
results.append(result)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Error Handling with TaskGroup
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
async def resilient_batch(items: list[Item]) -> tuple[list[Result], list[Error]]:
|
|
56
|
+
results: list[Result] = []
|
|
57
|
+
errors: list[Error] = []
|
|
58
|
+
|
|
59
|
+
# TaskGroup cancels all tasks if one raises — wrap individual tasks
|
|
60
|
+
async def safe_process(item: Item) -> None:
|
|
61
|
+
try:
|
|
62
|
+
result = await process_item(item)
|
|
63
|
+
results.append(result)
|
|
64
|
+
except ProcessingError as e:
|
|
65
|
+
errors.append(Error(item_id=item.id, message=str(e)))
|
|
66
|
+
|
|
67
|
+
async with asyncio.TaskGroup() as tg:
|
|
68
|
+
for item in items:
|
|
69
|
+
tg.create_task(safe_process(item))
|
|
70
|
+
|
|
71
|
+
return results, errors
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Async Generators
|
|
75
|
+
|
|
76
|
+
### Streaming Results
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from typing import AsyncGenerator
|
|
80
|
+
|
|
81
|
+
async def stream_results(query: str, params: tuple = ()) -> AsyncGenerator[Record, None]:
|
|
82
|
+
async with get_connection() as conn:
|
|
83
|
+
cursor = await conn.execute(query, params)
|
|
84
|
+
async for row in cursor:
|
|
85
|
+
yield Record.from_row(row)
|
|
86
|
+
|
|
87
|
+
# Usage
|
|
88
|
+
async for record in stream_results("SELECT * FROM events WHERE type = ?", ("click",)):
|
|
89
|
+
await process(record)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Async Generator with Cleanup
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
async def paginated_fetch(url: str, page_size: int = 100) -> AsyncGenerator[Item, None]:
|
|
96
|
+
page = 0
|
|
97
|
+
while True:
|
|
98
|
+
response = await fetch_page(url, page=page, size=page_size)
|
|
99
|
+
if not response.items:
|
|
100
|
+
break
|
|
101
|
+
for item in response.items:
|
|
102
|
+
yield item
|
|
103
|
+
page += 1
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Semaphore for Rate Limiting
|
|
107
|
+
|
|
108
|
+
### Bounded Concurrency
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
async def fetch_all(urls: list[str], max_concurrent: int = 10) -> list[Response]:
|
|
112
|
+
semaphore = asyncio.Semaphore(max_concurrent)
|
|
113
|
+
|
|
114
|
+
async def bounded_fetch(url: str) -> Response:
|
|
115
|
+
async with semaphore:
|
|
116
|
+
return await fetch(url)
|
|
117
|
+
|
|
118
|
+
return await asyncio.gather(*[bounded_fetch(url) for url in urls])
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## aiohttp Patterns
|
|
122
|
+
|
|
123
|
+
### Client Session Management
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from contextlib import asynccontextmanager
|
|
127
|
+
from typing import AsyncGenerator
|
|
128
|
+
|
|
129
|
+
@asynccontextmanager
|
|
130
|
+
async def api_client(base_url: str) -> AsyncGenerator[aiohttp.ClientSession, None]:
|
|
131
|
+
timeout = aiohttp.ClientTimeout(total=30)
|
|
132
|
+
async with aiohttp.ClientSession(base_url, timeout=timeout) as session:
|
|
133
|
+
yield session
|
|
134
|
+
|
|
135
|
+
# Usage — session reused for multiple requests
|
|
136
|
+
async def sync_users(user_ids: list[str]) -> list[User]:
|
|
137
|
+
async with api_client("https://api.example.com") as client:
|
|
138
|
+
tasks = [fetch_user_with_session(client, uid) for uid in user_ids]
|
|
139
|
+
return await asyncio.gather(*tasks)
|
|
140
|
+
|
|
141
|
+
async def fetch_user_with_session(
|
|
142
|
+
client: aiohttp.ClientSession, user_id: str
|
|
143
|
+
) -> User:
|
|
144
|
+
async with client.get(f"/users/{user_id}") as response:
|
|
145
|
+
response.raise_for_status()
|
|
146
|
+
data = await response.json()
|
|
147
|
+
return User.model_validate(data)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Timeout Patterns
|
|
151
|
+
|
|
152
|
+
### Per-Operation Timeout
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
async def fetch_with_timeout(url: str, timeout_seconds: float = 5.0) -> dict[str, Any]:
|
|
156
|
+
try:
|
|
157
|
+
async with asyncio.timeout(timeout_seconds):
|
|
158
|
+
return await fetch(url)
|
|
159
|
+
except TimeoutError:
|
|
160
|
+
raise OperationTimeout(f"Request to {url} timed out after {timeout_seconds}s")
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Anti-Patterns
|
|
164
|
+
|
|
165
|
+
### Blocking Calls in Async Code
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# VIOLATION: Blocks the event loop
|
|
169
|
+
async def bad_fetch(url: str) -> str:
|
|
170
|
+
import requests
|
|
171
|
+
return requests.get(url).text # Blocks entire event loop!
|
|
172
|
+
|
|
173
|
+
# CORRECT: Use async library or run in executor
|
|
174
|
+
async def good_fetch(url: str) -> str:
|
|
175
|
+
async with aiohttp.ClientSession() as session:
|
|
176
|
+
async with session.get(url) as response:
|
|
177
|
+
return await response.text()
|
|
178
|
+
|
|
179
|
+
# CORRECT: Offload blocking call to thread pool
|
|
180
|
+
async def run_blocking(func: Callable[..., R], *args: Any) -> R:
|
|
181
|
+
loop = asyncio.get_running_loop()
|
|
182
|
+
return await loop.run_in_executor(None, func, *args)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Fire-and-Forget Without Error Handling
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
# VIOLATION: Exception silently lost
|
|
189
|
+
async def bad_handler(event: Event) -> None:
|
|
190
|
+
asyncio.create_task(send_notification(event)) # Error vanishes
|
|
191
|
+
|
|
192
|
+
# CORRECT: Track the task and handle errors
|
|
193
|
+
async def good_handler(event: Event) -> None:
|
|
194
|
+
task = asyncio.create_task(send_notification(event))
|
|
195
|
+
task.add_done_callback(handle_task_exception)
|
|
196
|
+
|
|
197
|
+
def handle_task_exception(task: asyncio.Task[Any]) -> None:
|
|
198
|
+
if not task.cancelled() and task.exception() is not None:
|
|
199
|
+
logger.error("Background task failed", exc_info=task.exception())
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Sequential When Parallel Is Safe
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
# VIOLATION: Unnecessarily sequential — 3x slower
|
|
206
|
+
async def slow_load(user_id: str) -> Dashboard:
|
|
207
|
+
user = await fetch_user(user_id)
|
|
208
|
+
orders = await fetch_orders(user_id)
|
|
209
|
+
prefs = await fetch_preferences(user_id)
|
|
210
|
+
return Dashboard(user=user, orders=orders, preferences=prefs)
|
|
211
|
+
|
|
212
|
+
# CORRECT: Concurrent — ~1x latency
|
|
213
|
+
async def fast_load(user_id: str) -> Dashboard:
|
|
214
|
+
user, orders, prefs = await asyncio.gather(
|
|
215
|
+
fetch_user(user_id),
|
|
216
|
+
fetch_orders(user_id),
|
|
217
|
+
fetch_preferences(user_id),
|
|
218
|
+
)
|
|
219
|
+
return Dashboard(user=user, orders=orders, preferences=prefs)
|
|
220
|
+
```
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Python Issue Detection
|
|
2
|
+
|
|
3
|
+
Grep patterns for finding common Python issues. Reference from main SKILL.md.
|
|
4
|
+
|
|
5
|
+
## Type Safety Detection
|
|
6
|
+
|
|
7
|
+
### Missing Type Hints
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Functions without return type annotation
|
|
11
|
+
grep -rn "def .*):$" --include="*.py" | grep -v "->.*:"
|
|
12
|
+
|
|
13
|
+
# Functions without any annotations
|
|
14
|
+
grep -rn "def .*([a-z_][a-z_0-9]*\s*," --include="*.py" | grep -v ":"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Bare Except Clauses
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Bare except (catches everything)
|
|
21
|
+
grep -rn "except:" --include="*.py"
|
|
22
|
+
|
|
23
|
+
# Overly broad except Exception
|
|
24
|
+
grep -rn "except Exception:" --include="*.py" | grep -v "# noqa"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Data Modeling Detection
|
|
28
|
+
|
|
29
|
+
### Mutable Default Arguments
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# List defaults
|
|
33
|
+
grep -rn "def .*=\s*\[\]" --include="*.py"
|
|
34
|
+
|
|
35
|
+
# Dict defaults
|
|
36
|
+
grep -rn "def .*=\s*{}" --include="*.py"
|
|
37
|
+
|
|
38
|
+
# Set defaults
|
|
39
|
+
grep -rn "def .*=\s*set()" --include="*.py"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Raw Dict Usage Instead of Dataclasses
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Dict literals assigned to typed variables
|
|
46
|
+
grep -rn ': dict\[' --include="*.py" | grep "= {"
|
|
47
|
+
|
|
48
|
+
# Nested dict access patterns (fragile)
|
|
49
|
+
grep -rn '\["[a-z].*\]\["' --include="*.py"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Anti-Pattern Detection
|
|
53
|
+
|
|
54
|
+
### Assert in Production Code
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Assert statements outside test files
|
|
58
|
+
grep -rn "^ assert " --include="*.py" | grep -v "test_\|_test\.py\|tests/"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Global Mutable State
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Global variable assignment
|
|
65
|
+
grep -rn "^[a-z_].*= \[\]\|^[a-z_].*= {}\|^[a-z_].*= set()" --include="*.py"
|
|
66
|
+
|
|
67
|
+
# Global keyword usage
|
|
68
|
+
grep -rn "global " --include="*.py"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Old-Style String Formatting
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Percent formatting
|
|
75
|
+
grep -rn '"%.*" %' --include="*.py"
|
|
76
|
+
grep -rn "'%.*' %" --include="*.py"
|
|
77
|
+
|
|
78
|
+
# .format() where f-strings would be cleaner
|
|
79
|
+
grep -rn '\.format(' --include="*.py"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Wildcard Imports
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Star imports
|
|
86
|
+
grep -rn "from .* import \*" --include="*.py"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Async Detection
|
|
90
|
+
|
|
91
|
+
### Missing Await
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Coroutine called without await (common mistake)
|
|
95
|
+
grep -rn "async def" --include="*.py" -l | xargs grep -n "[^await ]fetch\|[^await ]save\|[^await ]send"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Blocking Calls in Async Code
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# time.sleep in async context
|
|
102
|
+
grep -rn "time\.sleep" --include="*.py"
|
|
103
|
+
|
|
104
|
+
# requests library in async files (should use aiohttp/httpx)
|
|
105
|
+
grep -rn "import requests\|from requests" --include="*.py"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Security Detection
|
|
109
|
+
|
|
110
|
+
### Unsafe Deserialization
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# pickle usage (arbitrary code execution risk)
|
|
114
|
+
grep -rn "pickle\.loads\|pickle\.load(" --include="*.py"
|
|
115
|
+
|
|
116
|
+
# yaml.load without SafeLoader
|
|
117
|
+
grep -rn "yaml\.load(" --include="*.py" | grep -v "SafeLoader\|safe_load"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Shell Injection
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# subprocess with shell=True
|
|
124
|
+
grep -rn "shell=True" --include="*.py"
|
|
125
|
+
|
|
126
|
+
# os.system calls
|
|
127
|
+
grep -rn "os\.system(" --include="*.py"
|
|
128
|
+
```
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Python Correct Patterns
|
|
2
|
+
|
|
3
|
+
Extended correct patterns for Python development. Reference from main SKILL.md.
|
|
4
|
+
|
|
5
|
+
## Dependency Injection
|
|
6
|
+
|
|
7
|
+
### Constructor Injection
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from typing import Protocol
|
|
11
|
+
|
|
12
|
+
class EmailSender(Protocol):
|
|
13
|
+
def send(self, to: str, subject: str, body: str) -> None: ...
|
|
14
|
+
|
|
15
|
+
class UserService:
|
|
16
|
+
def __init__(self, repo: UserRepository, emailer: EmailSender) -> None:
|
|
17
|
+
self._repo = repo
|
|
18
|
+
self._emailer = emailer
|
|
19
|
+
|
|
20
|
+
def create_user(self, request: CreateUserRequest) -> User:
|
|
21
|
+
user = User(name=request.name, email=request.email)
|
|
22
|
+
saved = self._repo.save(user)
|
|
23
|
+
self._emailer.send(user.email, "Welcome", f"Hello {user.name}")
|
|
24
|
+
return saved
|
|
25
|
+
|
|
26
|
+
# Easy to test — inject fakes
|
|
27
|
+
service = UserService(repo=FakeUserRepo(), emailer=FakeEmailSender())
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Factory Functions
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
def create_app(config: AppConfig) -> Flask:
|
|
34
|
+
app = Flask(__name__)
|
|
35
|
+
db = Database(config.database_url)
|
|
36
|
+
cache = RedisCache(config.redis_url)
|
|
37
|
+
user_service = UserService(repo=SqlUserRepo(db), emailer=SmtpSender(config.smtp))
|
|
38
|
+
register_routes(app, user_service)
|
|
39
|
+
return app
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Decorator Patterns
|
|
43
|
+
|
|
44
|
+
### Retry Decorator
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import functools
|
|
48
|
+
import time
|
|
49
|
+
from typing import TypeVar, Callable, ParamSpec
|
|
50
|
+
|
|
51
|
+
P = ParamSpec("P")
|
|
52
|
+
R = TypeVar("R")
|
|
53
|
+
|
|
54
|
+
def retry(
|
|
55
|
+
max_attempts: int = 3,
|
|
56
|
+
delay: float = 1.0,
|
|
57
|
+
exceptions: tuple[type[Exception], ...] = (Exception,),
|
|
58
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
59
|
+
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
|
60
|
+
@functools.wraps(func)
|
|
61
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
62
|
+
last_error: Exception | None = None
|
|
63
|
+
for attempt in range(max_attempts):
|
|
64
|
+
try:
|
|
65
|
+
return func(*args, **kwargs)
|
|
66
|
+
except exceptions as e:
|
|
67
|
+
last_error = e
|
|
68
|
+
if attempt < max_attempts - 1:
|
|
69
|
+
time.sleep(delay * (2 ** attempt))
|
|
70
|
+
raise last_error # type: ignore[misc]
|
|
71
|
+
return wrapper
|
|
72
|
+
return decorator
|
|
73
|
+
|
|
74
|
+
@retry(max_attempts=3, delay=0.5, exceptions=(ConnectionError, TimeoutError))
|
|
75
|
+
def fetch_data(url: str) -> dict[str, Any]:
|
|
76
|
+
...
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Validation Decorator
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
def validate_input(schema: type[BaseModel]):
|
|
83
|
+
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
|
84
|
+
@functools.wraps(func)
|
|
85
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
86
|
+
# Validate first positional arg after self/cls
|
|
87
|
+
data = args[1] if len(args) > 1 else args[0]
|
|
88
|
+
schema.model_validate(data)
|
|
89
|
+
return func(*args, **kwargs)
|
|
90
|
+
return wrapper
|
|
91
|
+
return decorator
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Context Manager Patterns
|
|
95
|
+
|
|
96
|
+
### Database Transaction
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from contextlib import contextmanager
|
|
100
|
+
from typing import Generator
|
|
101
|
+
|
|
102
|
+
@contextmanager
|
|
103
|
+
def transaction(session: Session) -> Generator[Session, None, None]:
|
|
104
|
+
try:
|
|
105
|
+
yield session
|
|
106
|
+
session.commit()
|
|
107
|
+
except Exception:
|
|
108
|
+
session.rollback()
|
|
109
|
+
raise
|
|
110
|
+
finally:
|
|
111
|
+
session.close()
|
|
112
|
+
|
|
113
|
+
# Usage
|
|
114
|
+
with transaction(db.session()) as session:
|
|
115
|
+
session.add(user)
|
|
116
|
+
session.add(audit_log)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Temporary Directory
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from contextlib import contextmanager
|
|
123
|
+
from pathlib import Path
|
|
124
|
+
import tempfile
|
|
125
|
+
import shutil
|
|
126
|
+
|
|
127
|
+
@contextmanager
|
|
128
|
+
def temp_workspace() -> Generator[Path, None, None]:
|
|
129
|
+
path = Path(tempfile.mkdtemp())
|
|
130
|
+
try:
|
|
131
|
+
yield path
|
|
132
|
+
finally:
|
|
133
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Pytest Fixture Patterns
|
|
137
|
+
|
|
138
|
+
### Service Fixtures with DI
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
import pytest
|
|
142
|
+
|
|
143
|
+
@pytest.fixture
|
|
144
|
+
def fake_repo() -> FakeUserRepo:
|
|
145
|
+
return FakeUserRepo()
|
|
146
|
+
|
|
147
|
+
@pytest.fixture
|
|
148
|
+
def fake_emailer() -> FakeEmailSender:
|
|
149
|
+
return FakeEmailSender()
|
|
150
|
+
|
|
151
|
+
@pytest.fixture
|
|
152
|
+
def user_service(fake_repo: FakeUserRepo, fake_emailer: FakeEmailSender) -> UserService:
|
|
153
|
+
return UserService(repo=fake_repo, emailer=fake_emailer)
|
|
154
|
+
|
|
155
|
+
def test_create_user_sends_welcome_email(
|
|
156
|
+
user_service: UserService,
|
|
157
|
+
fake_emailer: FakeEmailSender,
|
|
158
|
+
) -> None:
|
|
159
|
+
user_service.create_user(CreateUserRequest(name="Alice", email="a@b.com"))
|
|
160
|
+
assert fake_emailer.sent_count == 1
|
|
161
|
+
assert fake_emailer.last_to == "a@b.com"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Parametrized Tests
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
@pytest.mark.parametrize(
|
|
168
|
+
"input_age, expected_valid",
|
|
169
|
+
[
|
|
170
|
+
(25, True),
|
|
171
|
+
(0, True),
|
|
172
|
+
(150, True),
|
|
173
|
+
(-1, False),
|
|
174
|
+
(151, False),
|
|
175
|
+
(None, False),
|
|
176
|
+
],
|
|
177
|
+
)
|
|
178
|
+
def test_age_validation(input_age: int | None, expected_valid: bool) -> None:
|
|
179
|
+
result = validate_age(input_age)
|
|
180
|
+
assert result.is_valid == expected_valid
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Structured Logging
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
import structlog
|
|
187
|
+
|
|
188
|
+
logger = structlog.get_logger()
|
|
189
|
+
|
|
190
|
+
def process_order(order: Order) -> OrderResult:
|
|
191
|
+
log = logger.bind(order_id=order.id, user_id=order.user_id)
|
|
192
|
+
log.info("processing_order", item_count=len(order.items))
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
result = fulfill(order)
|
|
196
|
+
log.info("order_fulfilled", total=result.total)
|
|
197
|
+
return result
|
|
198
|
+
except InsufficientStockError as e:
|
|
199
|
+
log.warning("order_failed_stock", item_id=e.item_id)
|
|
200
|
+
raise
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Enum Patterns
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from enum import Enum, auto
|
|
207
|
+
|
|
208
|
+
class OrderStatus(Enum):
|
|
209
|
+
PENDING = auto()
|
|
210
|
+
PROCESSING = auto()
|
|
211
|
+
SHIPPED = auto()
|
|
212
|
+
DELIVERED = auto()
|
|
213
|
+
CANCELLED = auto()
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def is_terminal(self) -> bool:
|
|
217
|
+
return self in (OrderStatus.DELIVERED, OrderStatus.CANCELLED)
|
|
218
|
+
|
|
219
|
+
def can_transition_to(self, target: "OrderStatus") -> bool:
|
|
220
|
+
valid = {
|
|
221
|
+
OrderStatus.PENDING: {OrderStatus.PROCESSING, OrderStatus.CANCELLED},
|
|
222
|
+
OrderStatus.PROCESSING: {OrderStatus.SHIPPED, OrderStatus.CANCELLED},
|
|
223
|
+
OrderStatus.SHIPPED: {OrderStatus.DELIVERED},
|
|
224
|
+
}
|
|
225
|
+
return target in valid.get(self, set())
|
|
226
|
+
```
|