ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ADK Python 代码风格指南:adk-python 仓库的可见性、类型、Pydantic 与测试约定全解

ADK Python 代码风格指南:adk-python 仓库的可见性、类型、Pydantic 与测试约定全解 ADK Python 代码风格指南adk-python 仓库的可见性、类型、Pydantic 与测试约定全解【免费下载链接】adk-pythonAn open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents with flexibility and control.项目地址: https://gitcode.com/GitHub_Trending/ad/adk-python本篇指南系统梳理 adk-pythonAgent Development Kit源码仓库中的编码规范与代码库约定覆盖src/google/adk/与tests/unittests/两大区域文件与符号的私有化可见性规则、导入方向、类型注解与 mypy 严格模式、Pydantic v2 模型模式、格式化与 pre-commit 钩子、日志、异步 I/O、文件组织与单元测试结构。读完本文你将能够按照 ADK 的 house style 编写或修改源码与测试定位各类格式化/静态检查失败pyink、isort、ruff、mypy、addlicense、compliance-checks的对应参考文档并理解这些约定背后的设计理由。本指南的权威出处是仓库中的 .agents/skills/adk-style/SKILL.md 及其references/目录下的 10 份分主题参考文档文中的实现证据来自 src/google/adk、tests/unittests、pyproject.toml 与 scripts/compliance_checks.py。一、约定总览谁在强制这些规则ADK 的大多数代码风格约定并非停留在建议层面而是由pre-commit 钩子或CI 任务强制执行——违反规则会直接阻塞 PR而不是等到代码评审阶段才被发现。因此读懂约定与知道去哪里查约定同样重要。1.1 按任务查参考文档SKILL.md 给出了一张任务 → 参考文档速查表写作或修改代码时应只阅读与当前任务相关的唯一参考文档任务参考文档新增.py文件判断公共 vs 私有__init__.py与__all__visibility.md编写import行相对 vs 绝对循环导入TYPE_CHECKINGimports.md参数与返回值注解Optionalvs\| Nonekeyword-only 参数isinstanceassertmypytyping.md定义 Pydantic 模型、validator、私有属性或线上传输载荷pydantic.md缩进、行宽、引号运行格式化器各钩子检查什么formatting.md编写 docstring 或解释性注释documentation.md输出日志记录模块 logger 命名选择日志级别logging.md涉及 I/O 的一切——网络、磁盘、数据库async.md新文件放哪里license 头测试放哪里、叫什么file-organization.md编写或重构单元测试testing.md1.2 按失败的检查项查参考文档当某个钩子报错时对照下表定位问题来源失败的检查参考文档check-new-py-prefixvisibility.mdcompliance-checkslogging.mdlogger 名、typing.mdfrom __future__ import annotations、imports.mdcli/导入方向pyink、isort、ruff、addlicense、codespellformatting.mdMypy Check CI 任务typing.md二、可见性private-by-default 的文件与符号体系Python 没有访问修饰符因此 ADK 用命名约定与模块结构来定义可见性边界详见 visibility.md。2.1 模块私有 / 内部文件默认私有src/google/adk/下所有新增.py模块文件默认都必须带_前缀由 pre-commit 钩子check-new-py-prefix强制。即使文件内含有面向公共 API 的符号文件本身也必须带下划线符号再通过包的__init__.py暴露出去。仅供包内部使用的文件同样带前缀例如_task_models.py。这类文件绝不允许被 ADK 框架之外的代码直接导入。2.2 类与函数的可见性公共无下划线前缀供模块/包的消费者使用。内部/私有带下划线前缀如_private_method()仅供定义它的类或模块内部使用。2.3 包私有子系统可见性由于 Python 没有真正的 package-private 机制ADK 通过以下方式模拟不导出包级__init__.py不导出该符号下划线模块内部实现细节放在_前缀模块中同一包内的代码可以导入这些_模块包外代码不可以直接导入框架内部必须从具体模块导入绝不能从包的__init__.py导入原因见导入章节——避免循环导入与整包急切加载。2.4 公共 API 导出包公共 API 必须在__init__.py中显式导出必须定义__all__明确列出公共 API 符号只有面向包外使用的公共名称才能被导入进__init__.py并列入__all__用户应能直接从包级别导入公共符号而不是钻入内部模块。2.5 示例暴露公共接口 vs 隐藏实现细节# 文件 src/google/adk/agents/llm/task/_task_agent.py文件默认私有 class TaskAgent: # 公共符号 ... # 文件 src/google/adk/agents/llm/task/__init__.py from ._task_agent import TaskAgent __all__ [ TaskAgent, ]# 文件 src/google/adk/agents/llm/task/_task_models.py内部文件 class TaskRequest(BaseModel): # 模块内公共但模块本身私有 ... # 文件 src/google/adk/agents/llm/task/__init__.py # 若 TaskRequest 仅供 task 包内部使用则此处不导出它。三、导入规范方向、分组与 TYPE_CHECKINGimports.md 定义了导入的三条核心规则。3.1 相对导入 vs 绝对导入源码src/使用相对导入。from ..agents.llm_agent import LlmAgent测试tests/使用绝对导入。from google.adk.agents.llm_agent import LlmAgent从模块导入而非从包导入写from ..agents.llm_agent import LlmAgent绝不写from ..agents import LlmAgent。在框架内部经由__init__.py导入会制造循环导入并迫使包急切加载无关模块。cli/包的特殊规则把它当作外部包对待cli/内部文件之间用相对导入cli/外部文件用绝对导入依赖方向cli/可以导入代码库其余部分但代码库其余部分不得导入cli/。scripts/compliance_checks.py 会在任何包外from ...cli... import ...时让提交失败。3.2 一行一个名字isort使用googleprofile见 pyproject.toml 中profile google配置每个被导入的名字独占一行因此from typing import Any, Optional会被拆成两行。排序不区分大小写且不按类型分组——这正是PrivateAttr排在model_validator之后的原因from typing import Any from typing import Optional from pydantic import BaseModel from pydantic import Field from pydantic import model_validator from pydantic import PrivateAttr导入分为三个组以空行分隔标准库、第三方、相对导入。在测试中google.adk归入第三方组pyproject.toml的known_third_party与google.genai、pytest并列。3.3 不要折行长导入80 字符限制不适用于 import 行isort配置了line_length 200已确认位于 pyproject.tomlpyink 也会保持 import 行完整。长的from ... import ...保持单行——代码库中不存在带括号的 from-import。手动加括号或换行会在下一次格式化时被还原。3.4 TYPE_CHECKING 导入仅类型提示需要、但运行时可能造成循环导入的导入应放在TYPE_CHECKING块中from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from ..agents.invocation_context import InvocationContext其原理是from __future__ import annotations让所有注解变成字符串延迟求值因此该导入在运行时永远不会真正执行。四、类型注解与强类型mypy strict 时代的写法typing.md 是类型层面的核心参考。4.1 通用规则全量注解所有函数参数与返回值都要写类型提示最小化Any优先使用具体类型或TypeVar因为Any会禁用流经它的所有值的检查from __future__ import annotations位于每个模块顶部紧跟 license 头之后、其他任何 import 之前。scripts/compliance_checks.py 会在缺失时使提交失败。豁免对象__init__.py、version.py、tests/、contributing/samples/禁止带引号的类型提示延迟注解已让前向引用无需引号写list[str]而非list[str]内置泛型新代码使用list[str]、dict[str, int]、tuple[str, ...]。typing.List/typing.Dict只残留在旧模块中不要新增、也不要无谓改写旧的。4.2 Mypy 严格模式Mypy 以strict 模式针对src/运行并启用 Pydantic 插件目标 Python 3.11——这些配置已确认在 pyproject.toml 的[tool.mypy]段strict true、plugins [pydantic.mypy]。tests/与contributing/samples/被排除。mypy .CI 任务会把你分支的错误与基线分支对比只对新增错误失败——你触碰的文件里既有的错误不是你的问题但你新增行上的错误是。4.3Optional[X]vsX | None两种写法在代码库中并存约定是新代码尤其是workflow/优先X | None已有文件跟随文件内既有风格没有理由就不要把一种重构为另一种。4.4 参数用抽象类型、返回用具体类型参数用collections.abc的抽象类型让调用方可以传入任何兼容容器返回用具体类型让调用方确切知道拿到什么from collections.abc import Mapping from collections.abc import Sequence def merge_labels( labels: Mapping[str, str], extra: Sequence[str] ) - dict[str, str]: ...4.5 Keyword-Only 参数在参数顺序容易写错的构造器或函数中在参数前加*——两个同类型参数就足以造成静默 bugclass NodeRunner: def __init__( self, *, node: BaseNode, parent_ctx: Context, run_id: str | None None, ): ...适用场景含 2 个及以上非self参数的构造器、交换两个参数仍能通过类型检查的任何函数、接受多个str或int参数的方法。4.6 可变默认参数可变默认值在定义时只求值一次并被所有调用共享一个调用方的修改会泄漏给下一个调用方。用None作为哨兵# Bad——每个调用方共享同一个 list def add(item: str, items: list[str] []) - list[str]: ... # Good def add(item: str, items: list[str] | None None) - list[str]: items list(items) if items else [] ...这适用于list、dict、set及任何其他可变类型。4.7 运行时类型判别isinstance()isinstance()是代码库处理多态输入的标配。写穷尽的if/elif链并且总是以else收尾if isinstance(node, FunctionNode): ... elif isinstance(node, (JoinNode, ToolNode)): ... else: raise TypeError(fUnsupported node type: {type(node)})必须包含抛出TypeError或处理未知情况的else让新子类大声失败而不是静默无操作优先isinstance(x, SomeType)而非type(x) is SomeType——前者兼容子类用元组一次检查多个类型isinstance(x, (TypeA, TypeB))。4.8 生产代码禁用 assertassert在 Python 以-O运行时会被剥离因此断言不是运行时保证且其失败信息对调用方毫无帮助。应改抛ValueError、TypeError或RuntimeError。测试中的 assert 没有问题。五、Pydantic v2 模式ADK 模型的标准写法ADK 的模型全部基于Pydantic v2pydantic.md 给出了统一模式。5.1 基本模型结构用Field()表达校验、默认值与描述用PrivateAttr()表达不需要序列化的内部状态用model_post_init()做构造后置逻辑而不是覆盖__init__——覆盖 Pydantic 模型的__init__会绕过校验顺序使用model_dump()/model_dump_json()而不是 v1 的dict()/json()。5.2 机制选择速查表需求模式简单的数值/字符串边界Field(ge0, le100)单字段业务逻辑field_validator(field)跨字段一致性model_validator(modeafter)字段弃用/迁移model_validator(modebefore)内部可变状态PrivateAttr(default_factory...)构造后置设置model_post_init()5.3Field()约束把边界声明在字段上而不是为它们写 validator——规则紧贴数据并会出现在生成的 JSON Schema 中compaction_interval: Optional[int] Field(defaultNone, gt0) injected_latency_seconds: float Field(default0.0, le120.0)5.4 字段文档house style 是在字段正下方写属性 docstringSphinx 可直接拾取model: Union[str, BaseLlm] The model to use for the agent. When not set, the agent inherits the model from its ancestor. 这些 docstring 仅作文档用途。若要让它们同时成为 JSON Schema 中的字段描述在模型的ConfigDict中设置use_attribute_docstringsTrueSerializedBaseModel已默认启用class MyModel(BaseModel): model_config ConfigDict(use_attribute_docstringsTrue) field_name: str Description of the field.5.5 线上传输模型On-Wire Models跨越网络或存储边界的模型——API 载荷、WebSocket 消息、持久化事件——应继承google.adk.utils._serialized_base_model中的SerializedBaseModel而非BaseModel。它设置alias_generatorto_camel加populate_by_nameTrue并让model_dump_json()默认by_aliasTrue。于是 Python 侧保持 snake_case线上格式保持 camelCase调用方无需每次都记得传by_alias。5.6field_validator单字段校验当约束需要Field()无法表达的逻辑时使用field_validator(max_llm_calls) classmethod def validate_max_llm_calls(cls, value: int) - int: if value 0: raise ValueError(max_llm_calls must be positive.) return value规则在装饰器下显式加classmethod。Pydantic v2 会隐式应用但 ADK 代码库中每个 validator 都显式写出返回可能被转换后的值抛出命名了字段与边界的ValueError默认模式是after强转后运行几乎总是你想要的省略该参数。只有需要拦截原始输入时才传modebefore。5.7model_validator跨字段与迁移校验modebefore——弃用与字段迁移接收原始输入通常是 dict在任何字段被解析之前运行用于重命名或回填字段model_validator(modebefore) classmethod def check_for_deprecated_save_live_audio(cls, data: Any) - Any: If save_live_audio is passed, use it to set save_live_blob. if isinstance(data, dict) and save_live_audio in data: warnings.warn( The save_live_audio config is deprecated, use save_live_blob., DeprecationWarning, stacklevel2, ) if data[save_live_audio]: data[save_live_blob] True return data务必用isinstance(data, dict)防护输入也可能是已经构造好的模型实例直接索引会抛异常。modeafter——跨字段一致性接收构造好的实例并必须返回它model_validator(modeafter) def _validate_parallel_worker_config(self) - Node: if self.max_parallel_workers is not None and not self.parallel_worker: raise ValueError( max_parallel_workers can only be set when parallel_worker is True. ) return self六、格式化与 pre-commit 钩子每条规则由谁执行设置以 pyproject.toml 与.pre-commit-config.yaml为唯一权威来源formatting.md 是对两者的汇总。6.1 核心格式参数2 空格缩进绝不用 tabpyink-indentation 280 字符行宽pyinkline-length 80import 行例外pyinkGoogle 的 Black 分支负责 Python 格式化引号跟随文件内已有的主流风格pyink-use-majority-quotespyink 不会把x重写成x——跟随被编辑文件的风格而不是来回折腾引号isort以profile google排序导入已确认位于 pyproject.toml。6.2 每个钩子检查什么钩子作用ruff仅删除未使用的导入lint.select [F401]自动修复仅src/。__init__.py豁免因为它的导入是再导出isort导入顺序与分组pyink其余所有格式化addlicense为.py/.sh添加 Apache 2.0 头。Go 二进制未安装时跳过并告警CI 仍会捕获check-new-py-prefixsrc/google/adk/下新文件需_前缀——见可见性章节compliance-checkslogger 名、from __future__ import annotations、cli/导入方向、mTLS 端点codespell代码与散文中的拼写。确属误报的加入 pyproject.toml 的ignore-words-listpyproject-fmt规范化pyproject.toml自身mdformat仅README.md、CONTRIBUTING.md与contributing/**.mdcheck-yaml、end-of-file-fixer、trailing-whitespace空白与 YAML 语法卫生update-constraintspyproject.toml变化时重新生成constraints-3.*.txt需要网络src/google/adk/cli/browser/、src/google/adk/v1/与v1_tests/从所有钩子中排除。6.3 运行格式化器安装一次 git 钩子提交时自动格式化pre-commit install检查尚未提交的改动# 仅已暂存文件这正是提交钩子运行的内容 pre-commit run # 指定文件 pre-commit run --files {path/to/file.py} # 全部文件 pre-commit run --all-filesCI 运行的是同一套配置因此一次干净的pre-commit run --all-files意味着 lint 任务会通过。类型错误是独立的 CI 任务——见类型章节。七、文档与注释写 Why不写 Whatdocumentation.md 的要点类解释预期用法签名看不出用法时给出简洁示例记录每个公共属性。Pydantic 模型用字段下方的属性 docstring 记录字段——见 Pydantic 章节方法与函数记录每个参数、返回值与抛出的每个异常内部实现注释解释为什么而不是做了什么。代码本身已经说明了它做什么注释的价值在于说明为什么这样做——代码中看不到的约束、bug 或顺序要求不链接 RFC、设计文档、issue 或 PR它们腐化得比代码快打不开链接的读者什么都得不到。把推理写进注释本身链接放进 PR。八、日志统一 logger 树与级别语义logging.md 是日志规范。8.1 模块 logger每个需要记日志的模块都以google_adk.前缀声明 loggerlogger logging.getLogger(google_adk. __name__)scripts/compliance_checks.py 会在出现裸logging.getLogger(__name__)时使提交失败。前缀把所有 ADK 记录归入同一棵 logger 树因此嵌入 ADK 的应用只需一次logging.getLogger(google_adk)调用就能提升或静默框架的日志而不影响自己的日志。这一约定在源码中处处可见例如 src/google/adk/agents/base_agent.py、src/google/adk/agents/_managed_agent.py 等模块均按此模式声明。8.2 通用规则惰性格式化把值作为参数传入字符串只在级别启用时才构建。好logger.info(Processing item %s, item_id)坏logger.info(fProcessing item {item_id})绝不记录机密API key、凭据、token 或 PII上下文日志可用时包含 trace ID使记录在调用链上可关联。8.3 日志级别DEBUG诊断细节内部实现中放心使用INFO预期里程碑工作流开始、节点完成WARNING意外但不阻止操作继续一次重试、一个弃用字段ERROR阻止操作完成的失败。九、异步与并发I/O 必须进 async defasync.md 是异步代码的核心约束I/O 属于async def网络调用、文件系统访问、数据库查询、子进程等待都要放进 async 函数。ADK 在单个事件循环上运行一切async 代码里的一次同步调用会拖住所有并发 agent而不只是调用者不要阻塞事件循环async 代码内禁止同步 HTTP 客户端、time.sleep或阻塞式文件读取用asyncio.to_thread包装同步 I/O当某个库没有 async APIopen()、pathlib、多数云 SDK 客户端时async def save_data(path: Path, data: bytes) - None: # 包装阻塞写入让事件循环保持空闲。 await asyncio.to_thread(path.write_bytes, data)没有钩子检查这条规则——阻塞调用能通过 CI之后以并发下的莫名延迟形式暴露因此值得在评审阶段抓住。十、文件组织新文件放哪里、测试叫什么file-organization.md 定义了物理布局。10.1 文件头src/google/adk/下每个模块以如下顺序开头Apache 2.0 license 头由addlicense钩子添加from __future__ import annotations导入标准库、第三方、相对导入。from __future__ import annotations由 scripts/compliance_checks.py 检查豁免__init__.py、version.py、tests/与contributing/samples/。另有约定workflow/目录下一个类一个文件。10.2 测试放在哪里在tests/unittests/下镜像源码路径src/google/adk/tools/environment/_edit_file_tool.py tests/unittests/tools/environment/test_edit_file_tool.py一个源文件需要多个测试文件时使用去掉下划线与扩展名的源文件名作为共享前缀src/google/adk/workflow/_workflow.py tests/unittests/workflow/test_workflow.py tests/unittests/workflow/test_workflow_hitl.py tests/unittests/workflow/test_workflow_nested.py十一、单元测试测行为不测实现testing.md 是测试编写的完整规范仓库中 tests/unittests 的数千个测试均遵循此结构。11.1 核心原则通过公共接口测试——调用用户调用的断言用户看到的测行为而非实现——验证结果输出、副作用、错误不验证内部机制重构免疫——若内部重构保持了相同行为所有测试仍应通过。pytest以asyncio_mode auto运行裸async def test_...无需pytest.mark.asyncio。许多旧测试仍带着该标记无害也不值得清理。11.2 测试命名描述行为# 好——描述调用方观察到什么 def test_empty_queue_returns_none(): def test_retry_stops_after_max_attempts(): def test_missing_key_raises_key_error(): # 坏——描述实现细节 def test_deque_popleft_called(): def test_retry_counter_incremented(): def test_dict_getitem_raises():11.3 docstring一行概述复杂测试再补 Setup/Act/Assert# 好——简单测试一行足够 Getting from an empty cache returns the default value. # 好——复杂测试带结构化分解 Partial FR re-runs nested Workflow, resolved child completes while unresolved stays interrupted. Setup: outer_wf → inner_wf → (child_a, child_b) → join. Both children interrupt on first run. Act: - Run 2: resolve only child_as FR. - Run 3: resolve child_bs FR. Assert: - Run 2: child_a produces output, invocation still interrupted. - Run 3: child_b produces output, join completes, no interrupts. 11.4 一个测试只覆盖一个行为如果一个测试检查多个无关行为拆开它。无法用一句话描述测试说明它测得太多了。11.5 不测内部状态# 坏——伸手进私有属性 assert pool._workers[0].is_alive assert parser._state HEADER assert isinstance(router._handler, _FastHandler) # 好——通过公共接口测试 assert pool.active_count 1 assert parser.parse(data) expected assert router.route(/api) handler11.6 用真实组件只在边界处 mockMock 外部依赖LLM API、云服务、会话存储使用真实 ADK 组件BaseNode子类、Event、Context测试NodeRunner时 mockInvocationContext它是边界。11.7 fixture 保持最小定义能触发行为的最简单设置避免厨房水槽式 fixture。11.8 把 arrange 逻辑放在测试附近只被一个测试使用的辅助类或 fixture 应内联定义在测试函数内3 个以上测试共享时才提到模块级。11.9 断言要讲故事# 好——读起来像规格说明 assert queue.size 0 assert config.get(timeout) 30 assert response.status_code 404 # 坏——过度防御测的是框架行为 assert isinstance(queue, Queue) assert hasattr(config, get) assert len(response.headers) 011.10 结构化为 arrange、act、assert每个测试有三个清晰步骤Arrange设置场景特有外部状态通用设置放 fixture、Act调用被测系统通常一次调用、Assert验证返回值或可见状态变化此处不再调用被测系统。步骤间以空行分隔复杂测试可用 Given …/When …/Then … 描述性注释避免无信息量的裸标签。11.11 按被测单元组织测试文件而不是按改动新测试加入被测模块/功能已有的测试文件中绝不创建以 CL、bug 或一次改动命名的测试文件——那会把模块的覆盖碎片化且因为下一个改该模块的人会在模块文件里找测试而不是在一次性文件里找而腐烂。新增文件前先找现有归属test_module*.py若兄弟测试已断言同一行为扩展那条断言而非在新文件中复制。只有对真正的新模块/功能区才新建文件命名为test_module_feature.py# 坏——以改动命名把 llm_agent/runner/llm_request 的覆盖 # 碎片化进一个无人维护的大杂烩 tests/unittests/agents/test_improved_error_messages.py # 好——每个测试落在其单元所属的现有文件里 tests/unittests/agents/test_llm_agent_error_messages.py # LlmAgent 消息 tests/unittests/models/test_llm_request.py # LlmRequest 消息 tests/unittests/test_runners.py # Runner 消息11.12 测试文件结构模板Tests for ComponentName. Verifies that component correctly high-level behavior. # --- Fixtures (minimal, one purpose each) --- def _make_service(): ... # --- Tests (one behavior per test) --- def test_behavior_description(): One sentence: what the system does from the outside. # Given a service with default config service _make_service() input_data hello # When the operation is performed result service.do_something(input_data) # Then the result matches expectations assert result expected十二、快速上手指南为 adk-python 仓库贡献代码时按以下顺序核对风格新增.py文件src/google/adk/下文件名带_前缀测试文件镜像到tests/unittests/下命名为test_模块[_特性].py参考 visibility.md 与 file-organization.md编写源码文件头三件套license from __future__ import annotations 分组导入全量类型注解I/O 进async def日志用logging.getLogger(google_adk. __name__)参考 imports.md、typing.md、async.md、logging.mdPydantic 模型Field()表达约束、validator 处理逻辑、model_post_init()做置后设置线上载荷继承SerializedBaseModel参考 pydantic.md提交前pre-commit run --all-files本地跑通全部钩子再mypy .确认无新增类型错误若某个钩子失败按第二节的对照表定位参考文档参考 formatting.md。风格约定最终以 pyproject.toml 与.pre-commit-config.yaml为唯一权威来源本文档是对它们的系统性汇总与解读。任何这段代码是否符合 ADK 风格的问题都可以在 .agents/skills/adk-style 的 10 份参考文档中找到明确答案。【免费下载链接】adk-pythonAn open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents with flexibility and control.项目地址: https://gitcode.com/GitHub_Trending/ad/adk-python创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表