<div align="center">
# QThreadWithReturn

[](https://python.org)
[](https://www.qt.io/qt-for-python)
[](LICENSE)
[](tests/)
一个基于 PySide6 的线程工具库,简化 GUI 应用中的多线程编程。
QThreadWithReturn 为传统 QThread 提供了更直观的 API,支持返回值和回调机制,避免复杂的信号槽设置。
</div>
## ✨ 特性
### 🎯 QThreadWithReturn
- 支持获取线程执行结果,提供类似 `concurrent.futures.Future` 的 API
- 灵活的回调机制,支持多种回调函数签名
- 内置超时控制和任务取消
- 线程安全的状态管理
- 与 Qt 事件循环无缝集成
### 🏊♂️ QThreadPoolExecutor
- 完全兼容 `concurrent.futures.ThreadPoolExecutor` API
- 线程池管理和任务调度
- 支持线程初始化器和命名,便于调试
- 支持 `as_completed` 等标准接口
- 上下文管理器支持
## 🚀 安装
```bash
# 使用 uv
uv add qthreadwithreturn
# 使用 pip
pip install qthreadwithreturn
```
## 💡 使用示例
### 问题场景
在 GUI 应用中执行耗时操作时,传统做法会阻塞主线程,导致界面无响应。
#### ❌ 传统做法的问题
```python
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QLabel, QVBoxLayout, QWidget
import time
import sys
class BadExample(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("传统做法示例")
# 创建中心组件和布局
central_widget = QWidget()
layout = QVBoxLayout()
self.button = QPushButton("执行耗时任务")
self.label = QLabel("就绪")
layout.addWidget(self.label)
layout.addWidget(self.button)
central_widget.setLayout(layout)
self.setCentralWidget(central_widget)
self.button.clicked.connect(self.blocking_task)
def blocking_task(self):
"""在主线程执行,会阻塞界面"""
self.label.setText("处理中...")
time.sleep(5) # 主线程被阻塞
self.label.setText("完成")
# 运行示例
if __name__ == "__main__":
app = QApplication(sys.argv)
window = BadExample()
window.show()
app.exec()
```
#### ✅ 使用 QThreadWithReturn 的解决方案
```python
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QLabel, QProgressBar, QVBoxLayout, QWidget
from qthreadwithreturn import QThreadWithReturn
import time
import sys
class GoodExample(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("QThreadWithReturn 示例")
# 创建中心组件和布局
central_widget = QWidget()
layout = QVBoxLayout()
self.button = QPushButton("执行耗时任务")
self.label = QLabel("就绪")
self.progress = QProgressBar()
layout.addWidget(self.label)
layout.addWidget(self.progress)
layout.addWidget(self.button)
central_widget.setLayout(layout)
self.setCentralWidget(central_widget)
self.button.clicked.connect(self.start_task)
def start_task(self):
"""使用 QThreadWithReturn 在后台执行任务"""
def heavy_computation():
# 模拟耗时计算
for i in range(100):
time.sleep(0.05)
return "处理完成"
# 创建线程
self.thread = QThreadWithReturn(heavy_computation)
# 更新UI状态
self.button.setEnabled(False)
self.label.setText("后台处理中...")
self.progress.setRange(0, 0)
# 设置回调
self.thread.add_done_callback(self.on_task_completed)
self.thread.add_failure_callback(self.on_task_failed)
# 启动线程,界面保持响应
self.thread.start()
def on_task_completed(self, result):
"""任务完成回调"""
self.button.setEnabled(True)
self.label.setText(result)
self.progress.setRange(0, 1)
self.progress.setValue(1)
def on_task_failed(self, exception):
"""任务失败回调"""
self.button.setEnabled(True)
self.label.setText(f"处理失败: {exception}")
self.progress.setRange(0, 1)
# 运行示例
if __name__ == "__main__":
app = QApplication(sys.argv)
window = GoodExample()
window.show()
app.exec()
```
#### 🏊♂️ 线程池使用示例
```python
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QLabel, QProgressBar, QVBoxLayout, QWidget
from qthreadwithreturn import QThreadPoolExecutor
import time
import sys
class BatchProcessingExample(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("线程池批处理示例")
# 创建中心组件和布局
central_widget = QWidget()
layout = QVBoxLayout()
self.start_btn = QPushButton("开始批处理")
self.progress = QProgressBar()
self.status = QLabel("就绪")
layout.addWidget(self.status)
layout.addWidget(self.progress)
layout.addWidget(self.start_btn)
central_widget.setLayout(layout)
self.setCentralWidget(central_widget)
self.start_btn.clicked.connect(self.process_files)
def process_files(self):
file_list = ["file1.txt", "file2.txt", "file3.txt", "file4.txt"]
def process_single_file(filename):
# 模拟文件处理
time.sleep(2)
return f"{filename} 完成"
# 创建线程池
self.pool = QThreadPoolExecutor(max_workers=2)
self.completed_count = 0
self.total_files = len(file_list)
# 更新UI
self.progress.setMaximum(self.total_files)
self.progress.setValue(0)
self.status.setText("处理中...")
self.start_btn.setEnabled(False)
# 提交任务
for filename in file_list:
future = self.pool.submit(process_single_file, filename)
future.add_done_callback(self.on_file_completed)
def on_file_completed(self, result):
"""任务完成回调"""
self.completed_count += 1
self.progress.setValue(self.completed_count)
self.status.setText(f"完成 {self.completed_count}/{self.total_files}: {result}")
if self.completed_count == self.total_files:
self.status.setText("所有任务完成")
self.start_btn.setEnabled(True)
self.pool.shutdown()
# 运行示例
if __name__ == "__main__":
app = QApplication(sys.argv)
window = BatchProcessingExample()
window.show()
app.exec()
```
## 🆚 与传统 QThread 对比
### 传统 QThread 实现
```python
from PySide6.QtCore import QThread, QObject, Signal
# 传统方式需要较多样板代码
class Worker(QObject):
finished = Signal(object)
error = Signal(Exception)
def __init__(self, func, *args, **kwargs):
super().__init__()
self.func = func
self.args = args
self.kwargs = kwargs
def run(self):
try:
result = self.func(*self.args, **self.kwargs)
self.finished.emit(result)
except Exception as e:
self.error.emit(e)
# 使用时的设置
def traditional_approach():
thread = QThread()
worker = Worker(my_function, arg1, arg2)
worker.moveToThread(thread)
# 信号连接
thread.started.connect(worker.run)
worker.finished.connect(lambda result: print(f"结果: {result}"))
worker.finished.connect(thread.quit)
worker.finished.connect(worker.deleteLater)
thread.finished.connect(thread.deleteLater)
thread.start()
# 获取返回值需要通过信号处理
```
### QThreadWithReturn 实现
```python
# 简化的使用方式
thread = QThreadWithReturn(my_function, arg1, arg2)
thread.add_done_callback(lambda result: print(f"结果: {result}"))
thread.start()
# 直接获取返回值
result = thread.result()
```
### 对比总结
| 特性 | 传统 QThread | QThreadWithReturn |
|------|-------------|------------------|
| **代码量** | 较多样板代码 | 简化的接口 |
| **返回值** | 信号传递 | 直接 `result()` 获取 |
| **错误处理** | 手动信号连接 | 自动异常传播 |
| **资源清理** | 手动管理 | 自动清理 |
| **超时控制** | 需额外实现 | 内置支持 |
| **任务取消** | 需自行处理 | 内置 `cancel()` |
| **线程池** | 需自己实现 | 提供现成实现 |
| **学习成本** | 需理解信号槽 | 接近标准库 API |
## 📚 高级功能
### 🎨 回调机制
```python
# 无参数回调
thread.add_done_callback(lambda: print("任务完成"))
# 单参数回调
thread.add_done_callback(lambda result: print(f"结果: {result}"))
# 多参数回调 - 自动解包
def multi_return_task():
return 1, 2, 3
thread = QThreadWithReturn(multi_return_task)
thread.add_done_callback(lambda a, b, c: print(f"{a}, {b}, {c}"))
# 类方法回调
class ResultHandler:
def handle_result(self, result):
self.result = result
handler = ResultHandler()
thread.add_done_callback(handler.handle_result)
```
### ⏰ 超时控制
```python
# 设置5秒超时
thread.start(timeout_ms=5000)
try:
result = thread.result(timeout=5.0)
except TimeoutError:
print("任务超时")
except Exception as e:
print(f"任务失败: {e}")
```
### 🛑 任务取消
```python
# 优雅取消
success = thread.cancel()
# 强制终止(需谨慎使用)
success = thread.cancel(force_stop=True)
# 检查状态
if thread.cancelled():
print("任务已取消")
```
### 🔄 错误处理
```python
def failing_task():
raise ValueError("模拟错误")
thread = QThreadWithReturn(failing_task)
# 添加失败回调
thread.add_failure_callback(lambda exc: print(f"任务失败: {exc}"))
thread.start()
try:
result = thread.result()
except ValueError as e:
print(f"捕获异常: {e}")
```
### 🏊♂️ 线程池高级用法
```python
def init_worker(worker_name):
"""工作线程初始化"""
print(f"初始化工作线程: {worker_name}")
def compute_task(x):
return x ** 2
with QThreadPoolExecutor(
max_workers=4,
thread_name_prefix="计算线程",
initializer=init_worker,
initargs=("数据处理器",)
) as pool:
# 提交任务并添加回调
future = pool.submit(compute_task, 10)
future.add_done_callback(lambda result: print(f"计算完成: {result}"))
# 等待结果
print(future.result()) # 输出: 100
```
## 🎮 演示程序
### 🆚 GUI 对比演示
运行对比程序,体验不同实现方式的差异:
```bash
# 对比演示
python examples/gui_demo_comparison.py
```
演示内容:
- 传统做法:界面阻塞演示
- QThreadWithReturn:响应式界面演示
- 线程池:并行任务处理演示
### 📱 完整功能演示
```bash
# 完整演示程序
python -m demo.thread_demo_gui
```
### 💻 命令行示例
```bash
# 基本用法示例
python examples/basic_usage.py
```
## 🎯 应用场景
QThreadWithReturn 适合以下 GUI 应用场景:
### 📊 数据处理应用
```python
# 数据分析、文件处理
thread = QThreadWithReturn(pandas.read_csv, "large_file.csv")
thread.add_done_callback(lambda df: self.update_table_view(df))
```
### 🌐 网络应用
```python
# HTTP请求、API调用
thread = QThreadWithReturn(requests.get, "https://api.example.com/data")
thread.add_done_callback(lambda resp: self.display_data(resp.json()))
```
### 🎨 图像处理工具
```python
# 图像处理、批量转换
with QThreadPoolExecutor(max_workers=4) as pool:
futures = [pool.submit(process_image, img) for img in images]
for future in pool.as_completed(futures):
self.update_progress()
```
### 📁 文件管理器
```python
# 文件操作、批量处理
thread = QThreadWithReturn(shutil.copy2, source, destination)
thread.add_done_callback(lambda: self.refresh_file_list())
```
### 🤖 机器学习工具
```python
# 模型推理、数据处理
thread = QThreadWithReturn(model.predict, input_data)
thread.add_done_callback(lambda result: self.show_predictions(result))
```
## 🔧 兼容性
- **Python**: 3.10+
- **Qt 版本**: PySide6 6.4+
- **操作系统**: Windows, macOS, Linux
- **运行环境**:
- 有 Qt 应用环境:使用 Qt 信号机制
- 无 Qt 应用环境:自动切换到标准线程模式
- 已在 Python 3.10、3.11、3.13 中测试
## 🧪 测试
项目包含 73 个测试用例:
```bash
# 运行所有测试
pytest tests/
# 运行特定测试
pytest tests/test_thread_utils.py -v
# 生成覆盖率报告
pytest tests/ --cov=qthreadwithreturn
```
## 📖 API 参考
### QThreadWithReturn
| 方法 | 描述 |
|------|------|
| `start(timeout_ms=-1)` | 启动线程,可选超时设置 |
| `result(timeout=None)` | 获取执行结果,阻塞等待 |
| `exception(timeout=None)` | 获取异常信息 |
| `cancel(force_stop=False)` | 取消线程执行 |
| `running()` | 检查是否正在运行 |
| `done()` | 检查是否已完成 |
| `cancelled()` | 检查是否已取消 |
| `add_done_callback(callback)` | 添加成功完成回调 |
| `add_failure_callback(callback)` | 添加失败回调 |
### QThreadPoolExecutor
| 方法 | 描述 |
|------|------|
| `submit(fn, *args, **kwargs)` | 提交任务到线程池 |
| `shutdown(wait=True, cancel_futures=False, force_stop=False)` | 关闭线程池 |
| `as_completed(futures, timeout=None)` | 按完成顺序迭代 Future 对象 |
## 💡 最佳实践
### ✅ 推荐做法
```python
# 1. 使用上下文管理器自动资源清理
with QThreadPoolExecutor(max_workers=4) as pool:
futures = [pool.submit(task, i) for i in range(10)]
results = [f.result() for f in futures]
# 2. 在回调中更新 UI(回调在主线程执行)
def update_progress(result):
progress_bar.setValue(result)
thread.add_done_callback(update_progress)
# 3. 合理设置超时时间
thread.start(timeout_ms=30000) # 30秒超时
# 4. 异常处理
try:
result = thread.result()
except Exception as e:
logger.error(f"任务失败: {e}")
```
### ⚠️ 注意事项
```python
# 避免在工作线程中直接更新 UI
def bad_worker():
label.setText("更新") # 错误:跨线程UI更新
# 记得资源清理
pool = QThreadPoolExecutor()
# 使用完后记得调用 shutdown()
# 谨慎使用强制终止
thread.cancel(force_stop=True) # 可能导致资源泄漏
```
## 🤝 贡献
欢迎贡献代码,请遵循以下步骤:
1. Fork 本仓库
2. 创建特性分支 (`git checkout -b feature/new-feature`)
3. 提交更改 (`git commit -m 'Add new feature'`)
4. 推送到分支 (`git push origin feature/new-feature`)
5. 开启 Pull Request
### 🛠️ 开发环境设置
```bash
# 克隆仓库
git clone https://github.com/271374667/QThreadWithReturn.git
cd QThreadWithReturn
# 使用 uv 安装依赖
uv sync
# 运行测试
uv run pytest
# 运行演示
uv run python -m demo.thread_demo_gui
```
## 📄 许可证
本项目使用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
## 📞 支持
- **问题报告**: [GitHub Issues](https://github.com/271374667/QThreadWithReturn/issues)
- **讨论**: [GitHub Discussions](https://github.com/271374667/QThreadWithReturn/discussions)
- **邮件**: 271374667@qq.com
Raw data
{
"_id": null,
"home_page": null,
"name": "qthreadwithreturn",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.10",
"maintainer_email": "PythonImporter <271374667@qq.com>",
"keywords": "qt, pyside6, threading, concurrent, futures, gui, pyqt, async, thread-pool, worker-thread",
"author": null,
"author_email": "PythonImporter <271374667@qq.com>",
"download_url": "https://files.pythonhosted.org/packages/00/44/ff0eb48bb740e32fdfda991abea78070ccc3678833c36e72e7233896f3eb/qthreadwithreturn-1.1.1.tar.gz",
"platform": null,
"description": "<div align=\"center\">\r\n\r\n# QThreadWithReturn\r\n\r\n\r\n\r\n[](https://python.org)\r\n[](https://www.qt.io/qt-for-python)\r\n[](LICENSE)\r\n[](tests/)\r\n\r\n\u4e00\u4e2a\u57fa\u4e8e PySide6 \u7684\u7ebf\u7a0b\u5de5\u5177\u5e93\uff0c\u7b80\u5316 GUI \u5e94\u7528\u4e2d\u7684\u591a\u7ebf\u7a0b\u7f16\u7a0b\u3002\r\n\r\nQThreadWithReturn \u4e3a\u4f20\u7edf QThread \u63d0\u4f9b\u4e86\u66f4\u76f4\u89c2\u7684 API\uff0c\u652f\u6301\u8fd4\u56de\u503c\u548c\u56de\u8c03\u673a\u5236\uff0c\u907f\u514d\u590d\u6742\u7684\u4fe1\u53f7\u69fd\u8bbe\u7f6e\u3002\r\n\r\n</div>\r\n\r\n## \u2728 \u7279\u6027\r\n\r\n### \ud83c\udfaf QThreadWithReturn\r\n- \u652f\u6301\u83b7\u53d6\u7ebf\u7a0b\u6267\u884c\u7ed3\u679c\uff0c\u63d0\u4f9b\u7c7b\u4f3c `concurrent.futures.Future` \u7684 API\r\n- \u7075\u6d3b\u7684\u56de\u8c03\u673a\u5236\uff0c\u652f\u6301\u591a\u79cd\u56de\u8c03\u51fd\u6570\u7b7e\u540d\r\n- \u5185\u7f6e\u8d85\u65f6\u63a7\u5236\u548c\u4efb\u52a1\u53d6\u6d88\r\n- \u7ebf\u7a0b\u5b89\u5168\u7684\u72b6\u6001\u7ba1\u7406\r\n- \u4e0e Qt \u4e8b\u4ef6\u5faa\u73af\u65e0\u7f1d\u96c6\u6210\r\n\r\n### \ud83c\udfca\u200d\u2642\ufe0f QThreadPoolExecutor\r\n- \u5b8c\u5168\u517c\u5bb9 `concurrent.futures.ThreadPoolExecutor` API\r\n- \u7ebf\u7a0b\u6c60\u7ba1\u7406\u548c\u4efb\u52a1\u8c03\u5ea6\r\n- \u652f\u6301\u7ebf\u7a0b\u521d\u59cb\u5316\u5668\u548c\u547d\u540d\uff0c\u4fbf\u4e8e\u8c03\u8bd5\r\n- \u652f\u6301 `as_completed` \u7b49\u6807\u51c6\u63a5\u53e3\r\n- \u4e0a\u4e0b\u6587\u7ba1\u7406\u5668\u652f\u6301\r\n\r\n## \ud83d\ude80 \u5b89\u88c5\r\n\r\n```bash\r\n# \u4f7f\u7528 uv\r\nuv add qthreadwithreturn\r\n\r\n# \u4f7f\u7528 pip \r\npip install qthreadwithreturn\r\n```\r\n\r\n## \ud83d\udca1 \u4f7f\u7528\u793a\u4f8b\r\n\r\n### \u95ee\u9898\u573a\u666f\r\n\r\n\u5728 GUI \u5e94\u7528\u4e2d\u6267\u884c\u8017\u65f6\u64cd\u4f5c\u65f6\uff0c\u4f20\u7edf\u505a\u6cd5\u4f1a\u963b\u585e\u4e3b\u7ebf\u7a0b\uff0c\u5bfc\u81f4\u754c\u9762\u65e0\u54cd\u5e94\u3002\r\n\r\n#### \u274c \u4f20\u7edf\u505a\u6cd5\u7684\u95ee\u9898\r\n\r\n```python\r\nfrom PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QLabel, QVBoxLayout, QWidget\r\nimport time\r\nimport sys\r\n\r\nclass BadExample(QMainWindow):\r\n def __init__(self):\r\n super().__init__()\r\n self.setWindowTitle(\"\u4f20\u7edf\u505a\u6cd5\u793a\u4f8b\")\r\n \r\n # \u521b\u5efa\u4e2d\u5fc3\u7ec4\u4ef6\u548c\u5e03\u5c40\r\n central_widget = QWidget()\r\n layout = QVBoxLayout()\r\n \r\n self.button = QPushButton(\"\u6267\u884c\u8017\u65f6\u4efb\u52a1\")\r\n self.label = QLabel(\"\u5c31\u7eea\")\r\n \r\n layout.addWidget(self.label)\r\n layout.addWidget(self.button)\r\n central_widget.setLayout(layout)\r\n self.setCentralWidget(central_widget)\r\n \r\n self.button.clicked.connect(self.blocking_task)\r\n \r\n def blocking_task(self):\r\n \"\"\"\u5728\u4e3b\u7ebf\u7a0b\u6267\u884c\uff0c\u4f1a\u963b\u585e\u754c\u9762\"\"\"\r\n self.label.setText(\"\u5904\u7406\u4e2d...\")\r\n time.sleep(5) # \u4e3b\u7ebf\u7a0b\u88ab\u963b\u585e\r\n self.label.setText(\"\u5b8c\u6210\")\r\n\r\n# \u8fd0\u884c\u793a\u4f8b\r\nif __name__ == \"__main__\":\r\n app = QApplication(sys.argv)\r\n window = BadExample()\r\n window.show()\r\n app.exec()\r\n```\r\n\r\n#### \u2705 \u4f7f\u7528 QThreadWithReturn \u7684\u89e3\u51b3\u65b9\u6848\r\n\r\n```python\r\nfrom PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QLabel, QProgressBar, QVBoxLayout, QWidget\r\nfrom qthreadwithreturn import QThreadWithReturn\r\nimport time\r\nimport sys\r\n\r\nclass GoodExample(QMainWindow):\r\n def __init__(self):\r\n super().__init__()\r\n self.setWindowTitle(\"QThreadWithReturn \u793a\u4f8b\")\r\n \r\n # \u521b\u5efa\u4e2d\u5fc3\u7ec4\u4ef6\u548c\u5e03\u5c40\r\n central_widget = QWidget()\r\n layout = QVBoxLayout()\r\n \r\n self.button = QPushButton(\"\u6267\u884c\u8017\u65f6\u4efb\u52a1\")\r\n self.label = QLabel(\"\u5c31\u7eea\")\r\n self.progress = QProgressBar()\r\n \r\n layout.addWidget(self.label)\r\n layout.addWidget(self.progress)\r\n layout.addWidget(self.button)\r\n central_widget.setLayout(layout)\r\n self.setCentralWidget(central_widget)\r\n \r\n self.button.clicked.connect(self.start_task)\r\n \r\n def start_task(self):\r\n \"\"\"\u4f7f\u7528 QThreadWithReturn \u5728\u540e\u53f0\u6267\u884c\u4efb\u52a1\"\"\"\r\n def heavy_computation():\r\n # \u6a21\u62df\u8017\u65f6\u8ba1\u7b97\r\n for i in range(100):\r\n time.sleep(0.05)\r\n return \"\u5904\u7406\u5b8c\u6210\"\r\n \r\n # \u521b\u5efa\u7ebf\u7a0b\r\n self.thread = QThreadWithReturn(heavy_computation)\r\n \r\n # \u66f4\u65b0UI\u72b6\u6001\r\n self.button.setEnabled(False)\r\n self.label.setText(\"\u540e\u53f0\u5904\u7406\u4e2d...\")\r\n self.progress.setRange(0, 0)\r\n \r\n # \u8bbe\u7f6e\u56de\u8c03\r\n self.thread.add_done_callback(self.on_task_completed)\r\n self.thread.add_failure_callback(self.on_task_failed)\r\n \r\n # \u542f\u52a8\u7ebf\u7a0b\uff0c\u754c\u9762\u4fdd\u6301\u54cd\u5e94\r\n self.thread.start()\r\n \r\n def on_task_completed(self, result):\r\n \"\"\"\u4efb\u52a1\u5b8c\u6210\u56de\u8c03\"\"\"\r\n self.button.setEnabled(True)\r\n self.label.setText(result)\r\n self.progress.setRange(0, 1)\r\n self.progress.setValue(1)\r\n \r\n def on_task_failed(self, exception):\r\n \"\"\"\u4efb\u52a1\u5931\u8d25\u56de\u8c03\"\"\"\r\n self.button.setEnabled(True)\r\n self.label.setText(f\"\u5904\u7406\u5931\u8d25: {exception}\")\r\n self.progress.setRange(0, 1)\r\n\r\n# \u8fd0\u884c\u793a\u4f8b\r\nif __name__ == \"__main__\":\r\n app = QApplication(sys.argv)\r\n window = GoodExample()\r\n window.show()\r\n app.exec()\r\n```\r\n\r\n#### \ud83c\udfca\u200d\u2642\ufe0f \u7ebf\u7a0b\u6c60\u4f7f\u7528\u793a\u4f8b\r\n\r\n```python\r\nfrom PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QLabel, QProgressBar, QVBoxLayout, QWidget\r\nfrom qthreadwithreturn import QThreadPoolExecutor\r\nimport time\r\nimport sys\r\n\r\nclass BatchProcessingExample(QMainWindow):\r\n def __init__(self):\r\n super().__init__()\r\n self.setWindowTitle(\"\u7ebf\u7a0b\u6c60\u6279\u5904\u7406\u793a\u4f8b\")\r\n \r\n # \u521b\u5efa\u4e2d\u5fc3\u7ec4\u4ef6\u548c\u5e03\u5c40\r\n central_widget = QWidget()\r\n layout = QVBoxLayout()\r\n \r\n self.start_btn = QPushButton(\"\u5f00\u59cb\u6279\u5904\u7406\")\r\n self.progress = QProgressBar()\r\n self.status = QLabel(\"\u5c31\u7eea\")\r\n \r\n layout.addWidget(self.status)\r\n layout.addWidget(self.progress)\r\n layout.addWidget(self.start_btn)\r\n central_widget.setLayout(layout)\r\n self.setCentralWidget(central_widget)\r\n \r\n self.start_btn.clicked.connect(self.process_files)\r\n \r\n def process_files(self):\r\n file_list = [\"file1.txt\", \"file2.txt\", \"file3.txt\", \"file4.txt\"]\r\n \r\n def process_single_file(filename):\r\n # \u6a21\u62df\u6587\u4ef6\u5904\u7406\r\n time.sleep(2)\r\n return f\"{filename} \u5b8c\u6210\"\r\n \r\n # \u521b\u5efa\u7ebf\u7a0b\u6c60\r\n self.pool = QThreadPoolExecutor(max_workers=2)\r\n self.completed_count = 0\r\n self.total_files = len(file_list)\r\n \r\n # \u66f4\u65b0UI\r\n self.progress.setMaximum(self.total_files)\r\n self.progress.setValue(0)\r\n self.status.setText(\"\u5904\u7406\u4e2d...\")\r\n self.start_btn.setEnabled(False)\r\n \r\n # \u63d0\u4ea4\u4efb\u52a1\r\n for filename in file_list:\r\n future = self.pool.submit(process_single_file, filename)\r\n future.add_done_callback(self.on_file_completed)\r\n \r\n def on_file_completed(self, result):\r\n \"\"\"\u4efb\u52a1\u5b8c\u6210\u56de\u8c03\"\"\"\r\n self.completed_count += 1\r\n self.progress.setValue(self.completed_count)\r\n self.status.setText(f\"\u5b8c\u6210 {self.completed_count}/{self.total_files}: {result}\")\r\n \r\n if self.completed_count == self.total_files:\r\n self.status.setText(\"\u6240\u6709\u4efb\u52a1\u5b8c\u6210\")\r\n self.start_btn.setEnabled(True)\r\n self.pool.shutdown()\r\n\r\n# \u8fd0\u884c\u793a\u4f8b\r\nif __name__ == \"__main__\":\r\n app = QApplication(sys.argv)\r\n window = BatchProcessingExample()\r\n window.show()\r\n app.exec()\r\n```\r\n\r\n## \ud83c\udd9a \u4e0e\u4f20\u7edf QThread \u5bf9\u6bd4\r\n\r\n### \u4f20\u7edf QThread \u5b9e\u73b0\r\n\r\n```python\r\nfrom PySide6.QtCore import QThread, QObject, Signal\r\n\r\n# \u4f20\u7edf\u65b9\u5f0f\u9700\u8981\u8f83\u591a\u6837\u677f\u4ee3\u7801\r\nclass Worker(QObject):\r\n finished = Signal(object)\r\n error = Signal(Exception)\r\n \r\n def __init__(self, func, *args, **kwargs):\r\n super().__init__()\r\n self.func = func\r\n self.args = args\r\n self.kwargs = kwargs\r\n \r\n def run(self):\r\n try:\r\n result = self.func(*self.args, **self.kwargs)\r\n self.finished.emit(result)\r\n except Exception as e:\r\n self.error.emit(e)\r\n\r\n# \u4f7f\u7528\u65f6\u7684\u8bbe\u7f6e\r\ndef traditional_approach():\r\n thread = QThread()\r\n worker = Worker(my_function, arg1, arg2)\r\n worker.moveToThread(thread)\r\n \r\n # \u4fe1\u53f7\u8fde\u63a5\r\n thread.started.connect(worker.run)\r\n worker.finished.connect(lambda result: print(f\"\u7ed3\u679c: {result}\"))\r\n worker.finished.connect(thread.quit)\r\n worker.finished.connect(worker.deleteLater)\r\n thread.finished.connect(thread.deleteLater)\r\n \r\n thread.start()\r\n # \u83b7\u53d6\u8fd4\u56de\u503c\u9700\u8981\u901a\u8fc7\u4fe1\u53f7\u5904\u7406\r\n```\r\n\r\n### QThreadWithReturn \u5b9e\u73b0\r\n\r\n```python\r\n# \u7b80\u5316\u7684\u4f7f\u7528\u65b9\u5f0f\r\nthread = QThreadWithReturn(my_function, arg1, arg2)\r\nthread.add_done_callback(lambda result: print(f\"\u7ed3\u679c: {result}\"))\r\nthread.start()\r\n\r\n# \u76f4\u63a5\u83b7\u53d6\u8fd4\u56de\u503c\r\nresult = thread.result()\r\n```\r\n\r\n### \u5bf9\u6bd4\u603b\u7ed3\r\n\r\n| \u7279\u6027 | \u4f20\u7edf QThread | QThreadWithReturn |\r\n|------|-------------|------------------|\r\n| **\u4ee3\u7801\u91cf** | \u8f83\u591a\u6837\u677f\u4ee3\u7801 | \u7b80\u5316\u7684\u63a5\u53e3 |\r\n| **\u8fd4\u56de\u503c** | \u4fe1\u53f7\u4f20\u9012 | \u76f4\u63a5 `result()` \u83b7\u53d6 |\r\n| **\u9519\u8bef\u5904\u7406** | \u624b\u52a8\u4fe1\u53f7\u8fde\u63a5 | \u81ea\u52a8\u5f02\u5e38\u4f20\u64ad |\r\n| **\u8d44\u6e90\u6e05\u7406** | \u624b\u52a8\u7ba1\u7406 | \u81ea\u52a8\u6e05\u7406 |\r\n| **\u8d85\u65f6\u63a7\u5236** | \u9700\u989d\u5916\u5b9e\u73b0 | \u5185\u7f6e\u652f\u6301 |\r\n| **\u4efb\u52a1\u53d6\u6d88** | \u9700\u81ea\u884c\u5904\u7406 | \u5185\u7f6e `cancel()` |\r\n| **\u7ebf\u7a0b\u6c60** | \u9700\u81ea\u5df1\u5b9e\u73b0 | \u63d0\u4f9b\u73b0\u6210\u5b9e\u73b0 |\r\n| **\u5b66\u4e60\u6210\u672c** | \u9700\u7406\u89e3\u4fe1\u53f7\u69fd | \u63a5\u8fd1\u6807\u51c6\u5e93 API |\r\n\r\n## \ud83d\udcda \u9ad8\u7ea7\u529f\u80fd\r\n\r\n### \ud83c\udfa8 \u56de\u8c03\u673a\u5236\r\n\r\n```python\r\n# \u65e0\u53c2\u6570\u56de\u8c03\r\nthread.add_done_callback(lambda: print(\"\u4efb\u52a1\u5b8c\u6210\"))\r\n\r\n# \u5355\u53c2\u6570\u56de\u8c03\r\nthread.add_done_callback(lambda result: print(f\"\u7ed3\u679c: {result}\"))\r\n\r\n# \u591a\u53c2\u6570\u56de\u8c03 - \u81ea\u52a8\u89e3\u5305\r\ndef multi_return_task():\r\n return 1, 2, 3\r\n\r\nthread = QThreadWithReturn(multi_return_task)\r\nthread.add_done_callback(lambda a, b, c: print(f\"{a}, {b}, {c}\"))\r\n\r\n# \u7c7b\u65b9\u6cd5\u56de\u8c03\r\nclass ResultHandler:\r\n def handle_result(self, result):\r\n self.result = result\r\n\r\nhandler = ResultHandler()\r\nthread.add_done_callback(handler.handle_result)\r\n```\r\n\r\n### \u23f0 \u8d85\u65f6\u63a7\u5236\r\n\r\n```python\r\n# \u8bbe\u7f6e5\u79d2\u8d85\u65f6\r\nthread.start(timeout_ms=5000)\r\n\r\ntry:\r\n result = thread.result(timeout=5.0)\r\nexcept TimeoutError:\r\n print(\"\u4efb\u52a1\u8d85\u65f6\")\r\nexcept Exception as e:\r\n print(f\"\u4efb\u52a1\u5931\u8d25: {e}\")\r\n```\r\n\r\n### \ud83d\uded1 \u4efb\u52a1\u53d6\u6d88\r\n\r\n```python\r\n# \u4f18\u96c5\u53d6\u6d88\r\nsuccess = thread.cancel()\r\n\r\n# \u5f3a\u5236\u7ec8\u6b62\uff08\u9700\u8c28\u614e\u4f7f\u7528\uff09\r\nsuccess = thread.cancel(force_stop=True)\r\n\r\n# \u68c0\u67e5\u72b6\u6001\r\nif thread.cancelled():\r\n print(\"\u4efb\u52a1\u5df2\u53d6\u6d88\")\r\n```\r\n\r\n### \ud83d\udd04 \u9519\u8bef\u5904\u7406\r\n\r\n```python\r\ndef failing_task():\r\n raise ValueError(\"\u6a21\u62df\u9519\u8bef\")\r\n\r\nthread = QThreadWithReturn(failing_task)\r\n\r\n# \u6dfb\u52a0\u5931\u8d25\u56de\u8c03\r\nthread.add_failure_callback(lambda exc: print(f\"\u4efb\u52a1\u5931\u8d25: {exc}\"))\r\n\r\nthread.start()\r\n\r\ntry:\r\n result = thread.result()\r\nexcept ValueError as e:\r\n print(f\"\u6355\u83b7\u5f02\u5e38: {e}\")\r\n```\r\n\r\n### \ud83c\udfca\u200d\u2642\ufe0f \u7ebf\u7a0b\u6c60\u9ad8\u7ea7\u7528\u6cd5\r\n\r\n```python\r\ndef init_worker(worker_name):\r\n \"\"\"\u5de5\u4f5c\u7ebf\u7a0b\u521d\u59cb\u5316\"\"\"\r\n print(f\"\u521d\u59cb\u5316\u5de5\u4f5c\u7ebf\u7a0b: {worker_name}\")\r\n\r\ndef compute_task(x):\r\n return x ** 2\r\n\r\nwith QThreadPoolExecutor(\r\n max_workers=4,\r\n thread_name_prefix=\"\u8ba1\u7b97\u7ebf\u7a0b\",\r\n initializer=init_worker,\r\n initargs=(\"\u6570\u636e\u5904\u7406\u5668\",)\r\n) as pool:\r\n # \u63d0\u4ea4\u4efb\u52a1\u5e76\u6dfb\u52a0\u56de\u8c03\r\n future = pool.submit(compute_task, 10)\r\n future.add_done_callback(lambda result: print(f\"\u8ba1\u7b97\u5b8c\u6210: {result}\"))\r\n \r\n # \u7b49\u5f85\u7ed3\u679c\r\n print(future.result()) # \u8f93\u51fa: 100\r\n```\r\n\r\n## \ud83c\udfae \u6f14\u793a\u7a0b\u5e8f\r\n\r\n### \ud83c\udd9a GUI \u5bf9\u6bd4\u6f14\u793a\r\n\u8fd0\u884c\u5bf9\u6bd4\u7a0b\u5e8f\uff0c\u4f53\u9a8c\u4e0d\u540c\u5b9e\u73b0\u65b9\u5f0f\u7684\u5dee\u5f02\uff1a\r\n\r\n```bash\r\n# \u5bf9\u6bd4\u6f14\u793a\r\npython examples/gui_demo_comparison.py\r\n```\r\n\r\n\u6f14\u793a\u5185\u5bb9\uff1a\r\n- \u4f20\u7edf\u505a\u6cd5\uff1a\u754c\u9762\u963b\u585e\u6f14\u793a\r\n- QThreadWithReturn\uff1a\u54cd\u5e94\u5f0f\u754c\u9762\u6f14\u793a\r\n- \u7ebf\u7a0b\u6c60\uff1a\u5e76\u884c\u4efb\u52a1\u5904\u7406\u6f14\u793a\r\n\r\n### \ud83d\udcf1 \u5b8c\u6574\u529f\u80fd\u6f14\u793a\r\n```bash\r\n# \u5b8c\u6574\u6f14\u793a\u7a0b\u5e8f\r\npython -m demo.thread_demo_gui\r\n```\r\n\r\n### \ud83d\udcbb \u547d\u4ee4\u884c\u793a\u4f8b\r\n```bash\r\n# \u57fa\u672c\u7528\u6cd5\u793a\u4f8b\r\npython examples/basic_usage.py\r\n```\r\n\r\n## \ud83c\udfaf \u5e94\u7528\u573a\u666f\r\n\r\nQThreadWithReturn \u9002\u5408\u4ee5\u4e0b GUI \u5e94\u7528\u573a\u666f\uff1a\r\n\r\n### \ud83d\udcca \u6570\u636e\u5904\u7406\u5e94\u7528\r\n```python\r\n# \u6570\u636e\u5206\u6790\u3001\u6587\u4ef6\u5904\u7406\r\nthread = QThreadWithReturn(pandas.read_csv, \"large_file.csv\")\r\nthread.add_done_callback(lambda df: self.update_table_view(df))\r\n```\r\n\r\n### \ud83c\udf10 \u7f51\u7edc\u5e94\u7528\r\n```python \r\n# HTTP\u8bf7\u6c42\u3001API\u8c03\u7528\r\nthread = QThreadWithReturn(requests.get, \"https://api.example.com/data\")\r\nthread.add_done_callback(lambda resp: self.display_data(resp.json()))\r\n```\r\n\r\n### \ud83c\udfa8 \u56fe\u50cf\u5904\u7406\u5de5\u5177\r\n```python\r\n# \u56fe\u50cf\u5904\u7406\u3001\u6279\u91cf\u8f6c\u6362\r\nwith QThreadPoolExecutor(max_workers=4) as pool:\r\n futures = [pool.submit(process_image, img) for img in images]\r\n for future in pool.as_completed(futures):\r\n self.update_progress()\r\n```\r\n\r\n### \ud83d\udcc1 \u6587\u4ef6\u7ba1\u7406\u5668\r\n```python\r\n# \u6587\u4ef6\u64cd\u4f5c\u3001\u6279\u91cf\u5904\u7406\r\nthread = QThreadWithReturn(shutil.copy2, source, destination) \r\nthread.add_done_callback(lambda: self.refresh_file_list())\r\n```\r\n\r\n### \ud83e\udd16 \u673a\u5668\u5b66\u4e60\u5de5\u5177\r\n```python\r\n# \u6a21\u578b\u63a8\u7406\u3001\u6570\u636e\u5904\u7406\r\nthread = QThreadWithReturn(model.predict, input_data)\r\nthread.add_done_callback(lambda result: self.show_predictions(result))\r\n```\r\n\r\n## \ud83d\udd27 \u517c\u5bb9\u6027\r\n\r\n- **Python**: 3.10+\r\n- **Qt \u7248\u672c**: PySide6 6.4+ \r\n- **\u64cd\u4f5c\u7cfb\u7edf**: Windows, macOS, Linux\r\n- **\u8fd0\u884c\u73af\u5883**: \r\n - \u6709 Qt \u5e94\u7528\u73af\u5883\uff1a\u4f7f\u7528 Qt \u4fe1\u53f7\u673a\u5236\r\n - \u65e0 Qt \u5e94\u7528\u73af\u5883\uff1a\u81ea\u52a8\u5207\u6362\u5230\u6807\u51c6\u7ebf\u7a0b\u6a21\u5f0f\r\n - \u5df2\u5728 Python 3.10\u30013.11\u30013.13 \u4e2d\u6d4b\u8bd5\r\n\r\n## \ud83e\uddea \u6d4b\u8bd5\r\n\r\n\u9879\u76ee\u5305\u542b 73 \u4e2a\u6d4b\u8bd5\u7528\u4f8b\uff1a\r\n\r\n```bash\r\n# \u8fd0\u884c\u6240\u6709\u6d4b\u8bd5\r\npytest tests/\r\n\r\n# \u8fd0\u884c\u7279\u5b9a\u6d4b\u8bd5\r\npytest tests/test_thread_utils.py -v\r\n\r\n# \u751f\u6210\u8986\u76d6\u7387\u62a5\u544a\r\npytest tests/ --cov=qthreadwithreturn\r\n```\r\n\r\n## \ud83d\udcd6 API \u53c2\u8003\r\n\r\n### QThreadWithReturn\r\n\r\n| \u65b9\u6cd5 | \u63cf\u8ff0 |\r\n|------|------|\r\n| `start(timeout_ms=-1)` | \u542f\u52a8\u7ebf\u7a0b\uff0c\u53ef\u9009\u8d85\u65f6\u8bbe\u7f6e |\r\n| `result(timeout=None)` | \u83b7\u53d6\u6267\u884c\u7ed3\u679c\uff0c\u963b\u585e\u7b49\u5f85 |\r\n| `exception(timeout=None)` | \u83b7\u53d6\u5f02\u5e38\u4fe1\u606f |\r\n| `cancel(force_stop=False)` | \u53d6\u6d88\u7ebf\u7a0b\u6267\u884c |\r\n| `running()` | \u68c0\u67e5\u662f\u5426\u6b63\u5728\u8fd0\u884c |\r\n| `done()` | \u68c0\u67e5\u662f\u5426\u5df2\u5b8c\u6210 |\r\n| `cancelled()` | \u68c0\u67e5\u662f\u5426\u5df2\u53d6\u6d88 |\r\n| `add_done_callback(callback)` | \u6dfb\u52a0\u6210\u529f\u5b8c\u6210\u56de\u8c03 |\r\n| `add_failure_callback(callback)` | \u6dfb\u52a0\u5931\u8d25\u56de\u8c03 |\r\n\r\n### QThreadPoolExecutor \r\n\r\n| \u65b9\u6cd5 | \u63cf\u8ff0 |\r\n|------|------|\r\n| `submit(fn, *args, **kwargs)` | \u63d0\u4ea4\u4efb\u52a1\u5230\u7ebf\u7a0b\u6c60 |\r\n| `shutdown(wait=True, cancel_futures=False, force_stop=False)` | \u5173\u95ed\u7ebf\u7a0b\u6c60 |\r\n| `as_completed(futures, timeout=None)` | \u6309\u5b8c\u6210\u987a\u5e8f\u8fed\u4ee3 Future \u5bf9\u8c61 |\r\n\r\n## \ud83d\udca1 \u6700\u4f73\u5b9e\u8df5\r\n\r\n### \u2705 \u63a8\u8350\u505a\u6cd5\r\n\r\n```python\r\n# 1. \u4f7f\u7528\u4e0a\u4e0b\u6587\u7ba1\u7406\u5668\u81ea\u52a8\u8d44\u6e90\u6e05\u7406\r\nwith QThreadPoolExecutor(max_workers=4) as pool:\r\n futures = [pool.submit(task, i) for i in range(10)]\r\n results = [f.result() for f in futures]\r\n\r\n# 2. \u5728\u56de\u8c03\u4e2d\u66f4\u65b0 UI\uff08\u56de\u8c03\u5728\u4e3b\u7ebf\u7a0b\u6267\u884c\uff09\r\ndef update_progress(result):\r\n progress_bar.setValue(result)\r\n \r\nthread.add_done_callback(update_progress)\r\n\r\n# 3. \u5408\u7406\u8bbe\u7f6e\u8d85\u65f6\u65f6\u95f4\r\nthread.start(timeout_ms=30000) # 30\u79d2\u8d85\u65f6\r\n\r\n# 4. \u5f02\u5e38\u5904\u7406\r\ntry:\r\n result = thread.result()\r\nexcept Exception as e:\r\n logger.error(f\"\u4efb\u52a1\u5931\u8d25: {e}\")\r\n```\r\n\r\n### \u26a0\ufe0f \u6ce8\u610f\u4e8b\u9879\r\n\r\n```python\r\n# \u907f\u514d\u5728\u5de5\u4f5c\u7ebf\u7a0b\u4e2d\u76f4\u63a5\u66f4\u65b0 UI\r\ndef bad_worker():\r\n label.setText(\"\u66f4\u65b0\") # \u9519\u8bef\uff1a\u8de8\u7ebf\u7a0bUI\u66f4\u65b0\r\n\r\n# \u8bb0\u5f97\u8d44\u6e90\u6e05\u7406 \r\npool = QThreadPoolExecutor()\r\n# \u4f7f\u7528\u5b8c\u540e\u8bb0\u5f97\u8c03\u7528 shutdown()\r\n\r\n# \u8c28\u614e\u4f7f\u7528\u5f3a\u5236\u7ec8\u6b62\r\nthread.cancel(force_stop=True) # \u53ef\u80fd\u5bfc\u81f4\u8d44\u6e90\u6cc4\u6f0f\r\n```\r\n\r\n## \ud83e\udd1d \u8d21\u732e\r\n\r\n\u6b22\u8fce\u8d21\u732e\u4ee3\u7801\uff0c\u8bf7\u9075\u5faa\u4ee5\u4e0b\u6b65\u9aa4\uff1a\r\n\r\n1. Fork \u672c\u4ed3\u5e93\r\n2. \u521b\u5efa\u7279\u6027\u5206\u652f (`git checkout -b feature/new-feature`)\r\n3. \u63d0\u4ea4\u66f4\u6539 (`git commit -m 'Add new feature'`)\r\n4. \u63a8\u9001\u5230\u5206\u652f (`git push origin feature/new-feature`)\r\n5. \u5f00\u542f Pull Request\r\n\r\n### \ud83d\udee0\ufe0f \u5f00\u53d1\u73af\u5883\u8bbe\u7f6e\r\n\r\n```bash\r\n# \u514b\u9686\u4ed3\u5e93\r\ngit clone https://github.com/271374667/QThreadWithReturn.git\r\ncd QThreadWithReturn\r\n\r\n# \u4f7f\u7528 uv \u5b89\u88c5\u4f9d\u8d56\r\nuv sync\r\n\r\n# \u8fd0\u884c\u6d4b\u8bd5\r\nuv run pytest\r\n\r\n# \u8fd0\u884c\u6f14\u793a\r\nuv run python -m demo.thread_demo_gui\r\n```\r\n\r\n## \ud83d\udcc4 \u8bb8\u53ef\u8bc1\r\n\r\n\u672c\u9879\u76ee\u4f7f\u7528 MIT \u8bb8\u53ef\u8bc1 - \u67e5\u770b [LICENSE](LICENSE) \u6587\u4ef6\u4e86\u89e3\u8be6\u60c5\u3002\r\n\r\n## \ud83d\udcde \u652f\u6301\r\n\r\n- **\u95ee\u9898\u62a5\u544a**: [GitHub Issues](https://github.com/271374667/QThreadWithReturn/issues)\r\n- **\u8ba8\u8bba**: [GitHub Discussions](https://github.com/271374667/QThreadWithReturn/discussions)\r\n- **\u90ae\u4ef6**: 271374667@qq.com\r\n",
"bugtrack_url": null,
"license": null,
"summary": "PySide6 \u9ad8\u7ea7\u7ebf\u7a0b\u5de5\u5177\u5e93 - \u5e26\u8fd4\u56de\u503c\u7684\u7ebf\u7a0b\u7c7b\u548c\u7ebf\u7a0b\u6c60\u6267\u884c\u5668",
"version": "1.1.1",
"project_urls": {
"Homepage": "https://github.com/271374667/QThreadWithReturn",
"Issues": "https://github.com/271374667/QThreadWithReturn/issues",
"Repository": "https://github.com/271374667/QThreadWithReturn.git"
},
"split_keywords": [
"qt",
" pyside6",
" threading",
" concurrent",
" futures",
" gui",
" pyqt",
" async",
" thread-pool",
" worker-thread"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "fde8fd123004c7d454c7dc04a89dcaebdfe40ea97441587e1efeecf943598a1f",
"md5": "bebe71e839d03685d6fe0247434634e6",
"sha256": "5b7f8a237ee4349105df8958a7e48c3ced678c6316f393a4d133bf67e11247c3"
},
"downloads": -1,
"filename": "qthreadwithreturn-1.1.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "bebe71e839d03685d6fe0247434634e6",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.10",
"size": 18218,
"upload_time": "2025-08-12T08:43:03",
"upload_time_iso_8601": "2025-08-12T08:43:03.174268Z",
"url": "https://files.pythonhosted.org/packages/fd/e8/fd123004c7d454c7dc04a89dcaebdfe40ea97441587e1efeecf943598a1f/qthreadwithreturn-1.1.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "0044ff0eb48bb740e32fdfda991abea78070ccc3678833c36e72e7233896f3eb",
"md5": "276761a884105623f5815b1801ad3b74",
"sha256": "fe8a43c553bade91f6d09fcdcd52165fd9c14b79b8e74b3f289fb212f8bcf9bd"
},
"downloads": -1,
"filename": "qthreadwithreturn-1.1.1.tar.gz",
"has_sig": false,
"md5_digest": "276761a884105623f5815b1801ad3b74",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.10",
"size": 41575,
"upload_time": "2025-08-12T08:43:04",
"upload_time_iso_8601": "2025-08-12T08:43:04.399287Z",
"url": "https://files.pythonhosted.org/packages/00/44/ff0eb48bb740e32fdfda991abea78070ccc3678833c36e72e7233896f3eb/qthreadwithreturn-1.1.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-08-12 08:43:04",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "271374667",
"github_project": "QThreadWithReturn",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"lcname": "qthreadwithreturn"
}