# https://github.com/beanbaginc/kgb 项目说明书

生成时间：2026-06-14 16:17:06 UTC

## 目录

- [kgb 概述与快速入门](#page-1)
- [核心架构：FunctionSpy、签名内省与字节码替换](#page-2)
- [Spy 操作规划、调用跟踪与断言](#page-3)
- [Python 兼容性、框架集成与常见问题](#page-4)

<a id='page-1'></a>

## kgb 概述与快速入门

### 相关页面

相关主题：[核心架构：FunctionSpy、签名内省与字节码替换](#page-2), [Spy 操作规划、调用跟踪与断言](#page-3), [Python 兼容性、框架集成与常见问题](#page-4)

<details>
<summary>相关源码文件</summary>

以下源码文件用于生成本页说明：

- [README.rst](https://github.com/beanbaginc/kgb/blob/main/README.rst)
- [kgb/__init__.py](https://github.com/beanbaginc/kgb/blob/main/kgb/__init__.py)
- [kgb/agency.py](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py)
- [kgb/contextmanagers.py](https://github.com/beanbaginc/kgb/blob/main/kgb/contextmanagers.py)
- [kgb/spies.py](https://github.com/beanbaginc/kgb/blob/main/kgb/spies.py)
- [kgb/pytest_plugin.py](https://github.com/beanbaginc/kgb/blob/main/kgb/pytest_plugin.py)
- [kgb/asserts.py](https://github.com/beanbaginc/kgb/blob/main/kgb/asserts.py)
- [kgb/errors.py](https://github.com/beanbaginc/kgb/blob/main/kgb/errors.py)
- [kgb/signature.py](https://github.com/beanbaginc/kgb/blob/main/kgb/signature.py)
- [NEWS.rst](https://github.com/beanbaginc/kgb/blob/main/NEWS.rst)
- [conftest.py](https://github.com/beanbaginc/kgb/blob/main/conftest.py)
</details>

# kgb 概述与快速入门

## 项目定位与用途

kgb 是一个用于 Python 单元测试的"间谍"（Spy）库，主要目的是在不修改被测代码的前提下，记录、替换或断言函数的调用行为。它既可以作为独立的单元测试辅助库使用，也已经集成到 Review Board、Djblets、Django Evolution 等多个项目中，资料来源：[README.rst:1-58]()。

kgb 的核心价值体现在三个方面：

1. **行为替换**：可以将某个函数临时替换为另一个函数（`call_fake`），或阻止其继续调用原始实现（`call_original=False`）。
2. **调用记录**：自动记录每一次调用的参数、返回值以及异常。
3. **断言能力**：提供丰富的方法验证函数是否被调用、是否以预期参数调用、是否抛出指定异常。

## 安装与版本要求

通过 pip 即可安装：

```bash
pip install kgb
```

根据 [NEWS.rst:1-7]() 的发布说明，kgb 当前发布版本（截至 7.3 alpha）为 `(7, 3, 1, 'alpha', 0, False)`，资料来源：[kgb/__init__.py:30-50]()。版本兼容性演进如下：

| 版本 | 新增兼容的 Python | 重要变更 |
| --- | --- | --- |
| kgb 7.0 | Python 3.10 | 引入 pytest 插件，添加 snake_case 断言方法 |
| kgb 7.1 | Python 3.11 | 完成 3.11 兼容性修复（对应社区 Issue #9） |
| kgb 7.1.1 | — | 打包包含 `LICENSE` 文件（对应 Issue #10） |
| kgb 7.2 | Python 3.13 | 修复 3.13 函数对象变更引发的崩溃（对应 Issue #11） |
| kgb 7.3 | Python 3.14 | 新增 `SpyAgency.get_spy()`，修复生成器函数的 `DeprecationWarning` |

kgb 7.0 起正式放弃对 Python 2.6、3.4、3.5 的支持，资料来源：[NEWS.rst:33-52]()。

## 核心概念与架构

kgb 的内部架构由四个互相协作的组件构成：`SpyAgency`、`FunctionSpy`、`SpyCall` 以及一组 `SpyOp*` 操作符。

```mermaid
flowchart LR
    A[测试代码] --> B[SpyAgency<br/>spy_on / spy_for]
    B --> C[FunctionSpy<br/>替换函数对象]
    C --> D[SpyCall<br/>单次调用记录]
    D --> E{SpyOp* 操作符}
    E -->|SpyOpReturn| F[返回值替换]
    E -->|SpyOpRaise| G[抛出异常]
    E -->|SpyOpMatchAny / InOrder| H[参数匹配]
```

- **SpyAgency**：负责登记、追踪和清理所有间谍。可以在 `unittest.TestCase` 中作为 mixin 混入，也支持直接 `SpyAgency()` 实例化，资料来源：[kgb/agency.py:11-60]()。
- **FunctionSpy**：在函数对象层面替换原始实现，记录每次调用上下文，并支持 `call_original`/`call_fake`/返回值/异常替换，资料来源：[kgb/spies.py:1-50]()。
- **SpyCall**：对应一次具体的调用记录，包含参数、返回值与异常信息，资料来源：[kgb/spies.py:1-30]()。
- **SpyOp 操作符**：通过 `SpyOpReturn`、`SpyOpReturnInOrder`、`SpyOpMatchAny` 等对象控制间谍在多次调用时的不同行为，资料来源：[kgb/__init__.py:15-28]()。

函数签名层面的内部工作委托给 `kgb.signature.BaseFunctionSig`，它会针对不同 Python 版本（CPython、PyPy、不同小版本）生成相应的间谍代码对象，资料来源：[kgb/signature.py:1-60]()。常见错误（如重复间谍）由 `kgb.errors.ExistingSpyError` 与 `IncompatibleFunctionError` 提供友好提示，资料来源：[kgb/errors.py:14-46]()。

## 四种启动间谍的方式

README 中介绍了四种主要的间谍启动方式，资料来源：[README.rst:60-130]()：

### 直接实例化 SpyAgency

```python
from kgb import SpyAgency

def test_mind_control_device():
    mcd = MindControlDevice()
    agency = SpyAgency()
    agency.spy_on(mcd.assassinate, call_fake=give_hugs)
```

### 混入 unittest TestCase

```python
import unittest
from kgb import SpyAgency

class TopSecretTests(SpyAgency, unittest.TestCase):
    def test_doomsday_device(self):
        dd = DoomsdayDevice()
        @self.spy_for(dd.kaboom)
        def _save_world(*args, **kwargs):
            print('Sprinkles and ponies!')
        dd.kaboom()
```

### 使用 pytest fixture

`kgb` 7.0 起提供 pytest 插件，在测试中可直接注入 `spy_agency`，测试结束后自动 `unspy_all()`，资料来源：[kgb/pytest_plugin.py:1-30]()。

```python
def test_doomsday_device(spy_agency):
    @spy_agency.spy_for(dd.kaboom)
    def _save_world(*args, **kwargs):
        ...
```

### 上下文管理器

仅需一个临时间谍时，可以使用 `with spy_on(...)`，离开上下文即自动注销，资料来源：[kgb/contextmanagers.py:1-30]()。

```python
from kgb import spy_on

def test_the_bomb():
    bomb = Bomb()
    with spy_on(bomb.explode, call_original=False):
        bomb.explode()  # 不会爆炸
```

## 快速入门示例

下面给出一个最小可运行示例，覆盖"安装间谍 → 调用 → 断言 → 还原"完整流程：

```python
from kgb import SpyAgency

class SampleTests(SpyAgency):
    def test_calls(self):
        target = TargetClass()

        # 启动间谍：阻止原函数被调用，并提供伪造返回值
        self.spy_on(target.dangerous_call,
                    call_original=False,
                    call_fake=lambda *a, **kw: 'safe')

        # 业务调用
        result = target.dangerous_call(1, mode='test')
        assert result == 'safe'

        # 断言调用次数与参数
        self.assertSpyCalledWith(target.dangerous_call, 1, mode='test')
        self.assertSpyCalledOnceWith(target.dangerous_call, 1, mode='test')

        # tearDown 会自动 unspy_all
```

如果不想继承 `SpyAgency`，可以在 pytest 中通过 `spy_agency.assert_spy_called_with(...)` 完成同样的断言，资料来源：[kgb/pytest_plugin.py:1-25]()；在其它环境下可以使用独立断言函数 `from kgb.asserts import assert_spy_called`，资料来源：[kgb/asserts.py:1-15]()。

## 已知问题与最佳实践

社区中已记录的若干使用陷阱值得注意：

- **重复间谍**：使用 DDT 等参数化测试时，同一函数会被多次设置间谍。重复设置会抛出 `ExistingSpyError`，并附带上一次间谍设置的堆栈，便于定位，资料来源：[kgb/errors.py:14-37]()。
- **Python 版本兼容性**：PyPy 3.11 上 `test_spy_on_generator` 存在已知失败（Issue #13）。3.13 早期 beta 中闭包 + 间谍的组合会崩溃，已在 kgb 7.2 修复（Issue #11）。3.12 移除了 `assertRaisesRegexp`，但 kgb 自身未直接依赖该别名（Issue #12）。
- **异步测试**：可通过 `asynctest.TestCase` 配合 `SpyAgency` mixin 来测试异步函数（Issue #4）。
- **`call_fake` 多返回值**：若希望不同调用返回不同结果，可在 `call_fake` 中维护状态，或改用 `SpyOpReturnInOrder` 实现顺序返回，资料来源：[kgb/__init__.py:20-28]()。

最佳实践：每次测试结束后调用 `agency.unspy_all()`（在 `SpyAgency.tearDown()` 中会自动执行），并在可能的情况下优先使用 pytest 插件或上下文管理器，以减少手动清理负担，资料来源：[kgb/agency.py:40-60]()。

## See Also

- kgb 7.2 发布说明：<https://github.com/beanbaginc/kgb/releases/tag/release-7.2>
- kgb 在 PyPI：<https://pypi.org/project/kgb/>
- 相关 Issue 列表：#4、#5、#7、#8、#9、#10、#11、#12、#13

---

<a id='page-2'></a>

## 核心架构：FunctionSpy、签名内省与字节码替换

### 相关页面

相关主题：[kgb 概述与快速入门](#page-1), [Spy 操作规划、调用跟踪与断言](#page-3), [Python 兼容性、框架集成与常见问题](#page-4)

<details>
<summary>相关源码文件</summary>

以下源码文件用于生成本页说明：

- [kgb/spies.py](https://github.com/beanbaginc/kgb/blob/main/kgb/spies.py)
- [kgb/signature.py](https://github.com/beanbaginc/kgb/blob/main/kgb/signature.py)
- [kgb/calls.py](https://github.com/beanbaginc/kgb/blob/main/kgb/calls.py)
- [kgb/agency.py](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py)
- [kgb/errors.py](https://github.com/beanbaginc/kgb/blob/main/kgb/errors.py)
- [kgb/utils.py](https://github.com/beanbaginc/kgb/blob/main/kgb/utils.py)
</details>

# 核心架构：FunctionSpy、签名内省与字节码替换

## 概述与设计目标

kgb 是一个面向 Python 的函数间谍（function spy）库，其核心能力是「在不改动调用方代码的前提下」拦截任意可调用对象并记录其行为。整体架构围绕三个相互协作的层次展开：[FunctionSpy](https://github.com/beanbaginc/kgb/blob/main/kgb/spies.py) 负责运行时拦截与状态记录，[FunctionSig](https://github.com/beanbaginc/kgb/blob/main/kgb/signature.py) 负责跨 Python 版本的函数签名内省，[SpyCall](https://github.com/beanbaginc/kgb/blob/main/kgb/calls.py) 负责把每一次调用实例化为可断言的对象。最高层则由 [SpyAgency](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py) 统一管理所有间谍的生命周期，包括与 `unittest.TestCase` 的 mixin 集成以及 pytest 插件的 fixture 支持。

这种分层设计的目的，是为了在 Python 2.7 直至 3.14 之间的多版本差异下，提供一套统一而稳定的「拦截—记录—断言」API。资料来源：[kgb/spies.py:1-40](https://github.com/beanbaginc/kgb/blob/main/kgb/spies.py)、[kgb/signature.py:1-20](https://github.com/beanbaginc/kgb/blob/main/kgb/signature.py)

## FunctionSpy：间谍代理

`FunctionSpy` 是 kgb 的核心类，它在内部持有一个「原始函数对象」与一个「替换函数对象」，并通过 `__call__` 暴露与原函数完全一致的调用接口。每次被调用时，spy 都会先记录参数、转发至 `call_fake` 或原函数、再记录返回值与异常，最后把结果回传给调用方。资料来源：[kgb/spies.py:1-60](https://github.com/beanbaginc/kgb/blob/main/kgb/spies.py)

spy 的三种工作模式如下表所示。

| 模式 | 触发参数 | 行为 |
|------|----------|------|
| 透传模式 | 默认 | 调用 `call_original()` 执行原始函数 |
| 阻断模式 | `call_original=False` | 完全不调用原函数，返回 `None` |
| 替代模式 | `call_fake=fn` | 调用 `fn(*args, **kwargs)` 取代原函数 |

在 `calls` 列表中可访问每一次调用的 `SpyCall` 对象，从而查询是否抛出了 `TypeError`、返回值是否匹配等。`spy_on()` 的入口同时支持位置参数与关键字参数，并通过 `assertSpyCalledWith`、`assertSpyLastCalledWith` 等命名风格方法（kgb 7+ 还提供 `assert_spy_called_with` 蛇形命名别名）进行断言。资料来源：[kgb/agency.py:1-80](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py)、[kgb/calls.py:1-50](https://github.com/beanbaginc/kgb/blob/main/kgb/calls.py)

## 签名内省：FunctionSig 层次结构

由于 Python 3.0 起 `inspect.getfullargspec` 取代了 `getargspec`，并且 3.8+ 引入了仅限位置参数（positional-only parameters），kgb 通过 `FunctionSigPy2` / `FunctionSigPy3` 两个子类做版本分流，并在模块底部按 `sys.version_info[0]` 选定具体实现。资料来源：[kgb/signature.py:1-30](https://github.com/beanbaginc/kgb/blob/main/kgb/signature.py)

内省过程需要解决三件事：确定函数的「真正所有者」（区分类方法、绑定方法、静态方法）、解析参数顺序与默认值、以及为生成替换代码对象提供 `format_arg_spec()` 输出。在 Python 3 分支中，[`FunctionSigPy3.format_arg_spec`](https://github.com/beanbaginc/kgb/blob/main/kgb/signature.py) 直接基于 `inspect.Signature` 重建参数列表，并把默认值占位为 `_UNSET_ARG`，从而让生成的转发函数能在调用时动态注入真实参数。资料来源：[kgb/signature.py:60-110](https://github.com/beanbaginc/kgb/blob/main/kgb/signature.py)

辅助函数 [`get_defined_attr_value`](https://github.com/beanbaginc/kgb/blob/main/kgb/utils.py) 用于在类的 `__dict__` 与所有 `__bases__` 中递归查找真实定义，绕过描述符（descriptor）协议的影响——这是判断一个方法究竟属于「绑定方法」还是「未绑定方法」的前提。资料来源：[kgb/utils.py:1-40](https://github.com/beanbaginc/kgb/blob/main/kgb/utils.py)

## 字节码替换机制

kgb 实现 spy 的关键在于「代码对象替换」：它不修改原函数体，而是构造一个与原函数共享 `__globals__`、`__name__`、`__defaults__` 等元数据的新 `code` 对象，然后把这个新 code 装入原函数。这样，调用栈中的所有位置看到的都是「同一个函数对象」，却执行了不同的字节码。资料来源：[kgb/spies.py:100-180](https://github.com/beanbaginc/kgb/blob/main/kgb/spies.py)

```mermaid
flowchart LR
    A[原始函数 func] -->|spy_on| B[FunctionSpy]
    B -->|读取签名| C[FunctionSigPy3]
    C -->|format_arg_spec| D[生成转发函数源码]
    D -->|compile| E[新 code 对象]
    E -->|func.__code__ = new_code| F[拦截生效]
    F -->|调用| G[SpyCall 记录]
    G -->|call_original / call_fake| H[返回值/异常]
```

在 3.11 之前，这一替换策略相对稳定；但 CPython 3.11 引入的「内联生成器 / 协程标志」以及 3.13 改动的函数构造方式，使早期 kgb 出现崩溃。issue #11 报告的闭包崩溃案例，正是因为新版本对 `gi_code`、`co_flags` 的处理更严格；kgb 7.2 之后通过把 `CO_GENERATOR` / `CO_COROUTINE` / `CO_ASYNC_GENERATOR` 标志从旧 code 镜像到新 code 解决了 3.13 的崩溃。资料来源：[kgb/spies.py:120-170](https://github.com/beanbaginc/kgb/blob/main/kgb/spies.py)、[issue #11](https://github.com/beanbaginc/kgb/issues/11)

## 调用追踪与代理管理

`SpyCall` 持有 `spy`、`args`、`kwargs`、`return_value`、`exception` 五个字段，并提供 `called_with()`、`raised()`、`returned()` 等语义化查询。这些方法以「子集匹配」为原则——传入的参数只需是真实调用参数的前缀，从而让断言更宽容。资料来源：[kgb/calls.py:1-60](https://github.com/beanbaginc/kgb/blob/main/kgb/calls.py)

`SpyAgency` 是 spy 的注册中心，其内部 `self.spies` 集合确保同一函数不能被重复 spy，否则抛出 [`ExistingSpyError`](https://github.com/beanbaginc/kgb/blob/main/kgb/errors.py)。当 `SpyAgency` 以 mixin 形式被 `TestCase` 继承时，`tearDown` 会自动调用 `unspy_all()`；pytest 用户则通过 [`spy_agency`](https://github.com/beanbaginc/kgb/blob/main/kgb/pytest_plugin.py) fixture 获得同样的自动清理。资料来源：[kgb/agency.py:1-50](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py)、[kgb/pytest_plugin.py:1-20](https://github.com/beanbaginc/kgb/blob/main/kgb/pytest_plugin.py)

## 已知限制与跨版本兼容性

社区中讨论较多的几个边界场景均与字节码替换的版本敏感性相关：

- **PyPy 3.11 上的生成器 spy**：[issue #13](https://github.com/beanbaginc/kgb/issues/13) 报告 `test_spy_on_generator` 失败，因为 PyPy 的代码对象布局与 CPython 存在差异。kgb 7.3 的 NEWS 中也专门提到修复了 spy 生成器函数时的 `DeprecationWarning`。
- **Python 3.10 未绑定方法**：[issue #8](https://github.com/beanbaginc/kgb/issues/8) 显示 `call_original` 对无实例方法支持不佳，已在 7.1.1 后续版本逐步完善。
- **asynctest 异步用例**：[issue #4](https://github.com/beanbaginc/kgb/issues/4) 中用户通过把 `SpyAgency` 与 `asynctest.TestCase` 多重继承来实现 `async def` 测试，这是 mixin 模式的合理外推。
- **重复 spy**：[issue #5](https://github.com/beanbaginc/kgb/issues/5) 中用户在 DDT 参数化测试里多次执行同一 spy 触发 `ExistingSpyError`，需要在每个测试用例的 `setUp` 中重新初始化 agency。

整体而言，kgb 通过「签名内省 → 字节码构造 → 运行时拦截 → 调用记录」四级流水线，把 Python 跨版本差异封装在 `FunctionSig` 与 `spies.py` 的版本分支中，使上层用户面对的是稳定而一致的 API。资料来源：[kgb/NEWS.rst:1-30](https://github.com/beanbaginc/kgb/blob/main/NEWS.rst)、[kgb/__init__.py:1-40](https://github.com/beanbaginc/kgb/blob/main/kgb/__init__.py)

## See Also

- [kgb README](https://github.com/beanbaginc/kgb/blob/main/README.rst)
- [kgb 7.2 Release Notes](https://github.com/beanbaginc/kgb/releases/tag/release-7.2)
- [issue #11: Python 3.13 闭包崩溃](https://github.com/beanbaginc/kgb/issues/11)
- [issue #13: PyPy3.11 生成器 spy](https://github.com/beanbaginc/kgb/issues/13)

---

<a id='page-3'></a>

## Spy 操作规划、调用跟踪与断言

### 相关页面

相关主题：[kgb 概述与快速入门](#page-1), [核心架构：FunctionSpy、签名内省与字节码替换](#page-2)

<details>
<summary>相关源码文件</summary>

以下源码文件用于生成本页说明：

- [kgb/ops.py](https://github.com/beanbaginc/kgb/blob/main/kgb/ops.py)
- [kgb/calls.py](https://github.com/beanbaginc/kgb/blob/main/kgb/calls.py)
- [kgb/agency.py](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py)
- [kgb/asserts.py](https://github.com/beanbaginc/kgb/blob/main/kgb/asserts.py)
- [kgb/spies.py](https://github.com/beanbaginc/kgb/blob/main/kgb/spies.py)
- [kgb/__init__.py](https://github.com/beanbaginc/kgb/blob/main/kgb/__init__.py)
- [kgb/pytest_plugin.py](https://github.com/beanbaginc/kgb/blob/main/kgb/pytest_plugin.py)
- [kgb/contextmanagers.py](https://github.com/beanbaginc/kgb/blob/main/kgb/contextmanagers.py)
</details>

# Spy 操作规划、调用跟踪与断言

## 概述

`kgb` 的核心价值在于三件事：**拦截函数调用**、**记录调用上下文**、**断言调用行为**。本页聚焦于这三件事背后的内部机制——`BaseSpyOperation`/`SpyOp*` 系列（规划调用应如何被响应）、`SpyCall`（记录每一次调用的实参、关键字、返回值、异常），以及 `SpyAgency` 与 `kgb.asserts`（基于上述记录进行断言）。它们共同构成"spy 三件套"，使测试代码既能控制被测函数的副作用，也能事后验证它确实被以预期方式调用过。

资料来源：[kgb/ops.py:1-12]()、[kgb/calls.py:1-9]()、[kgb/agency.py:1-19]()。

---

## Spy 操作规划（Planned Operations）

`kgb/ops.py` 定义了 `BaseSpyOperation` 基类及其若干内置子类。与简单的 `call_fake=...` 不同，`op=` 关键字支持**按计划分次响应**：可以指定第 N 次调用应返回什么、抛什么异常、或按参数匹配去分支。

### 内置操作类型

| 操作类 | 触发方式 | 典型用途 |
| --- | --- | --- |
| `SpyOpReturn(value)` | 每次被调用时返回固定值 | 固定桩 |
| `SpyOpRaise(exception)` | 每次被调用时抛出固定异常 | 模拟失败 |
| `SpyOpReturnInOrder([...])` | 按列表顺序依次返回 | 模拟状态机 |
| `SpyOpRaiseInOrder([...])` | 按列表顺序依次抛出 | 模拟间歇性故障 |
| `SpyOpMatchAny([...])` | 任意一次参数匹配即触发 | 多分支 |
| `SpyOpMatchInOrder([...])` | 必须按顺序匹配 | 状态序列验证 |

操作通过 `op=...` 关键字参数传递，并可在 `SpyOpMatchAny` / `SpyOpMatchInOrder` 的字典条目中通过 `op` 键进行嵌套。`BaseSpyOperation.handle_call()` 是真正的执行入口；`setup()` 则返回一个会被注入到被 spy 函数的"伪函数"。

```python
spy_on(lockbox.enter_code, op=kgb.SpyOpMatchInOrder([
    {
        'args': (42, 42, 42, 42, 42, 42),
        'op': kgb.SpyOpRaise(Kaboom()),
        'call_original': True,
    },
]))
```

`UnexpectedCallError` 用于在匹配规则耗尽且调用者仍继续调用时立即抛出，从而避免"沉默的多余调用"。

资料来源：[kgb/ops.py:9-50]()、[kgb/__init__.py:5-11]()、[kgb/errors.py]()（`UnexpectedCallError` 引入位置）。

---

## 调用跟踪（SpyCall）

`kgb/calls.py` 定义的 `SpyCall` 是每次拦截到的"调用快照"对象，由 `FunctionSpy` 在转发调用前自动构造并追加到 `self.calls` 列表中。它只暴露四个状态字段：

| 字段 | 类型 | 含义 |
| --- | --- | --- |
| `spy` | `FunctionSpy` | 所属 spy 引用 |
| `args` | `tuple` | 位置参数 |
| `kwargs` | `dict` | 关键字参数 |
| `return_value` | `object` | 本次调用的返回值 |
| `exception` | `BaseException` | 本次调用抛出的异常（如有） |

每个 `SpyCall` 还提供 `called_with`、`returned`、`raised`、`last_called_with` 等查询方法，使**断言既可以针对"整个 spy"也可以针对"某一次具体调用"**：

```python
# 检查是否曾以某参数被调用
spy.called_with('foo', bar='baz')
# 检查最近一次调用
spy.last_called_with('foo', bar='baz')
# 检查指定一次调用
spy.calls[0].returned(True)
```

> 社区实践：issue #3 中用户希望 `call_fake` 每次返回不同值；其解决方案正是通过 `SpyOpReturnInOrder` 或让 `call_fake` 维护内部状态再写入 `spy_call.return_value`，由此 `SpyCall` 的状态字段成为关键载体。

`reset_calls()` 用于清空历史而不卸载 spy，这是 DDT 等参数化测试中复用同一 spy 的关键支持（参见 issue #5：`ExistingSpyError` 的常见成因之一是忘记 reset）。

资料来源：[kgb/calls.py:11-43]()、[kgb/spies.py:1-30]()（`FunctionSpy.__repr__` 中对 `call_count` 的引用）。

---

## 断言机制（Assertions）

断言层分为两类 API：**作为 `SpyAgency` 方法**（适合混入 `unittest.TestCase`）和**作为 `kgb.asserts` 中的独立函数**（适合 pytest 等不依赖 mixin 的场景）。自 kgb 7.0 起，两者均提供 CamelCase 与 snake_case 两套命名。

### 常用断言方法

| 方法（snake_case） | 验证内容 |
| --- | --- |
| `assert_has_spy` | 当前 spy 是否被该 agency 跟踪 |
| `assert_spy_called` / `assert_spy_not_called` | 函数是否（未）被调用过 |
| `assert_spy_call_count(n)` | 调用次数是否等于 `n` |
| `assert_spy_called_with(...)` | 是否存在一次匹配实参的调用 |
| `assert_spy_last_called_with(...)` | 最近一次调用是否匹配 |
| `assert_spy_not_called_with(...)` | 是否**从未**以某参数被调用 |
| `assert_spy_returned(value)` | 是否存在一次返回 `value` 的调用 |
| `assert_spy_last_returned(value)` | 最近一次调用是否返回 `value` |
| `assert_spy_raised(exc)` / `assert_spy_raised_with_message` | 是否抛过指定异常或异常消息 |
| `assert_spy_last_raised(...)` | 最近一次调用是否抛过指定异常 |

所有断言在被违反时会调用 `_kgb_assert_fail()`，该方法会格式化出**完整的调用历史**（参数、关键字、返回值、异常），使失败信息可读性远高于默认 `assertEqual`。

```python
# 独立函数用法（pytest 友好）
from kgb.asserts import assert_spy_called, assert_spy_returned

assert_spy_called(obj.func)
assert_spy_returned(obj.func, expected_value)
```

资料来源：[kgb/agency.py:13-19, 23-100]()、`[kgb/asserts.py]()`（独立函数入口）。

---

## 数据流：拦截 → 记录 → 断言

```mermaid
sequenceDiagram
    participant U as 被测代码
    participant S as FunctionSpy
    participant O as BaseSpyOperation(op=)
    participant C as SpyCall.calls
    participant A as assert_spy_*

    U->>S: 调用被 spy 的函数
    S->>C: 构造 SpyCall(args, kwargs)
    S->>O: handle_call(spy_call, *args, **kwargs)
    alt op 提供返回值
        O-->>S: return value
    else op 抛出异常
        O-->>S: raise exception
    else 未提供 op
        S->>S: call_original()/call_fake()
    end
    S->>C: 写入 return_value / exception
    U-->>A: 测试代码查询历史
    A->>C: 遍历并匹配
    A-->>U: 通过 / 抛出 AssertionError
```

**关键点**：`SpyCall` 是在 `handle_call` **之前**构造的，因此即使操作本身抛异常，`args`/`kwargs` 也已被记录——这正是 `assert_spy_raised` 能稳定工作的前提。

资料来源：[kgb/spies.py:30-50]()（`FunctionSpy.__call__` 调度路径）、[kgb/calls.py:25-33]()（字段初始化时机）。

---

## 常见失败模式

- **重复 spy**：`ExistingSpyError` 提示同一函数已被 spy（issue #5）。在 DDT 等参数化场景中应在每个用例间调用 `spy.unspy()` 或 `agency.unspy_all()`。
- **调用次数不符**：未调用 `reset_calls()` 而 `assert_spy_call_count(1)` 在二次进入时失败。
- **op 耗尽**：`SpyOpReturnInOrder` / `SpyOpMatchInOrder` 在超出计划次数后继续被调用，将抛 `UnexpectedCallError`——这是设计而非 bug。
- **PyCharm 误报 `*args (tuple)`**：issue #7 中 `spy_on` 的 docstring 把 `*args` 标注为 tuple，导致 IDE 类型推断异常；可在调用处显式 `spy_on(target=..., ...)` 规避。

资料来源：[kgb/agency.py:23-30]()（`tearDown` 行为）、[kgb/ops.py]()`UnexpectedCallError`。

---

## See Also

- [安装与快速开始](README.rst) — 入门示例
- [SpyAgency 与 pytest 插件](kgb/pytest_plugin.py) — 自动化清理
- [`spy_on` 上下文管理器](kgb/contextmanagers.py) — 短生命周期 spy
- [版本变更](NEWS.rst) — kgb 7.0（snake_case 断言）、7.2（Python 3.13 修复）、7.3（`get_spy`、Python 3.14）

---

<a id='page-4'></a>

## Python 兼容性、框架集成与常见问题

### 相关页面

相关主题：[kgb 概述与快速入门](#page-1), [核心架构：FunctionSpy、签名内省与字节码替换](#page-2)

<details>
<summary>相关源码文件</summary>

以下源码文件用于生成本页说明：

- [kgb/pytest_plugin.py](https://github.com/beanbaginc/kgb/blob/main/kgb/pytest_plugin.py)
- [kgb/agency.py](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py)
- [kgb/errors.py](https://github.com/beanbaginc/kgb/blob/main/kgb/errors.py)
- [kgb/contextmanagers.py](https://github.com/beanbaginc/kgb/contextmanagers.py)
- [kgb/spies.py](https://github.com/beanbaginc/kgb/blob/main/kgb/spies.py)
- [kgb/signature.py](https://github.com/beanbaginc/kgb/blob/main/kgb/signature.py)
- [kgb/pycompat.py](https://github.com/beanbaginc/kgb/blob/main/kgb/pycompat.py)
- [kgb/asserts.py](https://github.com/beanbaginc/kgb/blob/main/kgb/asserts.py)
- [kgb/utils.py](https://github.com/beanbaginc/kgb/blob/main/kgb/utils.py)
- [kgb/__init__.py](https://github.com/beanbaginc/kgb/blob/main/kgb/__init__.py)
- [conftest.py](https://github.com/beanbaginc/kgb/blob/main/conftest.py)
- [NEWS.rst](https://github.com/beanbaginc/kgb/blob/main/NEWS.rst)
- [README.rst](https://github.com/beanbaginc/kgb/blob/main/README.rst)
</details>

# Python 兼容性、框架集成与常见问题

本页聚焦于 kgb 在不同 Python 解释器与测试框架中的兼容策略、集成方式以及社区中常见的故障与排查思路。kgb 通过版本分支机制和分发插件机制，使其能在多种环境下保持一致的间谍（spy）行为。

## Python 版本兼容策略

kgb 不是一个"一码通吃"的库。它通过运行时检查 `sys.version_info` 来选择不同的实现分支，以应对 Python 解释器在函数对象表示、签名规范和字节码结构上的差异。

### 分支选择机制

在 [kgb/signature.py](https://github.com/beanbaginc/kgb/blob/main/kgb/signature.py) 末尾，kgb 通过版本三元组选择具体的 `FunctionSig` 实现：

- `FunctionSigPy2` 适用于 Python 2；
- `FunctionSigPy3` 适用于 Python 3，并使用 `inspect.Signature` 处理参数；
- 其余版本会抛出 `Exception('Unsupported Python version')`。

这种分支式设计使得 kgb 能在 [Python 2.7 和 3.6 至 3.11](README.rst) 的广泛范围内正常工作。最新版本 [kgb 7.3](NEWS.rst) 又新增了对 Python 3.14 的支持。

### 函数代码对象的版本适配

`FunctionSpy` 在替换原函数时，需要构造一个与原函数签名兼容的新代码对象。在 [kgb/spies.py](https://github.com/beanbaginc/kgb/blob/main/kgb/spies.py) 中可以看到，kgb 对 `temp_code.replace(...)` 的调用会根据 Python 版本传入不同的关键字参数。特别地，对于 3.11+，kgb 会镜像 `co_flags` 上的 `CO_GENERATOR`、`CO_COROUTINE` 和 `CO_ASYNC_GENERATOR` 标志位，避免在替换生成器函数时触发解释器的 `DeprecationWarning`（[kgb 7.3 修复](NEWS.rst)）。

### 与 PyPy 的兼容性

[kgb/spies.py](https://github.com/beanbaginc/kgb/blob/main/kgb/spies.py) 中针对 PyPy 3.11 的生成器函数间谍测试存在已知缺陷（[Issue #13](https://github.com/beanbaginc/kgb/issues/13)）。这是由于 PyPy 的字节码格式与 CPython 不同所导致。如果项目需要同时覆盖 CPython 与 PyPy，建议在使用 `spy_on` 包装生成器函数时增加 CI 矩阵验证。

## 框架集成

kgb 的设计目标是同时支持 unittest、pytest 以及第三方异步测试框架。

### pytest 插件集成

[kgb/pytest_plugin.py](https://github.com/beanbaginc/kgb/blob/main/kgb/pytest_plugin.py) 注册了一个名为 `spy_agency` 的 pytest fixture。它在测试执行前实例化一个 `SpyAgency`，并在测试结束后调用 `agency.unspy_all()` 自动清理间谍：

```python
@pytest.fixture
def spy_agency():
    agency = SpyAgency()
    try:
        yield agency
    finally:
        agency.unspy_all()
```

只要安装 kgb，pytest 会自动发现这个插件；用户也可以通过显式声明 `pytest_plugins = ['kgb.pytest_plugin']` 来加载。

### unittest 集成

`SpyAgency` 实现了 `tearDown`，因此可以当作 mixin 混入 `unittest.TestCase` 的子类（[kgb/agency.py](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py)）。当 `SpyAgency` 与 `TestCase` 一起使用时，每个测试方法运行结束后都会自动解除间谍注册，避免跨用例的污染。

### 独立断言函数

[kgb/asserts.py](https://github.com/beanbaginc/kgb/blob/main/kgb/asserts.py) 提供了一套独立于 mixin 的断言函数。它在模块加载时实例化一个共享的 `_agency`，并把 `SpyAgency` 中所有以 `assert_` 开头的方法重新暴露为模块级函数。这样在 pytest 函数式测试中可以直接调用 `assert_spy_called_with(...)`，无需显式创建机构。

### 异步测试框架（asynctest）

社区已验证 kgb 可与 `asynctest` 配合使用（[Issue #4](https://github.com/beanbaginc/kgb/issues/4)）。典型做法是将 `SpyAgency` 与 `asynctest.TestCase` 多重继承：

```python
import asynctest
from kgb import SpyAgency

class testApi(SpyAgency, asynctest.TestCase):
    async def test_register(self):
        ...
```

由于 `spy_on` 和 `assert_spy_*` 都不涉及阻塞或事件循环，普通 spy 可在 `async def` 中安全使用；若需要 spy 协程函数本身，则依赖前述的 `CO_COROUTINE` 标志位镜像机制。

## 常见错误与排查思路

### ExistingSpyError

[kgb/errors.py](https://github.com/beanbaginc/kgb/blob/main/kgb/errors.py) 定义了 `ExistingSpyError`，它在重复注册间谍时抛出。错误消息会打印第一次注册时的栈帧回溯，便于定位"未清理"的代码路径。在 [Issue #5](https://github.com/beanbaginc/kgb/issues/5) 中，使用 DDT 进行参数化测试时常见此错误——DDT 会把同一个测试函数展开成多个用例，每个用例都需要重新注册间谍。建议在参数化测试外显式调用 `spy.unspy()` 或在 fixture 层使用 `spy_agency`。

### Python 3.13 上的闭包崩溃

[Issue #11](https://github.com/beanbaginc/kgb/issues/11) 报告了在 Python 3.13 b2 上，结合闭包外层函数与 `SpyAgency` mixin 触发的崩溃。该问题已在 [kgb 7.2 发布说明](https://github.com/beanbaginc/kgb/releases/tag/release-7.2) 中修复，原因是 3.13 重构了函数对象的构造方式。如果仍在使用 7.1 或更早版本，建议升级到 >= 7.2。

### Python 3.12 上 `assertRaisesRegexp` 失效

[Issue #12](https://github.com/beanbaginc/kgb/issues/12) 指出 Python 3.12 移除了 `unittest.TestCase.assertRaisesRegexp` 别名，导致部分历史测试失败。如果你的测试代码沿用旧别名，应替换为 `assertRaisesRegex`。

### PyCharm 静态分析误报

`spy_on(*args, **kwargs)` 的文档中使用了 `*args (tuple)` 写法（[kgb/pytest_plugin.py](https://github.com/beanbaginc/kgb/blob/main/kgb/pytest_plugin.py)、[kgb/contextmanagers.py](https://github.com/beanbaginc/kgb/contextmanagers.py)），PyCharm 会把 `tuple` 误识别为内置类型而非 Python 的 `*args` 语义（[Issue #7](https://github.com/beanbaginc/kgb/issues/7)）。规避方式是使用 `*args: tuple` 或 `*args: Any` 风格。

## 总体流程示意

下图概括了 kgb 在一次典型 spy 调用中的版本检测、对象构造和清理路径：

```mermaid
flowchart TD
    A[pytest/unittest 启动测试] --> B{是否使用 spy_agency fixture?}
    B -- 是 --> C[SpyAgency 实例化]
    B -- 否 --> D[用户手动创建 SpyAgency 或 with spy_on]
    C --> E[FunctionSpy 解析函数签名]
    D --> E
    E --> F{Python 版本分支}
    F -- Py3 --> G[FunctionSigPy3 + inspect.Signature]
    F -- Py2 --> H[FunctionSigPy2 + inspect.formatargspec]
    G --> I[构造新代码对象并镜像 co_flags]
    H --> I
    I --> J[替换原函数并记录 calls]
    J --> K[测试执行完毕]
    K --> L{自动清理?}
    L -- 是 (fixture/tearDown) --> M[unspy_all]
    L -- 否 --> N[手动 spy.unspy 或抛 ExistingSpyError]
```

## 版本历史速查

下表总结了 kgb 各版本对 Python 解释器和测试框架的关键变更，便于在选型时参考。

| 版本 | 发布日期 | 主要兼容性变更 |
|------|----------|----------------|
| 7.0 | 2022-01-20 | 显式支持 Python 3.10；新增 pytest 插件；提供 snake_case 断言方法 |
| 7.1 | 2022-08-04 | 支持 Python 3.11 |
| 7.1.1 | 2022-08-06 | 加入 `LICENSE` 文件以便打包分发 |
| 7.2 | 2024-11-03 | 修复 Python 3.13 上的崩溃 |
| 7.3 | 2025-12-09 | 支持 Python 3.14；新增 `SpyAgency.get_spy()` |

资料来源：[NEWS.rst](NEWS.rst)。

## See Also

- [README.rst](README.rst) — 项目主文档与安装说明
- [kgb/spies.py](https://github.com/beanbaginc/kgb/blob/main/kgb/spies.py) — `FunctionSpy` 的完整 API
- [kgb/signature.py](https://github.com/beanbaginc/kgb/blob/main/kgb/signature.py) — 签名内省实现
- [kgb/agency.py](https://github.com/beanbaginc/kgb/blob/main/kgb/agency.py) — `SpyAgency` 主体类
- GitHub Issue 跟踪：[#11 Python 3.13 崩溃](https://github.com/beanbaginc/kgb/issues/11)、[#13 PyPy 3.11 生成器测试](https://github.com/beanbaginc/kgb/issues/13)

---

<!-- evidence_pipeline_checked: true -->
<!-- evidence_injected: true -->

---

## Doramagic 踩坑日志

项目：beanbaginc/kgb

摘要：发现 12 个潜在踩坑项，其中 2 个为 high/blocking；最高优先级：安装坑 - 来源证据：`kgb/tests/test_function_spy.py::FunctionSpyTests::test_spy_on_generator` fails on PyPy3.11。

## 1. 安装坑 · 来源证据：`kgb/tests/test_function_spy.py::FunctionSpyTests::test_spy_on_generator` fails on PyPy3.11

- 严重度：high
- 证据强度：source_linked
- 发现：GitHub 社区证据显示该项目存在一个安装相关的待验证问题：`kgb/tests/test_function_spy.py::FunctionSpyTests::test_spy_on_generator` fails on PyPy3.11
- 对用户的影响：可能增加新用户试用和生产接入成本。
- 证据：community_evidence:github | https://github.com/beanbaginc/kgb/issues/13 | 来源讨论提到 python 相关条件，需在安装/试用前复核。

## 2. 配置坑 · 来源证据：Crashes with Python 3.13 b2

- 严重度：high
- 证据强度：source_linked
- 发现：GitHub 社区证据显示该项目存在一个配置相关的待验证问题：Crashes with Python 3.13 b2
- 对用户的影响：可能阻塞安装或首次运行。
- 证据：community_evidence:github | https://github.com/beanbaginc/kgb/issues/11 | 来源讨论提到 python 相关条件，需在安装/试用前复核。

## 3. 安装坑 · 来源证据：Pytest failing on python 3.10

- 严重度：medium
- 证据强度：source_linked
- 发现：GitHub 社区证据显示该项目存在一个安装相关的待验证问题：Pytest failing on python 3.10
- 对用户的影响：可能增加新用户试用和生产接入成本。
- 证据：community_evidence:github | https://github.com/beanbaginc/kgb/issues/8 | 来源讨论提到 python 相关条件，需在安装/试用前复核。

## 4. 安装坑 · 来源证据：Python 3.11 compatibility

- 严重度：medium
- 证据强度：source_linked
- 发现：GitHub 社区证据显示该项目存在一个安装相关的待验证问题：Python 3.11 compatibility
- 对用户的影响：可能增加新用户试用和生产接入成本。
- 证据：community_evidence:github | https://github.com/beanbaginc/kgb/issues/9 | 来源讨论提到 python 相关条件，需在安装/试用前复核。

## 5. 能力坑 · 能力判断依赖假设

- 严重度：medium
- 证据强度：source_linked
- 发现：README/documentation is current enough for a first validation pass.
- 对用户的影响：假设不成立时，用户拿不到承诺的能力。
- 证据：capability.assumptions | github_repo:10215090 | https://github.com/beanbaginc/kgb | README/documentation is current enough for a first validation pass.

## 6. 运行坑 · 来源证据：ExistingSpyError: The function <function foobar at ......> has already been spied on.

- 严重度：medium
- 证据强度：source_linked
- 发现：GitHub 社区证据显示该项目存在一个运行相关的待验证问题：ExistingSpyError: The function <function foobar at ......> has already been spied on.
- 对用户的影响：可能阻塞安装或首次运行。
- 证据：community_evidence:github | https://github.com/beanbaginc/kgb/issues/5 | 来源讨论提到 python 相关条件，需在安装/试用前复核。

## 7. 维护坑 · 来源证据：3.12+: `assertRaisesRegexp` alias removed

- 严重度：medium
- 证据强度：source_linked
- 发现：GitHub 社区证据显示该项目存在一个维护/版本相关的待验证问题：3.12+: `assertRaisesRegexp` alias removed
- 对用户的影响：可能影响升级、迁移或版本选择。
- 证据：community_evidence:github | https://github.com/beanbaginc/kgb/issues/12 | 来源讨论提到 python 相关条件，需在安装/试用前复核。

## 8. 维护坑 · 维护活跃度未知

- 严重度：medium
- 证据强度：source_linked
- 发现：未记录 last_activity_observed。
- 对用户的影响：新项目、停更项目和活跃项目会被混在一起，推荐信任度下降。
- 证据：evidence.maintainer_signals | github_repo:10215090 | https://github.com/beanbaginc/kgb | last_activity_observed missing

- 严重度：medium
- 证据强度：source_linked
- 发现：no_demo
- 证据：downstream_validation.risk_items | github_repo:10215090 | https://github.com/beanbaginc/kgb | no_demo; severity=medium

## 10. 安全/权限坑 · 存在评分风险

- 严重度：medium
- 证据强度：source_linked
- 发现：no_demo
- 对用户的影响：风险会影响是否适合普通用户安装。
- 证据：risks.scoring_risks | github_repo:10215090 | https://github.com/beanbaginc/kgb | no_demo; severity=medium

## 11. 维护坑 · issue/PR 响应质量未知

- 严重度：low
- 证据强度：source_linked
- 发现：issue_or_pr_quality=unknown。
- 对用户的影响：用户无法判断遇到问题后是否有人维护。
- 证据：evidence.maintainer_signals | github_repo:10215090 | https://github.com/beanbaginc/kgb | issue_or_pr_quality=unknown

## 12. 维护坑 · 发布节奏不明确

- 严重度：low
- 证据强度：source_linked
- 发现：release_recency=unknown。
- 对用户的影响：安装命令和文档可能落后于代码，用户踩坑概率升高。
- 证据：evidence.maintainer_signals | github_repo:10215090 | https://github.com/beanbaginc/kgb | release_recency=unknown

<!-- canonical_name: beanbaginc/kgb; human_manual_source: deepwiki_human_wiki -->
