needle-fixer 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- needle_fixer-0.1.0/LICENSE +21 -0
- needle_fixer-0.1.0/PKG-INFO +400 -0
- needle_fixer-0.1.0/README.md +358 -0
- needle_fixer-0.1.0/needle/__init__.py +39 -0
- needle_fixer-0.1.0/needle/builtin/__init__.py +41 -0
- needle_fixer-0.1.0/needle/builtin/backends.py +122 -0
- needle_fixer-0.1.0/needle/builtin/kimi_client.py +312 -0
- needle_fixer-0.1.0/needle/builtin/locator_solutions.py +206 -0
- needle_fixer-0.1.0/needle/builtin/timeout_solution.py +49 -0
- needle_fixer-0.1.0/needle/core/__init__.py +24 -0
- needle_fixer-0.1.0/needle/core/context.py +265 -0
- needle_fixer-0.1.0/needle/core/decorator.py +71 -0
- needle_fixer-0.1.0/needle/core/exceptions.py +50 -0
- needle_fixer-0.1.0/needle/core/handler.py +80 -0
- needle_fixer-0.1.0/needle/core/registry.py +126 -0
- needle_fixer-0.1.0/needle/core/solution.py +60 -0
- needle_fixer-0.1.0/needle_fixer.egg-info/PKG-INFO +400 -0
- needle_fixer-0.1.0/needle_fixer.egg-info/SOURCES.txt +27 -0
- needle_fixer-0.1.0/needle_fixer.egg-info/dependency_links.txt +1 -0
- needle_fixer-0.1.0/needle_fixer.egg-info/requires.txt +21 -0
- needle_fixer-0.1.0/needle_fixer.egg-info/top_level.txt +1 -0
- needle_fixer-0.1.0/pyproject.toml +47 -0
- needle_fixer-0.1.0/setup.cfg +4 -0
- needle_fixer-0.1.0/tests/test_custom_solution.py +53 -0
- needle_fixer-0.1.0/tests/test_handler.py +209 -0
- needle_fixer-0.1.0/tests/test_kimi_client.py +205 -0
- needle_fixer-0.1.0/tests/test_locator_solutions.py +270 -0
- needle_fixer-0.1.0/tests/test_recovery.py +89 -0
- needle_fixer-0.1.0/tests/test_registry.py +82 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 needle
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: needle-fixer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 自动化测试异常自愈库:handle_exception → 责任链查找 NeedleSolution 自动修复
|
|
5
|
+
Author: laowooooo
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/alucard1123/needle
|
|
8
|
+
Project-URL: Repository, https://github.com/alucard1123/needle
|
|
9
|
+
Project-URL: Issues, https://github.com/alucard1123/needle/issues
|
|
10
|
+
Keywords: testing,playwright,exception,self-healing,recovery,automation
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: image
|
|
26
|
+
Requires-Dist: opencv-python; extra == "image"
|
|
27
|
+
Requires-Dist: numpy; extra == "image"
|
|
28
|
+
Requires-Dist: Pillow; extra == "image"
|
|
29
|
+
Provides-Extra: ai
|
|
30
|
+
Requires-Dist: httpx>=0.25; extra == "ai"
|
|
31
|
+
Provides-Extra: playwright
|
|
32
|
+
Requires-Dist: playwright>=1.40; extra == "playwright"
|
|
33
|
+
Provides-Extra: all
|
|
34
|
+
Requires-Dist: opencv-python; extra == "all"
|
|
35
|
+
Requires-Dist: numpy; extra == "all"
|
|
36
|
+
Requires-Dist: Pillow; extra == "all"
|
|
37
|
+
Requires-Dist: httpx>=0.25; extra == "all"
|
|
38
|
+
Requires-Dist: playwright>=1.40; extra == "all"
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# needle
|
|
44
|
+
|
|
45
|
+
[](https://pypi.org/project/needle/)
|
|
46
|
+
[](https://pypi.org/project/needle/)
|
|
47
|
+
[](https://opensource.org/licenses/MIT)
|
|
48
|
+
[](./tests)
|
|
49
|
+
|
|
50
|
+
> 自动化测试异常自愈(Self-Healing)库。
|
|
51
|
+
>
|
|
52
|
+
> 当 Playwright / Selenium / 任意自动化脚本抛出异常时,`needle` 会按「责任链」模式自动寻找并执行可用的修复策略,把原本会失败的用例救回来。
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from needle import ExceptionHandler
|
|
56
|
+
from needle.builtin import register_builtins
|
|
57
|
+
|
|
58
|
+
register_builtins() # 注册内置修复策略(可选)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
page.get_by_role("button", name="SEARCH").click(timeout=5000)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
ExceptionHandler(page=page).handle_exception(e)
|
|
64
|
+
# 修复成功:正常返回;全部失败:抛出 RepairFailedException
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 目录
|
|
70
|
+
|
|
71
|
+
- [needle](#needle)
|
|
72
|
+
- [目录](#目录)
|
|
73
|
+
- [特性](#特性)
|
|
74
|
+
- [安装](#安装)
|
|
75
|
+
- [快速开始](#快速开始)
|
|
76
|
+
- [1. 模块级便捷函数](#1-模块级便捷函数)
|
|
77
|
+
- [2. 类级入口](#2-类级入口)
|
|
78
|
+
- [3. 装饰器模式](#3-装饰器模式)
|
|
79
|
+
- [核心概念](#核心概念)
|
|
80
|
+
- [内置修复策略](#内置修复策略)
|
|
81
|
+
- [自定义修复策略](#自定义修复策略)
|
|
82
|
+
- [注入自定义后端](#注入自定义后端)
|
|
83
|
+
- [默认后端的实例化](#默认后端的实例化)
|
|
84
|
+
- [自定义 AI 客户端(ByPromptSolution 必须)](#自定义-ai-客户端bypromptsolution-必须)
|
|
85
|
+
- [内置 Kimi / Moonshot 客户端](#内置-kimi--moonshot-客户端)
|
|
86
|
+
- [装饰器模式](#装饰器模式)
|
|
87
|
+
- [开发与测试](#开发与测试)
|
|
88
|
+
- [许可证](#许可证)
|
|
89
|
+
- [关于作者](#关于作者)
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 特性
|
|
94
|
+
|
|
95
|
+
- **核心零依赖**:`handler` / `registry` / 基类 / 异常 / 装饰器 仅使用 Python 标准库。
|
|
96
|
+
- **责任链调度**:按 `PRIORITY` 自动排序,逐个尝试 `can_fix → fix`,单个策略失败不会打断后续策略。
|
|
97
|
+
- **Playwright 原生友好**:自动解析 `Locator` 超时异常,提取原始 locator、操作名与调用参数,修复后可保持参数一致重试。
|
|
98
|
+
- **多种内置策略**:缓存、图像模板匹配、AI 提示词、环境型超时处理,按需安装依赖。
|
|
99
|
+
- **可插拔后端**:重依赖(OpenCV、LLM 客户端)通过协议注入,可替换为项目自有实现。
|
|
100
|
+
- **易于扩展**:继承 `NeedleSolution` 实现 `can_fix` / `fix`,注册即用。
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 安装
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# 仅安装核心(零依赖)
|
|
108
|
+
pip install needle-fixer
|
|
109
|
+
|
|
110
|
+
# 图像策略(OpenCV 模板匹配)
|
|
111
|
+
pip install "needle-fixer[image]"
|
|
112
|
+
|
|
113
|
+
# AI 策略(内置 Kimi / Moonshot 客户端)
|
|
114
|
+
pip install "needle-fixer[ai]"
|
|
115
|
+
|
|
116
|
+
# Playwright 运行时(如项目尚未安装)
|
|
117
|
+
pip install "needle-fixer[playwright]"
|
|
118
|
+
|
|
119
|
+
# 全部可选依赖
|
|
120
|
+
pip install "needle-fixer[all]"
|
|
121
|
+
|
|
122
|
+
# 开发依赖
|
|
123
|
+
pip install "needle-fixer[dev]"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
> **注意**:缓存策略基于 JSON 文件,零依赖,已包含在核心包中。
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 快速开始
|
|
131
|
+
|
|
132
|
+
### 1. 模块级便捷函数
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from needle import handle_exception
|
|
136
|
+
from needle.builtin import register_builtins
|
|
137
|
+
|
|
138
|
+
register_builtins()
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
page.get_by_role("button", name="SEARCH").click(timeout=5000)
|
|
142
|
+
except Exception as e:
|
|
143
|
+
handle_exception(e, page=page)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 2. 类级入口
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from needle import ExceptionHandler
|
|
150
|
+
from needle.builtin import register_builtins
|
|
151
|
+
|
|
152
|
+
register_builtins()
|
|
153
|
+
|
|
154
|
+
handler = ExceptionHandler(page=page)
|
|
155
|
+
try:
|
|
156
|
+
page.get_by_label("Name").fill("needle")
|
|
157
|
+
except Exception as e:
|
|
158
|
+
handler.handle_exception(e)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 3. 装饰器模式
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from needle import with_recovery
|
|
165
|
+
|
|
166
|
+
@with_recovery(
|
|
167
|
+
context_extractor=lambda page, locator, **kw: {
|
|
168
|
+
"page": page,
|
|
169
|
+
"locator": locator,
|
|
170
|
+
**kw,
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
def click_element(page, locator):
|
|
174
|
+
page.locator(locator).click()
|
|
175
|
+
|
|
176
|
+
click_element(page, "#submit")
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 核心概念
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
ExceptionHandler.handle_exception(e)
|
|
185
|
+
└─ ContextBuilder.build(e) # 异常 → 修复上下文 dict(可替换)
|
|
186
|
+
└─ RepairFailedException.attempt_recovery()
|
|
187
|
+
└─ SolutionRegistry.create_chain(ctx) # 按 PRIORITY 组装责任链
|
|
188
|
+
└─ NeedleSolution.handle(ctx) # 逐个 can_fix → fix
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
| 组件 | 职责 |
|
|
192
|
+
|------|------|
|
|
193
|
+
| `ExceptionHandler` | 统一入口,负责构建上下文并触发修复。 |
|
|
194
|
+
| `ContextBuilder` | 把原始异常翻译成修复策略可消费的 `dict`。内置 `PlaywrightContextBuilder` 与 `DefaultContextBuilder`。 |
|
|
195
|
+
| `SolutionRegistry` | 策略注册中心,维护优先级排序,负责建链。 |
|
|
196
|
+
| `NeedleSolution` | 修复策略基类,子类实现 `can_fix` / `fix`。 |
|
|
197
|
+
| `RepairFailedException` | 修复失败时抛出,通过 `__cause__` 保留原始异常。 |
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## 内置修复策略
|
|
202
|
+
|
|
203
|
+
| 策略 | 优先级 | 说明 | 依赖 |
|
|
204
|
+
|------|--------|------|------|
|
|
205
|
+
| `ByCacheSolution` | 10 | 从 JSON 缓存读取备选 locator 逐个尝试。 | 无 |
|
|
206
|
+
| `ByImageSolution` | 20 | OpenCV 模板匹配,按坐标定位元素。 | `opencv-python`, `numpy`, `Pillow` |
|
|
207
|
+
| `ByPromptSolution` | 30 | 调用 AI 分析 DOM,给出新 locator。 | `httpx` |
|
|
208
|
+
| `TimeoutSolution` | 50 | 处理环境型超时(如页面出现 waiting 提示)。 | 无 |
|
|
209
|
+
|
|
210
|
+
使用全部内置策略:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from needle.builtin import register_builtins
|
|
214
|
+
|
|
215
|
+
register_builtins()
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
或单独注册:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from needle.core.registry import SolutionRegistry
|
|
222
|
+
from needle.builtin import ByCacheSolution, ByImageSolution
|
|
223
|
+
|
|
224
|
+
SolutionRegistry.register(ByCacheSolution)
|
|
225
|
+
SolutionRegistry.register(ByImageSolution)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## 自定义修复策略
|
|
231
|
+
|
|
232
|
+
只需继承 `NeedleSolution` 并实现 `can_fix` / `fix`,然后注册:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from needle import NeedleSolution, register_solution
|
|
236
|
+
|
|
237
|
+
@register_solution
|
|
238
|
+
class MyRetrySolution(NeedleSolution):
|
|
239
|
+
PRIORITY = 5 # 数字越小越先尝试
|
|
240
|
+
|
|
241
|
+
def can_fix(self) -> bool:
|
|
242
|
+
return "locator" in self.context
|
|
243
|
+
|
|
244
|
+
def fix(self) -> bool:
|
|
245
|
+
page, locator = self.context["page"], self.context["locator"]
|
|
246
|
+
page.locator(locator).click()
|
|
247
|
+
self.context["fixed_element"] = locator # 可选:记录修复结果
|
|
248
|
+
return True
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 注入自定义后端
|
|
254
|
+
|
|
255
|
+
内置策略的重依赖通过协议注入,可用宿主项目自己的实现替换默认实现:
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
from needle.builtin import ByCacheSolution, ByImageSolution, ByPromptSolution
|
|
259
|
+
|
|
260
|
+
ByCacheSolution.configure(my_cache_backend) # CacheBackend
|
|
261
|
+
ByImageSolution.configure(my_image_matcher) # ImageMatcher
|
|
262
|
+
ByPromptSolution.configure(my_ai_client) # AIClient
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### 默认后端的实例化
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
from needle.builtin import (
|
|
269
|
+
ByCacheSolution,
|
|
270
|
+
ByImageSolution,
|
|
271
|
+
ByPromptSolution,
|
|
272
|
+
PickleDBCacheBackend,
|
|
273
|
+
OpenCVImageMatcher,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# 1. 缓存后端:JSON 文件,key 为原 locator,value 为备选 locator 列表
|
|
277
|
+
my_cache_backend = PickleDBCacheBackend(db_path="needle_locator_cache.db")
|
|
278
|
+
ByCacheSolution.configure(my_cache_backend)
|
|
279
|
+
|
|
280
|
+
# 2. 图像匹配后端:OpenCV 模板匹配
|
|
281
|
+
my_image_matcher = OpenCVImageMatcher(
|
|
282
|
+
image_dir="image_locator", # 模板图片目录
|
|
283
|
+
threshold=0.8, # 匹配阈值,范围 0~1
|
|
284
|
+
)
|
|
285
|
+
ByImageSolution.configure(my_image_matcher)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### 自定义 AI 客户端(ByPromptSolution 必须)
|
|
289
|
+
|
|
290
|
+
`needle` 没有内置通用 LLM 客户端,需要你自己实现 `AIClient` 协议:
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
from typing import Optional
|
|
294
|
+
|
|
295
|
+
class MyAIClient:
|
|
296
|
+
def analyze(self, html: str, description: str) -> Optional[str]:
|
|
297
|
+
"""根据页面 HTML 和描述返回一个可用的 Playwright locator 表达式。
|
|
298
|
+
|
|
299
|
+
返回 None 表示无法给出建议;返回的字符串会被写入 context["fixed_element"]。
|
|
300
|
+
"""
|
|
301
|
+
prompt = (
|
|
302
|
+
f"根据以下页面 HTML,给出一个能定位到「{description}」的 "
|
|
303
|
+
f"Playwright locator 表达式。只返回表达式本身,不要任何解释。\n\n"
|
|
304
|
+
f"{html[:8000]}"
|
|
305
|
+
)
|
|
306
|
+
# 这里接入你实际使用的 LLM(OpenAI / Kimi / Claude / 自部署模型等)
|
|
307
|
+
# response = call_your_llm(prompt)
|
|
308
|
+
# return response.strip()
|
|
309
|
+
raise NotImplementedError("请接入实际的 LLM API")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
my_ai_client = MyAIClient(api_key="sk-xxx")
|
|
313
|
+
ByPromptSolution.configure(my_ai_client)
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### 内置 Kimi / Moonshot 客户端
|
|
317
|
+
|
|
318
|
+
`needle` 内置了一个基于 Moonshot AI(Kimi)的 `AIClient` 实现,安装 `ai` 依赖后即可使用:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
pip install "needle[ai]"
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
from needle.builtin import ByPromptSolution, KimiClient
|
|
326
|
+
|
|
327
|
+
my_ai_client = KimiClient(
|
|
328
|
+
api_key="sk-xxxxxxxxxxxxxxxx", # Moonshot API Key
|
|
329
|
+
model="moonshot-v1-8k", # 可选:moonshot-v1-32k / moonshot-v1-128k
|
|
330
|
+
base_url="https://api.moonshot.cn/v1",
|
|
331
|
+
temperature=0.1,
|
|
332
|
+
max_tokens=512,
|
|
333
|
+
)
|
|
334
|
+
ByPromptSolution.configure(my_ai_client)
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
> **注意**:`ByPromptSolution` 在未配置 `AIClient` 时会直接跳过,因此如果只使用缓存/图像策略,可以不实现 AI 客户端。
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## 装饰器模式
|
|
342
|
+
|
|
343
|
+
`with_recovery` 可以把自动修复能力注入到任意函数:
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
from needle import with_recovery
|
|
347
|
+
|
|
348
|
+
@with_recovery(
|
|
349
|
+
context_extractor=lambda page, locator, **kw: {
|
|
350
|
+
"page": page,
|
|
351
|
+
"locator": locator,
|
|
352
|
+
**kw,
|
|
353
|
+
}
|
|
354
|
+
)
|
|
355
|
+
def click_element(page, locator):
|
|
356
|
+
page.locator(locator).click()
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
- `context_extractor`:从被装饰函数参数中提取修复上下文,返回 `dict`。
|
|
360
|
+
- `reraise_on_failure`:修复失败时是否重新抛出 `RepairFailedException`(默认 `True`)。
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## 开发与测试
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
# 克隆仓库
|
|
368
|
+
git clone https://github.com/alucard1123/needle
|
|
369
|
+
cd needle
|
|
370
|
+
|
|
371
|
+
# 创建虚拟环境并安装开发依赖
|
|
372
|
+
python -m venv .venv
|
|
373
|
+
source .venv/bin/activate
|
|
374
|
+
pip install -e ".[dev]"
|
|
375
|
+
|
|
376
|
+
# 运行测试
|
|
377
|
+
pytest
|
|
378
|
+
|
|
379
|
+
# 运行测试并生成覆盖率报告
|
|
380
|
+
pytest --cov=needle --cov-report=term-missing
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## 许可证
|
|
386
|
+
|
|
387
|
+
[MIT](./LICENSE)
|
|
388
|
+
|
|
389
|
+
## 关于作者
|
|
390
|
+
公众号: 中年老吴
|
|
391
|
+
|
|
392
|
+
<img src="image.png" width="300" height="300" alt="公众号二维码">
|
|
393
|
+
|
|
394
|
+
赏老吴杯咖啡:
|
|
395
|
+
|
|
396
|
+
<img src="IMG_0550.JPG" width="300" height="400" alt="赏老吴杯咖啡">
|
|
397
|
+
|
|
398
|
+
找老吴私聊:
|
|
399
|
+
|
|
400
|
+
<img src="IMG_0549.JPG" width="300" height="400" alt="找老吴私聊">
|