Files
PolyLoop/pyproject.toml
T
iomgaa d833b7a4d9 chore(release): 定版 1.0.2
CHANGELOG 的「未发布」段落定成 1.0.2(2026-08-27),上面留一个空段给下一版接着攒;
两处版本号(pyproject.toml 与 polyloop/__init__.py)同步。

发之前跑了一轮完整压测,与 1.0.1 那次验收逐项可比:正常负载 197 次运行、1845 次真实模型
调用,十一条不变量全部通过零击穿;九类故障注入 58 条判据全部通过、零 failed。库本身没有被
压出 bug。产物在 tools/soak/runs/v1.0.2/(该目录在 gitignore 里)。

README 的三条安装命令不用改:约束是 polyloop==1.0.*,1.0.2 落在里面。这同时意味着钉了那个
约束的下游一次例行升级就会拿到这一版,而这一版有一处行为变化(参数快照里注入内容的键形状)
——眼下没有下游在用,所以实际影响为零,CHANGELOG 里那一节把失败形态写清楚了。
2026-08-27 05:29:28 -04:00

213 lines
11 KiB
TOML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
[build-system]
requires = ["setuptools>=68.0"]
build-backend = "setuptools.build_meta"
[project]
name = "polyloop"
# 与 src/polyloop/__init__.py 的 __version__ 必须一致,由 tests/unit/test_package.py 断言。
version = "1.0.2"
description = "PolyLoop:实验室共用的 Agent 执行内核——一次运行的预算、停止语义、取消、逐步轨迹与 Skill 注入"
# registry 的包页面正文只认这一项:缺了它页面就是一片空白,而 twine 只会警告
# long_description missing,不阻塞上传——三步全绿、产物是坏的(PolyGateway 1.1.2 的教训)。
# README 在打包时被固化进产物,发布之后再改无效,所以改 README 必须排在构建之前。
readme = "README.md"
requires-python = ">=3.11"
# 3.11 是被下游钉死的:dissect 与 GovDoc-SaaS 都跑在 3.11,一个库不能要求比它的消费者更高的版本。
dependencies = []
# 核心没有运行时依赖,这是刻意的。公共类型与接缝签名上不许出现第三方类型(依赖规则 6),
# 否则那个包的 major 就是我们的 major。
[project.optional-dependencies]
# 模型适配器单独成一个 extra:不用它的人不该被迫装上网关,也不该在 import 时把网关连同
# 它的 provider 目录一起拉起来(依赖规则 9)。PolyGateway 不在公共 PyPI 上,装它要配私有源。
gateway = ["polygateway>=1.1,<2"]
# 契约套件(polyloop.testing)跑起来要的两个包。**版本只写下界,不钉死**——这和下面 dev 那组
# 正好相反,理由也正好相反:dev 是本仓库自己的工具链,钉死是为了本地和 CI 跑的是同一套;
# testing 装进的是下游自己的环境,钉死会和下游已经在用的 pytest 打架,而下游没有第二个
# 虚拟环境可以放我们钉的那一版。下次想「顺手统一一下」这两组的写法时,先看这段。
testing = ["pytest>=8", "pytest-asyncio>=0.24"]
# dev 工具链的版本必须钉死,不用下界。教训来自 CHSAnalyzer:本地 conda 是 ruff 0.15.1、
# CI 装到刚发布的 0.16.0,新版本多了几条规则,于是「本地全绿、CI 全红」。不钉的话 CI 会在
# 没有任何人改代码的情况下随上游发版随机变红,而随机的红叉很快就会训练所有人忽略红叉。
# 升级流程:改这里的版本号 → 本地重装 → 修掉新规则的告警 → 一起提交。
dev = [
"pytest==9.1.1",
"pytest-asyncio==1.4.0",
"ruff==0.16.2",
"import-linter==2.13",
# 发布用的两个。**刻意放进 dev 而不是靠人手装**:PolyGateway 那边它们不在任何 extra 里,
# 于是发布指南必须多写一条「记得先装这两个」,而那种步骤迟早有人漏。
"build==1.5.0",
"twine==7.0.0",
]
# 包页面上那几个链接。PyPI 的元数据里没有「仓库」这个字段,所以 registry 不会自动把包挂到
# 仓库上——那一步只能在网页上手动做,这里这几条是包页面上唯一能自带的去处。
[project.urls]
Homepage = "https://gitea.iomgaa.online/iomgaa/PolyLoop"
Changelog = "https://gitea.iomgaa.online/iomgaa/PolyLoop/src/branch/main/CHANGELOG.md"
Issues = "https://gitea.iomgaa.online/iomgaa/PolyLoop/issues"
# pytest 插件入口。它换回来的是**断言重写**:契约模块不在下游的 `python_files` 匹配范围里,
# 默认不被重写,于是一条契约用例失败时下游只看到光秃秃的 AssertionError,没有等号两边的值。
# 声明了 pytest11 入口的发行包,它的每个 .py 文件都会被 pytest 标记为可重写。
# 不做的话没有任何东西会报错——纯静默退化,理由与边界写在 polyloop/testing/_plugin.py 里。
[project.entry-points.pytest11]
polyloop = "polyloop.testing._plugin"
[tool.setuptools.packages.find]
where = ["src"]
# py.typed 让下游装上之后拿得到类型信息。一个库不带它,下游的类型检查器会把整个包当成
# 无注解的黑盒,签名和公共类型上写的东西一条都用不上——而这个库对下游的承诺大半就写在
# 签名里(CLAUDE.md §6:下游看不到实现,只能靠签名和类型判断该传什么)。
[tool.setuptools.package-data]
polyloop = ["py.typed"]
[tool.ruff]
target-version = "py311"
line-length = 100
[tool.ruff.lint]
# S110try-except-pass)与 E722(裸 except)断言 CLAUDE.md §1.7「禁止吞掉错误」。
# 这两条是硬约束的机器形式,不许在本文件或行内 noqa 里放开。
select = ["E", "F", "W", "I", "N", "UP", "B", "A", "C4", "SIM", "TCH", "S110"]
ignore = ["E501"]
[tool.ruff.lint.isort]
known-first-party = ["polyloop"]
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
asyncio_mode = "auto"
# 四层的判据是「依赖什么」,不是「叫什么」(CLAUDE.md §1.9)。按名字分层的话,改个函数名
# 就要挪测试文件;按依赖分,只要这个测试还是不连外部服务,它就一直待在原地。
#
# unit 里会出现少数只读源码文本、根本不执行被测代码的测试(依赖规则 6 那条「types 与 ports
# 禁止 import 任何第三方包」就是这种,它写不成 import-linter 契约)。它们不连任何外部服务,
# 按上面那条判据就是 unit,不为它们单开一层。哪天这类测试多到想单独跑,再拆不迟。
markers = [
"unit: 用测试替身,不连任何外部服务",
"integration: 连真 PolyGateway",
"e2e: 打真实模型网关,会产生真实调用与费用",
"contract: 公共 Protocol 的行为一致性套件,也是任何新适配器的准入标准",
]
# --strict-markers:拼错的标记直接报错,不给警告。默认配置下 @pytest.mark.uint 只是一条
# 警告,而那个文件会从此不属于任何一层、被所有筛选漏掉,静默消失。
# --import-mode=importlib:按文件路径建模块名,不按 basename。默认的 prepend 模式下
# tests/unit/test_stopping.py 与 tests/integration/test_stopping.py 会被当成同一个模块,
# 收集时报 import file mismatch——而且挂掉的是**整次收集**,连没被 -m 选中的层也一起挂。
# 另一种解法是给每个层目录补 __init__.py,选这个是因为它不要求以后新建目录的人记得补。
# -m 'not e2e'e2e 打真实网关、要花钱,默认不跑。显式 `pytest -m e2e` 覆盖(CLI 的 -m
# 后到优先)。这一条是刻意的默认排除,不是静默降级——它写在这里,且 markers 里注明了原因。
addopts = "--strict-markers --import-mode=importlib -m 'not e2e'"
# ---------------------------------------------------------------------------
# 依赖规则的机器形式。十条规则本身与它们各自的理由在
# research-wiki/design/0003-public-api-shape.md 决策八,当前形状在
# research-wiki/explanation/architecture.md 第七节。这里只写契约,不复述理由。
#
# 十条里有两条写不成 import-linter 契约,还有一条只有前半写得成;写不成的那些是 tests/ 里的测试:
# 规则 6types 与 ports 禁止 import 任何第三方包)——「任何第三方」不是一份可枚举的清单。
# 规则 9import polyloop 之后 sys.modules 里没有 polygateway)——那是运行时事实,不是静态图。
# 规则 10 的后半(import polyloop 之后 sys.modules 里没有 pytest)——同上。前半在下面。
# ---------------------------------------------------------------------------
[tool.importlinter]
root_packages = ["polyloop"]
# 有几条契约要禁止 import 外部包(下游项目、polygateway、asyncio / pathlib),
# 而外部包默认不进 import 图,所以必须在顶层打开这个开关。
include_external_packages = true
# 规则 1、2、3、8。`|` 表示同层且互不 import`:` 表示同层可以互相 import。
# 装配层那三个用 `|`session 一旦 import 了某个存储实现,那个实现就成了隐式默认。
# 逻辑层那五个也用 `|`:它们之间的编织只能发生在 session 里。
[[tool.importlinter.contracts]]
name = "分层:装配层 > 逻辑层 > ports > types,且同层互不 import"
type = "layers"
layers = [
"polyloop.session | polyloop.stores | polyloop.adapters | polyloop.testing",
"polyloop.tools | polyloop._assembly | polyloop._stopping | polyloop._recovery | polyloop.serialization",
"polyloop.ports",
"polyloop.types",
]
# 规则 8 单列。上一条已经覆盖它,重复一遍是为了让违规信息直接指向
# 「接缝定义模块被污染了」,而不是一条读起来要绕一圈的分层报错。
[[tool.importlinter.contracts]]
name = "ports 只依赖 types,不依赖任何其他 polyloop 模块"
type = "forbidden"
source_modules = ["polyloop.ports"]
forbidden_modules = [
"polyloop.session",
"polyloop.stores",
"polyloop.adapters",
"polyloop.testing",
"polyloop.tools",
"polyloop._assembly",
"polyloop._stopping",
"polyloop._recovery",
"polyloop.serialization",
]
# 规则 4。三个下游的顶层包名:dissect 是 harnessGovDoc-SaaS 是 docagent_core
# CHSAnalyzer 还没写到 agent 那一步,等它有了包名再加进来。
[[tool.importlinter.contracts]]
name = "不反向 import 任何下游项目"
type = "forbidden"
source_modules = ["polyloop"]
forbidden_modules = ["harness", "docagent_core"]
# 规则 5。适配器是唯一允许碰网关的地方。
[[tool.importlinter.contracts]]
name = "除 adapters 外一切禁止 import polygateway"
type = "forbidden"
source_modules = [
"polyloop.session",
"polyloop.stores",
"polyloop.testing",
"polyloop.tools",
"polyloop._assembly",
"polyloop._stopping",
"polyloop._recovery",
"polyloop.serialization",
"polyloop.ports",
"polyloop.types",
]
forbidden_modules = ["polygateway"]
# 规则 10 的前半。契约套件是 polyloop 里唯一允许碰 pytest 的地方,形状照着上面那条 polygateway 写。
# 别处 import pytest 的后果是每个装了本库的下游都被迫装上 pytest 才 import 得动 polyloop
# 而 pytest 只在 testing 这个 extra 里,核心的 dependencies 是空的。
[[tool.importlinter.contracts]]
name = "除 testing 外一切禁止 import pytest"
type = "forbidden"
source_modules = [
"polyloop.session",
"polyloop.stores",
"polyloop.adapters",
"polyloop.tools",
"polyloop._assembly",
"polyloop._stopping",
"polyloop._recovery",
"polyloop.serialization",
"polyloop.ports",
"polyloop.types",
]
forbidden_modules = ["pytest"]
# 规则 7。这一条是烟雾报警,不是纯度契约——os、subprocess、sqlite3 都能绕过它,
# 而 open() 是内置函数,import-linter 根本看不见。它拦得住最常见的那种偷懒
# (写着写着顺手 await 一下存储、顺手读个文件),拦不住存心的。
[[tool.importlinter.contracts]]
name = "三个纯逻辑模块不碰 I/O 与事件循环"
type = "forbidden"
source_modules = [
"polyloop._assembly",
"polyloop._stopping",
"polyloop._recovery",
]
forbidden_modules = ["asyncio", "pathlib"]