chore: 引入 pyright 类型检查清零 + README 工程化章节
pyright(basic 模式)从 15 errors / 10 warnings 降到 0/0: - pyproject: pythonPlatform="All"(termios/msvcrt 双平台分支不再误报), reportUnsupportedDunderAll=false(__init__ 惰性导出属正常写法) - keys.py: 捕获 sys.stdin 局部变量 + 提前返回,消除 fileno() 的 None 收窄告警 - server/app.py: lifespan 改用 @asynccontextmanager 并标注 AsyncIterator[None] - server/actions.py: 显式标注 result: dict[str, Any],消除 isinstance 冗余报错 - server/__main__.py: 用 importlib.util.find_spec 探测 websockets - tests/test_server.py: 同上,uvicorn 探测改为 find_spec - tools/preview_tui.py: TYPE_CHECKING 下引入 RemoteFarm,修正返回类型标注 - farm/tui.py: 取消未使用变量,统一 round/取整写法 其他: - README: 新增第 11 节「工程化:uv + ruff + pyright」,快速开始全部改用 uv sync / uv run;原第 11 节起顺延为 12 - requirements.txt: 由 uv export --no-dev --no-hashes 重新生成(锁定版本) - 清理临时日志文件 验收:ruff check 全通过 / ruff format 26 文件无改动 / pyright 0 错误 / unittest 97 项全部通过
This commit is contained in:
1 parent
fcd4cb4649
commit
d7a7107b0a
11 files changed
+200
-74
No files matched your search
+3
-1
@@ -24,7 +24,9 @@ venv/
|
|||||||
run/
|
run/
|
||||||
saves/
|
saves/
|
||||||
tests/_tmp/
|
tests/_tmp/
|
||||||
_uvpy.txt
|
# 仓库根目录下以 _ 开头的都是临时脚本/日志(补丁脚本、重定向输出、提交信息等),
|
||||||
|
# 用前导斜杠锚定根目录,否则 `_*` 会误伤 farm/__init__.py 这类文件。
|
||||||
|
/_*
|
||||||
|
|
||||||
# --------------------------------------------------------- 编辑器 / 系统
|
# --------------------------------------------------------- 编辑器 / 系统
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
@@ -28,13 +28,13 @@
|
|||||||
## 1. 快速开始
|
## 1. 快速开始
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt # 只有服务端需要第三方依赖
|
uv sync # 建好环境(只有服务端需要第三方依赖)
|
||||||
|
|
||||||
python main.py serve --daemon # ① 后台启动服务器
|
uv run main.py serve --daemon # ① 后台启动服务器
|
||||||
python main.py play --farm 我的农场 # ② 终端里玩(另开一个窗口)
|
uv run main.py play --farm 我的农场 # ② 终端里玩(另开一个窗口)
|
||||||
python main.py bot --farm ai-1 # ③ 让机器人玩家一起经营
|
uv run main.py bot --farm ai-1 # ③ 让机器人玩家一起经营
|
||||||
python main.py status # ④ 看服务器状态
|
uv run main.py status # ④ 看服务器状态
|
||||||
python main.py stop # ⑤ 优雅停机(会把所有牧场存档)
|
uv run main.py stop # ⑤ 优雅停机(会把所有牧场存档)
|
||||||
```
|
```
|
||||||
|
|
||||||
浏览器打开 <http://127.0.0.1:8765/> 就是网页版客户端(可多开几个标签页,用不同牧场名,
|
浏览器打开 <http://127.0.0.1:8765/> 就是网页版客户端(可多开几个标签页,用不同牧场名,
|
||||||
@@ -43,9 +43,12 @@ python main.py stop # ⑤ 优雅停机(会把所有牧场
|
|||||||
不想装任何第三方库、只想单机玩玩:
|
不想装任何第三方库、只想单机玩玩:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python main.py solo # 本地引擎 + 终端界面,不开服务器
|
uv run main.py solo # 本地引擎 + 终端界面,不开服务器
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **没装 uv 也能跑**:把命令里的 `uv run ` 去掉,直接用 `python main.py ...`;
|
||||||
|
> 依赖用 `pip install fastapi 'uvicorn[standard]'` 装。两种方式行为完全一致。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 玩法
|
## 2. 玩法
|
||||||
@@ -68,7 +71,7 @@ python main.py solo # 本地引擎 + 终端界面,不开
|
|||||||
|
|
||||||
## 3. 终端界面
|
## 3. 终端界面
|
||||||
|
|
||||||
`python main.py play` 打开。核心交互:
|
`uv run main.py play` 打开。核心交互:
|
||||||
|
|
||||||
```
|
```
|
||||||
[↑↓←→]选地 [P]播种 [W]浇水 [H]收获 [E]全部收获
|
[↑↓←→]选地 [P]播种 [W]浇水 [H]收获 [E]全部收获
|
||||||
@@ -287,8 +290,8 @@ asyncio.run(main())
|
|||||||
局域网联机:
|
局域网联机:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python main.py serve --daemon --host 0.0.0.0 --port 8765 --token 找个密码
|
uv run main.py serve --daemon --host 0.0.0.0 --port 8765 --token 找个密码
|
||||||
python main.py play --url http://192.168.1.20:8765 --farm alice --token 找个密码
|
uv run main.py play --url http://192.168.1.20:8765 --farm alice --token 找个密码
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -300,9 +303,9 @@ python main.py play --url http://192.168.1.20:8765 --farm alice --token 找个
|
|||||||
| 项目 | 位置 / 命令 |
|
| 项目 | 位置 / 命令 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 进程号 | `run/server.pid`(**由服务进程自己写**,见下方说明) |
|
| 进程号 | `run/server.pid`(**由服务进程自己写**,见下方说明) |
|
||||||
| 日志 | `run/server.log`(`python main.py logs -n 50`) |
|
| 日志 | `run/server.log`(`uv run main.py logs -n 50`) |
|
||||||
| 停止 | `python main.py stop`(先走 HTTP 优雅停机,失败再发信号) |
|
| 停止 | `uv run main.py stop`(先走 HTTP 优雅停机,失败再发信号) |
|
||||||
| 状态 | `python main.py status` |
|
| 状态 | `uv run main.py status` |
|
||||||
| 存档 | `saves/<牧场名>.json`(纯 JSON,可直接阅读分析) |
|
| 存档 | `saves/<牧场名>.json`(纯 JSON,可直接阅读分析) |
|
||||||
| 接口文档 | <http://127.0.0.1:8765/docs> |
|
| 接口文档 | <http://127.0.0.1:8765/docs> |
|
||||||
|
|
||||||
@@ -321,6 +324,10 @@ python main.py play --url http://192.168.1.20:8765 --farm alice --token 找个
|
|||||||
## 9. 代码结构
|
## 9. 代码结构
|
||||||
|
|
||||||
```
|
```
|
||||||
|
pyproject.toml 唯一真源:依赖、Python 版本、ruff 与 pyright 规则
|
||||||
|
uv.lock 锁定的依赖版本(可复现构建,应当提交)
|
||||||
|
.python-version 钉住解释器版本
|
||||||
|
.gitattributes 行尾统一为 LF,避免 Windows 上 CRLF 反复抖动
|
||||||
main.py 统一入口:serve / play / do / bot / status ...
|
main.py 统一入口:serve / play / do / bot / status ...
|
||||||
ai_player.py 外挂式机器人玩家(只用接口,不 import 任何游戏规则)
|
ai_player.py 外挂式机器人玩家(只用接口,不 import 任何游戏规则)
|
||||||
farm/
|
farm/
|
||||||
@@ -358,9 +365,9 @@ tests/
|
|||||||
## 10. 测试
|
## 10. 测试
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m unittest discover -s tests -v # 全部(97 项)
|
uv run python -m unittest discover -s tests -v # 全部(97 项)
|
||||||
python -m unittest tests.test_engine # 只测游戏规则,不需要 FastAPI
|
uv run python -m unittest tests.test_engine # 只测游戏规则,不需要 FastAPI
|
||||||
python -m unittest tests.test_server.TestLiveServer # 真起服务器 + 真 WebSocket
|
uv run python -m unittest tests.test_server.TestLiveServer # 真起服务器 + 真 WebSocket
|
||||||
```
|
```
|
||||||
|
|
||||||
覆盖范围:
|
覆盖范围:
|
||||||
@@ -386,7 +393,63 @@ serve --daemon → status 显示正确 pid → actions 列出 13 个候选动作
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. 边界处理与设计取舍
|
## 11. 工程化:uv + ruff + pyright
|
||||||
|
|
||||||
|
依赖版本、Python 版本、lint 规则、类型检查规则**全部**写在 `pyproject.toml` 里——
|
||||||
|
单一真源,不再存在"requirements 与实际环境不一致"这种事。`uv.lock` 记录完整解析结果,
|
||||||
|
提交进版本库,换台机器 `uv sync` 得到的是同一个环境。
|
||||||
|
|
||||||
|
| 目的 | 命令 |
|
||||||
|
| --- | --- |
|
||||||
|
| 同步环境(含开发依赖) | `uv sync` |
|
||||||
|
| 只装运行依赖 | `uv sync --no-dev` |
|
||||||
|
| 添加依赖 | `uv add <包>` / `uv add --dev <包>` |
|
||||||
|
| 升级锁定版本 | `uv lock --upgrade` |
|
||||||
|
| 导出给 pip 用户 | `uv export --no-dev --no-hashes -o requirements.txt` |
|
||||||
|
| 静态检查 | `uv run ruff check .` |
|
||||||
|
| 自动修复 | `uv run ruff check . --fix` |
|
||||||
|
| 格式化 | `uv run ruff format .` |
|
||||||
|
| 类型检查 | `uv run pyright` |
|
||||||
|
|
||||||
|
### 为什么是 uv
|
||||||
|
|
||||||
|
* **一个工具顶四个**:解释器管理、依赖解析、虚拟环境、命令运行都在里面,
|
||||||
|
不需要 pyenv + pip + venv + pip-tools 拼装;
|
||||||
|
* **可复现**:`uv.lock` 精确到哈希,跨机器一致;
|
||||||
|
* **不用激活环境**:`uv run <命令>` 自动使用项目环境,少一类"我明明装了"的事故。
|
||||||
|
|
||||||
|
### ruff 的取舍
|
||||||
|
|
||||||
|
`ruff check` 管正确性,`ruff format` 管排版,一个工具替代
|
||||||
|
Black + isort + flake8 三件套(避免"格式化完 lint 又报"的拉锯)。
|
||||||
|
|
||||||
|
启用 `E W F I UP B C4 SIM RET PERF RUF`。两个刻意的**不**:
|
||||||
|
|
||||||
|
* **不启用 PTH**(`os.path` → `pathlib`)。本项目的路径大多是"字符串身份":
|
||||||
|
要拼进 `subprocess` 命令行、写进 JSON 存档、打印给用户看。全换成 `Path`
|
||||||
|
只是把字符串包一层再拆一层,换来 70 处纯风格改动,反而淹没真正的问题。
|
||||||
|
* **忽略 RUF001/002/003**。它们会把中文全角标点当成"含糊的 Unicode 字符",
|
||||||
|
但这里的注释与界面文案本来就是中文,全角括号是正确写法。
|
||||||
|
|
||||||
|
另外忽略 `PERF203`(循环里的 `try`):保留下来的两处都是**逐项容错**语义
|
||||||
|
(逐个牧场存档要立刻上报失败、收获全部要跳过失败地块)。把 `try` 提到循环外
|
||||||
|
会让"一项失败不影响其它项"这个行为消失——那是错的优化。
|
||||||
|
|
||||||
|
`line-length = 100` 是**贴着现有代码**定的:全项目只有 18 行超过 100 列;
|
||||||
|
若采用默认的 88,需要额外重排 122 行,diff 噪音会淹没真实改动。
|
||||||
|
|
||||||
|
### pyright 的取舍
|
||||||
|
|
||||||
|
用 `basic` 模式。目标不是"消灭所有 Unknown",而是**能在 CI 里拦住真 bug**:
|
||||||
|
None 误用、属性不存在、参数类型不符。本项目的状态快照是 `dict[str, Any]` 结构,
|
||||||
|
开 `strict` 去追每个 `Unknown` 只会得到一堆噪音。
|
||||||
|
|
||||||
|
配合 `pythonVersion = "3.10"`,守住 `requires-python` 的下限——
|
||||||
|
哪天不小心用上 3.11+ 独有语法,会立刻报错。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 边界处理与设计取舍
|
||||||
|
|
||||||
**输入校验**(唯一入口 `server/actions.py`):
|
**输入校验**(唯一入口 `server/actions.py`):
|
||||||
|
|
||||||
|
|||||||
+25
-16
@@ -9,6 +9,7 @@ TUI 会退化为无输入的只读模式。
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -40,28 +41,36 @@ class KeyReader:
|
|||||||
"""把终端的原始字节流转换成按键名。"""
|
"""把终端的原始字节流转换成按键名。"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.available = sys.stdin is not None and sys.stdin.isatty()
|
|
||||||
self._fd: int | None = None
|
self._fd: int | None = None
|
||||||
self._saved_term = None
|
self._saved_term = None
|
||||||
|
|
||||||
if self.available and not IS_WINDOWS:
|
# 先把 stdin 取到局部变量:下面每处都要用它。同时它的类型被收窄到非 None,
|
||||||
try:
|
# 省掉反复 `sys.stdin.xxx` 时"可能是 None"的隐患。
|
||||||
import termios
|
stdin = sys.stdin
|
||||||
import tty
|
if stdin is None or not stdin.isatty():
|
||||||
|
self.available = False
|
||||||
|
return
|
||||||
|
self.available = True
|
||||||
|
|
||||||
self._fd = sys.stdin.fileno()
|
if IS_WINDOWS:
|
||||||
self._saved_term = termios.tcgetattr(self._fd)
|
# Windows 靠 msvcrt 做非阻塞读;拿不到就认为终端不可用
|
||||||
# cbreak: 立即返回按键,但保留 Ctrl-C 等信号
|
if importlib.util.find_spec("msvcrt") is None:
|
||||||
tty.setcbreak(self._fd)
|
|
||||||
except Exception:
|
|
||||||
self.available = False
|
self.available = False
|
||||||
self._saved_term = None
|
return
|
||||||
|
|
||||||
if self.available and IS_WINDOWS:
|
try:
|
||||||
try:
|
import termios
|
||||||
import msvcrt # noqa: F401
|
import tty
|
||||||
except Exception:
|
|
||||||
self.available = False
|
# 用局部变量:self._fd 声明为 int | None,直接传进去会被判为可能为 None
|
||||||
|
fd = stdin.fileno()
|
||||||
|
self._fd = fd
|
||||||
|
self._saved_term = termios.tcgetattr(fd)
|
||||||
|
# cbreak: 立即返回按键,但保留 Ctrl-C 等信号
|
||||||
|
tty.setcbreak(fd)
|
||||||
|
except Exception:
|
||||||
|
self.available = False
|
||||||
|
self._saved_term = None
|
||||||
|
|
||||||
# ------------------------------------------------------------------ 读取
|
# ------------------------------------------------------------------ 读取
|
||||||
def poll(self) -> str | None:
|
def poll(self) -> str | None:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import importlib.util
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -97,12 +98,8 @@ def main(argv=None) -> int:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
print(dependency_hint(), file=sys.stderr)
|
print(dependency_hint(), file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
try:
|
# 只探测"在不在",不真的导入——uvicorn 需要它才能完成 WebSocket 协议升级
|
||||||
import websockets # noqa: F401 只是探测,uvicorn 需要它来做 WS 协议升级
|
ws_ok = importlib.util.find_spec("websockets") is not None
|
||||||
|
|
||||||
ws_ok = True
|
|
||||||
except ImportError:
|
|
||||||
ws_ok = False
|
|
||||||
|
|
||||||
from .app import ServerConfig, create_app
|
from .app import ServerConfig, create_app
|
||||||
|
|
||||||
|
|||||||
@@ -258,9 +258,7 @@ def describe_actions() -> list[dict[str, Any]]:
|
|||||||
return [ACTIONS[name].to_dict() for name in action_names()]
|
return [ACTIONS[name].to_dict() for name in action_names()]
|
||||||
|
|
||||||
|
|
||||||
def run_action(
|
def run_action(engine: FarmEngine, name: Any, params: Any = None) -> dict[str, Any]:
|
||||||
engine: FarmEngine, name: Any, params: dict[str, Any] | None = None
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""校验参数并执行动作。
|
"""校验参数并执行动作。
|
||||||
|
|
||||||
这是所有入口的**唯一**执行通道,因此输入校验只需要在这里做一次。
|
这是所有入口的**唯一**执行通道,因此输入校验只需要在这里做一次。
|
||||||
@@ -276,6 +274,8 @@ def run_action(
|
|||||||
)
|
)
|
||||||
if params is None:
|
if params is None:
|
||||||
params = {}
|
params = {}
|
||||||
|
# params 来自外部 JSON,类型标注是 Any 而不是 dict——这里的检查是**真的**校验,
|
||||||
|
# 不是多余防御(`params` 完全可能就是数组或字符串)
|
||||||
if not isinstance(params, dict):
|
if not isinstance(params, dict):
|
||||||
raise GameError("bad_param", "params 必须是对象")
|
raise GameError("bad_param", "params 必须是对象")
|
||||||
|
|
||||||
@@ -297,9 +297,10 @@ def run_action(
|
|||||||
elif spec.default is not None:
|
elif spec.default is not None:
|
||||||
clean[spec.name] = spec.default
|
clean[spec.name] = spec.default
|
||||||
|
|
||||||
result = action.handler(engine, clean)
|
# handler 声明返回 dict,但这里是所有入口的唯一执行通道,仍然兜一层:
|
||||||
if not isinstance(result, dict):
|
# 万一以后有 handler 返回了别的类型,也不会污染接口格式
|
||||||
result = {"message": str(result)}
|
raw: Any = action.handler(engine, clean)
|
||||||
|
result: dict[str, Any] = raw if isinstance(raw, dict) else {"message": str(raw)}
|
||||||
result.setdefault("ok", True)
|
result.setdefault("ok", True)
|
||||||
result["action"] = key
|
result["action"] = key
|
||||||
return result
|
return result
|
||||||
+5
-2
@@ -18,7 +18,7 @@ import asyncio
|
|||||||
import contextlib
|
import contextlib
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from collections.abc import Callable
|
from collections.abc import AsyncIterator, Callable
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -164,7 +164,10 @@ def create_app(cfg: ServerConfig | None = None) -> FastAPI:
|
|||||||
started_at = time.time()
|
started_at = time.time()
|
||||||
counters = {"requests": 0, "actions": 0, "ws_messages": 0, "errors": 0}
|
counters = {"requests": 0, "actions": 0, "ws_messages": 0, "errors": 0}
|
||||||
|
|
||||||
async def lifespan(_app: FastAPI) -> Any:
|
# 标注成 asynccontextmanager:FastAPI 的 lifespan 参数要的就是
|
||||||
|
# "返回异步上下文管理器",裸 async generator 在类型上并不匹配
|
||||||
|
@contextlib.asynccontextmanager
|
||||||
|
async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
|
||||||
clock.start()
|
clock.start()
|
||||||
publisher.start()
|
publisher.start()
|
||||||
# 端口已经绑定成功(uvicorn 先 bind 再跑 lifespan),可以对外宣告"我在了"
|
# 端口已经绑定成功(uvicorn 先 bind 再跑 lifespan),可以对外宣告"我在了"
|
||||||
|
|||||||
+6
-3
@@ -198,9 +198,12 @@ class TuiApp:
|
|||||||
# ================================================================ 生命周期
|
# ================================================================ 生命周期
|
||||||
def _enter(self) -> None:
|
def _enter(self) -> None:
|
||||||
if enable_ansi() and os.name == "nt":
|
if enable_ansi() and os.name == "nt":
|
||||||
# 个别终端不支持重新配置,失败就按原编码继续
|
# 只有 TextIOWrapper 才有 reconfigure;被重定向时可能拿到别的流。
|
||||||
with contextlib.suppress(Exception):
|
# 取不到就按原编码继续,不影响后续渲染。
|
||||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
reconfigure = getattr(sys.stdout, "reconfigure", None)
|
||||||
|
if reconfigure is not None:
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
reconfigure(encoding="utf-8", errors="replace")
|
||||||
sys.stdout.write("\x1b[?25l\x1b[2J\x1b[H")
|
sys.stdout.write("\x1b[?25l\x1b[2J\x1b[H")
|
||||||
sys.stdout.write(C_RESET)
|
sys.stdout.write(C_RESET)
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|||||||
@@ -112,7 +112,13 @@ line-ending = "auto" # Windows/Linux 混用时不要强改行尾,避免
|
|||||||
include = ["farm", "tests", "tools", "main.py", "ai_player.py"]
|
include = ["farm", "tests", "tools", "main.py", "ai_player.py"]
|
||||||
exclude = [".venv", "**/__pycache__", ".ruff_cache", "saves", "run", "tests/_tmp"]
|
exclude = [".venv", "**/__pycache__", ".ruff_cache", "saves", "run", "tests/_tmp"]
|
||||||
pythonVersion = "3.10"
|
pythonVersion = "3.10"
|
||||||
|
# 代码里有 termios / msvcrt 这类平台专属分支。只按当前平台分析,
|
||||||
|
# 会把另一侧的调用误报成"模块没有这个属性",所以让 pyright 同时考虑所有平台。
|
||||||
|
pythonPlatform = "All"
|
||||||
typeCheckingMode = "basic"
|
typeCheckingMode = "basic"
|
||||||
reportMissingTypeStubs = false # 让"没装 third-party 存根"保持安静
|
reportMissingTypeStubs = false # 让"没装 third-party 存根"保持安静
|
||||||
reportUnusedImport = true
|
reportUnusedImport = true
|
||||||
reportUnnecessaryIsInstance = true
|
reportUnnecessaryIsInstance = true
|
||||||
|
# __all__ 里写子模块名是 `farm/__init__.py` 惰性导出的正常写法,
|
||||||
|
# pyright 只认模块里真实存在的名字,这条对我们是纯噪音
|
||||||
|
reportUnsupportedDunderAll = false
|
||||||
+56
-15
@@ -1,15 +1,56 @@
|
|||||||
# SimpleFarm 运行依赖
|
# This file was autogenerated by uv via the following command:
|
||||||
#
|
# uv export --no-dev --no-hashes --output-file requirements.txt
|
||||||
# 服务器需要:
|
annotated-doc==0.0.5
|
||||||
fastapi>=0.110
|
# via fastapi
|
||||||
uvicorn[standard]>=0.27 # 含 websockets,WebSocket 实时通道需要它
|
annotated-types==0.8.0
|
||||||
|
# via pydantic
|
||||||
# 只有跑测试才需要(TestClient 依赖 httpx):
|
anyio==4.15.1
|
||||||
# httpx>=0.27
|
# via
|
||||||
#
|
# starlette
|
||||||
# 装一次就好:
|
# watchfiles
|
||||||
# pip install -r requirements.txt
|
click==8.5.0
|
||||||
#
|
# via uvicorn
|
||||||
# 注意:终端界面(TUI)与命令行客户端只用标准库,
|
exceptiongroup==1.3.1 ; python_full_version < '3.11'
|
||||||
# 所以即使不装上面这些,`python main.py solo / play / do` 依然能跑;
|
# via anyio
|
||||||
# 只有 `python main.py serve`(服务端)需要 FastAPI。
|
fastapi==0.141.1
|
||||||
|
# via simplefarm
|
||||||
|
h11==0.16.0
|
||||||
|
# via uvicorn
|
||||||
|
httptools==0.8.0
|
||||||
|
# via uvicorn
|
||||||
|
idna==3.20
|
||||||
|
# via anyio
|
||||||
|
pydantic==2.13.5
|
||||||
|
# via fastapi
|
||||||
|
pydantic-core==2.46.5
|
||||||
|
# via pydantic
|
||||||
|
python-dotenv==1.2.3
|
||||||
|
# via uvicorn
|
||||||
|
pyyaml==6.0.3
|
||||||
|
# via uvicorn
|
||||||
|
starlette==1.6.0
|
||||||
|
# via fastapi
|
||||||
|
typing-extensions==4.16.0
|
||||||
|
# via
|
||||||
|
# anyio
|
||||||
|
# exceptiongroup
|
||||||
|
# fastapi
|
||||||
|
# pydantic
|
||||||
|
# pydantic-core
|
||||||
|
# starlette
|
||||||
|
# typing-inspection
|
||||||
|
# uvicorn
|
||||||
|
typing-inspection==0.4.4
|
||||||
|
# via
|
||||||
|
# fastapi
|
||||||
|
# pydantic
|
||||||
|
uvicorn==0.53.0
|
||||||
|
# via simplefarm
|
||||||
|
uvloop==0.22.1 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'
|
||||||
|
# via uvicorn
|
||||||
|
watchfiles==1.2.0
|
||||||
|
# via uvicorn
|
||||||
|
websockets==16.1.1 ; python_full_version < '3.11'
|
||||||
|
# via uvicorn
|
||||||
|
websockets==17.1 ; python_full_version >= '3.11'
|
||||||
|
# via uvicorn
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
@@ -28,19 +29,15 @@ from farm.server.actions import ACTIONS, describe_actions, run_action
|
|||||||
from farm.server.session import SessionManager, validate_farm_id
|
from farm.server.session import SessionManager, validate_farm_id
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import fastapi # noqa: F401
|
# 能导入 TestClient 就说明 fastapi 在,不需要再单独 import 一次
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
HAS_FASTAPI = True
|
HAS_FASTAPI = True
|
||||||
except ImportError: # pragma: no cover - 取决于环境
|
except ImportError: # pragma: no cover - 取决于环境
|
||||||
HAS_FASTAPI = False
|
HAS_FASTAPI = False
|
||||||
|
|
||||||
try:
|
# uvicorn 只需要知道"在不在",不必真的导入(导入它是 TestLiveServer 里的事)
|
||||||
import uvicorn # noqa: F401
|
HAS_UVICORN = importlib.util.find_spec("uvicorn") is not None
|
||||||
|
|
||||||
HAS_UVICORN = True
|
|
||||||
except ImportError: # pragma: no cover
|
|
||||||
HAS_UVICORN = False
|
|
||||||
|
|
||||||
|
|
||||||
def free_port() -> int:
|
def free_port() -> int:
|
||||||
|
|||||||
@@ -23,16 +23,20 @@ import os
|
|||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from farm.engine import FarmEngine
|
from farm.engine import FarmEngine
|
||||||
from farm.tui import TuiApp
|
from farm.tui import TuiApp
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # 只在类型检查时导入,运行时不引入依赖
|
||||||
|
from farm.client import RemoteFarm
|
||||||
|
|
||||||
SCENES = ("empty", "mixed", "rich", "menu", "help")
|
SCENES = ("empty", "mixed", "rich", "menu", "help")
|
||||||
|
|
||||||
|
|
||||||
def build_remote(url: str, farm: str, token: str | None) -> tuple[object, TuiApp]:
|
def build_remote(url: str, farm: str, token: str | None) -> tuple[RemoteFarm, TuiApp]:
|
||||||
"""连到真实服务器,取一份现状来渲染。"""
|
"""连到真实服务器,取一份现状来渲染。"""
|
||||||
from farm.client import RemoteFarm
|
from farm.client import RemoteFarm
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user