fix(operator): don't import the server stack on the operator path
cmd_chat/__init__.py eagerly imported run_server (sanic/pydantic) and Client, so any `cmd_chat.*` import — including `python -m cmd_chat.operator` — pulled the server stack at package init. On a phone/Termux operator that installs only the operator deps (and where srp may be unbuildable), this failed before the client's pure-SRP fallback could help. Make both imports lazy inside main()'s serve/connect branches. The operator path now imports cleanly with srp/sanic/pydantic all absent (client falls back to _srp_pure). Add a subprocess regression test to keep it that way. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,11 @@ import argparse
|
|||||||
import getpass
|
import getpass
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from cmd_chat.server.server import run_server
|
# NOTE: the server (run_server) and client (Client) are imported lazily inside
|
||||||
from cmd_chat.client.client import Client
|
# main() below. Importing them here would pull sanic/pydantic (server) at
|
||||||
|
# package-init time for *any* `cmd_chat.*` import — including
|
||||||
|
# `python -m cmd_chat.operator` on a phone/Termux where those server-only deps
|
||||||
|
# aren't installed. See docs/termux-operator.md (Phase 0).
|
||||||
|
|
||||||
|
|
||||||
def resolve_password(args_password: str | None, prompt: str = "Room password: ") -> str:
|
def resolve_password(args_password: str | None, prompt: str = "Room password: ") -> str:
|
||||||
@@ -43,6 +46,8 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.command == "serve":
|
if args.command == "serve":
|
||||||
|
from cmd_chat.server.server import run_server
|
||||||
|
|
||||||
password = resolve_password(args.password)
|
password = resolve_password(args.password)
|
||||||
run_server(
|
run_server(
|
||||||
host=args.ip_address,
|
host=args.ip_address,
|
||||||
@@ -53,6 +58,8 @@ def main():
|
|||||||
no_tls=args.no_tls,
|
no_tls=args.no_tls,
|
||||||
)
|
)
|
||||||
elif args.command == "connect":
|
elif args.command == "connect":
|
||||||
|
from cmd_chat.client.client import Client
|
||||||
|
|
||||||
password = resolve_password(args.password)
|
password = resolve_password(args.password)
|
||||||
Client(
|
Client(
|
||||||
server=args.ip_address,
|
server=args.ip_address,
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""Guard the operator's lightweight import surface (docs/termux-operator.md P0).
|
||||||
|
|
||||||
|
A phone/Termux operator installs only the operator deps (requests, rich,
|
||||||
|
websockets, cryptography, srp) — never the server stack (sanic, pydantic) — and
|
||||||
|
`srp` may be unbuildable there (pure-Python fallback). So `python -m
|
||||||
|
cmd_chat.operator` must import with srp/sanic/pydantic ALL absent. This is easy
|
||||||
|
to regress: any eager `import srp`/sanic on the operator path breaks it. Run the
|
||||||
|
check in a subprocess so blocking modules can't pollute the test interpreter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
_SNIPPET = """
|
||||||
|
import sys
|
||||||
|
# Simulate a Termux operator box: no srp C-ext build, no server-only deps.
|
||||||
|
for missing in ("srp", "sanic", "sanic_ext", "pydantic"):
|
||||||
|
sys.modules[missing] = None # 'import X' now raises ImportError
|
||||||
|
|
||||||
|
import cmd_chat.operator # must not need srp/sanic/pydantic
|
||||||
|
assert hasattr(cmd_chat.operator, "OperatorBridge")
|
||||||
|
|
||||||
|
import cmd_chat.client.client as c # client must fall back to the shim
|
||||||
|
assert c.srp.__name__.endswith("_srp_pure"), c.srp.__name__
|
||||||
|
print("OK")
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def test_operator_imports_without_srp_or_server_deps():
|
||||||
|
proc = subprocess.run(
|
||||||
|
[sys.executable, "-c", _SNIPPET],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
assert proc.returncode == 0, (
|
||||||
|
"operator import failed without srp/sanic/pydantic:\n"
|
||||||
|
f"stdout:\n{proc.stdout}\nstderr:\n{proc.stderr}"
|
||||||
|
)
|
||||||
|
assert proc.stdout.strip().endswith("OK"), proc.stdout
|
||||||
Reference in New Issue
Block a user