style: 引入 ruff,474 项告警清零

配置见 pyproject.toml:
- line-length=100:贴着现有代码定。若用默认的 88 会重排 122 行,
  噪音淹没真正有价值的修复。
- 启用 E/W/F/I/UP/B/C4/SIM/RET/PERF/RUF;**不**启用 PTH。因为本项目的路径
  多是"字符串身份"——要塞进 subprocess 命令行、写进 JSON 存档、打印给用户,
  全换 pathlib 只是包一层再拆一层,换来 70 处纯风格改动。
- 忽略 RUF001/002/003:中文全角标点是正确写法,不是"歧义字符"。
- 忽略 PERF203:剩下两处都是"逐项容错"语义(逐个牧场存档要立刻上报失败、
  收获全部要跳过失败地块)。把 try 提到循环外会毁掉"一项失败不影响其它项"
  这个行为,是错的优化。

自动修复 358 项:Dict/List/Optional 换成 PEP 585/604 写法、导入排序、
失效的 noqa、空占位 f-string 等。

手工修复的(这些是真问题,不是风格偏好):
- B904 x9:except 中 raise 补异常链。刻意区分——网络/IO 失败用 from exc 保留
  根因;参数解析失败用 from None,因为底层 ValueError 对玩家只是噪音。
- RUF012 x2:StateView 的 plots/animals 原本是**类级共享可变列表**,
  改成在 __init__ 里初始化(apply({}) 会提前返回,所以必须显式摆好)。
- F841:TUI 里一个赋值后再没用过的死变量。
- SIM105 x11:try/except/pass 统一改成 contextlib.suppress。
- SIM115:日志句柄改用 with。Popen 已把 fd 复制给子进程,父进程可安全关闭。
- E501 x7:拆超长行;main.py 那处顺手提取变量,避免超长嵌套表达式。
- E741:把 l/r 改成 left_line/right_line。
- 其余:RUF046 / PERF401 / RET504 / RUF005 / UP031 / B007。

ruff format 重排 24 个文件。

验证:97 项测试全绿。
This commit is contained in:
snowy-yang committed 2026-09-18 21:18:22 +08:00
1 parent c7c8a6308a
commit fcd4cb4649
25 files changed
+1204 -794

No files matched your search

+73 -48
View File
@@ -25,21 +25,21 @@ import unittest
from farm import config
from farm.engine import FarmEngine, GameError
from farm.server.actions import ACTIONS, describe_actions, run_action
from farm.server.session import FarmSession, SessionManager, validate_farm_id
from farm.server.session import SessionManager, validate_farm_id
try:
import fastapi # noqa: F401
from fastapi.testclient import TestClient
HAS_FASTAPI = True
except ImportError: # pragma: no cover - 取决于环境
except ImportError: # pragma: no cover - 取决于环境
HAS_FASTAPI = False
try:
import uvicorn # noqa: F401
HAS_UVICORN = True
except ImportError: # pragma: no cover
except ImportError: # pragma: no cover
HAS_UVICORN = False
@@ -231,8 +231,7 @@ class TestRestAPI(unittest.TestCase):
from farm.server.app import ServerConfig, create_app
self.tmp = tempfile.mkdtemp(prefix="farm-api-")
self.cfg = ServerConfig(host="127.0.0.1", port=8765, save_dir=self.tmp,
tick_seconds=60)
self.cfg = ServerConfig(host="127.0.0.1", port=8765, save_dir=self.tmp, tick_seconds=60)
self.app = create_app(self.cfg)
self.client = TestClient(self.app)
self.client.__enter__()
@@ -277,15 +276,18 @@ class TestRestAPI(unittest.TestCase):
data = self.client.post("/api/actions?farm=one", json=body).json()
self.assertTrue(data["ok"])
self.assertIn("已种下", data["message"])
self.assertEqual(data["state"]["gold"], config.START_GOLD - config.CROPS["wheat"]["seed_cost"])
self.assertEqual(
data["state"]["gold"], config.START_GOLD - config.CROPS["wheat"]["seed_cost"]
)
state = self.client.get("/api/state?farm=one").json()["state"]
self.assertEqual(state["plots"][0]["crop"], "wheat")
def test_flat_params_also_accepted(self) -> None:
"""{"action":"plant","plot":1,"crop":"wheat"} 这种平铺写法也要能用。"""
data = self.client.post("/api/actions?farm=flat",
json={"action": "plant", "plot": 1, "crop": "wheat"}).json()
data = self.client.post(
"/api/actions?farm=flat", json={"action": "plant", "plot": 1, "crop": "wheat"}
).json()
self.assertTrue(data["ok"])
def test_convenience_route(self) -> None:
@@ -294,8 +296,9 @@ class TestRestAPI(unittest.TestCase):
self.assertEqual(data["action"], "plant")
def test_action_error_shape(self) -> None:
response = self.client.post("/api/actions?farm=err",
json={"action": "plant", "params": {"plot": 1, "crop": "nope"}})
response = self.client.post(
"/api/actions?farm=err", json={"action": "plant", "params": {"plot": 1, "crop": "nope"}}
)
self.assertEqual(response.status_code, 400)
data = response.json()
self.assertFalse(data["ok"])
@@ -303,37 +306,43 @@ class TestRestAPI(unittest.TestCase):
self.assertIn("message", data)
def test_out_of_range_plot(self) -> None:
response = self.client.post("/api/actions?farm=err",
json={"action": "water", "params": {"plot": 999}})
response = self.client.post(
"/api/actions?farm=err", json={"action": "water", "params": {"plot": 999}}
)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()["error"], "no_such_plot")
def test_bad_json_body(self) -> None:
response = self.client.post("/api/actions?farm=err", content=b"{oops",
headers={"Content-Type": "application/json"})
response = self.client.post(
"/api/actions?farm=err", content=b"{oops", headers={"Content-Type": "application/json"}
)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()["error"], "bad_json")
def test_actions_endpoint_matches_state(self) -> None:
data = self.client.get("/api/actions?farm=solo").json()
names = {item["action"] for item in data["actions"]}
self.assertIn("plant", names) # 有空地 + 有钱 → 可播种
self.assertNotIn("harvest", names) # 还没种东西 → 不可收获
self.assertIn("plant", names) # 有空地 + 有钱 → 可播种
self.assertNotIn("harvest", names) # 还没种东西 → 不可收获
def test_batch_endpoint(self) -> None:
items = {"actions": [
{"action": "buy_feed", "params": {"quantity": 5}},
{"action": "plant", "params": {"plot": 1, "crop": "wheat"}},
{"action": "harvest", "params": {"plot": 1}}, # 必然失败
]}
items = {
"actions": [
{"action": "buy_feed", "params": {"quantity": 5}},
{"action": "plant", "params": {"plot": 1, "crop": "wheat"}},
{"action": "harvest", "params": {"plot": 1}}, # 必然失败
]
}
data = self.client.post("/api/batch?farm=multi", json=items).json()
self.assertEqual(data["done"], 2)
self.assertEqual(data["failed"], 1)
self.assertEqual(data["results"][2]["error"], "not_ripe")
def test_log_entries_and_since(self) -> None:
self.client.post("/api/actions?farm=logging",
json={"action": "plant", "params": {"plot": 1, "crop": "wheat"}})
self.client.post(
"/api/actions?farm=logging",
json={"action": "plant", "params": {"plot": 1, "crop": "wheat"}},
)
entries = self.client.get("/api/log?farm=logging").json()["entries"]
self.assertTrue(entries)
last = entries[-1]["seq"]
@@ -344,14 +353,14 @@ class TestRestAPI(unittest.TestCase):
self.assertTrue(newer)
def test_farms_list_and_isolation(self) -> None:
self.client.post("/api/actions?farm=alice",
json={"action": "buy_feed", "params": {"quantity": 10}})
self.client.post(
"/api/actions?farm=alice", json={"action": "buy_feed", "params": {"quantity": 10}}
)
self.client.post("/api/actions?farm=bob", json={"action": "save"})
rows = self.client.get("/api/farms").json()["farms"]
by_name = {row["farm"]: row for row in rows}
self.assertIn("alice", by_name)
self.assertEqual(by_name["alice"]["gold"],
config.START_GOLD - 10 * config.FEED_UNIT_PRICE)
self.assertEqual(by_name["alice"]["gold"], config.START_GOLD - 10 * config.FEED_UNIT_PRICE)
def test_create_farm_reports_created_flag(self) -> None:
first = self.client.post("/api/farms", json={"farm": "zoe", "owner": "小Z"}).json()
@@ -370,8 +379,10 @@ class TestRestAPI(unittest.TestCase):
self.assertEqual(data["farm"], "header-farm")
def test_admin_save(self) -> None:
self.client.post("/api/actions?farm=saver",
json={"action": "plant", "params": {"plot": 1, "crop": "wheat"}})
self.client.post(
"/api/actions?farm=saver",
json={"action": "plant", "params": {"plot": 1, "crop": "wheat"}},
)
data = self.client.post("/api/admin/save").json()
self.assertIn("saver", data["farms"])
self.assertTrue(os.path.exists(os.path.join(self.tmp, "saver.json")))
@@ -419,8 +430,7 @@ class TestAuth(unittest.TestCase):
self.assertEqual(self.client.get("/api/state?token=nope").status_code, 401)
def test_action_blocked_without_token(self) -> None:
response = self.client.post("/api/actions",
json={"action": "buy_plot"})
response = self.client.post("/api/actions", json={"action": "buy_plot"})
self.assertEqual(response.status_code, 401)
@@ -436,14 +446,27 @@ class TestLiveServer(unittest.TestCase):
cls.tmp = tempfile.mkdtemp(prefix="farm-live-")
cls.port = free_port()
cls.url = f"http://127.0.0.1:{cls.port}"
app = create_app(ServerConfig(host="127.0.0.1", port=cls.port, save_dir=cls.tmp,
tick_seconds=600, autosave_interval=3600))
app = create_app(
ServerConfig(
host="127.0.0.1",
port=cls.port,
save_dir=cls.tmp,
tick_seconds=600,
autosave_interval=3600,
)
)
import uvicorn
cls.server = uvicorn.Server(uvicorn.Config(
app, host="127.0.0.1", port=cls.port, log_level="warning",
access_log=False, lifespan="on",
))
cls.server = uvicorn.Server(
uvicorn.Config(
app,
host="127.0.0.1",
port=cls.port,
log_level="warning",
access_log=False,
lifespan="on",
)
)
cls.thread = threading.Thread(target=cls.server.run, name="live-server", daemon=True)
cls.thread.start()
@@ -454,7 +477,7 @@ class TestLiveServer(unittest.TestCase):
break
except OSError:
time.sleep(0.2)
else: # pragma: no cover
else: # pragma: no cover
raise unittest.SkipTest("服务器没能在 20 秒内起来")
@classmethod
@@ -466,8 +489,9 @@ class TestLiveServer(unittest.TestCase):
def client(self, farm: str, realtime: bool = True):
from farm.client import RemoteFarm
return RemoteFarm(endpoint=self.url, farm=farm, realtime=realtime, timeout=8.0,
autostart=True)
return RemoteFarm(
endpoint=self.url, farm=farm, realtime=realtime, timeout=8.0, autostart=True
)
def test_http_roundtrip_through_real_client(self) -> None:
client = self.client("live-http", realtime=False)
@@ -480,18 +504,18 @@ class TestLiveServer(unittest.TestCase):
self.assertFalse(client.state.plots[0].is_ripe)
with self.assertRaises(GameError) as ctx:
client.plant(1, "carrot") # 地已经占用了
client.plant(1, "carrot") # 地已经占用了
self.assertEqual(ctx.exception.code, "plot_busy")
actions = client.available_actions()
names = {item["action"] for item in actions}
self.assertIn("plant", names) # 还有空地可以种
self.assertNotIn("harvest", names) # 小麦还没成熟
self.assertIn("plant", names) # 还有空地可以种
self.assertNotIn("harvest", names) # 小麦还没成熟
client.water(1) # 播种后水分 2/3,可以补满
client.water(1) # 播种后水分 2/3,可以补满
self.assertEqual(client.state.plots[0].moisture, config.WATER_CAPACITY)
with self.assertRaises(GameError) as ctx:
client.water(1) # 水分已满,再浇就是错的
client.water(1) # 水分已满,再浇就是错的
self.assertEqual(ctx.exception.code, "already_watered")
catalog = client.catalog()
@@ -515,8 +539,9 @@ class TestLiveServer(unittest.TestCase):
deadline = time.time() + 8
while time.time() < deadline and watcher.state.plots[0].crop != "carrot":
time.sleep(0.1)
self.assertEqual(watcher.state.plots[0].crop, "carrot",
"实时通道应该把另一方的操作推过来")
self.assertEqual(
watcher.state.plots[0].crop, "carrot", "实时通道应该把另一方的操作推过来"
)
self.assertEqual(watcher.mode, "ws")
finally:
watcher.stop()
@@ -554,7 +579,7 @@ class TestLiveServer(unittest.TestCase):
client.save()
path = os.path.join(self.tmp, "persist.json")
self.assertTrue(os.path.exists(path))
with open(path, "r", encoding="utf-8") as handle:
with open(path, encoding="utf-8") as handle:
payload = json.load(handle)
self.assertEqual(payload["state"]["plots"][0]["crop"], "wheat")
finally: