mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7cb442843 |
@@ -33,6 +33,7 @@
|
||||
"./agents/go-build-resolver.md",
|
||||
"./agents/go-reviewer.md",
|
||||
"./agents/planner.md",
|
||||
"./agents/python-reviewer.md",
|
||||
"./agents/refactor-cleaner.md",
|
||||
"./agents/security-reviewer.md",
|
||||
"./agents/tdd-guide.md"
|
||||
|
||||
85
README.md
85
README.md
@@ -9,10 +9,17 @@
|
||||

|
||||

|
||||
|
||||
<p align="left">
|
||||
<span>English</span> |
|
||||
<a href="README.zh-CN.md">简体中文</a>
|
||||
</p>
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**🌐 Language / 语言 / 語言**
|
||||
|
||||
[**English**](README.md) | [简体中文](README.zh-CN.md) | [繁體中文](docs/zh-TW/README.md)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
**The complete collection of Claude Code configs from an Anthropic hackathon winner.**
|
||||
|
||||
@@ -54,7 +61,47 @@ This repo is the raw code only. The guides explain everything.
|
||||
|
||||
---
|
||||
|
||||
## Cross-Platform Support
|
||||
## 🚀 Quick Start
|
||||
|
||||
Get up and running in under 2 minutes:
|
||||
|
||||
### Step 1: Install the Plugin
|
||||
|
||||
```bash
|
||||
# Add marketplace
|
||||
/plugin marketplace add affaan-m/everything-claude-code
|
||||
|
||||
# Install plugin
|
||||
/plugin install everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
### Step 2: Install Rules (Required)
|
||||
|
||||
> ⚠️ **Important:** Claude Code plugins cannot distribute `rules` automatically. Install them manually:
|
||||
|
||||
```bash
|
||||
# Clone the repo first
|
||||
git clone https://github.com/affaan-m/everything-claude-code.git
|
||||
|
||||
# Copy rules (applies to all projects)
|
||||
cp -r everything-claude-code/rules/* ~/.claude/rules/
|
||||
```
|
||||
|
||||
### Step 3: Start Using
|
||||
|
||||
```bash
|
||||
# Try a command
|
||||
/plan "Add user authentication"
|
||||
|
||||
# Check available commands
|
||||
/plugin list everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
✨ **That's it!** You now have access to 15+ agents, 30+ skills, and 20+ commands.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Cross-Platform Support
|
||||
|
||||
This plugin now fully supports **Windows, macOS, and Linux**. All hooks and scripts have been rewritten in Node.js for maximum compatibility.
|
||||
|
||||
@@ -89,7 +136,7 @@ Or use the `/setup-pm` command in Claude Code.
|
||||
|
||||
---
|
||||
|
||||
## What's Inside
|
||||
## 📦 What's Inside
|
||||
|
||||
This repo is a **Claude Code plugin** - install it directly or copy components manually.
|
||||
|
||||
@@ -194,7 +241,7 @@ everything-claude-code/
|
||||
|
||||
---
|
||||
|
||||
## Ecosystem Tools
|
||||
## 🛠️ Ecosystem Tools
|
||||
|
||||
### Skill Creator
|
||||
|
||||
@@ -229,7 +276,7 @@ Both options create:
|
||||
- **Instinct collections** - For continuous-learning-v2
|
||||
- **Pattern extraction** - Learns from your commit history
|
||||
|
||||
### Continuous Learning v2
|
||||
### 🧠 Continuous Learning v2
|
||||
|
||||
The instinct-based learning system automatically learns your patterns:
|
||||
|
||||
@@ -244,7 +291,7 @@ See `skills/continuous-learning-v2/` for full documentation.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
## 📋 Requirements
|
||||
|
||||
### Claude Code CLI Version
|
||||
|
||||
@@ -271,7 +318,7 @@ Duplicate hooks file detected: ./hooks/hooks.json resolves to already-loaded fil
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
## 📥 Installation
|
||||
|
||||
### Option 1: Install as Plugin (Recommended)
|
||||
|
||||
@@ -321,7 +368,7 @@ This gives you instant access to all commands, agents, skills, and hooks.
|
||||
|
||||
---
|
||||
|
||||
### Option 2: Manual Installation
|
||||
### 🔧 Option 2: Manual Installation
|
||||
|
||||
If you prefer manual control over what's installed:
|
||||
|
||||
@@ -354,7 +401,7 @@ Copy desired MCP servers from `mcp-configs/mcp-servers.json` to your `~/.claude.
|
||||
|
||||
---
|
||||
|
||||
## Key Concepts
|
||||
## 🎯 Key Concepts
|
||||
|
||||
### Agents
|
||||
|
||||
@@ -412,7 +459,7 @@ Rules are always-follow guidelines. Keep them modular:
|
||||
|
||||
---
|
||||
|
||||
## Running Tests
|
||||
## 🧪 Running Tests
|
||||
|
||||
The plugin includes a comprehensive test suite:
|
||||
|
||||
@@ -428,7 +475,7 @@ node tests/hooks/hooks.test.js
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
## 🤝 Contributing
|
||||
|
||||
**Contributions are welcome and encouraged.**
|
||||
|
||||
@@ -450,7 +497,7 @@ Please contribute! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
||||
|
||||
---
|
||||
|
||||
## Background
|
||||
## 📖 Background
|
||||
|
||||
I've been using Claude Code since the experimental rollout. Won the Anthropic x Forum Ventures hackathon in Sep 2025 building [zenith.chat](https://zenith.chat) with [@DRodriguezFX](https://x.com/DRodriguezFX) - entirely using Claude Code.
|
||||
|
||||
@@ -458,7 +505,7 @@ These configs are battle-tested across multiple production applications.
|
||||
|
||||
---
|
||||
|
||||
## Important Notes
|
||||
## ⚠️ Important Notes
|
||||
|
||||
### Context Window Management
|
||||
|
||||
@@ -481,13 +528,13 @@ These configs work for my workflow. You should:
|
||||
|
||||
---
|
||||
|
||||
## Star History
|
||||
## 🌟 Star History
|
||||
|
||||
[](https://star-history.com/#affaan-m/everything-claude-code&Date)
|
||||
|
||||
---
|
||||
|
||||
## Links
|
||||
## 🔗 Links
|
||||
|
||||
- **Shorthand Guide (Start Here):** [The Shorthand Guide to Everything Claude Code](https://x.com/affaanmustafa/status/2012378465664745795)
|
||||
- **Longform Guide (Advanced):** [The Longform Guide to Everything Claude Code](https://x.com/affaanmustafa/status/2014040193557471352)
|
||||
@@ -496,7 +543,7 @@ These configs work for my workflow. You should:
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
## 📄 License
|
||||
|
||||
MIT - Use freely, modify as needed, contribute back if you can.
|
||||
|
||||
|
||||
@@ -7,10 +7,17 @@
|
||||

|
||||

|
||||
|
||||
<p align="left">
|
||||
<a href="README.md">English</a> |
|
||||
<span>简体中文</span>
|
||||
</p>
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**🌐 Language / 语言 / 語言**
|
||||
|
||||
[**English**](README.md) | [简体中文](README.zh-CN.md) | [繁體中文](docs/zh-TW/README.md)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
**来自 Anthropic 黑客马拉松获胜者的完整 Claude Code 配置集合。**
|
||||
|
||||
@@ -52,7 +59,47 @@
|
||||
|
||||
---
|
||||
|
||||
## 跨平台支持
|
||||
## 🚀 快速开始
|
||||
|
||||
在 2 分钟内快速上手:
|
||||
|
||||
### 第一步:安装插件
|
||||
|
||||
```bash
|
||||
# 添加市场
|
||||
/plugin marketplace add affaan-m/everything-claude-code
|
||||
|
||||
# 安装插件
|
||||
/plugin install everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
### 第二步:安装规则(必需)
|
||||
|
||||
> ⚠️ **重要提示:** Claude Code 插件无法自动分发 `rules`,需要手动安装:
|
||||
|
||||
```bash
|
||||
# 首先克隆仓库
|
||||
git clone https://github.com/affaan-m/everything-claude-code.git
|
||||
|
||||
# 复制规则(应用于所有项目)
|
||||
cp -r everything-claude-code/rules/* ~/.claude/rules/
|
||||
```
|
||||
|
||||
### 第三步:开始使用
|
||||
|
||||
```bash
|
||||
# 尝试一个命令
|
||||
/plan "添加用户认证"
|
||||
|
||||
# 查看可用命令
|
||||
/plugin list everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
✨ **完成!** 你现在可以使用 15+ 代理、30+ 技能和 20+ 命令。
|
||||
|
||||
---
|
||||
|
||||
## 🌐 跨平台支持
|
||||
|
||||
此插件现在完全支持 **Windows、macOS 和 Linux**。所有钩子和脚本都已用 Node.js 重写,以实现最大的兼容性。
|
||||
|
||||
@@ -87,7 +134,7 @@ node scripts/setup-package-manager.js --detect
|
||||
|
||||
---
|
||||
|
||||
## 里面有什么
|
||||
## 📦 里面有什么
|
||||
|
||||
这个仓库是一个 **Claude Code 插件** - 直接安装或手动复制组件。
|
||||
|
||||
@@ -192,7 +239,7 @@ everything-claude-code/
|
||||
|
||||
---
|
||||
|
||||
## 生态系统工具
|
||||
## 🛠️ 生态系统工具
|
||||
|
||||
### 技能创建器
|
||||
|
||||
@@ -227,7 +274,7 @@ everything-claude-code/
|
||||
- **直觉集合** - 用于 continuous-learning-v2
|
||||
- **模式提取** - 从你的提交历史中学习
|
||||
|
||||
### 持续学习 v2
|
||||
### 🧠 持续学习 v2
|
||||
|
||||
基于直觉的学习系统自动学习你的模式:
|
||||
|
||||
@@ -242,7 +289,7 @@ everything-claude-code/
|
||||
|
||||
---
|
||||
|
||||
## 安装
|
||||
## 📥 安装
|
||||
|
||||
### 选项 1:作为插件安装(推荐)
|
||||
|
||||
@@ -292,7 +339,7 @@ everything-claude-code/
|
||||
|
||||
---
|
||||
|
||||
### 选项 2:手动安装
|
||||
### 🔧 选项 2:手动安装
|
||||
|
||||
如果你希望对安装的内容进行手动控制:
|
||||
|
||||
@@ -325,7 +372,7 @@ cp -r everything-claude-code/skills/* ~/.claude/skills/
|
||||
|
||||
---
|
||||
|
||||
## 关键概念
|
||||
## 🎯 关键概念
|
||||
|
||||
### 代理
|
||||
|
||||
@@ -383,7 +430,7 @@ model: opus
|
||||
|
||||
---
|
||||
|
||||
## 运行测试
|
||||
## 🧪 运行测试
|
||||
|
||||
插件包含一个全面的测试套件:
|
||||
|
||||
@@ -399,7 +446,7 @@ node tests/hooks/hooks.test.js
|
||||
|
||||
---
|
||||
|
||||
## 贡献
|
||||
## 🤝 贡献
|
||||
|
||||
**欢迎并鼓励贡献。**
|
||||
|
||||
@@ -421,7 +468,7 @@ node tests/hooks/hooks.test.js
|
||||
|
||||
---
|
||||
|
||||
## 背景
|
||||
## 📖 背景
|
||||
|
||||
自实验性推出以来,我一直在使用 Claude Code。2025 年 9 月,与 [@DRodriguezFX](https://x.com/DRodriguezFX) 一起使用 Claude Code 构建 [zenith.chat](https://zenith.chat),赢得了 Anthropic x Forum Ventures 黑客马拉松。
|
||||
|
||||
@@ -429,7 +476,7 @@ node tests/hooks/hooks.test.js
|
||||
|
||||
---
|
||||
|
||||
## 重要说明
|
||||
## ⚠️ 重要说明
|
||||
|
||||
### 上下文窗口管理
|
||||
|
||||
@@ -452,13 +499,13 @@ node tests/hooks/hooks.test.js
|
||||
|
||||
---
|
||||
|
||||
## Star 历史
|
||||
## 🌟 Star 历史
|
||||
|
||||
[](https://star-history.com/#affaan-m/everything-claude-code&Date)
|
||||
|
||||
---
|
||||
|
||||
## 链接
|
||||
## 🔗 链接
|
||||
|
||||
- **精简指南(从这里开始):** [The Shorthand Guide to Everything Claude Code](https://x.com/affaanmustafa/status/2012378465664745795)
|
||||
- **详细指南(高级):** [The Longform Guide to Everything Claude Code](https://x.com/affaanmustafa/status/2014040193557471352)
|
||||
@@ -467,7 +514,7 @@ node tests/hooks/hooks.test.js
|
||||
|
||||
---
|
||||
|
||||
## 许可证
|
||||
## 📄 许可证
|
||||
|
||||
MIT - 自由使用,根据需要修改,如果可以请回馈。
|
||||
|
||||
|
||||
469
agents/python-reviewer.md
Normal file
469
agents/python-reviewer.md
Normal file
@@ -0,0 +1,469 @@
|
||||
---
|
||||
name: python-reviewer
|
||||
description: Expert Python code reviewer specializing in PEP 8 compliance, Pythonic idioms, type hints, security, and performance. Use for all Python code changes. MUST BE USED for Python projects.
|
||||
tools: ["Read", "Grep", "Glob", "Bash"]
|
||||
model: opus
|
||||
---
|
||||
|
||||
You are a senior Python code reviewer ensuring high standards of Pythonic code and best practices.
|
||||
|
||||
When invoked:
|
||||
1. Run `git diff -- '*.py'` to see recent Python file changes
|
||||
2. Run static analysis tools if available (ruff, mypy, pylint, black --check)
|
||||
3. Focus on modified `.py` files
|
||||
4. Begin review immediately
|
||||
|
||||
## Security Checks (CRITICAL)
|
||||
|
||||
- **SQL Injection**: String concatenation in database queries
|
||||
```python
|
||||
# Bad
|
||||
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
|
||||
# Good
|
||||
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
|
||||
```
|
||||
|
||||
- **Command Injection**: Unvalidated input in subprocess/os.system
|
||||
```python
|
||||
# Bad
|
||||
os.system(f"curl {url}")
|
||||
# Good
|
||||
subprocess.run(["curl", url], check=True)
|
||||
```
|
||||
|
||||
- **Path Traversal**: User-controlled file paths
|
||||
```python
|
||||
# Bad
|
||||
open(os.path.join(base_dir, user_path))
|
||||
# Good
|
||||
clean_path = os.path.normpath(user_path)
|
||||
if clean_path.startswith(".."):
|
||||
raise ValueError("Invalid path")
|
||||
safe_path = os.path.join(base_dir, clean_path)
|
||||
```
|
||||
|
||||
- **Eval/Exec Abuse**: Using eval/exec with user input
|
||||
- **Pickle Unsafe Deserialization**: Loading untrusted pickle data
|
||||
- **Hardcoded Secrets**: API keys, passwords in source
|
||||
- **Weak Crypto**: Use of MD5/SHA1 for security purposes
|
||||
- **YAML Unsafe Load**: Using yaml.load without Loader
|
||||
|
||||
## Error Handling (CRITICAL)
|
||||
|
||||
- **Bare Except Clauses**: Catching all exceptions
|
||||
```python
|
||||
# Bad
|
||||
try:
|
||||
process()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Good
|
||||
try:
|
||||
process()
|
||||
except ValueError as e:
|
||||
logger.error(f"Invalid value: {e}")
|
||||
```
|
||||
|
||||
- **Swallowing Exceptions**: Silent failures
|
||||
- **Exception Instead of Flow Control**: Using exceptions for normal control flow
|
||||
- **Missing Finally**: Resources not cleaned up
|
||||
```python
|
||||
# Bad
|
||||
f = open("file.txt")
|
||||
data = f.read()
|
||||
# If exception occurs, file never closes
|
||||
|
||||
# Good
|
||||
with open("file.txt") as f:
|
||||
data = f.read()
|
||||
# or
|
||||
f = open("file.txt")
|
||||
try:
|
||||
data = f.read()
|
||||
finally:
|
||||
f.close()
|
||||
```
|
||||
|
||||
## Type Hints (HIGH)
|
||||
|
||||
- **Missing Type Hints**: Public functions without type annotations
|
||||
```python
|
||||
# Bad
|
||||
def process_user(user_id):
|
||||
return get_user(user_id)
|
||||
|
||||
# Good
|
||||
from typing import Optional
|
||||
|
||||
def process_user(user_id: str) -> Optional[User]:
|
||||
return get_user(user_id)
|
||||
```
|
||||
|
||||
- **Using Any Instead of Specific Types**
|
||||
```python
|
||||
# Bad
|
||||
from typing import Any
|
||||
|
||||
def process(data: Any) -> Any:
|
||||
return data
|
||||
|
||||
# Good
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
def process(data: T) -> T:
|
||||
return data
|
||||
```
|
||||
|
||||
- **Incorrect Return Types**: Mismatched annotations
|
||||
- **Optional Not Used**: Nullable parameters not marked as Optional
|
||||
|
||||
## Pythonic Code (HIGH)
|
||||
|
||||
- **Not Using Context Managers**: Manual resource management
|
||||
```python
|
||||
# Bad
|
||||
f = open("file.txt")
|
||||
try:
|
||||
content = f.read()
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
# Good
|
||||
with open("file.txt") as f:
|
||||
content = f.read()
|
||||
```
|
||||
|
||||
- **C-Style Looping**: Not using comprehensions or iterators
|
||||
```python
|
||||
# Bad
|
||||
result = []
|
||||
for item in items:
|
||||
if item.active:
|
||||
result.append(item.name)
|
||||
|
||||
# Good
|
||||
result = [item.name for item in items if item.active]
|
||||
```
|
||||
|
||||
- **Checking Types with isinstance**: Using type() instead
|
||||
```python
|
||||
# Bad
|
||||
if type(obj) == str:
|
||||
process(obj)
|
||||
|
||||
# Good
|
||||
if isinstance(obj, str):
|
||||
process(obj)
|
||||
```
|
||||
|
||||
- **Not Using Enum/Magic Numbers**
|
||||
```python
|
||||
# Bad
|
||||
if status == 1:
|
||||
process()
|
||||
|
||||
# Good
|
||||
from enum import Enum
|
||||
|
||||
class Status(Enum):
|
||||
ACTIVE = 1
|
||||
INACTIVE = 2
|
||||
|
||||
if status == Status.ACTIVE:
|
||||
process()
|
||||
```
|
||||
|
||||
- **String Concatenation in Loops**: Using + for building strings
|
||||
```python
|
||||
# Bad
|
||||
result = ""
|
||||
for item in items:
|
||||
result += str(item)
|
||||
|
||||
# Good
|
||||
result = "".join(str(item) for item in items)
|
||||
```
|
||||
|
||||
- **Mutable Default Arguments**: Classic Python pitfall
|
||||
```python
|
||||
# Bad
|
||||
def process(items=[]):
|
||||
items.append("new")
|
||||
return items
|
||||
|
||||
# Good
|
||||
def process(items=None):
|
||||
if items is None:
|
||||
items = []
|
||||
items.append("new")
|
||||
return items
|
||||
```
|
||||
|
||||
## Code Quality (HIGH)
|
||||
|
||||
- **Too Many Parameters**: Functions with >5 parameters
|
||||
```python
|
||||
# Bad
|
||||
def process_user(name, email, age, address, phone, status):
|
||||
pass
|
||||
|
||||
# Good
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class UserData:
|
||||
name: str
|
||||
email: str
|
||||
age: int
|
||||
address: str
|
||||
phone: str
|
||||
status: str
|
||||
|
||||
def process_user(data: UserData):
|
||||
pass
|
||||
```
|
||||
|
||||
- **Long Functions**: Functions over 50 lines
|
||||
- **Deep Nesting**: More than 4 levels of indentation
|
||||
- **God Classes/Modules**: Too many responsibilities
|
||||
- **Duplicate Code**: Repeated patterns
|
||||
- **Magic Numbers**: Unnamed constants
|
||||
```python
|
||||
# Bad
|
||||
if len(data) > 512:
|
||||
compress(data)
|
||||
|
||||
# Good
|
||||
MAX_UNCOMPRESSED_SIZE = 512
|
||||
|
||||
if len(data) > MAX_UNCOMPRESSED_SIZE:
|
||||
compress(data)
|
||||
```
|
||||
|
||||
## Concurrency (HIGH)
|
||||
|
||||
- **Missing Lock**: Shared state without synchronization
|
||||
```python
|
||||
# Bad
|
||||
counter = 0
|
||||
|
||||
def increment():
|
||||
global counter
|
||||
counter += 1 # Race condition!
|
||||
|
||||
# Good
|
||||
import threading
|
||||
|
||||
counter = 0
|
||||
lock = threading.Lock()
|
||||
|
||||
def increment():
|
||||
global counter
|
||||
with lock:
|
||||
counter += 1
|
||||
```
|
||||
|
||||
- **Global Interpreter Lock Assumptions**: Assuming thread safety
|
||||
- **Async/Await Misuse**: Mixing sync and async code incorrectly
|
||||
|
||||
## Performance (MEDIUM)
|
||||
|
||||
- **N+1 Queries**: Database queries in loops
|
||||
```python
|
||||
# Bad
|
||||
for user in users:
|
||||
orders = get_orders(user.id) # N queries!
|
||||
|
||||
# Good
|
||||
user_ids = [u.id for u in users]
|
||||
orders = get_orders_for_users(user_ids) # 1 query
|
||||
```
|
||||
|
||||
- **Inefficient String Operations**
|
||||
```python
|
||||
# Bad
|
||||
text = "hello"
|
||||
for i in range(1000):
|
||||
text += " world" # O(n²)
|
||||
|
||||
# Good
|
||||
parts = ["hello"]
|
||||
for i in range(1000):
|
||||
parts.append(" world")
|
||||
text = "".join(parts) # O(n)
|
||||
```
|
||||
|
||||
- **List in Boolean Context**: Using len() instead of truthiness
|
||||
```python
|
||||
# Bad
|
||||
if len(items) > 0:
|
||||
process(items)
|
||||
|
||||
# Good
|
||||
if items:
|
||||
process(items)
|
||||
```
|
||||
|
||||
- **Unnecessary List Creation**: Using list() when not needed
|
||||
```python
|
||||
# Bad
|
||||
for item in list(dict.keys()):
|
||||
process(item)
|
||||
|
||||
# Good
|
||||
for item in dict:
|
||||
process(item)
|
||||
```
|
||||
|
||||
## Best Practices (MEDIUM)
|
||||
|
||||
- **PEP 8 Compliance**: Code formatting violations
|
||||
- Import order (stdlib, third-party, local)
|
||||
- Line length (default 88 for Black, 79 for PEP 8)
|
||||
- Naming conventions (snake_case for functions/variables, PascalCase for classes)
|
||||
- Spacing around operators
|
||||
|
||||
- **Docstrings**: Missing or poorly formatted docstrings
|
||||
```python
|
||||
# Bad
|
||||
def process(data):
|
||||
return data.strip()
|
||||
|
||||
# Good
|
||||
def process(data: str) -> str:
|
||||
"""Remove leading and trailing whitespace from input string.
|
||||
|
||||
Args:
|
||||
data: The input string to process.
|
||||
|
||||
Returns:
|
||||
The processed string with whitespace removed.
|
||||
"""
|
||||
return data.strip()
|
||||
```
|
||||
|
||||
- **Logging vs Print**: Using print() for logging
|
||||
```python
|
||||
# Bad
|
||||
print("Error occurred")
|
||||
|
||||
# Good
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error("Error occurred")
|
||||
```
|
||||
|
||||
- **Relative Imports**: Using relative imports in scripts
|
||||
- **Unused Imports**: Dead code
|
||||
- **Missing `if __name__ == "__main__"`**: Script entry point not guarded
|
||||
|
||||
## Python-Specific Anti-Patterns
|
||||
|
||||
- **`from module import *`**: Namespace pollution
|
||||
```python
|
||||
# Bad
|
||||
from os.path import *
|
||||
|
||||
# Good
|
||||
from os.path import join, exists
|
||||
```
|
||||
|
||||
- **Not Using `with` Statement**: Resource leaks
|
||||
- **Silencing Exceptions**: Bare `except: pass`
|
||||
- **Comparing to None with ==**
|
||||
```python
|
||||
# Bad
|
||||
if value == None:
|
||||
process()
|
||||
|
||||
# Good
|
||||
if value is None:
|
||||
process()
|
||||
```
|
||||
|
||||
- **Not Using `isinstance` for Type Checking**: Using type()
|
||||
- **Shadowing Built-ins**: Naming variables `list`, `dict`, `str`, etc.
|
||||
```python
|
||||
# Bad
|
||||
list = [1, 2, 3] # Shadows built-in list type
|
||||
|
||||
# Good
|
||||
items = [1, 2, 3]
|
||||
```
|
||||
|
||||
## Review Output Format
|
||||
|
||||
For each issue:
|
||||
```text
|
||||
[CRITICAL] SQL Injection vulnerability
|
||||
File: app/routes/user.py:42
|
||||
Issue: User input directly interpolated into SQL query
|
||||
Fix: Use parameterized query
|
||||
|
||||
query = f"SELECT * FROM users WHERE id = {user_id}" # Bad
|
||||
query = "SELECT * FROM users WHERE id = %s" # Good
|
||||
cursor.execute(query, (user_id,))
|
||||
```
|
||||
|
||||
## Diagnostic Commands
|
||||
|
||||
Run these checks:
|
||||
```bash
|
||||
# Type checking
|
||||
mypy .
|
||||
|
||||
# Linting
|
||||
ruff check .
|
||||
pylint app/
|
||||
|
||||
# Formatting check
|
||||
black --check .
|
||||
isort --check-only .
|
||||
|
||||
# Security scanning
|
||||
bandit -r .
|
||||
|
||||
# Dependencies audit
|
||||
pip-audit
|
||||
safety check
|
||||
|
||||
# Testing
|
||||
pytest --cov=app --cov-report=term-missing
|
||||
```
|
||||
|
||||
## Approval Criteria
|
||||
|
||||
- **Approve**: No CRITICAL or HIGH issues
|
||||
- **Warning**: MEDIUM issues only (can merge with caution)
|
||||
- **Block**: CRITICAL or HIGH issues found
|
||||
|
||||
## Python Version Considerations
|
||||
|
||||
- Check `pyproject.toml` or `setup.py` for Python version requirements
|
||||
- Note if code uses features from newer Python versions (type hints | 3.5+, f-strings 3.6+, walrus 3.8+, match 3.10+)
|
||||
- Flag deprecated standard library modules
|
||||
- Ensure type hints are compatible with minimum Python version
|
||||
|
||||
## Framework-Specific Checks
|
||||
|
||||
### Django
|
||||
- **N+1 Queries**: Use `select_related` and `prefetch_related`
|
||||
- **Missing migrations**: Model changes without migrations
|
||||
- **Raw SQL**: Using `raw()` or `execute()` when ORM could work
|
||||
- **Transaction management**: Missing `atomic()` for multi-step operations
|
||||
|
||||
### FastAPI/Flask
|
||||
- **CORS misconfiguration**: Overly permissive origins
|
||||
- **Dependency injection**: Proper use of Depends/injection
|
||||
- **Response models**: Missing or incorrect response models
|
||||
- **Validation**: Pydantic models for request validation
|
||||
|
||||
### Async (FastAPI/aiohttp)
|
||||
- **Blocking calls in async functions**: Using sync libraries in async context
|
||||
- **Missing await**: Forgetting to await coroutines
|
||||
- **Async generators**: Proper async iteration
|
||||
|
||||
Review with the mindset: "Would this code pass review at a top Python shop or open-source project?"
|
||||
297
commands/python-review.md
Normal file
297
commands/python-review.md
Normal file
@@ -0,0 +1,297 @@
|
||||
---
|
||||
description: Comprehensive Python code review for PEP 8 compliance, type hints, security, and Pythonic idioms. Invokes the python-reviewer agent.
|
||||
---
|
||||
|
||||
# Python Code Review
|
||||
|
||||
This command invokes the **python-reviewer** agent for comprehensive Python-specific code review.
|
||||
|
||||
## What This Command Does
|
||||
|
||||
1. **Identify Python Changes**: Find modified `.py` files via `git diff`
|
||||
2. **Run Static Analysis**: Execute `ruff`, `mypy`, `pylint`, `black --check`
|
||||
3. **Security Scan**: Check for SQL injection, command injection, unsafe deserialization
|
||||
4. **Type Safety Review**: Analyze type hints and mypy errors
|
||||
5. **Pythonic Code Check**: Verify code follows PEP 8 and Python best practices
|
||||
6. **Generate Report**: Categorize issues by severity
|
||||
|
||||
## When to Use
|
||||
|
||||
Use `/python-review` when:
|
||||
- After writing or modifying Python code
|
||||
- Before committing Python changes
|
||||
- Reviewing pull requests with Python code
|
||||
- Onboarding to a new Python codebase
|
||||
- Learning Pythonic patterns and idioms
|
||||
|
||||
## Review Categories
|
||||
|
||||
### CRITICAL (Must Fix)
|
||||
- SQL/Command injection vulnerabilities
|
||||
- Unsafe eval/exec usage
|
||||
- Pickle unsafe deserialization
|
||||
- Hardcoded credentials
|
||||
- YAML unsafe load
|
||||
- Bare except clauses hiding errors
|
||||
|
||||
### HIGH (Should Fix)
|
||||
- Missing type hints on public functions
|
||||
- Mutable default arguments
|
||||
- Swallowing exceptions silently
|
||||
- Not using context managers for resources
|
||||
- C-style looping instead of comprehensions
|
||||
- Using type() instead of isinstance()
|
||||
- Race conditions without locks
|
||||
|
||||
### MEDIUM (Consider)
|
||||
- PEP 8 formatting violations
|
||||
- Missing docstrings on public functions
|
||||
- Print statements instead of logging
|
||||
- Inefficient string operations
|
||||
- Magic numbers without named constants
|
||||
- Not using f-strings for formatting
|
||||
- Unnecessary list creation
|
||||
|
||||
## Automated Checks Run
|
||||
|
||||
```bash
|
||||
# Type checking
|
||||
mypy .
|
||||
|
||||
# Linting and formatting
|
||||
ruff check .
|
||||
black --check .
|
||||
isort --check-only .
|
||||
|
||||
# Security scanning
|
||||
bandit -r .
|
||||
|
||||
# Dependency audit
|
||||
pip-audit
|
||||
safety check
|
||||
|
||||
# Testing
|
||||
pytest --cov=app --cov-report=term-missing
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
|
||||
```text
|
||||
User: /python-review
|
||||
|
||||
Agent:
|
||||
# Python Code Review Report
|
||||
|
||||
## Files Reviewed
|
||||
- app/routes/user.py (modified)
|
||||
- app/services/auth.py (modified)
|
||||
|
||||
## Static Analysis Results
|
||||
✓ ruff: No issues
|
||||
✓ mypy: No errors
|
||||
⚠️ black: 2 files need reformatting
|
||||
✓ bandit: No security issues
|
||||
|
||||
## Issues Found
|
||||
|
||||
[CRITICAL] SQL Injection vulnerability
|
||||
File: app/routes/user.py:42
|
||||
Issue: User input directly interpolated into SQL query
|
||||
```python
|
||||
query = f"SELECT * FROM users WHERE id = {user_id}" # Bad
|
||||
```
|
||||
Fix: Use parameterized query
|
||||
```python
|
||||
query = "SELECT * FROM users WHERE id = %s" # Good
|
||||
cursor.execute(query, (user_id,))
|
||||
```
|
||||
|
||||
[HIGH] Mutable default argument
|
||||
File: app/services/auth.py:18
|
||||
Issue: Mutable default argument causes shared state
|
||||
```python
|
||||
def process_items(items=[]): # Bad
|
||||
items.append("new")
|
||||
return items
|
||||
```
|
||||
Fix: Use None as default
|
||||
```python
|
||||
def process_items(items=None): # Good
|
||||
if items is None:
|
||||
items = []
|
||||
items.append("new")
|
||||
return items
|
||||
```
|
||||
|
||||
[MEDIUM] Missing type hints
|
||||
File: app/services/auth.py:25
|
||||
Issue: Public function without type annotations
|
||||
```python
|
||||
def get_user(user_id): # Bad
|
||||
return db.find(user_id)
|
||||
```
|
||||
Fix: Add type hints
|
||||
```python
|
||||
def get_user(user_id: str) -> Optional[User]: # Good
|
||||
return db.find(user_id)
|
||||
```
|
||||
|
||||
[MEDIUM] Not using context manager
|
||||
File: app/routes/user.py:55
|
||||
Issue: File not closed on exception
|
||||
```python
|
||||
f = open("config.json") # Bad
|
||||
data = f.read()
|
||||
f.close()
|
||||
```
|
||||
Fix: Use context manager
|
||||
```python
|
||||
with open("config.json") as f: # Good
|
||||
data = f.read()
|
||||
```
|
||||
|
||||
## Summary
|
||||
- CRITICAL: 1
|
||||
- HIGH: 1
|
||||
- MEDIUM: 2
|
||||
|
||||
Recommendation: ❌ Block merge until CRITICAL issue is fixed
|
||||
|
||||
## Formatting Required
|
||||
Run: `black app/routes/user.py app/services/auth.py`
|
||||
```
|
||||
|
||||
## Approval Criteria
|
||||
|
||||
| Status | Condition |
|
||||
|--------|-----------|
|
||||
| ✅ Approve | No CRITICAL or HIGH issues |
|
||||
| ⚠️ Warning | Only MEDIUM issues (merge with caution) |
|
||||
| ❌ Block | CRITICAL or HIGH issues found |
|
||||
|
||||
## Integration with Other Commands
|
||||
|
||||
- Use `/python-test` first to ensure tests pass
|
||||
- Use `/code-review` for non-Python specific concerns
|
||||
- Use `/python-review` before committing
|
||||
- Use `/build-fix` if static analysis tools fail
|
||||
|
||||
## Framework-Specific Reviews
|
||||
|
||||
### Django Projects
|
||||
The reviewer checks for:
|
||||
- N+1 query issues (use `select_related` and `prefetch_related`)
|
||||
- Missing migrations for model changes
|
||||
- Raw SQL usage when ORM could work
|
||||
- Missing `transaction.atomic()` for multi-step operations
|
||||
|
||||
### FastAPI Projects
|
||||
The reviewer checks for:
|
||||
- CORS misconfiguration
|
||||
- Pydantic models for request validation
|
||||
- Response models correctness
|
||||
- Proper async/await usage
|
||||
- Dependency injection patterns
|
||||
|
||||
### Flask Projects
|
||||
The reviewer checks for:
|
||||
- Context management (app context, request context)
|
||||
- Proper error handling
|
||||
- Blueprint organization
|
||||
- Configuration management
|
||||
|
||||
## Related
|
||||
|
||||
- Agent: `agents/python-reviewer.md`
|
||||
- Skills: `skills/python-patterns/`, `skills/python-testing/`
|
||||
|
||||
## Common Fixes
|
||||
|
||||
### Add Type Hints
|
||||
```python
|
||||
# Before
|
||||
def calculate(x, y):
|
||||
return x + y
|
||||
|
||||
# After
|
||||
from typing import Union
|
||||
|
||||
def calculate(x: Union[int, float], y: Union[int, float]) -> Union[int, float]:
|
||||
return x + y
|
||||
```
|
||||
|
||||
### Use Context Managers
|
||||
```python
|
||||
# Before
|
||||
f = open("file.txt")
|
||||
data = f.read()
|
||||
f.close()
|
||||
|
||||
# After
|
||||
with open("file.txt") as f:
|
||||
data = f.read()
|
||||
```
|
||||
|
||||
### Use List Comprehensions
|
||||
```python
|
||||
# Before
|
||||
result = []
|
||||
for item in items:
|
||||
if item.active:
|
||||
result.append(item.name)
|
||||
|
||||
# After
|
||||
result = [item.name for item in items if item.active]
|
||||
```
|
||||
|
||||
### Fix Mutable Defaults
|
||||
```python
|
||||
# Before
|
||||
def append(value, items=[]):
|
||||
items.append(value)
|
||||
return items
|
||||
|
||||
# After
|
||||
def append(value, items=None):
|
||||
if items is None:
|
||||
items = []
|
||||
items.append(value)
|
||||
return items
|
||||
```
|
||||
|
||||
### Use f-strings (Python 3.6+)
|
||||
```python
|
||||
# Before
|
||||
name = "Alice"
|
||||
greeting = "Hello, " + name + "!"
|
||||
greeting2 = "Hello, {}".format(name)
|
||||
|
||||
# After
|
||||
greeting = f"Hello, {name}!"
|
||||
```
|
||||
|
||||
### Fix String Concatenation in Loops
|
||||
```python
|
||||
# Before
|
||||
result = ""
|
||||
for item in items:
|
||||
result += str(item)
|
||||
|
||||
# After
|
||||
result = "".join(str(item) for item in items)
|
||||
```
|
||||
|
||||
## Python Version Compatibility
|
||||
|
||||
The reviewer notes when code uses features from newer Python versions:
|
||||
|
||||
| Feature | Minimum Python |
|
||||
|---------|----------------|
|
||||
| Type hints | 3.5+ |
|
||||
| f-strings | 3.6+ |
|
||||
| Walrus operator (`:=`) | 3.8+ |
|
||||
| Position-only parameters | 3.8+ |
|
||||
| Match statements | 3.10+ |
|
||||
| Type unions (`x | None`) | 3.10+ |
|
||||
|
||||
Ensure your project's `pyproject.toml` or `setup.py` specifies the correct minimum Python version.
|
||||
@@ -7,6 +7,18 @@
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**🌐 Language / 语言 / 語言**
|
||||
|
||||
[**English**](../../README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](README.md)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
**來自 Anthropic 黑客松冠軍的完整 Claude Code 設定集合。**
|
||||
|
||||
經過 10 個月以上密集日常使用、打造真實產品所淬煉出的生產就緒代理程式、技能、鉤子、指令、規則和 MCP 設定。
|
||||
@@ -47,7 +59,47 @@
|
||||
|
||||
---
|
||||
|
||||
## 跨平台支援
|
||||
## 🚀 快速開始
|
||||
|
||||
在 2 分鐘內快速上手:
|
||||
|
||||
### 第一步:安裝外掛程式
|
||||
|
||||
```bash
|
||||
# 新增市集
|
||||
/plugin marketplace add affaan-m/everything-claude-code
|
||||
|
||||
# 安裝外掛程式
|
||||
/plugin install everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
### 第二步:安裝規則(必需)
|
||||
|
||||
> ⚠️ **重要提示:** Claude Code 外掛程式無法自動分發 `rules`,需要手動安裝:
|
||||
|
||||
```bash
|
||||
# 首先複製儲存庫
|
||||
git clone https://github.com/affaan-m/everything-claude-code.git
|
||||
|
||||
# 複製規則(應用於所有專案)
|
||||
cp -r everything-claude-code/rules/* ~/.claude/rules/
|
||||
```
|
||||
|
||||
### 第三步:開始使用
|
||||
|
||||
```bash
|
||||
# 嘗試一個指令
|
||||
/plan "新增使用者認證"
|
||||
|
||||
# 查看可用指令
|
||||
/plugin list everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
✨ **完成!** 您現在使用 15+ 代理程式、30+ 技能和 20+ 指令。
|
||||
|
||||
---
|
||||
|
||||
## 🌐 跨平台支援
|
||||
|
||||
此外掛程式現已完整支援 **Windows、macOS 和 Linux**。所有鉤子和腳本已使用 Node.js 重寫以獲得最佳相容性。
|
||||
|
||||
@@ -82,7 +134,7 @@ node scripts/setup-package-manager.js --detect
|
||||
|
||||
---
|
||||
|
||||
## 內容概覽
|
||||
## 📦 內容概覽
|
||||
|
||||
本儲存庫是一個 **Claude Code 外掛程式** - 可直接安裝或手動複製元件。
|
||||
|
||||
@@ -182,7 +234,7 @@ everything-claude-code/
|
||||
|
||||
---
|
||||
|
||||
## 生態系統工具
|
||||
## 🛠️ 生態系統工具
|
||||
|
||||
### ecc.tools - 技能建立器
|
||||
|
||||
@@ -204,7 +256,7 @@ everything-claude-code/
|
||||
|
||||
---
|
||||
|
||||
## 安裝
|
||||
## 📥 安裝
|
||||
|
||||
### 選項 1:以外掛程式安裝(建議)
|
||||
|
||||
@@ -240,7 +292,7 @@ everything-claude-code/
|
||||
|
||||
---
|
||||
|
||||
### 選項 2:手動安裝
|
||||
### 🔧 選項 2:手動安裝
|
||||
|
||||
如果您偏好手動控制安裝內容:
|
||||
|
||||
@@ -273,7 +325,7 @@ cp -r everything-claude-code/skills/* ~/.claude/skills/
|
||||
|
||||
---
|
||||
|
||||
## 核心概念
|
||||
## 🎯 核心概念
|
||||
|
||||
### 代理程式(Agents)
|
||||
|
||||
@@ -331,7 +383,7 @@ You are a senior code reviewer...
|
||||
|
||||
---
|
||||
|
||||
## 執行測試
|
||||
## 🧪 執行測試
|
||||
|
||||
外掛程式包含完整的測試套件:
|
||||
|
||||
@@ -347,7 +399,7 @@ node tests/hooks/hooks.test.js
|
||||
|
||||
---
|
||||
|
||||
## 貢獻
|
||||
## 🤝 貢獻
|
||||
|
||||
**歡迎並鼓勵貢獻。**
|
||||
|
||||
@@ -369,7 +421,7 @@ node tests/hooks/hooks.test.js
|
||||
|
||||
---
|
||||
|
||||
## 背景
|
||||
## 📖 背景
|
||||
|
||||
我從實驗性推出就開始使用 Claude Code。2025 年 9 月與 [@DRodriguezFX](https://x.com/DRodriguezFX) 一起使用 Claude Code 打造 [zenith.chat](https://zenith.chat),贏得了 Anthropic x Forum Ventures 黑客松。
|
||||
|
||||
@@ -377,7 +429,7 @@ node tests/hooks/hooks.test.js
|
||||
|
||||
---
|
||||
|
||||
## 重要注意事項
|
||||
## ⚠️ 重要注意事項
|
||||
|
||||
### 上下文視窗管理
|
||||
|
||||
@@ -400,13 +452,13 @@ node tests/hooks/hooks.test.js
|
||||
|
||||
---
|
||||
|
||||
## Star 歷史
|
||||
## 🌟 Star 歷史
|
||||
|
||||
[](https://star-history.com/#affaan-m/everything-claude-code&Date)
|
||||
|
||||
---
|
||||
|
||||
## 連結
|
||||
## 🔗 連結
|
||||
|
||||
- **簡明指南(從這裡開始):** [Everything Claude Code 簡明指南](https://x.com/affaanmustafa/status/2012378465664745795)
|
||||
- **完整指南(進階):** [Everything Claude Code 完整指南](https://x.com/affaanmustafa/status/2014040193557471352)
|
||||
@@ -415,7 +467,7 @@ node tests/hooks/hooks.test.js
|
||||
|
||||
---
|
||||
|
||||
## 授權
|
||||
## 📄 授權
|
||||
|
||||
MIT - 自由使用、依需求修改、如可能請回饋貢獻。
|
||||
|
||||
|
||||
733
skills/django-patterns/SKILL.md
Normal file
733
skills/django-patterns/SKILL.md
Normal file
@@ -0,0 +1,733 @@
|
||||
---
|
||||
name: django-patterns
|
||||
description: Django architecture patterns, REST API design with DRF, ORM best practices, caching, signals, middleware, and production-grade Django apps.
|
||||
---
|
||||
|
||||
# Django Development Patterns
|
||||
|
||||
Production-grade Django architecture patterns for scalable, maintainable applications.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Building Django web applications
|
||||
- Designing Django REST Framework APIs
|
||||
- Working with Django ORM and models
|
||||
- Setting up Django project structure
|
||||
- Implementing caching, signals, middleware
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Recommended Layout
|
||||
|
||||
```
|
||||
myproject/
|
||||
├── config/
|
||||
│ ├── __init__.py
|
||||
│ ├── settings/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── base.py # Base settings
|
||||
│ │ ├── development.py # Dev settings
|
||||
│ │ ├── production.py # Production settings
|
||||
│ │ └── test.py # Test settings
|
||||
│ ├── urls.py
|
||||
│ ├── wsgi.py
|
||||
│ └── asgi.py
|
||||
├── manage.py
|
||||
└── apps/
|
||||
├── __init__.py
|
||||
├── users/
|
||||
│ ├── __init__.py
|
||||
│ ├── models.py
|
||||
│ ├── views.py
|
||||
│ ├── serializers.py
|
||||
│ ├── urls.py
|
||||
│ ├── permissions.py
|
||||
│ ├── filters.py
|
||||
│ ├── services.py
|
||||
│ └── tests/
|
||||
└── products/
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Split Settings Pattern
|
||||
|
||||
```python
|
||||
# config/settings/base.py
|
||||
from pathlib import Path
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
SECRET_KEY = env('DJANGO_SECRET_KEY')
|
||||
DEBUG = False
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'corsheaders',
|
||||
# Local apps
|
||||
'apps.users',
|
||||
'apps.products',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'config.urls'
|
||||
WSGI_APPLICATION = 'config.wsgi.application'
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': env('DB_NAME'),
|
||||
'USER': env('DB_USER'),
|
||||
'PASSWORD': env('DB_PASSWORD'),
|
||||
'HOST': env('DB_HOST'),
|
||||
'PORT': env('DB_PORT', default='5432'),
|
||||
}
|
||||
}
|
||||
|
||||
# config/settings/development.py
|
||||
from .base import *
|
||||
|
||||
DEBUG = True
|
||||
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
|
||||
|
||||
DATABASES['default']['NAME'] = 'myproject_dev'
|
||||
|
||||
INSTALLED_APPS += ['debug_toolbar']
|
||||
|
||||
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
|
||||
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
|
||||
# config/settings/production.py
|
||||
from .base import *
|
||||
|
||||
DEBUG = False
|
||||
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')
|
||||
SECURE_SSL_REDIRECT = True
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
SECURE_HSTS_SECONDS = 31536000
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
SECURE_HSTS_PRELOAD = True
|
||||
|
||||
# Logging
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'file': {
|
||||
'level': 'WARNING',
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': '/var/log/django/django.log',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'django': {
|
||||
'handlers': ['file'],
|
||||
'level': 'WARNING',
|
||||
'propagate': True,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Model Design Patterns
|
||||
|
||||
### Model Best Practices
|
||||
|
||||
```python
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
|
||||
class User(AbstractUser):
|
||||
"""Custom user model extending AbstractUser."""
|
||||
email = models.EmailField(unique=True)
|
||||
phone = models.CharField(max_length=20, blank=True)
|
||||
birth_date = models.DateField(null=True, blank=True)
|
||||
|
||||
USERNAME_FIELD = 'email'
|
||||
REQUIRED_FIELDS = ['username']
|
||||
|
||||
class Meta:
|
||||
db_table = 'users'
|
||||
verbose_name = 'user'
|
||||
verbose_name_plural = 'users'
|
||||
ordering = ['-date_joined']
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
||||
|
||||
def get_full_name(self):
|
||||
return f"{self.first_name} {self.last_name}".strip()
|
||||
|
||||
class Product(models.Model):
|
||||
"""Product model with proper field configuration."""
|
||||
name = models.CharField(max_length=200)
|
||||
slug = models.SlugField(unique=True, max_length=250)
|
||||
description = models.TextField(blank=True)
|
||||
price = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
validators=[MinValueValidator(0)]
|
||||
)
|
||||
stock = models.PositiveIntegerField(default=0)
|
||||
is_active = models.BooleanField(default=True)
|
||||
category = models.ForeignKey(
|
||||
'Category',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='products'
|
||||
)
|
||||
tags = models.ManyToManyField('Tag', blank=True, related_name='products')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'products'
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['slug']),
|
||||
models.Index(fields=['-created_at']),
|
||||
models.Index(fields=['category', 'is_active']),
|
||||
]
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
check=models.Q(price__gte=0),
|
||||
name='price_non_negative'
|
||||
)
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super().save(*args, **kwargs)
|
||||
```
|
||||
|
||||
### QuerySet Best Practices
|
||||
|
||||
```python
|
||||
from django.db import models
|
||||
|
||||
class ProductQuerySet(models.QuerySet):
|
||||
"""Custom QuerySet for Product model."""
|
||||
|
||||
def active(self):
|
||||
"""Return only active products."""
|
||||
return self.filter(is_active=True)
|
||||
|
||||
def with_category(self):
|
||||
"""Select related category to avoid N+1 queries."""
|
||||
return self.select_related('category')
|
||||
|
||||
def with_tags(self):
|
||||
"""Prefetch tags for many-to-many relationship."""
|
||||
return self.prefetch_related('tags')
|
||||
|
||||
def in_stock(self):
|
||||
"""Return products with stock > 0."""
|
||||
return self.filter(stock__gt=0)
|
||||
|
||||
def search(self, query):
|
||||
"""Search products by name or description."""
|
||||
return self.filter(
|
||||
models.Q(name__icontains=query) |
|
||||
models.Q(description__icontains=query)
|
||||
)
|
||||
|
||||
class Product(models.Model):
|
||||
# ... fields ...
|
||||
|
||||
objects = ProductQuerySet.as_manager() # Use custom QuerySet
|
||||
|
||||
# Usage
|
||||
Product.objects.active().with_category().in_stock()
|
||||
```
|
||||
|
||||
### Manager Methods
|
||||
|
||||
```python
|
||||
class ProductManager(models.Manager):
|
||||
"""Custom manager for complex queries."""
|
||||
|
||||
def get_or_none(self, **kwargs):
|
||||
"""Return object or None instead of DoesNotExist."""
|
||||
try:
|
||||
return self.get(**kwargs)
|
||||
except self.model.DoesNotExist:
|
||||
return None
|
||||
|
||||
def create_with_tags(self, name, price, tag_names):
|
||||
"""Create product with associated tags."""
|
||||
product = self.create(name=name, price=price)
|
||||
tags = [Tag.objects.get_or_create(name=name)[0] for name in tag_names]
|
||||
product.tags.set(tags)
|
||||
return product
|
||||
|
||||
def bulk_update_stock(self, product_ids, quantity):
|
||||
"""Bulk update stock for multiple products."""
|
||||
return self.filter(id__in=product_ids).update(stock=quantity)
|
||||
|
||||
# In model
|
||||
class Product(models.Model):
|
||||
# ... fields ...
|
||||
custom = ProductManager()
|
||||
```
|
||||
|
||||
## Django REST Framework Patterns
|
||||
|
||||
### Serializer Patterns
|
||||
|
||||
```python
|
||||
from rest_framework import serializers
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from .models import Product, User
|
||||
|
||||
class ProductSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for Product model."""
|
||||
|
||||
category_name = serializers.CharField(source='category.name', read_only=True)
|
||||
average_rating = serializers.FloatField(read_only=True)
|
||||
discount_price = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = [
|
||||
'id', 'name', 'slug', 'description', 'price',
|
||||
'discount_price', 'stock', 'category_name',
|
||||
'average_rating', 'created_at'
|
||||
]
|
||||
read_only_fields = ['id', 'slug', 'created_at']
|
||||
|
||||
def get_discount_price(self, obj):
|
||||
"""Calculate discount price if applicable."""
|
||||
if hasattr(obj, 'discount') and obj.discount:
|
||||
return obj.price * (1 - obj.discount.percent / 100)
|
||||
return obj.price
|
||||
|
||||
def validate_price(self, value):
|
||||
"""Ensure price is non-negative."""
|
||||
if value < 0:
|
||||
raise serializers.ValidationError("Price cannot be negative.")
|
||||
return value
|
||||
|
||||
class ProductCreateSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for creating products."""
|
||||
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['name', 'description', 'price', 'stock', 'category']
|
||||
|
||||
def validate(self, data):
|
||||
"""Custom validation for multiple fields."""
|
||||
if data['price'] > 10000 and data['stock'] > 100:
|
||||
raise serializers.ValidationError(
|
||||
"Cannot have high-value products with large stock."
|
||||
)
|
||||
return data
|
||||
|
||||
class UserRegistrationSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for user registration."""
|
||||
|
||||
password = serializers.CharField(
|
||||
write_only=True,
|
||||
required=True,
|
||||
validators=[validate_password],
|
||||
style={'input_type': 'password'}
|
||||
)
|
||||
password_confirm = serializers.CharField(write_only=True, style={'input_type': 'password'})
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['email', 'username', 'password', 'password_confirm']
|
||||
|
||||
def validate(self, data):
|
||||
"""Validate passwords match."""
|
||||
if data['password'] != data['password_confirm']:
|
||||
raise serializers.ValidationError({
|
||||
"password_confirm": "Password fields didn't match."
|
||||
})
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Create user with hashed password."""
|
||||
validated_data.pop('password_confirm')
|
||||
password = validated_data.pop('password')
|
||||
user = User.objects.create(**validated_data)
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
return user
|
||||
```
|
||||
|
||||
### ViewSet Patterns
|
||||
|
||||
```python
|
||||
from rest_framework import viewsets, status, filters
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated, IsAdminUser
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from .models import Product
|
||||
from .serializers import ProductSerializer, ProductCreateSerializer
|
||||
from .permissions import IsOwnerOrReadOnly
|
||||
from .filters import ProductFilter
|
||||
from .services import ProductService
|
||||
|
||||
class ProductViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for Product model."""
|
||||
|
||||
queryset = Product.objects.select_related('category').prefetch_related('tags')
|
||||
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_class = ProductFilter
|
||||
search_fields = ['name', 'description']
|
||||
ordering_fields = ['price', 'created_at', 'name']
|
||||
ordering = ['-created_at']
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""Return appropriate serializer based on action."""
|
||||
if self.action == 'create':
|
||||
return ProductCreateSerializer
|
||||
return ProductSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Save with user context."""
|
||||
serializer.save(created_by=self.request.user)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def featured(self, request):
|
||||
"""Return featured products."""
|
||||
featured = self.queryset.filter(is_featured=True)[:10]
|
||||
serializer = self.get_serializer(featured, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def purchase(self, request, pk=None):
|
||||
"""Purchase a product."""
|
||||
product = self.get_object()
|
||||
service = ProductService()
|
||||
result = service.purchase(product, request.user)
|
||||
return Response(result, status=status.HTTP_201_CREATED)
|
||||
|
||||
@action(detail=False, methods=['get'], permission_classes=[IsAuthenticated])
|
||||
def my_products(self, request):
|
||||
"""Return products created by current user."""
|
||||
products = self.queryset.filter(created_by=request.user)
|
||||
page = self.paginate_queryset(products)
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
```
|
||||
|
||||
### Custom Actions
|
||||
|
||||
```python
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def add_to_cart(request):
|
||||
"""Add product to user cart."""
|
||||
product_id = request.data.get('product_id')
|
||||
quantity = request.data.get('quantity', 1)
|
||||
|
||||
try:
|
||||
product = Product.objects.get(id=product_id)
|
||||
except Product.DoesNotExist:
|
||||
return Response(
|
||||
{'error': 'Product not found'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
cart, _ = Cart.objects.get_or_create(user=request.user)
|
||||
CartItem.objects.create(
|
||||
cart=cart,
|
||||
product=product,
|
||||
quantity=quantity
|
||||
)
|
||||
|
||||
return Response({'message': 'Added to cart'}, status=status.HTTP_201_CREATED)
|
||||
```
|
||||
|
||||
## Service Layer Pattern
|
||||
|
||||
```python
|
||||
# apps/orders/services.py
|
||||
from typing import Optional
|
||||
from django.db import transaction
|
||||
from .models import Order, OrderItem
|
||||
|
||||
class OrderService:
|
||||
"""Service layer for order-related business logic."""
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def create_order(user, cart: Cart) -> Order:
|
||||
"""Create order from cart."""
|
||||
order = Order.objects.create(
|
||||
user=user,
|
||||
total_price=cart.total_price
|
||||
)
|
||||
|
||||
for item in cart.items.all():
|
||||
OrderItem.objects.create(
|
||||
order=order,
|
||||
product=item.product,
|
||||
quantity=item.quantity,
|
||||
price=item.product.price
|
||||
)
|
||||
|
||||
# Clear cart
|
||||
cart.items.all().delete()
|
||||
|
||||
return order
|
||||
|
||||
@staticmethod
|
||||
def process_payment(order: Order, payment_data: dict) -> bool:
|
||||
"""Process payment for order."""
|
||||
# Integration with payment gateway
|
||||
payment = PaymentGateway.charge(
|
||||
amount=order.total_price,
|
||||
token=payment_data['token']
|
||||
)
|
||||
|
||||
if payment.success:
|
||||
order.status = Order.Status.PAID
|
||||
order.save()
|
||||
# Send confirmation email
|
||||
OrderService.send_confirmation_email(order)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def send_confirmation_email(order: Order):
|
||||
"""Send order confirmation email."""
|
||||
# Email sending logic
|
||||
pass
|
||||
```
|
||||
|
||||
## Caching Strategies
|
||||
|
||||
### View-Level Caching
|
||||
|
||||
```python
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
@method_decorator(cache_page(60 * 15), name='dispatch') # 15 minutes
|
||||
class ProductListView(generic.ListView):
|
||||
model = Product
|
||||
template_name = 'products/list.html'
|
||||
context_object_name = 'products'
|
||||
```
|
||||
|
||||
### Template Fragment Caching
|
||||
|
||||
```django
|
||||
{% load cache %}
|
||||
{% cache 500 sidebar %}
|
||||
... expensive sidebar content ...
|
||||
{% endcache %}
|
||||
```
|
||||
|
||||
### Low-Level Caching
|
||||
|
||||
```python
|
||||
from django.core.cache import cache
|
||||
|
||||
def get_featured_products():
|
||||
"""Get featured products with caching."""
|
||||
cache_key = 'featured_products'
|
||||
products = cache.get(cache_key)
|
||||
|
||||
if products is None:
|
||||
products = list(Product.objects.filter(is_featured=True))
|
||||
cache.set(cache_key, products, timeout=60 * 15) # 15 minutes
|
||||
|
||||
return products
|
||||
```
|
||||
|
||||
### QuerySet Caching
|
||||
|
||||
```python
|
||||
from django.core.cache import cache
|
||||
|
||||
def get_popular_categories():
|
||||
cache_key = 'popular_categories'
|
||||
categories = cache.get(cache_key)
|
||||
|
||||
if categories is None:
|
||||
categories = list(Category.objects.annotate(
|
||||
product_count=Count('products')
|
||||
).filter(product_count__gt=10).order_by('-product_count')[:20])
|
||||
cache.set(cache_key, categories, timeout=60 * 60) # 1 hour
|
||||
|
||||
return categories
|
||||
```
|
||||
|
||||
## Signals
|
||||
|
||||
### Signal Patterns
|
||||
|
||||
```python
|
||||
# apps/users/signals.py
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.contrib.auth import get_user_model
|
||||
from .models import Profile
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_user_profile(sender, instance, created, **kwargs):
|
||||
"""Create profile when user is created."""
|
||||
if created:
|
||||
Profile.objects.create(user=instance)
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def save_user_profile(sender, instance, **kwargs):
|
||||
"""Save profile when user is saved."""
|
||||
instance.profile.save()
|
||||
|
||||
# apps/users/apps.py
|
||||
from django.apps import AppConfig
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.users'
|
||||
|
||||
def ready(self):
|
||||
"""Import signals when app is ready."""
|
||||
import apps.users.signals
|
||||
```
|
||||
|
||||
## Middleware
|
||||
|
||||
### Custom Middleware
|
||||
|
||||
```python
|
||||
# middleware/active_user_middleware.py
|
||||
import time
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
|
||||
class ActiveUserMiddleware(MiddlewareMixin):
|
||||
"""Middleware to track active users."""
|
||||
|
||||
def process_request(self, request):
|
||||
"""Process incoming request."""
|
||||
if request.user.is_authenticated:
|
||||
# Update last active time
|
||||
request.user.last_active = timezone.now()
|
||||
request.user.save(update_fields=['last_active'])
|
||||
|
||||
class RequestLoggingMiddleware(MiddlewareMixin):
|
||||
"""Middleware for logging requests."""
|
||||
|
||||
def process_request(self, request):
|
||||
"""Log request start time."""
|
||||
request.start_time = time.time()
|
||||
|
||||
def process_response(self, request, response):
|
||||
"""Log request duration."""
|
||||
if hasattr(request, 'start_time'):
|
||||
duration = time.time() - request.start_time
|
||||
logger.info(f'{request.method} {request.path} - {response.status_code} - {duration:.3f}s')
|
||||
return response
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### N+1 Query Prevention
|
||||
|
||||
```python
|
||||
# Bad - N+1 queries
|
||||
products = Product.objects.all()
|
||||
for product in products:
|
||||
print(product.category.name) # Separate query for each product
|
||||
|
||||
# Good - Single query with select_related
|
||||
products = Product.objects.select_related('category').all()
|
||||
for product in products:
|
||||
print(product.category.name)
|
||||
|
||||
# Good - Prefetch for many-to-many
|
||||
products = Product.objects.prefetch_related('tags').all()
|
||||
for product in products:
|
||||
for tag in product.tags.all():
|
||||
print(tag.name)
|
||||
```
|
||||
|
||||
### Database Indexing
|
||||
|
||||
```python
|
||||
class Product(models.Model):
|
||||
name = models.CharField(max_length=200, db_index=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
category = models.ForeignKey('Category', on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=['name']),
|
||||
models.Index(fields=['-created_at']),
|
||||
models.Index(fields=['category', 'created_at']),
|
||||
]
|
||||
```
|
||||
|
||||
### Bulk Operations
|
||||
|
||||
```python
|
||||
# Bulk create
|
||||
Product.objects.bulk_create([
|
||||
Product(name=f'Product {i}', price=10.00)
|
||||
for i in range(1000)
|
||||
])
|
||||
|
||||
# Bulk update
|
||||
products = Product.objects.all()[:100]
|
||||
for product in products:
|
||||
product.is_active = True
|
||||
Product.objects.bulk_update(products, ['is_active'])
|
||||
|
||||
# Bulk delete
|
||||
Product.objects.filter(stock=0).delete()
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Description |
|
||||
|---------|-------------|
|
||||
| Split settings | Separate dev/prod/test settings |
|
||||
| Custom QuerySet | Reusable query methods |
|
||||
| Service Layer | Business logic separation |
|
||||
| ViewSet | REST API endpoints |
|
||||
| Serializer validation | Request/response transformation |
|
||||
| select_related | Foreign key optimization |
|
||||
| prefetch_related | Many-to-many optimization |
|
||||
| Cache first | Cache expensive operations |
|
||||
| Signals | Event-driven actions |
|
||||
| Middleware | Request/response processing |
|
||||
|
||||
Remember: Django provides many shortcuts, but for production applications, structure and organization matter more than concise code. Build for maintainability.
|
||||
592
skills/django-security/SKILL.md
Normal file
592
skills/django-security/SKILL.md
Normal file
@@ -0,0 +1,592 @@
|
||||
---
|
||||
name: django-security
|
||||
description: Django security best practices, authentication, authorization, CSRF protection, SQL injection prevention, XSS prevention, and secure deployment configurations.
|
||||
---
|
||||
|
||||
# Django Security Best Practices
|
||||
|
||||
Comprehensive security guidelines for Django applications to protect against common vulnerabilities.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Setting up Django authentication and authorization
|
||||
- Implementing user permissions and roles
|
||||
- Configuring production security settings
|
||||
- Reviewing Django application for security issues
|
||||
- Deploying Django applications to production
|
||||
|
||||
## Core Security Settings
|
||||
|
||||
### Production Settings Configuration
|
||||
|
||||
```python
|
||||
# settings/production.py
|
||||
import os
|
||||
|
||||
DEBUG = False # CRITICAL: Never use True in production
|
||||
|
||||
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')
|
||||
|
||||
# Security headers
|
||||
SECURE_SSL_REDIRECT = True
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
SECURE_HSTS_SECONDS = 31536000 # 1 year
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
SECURE_HSTS_PRELOAD = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
X_FRAME_OPTIONS = 'DENY'
|
||||
|
||||
# HTTPS and Cookies
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
CSRF_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||
CSRF_COOKIE_SAMESITE = 'Lax'
|
||||
|
||||
# Secret key (must be set via environment variable)
|
||||
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
|
||||
if not SECRET_KEY:
|
||||
raise ImproperlyConfigured('DJANGO_SECRET_KEY environment variable is required')
|
||||
|
||||
# Password validation
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
'OPTIONS': {
|
||||
'min_length': 12,
|
||||
}
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
### Custom User Model
|
||||
|
||||
```python
|
||||
# apps/users/models.py
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
class User(AbstractUser):
|
||||
"""Custom user model for better security."""
|
||||
|
||||
email = models.EmailField(unique=True)
|
||||
phone = models.CharField(max_length=20, blank=True)
|
||||
|
||||
USERNAME_FIELD = 'email' # Use email as username
|
||||
REQUIRED_FIELDS = ['username']
|
||||
|
||||
class Meta:
|
||||
db_table = 'users'
|
||||
verbose_name = 'User'
|
||||
verbose_name_plural = 'Users'
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
||||
|
||||
# settings/base.py
|
||||
AUTH_USER_MODEL = 'users.User'
|
||||
```
|
||||
|
||||
### Password Hashing
|
||||
|
||||
```python
|
||||
# Django uses PBKDF2 by default. For stronger security:
|
||||
PASSWORD_HASHERS = [
|
||||
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||
]
|
||||
```
|
||||
|
||||
### Session Management
|
||||
|
||||
```python
|
||||
# Session configuration
|
||||
SESSION_ENGINE = 'django.contrib.sessions.backends.cache' # Or 'db'
|
||||
SESSION_CACHE_ALIAS = 'default'
|
||||
SESSION_COOKIE_AGE = 3600 * 24 * 7 # 1 week
|
||||
SESSION_SAVE_EVERY_REQUEST = False
|
||||
SESSION_EXPIRE_AT_BROWSER_CLOSE = False # Better UX, but less secure
|
||||
```
|
||||
|
||||
## Authorization
|
||||
|
||||
### Permissions
|
||||
|
||||
```python
|
||||
# models.py
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
class Post(models.Model):
|
||||
title = models.CharField(max_length=200)
|
||||
content = models.TextField()
|
||||
author = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
permissions = [
|
||||
('can_publish', 'Can publish posts'),
|
||||
('can_edit_others', 'Can edit posts of others'),
|
||||
]
|
||||
|
||||
def user_can_edit(self, user):
|
||||
"""Check if user can edit this post."""
|
||||
return self.author == user or user.has_perm('app.can_edit_others')
|
||||
|
||||
# views.py
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
||||
from django.views.generic import UpdateView
|
||||
|
||||
class PostUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||||
model = Post
|
||||
permission_required = 'app.can_edit_others'
|
||||
raise_exception = True # Return 403 instead of redirect
|
||||
|
||||
def get_queryset(self):
|
||||
"""Only allow users to edit their own posts."""
|
||||
return Post.objects.filter(author=self.request.user)
|
||||
```
|
||||
|
||||
### Custom Permissions
|
||||
|
||||
```python
|
||||
# permissions.py
|
||||
from rest_framework import permissions
|
||||
|
||||
class IsOwnerOrReadOnly(permissions.BasePermission):
|
||||
"""Allow only owners to edit objects."""
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
# Read permissions allowed for any request
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return True
|
||||
|
||||
# Write permissions only for owner
|
||||
return obj.author == request.user
|
||||
|
||||
class IsAdminOrReadOnly(permissions.BasePermission):
|
||||
"""Allow admins to do anything, others read-only."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return True
|
||||
return request.user and request.user.is_staff
|
||||
|
||||
class IsVerifiedUser(permissions.BasePermission):
|
||||
"""Allow only verified users."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return request.user and request.user.is_authenticated and request.user.is_verified
|
||||
```
|
||||
|
||||
### Role-Based Access Control (RBAC)
|
||||
|
||||
```python
|
||||
# models.py
|
||||
from django.contrib.auth.models import AbstractUser, Group
|
||||
|
||||
class User(AbstractUser):
|
||||
ROLE_CHOICES = [
|
||||
('admin', 'Administrator'),
|
||||
('moderator', 'Moderator'),
|
||||
('user', 'Regular User'),
|
||||
]
|
||||
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='user')
|
||||
|
||||
def is_admin(self):
|
||||
return self.role == 'admin' or self.is_superuser
|
||||
|
||||
def is_moderator(self):
|
||||
return self.role in ['admin', 'moderator']
|
||||
|
||||
# Mixins
|
||||
class AdminRequiredMixin:
|
||||
"""Mixin to require admin role."""
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.user.is_authenticated or not request.user.is_admin():
|
||||
from django.core.exceptions import PermissionDenied
|
||||
raise PermissionDenied
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
```
|
||||
|
||||
## SQL Injection Prevention
|
||||
|
||||
### Django ORM Protection
|
||||
|
||||
```python
|
||||
# GOOD: Django ORM automatically escapes parameters
|
||||
def get_user(username):
|
||||
return User.objects.get(username=username) # Safe
|
||||
|
||||
# GOOD: Using parameters with raw()
|
||||
def search_users(query):
|
||||
return User.objects.raw('SELECT * FROM users WHERE username = %s', [query])
|
||||
|
||||
# BAD: Never directly interpolate user input
|
||||
def get_user_bad(username):
|
||||
return User.objects.raw(f'SELECT * FROM users WHERE username = {username}') # VULNERABLE!
|
||||
|
||||
# GOOD: Using filter with proper escaping
|
||||
def get_users_by_email(email):
|
||||
return User.objects.filter(email__iexact=email) # Safe
|
||||
|
||||
# GOOD: Using Q objects for complex queries
|
||||
from django.db.models import Q
|
||||
def search_users_complex(query):
|
||||
return User.objects.filter(
|
||||
Q(username__icontains=query) |
|
||||
Q(email__icontains=query)
|
||||
) # Safe
|
||||
```
|
||||
|
||||
### Extra Security with raw()
|
||||
|
||||
```python
|
||||
# If you must use raw SQL, always use parameters
|
||||
User.objects.raw(
|
||||
'SELECT * FROM users WHERE email = %s AND status = %s',
|
||||
[user_input_email, status]
|
||||
)
|
||||
```
|
||||
|
||||
## XSS Prevention
|
||||
|
||||
### Template Escaping
|
||||
|
||||
```django
|
||||
{# Django auto-escapes variables by default - SAFE #}
|
||||
{{ user_input }} {# Escaped HTML #}
|
||||
|
||||
{# Explicitly mark safe only for trusted content #}
|
||||
{{ trusted_html|safe }} {# Not escaped #}
|
||||
|
||||
{# Use template filters for safe HTML #}
|
||||
{{ user_input|escape }} {# Same as default #}
|
||||
{{ user_input|striptags }} {# Remove all HTML tags #}
|
||||
|
||||
{# JavaScript escaping #}
|
||||
<script>
|
||||
var username = {{ username|escapejs }};
|
||||
</script>
|
||||
```
|
||||
|
||||
### Safe String Handling
|
||||
|
||||
```python
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.html import escape
|
||||
|
||||
# BAD: Never mark user input as safe without escaping
|
||||
def render_bad(user_input):
|
||||
return mark_safe(user_input) # VULNERABLE!
|
||||
|
||||
# GOOD: Escape first, then mark safe
|
||||
def render_good(user_input):
|
||||
return mark_safe(escape(user_input))
|
||||
|
||||
# GOOD: Use format_html for HTML with variables
|
||||
from django.utils.html import format_html
|
||||
|
||||
def greet_user(username):
|
||||
return format_html('<span class="user">{}</span>', escape(username))
|
||||
```
|
||||
|
||||
### HTTP Headers
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True # Prevent MIME sniffing
|
||||
SECURE_BROWSER_XSS_FILTER = True # Enable XSS filter
|
||||
X_FRAME_OPTIONS = 'DENY' # Prevent clickjacking
|
||||
|
||||
# Custom middleware
|
||||
from django.conf import settings
|
||||
|
||||
class SecurityHeaderMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
response = self.get_response(request)
|
||||
response['X-Content-Type-Options'] = 'nosniff'
|
||||
response['X-Frame-Options'] = 'DENY'
|
||||
response['X-XSS-Protection'] = '1; mode=block'
|
||||
response['Content-Security-Policy'] = "default-src 'self'"
|
||||
return response
|
||||
```
|
||||
|
||||
## CSRF Protection
|
||||
|
||||
### Default CSRF Protection
|
||||
|
||||
```python
|
||||
# settings.py - CSRF is enabled by default
|
||||
CSRF_COOKIE_SECURE = True # Only send over HTTPS
|
||||
CSRF_COOKIE_HTTPONLY = True # Prevent JavaScript access
|
||||
CSRF_COOKIE_SAMESITE = 'Lax' # Prevent CSRF in some cases
|
||||
CSRF_TRUSTED_ORIGINS = ['https://example.com'] # Trusted domains
|
||||
|
||||
# Template usage
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
# AJAX requests
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
fetch('/api/endpoint/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken'),
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
```
|
||||
|
||||
### Exempting Views (Use Carefully)
|
||||
|
||||
```python
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
@csrf_exempt # Only use when absolutely necessary!
|
||||
def webhook_view(request):
|
||||
# Webhook from external service
|
||||
pass
|
||||
```
|
||||
|
||||
## File Upload Security
|
||||
|
||||
### File Validation
|
||||
|
||||
```python
|
||||
import os
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
def validate_file_extension(value):
|
||||
"""Validate file extension."""
|
||||
ext = os.path.splitext(value.name)[1]
|
||||
valid_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.pdf']
|
||||
if not ext.lower() in valid_extensions:
|
||||
raise ValidationError('Unsupported file extension.')
|
||||
|
||||
def validate_file_size(value):
|
||||
"""Validate file size (max 5MB)."""
|
||||
filesize = value.size
|
||||
if filesize > 5 * 1024 * 1024:
|
||||
raise ValidationError('File too large. Max size is 5MB.')
|
||||
|
||||
# models.py
|
||||
class Document(models.Model):
|
||||
file = models.FileField(
|
||||
upload_to='documents/',
|
||||
validators=[validate_file_extension, validate_file_size]
|
||||
)
|
||||
```
|
||||
|
||||
### Secure File Storage
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
MEDIA_ROOT = '/var/www/media/'
|
||||
MEDIA_URL = '/media/'
|
||||
|
||||
# Use a separate domain for media in production
|
||||
MEDIA_DOMAIN = 'https://media.example.com'
|
||||
|
||||
# Don't serve user uploads directly
|
||||
# Use whitenoise or a CDN for static files
|
||||
# Use a separate server or S3 for media files
|
||||
```
|
||||
|
||||
## API Security
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_THROTTLE_CLASSES': [
|
||||
'rest_framework.throttling.AnonRateThrottle',
|
||||
'rest_framework.throttling.UserRateThrottle'
|
||||
],
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'anon': '100/day',
|
||||
'user': '1000/day',
|
||||
'upload': '10/hour',
|
||||
}
|
||||
}
|
||||
|
||||
# Custom throttle
|
||||
from rest_framework.throttling import UserRateThrottle
|
||||
|
||||
class BurstRateThrottle(UserRateThrottle):
|
||||
scope = 'burst'
|
||||
rate = '60/min'
|
||||
|
||||
class SustainedRateThrottle(UserRateThrottle):
|
||||
scope = 'sustained'
|
||||
rate = '1000/day'
|
||||
```
|
||||
|
||||
### Authentication for APIs
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||
],
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
],
|
||||
}
|
||||
|
||||
# views.py
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
@api_view(['GET', 'POST'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def protected_view(request):
|
||||
return Response({'message': 'You are authenticated'})
|
||||
```
|
||||
|
||||
## Security Headers
|
||||
|
||||
### Content Security Policy
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
CSP_DEFAULT_SRC = "'self'"
|
||||
CSP_SCRIPT_SRC = "'self' https://cdn.example.com"
|
||||
CSP_STYLE_SRC = "'self' 'unsafe-inline'"
|
||||
CSP_IMG_SRC = "'self' data: https:"
|
||||
CSP_CONNECT_SRC = "'self' https://api.example.com"
|
||||
|
||||
# Middleware
|
||||
class CSPMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
response = self.get_response(request)
|
||||
response['Content-Security-Policy'] = (
|
||||
f"default-src {CSP_DEFAULT_SRC}; "
|
||||
f"script-src {CSP_SCRIPT_SRC}; "
|
||||
f"style-src {CSP_STYLE_SRC}; "
|
||||
f"img-src {CSP_IMG_SRC}; "
|
||||
f"connect-src {CSP_CONNECT_SRC}"
|
||||
)
|
||||
return response
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Managing Secrets
|
||||
|
||||
```python
|
||||
# Use python-decouple or django-environ
|
||||
import environ
|
||||
|
||||
env = environ.Env(
|
||||
# set casting, default value
|
||||
DEBUG=(bool, False)
|
||||
)
|
||||
|
||||
# reading .env file
|
||||
environ.Env.read_env()
|
||||
|
||||
SECRET_KEY = env('DJANGO_SECRET_KEY')
|
||||
DATABASE_URL = env('DATABASE_URL')
|
||||
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')
|
||||
|
||||
# .env file (never commit this)
|
||||
DEBUG=False
|
||||
SECRET_KEY=your-secret-key-here
|
||||
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
||||
ALLOWED_HOSTS=example.com,www.example.com
|
||||
```
|
||||
|
||||
## Logging Security Events
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'file': {
|
||||
'level': 'WARNING',
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': '/var/log/django/security.log',
|
||||
},
|
||||
'console': {
|
||||
'level': 'INFO',
|
||||
'class': 'logging.StreamHandler',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'django.security': {
|
||||
'handlers': ['file', 'console'],
|
||||
'level': 'WARNING',
|
||||
'propagate': True,
|
||||
},
|
||||
'django.request': {
|
||||
'handlers': ['file'],
|
||||
'level': 'ERROR',
|
||||
'propagate': False,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Security Checklist
|
||||
|
||||
| Check | Description |
|
||||
|-------|-------------|
|
||||
| `DEBUG = False` | Never run with DEBUG in production |
|
||||
| HTTPS only | Force SSL, secure cookies |
|
||||
| Strong secrets | Use environment variables for SECRET_KEY |
|
||||
| Password validation | Enable all password validators |
|
||||
| CSRF protection | Enabled by default, don't disable |
|
||||
| XSS prevention | Django auto-escapes, don't use `|safe` with user input |
|
||||
| SQL injection | Use ORM, never concatenate strings in queries |
|
||||
| File uploads | Validate file type and size |
|
||||
| Rate limiting | Throttle API endpoints |
|
||||
| Security headers | CSP, X-Frame-Options, HSTS |
|
||||
| Logging | Log security events |
|
||||
| Updates | Keep Django and dependencies updated |
|
||||
|
||||
Remember: Security is a process, not a product. Regularly review and update your security practices.
|
||||
728
skills/django-tdd/SKILL.md
Normal file
728
skills/django-tdd/SKILL.md
Normal file
@@ -0,0 +1,728 @@
|
||||
---
|
||||
name: django-tdd
|
||||
description: Django testing strategies with pytest-django, TDD methodology, factory_boy, mocking, coverage, and testing Django REST Framework APIs.
|
||||
---
|
||||
|
||||
# Django Testing with TDD
|
||||
|
||||
Test-driven development for Django applications using pytest, factory_boy, and Django REST Framework.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Writing new Django applications
|
||||
- Implementing Django REST Framework APIs
|
||||
- Testing Django models, views, and serializers
|
||||
- Setting up testing infrastructure for Django projects
|
||||
|
||||
## TDD Workflow for Django
|
||||
|
||||
### Red-Green-Refactor Cycle
|
||||
|
||||
```python
|
||||
# Step 1: RED - Write failing test
|
||||
def test_user_creation():
|
||||
user = User.objects.create_user(email='test@example.com', password='testpass123')
|
||||
assert user.email == 'test@example.com'
|
||||
assert user.check_password('testpass123')
|
||||
assert not user.is_staff
|
||||
|
||||
# Step 2: GREEN - Make test pass
|
||||
# Create User model or factory
|
||||
|
||||
# Step 3: REFACTOR - Improve while keeping tests green
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
### pytest Configuration
|
||||
|
||||
```ini
|
||||
# pytest.ini
|
||||
[pytest]
|
||||
DJANGO_SETTINGS_MODULE = config.settings.test
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
addopts =
|
||||
--reuse-db
|
||||
--nomigrations
|
||||
--cov=apps
|
||||
--cov-report=html
|
||||
--cov-report=term-missing
|
||||
--strict-markers
|
||||
markers =
|
||||
slow: marks tests as slow
|
||||
integration: marks tests as integration tests
|
||||
```
|
||||
|
||||
### Test Settings
|
||||
|
||||
```python
|
||||
# config/settings/test.py
|
||||
from .base import *
|
||||
|
||||
DEBUG = True
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ':memory:',
|
||||
}
|
||||
}
|
||||
|
||||
# Disable migrations for speed
|
||||
class DisableMigrations:
|
||||
def __contains__(self, item):
|
||||
return True
|
||||
|
||||
def __getitem__(self, item):
|
||||
return None
|
||||
|
||||
MIGRATION_MODULES = DisableMigrations()
|
||||
|
||||
# Faster password hashing
|
||||
PASSWORD_HASHERS = [
|
||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||
]
|
||||
|
||||
# Email backend
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
|
||||
# Celery always eager
|
||||
CELERY_TASK_ALWAYS_EAGER = True
|
||||
CELERY_TASK_EAGER_PROPAGATES = True
|
||||
```
|
||||
|
||||
### conftest.py
|
||||
|
||||
```python
|
||||
# tests/conftest.py
|
||||
import pytest
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def timezone_settings(settings):
|
||||
"""Ensure consistent timezone."""
|
||||
settings.TIME_ZONE = 'UTC'
|
||||
|
||||
@pytest.fixture
|
||||
def user(db):
|
||||
"""Create a test user."""
|
||||
return User.objects.create_user(
|
||||
email='test@example.com',
|
||||
password='testpass123',
|
||||
username='testuser'
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def admin_user(db):
|
||||
"""Create an admin user."""
|
||||
return User.objects.create_superuser(
|
||||
email='admin@example.com',
|
||||
password='adminpass123',
|
||||
username='admin'
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def authenticated_client(client, user):
|
||||
"""Return authenticated client."""
|
||||
client.force_login(user)
|
||||
return client
|
||||
|
||||
@pytest.fixture
|
||||
def api_client():
|
||||
"""Return DRF API client."""
|
||||
from rest_framework.test import APIClient
|
||||
return APIClient()
|
||||
|
||||
@pytest.fixture
|
||||
def authenticated_api_client(api_client, user):
|
||||
"""Return authenticated API client."""
|
||||
api_client.force_authenticate(user=user)
|
||||
return api_client
|
||||
```
|
||||
|
||||
## Factory Boy
|
||||
|
||||
### Factory Setup
|
||||
|
||||
```python
|
||||
# tests/factories.py
|
||||
import factory
|
||||
from factory import fuzzy
|
||||
from datetime import datetime, timedelta
|
||||
from django.contrib.auth import get_user_model
|
||||
from apps.products.models import Product, Category
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
class UserFactory(factory.django.DjangoModelFactory):
|
||||
"""Factory for User model."""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
||||
email = factory.Sequence(lambda n: f"user{n}@example.com")
|
||||
username = factory.Sequence(lambda n: f"user{n}")
|
||||
password = factory.PostGenerationMethodCall('set_password', 'testpass123')
|
||||
first_name = factory.Faker('first_name')
|
||||
last_name = factory.Faker('last_name')
|
||||
is_active = True
|
||||
|
||||
class CategoryFactory(factory.django.DjangoModelFactory):
|
||||
"""Factory for Category model."""
|
||||
|
||||
class Meta:
|
||||
model = Category
|
||||
|
||||
name = factory.Faker('word')
|
||||
slug = factory.LazyAttribute(lambda obj: obj.name.lower())
|
||||
description = factory.Faker('text')
|
||||
|
||||
class ProductFactory(factory.django.DjangoModelFactory):
|
||||
"""Factory for Product model."""
|
||||
|
||||
class Meta:
|
||||
model = Product
|
||||
|
||||
name = factory.Faker('sentence', nb_words=3)
|
||||
slug = factory.LazyAttribute(lambda obj: obj.name.lower().replace(' ', '-'))
|
||||
description = factory.Faker('text')
|
||||
price = fuzzy.FuzzyDecimal(10.00, 1000.00, 2)
|
||||
stock = fuzzy.FuzzyInteger(0, 100)
|
||||
is_active = True
|
||||
category = factory.SubFactory(CategoryFactory)
|
||||
created_by = factory.SubFactory(UserFactory)
|
||||
|
||||
@factory.post_generation
|
||||
def tags(self, create, extracted, **kwargs):
|
||||
"""Add tags to product."""
|
||||
if not create:
|
||||
return
|
||||
if extracted:
|
||||
for tag in extracted:
|
||||
self.tags.add(tag)
|
||||
```
|
||||
|
||||
### Using Factories
|
||||
|
||||
```python
|
||||
# tests/test_models.py
|
||||
import pytest
|
||||
from tests.factories import ProductFactory, UserFactory
|
||||
|
||||
def test_product_creation():
|
||||
"""Test product creation using factory."""
|
||||
product = ProductFactory(price=100.00, stock=50)
|
||||
assert product.price == 100.00
|
||||
assert product.stock == 50
|
||||
assert product.is_active is True
|
||||
|
||||
def test_product_with_tags():
|
||||
"""Test product with tags."""
|
||||
tags = [TagFactory(name='electronics'), TagFactory(name='new')]
|
||||
product = ProductFactory(tags=tags)
|
||||
assert product.tags.count() == 2
|
||||
|
||||
def test_multiple_products():
|
||||
"""Test creating multiple products."""
|
||||
products = ProductFactory.create_batch(10)
|
||||
assert len(products) == 10
|
||||
```
|
||||
|
||||
## Model Testing
|
||||
|
||||
### Model Tests
|
||||
|
||||
```python
|
||||
# tests/test_models.py
|
||||
import pytest
|
||||
from django.core.exceptions import ValidationError
|
||||
from tests.factories import UserFactory, ProductFactory
|
||||
|
||||
class TestUserModel:
|
||||
"""Test User model."""
|
||||
|
||||
def test_create_user(self, db):
|
||||
"""Test creating a regular user."""
|
||||
user = UserFactory(email='test@example.com')
|
||||
assert user.email == 'test@example.com'
|
||||
assert user.check_password('testpass123')
|
||||
assert not user.is_staff
|
||||
assert not user.is_superuser
|
||||
|
||||
def test_create_superuser(self, db):
|
||||
"""Test creating a superuser."""
|
||||
user = UserFactory(
|
||||
email='admin@example.com',
|
||||
is_staff=True,
|
||||
is_superuser=True
|
||||
)
|
||||
assert user.is_staff
|
||||
assert user.is_superuser
|
||||
|
||||
def test_user_str(self, db):
|
||||
"""Test user string representation."""
|
||||
user = UserFactory(email='test@example.com')
|
||||
assert str(user) == 'test@example.com'
|
||||
|
||||
class TestProductModel:
|
||||
"""Test Product model."""
|
||||
|
||||
def test_product_creation(self, db):
|
||||
"""Test creating a product."""
|
||||
product = ProductFactory()
|
||||
assert product.id is not None
|
||||
assert product.is_active is True
|
||||
assert product.created_at is not None
|
||||
|
||||
def test_product_slug_generation(self, db):
|
||||
"""Test automatic slug generation."""
|
||||
product = ProductFactory(name='Test Product')
|
||||
assert product.slug == 'test-product'
|
||||
|
||||
def test_product_price_validation(self, db):
|
||||
"""Test price cannot be negative."""
|
||||
product = ProductFactory(price=-10)
|
||||
with pytest.raises(ValidationError):
|
||||
product.full_clean()
|
||||
|
||||
def test_product_manager_active(self, db):
|
||||
"""Test active manager method."""
|
||||
ProductFactory.create_batch(5, is_active=True)
|
||||
ProductFactory.create_batch(3, is_active=False)
|
||||
|
||||
active_count = Product.objects.active().count()
|
||||
assert active_count == 5
|
||||
|
||||
def test_product_stock_management(self, db):
|
||||
"""Test stock management."""
|
||||
product = ProductFactory(stock=10)
|
||||
product.reduce_stock(5)
|
||||
product.refresh_from_db()
|
||||
assert product.stock == 5
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
product.reduce_stock(10) # Not enough stock
|
||||
```
|
||||
|
||||
## View Testing
|
||||
|
||||
### Django View Testing
|
||||
|
||||
```python
|
||||
# tests/test_views.py
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from tests.factories import ProductFactory, UserFactory
|
||||
|
||||
class TestProductViews:
|
||||
"""Test product views."""
|
||||
|
||||
def test_product_list(self, client, db):
|
||||
"""Test product list view."""
|
||||
ProductFactory.create_batch(10)
|
||||
|
||||
response = client.get(reverse('products:list'))
|
||||
|
||||
assert response.status_code == 200
|
||||
assert len(response.context['products']) == 10
|
||||
|
||||
def test_product_detail(self, client, db):
|
||||
"""Test product detail view."""
|
||||
product = ProductFactory()
|
||||
|
||||
response = client.get(reverse('products:detail', kwargs={'slug': product.slug}))
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.context['product'] == product
|
||||
|
||||
def test_product_create_requires_login(self, client, db):
|
||||
"""Test product creation requires authentication."""
|
||||
response = client.get(reverse('products:create'))
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.url.startswith('/accounts/login/')
|
||||
|
||||
def test_product_create_authenticated(self, authenticated_client, db):
|
||||
"""Test product creation as authenticated user."""
|
||||
response = authenticated_client.get(reverse('products:create'))
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_product_create_post(self, authenticated_client, db, category):
|
||||
"""Test creating a product via POST."""
|
||||
data = {
|
||||
'name': 'Test Product',
|
||||
'description': 'A test product',
|
||||
'price': '99.99',
|
||||
'stock': 10,
|
||||
'category': category.id,
|
||||
}
|
||||
|
||||
response = authenticated_client.post(reverse('products:create'), data)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert Product.objects.filter(name='Test Product').exists()
|
||||
```
|
||||
|
||||
## DRF API Testing
|
||||
|
||||
### Serializer Testing
|
||||
|
||||
```python
|
||||
# tests/test_serializers.py
|
||||
import pytest
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from apps.products.serializers import ProductSerializer
|
||||
from tests.factories import ProductFactory
|
||||
|
||||
class TestProductSerializer:
|
||||
"""Test ProductSerializer."""
|
||||
|
||||
def test_serialize_product(self, db):
|
||||
"""Test serializing a product."""
|
||||
product = ProductFactory()
|
||||
serializer = ProductSerializer(product)
|
||||
|
||||
data = serializer.data
|
||||
|
||||
assert data['id'] == product.id
|
||||
assert data['name'] == product.name
|
||||
assert data['price'] == str(product.price)
|
||||
|
||||
def test_deserialize_product(self, db):
|
||||
"""Test deserializing product data."""
|
||||
data = {
|
||||
'name': 'Test Product',
|
||||
'description': 'Test description',
|
||||
'price': '99.99',
|
||||
'stock': 10,
|
||||
'category': 1,
|
||||
}
|
||||
|
||||
serializer = ProductSerializer(data=data)
|
||||
|
||||
assert serializer.is_valid()
|
||||
product = serializer.save()
|
||||
|
||||
assert product.name == 'Test Product'
|
||||
assert float(product.price) == 99.99
|
||||
|
||||
def test_price_validation(self, db):
|
||||
"""Test price validation."""
|
||||
data = {
|
||||
'name': 'Test Product',
|
||||
'price': '-10.00',
|
||||
'stock': 10,
|
||||
}
|
||||
|
||||
serializer = ProductSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert 'price' in serializer.errors
|
||||
|
||||
def test_stock_validation(self, db):
|
||||
"""Test stock cannot be negative."""
|
||||
data = {
|
||||
'name': 'Test Product',
|
||||
'price': '99.99',
|
||||
'stock': -5,
|
||||
}
|
||||
|
||||
serializer = ProductSerializer(data=data)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert 'stock' in serializer.errors
|
||||
```
|
||||
|
||||
### API ViewSet Testing
|
||||
|
||||
```python
|
||||
# tests/test_api.py
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework import status
|
||||
from django.urls import reverse
|
||||
from tests.factories import ProductFactory, UserFactory
|
||||
|
||||
class TestProductAPI:
|
||||
"""Test Product API endpoints."""
|
||||
|
||||
@pytest.fixture
|
||||
def api_client(self):
|
||||
"""Return API client."""
|
||||
return APIClient()
|
||||
|
||||
def test_list_products(self, api_client, db):
|
||||
"""Test listing products."""
|
||||
ProductFactory.create_batch(10)
|
||||
|
||||
url = reverse('api:product-list')
|
||||
response = api_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data['count'] == 10
|
||||
|
||||
def test_retrieve_product(self, api_client, db):
|
||||
"""Test retrieving a product."""
|
||||
product = ProductFactory()
|
||||
|
||||
url = reverse('api:product-detail', kwargs={'pk': product.id})
|
||||
response = api_client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data['id'] == product.id
|
||||
|
||||
def test_create_product_unauthorized(self, api_client, db):
|
||||
"""Test creating product without authentication."""
|
||||
url = reverse('api:product-list')
|
||||
data = {'name': 'Test Product', 'price': '99.99'}
|
||||
|
||||
response = api_client.post(url, data)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_create_product_authorized(self, authenticated_api_client, db):
|
||||
"""Test creating product as authenticated user."""
|
||||
url = reverse('api:product-list')
|
||||
data = {
|
||||
'name': 'Test Product',
|
||||
'description': 'Test',
|
||||
'price': '99.99',
|
||||
'stock': 10,
|
||||
}
|
||||
|
||||
response = authenticated_api_client.post(url, data)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.data['name'] == 'Test Product'
|
||||
|
||||
def test_update_product(self, authenticated_api_client, db):
|
||||
"""Test updating a product."""
|
||||
product = ProductFactory(created_by=authenticated_api_client.user)
|
||||
|
||||
url = reverse('api:product-detail', kwargs={'pk': product.id})
|
||||
data = {'name': 'Updated Product'}
|
||||
|
||||
response = authenticated_api_client.patch(url, data)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data['name'] == 'Updated Product'
|
||||
|
||||
def test_delete_product(self, authenticated_api_client, db):
|
||||
"""Test deleting a product."""
|
||||
product = ProductFactory(created_by=authenticated_api_client.user)
|
||||
|
||||
url = reverse('api:product-detail', kwargs={'pk': product.id})
|
||||
response = authenticated_api_client.delete(url)
|
||||
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
|
||||
def test_filter_products_by_price(self, api_client, db):
|
||||
"""Test filtering products by price."""
|
||||
ProductFactory(price=50)
|
||||
ProductFactory(price=150)
|
||||
|
||||
url = reverse('api:product-list')
|
||||
response = api_client.get(url, {'price_min': 100})
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data['count'] == 1
|
||||
|
||||
def test_search_products(self, api_client, db):
|
||||
"""Test searching products."""
|
||||
ProductFactory(name='Apple iPhone')
|
||||
ProductFactory(name='Samsung Galaxy')
|
||||
|
||||
url = reverse('api:product-list')
|
||||
response = api_client.get(url, {'search': 'Apple'})
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data['count'] == 1
|
||||
```
|
||||
|
||||
## Mocking and Patching
|
||||
|
||||
### Mocking External Services
|
||||
|
||||
```python
|
||||
# tests/test_views.py
|
||||
from unittest.mock import patch, Mock
|
||||
import pytest
|
||||
|
||||
class TestPaymentView:
|
||||
"""Test payment view with mocked payment gateway."""
|
||||
|
||||
@patch('apps.payments.services.stripe')
|
||||
def test_successful_payment(self, mock_stripe, client, user, product):
|
||||
"""Test successful payment with mocked Stripe."""
|
||||
# Configure mock
|
||||
mock_stripe.Charge.create.return_value = {
|
||||
'id': 'ch_123',
|
||||
'status': 'succeeded',
|
||||
'amount': 9999,
|
||||
}
|
||||
|
||||
client.force_login(user)
|
||||
response = client.post(reverse('payments:process'), {
|
||||
'product_id': product.id,
|
||||
'token': 'tok_visa',
|
||||
})
|
||||
|
||||
assert response.status_code == 302
|
||||
mock_stripe.Charge.create.assert_called_once()
|
||||
|
||||
@patch('apps.payments.services.stripe')
|
||||
def test_failed_payment(self, mock_stripe, client, user, product):
|
||||
"""Test failed payment."""
|
||||
mock_stripe.Charge.create.side_effect = Exception('Card declined')
|
||||
|
||||
client.force_login(user)
|
||||
response = client.post(reverse('payments:process'), {
|
||||
'product_id': product.id,
|
||||
'token': 'tok_visa',
|
||||
})
|
||||
|
||||
assert response.status_code == 302
|
||||
assert 'error' in response.url
|
||||
```
|
||||
|
||||
### Mocking Email Sending
|
||||
|
||||
```python
|
||||
# tests/test_email.py
|
||||
from django.core import mail
|
||||
from django.test import override_settings
|
||||
|
||||
@override_settings(EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend')
|
||||
def test_order_confirmation_email(db, order):
|
||||
"""Test order confirmation email."""
|
||||
order.send_confirmation_email()
|
||||
|
||||
assert len(mail.outbox) == 1
|
||||
assert order.user.email in mail.outbox[0].to
|
||||
assert 'Order Confirmation' in mail.outbox[0].subject
|
||||
```
|
||||
|
||||
## Integration Testing
|
||||
|
||||
### Full Flow Testing
|
||||
|
||||
```python
|
||||
# tests/test_integration.py
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from tests.factories import UserFactory, ProductFactory
|
||||
|
||||
class TestCheckoutFlow:
|
||||
"""Test complete checkout flow."""
|
||||
|
||||
def test_guest_to_purchase_flow(self, client, db):
|
||||
"""Test complete flow from guest to purchase."""
|
||||
# Step 1: Register
|
||||
response = client.post(reverse('users:register'), {
|
||||
'email': 'test@example.com',
|
||||
'password': 'testpass123',
|
||||
'password_confirm': 'testpass123',
|
||||
})
|
||||
assert response.status_code == 302
|
||||
|
||||
# Step 2: Login
|
||||
response = client.post(reverse('users:login'), {
|
||||
'email': 'test@example.com',
|
||||
'password': 'testpass123',
|
||||
})
|
||||
assert response.status_code == 302
|
||||
|
||||
# Step 3: Browse products
|
||||
product = ProductFactory(price=100)
|
||||
response = client.get(reverse('products:detail', kwargs={'slug': product.slug}))
|
||||
assert response.status_code == 200
|
||||
|
||||
# Step 4: Add to cart
|
||||
response = client.post(reverse('cart:add'), {
|
||||
'product_id': product.id,
|
||||
'quantity': 1,
|
||||
})
|
||||
assert response.status_code == 302
|
||||
|
||||
# Step 5: Checkout
|
||||
response = client.get(reverse('checkout:review'))
|
||||
assert response.status_code == 200
|
||||
assert product.name in response.content.decode()
|
||||
|
||||
# Step 6: Complete purchase
|
||||
with patch('apps.checkout.services.process_payment') as mock_payment:
|
||||
mock_payment.return_value = True
|
||||
response = client.post(reverse('checkout:complete'))
|
||||
|
||||
assert response.status_code == 302
|
||||
assert Order.objects.filter(user__email='test@example.com').exists()
|
||||
```
|
||||
|
||||
## Testing Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- **Use factories**: Instead of manual object creation
|
||||
- **One assertion per test**: Keep tests focused
|
||||
- **Descriptive test names**: `test_user_cannot_delete_others_post`
|
||||
- **Test edge cases**: Empty inputs, None values, boundary conditions
|
||||
- **Mock external services**: Don't depend on external APIs
|
||||
- **Use fixtures**: Eliminate duplication
|
||||
- **Test permissions**: Ensure authorization works
|
||||
- **Keep tests fast**: Use `--reuse-db` and `--nomigrations`
|
||||
|
||||
### DON'T
|
||||
|
||||
- **Don't test Django internals**: Trust Django to work
|
||||
- **Don't test third-party code**: Trust libraries to work
|
||||
- **Don't ignore failing tests**: All tests must pass
|
||||
- **Don't make tests dependent**: Tests should run in any order
|
||||
- **Don't over-mock**: Mock only external dependencies
|
||||
- **Don't test private methods**: Test public interface
|
||||
- **Don't use production database**: Always use test database
|
||||
|
||||
## Coverage
|
||||
|
||||
### Coverage Configuration
|
||||
|
||||
```bash
|
||||
# Run tests with coverage
|
||||
pytest --cov=apps --cov-report=html --cov-report=term-missing
|
||||
|
||||
# Generate HTML report
|
||||
open htmlcov/index.html
|
||||
```
|
||||
|
||||
### Coverage Goals
|
||||
|
||||
| Component | Target Coverage |
|
||||
|-----------|-----------------|
|
||||
| Models | 90%+ |
|
||||
| Serializers | 85%+ |
|
||||
| Views | 80%+ |
|
||||
| Services | 90%+ |
|
||||
| Utilities | 80%+ |
|
||||
| Overall | 80%+ |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Usage |
|
||||
|---------|-------|
|
||||
| `@pytest.mark.django_db` | Enable database access |
|
||||
| `client` | Django test client |
|
||||
| `api_client` | DRF API client |
|
||||
| `factory.create_batch(n)` | Create multiple objects |
|
||||
| `patch('module.function')` | Mock external dependencies |
|
||||
| `override_settings` | Temporarily change settings |
|
||||
| `force_authenticate()` | Bypass authentication in tests |
|
||||
| `assertRedirects` | Check for redirects |
|
||||
| `assertTemplateUsed` | Verify template usage |
|
||||
| `mail.outbox` | Check sent emails |
|
||||
|
||||
Remember: Tests are documentation. Good tests explain how your code should work. Keep them simple, readable, and maintainable.
|
||||
460
skills/django-verification/SKILL.md
Normal file
460
skills/django-verification/SKILL.md
Normal file
@@ -0,0 +1,460 @@
|
||||
---
|
||||
name: django-verification
|
||||
description: Verification loop for Django projects: migrations, linting, tests with coverage, security scans, and deployment readiness checks before release or PR.
|
||||
---
|
||||
|
||||
# Django Verification Loop
|
||||
|
||||
Run before PRs, after major changes, and pre-deploy to ensure Django application quality and security.
|
||||
|
||||
## Phase 1: Environment Check
|
||||
|
||||
```bash
|
||||
# Verify Python version
|
||||
python --version # Should match project requirements
|
||||
|
||||
# Check virtual environment
|
||||
which python
|
||||
pip list --outdated
|
||||
|
||||
# Verify environment variables
|
||||
python -c "import os; import environ; print('DJANGO_SECRET_KEY set' if os.environ.get('DJANGO_SECRET_KEY') else 'MISSING: DJANGO_SECRET_KEY')"
|
||||
```
|
||||
|
||||
If environment is misconfigured, stop and fix.
|
||||
|
||||
## Phase 2: Code Quality & Formatting
|
||||
|
||||
```bash
|
||||
# Type checking
|
||||
mypy . --config-file pyproject.toml
|
||||
|
||||
# Linting with ruff
|
||||
ruff check . --fix
|
||||
|
||||
# Formatting with black
|
||||
black . --check
|
||||
black . # Auto-fix
|
||||
|
||||
# Import sorting
|
||||
isort . --check-only
|
||||
isort . # Auto-fix
|
||||
|
||||
# Django-specific checks
|
||||
python manage.py check --deploy
|
||||
```
|
||||
|
||||
Common issues:
|
||||
- Missing type hints on public functions
|
||||
- PEP 8 formatting violations
|
||||
- Unsorted imports
|
||||
- Debug settings left in production configuration
|
||||
|
||||
## Phase 3: Migrations
|
||||
|
||||
```bash
|
||||
# Check for unapplied migrations
|
||||
python manage.py showmigrations
|
||||
|
||||
# Create missing migrations
|
||||
python manage.py makemigrations --check
|
||||
|
||||
# Dry-run migration application
|
||||
python manage.py migrate --plan
|
||||
|
||||
# Apply migrations (test environment)
|
||||
python manage.py migrate
|
||||
|
||||
# Check for migration conflicts
|
||||
python manage.py makemigrations --merge # Only if conflicts exist
|
||||
```
|
||||
|
||||
Report:
|
||||
- Number of pending migrations
|
||||
- Any migration conflicts
|
||||
- Model changes without migrations
|
||||
|
||||
## Phase 4: Tests + Coverage
|
||||
|
||||
```bash
|
||||
# Run all tests with pytest
|
||||
pytest --cov=apps --cov-report=html --cov-report=term-missing --reuse-db
|
||||
|
||||
# Run specific app tests
|
||||
pytest apps/users/tests/
|
||||
|
||||
# Run with markers
|
||||
pytest -m "not slow" # Skip slow tests
|
||||
pytest -m integration # Only integration tests
|
||||
|
||||
# Coverage report
|
||||
open htmlcov/index.html
|
||||
```
|
||||
|
||||
Report:
|
||||
- Total tests: X passed, Y failed, Z skipped
|
||||
- Overall coverage: XX%
|
||||
- Per-app coverage breakdown
|
||||
|
||||
Coverage targets:
|
||||
|
||||
| Component | Target |
|
||||
|-----------|--------|
|
||||
| Models | 90%+ |
|
||||
| Serializers | 85%+ |
|
||||
| Views | 80%+ |
|
||||
| Services | 90%+ |
|
||||
| Overall | 80%+ |
|
||||
|
||||
## Phase 5: Security Scan
|
||||
|
||||
```bash
|
||||
# Dependency vulnerabilities
|
||||
pip-audit
|
||||
safety check --full-report
|
||||
|
||||
# Django security checks
|
||||
python manage.py check --deploy
|
||||
|
||||
# Bandit security linter
|
||||
bandit -r . -f json -o bandit-report.json
|
||||
|
||||
# Secret scanning (if gitleaks is installed)
|
||||
gitleaks detect --source . --verbose
|
||||
|
||||
# Environment variable check
|
||||
python -c "from django.core.exceptions import ImproperlyConfigured; from django.conf import settings; settings.DEBUG"
|
||||
```
|
||||
|
||||
Report:
|
||||
- Vulnerable dependencies found
|
||||
- Security configuration issues
|
||||
- Hardcoded secrets detected
|
||||
- DEBUG mode status (should be False in production)
|
||||
|
||||
## Phase 6: Django Management Commands
|
||||
|
||||
```bash
|
||||
# Check for model issues
|
||||
python manage.py check
|
||||
|
||||
# Collect static files
|
||||
python manage.py collectstatic --noinput --clear
|
||||
|
||||
# Create superuser (if needed for tests)
|
||||
echo "from apps.users.models import User; User.objects.create_superuser('admin@example.com', 'admin')" | python manage.py shell
|
||||
|
||||
# Database integrity
|
||||
python manage.py check --database default
|
||||
|
||||
# Cache verification (if using Redis)
|
||||
python -c "from django.core.cache import cache; cache.set('test', 'value', 10); print(cache.get('test'))"
|
||||
```
|
||||
|
||||
## Phase 7: Performance Checks
|
||||
|
||||
```bash
|
||||
# Django Debug Toolbar output (check for N+1 queries)
|
||||
# Run in dev mode with DEBUG=True and access a page
|
||||
# Look for duplicate queries in SQL panel
|
||||
|
||||
# Query count analysis
|
||||
django-admin debugsqlshell # If django-debug-sqlshell installed
|
||||
|
||||
# Check for missing indexes
|
||||
python manage.py shell << EOF
|
||||
from django.db import connection
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT table_name, index_name FROM information_schema.statistics WHERE table_schema = 'public'")
|
||||
print(cursor.fetchall())
|
||||
EOF
|
||||
```
|
||||
|
||||
Report:
|
||||
- Number of queries per page (should be < 50 for typical pages)
|
||||
- Missing database indexes
|
||||
- Duplicate queries detected
|
||||
|
||||
## Phase 8: Static Assets
|
||||
|
||||
```bash
|
||||
# Check for npm dependencies (if using npm)
|
||||
npm audit
|
||||
npm audit fix
|
||||
|
||||
# Build static files (if using webpack/vite)
|
||||
npm run build
|
||||
|
||||
# Verify static files
|
||||
ls -la staticfiles/
|
||||
python manage.py findstatic css/style.css
|
||||
```
|
||||
|
||||
## Phase 9: Configuration Review
|
||||
|
||||
```python
|
||||
# Run in Python shell to verify settings
|
||||
python manage.py shell << EOF
|
||||
from django.conf import settings
|
||||
import os
|
||||
|
||||
# Critical checks
|
||||
checks = {
|
||||
'DEBUG is False': not settings.DEBUG,
|
||||
'SECRET_KEY set': bool(settings.SECRET_KEY and len(settings.SECRET_KEY) > 30),
|
||||
'ALLOWED_HOSTS set': len(settings.ALLOWED_HOSTS) > 0,
|
||||
'HTTPS enabled': getattr(settings, 'SECURE_SSL_REDIRECT', False),
|
||||
'HSTS enabled': getattr(settings, 'SECURE_HSTS_SECONDS', 0) > 0,
|
||||
'Database configured': settings.DATABASES['default']['ENGINE'] != 'django.db.backends.sqlite3',
|
||||
}
|
||||
|
||||
for check, result in checks.items():
|
||||
status = '✓' if result else '✗'
|
||||
print(f"{status} {check}")
|
||||
EOF
|
||||
```
|
||||
|
||||
## Phase 10: Logging Configuration
|
||||
|
||||
```bash
|
||||
# Test logging output
|
||||
python manage.py shell << EOF
|
||||
import logging
|
||||
logger = logging.getLogger('django')
|
||||
logger.warning('Test warning message')
|
||||
logger.error('Test error message')
|
||||
EOF
|
||||
|
||||
# Check log files (if configured)
|
||||
tail -f /var/log/django/django.log
|
||||
```
|
||||
|
||||
## Phase 11: API Documentation (if DRF)
|
||||
|
||||
```bash
|
||||
# Generate schema
|
||||
python manage.py generateschema --format openapi-json > schema.json
|
||||
|
||||
# Validate schema
|
||||
# Check if schema.json is valid JSON
|
||||
python -c "import json; json.load(open('schema.json'))"
|
||||
|
||||
# Access Swagger UI (if using drf-yasg)
|
||||
# Visit http://localhost:8000/swagger/ in browser
|
||||
```
|
||||
|
||||
## Phase 12: Diff Review
|
||||
|
||||
```bash
|
||||
# Show diff statistics
|
||||
git diff --stat
|
||||
|
||||
# Show actual changes
|
||||
git diff
|
||||
|
||||
# Show changed files
|
||||
git diff --name-only
|
||||
|
||||
# Check for common issues
|
||||
git diff | grep -i "todo\|fixme\|hack\|xxx"
|
||||
git diff | grep "print(" # Debug statements
|
||||
git diff | grep "DEBUG = True" # Debug mode
|
||||
git diff | grep "import pdb" # Debugger
|
||||
```
|
||||
|
||||
Checklist:
|
||||
- No debugging statements (print, pdb, breakpoint())
|
||||
- No TODO/FIXME comments in critical code
|
||||
- No hardcoded secrets or credentials
|
||||
- Database migrations included for model changes
|
||||
- Configuration changes documented
|
||||
- Error handling present for external calls
|
||||
- Transaction management where needed
|
||||
|
||||
## Output Template
|
||||
|
||||
```
|
||||
DJANGO VERIFICATION REPORT
|
||||
==========================
|
||||
|
||||
Phase 1: Environment Check
|
||||
✓ Python 3.11.5
|
||||
✓ Virtual environment active
|
||||
✓ All environment variables set
|
||||
|
||||
Phase 2: Code Quality
|
||||
✓ mypy: No type errors
|
||||
✗ ruff: 3 issues found (auto-fixed)
|
||||
✓ black: No formatting issues
|
||||
✓ isort: Imports properly sorted
|
||||
✓ manage.py check: No issues
|
||||
|
||||
Phase 3: Migrations
|
||||
✓ No unapplied migrations
|
||||
✓ No migration conflicts
|
||||
✓ All models have migrations
|
||||
|
||||
Phase 4: Tests + Coverage
|
||||
Tests: 247 passed, 0 failed, 5 skipped
|
||||
Coverage:
|
||||
Overall: 87%
|
||||
users: 92%
|
||||
products: 89%
|
||||
orders: 85%
|
||||
payments: 91%
|
||||
|
||||
Phase 5: Security Scan
|
||||
✗ pip-audit: 2 vulnerabilities found (fix required)
|
||||
✓ safety check: No issues
|
||||
✓ bandit: No security issues
|
||||
✓ No secrets detected
|
||||
✓ DEBUG = False
|
||||
|
||||
Phase 6: Django Commands
|
||||
✓ collectstatic completed
|
||||
✓ Database integrity OK
|
||||
✓ Cache backend reachable
|
||||
|
||||
Phase 7: Performance
|
||||
✓ No N+1 queries detected
|
||||
✓ Database indexes configured
|
||||
✓ Query count acceptable
|
||||
|
||||
Phase 8: Static Assets
|
||||
✓ npm audit: No vulnerabilities
|
||||
✓ Assets built successfully
|
||||
✓ Static files collected
|
||||
|
||||
Phase 9: Configuration
|
||||
✓ DEBUG = False
|
||||
✓ SECRET_KEY configured
|
||||
✓ ALLOWED_HOSTS set
|
||||
✓ HTTPS enabled
|
||||
✓ HSTS enabled
|
||||
✓ Database configured
|
||||
|
||||
Phase 10: Logging
|
||||
✓ Logging configured
|
||||
✓ Log files writable
|
||||
|
||||
Phase 11: API Documentation
|
||||
✓ Schema generated
|
||||
✓ Swagger UI accessible
|
||||
|
||||
Phase 12: Diff Review
|
||||
Files changed: 12
|
||||
+450, -120 lines
|
||||
✓ No debug statements
|
||||
✓ No hardcoded secrets
|
||||
✓ Migrations included
|
||||
|
||||
RECOMMENDATION: ⚠️ Fix pip-audit vulnerabilities before deploying
|
||||
|
||||
NEXT STEPS:
|
||||
1. Update vulnerable dependencies
|
||||
2. Re-run security scan
|
||||
3. Deploy to staging for final testing
|
||||
```
|
||||
|
||||
## Pre-Deployment Checklist
|
||||
|
||||
- [ ] All tests passing
|
||||
- [ ] Coverage ≥ 80%
|
||||
- [ ] No security vulnerabilities
|
||||
- [ ] No unapplied migrations
|
||||
- [ ] DEBUG = False in production settings
|
||||
- [ ] SECRET_KEY properly configured
|
||||
- [ ] ALLOWED_HOSTS set correctly
|
||||
- [ ] Database backups enabled
|
||||
- [ ] Static files collected and served
|
||||
- [ ] Logging configured and working
|
||||
- [ ] Error monitoring (Sentry, etc.) configured
|
||||
- [ ] CDN configured (if applicable)
|
||||
- [ ] Redis/cache backend configured
|
||||
- [ ] Celery workers running (if applicable)
|
||||
- [ ] HTTPS/SSL configured
|
||||
- [ ] Environment variables documented
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
### GitHub Actions Example
|
||||
|
||||
```yaml
|
||||
# .github/workflows/django-verification.yml
|
||||
name: Django Verification
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install ruff black mypy pytest pytest-django pytest-cov bandit safety pip-audit
|
||||
|
||||
- name: Code quality checks
|
||||
run: |
|
||||
ruff check .
|
||||
black . --check
|
||||
isort . --check-only
|
||||
mypy .
|
||||
|
||||
- name: Security scan
|
||||
run: |
|
||||
bandit -r . -f json -o bandit-report.json
|
||||
safety check --full-report
|
||||
pip-audit
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
|
||||
DJANGO_SECRET_KEY: test-secret-key
|
||||
run: |
|
||||
pytest --cov=apps --cov-report=xml --cov-report=term-missing
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Check | Command |
|
||||
|-------|---------|
|
||||
| Environment | `python --version` |
|
||||
| Type checking | `mypy .` |
|
||||
| Linting | `ruff check .` |
|
||||
| Formatting | `black . --check` |
|
||||
| Migrations | `python manage.py makemigrations --check` |
|
||||
| Tests | `pytest --cov=apps` |
|
||||
| Security | `pip-audit && bandit -r .` |
|
||||
| Django check | `python manage.py check --deploy` |
|
||||
| Collectstatic | `python manage.py collectstatic --noinput` |
|
||||
| Diff stats | `git diff --stat` |
|
||||
|
||||
Remember: Automated verification catches common issues but doesn't replace manual code review and testing in staging environment.
|
||||
749
skills/python-patterns/SKILL.md
Normal file
749
skills/python-patterns/SKILL.md
Normal file
@@ -0,0 +1,749 @@
|
||||
---
|
||||
name: python-patterns
|
||||
description: Pythonic idioms, PEP 8 standards, type hints, and best practices for building robust, efficient, and maintainable Python applications.
|
||||
---
|
||||
|
||||
# Python Development Patterns
|
||||
|
||||
Idiomatic Python patterns and best practices for building robust, efficient, and maintainable applications.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Writing new Python code
|
||||
- Reviewing Python code
|
||||
- Refactoring existing Python code
|
||||
- Designing Python packages/modules
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. Readability Counts
|
||||
|
||||
Python prioritizes readability. Code should be obvious and easy to understand.
|
||||
|
||||
```python
|
||||
# Good: Clear and readable
|
||||
def get_active_users(users: list[User]) -> list[User]:
|
||||
"""Return only active users from the provided list."""
|
||||
return [user for user in users if user.is_active]
|
||||
|
||||
|
||||
# Bad: Clever but confusing
|
||||
def get_active_users(u):
|
||||
return [x for x in u if x.a]
|
||||
```
|
||||
|
||||
### 2. Explicit is Better Than Implicit
|
||||
|
||||
Avoid magic; be clear about what your code does.
|
||||
|
||||
```python
|
||||
# Good: Explicit configuration
|
||||
import logging
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
# Bad: Hidden side effects
|
||||
import some_module
|
||||
some_module.setup() # What does this do?
|
||||
```
|
||||
|
||||
### 3. EAFP - Easier to Ask Forgiveness Than Permission
|
||||
|
||||
Python prefers exception handling over checking conditions.
|
||||
|
||||
```python
|
||||
# Good: EAFP style
|
||||
def get_value(dictionary: dict, key: str) -> Any:
|
||||
try:
|
||||
return dictionary[key]
|
||||
except KeyError:
|
||||
return default_value
|
||||
|
||||
# Bad: LBYL (Look Before You Leap) style
|
||||
def get_value(dictionary: dict, key: str) -> Any:
|
||||
if key in dictionary:
|
||||
return dictionary[key]
|
||||
else:
|
||||
return default_value
|
||||
```
|
||||
|
||||
## Type Hints
|
||||
|
||||
### Basic Type Annotations
|
||||
|
||||
```python
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
def process_user(
|
||||
user_id: str,
|
||||
data: Dict[str, Any],
|
||||
active: bool = True
|
||||
) -> Optional[User]:
|
||||
"""Process a user and return the updated User or None."""
|
||||
if not active:
|
||||
return None
|
||||
return User(user_id, data)
|
||||
```
|
||||
|
||||
### Modern Type Hints (Python 3.9+)
|
||||
|
||||
```python
|
||||
# Python 3.9+ - Use built-in types
|
||||
def process_items(items: list[str]) -> dict[str, int]:
|
||||
return {item: len(item) for item in items}
|
||||
|
||||
# Python 3.8 and earlier - Use typing module
|
||||
from typing import List, Dict
|
||||
|
||||
def process_items(items: List[str]) -> Dict[str, int]:
|
||||
return {item: len(item) for item in items}
|
||||
```
|
||||
|
||||
### Type Aliases and TypeVar
|
||||
|
||||
```python
|
||||
from typing import TypeVar, Union
|
||||
|
||||
# Type alias for complex types
|
||||
JSON = Union[dict[str, Any], list[Any], str, int, float, bool, None]
|
||||
|
||||
def parse_json(data: str) -> JSON:
|
||||
return json.loads(data)
|
||||
|
||||
# Generic types
|
||||
T = TypeVar('T')
|
||||
|
||||
def first(items: list[T]) -> T | None:
|
||||
"""Return the first item or None if list is empty."""
|
||||
return items[0] if items else None
|
||||
```
|
||||
|
||||
### Protocol-Based Duck Typing
|
||||
|
||||
```python
|
||||
from typing import Protocol
|
||||
|
||||
class Renderable(Protocol):
|
||||
def render(self) -> str:
|
||||
"""Render the object to a string."""
|
||||
|
||||
def render_all(items: list[Renderable]) -> str:
|
||||
"""Render all items that implement the Renderable protocol."""
|
||||
return "\n".join(item.render() for item in items)
|
||||
```
|
||||
|
||||
## Error Handling Patterns
|
||||
|
||||
### Specific Exception Handling
|
||||
|
||||
```python
|
||||
# Good: Catch specific exceptions
|
||||
def load_config(path: str) -> Config:
|
||||
try:
|
||||
with open(path) as f:
|
||||
return Config.from_json(f.read())
|
||||
except FileNotFoundError as e:
|
||||
raise ConfigError(f"Config file not found: {path}") from e
|
||||
except json.JSONDecodeError as e:
|
||||
raise ConfigError(f"Invalid JSON in config: {path}") from e
|
||||
|
||||
# Bad: Bare except
|
||||
def load_config(path: str) -> Config:
|
||||
try:
|
||||
with open(path) as f:
|
||||
return Config.from_json(f.read())
|
||||
except:
|
||||
return None # Silent failure!
|
||||
```
|
||||
|
||||
### Exception Chaining
|
||||
|
||||
```python
|
||||
def process_data(data: str) -> Result:
|
||||
try:
|
||||
parsed = json.loads(data)
|
||||
except json.JSONDecodeError as e:
|
||||
# Chain exceptions to preserve the traceback
|
||||
raise ValueError(f"Failed to parse data: {data}") from e
|
||||
```
|
||||
|
||||
### Custom Exception Hierarchy
|
||||
|
||||
```python
|
||||
class AppError(Exception):
|
||||
"""Base exception for all application errors."""
|
||||
pass
|
||||
|
||||
class ValidationError(AppError):
|
||||
"""Raised when input validation fails."""
|
||||
pass
|
||||
|
||||
class NotFoundError(AppError):
|
||||
"""Raised when a requested resource is not found."""
|
||||
pass
|
||||
|
||||
# Usage
|
||||
def get_user(user_id: str) -> User:
|
||||
user = db.find_user(user_id)
|
||||
if not user:
|
||||
raise NotFoundError(f"User not found: {user_id}")
|
||||
return user
|
||||
```
|
||||
|
||||
## Context Managers
|
||||
|
||||
### Resource Management
|
||||
|
||||
```python
|
||||
# Good: Using context managers
|
||||
def process_file(path: str) -> str:
|
||||
with open(path, 'r') as f:
|
||||
return f.read()
|
||||
|
||||
# Bad: Manual resource management
|
||||
def process_file(path: str) -> str:
|
||||
f = open(path, 'r')
|
||||
try:
|
||||
return f.read()
|
||||
finally:
|
||||
f.close()
|
||||
```
|
||||
|
||||
### Custom Context Managers
|
||||
|
||||
```python
|
||||
from contextlib import contextmanager
|
||||
|
||||
@contextmanager
|
||||
def timer(name: str):
|
||||
"""Context manager to time a block of code."""
|
||||
start = time.perf_counter()
|
||||
yield
|
||||
elapsed = time.perf_counter() - start
|
||||
print(f"{name} took {elapsed:.4f} seconds")
|
||||
|
||||
# Usage
|
||||
with timer("data processing"):
|
||||
process_large_dataset()
|
||||
```
|
||||
|
||||
### Context Manager Classes
|
||||
|
||||
```python
|
||||
class DatabaseTransaction:
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
|
||||
def __enter__(self):
|
||||
self.connection.begin_transaction()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if exc_type is None:
|
||||
self.connection.commit()
|
||||
else:
|
||||
self.connection.rollback()
|
||||
return False # Don't suppress exceptions
|
||||
|
||||
# Usage
|
||||
with DatabaseTransaction(conn):
|
||||
user = conn.create_user(user_data)
|
||||
conn.create_profile(user.id, profile_data)
|
||||
```
|
||||
|
||||
## Comprehensions and Generators
|
||||
|
||||
### List Comprehensions
|
||||
|
||||
```python
|
||||
# Good: List comprehension for simple transformations
|
||||
names = [user.name for user in users if user.is_active]
|
||||
|
||||
# Bad: Manual loop
|
||||
names = []
|
||||
for user in users:
|
||||
if user.is_active:
|
||||
names.append(user.name)
|
||||
|
||||
# Complex comprehensions should be expanded
|
||||
# Bad: Too complex
|
||||
result = [x * 2 for x in items if x > 0 if x % 2 == 0]
|
||||
|
||||
# Good: Use a generator function
|
||||
def filter_and_transform(items: Iterable[int]) -> list[int]:
|
||||
result = []
|
||||
for x in items:
|
||||
if x > 0 and x % 2 == 0:
|
||||
result.append(x * 2)
|
||||
return result
|
||||
```
|
||||
|
||||
### Generator Expressions
|
||||
|
||||
```python
|
||||
# Good: Generator for lazy evaluation
|
||||
total = sum(x * x for x in range(1_000_000))
|
||||
|
||||
# Bad: Creates large intermediate list
|
||||
total = sum([x * x for x in range(1_000_000)])
|
||||
```
|
||||
|
||||
### Generator Functions
|
||||
|
||||
```python
|
||||
def read_large_file(path: str) -> Iterator[str]:
|
||||
"""Read a large file line by line."""
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
yield line.strip()
|
||||
|
||||
# Usage
|
||||
for line in read_large_file("huge.txt"):
|
||||
process(line)
|
||||
```
|
||||
|
||||
## Data Classes and Named Tuples
|
||||
|
||||
### Data Classes
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
"""User entity with automatic __init__, __repr__, and __eq__."""
|
||||
id: str
|
||||
name: str
|
||||
email: str
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
is_active: bool = True
|
||||
|
||||
# Usage
|
||||
user = User(
|
||||
id="123",
|
||||
name="Alice",
|
||||
email="alice@example.com"
|
||||
)
|
||||
```
|
||||
|
||||
### Data Classes with Validation
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class User:
|
||||
email: str
|
||||
age: int
|
||||
|
||||
def __post_init__(self):
|
||||
# Validate email format
|
||||
if "@" not in self.email:
|
||||
raise ValueError(f"Invalid email: {self.email}")
|
||||
# Validate age range
|
||||
if self.age < 0 or self.age > 150:
|
||||
raise ValueError(f"Invalid age: {self.age}")
|
||||
```
|
||||
|
||||
### Named Tuples
|
||||
|
||||
```python
|
||||
from typing import NamedTuple
|
||||
|
||||
class Point(NamedTuple):
|
||||
"""Immutable 2D point."""
|
||||
x: float
|
||||
y: float
|
||||
|
||||
def distance(self, other: 'Point') -> float:
|
||||
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
|
||||
|
||||
# Usage
|
||||
p1 = Point(0, 0)
|
||||
p2 = Point(3, 4)
|
||||
print(p1.distance(p2)) # 5.0
|
||||
```
|
||||
|
||||
## Decorators
|
||||
|
||||
### Function Decorators
|
||||
|
||||
```python
|
||||
import functools
|
||||
import time
|
||||
|
||||
def timer(func: Callable) -> Callable:
|
||||
"""Decorator to time function execution."""
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
start = time.perf_counter()
|
||||
result = func(*args, **kwargs)
|
||||
elapsed = time.perf_counter() - start
|
||||
print(f"{func.__name__} took {elapsed:.4f}s")
|
||||
return result
|
||||
return wrapper
|
||||
|
||||
@timer
|
||||
def slow_function():
|
||||
time.sleep(1)
|
||||
|
||||
# slow_function() prints: slow_function took 1.0012s
|
||||
```
|
||||
|
||||
### Parameterized Decorators
|
||||
|
||||
```python
|
||||
def repeat(times: int):
|
||||
"""Decorator to repeat a function multiple times."""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
results = []
|
||||
for _ in range(times):
|
||||
results.append(func(*args, **kwargs))
|
||||
return results
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
@repeat(times=3)
|
||||
def greet(name: str) -> str:
|
||||
return f"Hello, {name}!"
|
||||
|
||||
# greet("Alice") returns ["Hello, Alice!", "Hello, Alice!", "Hello, Alice!"]
|
||||
```
|
||||
|
||||
### Class-Based Decorators
|
||||
|
||||
```python
|
||||
class CountCalls:
|
||||
"""Decorator that counts how many times a function is called."""
|
||||
def __init__(self, func: Callable):
|
||||
functools.update_wrapper(self, func)
|
||||
self.func = func
|
||||
self.count = 0
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
self.count += 1
|
||||
print(f"{self.func.__name__} has been called {self.count} times")
|
||||
return self.func(*args, **kwargs)
|
||||
|
||||
@CountCalls
|
||||
def process():
|
||||
pass
|
||||
|
||||
# Each call to process() prints the call count
|
||||
```
|
||||
|
||||
## Concurrency Patterns
|
||||
|
||||
### Threading for I/O-Bound Tasks
|
||||
|
||||
```python
|
||||
import concurrent.futures
|
||||
import threading
|
||||
|
||||
def fetch_url(url: str) -> str:
|
||||
"""Fetch a URL (I/O-bound operation)."""
|
||||
import urllib.request
|
||||
with urllib.request.urlopen(url) as response:
|
||||
return response.read().decode()
|
||||
|
||||
def fetch_all_urls(urls: list[str]) -> dict[str, str]:
|
||||
"""Fetch multiple URLs concurrently using threads."""
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
||||
future_to_url = {executor.submit(fetch_url, url): url for url in urls}
|
||||
results = {}
|
||||
for future in concurrent.futures.as_completed(future_to_url):
|
||||
url = future_to_url[future]
|
||||
try:
|
||||
results[url] = future.result()
|
||||
except Exception as e:
|
||||
results[url] = f"Error: {e}"
|
||||
return results
|
||||
```
|
||||
|
||||
### Multiprocessing for CPU-Bound Tasks
|
||||
|
||||
```python
|
||||
def process_data(data: list[int]) -> int:
|
||||
"""CPU-intensive computation."""
|
||||
return sum(x ** 2 for x in data)
|
||||
|
||||
def process_all(datasets: list[list[int]]) -> list[int]:
|
||||
"""Process multiple datasets using multiple processes."""
|
||||
with concurrent.futures.ProcessPoolExecutor() as executor:
|
||||
results = list(executor.map(process_data, datasets))
|
||||
return results
|
||||
```
|
||||
|
||||
### Async/Await for Concurrent I/O
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
async def fetch_async(url: str) -> str:
|
||||
"""Fetch a URL asynchronously."""
|
||||
import aiohttp
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as response:
|
||||
return await response.text()
|
||||
|
||||
async def fetch_all(urls: list[str]) -> dict[str, str]:
|
||||
"""Fetch multiple URLs concurrently."""
|
||||
tasks = [fetch_async(url) for url in urls]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
return dict(zip(urls, results))
|
||||
```
|
||||
|
||||
## Package Organization
|
||||
|
||||
### Standard Project Layout
|
||||
|
||||
```
|
||||
myproject/
|
||||
├── src/
|
||||
│ └── mypackage/
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py
|
||||
│ ├── api/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── routes.py
|
||||
│ ├── models/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── user.py
|
||||
│ └── utils/
|
||||
│ ├── __init__.py
|
||||
│ └── helpers.py
|
||||
├── tests/
|
||||
│ ├── __init__.py
|
||||
│ ├── conftest.py
|
||||
│ ├── test_api.py
|
||||
│ └── test_models.py
|
||||
├── pyproject.toml
|
||||
├── README.md
|
||||
└── .gitignore
|
||||
```
|
||||
|
||||
### Import Conventions
|
||||
|
||||
```python
|
||||
# Good: Import order - stdlib, third-party, local
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from fastapi import FastAPI
|
||||
|
||||
from mypackage.models import User
|
||||
from mypackage.utils import format_name
|
||||
|
||||
# Good: Use isort for automatic import sorting
|
||||
# pip install isort
|
||||
```
|
||||
|
||||
### __init__.py for Package Exports
|
||||
|
||||
```python
|
||||
# mypackage/__init__.py
|
||||
"""mypackage - A sample Python package."""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
|
||||
# Export main classes/functions at package level
|
||||
from mypackage.models import User, Post
|
||||
from mypackage.utils import format_name
|
||||
|
||||
__all__ = ["User", "Post", "format_name"]
|
||||
```
|
||||
|
||||
## Memory and Performance
|
||||
|
||||
### Using __slots__ for Memory Efficiency
|
||||
|
||||
```python
|
||||
# Bad: Regular class uses __dict__ (more memory)
|
||||
class Point:
|
||||
def __init__(self, x: float, y: float):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
# Good: __slots__ reduces memory usage
|
||||
class Point:
|
||||
__slots__ = ['x', 'y']
|
||||
|
||||
def __init__(self, x: float, y: float):
|
||||
self.x = x
|
||||
self.y = y
|
||||
```
|
||||
|
||||
### Generator for Large Data
|
||||
|
||||
```python
|
||||
# Bad: Returns full list in memory
|
||||
def read_lines(path: str) -> list[str]:
|
||||
with open(path) as f:
|
||||
return [line.strip() for line in f]
|
||||
|
||||
# Good: Yields lines one at a time
|
||||
def read_lines(path: str) -> Iterator[str]:
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
yield line.strip()
|
||||
```
|
||||
|
||||
### Avoid String Concatenation in Loops
|
||||
|
||||
```python
|
||||
# Bad: O(n²) due to string immutability
|
||||
result = ""
|
||||
for item in items:
|
||||
result += str(item)
|
||||
|
||||
# Good: O(n) using join
|
||||
result = "".join(str(item) for item in items)
|
||||
|
||||
# Good: Using StringIO for building
|
||||
from io import StringIO
|
||||
|
||||
buffer = StringIO()
|
||||
for item in items:
|
||||
buffer.write(str(item))
|
||||
result = buffer.getvalue()
|
||||
```
|
||||
|
||||
## Python Tooling Integration
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```bash
|
||||
# Code formatting
|
||||
black .
|
||||
isort .
|
||||
|
||||
# Linting
|
||||
ruff check .
|
||||
pylint mypackage/
|
||||
|
||||
# Type checking
|
||||
mypy .
|
||||
|
||||
# Testing
|
||||
pytest --cov=mypackage --cov-report=html
|
||||
|
||||
# Security scanning
|
||||
bandit -r .
|
||||
|
||||
# Dependency management
|
||||
pip-audit
|
||||
safety check
|
||||
```
|
||||
|
||||
### pyproject.toml Configuration
|
||||
|
||||
```toml
|
||||
[project]
|
||||
name = "mypackage"
|
||||
version = "1.0.0"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"requests>=2.31.0",
|
||||
"pydantic>=2.0.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=7.4.0",
|
||||
"pytest-cov>=4.1.0",
|
||||
"black>=23.0.0",
|
||||
"ruff>=0.1.0",
|
||||
"mypy>=1.5.0",
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target-version = ['py39']
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
select = ["E", "F", "I", "N", "W"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.9"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
disallow_untyped_defs = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
addopts = "--cov=mypackage --cov-report=term-missing"
|
||||
```
|
||||
|
||||
## Quick Reference: Python Idioms
|
||||
|
||||
| Idiom | Description |
|
||||
|-------|-------------|
|
||||
| EAFP | Easier to Ask Forgiveness than Permission |
|
||||
| Context managers | Use `with` for resource management |
|
||||
| List comprehensions | For simple transformations |
|
||||
| Generators | For lazy evaluation and large datasets |
|
||||
| Type hints | Annotate function signatures |
|
||||
| Dataclasses | For data containers with auto-generated methods |
|
||||
| `__slots__` | For memory optimization |
|
||||
| f-strings | For string formatting (Python 3.6+) |
|
||||
| `pathlib.Path` | For path operations (Python 3.4+) |
|
||||
| `enumerate` | For index-element pairs in loops |
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
```python
|
||||
# Bad: Mutable default arguments
|
||||
def append_to(item, items=[]):
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
# Good: Use None and create new list
|
||||
def append_to(item, items=None):
|
||||
if items is None:
|
||||
items = []
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
# Bad: Checking type with type()
|
||||
if type(obj) == list:
|
||||
process(obj)
|
||||
|
||||
# Good: Use isinstance
|
||||
if isinstance(obj, list):
|
||||
process(obj)
|
||||
|
||||
# Bad: Comparing to None with ==
|
||||
if value == None:
|
||||
process()
|
||||
|
||||
# Good: Use is
|
||||
if value is None:
|
||||
process()
|
||||
|
||||
# Bad: from module import *
|
||||
from os.path import *
|
||||
|
||||
# Good: Explicit imports
|
||||
from os.path import join, exists
|
||||
|
||||
# Bad: Bare except
|
||||
try:
|
||||
risky_operation()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Good: Specific exception
|
||||
try:
|
||||
risky_operation()
|
||||
except SpecificError as e:
|
||||
logger.error(f"Operation failed: {e}")
|
||||
```
|
||||
|
||||
__Remember__: Python code should be readable, explicit, and follow the principle of least surprise. When in doubt, prioritize clarity over cleverness.
|
||||
815
skills/python-testing/SKILL.md
Normal file
815
skills/python-testing/SKILL.md
Normal file
@@ -0,0 +1,815 @@
|
||||
---
|
||||
name: python-testing
|
||||
description: Python testing strategies using pytest, TDD methodology, fixtures, mocking, parametrization, and coverage requirements.
|
||||
---
|
||||
|
||||
# Python Testing Patterns
|
||||
|
||||
Comprehensive testing strategies for Python applications using pytest, TDD methodology, and best practices.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Writing new Python code (follow TDD: red, green, refactor)
|
||||
- Designing test suites for Python projects
|
||||
- Reviewing Python test coverage
|
||||
- Setting up testing infrastructure
|
||||
|
||||
## Core Testing Philosophy
|
||||
|
||||
### Test-Driven Development (TDD)
|
||||
|
||||
Always follow the TDD cycle:
|
||||
|
||||
1. **RED**: Write a failing test for the desired behavior
|
||||
2. **GREEN**: Write minimal code to make the test pass
|
||||
3. **REFACTOR**: Improve code while keeping tests green
|
||||
|
||||
```python
|
||||
# Step 1: Write failing test (RED)
|
||||
def test_add_numbers():
|
||||
result = add(2, 3)
|
||||
assert result == 5
|
||||
|
||||
# Step 2: Write minimal implementation (GREEN)
|
||||
def add(a, b):
|
||||
return a + b
|
||||
|
||||
# Step 3: Refactor if needed (REFACTOR)
|
||||
```
|
||||
|
||||
### Coverage Requirements
|
||||
|
||||
- **Target**: 80%+ code coverage
|
||||
- **Critical paths**: 100% coverage required
|
||||
- Use `pytest --cov` to measure coverage
|
||||
|
||||
```bash
|
||||
pytest --cov=mypackage --cov-report=term-missing --cov-report=html
|
||||
```
|
||||
|
||||
## pytest Fundamentals
|
||||
|
||||
### Basic Test Structure
|
||||
|
||||
```python
|
||||
import pytest
|
||||
|
||||
def test_addition():
|
||||
"""Test basic addition."""
|
||||
assert 2 + 2 == 4
|
||||
|
||||
def test_string_uppercase():
|
||||
"""Test string uppercasing."""
|
||||
text = "hello"
|
||||
assert text.upper() == "HELLO"
|
||||
|
||||
def test_list_append():
|
||||
"""Test list append."""
|
||||
items = [1, 2, 3]
|
||||
items.append(4)
|
||||
assert 4 in items
|
||||
assert len(items) == 4
|
||||
```
|
||||
|
||||
### Assertions
|
||||
|
||||
```python
|
||||
# Equality
|
||||
assert result == expected
|
||||
|
||||
# Inequality
|
||||
assert result != unexpected
|
||||
|
||||
# Truthiness
|
||||
assert result # Truthy
|
||||
assert not result # Falsy
|
||||
assert result is True # Exactly True
|
||||
assert result is False # Exactly False
|
||||
assert result is None # Exactly None
|
||||
|
||||
# Membership
|
||||
assert item in collection
|
||||
assert item not in collection
|
||||
|
||||
# Comparisons
|
||||
assert result > 0
|
||||
assert 0 <= result <= 100
|
||||
|
||||
# Type checking
|
||||
assert isinstance(result, str)
|
||||
|
||||
# Exception testing (preferred approach)
|
||||
with pytest.raises(ValueError):
|
||||
raise ValueError("error message")
|
||||
|
||||
# Check exception message
|
||||
with pytest.raises(ValueError, match="invalid input"):
|
||||
raise ValueError("invalid input provided")
|
||||
|
||||
# Check exception attributes
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
raise ValueError("error message")
|
||||
assert str(exc_info.value) == "error message"
|
||||
```
|
||||
|
||||
## Fixtures
|
||||
|
||||
### Basic Fixture Usage
|
||||
|
||||
```python
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def sample_data():
|
||||
"""Fixture providing sample data."""
|
||||
return {"name": "Alice", "age": 30}
|
||||
|
||||
def test_sample_data(sample_data):
|
||||
"""Test using the fixture."""
|
||||
assert sample_data["name"] == "Alice"
|
||||
assert sample_data["age"] == 30
|
||||
```
|
||||
|
||||
### Fixture with Setup/Teardown
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def database():
|
||||
"""Fixture with setup and teardown."""
|
||||
# Setup
|
||||
db = Database(":memory:")
|
||||
db.create_tables()
|
||||
db.insert_test_data()
|
||||
|
||||
yield db # Provide to test
|
||||
|
||||
# Teardown
|
||||
db.close()
|
||||
|
||||
def test_database_query(database):
|
||||
"""Test database operations."""
|
||||
result = database.query("SELECT * FROM users")
|
||||
assert len(result) > 0
|
||||
```
|
||||
|
||||
### Fixture Scopes
|
||||
|
||||
```python
|
||||
# Function scope (default) - runs for each test
|
||||
@pytest.fixture
|
||||
def temp_file():
|
||||
with open("temp.txt", "w") as f:
|
||||
yield f
|
||||
os.remove("temp.txt")
|
||||
|
||||
# Module scope - runs once per module
|
||||
@pytest.fixture(scope="module")
|
||||
def module_db():
|
||||
db = Database(":memory:")
|
||||
db.create_tables()
|
||||
yield db
|
||||
db.close()
|
||||
|
||||
# Session scope - runs once per test session
|
||||
@pytest.fixture(scope="session")
|
||||
def shared_resource():
|
||||
resource = ExpensiveResource()
|
||||
yield resource
|
||||
resource.cleanup()
|
||||
```
|
||||
|
||||
### Fixture with Parameters
|
||||
|
||||
```python
|
||||
@pytest.fixture(params=[1, 2, 3])
|
||||
def number(request):
|
||||
"""Parameterized fixture."""
|
||||
return request.param
|
||||
|
||||
def test_numbers(number):
|
||||
"""Test runs 3 times, once for each parameter."""
|
||||
assert number > 0
|
||||
```
|
||||
|
||||
### Using Multiple Fixtures
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def user():
|
||||
return User(id=1, name="Alice")
|
||||
|
||||
@pytest.fixture
|
||||
def admin():
|
||||
return User(id=2, name="Admin", role="admin")
|
||||
|
||||
def test_user_admin_interaction(user, admin):
|
||||
"""Test using multiple fixtures."""
|
||||
assert admin.can_manage(user)
|
||||
```
|
||||
|
||||
### Autouse Fixtures
|
||||
|
||||
```python
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_config():
|
||||
"""Automatically runs before every test."""
|
||||
Config.reset()
|
||||
yield
|
||||
Config.cleanup()
|
||||
|
||||
def test_without_fixture_call():
|
||||
# reset_config runs automatically
|
||||
assert Config.get_setting("debug") is False
|
||||
```
|
||||
|
||||
### Conftest.py for Shared Fixtures
|
||||
|
||||
```python
|
||||
# tests/conftest.py
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
"""Shared fixture for all tests."""
|
||||
app = create_app(testing=True)
|
||||
with app.test_client() as client:
|
||||
yield client
|
||||
|
||||
@pytest.fixture
|
||||
def auth_headers(client):
|
||||
"""Generate auth headers for API testing."""
|
||||
response = client.post("/api/login", json={
|
||||
"username": "test",
|
||||
"password": "test"
|
||||
})
|
||||
token = response.json["token"]
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
```
|
||||
|
||||
## Parametrization
|
||||
|
||||
### Basic Parametrization
|
||||
|
||||
```python
|
||||
@pytest.mark.parametrize("input,expected", [
|
||||
("hello", "HELLO"),
|
||||
("world", "WORLD"),
|
||||
("PyThOn", "PYTHON"),
|
||||
])
|
||||
def test_uppercase(input, expected):
|
||||
"""Test runs 3 times with different inputs."""
|
||||
assert input.upper() == expected
|
||||
```
|
||||
|
||||
### Multiple Parameters
|
||||
|
||||
```python
|
||||
@pytest.mark.parametrize("a,b,expected", [
|
||||
(2, 3, 5),
|
||||
(0, 0, 0),
|
||||
(-1, 1, 0),
|
||||
(100, 200, 300),
|
||||
])
|
||||
def test_add(a, b, expected):
|
||||
"""Test addition with multiple inputs."""
|
||||
assert add(a, b) == expected
|
||||
```
|
||||
|
||||
### Parametrize with IDs
|
||||
|
||||
```python
|
||||
@pytest.mark.parametrize("input,expected", [
|
||||
("valid@email.com", True),
|
||||
("invalid", False),
|
||||
("@no-domain.com", False),
|
||||
], ids=["valid-email", "missing-at", "missing-domain"])
|
||||
def test_email_validation(input, expected):
|
||||
"""Test email validation with readable test IDs."""
|
||||
assert is_valid_email(input) is expected
|
||||
```
|
||||
|
||||
### Parametrized Fixtures
|
||||
|
||||
```python
|
||||
@pytest.fixture(params=["sqlite", "postgresql", "mysql"])
|
||||
def db(request):
|
||||
"""Test against multiple database backends."""
|
||||
if request.param == "sqlite":
|
||||
return Database(":memory:")
|
||||
elif request.param == "postgresql":
|
||||
return Database("postgresql://localhost/test")
|
||||
elif request.param == "mysql":
|
||||
return Database("mysql://localhost/test")
|
||||
|
||||
def test_database_operations(db):
|
||||
"""Test runs 3 times, once for each database."""
|
||||
result = db.query("SELECT 1")
|
||||
assert result is not None
|
||||
```
|
||||
|
||||
## Markers and Test Selection
|
||||
|
||||
### Custom Markers
|
||||
|
||||
```python
|
||||
# Mark slow tests
|
||||
@pytest.mark.slow
|
||||
def test_slow_operation():
|
||||
time.sleep(5)
|
||||
|
||||
# Mark integration tests
|
||||
@pytest.mark.integration
|
||||
def test_api_integration():
|
||||
response = requests.get("https://api.example.com")
|
||||
assert response.status_code == 200
|
||||
|
||||
# Mark unit tests
|
||||
@pytest.mark.unit
|
||||
def test_unit_logic():
|
||||
assert calculate(2, 3) == 5
|
||||
```
|
||||
|
||||
### Run Specific Tests
|
||||
|
||||
```bash
|
||||
# Run only fast tests
|
||||
pytest -m "not slow"
|
||||
|
||||
# Run only integration tests
|
||||
pytest -m integration
|
||||
|
||||
# Run integration or slow tests
|
||||
pytest -m "integration or slow"
|
||||
|
||||
# Run tests marked as unit but not slow
|
||||
pytest -m "unit and not slow"
|
||||
```
|
||||
|
||||
### Configure Markers in pytest.ini
|
||||
|
||||
```ini
|
||||
[pytest]
|
||||
markers =
|
||||
slow: marks tests as slow
|
||||
integration: marks tests as integration tests
|
||||
unit: marks tests as unit tests
|
||||
django: marks tests as requiring Django
|
||||
```
|
||||
|
||||
## Mocking and Patching
|
||||
|
||||
### Mocking Functions
|
||||
|
||||
```python
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
@patch("mypackage.external_api_call")
|
||||
def test_with_mock(api_call_mock):
|
||||
"""Test with mocked external API."""
|
||||
api_call_mock.return_value = {"status": "success"}
|
||||
|
||||
result = my_function()
|
||||
|
||||
api_call_mock.assert_called_once()
|
||||
assert result["status"] == "success"
|
||||
```
|
||||
|
||||
### Mocking Return Values
|
||||
|
||||
```python
|
||||
@patch("mypackage.Database.connect")
|
||||
def test_database_connection(connect_mock):
|
||||
"""Test with mocked database connection."""
|
||||
connect_mock.return_value = MockConnection()
|
||||
|
||||
db = Database()
|
||||
db.connect()
|
||||
|
||||
connect_mock.assert_called_once_with("localhost")
|
||||
```
|
||||
|
||||
### Mocking Exceptions
|
||||
|
||||
```python
|
||||
@patch("mypackage.api_call")
|
||||
def test_api_error_handling(api_call_mock):
|
||||
"""Test error handling with mocked exception."""
|
||||
api_call_mock.side_effect = ConnectionError("Network error")
|
||||
|
||||
with pytest.raises(ConnectionError):
|
||||
api_call()
|
||||
|
||||
api_call_mock.assert_called_once()
|
||||
```
|
||||
|
||||
### Mocking Context Managers
|
||||
|
||||
```python
|
||||
@patch("builtins.open", new_callable=mock_open)
|
||||
def test_file_reading(mock_file):
|
||||
"""Test file reading with mocked open."""
|
||||
mock_file.return_value.read.return_value = "file content"
|
||||
|
||||
result = read_file("test.txt")
|
||||
|
||||
mock_file.assert_called_once_with("test.txt", "r")
|
||||
assert result == "file content"
|
||||
```
|
||||
|
||||
### Using Autospec
|
||||
|
||||
```python
|
||||
@patch("mypackage.DBConnection", autospec=True)
|
||||
def test_autospec(db_mock):
|
||||
"""Test with autospec to catch API misuse."""
|
||||
db = db_mock.return_value
|
||||
db.query("SELECT * FROM users")
|
||||
|
||||
# This would fail if DBConnection doesn't have query method
|
||||
db_mock.assert_called_once()
|
||||
```
|
||||
|
||||
### Mock Class Instances
|
||||
|
||||
```python
|
||||
class TestUserService:
|
||||
@patch("mypackage.UserRepository")
|
||||
def test_create_user(self, repo_mock):
|
||||
"""Test user creation with mocked repository."""
|
||||
repo_mock.return_value.save.return_value = User(id=1, name="Alice")
|
||||
|
||||
service = UserService(repo_mock.return_value)
|
||||
user = service.create_user(name="Alice")
|
||||
|
||||
assert user.name == "Alice"
|
||||
repo_mock.return_value.save.assert_called_once()
|
||||
```
|
||||
|
||||
### Mock Property
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def mock_config():
|
||||
"""Create a mock with a property."""
|
||||
config = Mock()
|
||||
type(config).debug = PropertyMock(return_value=True)
|
||||
type(config).api_key = PropertyMock(return_value="test-key")
|
||||
return config
|
||||
|
||||
def test_with_mock_config(mock_config):
|
||||
"""Test with mocked config properties."""
|
||||
assert mock_config.debug is True
|
||||
assert mock_config.api_key == "test-key"
|
||||
```
|
||||
|
||||
## Testing Async Code
|
||||
|
||||
### Async Tests with pytest-asyncio
|
||||
|
||||
```python
|
||||
import pytest
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_function():
|
||||
"""Test async function."""
|
||||
result = await async_add(2, 3)
|
||||
assert result == 5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_with_fixture(async_client):
|
||||
"""Test async with async fixture."""
|
||||
response = await async_client.get("/api/users")
|
||||
assert response.status_code == 200
|
||||
```
|
||||
|
||||
### Async Fixture
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
async def async_client():
|
||||
"""Async fixture providing async test client."""
|
||||
app = create_app()
|
||||
async with app.test_client() as client:
|
||||
yield client
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_endpoint(async_client):
|
||||
"""Test using async fixture."""
|
||||
response = await async_client.get("/api/data")
|
||||
assert response.status_code == 200
|
||||
```
|
||||
|
||||
### Mocking Async Functions
|
||||
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
@patch("mypackage.async_api_call")
|
||||
async def test_async_mock(api_call_mock):
|
||||
"""Test async function with mock."""
|
||||
api_call_mock.return_value = {"status": "ok"}
|
||||
|
||||
result = await my_async_function()
|
||||
|
||||
api_call_mock.assert_awaited_once()
|
||||
assert result["status"] == "ok"
|
||||
```
|
||||
|
||||
## Testing Exceptions
|
||||
|
||||
### Testing Expected Exceptions
|
||||
|
||||
```python
|
||||
def test_divide_by_zero():
|
||||
"""Test that dividing by zero raises ZeroDivisionError."""
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
divide(10, 0)
|
||||
|
||||
def test_custom_exception():
|
||||
"""Test custom exception with message."""
|
||||
with pytest.raises(ValueError, match="invalid input"):
|
||||
validate_input("invalid")
|
||||
```
|
||||
|
||||
### Testing Exception Attributes
|
||||
|
||||
```python
|
||||
def test_exception_with_details():
|
||||
"""Test exception with custom attributes."""
|
||||
with pytest.raises(CustomError) as exc_info:
|
||||
raise CustomError("error", code=400)
|
||||
|
||||
assert exc_info.value.code == 400
|
||||
assert "error" in str(exc_info.value)
|
||||
```
|
||||
|
||||
## Testing Side Effects
|
||||
|
||||
### Testing File Operations
|
||||
|
||||
```python
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
def test_file_processing():
|
||||
"""Test file processing with temp file."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
|
||||
f.write("test content")
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
result = process_file(temp_path)
|
||||
assert result == "processed: test content"
|
||||
finally:
|
||||
os.unlink(temp_path)
|
||||
```
|
||||
|
||||
### Testing with pytest's tmp_path Fixture
|
||||
|
||||
```python
|
||||
def test_with_tmp_path(tmp_path):
|
||||
"""Test using pytest's built-in temp path fixture."""
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.write_text("hello world")
|
||||
|
||||
result = process_file(str(test_file))
|
||||
assert result == "hello world"
|
||||
# tmp_path automatically cleaned up
|
||||
```
|
||||
|
||||
### Testing with tmpdir Fixture
|
||||
|
||||
```python
|
||||
def test_with_tmpdir(tmpdir):
|
||||
"""Test using pytest's tmpdir fixture."""
|
||||
test_file = tmpdir.join("test.txt")
|
||||
test_file.write("data")
|
||||
|
||||
result = process_file(str(test_file))
|
||||
assert result == "data"
|
||||
```
|
||||
|
||||
## Test Organization
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── conftest.py # Shared fixtures
|
||||
├── __init__.py
|
||||
├── unit/ # Unit tests
|
||||
│ ├── __init__.py
|
||||
│ ├── test_models.py
|
||||
│ ├── test_utils.py
|
||||
│ └── test_services.py
|
||||
├── integration/ # Integration tests
|
||||
│ ├── __init__.py
|
||||
│ ├── test_api.py
|
||||
│ └── test_database.py
|
||||
└── e2e/ # End-to-end tests
|
||||
├── __init__.py
|
||||
└── test_user_flow.py
|
||||
```
|
||||
|
||||
### Test Classes
|
||||
|
||||
```python
|
||||
class TestUserService:
|
||||
"""Group related tests in a class."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
"""Setup runs before each test in this class."""
|
||||
self.service = UserService()
|
||||
|
||||
def test_create_user(self):
|
||||
"""Test user creation."""
|
||||
user = self.service.create_user("Alice")
|
||||
assert user.name == "Alice"
|
||||
|
||||
def test_delete_user(self):
|
||||
"""Test user deletion."""
|
||||
user = User(id=1, name="Bob")
|
||||
self.service.delete_user(user)
|
||||
assert not self.service.user_exists(1)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- **Follow TDD**: Write tests before code (red-green-refactor)
|
||||
- **Test one thing**: Each test should verify a single behavior
|
||||
- **Use descriptive names**: `test_user_login_with_invalid_credentials_fails`
|
||||
- **Use fixtures**: Eliminate duplication with fixtures
|
||||
- **Mock external dependencies**: Don't depend on external services
|
||||
- **Test edge cases**: Empty inputs, None values, boundary conditions
|
||||
- **Aim for 80%+ coverage**: Focus on critical paths
|
||||
- **Keep tests fast**: Use marks to separate slow tests
|
||||
|
||||
### DON'T
|
||||
|
||||
- **Don't test implementation**: Test behavior, not internals
|
||||
- **Don't use complex conditionals in tests**: Keep tests simple
|
||||
- **Don't ignore test failures**: All tests must pass
|
||||
- **Don't test third-party code**: Trust libraries to work
|
||||
- **Don't share state between tests**: Tests should be independent
|
||||
- **Don't catch exceptions in tests**: Use `pytest.raises`
|
||||
- **Don't use print statements**: Use assertions and pytest output
|
||||
- **Don't write tests that are too brittle**: Avoid over-specific mocks
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Testing API Endpoints (FastAPI/Flask)
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def client():
|
||||
app = create_app(testing=True)
|
||||
return app.test_client()
|
||||
|
||||
def test_get_user(client):
|
||||
response = client.get("/api/users/1")
|
||||
assert response.status_code == 200
|
||||
assert response.json["id"] == 1
|
||||
|
||||
def test_create_user(client):
|
||||
response = client.post("/api/users", json={
|
||||
"name": "Alice",
|
||||
"email": "alice@example.com"
|
||||
})
|
||||
assert response.status_code == 201
|
||||
assert response.json["name"] == "Alice"
|
||||
```
|
||||
|
||||
### Testing Database Operations
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def db_session():
|
||||
"""Create a test database session."""
|
||||
session = Session(bind=engine)
|
||||
session.begin_nested()
|
||||
yield session
|
||||
session.rollback()
|
||||
session.close()
|
||||
|
||||
def test_create_user(db_session):
|
||||
user = User(name="Alice", email="alice@example.com")
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
|
||||
retrieved = db_session.query(User).filter_by(name="Alice").first()
|
||||
assert retrieved.email == "alice@example.com"
|
||||
```
|
||||
|
||||
### Testing Class Methods
|
||||
|
||||
```python
|
||||
class TestCalculator:
|
||||
@pytest.fixture
|
||||
def calculator(self):
|
||||
return Calculator()
|
||||
|
||||
def test_add(self, calculator):
|
||||
assert calculator.add(2, 3) == 5
|
||||
|
||||
def test_divide_by_zero(self, calculator):
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
calculator.divide(10, 0)
|
||||
```
|
||||
|
||||
## pytest Configuration
|
||||
|
||||
### pytest.ini
|
||||
|
||||
```ini
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
addopts =
|
||||
--strict-markers
|
||||
--disable-warnings
|
||||
--cov=mypackage
|
||||
--cov-report=term-missing
|
||||
--cov-report=html
|
||||
markers =
|
||||
slow: marks tests as slow
|
||||
integration: marks tests as integration tests
|
||||
unit: marks tests as unit tests
|
||||
```
|
||||
|
||||
### pyproject.toml
|
||||
|
||||
```toml
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
python_classes = ["Test*"]
|
||||
python_functions = ["test_*"]
|
||||
addopts = [
|
||||
"--strict-markers",
|
||||
"--cov=mypackage",
|
||||
"--cov-report=term-missing",
|
||||
"--cov-report=html",
|
||||
]
|
||||
markers = [
|
||||
"slow: marks tests as slow",
|
||||
"integration: marks tests as integration tests",
|
||||
"unit: marks tests as unit tests",
|
||||
]
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pytest
|
||||
|
||||
# Run specific file
|
||||
pytest tests/test_utils.py
|
||||
|
||||
# Run specific test
|
||||
pytest tests/test_utils.py::test_function
|
||||
|
||||
# Run with verbose output
|
||||
pytest -v
|
||||
|
||||
# Run with coverage
|
||||
pytest --cov=mypackage --cov-report=html
|
||||
|
||||
# Run only fast tests
|
||||
pytest -m "not slow"
|
||||
|
||||
# Run until first failure
|
||||
pytest -x
|
||||
|
||||
# Run and stop on N failures
|
||||
pytest --maxfail=3
|
||||
|
||||
# Run last failed tests
|
||||
pytest --lf
|
||||
|
||||
# Run tests with pattern
|
||||
pytest -k "test_user"
|
||||
|
||||
# Run with debugger on failure
|
||||
pytest --pdb
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Usage |
|
||||
|---------|-------|
|
||||
| `pytest.raises()` | Test expected exceptions |
|
||||
| `@pytest.fixture()` | Create reusable test fixtures |
|
||||
| `@pytest.mark.parametrize()` | Run tests with multiple inputs |
|
||||
| `@pytest.mark.slow` | Mark slow tests |
|
||||
| `pytest -m "not slow"` | Skip slow tests |
|
||||
| `@patch()` | Mock functions and classes |
|
||||
| `tmp_path` fixture | Automatic temp directory |
|
||||
| `pytest --cov` | Generate coverage report |
|
||||
| `assert` | Simple and readable assertions |
|
||||
|
||||
**Remember**: Tests are code too. Keep them clean, readable, and maintainable. Good tests catch bugs; great tests prevent them.
|
||||
Reference in New Issue
Block a user