Files
PolyLoop/pyproject.toml
T
iomgaa 303e4ebc1c chore(release): 打包元数据补齐,版本定到 1.0.1
发布前置:readme 与 project.urls 从第一版就写全——缺 readme 的话 registry 包页面正文一片
空白,而 twine 只警告不阻塞上传,三步全绿产物却是坏的(PolyGateway 1.1.2 的教训)。
build 与 twine 钉进 dev extra 而不是靠人手装,PolyGateway 那边它们不在任何 extra 里,
于是发布指南得多写一条「记得先装」,那种步骤迟早有人漏。

README 里那条「项目还没有可用的功能、十个模块是空骨架」的横幅早就过期了,而打包会把当时的
README 固化进 sdist、发布后再改无效,所以在构建之前换成安装说明与现状。顺带修两处与事实
不符的:GovDoc-SaaS 的实现已经在 8 月 3 日整体清空,⑥ 对它的验收标准改成设计级验收。

新建 CHANGELOG.md,写清楚首个版本为什么是 1.0.1 而不是 0.x——「公共类型的字段只增不删不
改名」那条承诺从第一个下游装上它那天起就生效,而 0.x 意味着随时可以破坏兼容。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 00:11:04 -04:00

178 lines
8.9 KiB
TOML
Raw Permalink 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.1"
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"]
# 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"
[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)——那是运行时事实,不是静态图。
# ---------------------------------------------------------------------------
[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.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.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.tools",
"polyloop._assembly",
"polyloop._stopping",
"polyloop._recovery",
"polyloop.serialization",
"polyloop.ports",
"polyloop.types",
]
forbidden_modules = ["polygateway"]
# 规则 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"]