CLI-Anything:可编程CLI构建范式与工程化实践
发布时间:2026/9/28 16:54:43来源:尧图网络
1. CLI-Anything 不是又一个命令行工具而是一套可编程的 CLI 构建范式你有没有试过写一个 Python 脚本跑完之后得手动复制输出、粘贴进 Excel、再改个文件名、最后发邮件给同事或者更糟——把同样的逻辑硬编码进三个不同项目里每次改 bug 都得同步三份我干过。三年前在一家做数据中台的公司团队里有七个人维护着二十多个零散的 CLI 小工具sync-db,gen-report,clean-cache,mock-api……它们用的不是同一套参数解析器有的用argparse有的用fire有的甚至直接sys.argv[1:]硬切帮助文档格式五花八门有的写在 README 里有的藏在--help里还有的靠口头传授最要命的是——没人敢动gen-report的核心逻辑因为没人知道它依赖哪个版本的pandas也不知道--formatcsv和--formatexcel在底层到底调用了哪两个函数。直到我把所有这些脚本全删了用click重写了第一个统一入口cli-anywhere。注意不是cli-anything——那是后来开源时才定的名字。当时我就意识到问题从来不在“要不要写 CLI”而在于“怎么让 CLI 不再是临时胶水而是可组合、可继承、可测试的一等公民”。CLI-Anything 正是这个认知落地后的产物它不提供开箱即用的功能比如“一键部署”或“自动爬虫”它提供的是构建任意 CLI 的骨架、契约与扩展机制。它的核心关键词不是click而是command registry、context injection、plugin manifest和help composition。它解决的不是“如何执行一个命令”而是“如何让一百个命令共享同一套生命周期管理、配置加载、错误处理和文档生成逻辑”。这解释了为什么你在热搜词里反复看到codex cli、claude cli、minimax code cli——它们全是具体功能型 CLI而 CLI-Anything 是让它们能被快速、一致、可持续地构建出来的底层框架。它和click的关系就像 React 和 DOM API 的关系click是基础砖块CLI-Anything 是预制好的户型模块水电图纸承重墙标准。你不用从click.command()开始写而是从cli.command()开始后者自动注入日志上下文、自动加载.env、自动绑定--verbose全局开关、自动生成--help的子命令树结构。这不是语法糖是工程约束力。提示CLI-Anything 的设计哲学第一条就是“拒绝隐式依赖”。它不自动扫描commands/目录也不靠文件名约定加载插件。每个命令必须显式注册每个插件必须声明requires [click8.0, requests2.28]。这看起来多了一步但当你在 CI 中看到pip install -e .失败时能立刻定位到是auth-plugin没声明对pydantic的依赖而不是在凌晨三点翻三页日志找ImportError: cannot import name BaseModel。2. 为什么不用 argparse 或 fireCLI-Anything 的三层抽象设计很多人第一反应是“argparse不够用fire不香”——这问题我被问过至少四十七次。答案不是“不够”而是“不适合规模化协作”。让我用真实场景拆解这三层抽象2.1 第一层命令注册与发现机制解决“谁在提供命令”argparse没有注册中心。你写parser.add_argument()它就存在删掉那行它就消失。没有元数据没有依赖声明没有启用/禁用开关。fire更激进它直接把模块属性当命令python mytool.py upload --file data.csv实际调用的是mytool.upload()。问题在于如果upload()函数内部调用了另一个未声明的validate_file()而这个函数又依赖openpyxl那么pip install mytool后upload命令会直接报错但--help里根本看不到这个依赖。CLI-Anything 强制要求命令通过装饰器注册# commands/upload.py from cli_anything import command, option command( nameupload, helpUpload files to remote storage, requires[boto31.26, openpyxl3.1] ) option(--file, -f, requiredTrue, typestr, helpPath to file) option(--bucket, -b, defaultdefault-bucket, helpTarget S3 bucket) def upload(file: str, bucket: str): # 实际逻辑 pass关键点在于requires字段。CLI-Anything 在启动时会解析所有command装饰器收集全部requires列表然后调用pip check验证环境完整性。如果缺失openpyxl它不会等到upload()执行时才崩溃而是在cli-anywhere --help阶段就提示⚠️ Command upload requires missing packages: - openpyxl3.1 (not installed) Run pip install openpyxl3.1 to enable this command.这彻底改变了调试节奏——从“运行时报错→查源码→装包→重试”变成“首次查看帮助→按提示装包→立即可用”。2.2 第二层上下文注入与生命周期管理解决“命令之间如何共享状态”fire把函数当黑盒执行argparse的parse_args()返回一个Namespace对象但你得自己把它传给每个函数。结果就是日志配置重复写三次数据库连接对象在每个命令里都sqlite3.connect()一遍配置文件路径硬编码在五个地方。CLI-Anything 定义了标准上下文协议# context.py from typing import Optional, Dict, Any import logging class CLIContext: def __init__(self, verbose: bool False): self.verbose verbose self.logger logging.getLogger(cli-anywhere) self.config: Dict[str, Any] {} self.db_conn None def load_config(self, path: str): # 统一配置加载逻辑 pass def get_db_connection(self) - sqlite3.Connection: if not self.db_conn: self.db_conn sqlite3.connect(self.config.get(db_path, :memory:)) return self.db_conn然后所有命令自动接收该上下文command(namelist-users) def list_users(ctx: CLIContext): # ctx 自动注入 conn ctx.get_db_connection() cursor conn.execute(SELECT * FROM users) for row in cursor.fetchall(): print(row)更关键的是CLI-Anything 支持上下文继承链。主命令cli-anywhere初始化CLIContext(verboseTrue)子命令cli-anywhere db migrate会收到同一个实例而cli-anywhere db migrate --dry-run可以在子命令中修改ctx.dry_run True父命令依然保持原状态。这种细粒度控制在argparse里需要手动传递args对象在fire里根本不可控。2.3 第三层帮助系统与文档合成解决“用户怎么知道命令怎么用”argparse的--help是静态字符串fire的帮助是函数 docstring 的简单渲染。但真实 CLI 需要动态帮助比如--format选项的可选值取决于当前安装的插件json,yaml,xlsx--target的补全列表来自远程 API。CLI-Anything 将帮助生成拆分为三阶段静态定义装饰器中的help字符串动态补充命令执行前调用get_help_extras()方法合成渲染统一模板引擎Jinja2生成最终文本。例如sync-db命令command(namesync-db) def sync_db(ctx: CLIContext): pass def get_help_extras(): # 动态获取可用目标 targets [] for plugin in ctx.plugins: if hasattr(plugin, get_sync_targets): targets.extend(plugin.get_sync_targets()) return { available_targets: , .join(targets), example_usage: fcli-anywhere sync-db --target {targets[0] if targets else local} }当用户执行cli-anywhere sync-db --helpCLI-Anything 会自动调用get_help_extras()将返回字典注入帮助模板最终输出Sync database to target environment. Available targets: local, staging, production, aws-rds-us-east-1 Example usage: cli-anywhere sync-db --target staging这解决了argparse帮助文档过期、fire帮助无法反映运行时状态的根本缺陷。3. 从零搭建你的第一个 CLI-Anything 项目实操步骤与避坑清单现在我们动手创建一个最小可行 CLIweather-cli它能查询城市天气并支持插件扩展比如未来加一个“发送邮件提醒”插件。整个过程严格遵循 CLI-Anything 的工程约束我会标出每一步背后的原理和常见陷阱。3.1 初始化项目结构与依赖声明首先创建标准 Python 包结构mkdir weather-cli cd weather-cli python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install --upgrade pip关键点来了不要直接pip install click。CLI-Anything 要求所有依赖通过pyproject.toml声明且必须区分dependencies和optional-dependencies。创建pyproject.toml[build-system] requires [setuptools45, wheel] build-backend setuptools.build_meta [project] name weather-cli version 0.1.0 description A CLI for weather queries requires-python 3.8 dependencies [ click8.0, requests2.28, cli-anything0.5.0, # 注意这是核心框架 ] [project.optional-dependencies] email [smtplib, email-validator] # 插件依赖不默认安装注意cli-anything本身不包含任何业务逻辑它只是一个框架。你必须显式声明dependencies中的cli-anything否则command装饰器无法识别。很多新手在这里卡住以为click装了就能用 CLI-Anything结果运行cli-anywhere --help报ModuleNotFoundError: No module named cli_anything。3.2 编写主入口与基础命令创建weather_cli/__init__.py空文件仅作包标识和weather_cli/cli.py# weather_cli/cli.py from cli_anything import CLIApp, command, option import requests # 初始化应用实例 app CLIApp( nameweather-cli, version0.1.0, descriptionGet current weather for any city ) command( namecurrent, helpShow current weather for a city, requires[requests2.28] ) option(--city, -c, requiredTrue, typestr, helpCity name (e.g., Beijing)) option(--units, -u, defaultmetric, typestr, helpTemperature units: metric|imperial|kelvin) def current(city: str, units: str): Fetch and display current weather. try: # 使用 CLI-Anything 提供的内置 HTTP 客户端自动带超时和重试 response app.http.get( https://api.openweathermap.org/data/2.5/weather, params{q: city, appid: YOUR_API_KEY, units: units} ) data response.json() print(fWeather in {city}: {data[weather][0][description]}) print(fTemperature: {data[main][temp]}°C) except requests.exceptions.RequestException as e: app.error(fFailed to fetch weather: {e}) # 注册命令到应用 app.register_command(current)重点解析app.http.get()不是requests.get()的简单封装。CLI-Anything 的http客户端内置了默认 10 秒超时可全局配置3 次指数退避重试网络抖动时自动重试自动添加User-Agent头避免被 API 拒绝错误统一转为CLIError便于上层捕获。app.error()是标准化错误输出。它会以红色字体打印错误消息如果--verbose开启追加完整 traceback退出码设为 1符合 Unix 传统。3.3 创建可安装的 CLI 入口点在pyproject.toml中添加入口点配置[project.entry-points.console_scripts] weather weather_cli.cli:app.run这告诉pip install -e .把weather命令映射到weather_cli.cli.app.run()。注意app.run()是 CLI-Anything 提供的主循环方法它会解析命令行参数加载所有已注册命令验证依赖完整性注入上下文调用目标命令。3.4 安装与验证踩过的坑与解决方案执行安装pip install -e .此时可能遇到的第一个坑ERROR: Could not find a version that satisfies the requirement cli-anything0.5.0原因cli-anything尚未发布到 PyPI。解决方案是先安装开发版pip install githttps://github.com/cli-anything/cli-anything.gitmain第二个坑出现在运行时weather current --city Beijing # Error: Missing API key. Set WEATHER_API_KEY environment variable.这是 CLI-Anything 的安全设计敏感配置如 API Key必须通过环境变量或配置文件提供绝不允许硬编码。修复方式export WEATHER_API_KEYyour_actual_key_here weather current --city Beijing第三个坑是帮助文档不显示weather --help # Shows only basic usage, no current command listed原因app.register_command(current)必须在app.run()之前执行且current函数必须在app实例创建之后定义。检查cli.py文件顺序确保app CLIApp(...)在最顶部command装饰器在中间app.register_command()在底部。3.5 添加插件支持让 CLI 可扩展现在我们添加一个邮件插件。创建weather_cli/plugins/email_plugin.py# weather_cli/plugins/email_plugin.py from cli_anything import plugin, option import smtplib from email.mime.text import MIMEText plugin( nameemail-alert, descriptionSend weather report via email, requires[smtplib, email-validator] ) option(--to, -t, requiredTrue, typestr, helpRecipient email address) option(--subject, -s, defaultWeather Report, helpEmail subject) def send_email(to: str, subject: str, ctx): Send current weather as email. # 从上下文中获取最近一次查询结果演示上下文共享 if not hasattr(ctx, last_weather): raise ValueError(No weather data available. Run weather current first.) msg MIMEText(fWeather: {ctx.last_weather[description]}\nTemp: {ctx.last_weather[temp]}°C) msg[Subject] subject msg[From] weathercli msg[To] to with smtplib.SMTP(localhost) as server: server.send_message(msg) print(fEmail sent to {to})然后在cli.py中启用插件# 在 app CLIApp(...) 之后添加 app.load_plugins_from_package(weather_cli.plugins)关键点load_plugins_from_package()会扫描指定包下的所有模块自动发现plugin装饰的函数并将其注册为子命令。用户执行weather email-alert --to userexample.com时CLI-Anything 会验证smtplib是否已安装因requires声明将send_email注册为email-alert子命令在weather --help中显示该命令。注意插件模块必须放在plugins/子目录下且__init__.py文件不能为空需包含from .email_plugin import *或类似导入。否则load_plugins_from_package()扫描不到模块。4. CLI-Anything 的真实生产约束为什么它能在企业级项目中存活三年我在上一家公司推动 CLI-Anything 落地时CTO 提出三个尖锐问题“它比click多出的 200 行代码能带来什么不可替代的价值”、“如果团队新人不理解上下文注入会不会写出更难维护的代码”、“当我们要对接内部认证系统时它能否无缝集成”——这些问题的答案构成了 CLI-Anything 在生产环境存活三年的核心约束。以下是我用血泪经验总结的四条铁律4.1 铁律一所有命令必须可独立测试且测试覆盖率强制 ≥85%CLI-Anything 不提供测试工具但它定义了测试契约。每个command函数必须能脱离 CLI 环境单独调用。看current命令的测试用例# tests/test_current.py import pytest from weather_cli.cli import current from unittest.mock import patch, MagicMock def test_current_success(): # 模拟 HTTP 响应 mock_response MagicMock() mock_response.json.return_value { weather: [{description: clear sky}], main: {temp: 25.5} } with patch(weather_cli.cli.app.http.get, return_valuemock_response): # 直接调用函数不经过 CLI 解析 result current(cityBeijing, unitsmetric) assert result is None # 命令无返回值只打印输出 def test_current_failure(): with patch(weather_cli.cli.app.http.get) as mock_get: mock_get.side_effect Exception(Network error) with pytest.raises(Exception, matchNetwork error): current(cityBeijing, unitsmetric)关键点测试不依赖click.testing.CliRunner因为 CLI-Anything 的命令本质是普通函数。current(cityBeijing, unitsmetric)和cli-anywhere current --city Beijing --units metric应该产生相同副作用打印、HTTP 请求。这使得单元测试速度极快毫秒级且能精准定位问题在业务逻辑还是 CLI 层。实战教训曾有个团队把数据库连接逻辑写在command函数内部导致测试时每次都要启动 SQLite 内存库。后来我们强制要求所有外部依赖DB、HTTP、FS必须通过ctx参数注入测试时传入 Mock 对象即可。这条规则让平均测试执行时间从 12 秒降到 0.3 秒。4.2 铁律二配置必须分层且禁止跨层覆盖CLI-Anything 定义三级配置优先级环境变量最高优先级WEATHER_API_KEY用户配置文件中优先级~/.weather-cli/config.toml默认值最低优先级option(defaultmetric)。但严禁环境变量覆盖用户配置文件的结构。例如用户配置文件定义# ~/.weather-cli/config.toml [api] timeout 30 retry 5 [output] format json环境变量只能覆盖叶子节点WEATHER_API_TIMEOUT60有效但WEATHER_API{timeout:60}无效——CLI-Anything 会忽略整个字符串因为它无法解析为 TOML 结构。这防止了配置混乱运维人员设置环境变量时不会意外破坏开发者的本地配置结构。4.3 铁律三错误必须分类且每类错误对应唯一退出码CLI-Anything 内置错误类型体系CLIError退出码 1用户输入错误如--city缺失ConfigError退出码 2配置文件损坏或缺失NetworkError退出码 3HTTP 请求失败PluginError退出码 4插件加载失败。每个错误类型都有标准消息格式❌ ConfigError: Failed to load ~/.weather-cli/config.toml Reason: Invalid TOML syntax at line 5, column 3 Hint: Run weather config init to generate a valid template这种结构化错误让自动化脚本能精准判断失败原因。例如 CI 流程可以这样处理if ! weather current --city Tokyo; then case $? in 1) echo User error - fix command args; exit 1;; 2) echo Config issue - regenerate config; weather config init;; 3) echo Network outage - retry later; exit 0;; *) echo Unknown error; exit 1;; esac fi4.4 铁律四插件必须声明能力契约而非仅依赖声明requires[smtplib]只保证包存在但不保证功能可用。CLI-Anything 要求插件实现get_capabilities()方法# weather_cli/plugins/email_plugin.py def get_capabilities(): return { email_provider: smtp, max_recipients: 100, supports_attachments: False }主应用在加载插件后会调用此方法并缓存结果。当用户执行weather email-alert --to groupcompany.com150 个收件人时CLI-Anything 会在调用send_email()前检查if len(recipients) plugin.capabilities[max_recipients]: app.error(fToo many recipients. Plugin supports max {plugin.capabilities[max_recipients]})这比单纯检查smtplib是否安装更进一步——它验证了插件的实际服务能力。我们在对接内部邮件网关时正是靠这个机制避免了因收件人数量超限导致的批量邮件失败。5. CLI-Anything 与生态工具的协同如何不重复造轮子CLI-Anything 不是一个封闭王国它刻意设计为与现有生态工具共生。以下是它与四个关键工具的真实协同模式附带配置片段和效果对比。5.1 与 Click 的关系CLI-Anything 是 Click 的“企业级封装”CLI-Anything 底层完全基于click但它隐藏了click的复杂性。看一个典型对比纯 Click 写法易出错import click click.group() click.option(--verbose, is_flagTrue, helpEnable verbose output) click.pass_context def cli(ctx, verbose): ctx.ensure_object(dict) ctx.obj[verbose] verbose cli.command() click.option(--city, requiredTrue) click.pass_context def current(ctx, city): if ctx.obj[verbose]: print(Debug: fetching weather...) # ...业务逻辑问题click.pass_context必须在每层装饰器中显式传递ctx.obj是弱类型字典IDE 无法提示错误处理需手动raise click.UsageError()。CLI-Anything 写法类型安全from cli_anything import command, option command(namecurrent, helpShow current weather) option(--city, -c, requiredTrue, typestr) def current(city: str, ctx): # ctx 是 CLIContext 类型IDE 可提示 ctx.logger, ctx.config if ctx.verbose: ctx.logger.debug(Fetching weather...) # 自动日志 # ...业务逻辑CLI-Anything 的ctx是强类型对象ctx.verbose是布尔值ctx.logger是logging.Logger实例。这带来的收益是VS Code 中按CtrlSpace能看到所有可用属性类型检查器如 mypy能捕获ctx.nonexistent_attr错误。5.2 与 Poetry 的协同依赖管理的黄金搭档Poetry 是现代 Python 项目的事实标准。CLI-Anything 与 Poetry 的集成体现在pyproject.toml的tool.poetry部分[tool.poetry] name weather-cli version 0.1.0 description Weather CLI built with CLI-Anything authors [Your Name youexample.com] [tool.poetry.dependencies] python ^3.8 click ^8.1 cli-anything ^0.5.0 requests ^2.28 [tool.poetry.group.dev.dependencies] pytest ^7.0 pytest-cov ^4.0 [tool.poetry.group.plugin.dependencies] smtplib ^1.0 # 插件依赖单独分组 [[tool.poetry.source]] name internal-pypi url https://pypi.internal.company.com/simple/ priority explicit关键优势Poetry 的poetry install会自动处理optional-dependencies而 CLI-Anything 的app.load_plugins_from_package()会根据当前安装的依赖动态启用/禁用插件。例如poetry install # 只安装 core 依赖 poetry install --with plugin # 安装 email 插件依赖运行weather --help时CLI-Anything 会检测smtplib是否可用决定是否显示email-alert命令。这实现了真正的“按需加载”避免了传统 CLI 中“插件存在但无法用”的尴尬。5.3 与 VS Code 的深度整合开发体验优化CLI-Anything 提供 VS Code 调试配置模板。在项目根目录创建.vscode/launch.json{ version: 0.2.0, configurations: [ { name: Debug current weather, type: python, request: launch, module: weather_cli.cli, args: [current, --city, Beijing, --verbose], console: integratedTerminal, justMyCode: true, env: { WEATHER_API_KEY: test_key_123 } } ] }配合 CLI-Anything 的--debug标志自动启用pdb开发者能在current()函数内设断点查看ctx对象的所有属性实时修改ctx.config并观察后续命令行为。这比click.testing.CliRunner.invoke()的调试体验好得多——后者需要在测试代码中模拟参数而 CLI-Anything 允许直接调试真实命令流。5.4 与 GitHub Actions 的 CI/CD 流水线保障 CLI 质量我们为 CLI-Anything 项目定制了 GitHub Actions 工作流核心检查项# .github/workflows/ci.yml name: CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: [3.8, 3.9, 3.10] steps: - uses: actions/checkoutv3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-pythonv4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install poetry poetry install - name: Run unit tests run: poetry run pytest tests/ --covweather_cli --cov-reportterm-missing - name: Validate CLI help output run: | # 检查 help 文本是否包含所有注册命令 weather --help | grep -q current weather --help | grep -q email-alert || echo email-alert command missing - name: Check for uncommitted changes run: | # 确保 pyproject.toml 中的 version 与 __version__ 一致 python -c import weather_cli; assert weather_cli.__version__ 0.1.0其中Validate CLI help output步骤至关重要。它确保每次 PR 合并前--help输出与实际命令集一致。我们曾因忘记app.register_command()导致新命令上线后用户查不到帮助这条检查现在成了质量红线。6. CLI-Anything 的边界与演进它不适合做什么以及未来方向坦白说CLI-Anything 不是万能钥匙。它在某些场景下会成为累赘而它的演进也始终围绕“让 CLI 成为可靠基础设施”这一核心而非追逐热点。以下是明确的边界声明和路线图。6.1 明确的不适用场景什么时候该放弃 CLI-Anything场景一单次脚本生命周期 1 周如果你写一个脚本只为临时处理某次数据迁移且确定未来不会再用那么#!/usr/bin/env python3argparse是最优解。CLI-Anything 的项目结构pyproject.toml、包目录、插件机制会增加 5 分钟 setup 时间而收益为零。我的经验法则脚本预期使用次数 3 次或维护者 1 人就别用 CLI-Anything。场景二需要 GUI 交互的 CLICLI-Anything 严格遵循 Unix 哲学输入 → 处理 → 输出。它不提供dialog弹窗、rich进度条或inquirer交互式提问。如果你的需求是“让用户选择菜单项”应该用rich或questionary单独实现然后作为 CLI-Anything 命令的内部逻辑调用。CLI-Anything 的立场是“CLI 是管道不是界面”。场景三实时流式输出如 tail -fCLI-Anything 的命令执行模型是“同步完成”。它不支持async def命令尽管底层click支持因为异步会破坏上下文注入的确定性。如果你需要weather stream --city Tokyo持续推送更新正确做法是用asyncio写一个独立的stream.py模块在 CLI-Anything 命令中调用subprocess.run([python, stream.py, --city, Tokyo])让 CLI-Anything 负责参数解析和错误包装让stream.py负责异步逻辑。6.2 当前核心演进方向稳定性 新功能CLI-Anything 的 GitHub Issues 中92% 是 bug 报告和文档请求仅 8% 是新功能建议。团队明确聚焦三个方向方向一Windows 兼容性加固node_modules\opencode\cli\bin\opencode.exe 与你运行的 windows 版本不兼容这类错误在 Windows 用户中高频出现。CLI-Anything 正在重构二进制打包流程采用pyinstaller生成真正跨平台的weather.exe并内置 Windows SDK 版本检测。目标让pip install weather-cli后weather.exe在 Win7 上 100% 可运行。方向二配置文件 Schema 验证用户常因 TOML 语法错误导致 CLI 启动失败。CLI-Anything 将集成pydantic为~/.weather-cli/config.toml定义严格 Schemafrom pydantic import BaseModel, HttpUrl class Config(BaseModel): api: dict output: dict cache: dict # 自动生成验证错误提示 # Error: config.toml invalid. Field api.timeout must be integer, got 30s方向三插件市场协议标准化目前插件发现依赖包内路径。CLI-Anything 正在设计cli-plugin.json协议允许插件作者发布独立包{ name: weather-email-plugin, version: 0.1.0, cli-anything-version: 0.5.0, commands: [email-alert], requires: [smtplib] }用户执行weather plugin install weather-email-plugin时CLI-Anything 会从 PyPI 下载包验证cli-plugin.json兼容性自动安装依赖注册命令。这将终结“插件安装后不显示”的历史难题。6.3 我的个人体会CLI-Anything 是写给未来自己的情书最后分享一个真实故事。去年我离职前把weather-cli交接给新人。他第一天就问我“为什么current命令里要调用app.http.get()而不是requests.get()” 我没直接回答而是让他看tests/test_current.py里的patch(weather_cli.cli.app.http.get)。他愣了几秒然后笑了“哦这样测试就不用 mock requests 了。”那一刻我意识到CLI-Anything 最大的价值不是它省了多少行代码而是它把工程决策固化为代码契约。app.http.get()不是技术选择是“所有 HTTP 请求必须可 mock”的承诺ctx.logger不是便利是“所有日志必须可配置级别”的约束plugin装饰器不是语法糖是“扩展必须声明能力”的宣言。它不讨好当下它服务未来。当你在深夜修复一个三年前写的 CLI 命令时看到def current(city: str, ctx: CLIContext)这行签名你就知道——那个过去的自己
网站建设高端定制企业官网