add pytest in makefile
This commit is contained in:
114
Makefile
Normal file
114
Makefile
Normal file
@@ -0,0 +1,114 @@
|
||||
# RustFS S3 Toolkit - 开发工具
|
||||
|
||||
.PHONY: help install clean build check test publish-test publish
|
||||
|
||||
help: ## 显示帮助信息
|
||||
@echo "RustFS S3 Toolkit 开发命令:"
|
||||
@echo ""
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
install: ## 安装开发环境
|
||||
@echo "🔧 安装开发环境..."
|
||||
pip install -e ".[dev]"
|
||||
@echo "✅ 开发环境安装完成"
|
||||
|
||||
clean: ## 清理构建文件
|
||||
@echo "🧹 清理构建文件..."
|
||||
rm -rf dist/
|
||||
rm -rf build/
|
||||
rm -rf *.egg-info/
|
||||
rm -rf src/*.egg-info/
|
||||
find . -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -name '*.pyc' -delete 2>/dev/null || true
|
||||
@echo "✅ 清理完成"
|
||||
|
||||
format: ## 格式化代码
|
||||
@echo "🎨 格式化代码..."
|
||||
black src/ tests/ examples/
|
||||
isort src/ tests/ examples/
|
||||
@echo "✅ 代码格式化完成"
|
||||
|
||||
lint: ## 代码检查
|
||||
@echo "🔍 代码检查..."
|
||||
flake8 src/ tests/ examples/
|
||||
mypy src/
|
||||
@echo "✅ 代码检查完成"
|
||||
|
||||
test: ## 运行单元测试
|
||||
@echo "🧪 运行单元测试..."
|
||||
PYTHONPATH=src pytest tests/ -v --cov=rustfs_s3_toolkit --cov-report=term-missing
|
||||
@echo "✅ 单元测试完成"
|
||||
|
||||
test-all: ## 测试所有 Python 版本 (3.9-3.13)
|
||||
@echo "🧪 测试所有 Python 版本..."
|
||||
tox -e py39,py310,py311,py312,py313
|
||||
@echo "✅ 多版本测试完成"
|
||||
|
||||
test-quick: ## 快速测试 (当前 Python 版本)
|
||||
@echo "⚡ 快速测试..."
|
||||
PYTHONPATH=src pytest tests/ -v
|
||||
@echo "✅ 快速测试完成"
|
||||
|
||||
test-integration: ## 集成测试 (需要真实 S3 服务)
|
||||
@echo "🔗 集成测试 (需要 S3 服务)..."
|
||||
@echo "⚠️ 确保已配置 S3 服务连接信息"
|
||||
RUN_INTEGRATION_TESTS=1 PYTHONPATH=src pytest tests/test_toolkit.py::test_all_functions -v -s
|
||||
@echo "✅ 集成测试完成"
|
||||
|
||||
coverage: ## 生成覆盖率报告
|
||||
@echo "📊 生成覆盖率报告..."
|
||||
tox -e coverage
|
||||
@echo "✅ 覆盖率报告生成完成 (查看 htmlcov/index.html)"
|
||||
|
||||
build: clean ## 构建包
|
||||
@echo "📦 构建包..."
|
||||
python -m build
|
||||
@echo "✅ 构建完成"
|
||||
|
||||
check: build ## 检查包格式
|
||||
@echo "🔍 检查包格式..."
|
||||
python publish.py --check-only
|
||||
@echo "✅ 包格式检查完成"
|
||||
|
||||
publish-test: ## 发布到 TestPyPI
|
||||
@echo "🧪 发布到 TestPyPI..."
|
||||
python publish.py --test
|
||||
@echo "✅ TestPyPI 发布完成"
|
||||
|
||||
publish: ## 交互式发布到 PyPI
|
||||
@echo "🚀 交互式发布..."
|
||||
python publish.py
|
||||
|
||||
publish-auto: ## 自动发布到 PyPI (无交互)
|
||||
@echo "🚀 自动发布到 PyPI..."
|
||||
python publish.py --auto
|
||||
|
||||
# 发布前完整检查
|
||||
pre-publish: format lint test-all build check ## 发布前完整检查
|
||||
@echo "✅ 发布前检查全部通过!"
|
||||
|
||||
# 生产发布流程 (推荐)
|
||||
publish-prod: pre-publish ## 生产发布流程 (包含所有检查)
|
||||
@echo "🚀 开始生产发布流程..."
|
||||
@echo "📋 检查清单:"
|
||||
@echo " ✅ 代码格式化"
|
||||
@echo " ✅ 代码质量检查"
|
||||
@echo " ✅ 多版本测试 (Python 3.9-3.13)"
|
||||
@echo " ✅ 包构建和验证"
|
||||
@echo ""
|
||||
python publish.py
|
||||
|
||||
dev-setup: ## 完整开发环境设置
|
||||
@echo "🚀 设置完整开发环境..."
|
||||
python -m venv .venv || true
|
||||
@echo "请运行: source .venv/bin/activate"
|
||||
@echo "然后运行: make install"
|
||||
|
||||
# CI/CD 流程
|
||||
ci: lint test coverage ## CI 流程 (代码检查 + 测试 + 覆盖率)
|
||||
@echo "✅ CI 流程完成"
|
||||
|
||||
all: format lint test build check ## 运行所有检查和构建
|
||||
|
||||
# 默认目标
|
||||
.DEFAULT_GOAL := help
|
||||
187
PACKAGING_GUIDE.md
Normal file
187
PACKAGING_GUIDE.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# 🚀 Python 包发布到 PyPI 完整指南
|
||||
|
||||
## 📋 快速开始 (一键复现)
|
||||
|
||||
### 🚀 使用 Makefile (推荐)
|
||||
```bash
|
||||
make help # 查看所有命令
|
||||
make install # 安装开发环境
|
||||
make clean # 清理构建文件
|
||||
make build # 构建包
|
||||
make check # 检查包格式
|
||||
make publish # 发布到 PyPI
|
||||
make all # 运行所有检查和构建
|
||||
```
|
||||
|
||||
### 🔧 使用脚本
|
||||
```bash
|
||||
# 交互式发布 (推荐)
|
||||
python publish.py
|
||||
|
||||
# 自动发布到 PyPI (无交互)
|
||||
python publish.py --auto
|
||||
|
||||
# 发布到 TestPyPI
|
||||
python publish.py --test
|
||||
|
||||
# 仅构建和检查,不发布
|
||||
python publish.py --check-only
|
||||
```
|
||||
|
||||
### ⚡ 手动命令
|
||||
```bash
|
||||
# 环境准备
|
||||
UV_PYTHON=python3.12 uv venv .venv
|
||||
source .venv/bin/activate
|
||||
uv pip install -e ".[dev]"
|
||||
|
||||
# 构建发布
|
||||
rm -rf dist/ build/ *.egg-info/ src/*.egg-info/
|
||||
python -m build
|
||||
python -m twine check dist/*
|
||||
python -m twine upload dist/*
|
||||
```
|
||||
|
||||
## 📁 项目结构
|
||||
```
|
||||
project-name/
|
||||
├── pyproject.toml # ⭐ 核心配置文件
|
||||
├── README.md # 📖 项目文档
|
||||
├── LICENSE # 📄 许可证文件
|
||||
├── Makefile # 🔧 开发工具
|
||||
├── publish.py # 🚀 发布脚本
|
||||
├── src/
|
||||
│ └── package_name/
|
||||
│ ├── __init__.py # 包初始化
|
||||
│ └── module.py # 核心模块
|
||||
├── tests/
|
||||
│ └── test_module.py # 测试文件
|
||||
└── examples/
|
||||
└── basic_usage.py # 使用示例
|
||||
```
|
||||
|
||||
### ✅ 2. pyproject.toml 配置模板
|
||||
|
||||
```toml
|
||||
[project]
|
||||
name = "your-package-name"
|
||||
version = "0.1.0"
|
||||
description = "Your package description"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
{ name = "Your Name", email = "your.email@example.com" }
|
||||
]
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"dependency>=1.0.0",
|
||||
]
|
||||
keywords = ["keyword1", "keyword2"]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/username/repo"
|
||||
Repository = "https://github.com/username/repo"
|
||||
Documentation = "https://github.com/username/repo#readme"
|
||||
Issues = "https://github.com/username/repo/issues"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
# 测试工具
|
||||
"pytest>=7.0.0",
|
||||
"pytest-cov>=4.0.0",
|
||||
|
||||
# 代码格式化
|
||||
"black>=23.0.0",
|
||||
"isort>=5.12.0",
|
||||
|
||||
# 代码检查
|
||||
"flake8>=6.0.0",
|
||||
"mypy>=1.0.0",
|
||||
|
||||
# 打包工具 (仅开发环境需要)
|
||||
"build>=1.0.0",
|
||||
"twine>=4.0.0",
|
||||
"setuptools>=68.0.0",
|
||||
"wheel>=0.40.0",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=68.0.0", "wheel>=0.40.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
```
|
||||
|
||||
## 🔧 PyPI 认证配置
|
||||
|
||||
创建 `~/.pypirc` 文件:
|
||||
```ini
|
||||
[distutils]
|
||||
index-servers = pypi
|
||||
|
||||
[pypi]
|
||||
username = __token__
|
||||
password = pypi-your-api-token-here
|
||||
```
|
||||
|
||||
## ⚠️ 避免常见陷阱
|
||||
|
||||
1. **❌ 不要创建 build.py 文件** - 会与 `python -m build` 冲突
|
||||
2. **❌ 不要手动编辑许可证字段** - 使用 classifiers
|
||||
3. **❌ 不要混合开发和运行时依赖** - 打包工具放在 dev 依赖中
|
||||
4. **✅ 使用现代构建系统** - PEP 517/518 标准
|
||||
|
||||
## 🚨 故障排除
|
||||
|
||||
| 问题 | 解决方案 |
|
||||
|------|----------|
|
||||
| build.py 冲突 | 重命名或删除项目中的 build.py |
|
||||
| 许可证元数据错误 | 移除 pyproject.toml 中的 license 字段 |
|
||||
| 包名冲突 | 在 PyPI 搜索确认包名可用性 |
|
||||
| 依赖解析失败 | 检查版本约束和兼容性 |
|
||||
|
||||
## ✅ 发布检查清单
|
||||
|
||||
- [ ] pyproject.toml 配置完整
|
||||
- [ ] README.md 文档详细
|
||||
- [ ] LICENSE 文件存在
|
||||
- [ ] 测试通过
|
||||
- [ ] `twine check` 通过
|
||||
- [ ] 版本号正确
|
||||
- [ ] PyPI 认证配置
|
||||
|
||||
## 🎯 版本兼容性
|
||||
|
||||
- **Python 版本**: 3.9-3.13
|
||||
- **Wheel 标签**: `py3-none-any` (通用兼容)
|
||||
- **构建系统**: setuptools (稳定可靠)
|
||||
|
||||
## 📝 最佳实践
|
||||
|
||||
1. **版本管理**: 使用语义化版本 (SemVer)
|
||||
2. **文档**: 提供完整的 README.md 和使用示例
|
||||
3. **测试**: 包含完整的测试套件
|
||||
4. **CI/CD**: 设置自动化测试和发布流程
|
||||
5. **安全**: 使用 API token 而不是用户名密码
|
||||
|
||||
## <20> 发布后验证
|
||||
|
||||
```bash
|
||||
# 安装验证
|
||||
pip install your-package-name
|
||||
|
||||
# 功能验证
|
||||
python -c "import your_package; print('✅ 安装成功')"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**这个指南确保每次都能成功发布到 PyPI!** 🚀
|
||||
125
README.md
125
README.md
@@ -318,19 +318,124 @@ toolkit = S3StorageToolkit(
|
||||
)
|
||||
```
|
||||
|
||||
## 🧪 测试
|
||||
## 🧪 开发和测试
|
||||
|
||||
运行完整的功能测试:
|
||||
### 开发环境设置
|
||||
|
||||
```bash
|
||||
# 运行测试套件
|
||||
python tests/test_toolkit.py
|
||||
# 1. 克隆项目
|
||||
git clone https://github.com/mm644706215/rustfs-s3-toolkit.git
|
||||
cd rustfs-s3-toolkit
|
||||
|
||||
# 运行基本使用示例
|
||||
python examples/basic_usage.py
|
||||
# 2. 创建虚拟环境
|
||||
UV_PYTHON=python3.12 uv venv .venv
|
||||
source .venv/bin/activate
|
||||
|
||||
# 3. 安装开发依赖
|
||||
uv pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
测试将验证所有 9 个核心功能是否正常工作。
|
||||
### 测试命令
|
||||
|
||||
```bash
|
||||
# 快速测试 (当前 Python 版本)
|
||||
make test-quick
|
||||
|
||||
# 完整单元测试 (带覆盖率)
|
||||
make test
|
||||
|
||||
# 多版本测试 (Python 3.9-3.13)
|
||||
make test-all
|
||||
|
||||
# 集成测试 (需要真实 S3 服务)
|
||||
make test-integration
|
||||
|
||||
# 生成覆盖率报告
|
||||
make coverage
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- 单元测试 (`make test`) 使用模拟对象,不需要真实的 S3 服务
|
||||
- 集成测试 (`make test-integration`) 需要配置真实的 S3 服务连接信息
|
||||
|
||||
### 代码质量检查
|
||||
|
||||
```bash
|
||||
# 代码格式化
|
||||
make format
|
||||
|
||||
# 代码质量检查
|
||||
make lint
|
||||
|
||||
# 运行所有检查
|
||||
make all
|
||||
```
|
||||
|
||||
## 🚀 发布流程 (开发者)
|
||||
|
||||
### 推荐发布方式:使用 Makefile
|
||||
|
||||
```bash
|
||||
# 🎯 生产发布流程 (推荐) - 包含所有质量检查
|
||||
make publish-prod
|
||||
```
|
||||
|
||||
这个命令会自动执行:
|
||||
1. ✅ **代码格式化** (`black`, `isort`)
|
||||
2. ✅ **代码质量检查** (`flake8`, `mypy`)
|
||||
3. ✅ **多版本测试** (Python 3.9-3.13)
|
||||
4. ✅ **包构建和验证** (`build`, `twine check`)
|
||||
5. ✅ **交互式发布确认**
|
||||
|
||||
**重要**: 发布前必须确保所有测试通过!
|
||||
|
||||
### 其他发布选项
|
||||
|
||||
```bash
|
||||
# 快速发布 (跳过多版本测试)
|
||||
make publish
|
||||
|
||||
# 自动发布 (无交互,适合 CI/CD)
|
||||
make publish-auto
|
||||
|
||||
# 发布到 TestPyPI
|
||||
make publish-test
|
||||
|
||||
# 仅构建和检查 (不发布)
|
||||
make check
|
||||
```
|
||||
|
||||
### 发布前检查清单
|
||||
|
||||
在发布前,请确保:
|
||||
|
||||
- [ ] 更新了版本号 (`pyproject.toml` 中的 `version`)
|
||||
- [ ] 更新了 `CHANGELOG.md` (如果有)
|
||||
- [ ] **所有单元测试通过** (`make test`)
|
||||
- [ ] **多版本测试通过** (`make test-all`)
|
||||
- [ ] **代码质量检查通过** (`make lint`)
|
||||
- [ ] 文档是最新的
|
||||
- [ ] 配置了 PyPI 认证 (`~/.pypirc`)
|
||||
|
||||
### 测试策略说明
|
||||
|
||||
1. **单元测试** (`make test`): 使用模拟对象,测试代码逻辑,无需外部服务
|
||||
2. **多版本测试** (`make test-all`): 在 Python 3.9-3.13 上运行单元测试
|
||||
3. **集成测试** (`make test-integration`): 可选,需要真实 S3 服务连接
|
||||
|
||||
**发布要求**: 必须通过单元测试和多版本测试,集成测试为可选。
|
||||
|
||||
### PyPI 认证配置
|
||||
|
||||
创建 `~/.pypirc` 文件:
|
||||
```ini
|
||||
[distutils]
|
||||
index-servers = pypi
|
||||
|
||||
[pypi]
|
||||
username = __token__
|
||||
password = pypi-your-api-token-here
|
||||
```
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
@@ -339,8 +444,10 @@ rustfs-s3-toolkit/
|
||||
├── README.md # 项目文档
|
||||
├── pyproject.toml # 项目配置
|
||||
├── LICENSE # MIT 许可证
|
||||
├── install.py # 安装脚本
|
||||
├── build.py # 构建脚本
|
||||
├── Makefile # 开发工具
|
||||
├── tox.ini # 多版本测试配置
|
||||
├── publish.py # 发布脚本
|
||||
├── PACKAGING_GUIDE.md # 打包指南
|
||||
├── src/
|
||||
│ └── rustfs_s3_toolkit/
|
||||
│ ├── __init__.py # 包初始化
|
||||
|
||||
104
build.py
104
build.py
@@ -1,104 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RustFS S3 Storage Toolkit 构建和发布脚本
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def run_command(command, description):
|
||||
"""运行命令并显示结果"""
|
||||
print(f"🔧 {description}...")
|
||||
try:
|
||||
result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
|
||||
print(f"✅ {description}成功")
|
||||
if result.stdout.strip():
|
||||
print(f"输出: {result.stdout.strip()}")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ {description}失败: {e}")
|
||||
if e.stdout:
|
||||
print(f"输出: {e.stdout}")
|
||||
if e.stderr:
|
||||
print(f"错误: {e.stderr}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""主构建流程"""
|
||||
print("🚀 RustFS S3 Storage Toolkit 构建脚本")
|
||||
print("=" * 50)
|
||||
|
||||
# 检查当前目录
|
||||
current_dir = Path.cwd()
|
||||
if not (current_dir / "pyproject.toml").exists():
|
||||
print("❌ 请在项目根目录运行此脚本")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"✅ 项目目录: {current_dir}")
|
||||
|
||||
# 清理旧的构建文件
|
||||
print("\n🧹 清理旧的构建文件...")
|
||||
cleanup_commands = [
|
||||
"rm -rf dist/",
|
||||
"rm -rf build/",
|
||||
"rm -rf *.egg-info/",
|
||||
"find . -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null || true",
|
||||
"find . -name '*.pyc' -delete 2>/dev/null || true"
|
||||
]
|
||||
|
||||
for command in cleanup_commands:
|
||||
subprocess.run(command, shell=True, capture_output=True)
|
||||
|
||||
print("✅ 清理完成")
|
||||
|
||||
# 安装构建依赖
|
||||
build_deps = [
|
||||
"pip install --upgrade pip",
|
||||
"pip install --upgrade build twine"
|
||||
]
|
||||
|
||||
for command in build_deps:
|
||||
if not run_command(command, f"安装构建依赖"):
|
||||
print("❌ 安装构建依赖失败")
|
||||
sys.exit(1)
|
||||
|
||||
# 构建包
|
||||
if not run_command("python -m build", "构建包"):
|
||||
print("❌ 构建失败")
|
||||
sys.exit(1)
|
||||
|
||||
# 检查构建结果
|
||||
dist_dir = current_dir / "dist"
|
||||
if not dist_dir.exists():
|
||||
print("❌ 构建目录不存在")
|
||||
sys.exit(1)
|
||||
|
||||
built_files = list(dist_dir.glob("*"))
|
||||
if not built_files:
|
||||
print("❌ 没有找到构建文件")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\n📦 构建完成!生成的文件:")
|
||||
for file in built_files:
|
||||
print(f" - {file.name}")
|
||||
|
||||
# 验证包
|
||||
if not run_command("python -m twine check dist/*", "验证包"):
|
||||
print("❌ 包验证失败")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n🎉 构建和验证完成!")
|
||||
print("\n📖 发布到 PyPI:")
|
||||
print("1. 测试发布: python -m twine upload --repository testpypi dist/*")
|
||||
print("2. 正式发布: python -m twine upload dist/*")
|
||||
|
||||
print("\n💡 本地安装测试:")
|
||||
print("pip install dist/*.whl")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
164
publish.py
Normal file
164
publish.py
Normal file
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RustFS S3 Storage Toolkit 发布脚本
|
||||
支持交互式和自动化发布
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def run_command(command, description):
|
||||
"""运行命令并显示结果"""
|
||||
print(f"🔧 {description}...")
|
||||
try:
|
||||
result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
|
||||
print(f"✅ {description}成功")
|
||||
if result.stdout.strip():
|
||||
print(f"输出: {result.stdout.strip()}")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ {description}失败: {e}")
|
||||
if e.stdout:
|
||||
print(f"输出: {e.stdout}")
|
||||
if e.stderr:
|
||||
print(f"错误: {e.stderr}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""主发布流程"""
|
||||
parser = argparse.ArgumentParser(description="RustFS S3 Storage Toolkit 发布脚本")
|
||||
parser.add_argument("--auto", action="store_true", help="自动发布到 PyPI (跳过交互)")
|
||||
parser.add_argument("--test", action="store_true", help="发布到 TestPyPI")
|
||||
parser.add_argument("--check-only", action="store_true", help="仅构建和检查,不发布")
|
||||
args = parser.parse_args()
|
||||
|
||||
print("🚀 RustFS S3 Storage Toolkit 发布脚本")
|
||||
print("=" * 50)
|
||||
|
||||
# 检查当前目录
|
||||
current_dir = Path.cwd()
|
||||
if not (current_dir / "pyproject.toml").exists():
|
||||
print("❌ 请在项目根目录运行此脚本")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"✅ 项目目录: {current_dir}")
|
||||
|
||||
# 检查 PyPI 配置
|
||||
pypirc_path = Path.home() / ".pypirc"
|
||||
if not pypirc_path.exists() and not args.check_only:
|
||||
print("⚠️ 警告: 未找到 ~/.pypirc 配置文件")
|
||||
print("请先配置 PyPI 认证信息")
|
||||
sys.exit(1)
|
||||
|
||||
# 1. 清理和构建
|
||||
print("\n🧹 清理旧文件...")
|
||||
cleanup_commands = [
|
||||
"rm -rf dist/",
|
||||
"rm -rf build/",
|
||||
"rm -rf *.egg-info/",
|
||||
"rm -rf src/*.egg-info/",
|
||||
]
|
||||
for command in cleanup_commands:
|
||||
subprocess.run(command, shell=True, capture_output=True)
|
||||
print("✅ 清理完成")
|
||||
|
||||
print("\n📦 开始构建...")
|
||||
if not run_command("python -m build", "构建包"):
|
||||
print("❌ 构建失败,无法继续发布")
|
||||
sys.exit(1)
|
||||
|
||||
# 2. 检查构建结果
|
||||
dist_dir = current_dir / "dist"
|
||||
if not dist_dir.exists() or not list(dist_dir.glob("*")):
|
||||
print("❌ 没有找到构建文件")
|
||||
sys.exit(1)
|
||||
|
||||
built_files = list(dist_dir.glob("*"))
|
||||
print(f"\n📦 构建文件:")
|
||||
for file in built_files:
|
||||
print(f" - {file.name}")
|
||||
|
||||
# 3. 验证包
|
||||
print("\n🔍 验证包格式...")
|
||||
if not run_command("python -m twine check dist/*", "验证包"):
|
||||
print("❌ 包验证失败")
|
||||
sys.exit(1)
|
||||
|
||||
# 如果只是检查,到此结束
|
||||
if args.check_only:
|
||||
print("\n✅ 构建和验证完成!")
|
||||
return
|
||||
|
||||
# 4. 确定发布类型
|
||||
if args.test:
|
||||
publish_type = "test"
|
||||
elif args.auto:
|
||||
publish_type = "pypi"
|
||||
else:
|
||||
# 交互式选择
|
||||
print("\n🎯 选择发布类型:")
|
||||
print("1. 测试发布 (TestPyPI)")
|
||||
print("2. 正式发布 (PyPI)")
|
||||
print("3. 取消")
|
||||
|
||||
choice = input("\n请选择 (1/2/3): ").strip()
|
||||
if choice == "1":
|
||||
publish_type = "test"
|
||||
elif choice == "2":
|
||||
publish_type = "pypi"
|
||||
else:
|
||||
print("❌ 发布已取消")
|
||||
return
|
||||
|
||||
# 5. 执行发布
|
||||
if publish_type == "test":
|
||||
# 测试发布
|
||||
print("\n🧪 发布到 TestPyPI...")
|
||||
if run_command("python -m twine upload --repository testpypi dist/*", "测试发布"):
|
||||
print("\n🎉 测试发布成功!")
|
||||
print("📖 测试安装命令:")
|
||||
print("pip install --index-url https://test.pypi.org/simple/ rustfs-s3-toolkit")
|
||||
else:
|
||||
print("❌ 测试发布失败")
|
||||
sys.exit(1)
|
||||
|
||||
elif publish_type == "pypi":
|
||||
# 正式发布
|
||||
if not args.auto:
|
||||
print("\n⚠️ 确认正式发布到 PyPI?")
|
||||
confirm = input("输入 'yes' 确认: ").strip().lower()
|
||||
if confirm != "yes":
|
||||
print("❌ 发布已取消")
|
||||
return
|
||||
|
||||
print("\n🚀 发布到 PyPI...")
|
||||
if run_command("python -m twine upload dist/*", "正式发布"):
|
||||
print("\n🎉 正式发布成功!")
|
||||
print("📖 安装命令:")
|
||||
print("pip install rustfs-s3-toolkit")
|
||||
print("\n🔗 PyPI 链接:")
|
||||
print("https://pypi.org/project/rustfs-s3-toolkit/")
|
||||
else:
|
||||
print("❌ 正式发布失败")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n💡 后续步骤:")
|
||||
print("1. 检查 PyPI 页面确认发布成功")
|
||||
print("2. 测试安装: pip install rustfs-s3-toolkit")
|
||||
print("3. 更新版本号准备下次发布")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n❌ 发布已中断")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"\n❌ 发布失败: {e}")
|
||||
sys.exit(1)
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "rustfs-s3-toolkit"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
description = "RustFS S3 Storage Toolkit - A simple and powerful toolkit for RustFS and other S3-compatible object storage operations"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
@@ -14,7 +14,6 @@ keywords = ["rustfs", "s3", "object-storage", "file-management", "cloud-storage"
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
@@ -35,14 +34,88 @@ Issues = "https://github.com/mm644706215/rustfs-s3-toolkit/issues"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=7.0.0",
|
||||
"pytest-asyncio>=0.21.0",
|
||||
"black>=23.0.0",
|
||||
"isort>=5.12.0",
|
||||
"flake8>=6.0.0",
|
||||
"mypy>=1.0.0",
|
||||
# 测试工具
|
||||
"pytest>=8.0.0",
|
||||
"pytest-asyncio>=1.1.0",
|
||||
"pytest-cov>=6.0.0",
|
||||
"coverage[toml]>=7.9.0",
|
||||
|
||||
# 多版本测试
|
||||
"tox>=4.28.0",
|
||||
|
||||
# 代码格式化
|
||||
"black>=25.0.0",
|
||||
"isort>=6.0.0",
|
||||
|
||||
# 代码检查
|
||||
"flake8>=7.3.0",
|
||||
"mypy>=1.17.0",
|
||||
|
||||
# 打包工具 (仅开发环境需要)
|
||||
"build>=1.2.0",
|
||||
"twine>=6.1.0",
|
||||
"setuptools>=80.0.0",
|
||||
"wheel>=0.45.0",
|
||||
|
||||
# 文档工具
|
||||
"sphinx>=8.2.0",
|
||||
"sphinx-rtd-theme>=3.0.0",
|
||||
]
|
||||
|
||||
test = [
|
||||
"pytest>=8.0.0",
|
||||
"pytest-cov>=6.0.0",
|
||||
"pytest-asyncio>=1.1.0",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
requires = ["setuptools>=68.0.0", "wheel>=0.40.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
# 工具配置
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target-version = ['py39']
|
||||
include = '\.pyi?$'
|
||||
extend-exclude = '''
|
||||
/(
|
||||
# directories
|
||||
\.eggs
|
||||
| \.git
|
||||
| \.hg
|
||||
| \.mypy_cache
|
||||
| \.tox
|
||||
| \.venv
|
||||
| build
|
||||
| dist
|
||||
)/
|
||||
'''
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
multi_line_output = 3
|
||||
line_length = 88
|
||||
known_first_party = ["rustfs_s3_toolkit"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "7.0"
|
||||
addopts = "-ra -q --strict-markers --strict-config"
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py", "*_test.py"]
|
||||
python_classes = ["Test*"]
|
||||
python_functions = ["test_*"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.9"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
disallow_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
check_untyped_defs = true
|
||||
disallow_untyped_decorators = true
|
||||
no_implicit_optional = true
|
||||
warn_redundant_casts = true
|
||||
warn_unused_ignores = true
|
||||
warn_no_return = true
|
||||
warn_unreachable = true
|
||||
strict_equality = true
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
RustFS S3 Storage Toolkit 测试文件
|
||||
基于成功的 test_s3_flexible.py 创建的测试套件
|
||||
已在 RustFS 1.0.0-alpha.34 上完成测试
|
||||
|
||||
环境变量控制:
|
||||
- RUN_INTEGRATION_TESTS=1: 运行真实的集成测试
|
||||
- 默认: 运行模拟测试
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -10,17 +14,21 @@ import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import pytest
|
||||
|
||||
from rustfs_s3_toolkit import S3StorageToolkit
|
||||
|
||||
|
||||
# 检查是否运行集成测试
|
||||
RUN_INTEGRATION_TESTS = os.getenv('RUN_INTEGRATION_TESTS', '0') == '1'
|
||||
|
||||
# 测试配置 - 请根据实际情况修改
|
||||
TEST_CONFIG = {
|
||||
"endpoint_url": "https://rfs.jmsu.top",
|
||||
"access_key_id": "lingyuzeng",
|
||||
"secret_access_key": "rustAdminlingyuzeng",
|
||||
"bucket_name": "test",
|
||||
"region_name": "us-east-1"
|
||||
"endpoint_url": os.getenv("S3_ENDPOINT_URL", "https://rfs.jmsu.top"),
|
||||
"access_key_id": os.getenv("S3_ACCESS_KEY_ID", "lingyuzeng"),
|
||||
"secret_access_key": os.getenv("S3_SECRET_ACCESS_KEY", "rustAdminlingyuzeng"),
|
||||
"bucket_name": os.getenv("S3_BUCKET_NAME", "test"),
|
||||
"region_name": os.getenv("S3_REGION_NAME", "us-east-1")
|
||||
}
|
||||
|
||||
|
||||
@@ -52,18 +60,20 @@ def create_test_files():
|
||||
return test_dir
|
||||
|
||||
|
||||
@pytest.mark.skipif(not RUN_INTEGRATION_TESTS, reason="集成测试需要设置 RUN_INTEGRATION_TESTS=1")
|
||||
def test_all_functions():
|
||||
"""测试所有 9 个核心功能"""
|
||||
"""测试所有 9 个核心功能(集成测试,需要真实 S3 服务)"""
|
||||
print("🚀 RustFS S3 Storage Toolkit 完整功能测试")
|
||||
print("🧪 测试环境: RustFS 1.0.0-alpha.34")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
# 初始化工具包
|
||||
try:
|
||||
toolkit = S3StorageToolkit(**TEST_CONFIG)
|
||||
print("✅ 工具包初始化成功")
|
||||
except Exception as e:
|
||||
print(f"❌ 工具包初始化失败: {e}")
|
||||
pytest.skip(f"无法连接到 S3 服务: {e}")
|
||||
return False
|
||||
|
||||
test_results = {}
|
||||
@@ -236,8 +246,70 @@ def test_all_functions():
|
||||
else:
|
||||
print("❌ 所有测试都失败了。请检查 RustFS 配置和网络连接。")
|
||||
|
||||
# 对于 pytest,使用 assert 而不是 return
|
||||
assert passed_tests == len(test_names), f"只有 {passed_tests}/{len(test_names)} 个测试通过"
|
||||
|
||||
return test_results
|
||||
|
||||
|
||||
def test_toolkit_initialization():
|
||||
"""测试工具包初始化(单元测试,不需要真实 S3 服务)"""
|
||||
# 测试基本初始化
|
||||
toolkit = S3StorageToolkit(
|
||||
endpoint_url="https://test.example.com",
|
||||
access_key_id="test_key",
|
||||
secret_access_key="test_secret",
|
||||
bucket_name="test-bucket"
|
||||
)
|
||||
|
||||
assert toolkit.bucket_name == "test-bucket"
|
||||
assert toolkit.endpoint_url == "https://test.example.com"
|
||||
|
||||
# 测试带区域的初始化
|
||||
toolkit_with_region = S3StorageToolkit(
|
||||
endpoint_url="https://test.example.com",
|
||||
access_key_id="test_key",
|
||||
secret_access_key="test_secret",
|
||||
bucket_name="test-bucket",
|
||||
region_name="us-west-2"
|
||||
)
|
||||
|
||||
assert toolkit_with_region.bucket_name == "test-bucket"
|
||||
assert toolkit_with_region.endpoint_url == "https://test.example.com"
|
||||
|
||||
|
||||
def test_basic_functionality():
|
||||
"""测试基本功能(确保代码结构正确)"""
|
||||
# 这个测试确保所有方法都存在且可调用
|
||||
toolkit = S3StorageToolkit(
|
||||
endpoint_url="https://test.example.com",
|
||||
access_key_id="test_key",
|
||||
secret_access_key="test_secret",
|
||||
bucket_name="test-bucket"
|
||||
)
|
||||
|
||||
# 检查所有核心方法是否存在
|
||||
assert hasattr(toolkit, 'test_connection')
|
||||
assert hasattr(toolkit, 'upload_file')
|
||||
assert hasattr(toolkit, 'create_folder')
|
||||
assert hasattr(toolkit, 'upload_directory')
|
||||
assert hasattr(toolkit, 'download_file')
|
||||
assert hasattr(toolkit, 'download_directory')
|
||||
assert hasattr(toolkit, 'list_files')
|
||||
assert hasattr(toolkit, 'delete_file')
|
||||
assert hasattr(toolkit, 'delete_directory')
|
||||
|
||||
# 检查方法是否可调用
|
||||
assert callable(toolkit.test_connection)
|
||||
assert callable(toolkit.upload_file)
|
||||
assert callable(toolkit.create_folder)
|
||||
assert callable(toolkit.upload_directory)
|
||||
assert callable(toolkit.download_file)
|
||||
assert callable(toolkit.download_directory)
|
||||
assert callable(toolkit.list_files)
|
||||
assert callable(toolkit.delete_file)
|
||||
assert callable(toolkit.delete_directory)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_all_functions()
|
||||
|
||||
180
tests/test_unit.py
Normal file
180
tests/test_unit.py
Normal file
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RustFS S3 Storage Toolkit 单元测试
|
||||
不依赖外部服务的基础测试
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
from rustfs_s3_toolkit import S3StorageToolkit
|
||||
|
||||
|
||||
class TestS3StorageToolkit:
|
||||
"""S3StorageToolkit 单元测试类"""
|
||||
|
||||
def test_init(self):
|
||||
"""测试工具包初始化"""
|
||||
toolkit = S3StorageToolkit(
|
||||
endpoint_url="https://test.example.com",
|
||||
access_key_id="test_key",
|
||||
secret_access_key="test_secret",
|
||||
bucket_name="test-bucket"
|
||||
)
|
||||
|
||||
assert toolkit.bucket_name == "test-bucket"
|
||||
assert toolkit.endpoint_url == "https://test.example.com"
|
||||
|
||||
def test_init_with_region(self):
|
||||
"""测试带区域的初始化"""
|
||||
toolkit = S3StorageToolkit(
|
||||
endpoint_url="https://test.example.com",
|
||||
access_key_id="test_key",
|
||||
secret_access_key="test_secret",
|
||||
bucket_name="test-bucket",
|
||||
region_name="us-west-2"
|
||||
)
|
||||
|
||||
assert toolkit.bucket_name == "test-bucket"
|
||||
assert toolkit.endpoint_url == "https://test.example.com"
|
||||
|
||||
@patch('boto3.client')
|
||||
def test_test_connection_success(self, mock_boto_client):
|
||||
"""测试连接成功的情况"""
|
||||
# 模拟 boto3 客户端
|
||||
mock_client = Mock()
|
||||
mock_boto_client.return_value = mock_client
|
||||
|
||||
# 模拟 list_buckets 响应
|
||||
mock_client.list_buckets.return_value = {
|
||||
'Buckets': [
|
||||
{'Name': 'test-bucket'},
|
||||
{'Name': 'other-bucket'}
|
||||
]
|
||||
}
|
||||
|
||||
# 模拟 head_bucket 成功
|
||||
mock_client.head_bucket.return_value = {}
|
||||
|
||||
toolkit = S3StorageToolkit(
|
||||
endpoint_url="https://test.example.com",
|
||||
access_key_id="test_key",
|
||||
secret_access_key="test_secret",
|
||||
bucket_name="test-bucket"
|
||||
)
|
||||
|
||||
result = toolkit.test_connection()
|
||||
|
||||
assert result['success'] is True
|
||||
assert result['bucket_count'] == 2
|
||||
assert 'test-bucket' in result['bucket_names']
|
||||
assert result['target_bucket_exists'] is True
|
||||
|
||||
@patch('boto3.client')
|
||||
def test_test_connection_failure(self, mock_boto_client):
|
||||
"""测试连接失败的情况"""
|
||||
# 模拟 boto3 客户端
|
||||
mock_client = Mock()
|
||||
mock_boto_client.return_value = mock_client
|
||||
|
||||
# 模拟连接异常
|
||||
mock_client.list_buckets.side_effect = Exception("Connection failed")
|
||||
|
||||
toolkit = S3StorageToolkit(
|
||||
endpoint_url="https://test.example.com",
|
||||
access_key_id="test_key",
|
||||
secret_access_key="test_secret",
|
||||
bucket_name="test-bucket"
|
||||
)
|
||||
|
||||
result = toolkit.test_connection()
|
||||
|
||||
assert result['success'] is False
|
||||
assert 'error' in result
|
||||
|
||||
@patch('boto3.client')
|
||||
def test_upload_file_success(self, mock_boto_client):
|
||||
"""测试文件上传成功"""
|
||||
# 模拟 boto3 客户端
|
||||
mock_client = Mock()
|
||||
mock_boto_client.return_value = mock_client
|
||||
|
||||
# 模拟上传成功
|
||||
mock_client.upload_file.return_value = None
|
||||
|
||||
toolkit = S3StorageToolkit(
|
||||
endpoint_url="https://test.example.com",
|
||||
access_key_id="test_key",
|
||||
secret_access_key="test_secret",
|
||||
bucket_name="test-bucket"
|
||||
)
|
||||
|
||||
# 创建临时文件进行测试
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
|
||||
f.write("test content")
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
result = toolkit.upload_file(temp_file, "test/file.txt")
|
||||
|
||||
assert result['success'] is True
|
||||
assert result['key'] == "test/file.txt"
|
||||
assert result['bucket'] == "test-bucket"
|
||||
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
@patch('boto3.client')
|
||||
def test_create_folder_success(self, mock_boto_client):
|
||||
"""测试文件夹创建成功"""
|
||||
# 模拟 boto3 客户端
|
||||
mock_client = Mock()
|
||||
mock_boto_client.return_value = mock_client
|
||||
|
||||
# 模拟上传成功
|
||||
mock_client.put_object.return_value = {}
|
||||
|
||||
toolkit = S3StorageToolkit(
|
||||
endpoint_url="https://test.example.com",
|
||||
access_key_id="test_key",
|
||||
secret_access_key="test_secret",
|
||||
bucket_name="test-bucket"
|
||||
)
|
||||
|
||||
result = toolkit.create_folder("test/folder/")
|
||||
|
||||
assert result['success'] is True
|
||||
assert result['folder_path'] == "test/folder/"
|
||||
assert result['bucket'] == "test-bucket"
|
||||
|
||||
def test_folder_path_normalization(self):
|
||||
"""测试文件夹路径规范化"""
|
||||
toolkit = S3StorageToolkit(
|
||||
endpoint_url="https://test.example.com",
|
||||
access_key_id="test_key",
|
||||
secret_access_key="test_secret",
|
||||
bucket_name="test-bucket"
|
||||
)
|
||||
|
||||
# 测试路径规范化(这个方法可能需要在实际类中实现)
|
||||
# 这里只是示例,实际实现可能不同
|
||||
test_paths = [
|
||||
("folder", "folder/"),
|
||||
("folder/", "folder/"),
|
||||
("folder/subfolder", "folder/subfolder/"),
|
||||
("folder/subfolder/", "folder/subfolder/"),
|
||||
]
|
||||
|
||||
for input_path, expected in test_paths:
|
||||
# 假设有一个内部方法来规范化路径
|
||||
if not input_path.endswith('/'):
|
||||
normalized = input_path + '/'
|
||||
else:
|
||||
normalized = input_path
|
||||
assert normalized == expected
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
65
tox.ini
Normal file
65
tox.ini
Normal file
@@ -0,0 +1,65 @@
|
||||
[tox]
|
||||
envlist = py39,py310,py311,py312,py313,lint,coverage
|
||||
isolated_build = true
|
||||
|
||||
[testenv]
|
||||
deps =
|
||||
pytest>=7.0.0
|
||||
pytest-cov>=4.0.0
|
||||
pytest-asyncio>=0.21.0
|
||||
commands =
|
||||
pytest tests/ -v --cov=rustfs_s3_toolkit --cov-report=term-missing
|
||||
|
||||
[testenv:lint]
|
||||
deps =
|
||||
black>=23.0.0
|
||||
isort>=5.12.0
|
||||
flake8>=6.0.0
|
||||
mypy>=1.0.0
|
||||
commands =
|
||||
black --check src/ tests/ examples/
|
||||
isort --check-only src/ tests/ examples/
|
||||
flake8 src/ tests/ examples/
|
||||
mypy src/
|
||||
|
||||
[testenv:coverage]
|
||||
deps =
|
||||
pytest>=7.0.0
|
||||
pytest-cov>=4.0.0
|
||||
coverage[toml]>=7.0.0
|
||||
commands =
|
||||
pytest tests/ --cov=rustfs_s3_toolkit --cov-report=html --cov-report=xml --cov-fail-under=80
|
||||
|
||||
[testenv:format]
|
||||
deps =
|
||||
black>=23.0.0
|
||||
isort>=5.12.0
|
||||
commands =
|
||||
black src/ tests/ examples/
|
||||
isort src/ tests/ examples/
|
||||
|
||||
[flake8]
|
||||
max-line-length = 88
|
||||
extend-ignore = E203, W503
|
||||
exclude =
|
||||
.git,
|
||||
__pycache__,
|
||||
.tox,
|
||||
.venv,
|
||||
build,
|
||||
dist,
|
||||
*.egg-info
|
||||
|
||||
[coverage:run]
|
||||
source = src/
|
||||
omit =
|
||||
*/tests/*
|
||||
*/test_*
|
||||
*/__pycache__/*
|
||||
|
||||
[coverage:report]
|
||||
exclude_lines =
|
||||
pragma: no cover
|
||||
def __repr__
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
Reference in New Issue
Block a user