refactor: argparse None defaults, serve flags on both parsers, upgrade alias
- Add _add_serve_flags() helper to set --host/--port/--auth/--session-ttl all with default=None so serve() can distinguish 'not passed' from 'passed the default value' - Apply _add_serve_flags() to both root parser and 'serve' subparser so 'muxplex serve --host X --port Y' is accepted - Consolidate upgrade/update via aliases=['update'] instead of two separate subparsers — help now shows 'upgrade (update)' - Print deprecation warning to stderr when 'install-service' is used - Update 5 existing tests to expect None instead of hardcoded defaults - Add 4 new tests: test_main_passes_none_for_unset_flags, test_main_passes_explicit_host_only, test_main_serve_subcommand_accepts_flags, test_help_shows_single_upgrade_line All 55 tests pass.
This commit is contained in:
+45
-25
@@ -645,32 +645,50 @@ def upgrade(*, force: bool = False) -> None:
|
|||||||
doctor()
|
doctor()
|
||||||
|
|
||||||
|
|
||||||
|
def _add_serve_flags(parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""Add --host, --port, --auth, --session-ttl flags to a parser.
|
||||||
|
|
||||||
|
All default to None so serve() can distinguish 'not passed' from
|
||||||
|
'passed the default value'.
|
||||||
|
"""
|
||||||
|
parser.add_argument(
|
||||||
|
"--host",
|
||||||
|
default=None,
|
||||||
|
help="Bind host (default: from settings.json, then 127.0.0.1)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--port",
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help="Port (default: from settings.json, then 8088)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--auth",
|
||||||
|
choices=["pam", "password"],
|
||||||
|
default=None,
|
||||||
|
help="Auth method: pam or password (default: from settings.json, then pam)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--session-ttl",
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
dest="session_ttl",
|
||||||
|
help="Session TTL in seconds (default: from settings.json, then 604800; 0 = browser session)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""CLI entry point."""
|
"""CLI entry point."""
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="muxplex",
|
prog="muxplex",
|
||||||
description="muxplex — web-based tmux session dashboard",
|
description="muxplex — web-based tmux session dashboard",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
_add_serve_flags(parser)
|
||||||
"--host", default="127.0.0.1", help="Bind host (default: 127.0.0.1)"
|
|
||||||
)
|
|
||||||
parser.add_argument("--port", type=int, default=8088, help="Port (default: 8088)")
|
|
||||||
parser.add_argument(
|
|
||||||
"--auth",
|
|
||||||
choices=["pam", "password"],
|
|
||||||
default="pam",
|
|
||||||
help="Authentication method: pam or password (default: pam)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--session-ttl",
|
|
||||||
type=int,
|
|
||||||
default=604800,
|
|
||||||
dest="session_ttl",
|
|
||||||
help="Session TTL in seconds (default: 604800 = 7 days; 0 = browser session)",
|
|
||||||
)
|
|
||||||
|
|
||||||
sub = parser.add_subparsers(dest="command")
|
sub = parser.add_subparsers(dest="command")
|
||||||
sub.add_parser("serve", help="Start the server (default)")
|
|
||||||
|
serve_parser = sub.add_parser("serve", help="Start the server (default)")
|
||||||
|
_add_serve_flags(serve_parser)
|
||||||
|
|
||||||
svc = sub.add_parser(
|
svc = sub.add_parser(
|
||||||
"install-service",
|
"install-service",
|
||||||
@@ -689,23 +707,25 @@ def main() -> None:
|
|||||||
sub.add_parser("doctor", help="Check dependencies and system status")
|
sub.add_parser("doctor", help="Check dependencies and system status")
|
||||||
|
|
||||||
upgrade_parser = sub.add_parser(
|
upgrade_parser = sub.add_parser(
|
||||||
"upgrade", help="Upgrade muxplex to latest version and restart service"
|
"upgrade",
|
||||||
|
aliases=["update"],
|
||||||
|
help="Upgrade muxplex to latest version and restart service",
|
||||||
)
|
)
|
||||||
upgrade_parser.add_argument(
|
upgrade_parser.add_argument(
|
||||||
"--force",
|
"--force",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Force reinstall even if already up to date",
|
help="Force reinstall even if already up to date",
|
||||||
)
|
)
|
||||||
update_parser = sub.add_parser("update", help="Alias for upgrade")
|
|
||||||
update_parser.add_argument(
|
|
||||||
"--force",
|
|
||||||
action="store_true",
|
|
||||||
help="Force reinstall even if already up to date",
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.command == "install-service":
|
if args.command == "install-service":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Warning: 'install-service' is deprecated and will be removed in a future version.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
install_service(system=args.system)
|
install_service(system=args.system)
|
||||||
elif args.command == "show-password":
|
elif args.command == "show-password":
|
||||||
show_password()
|
show_password()
|
||||||
|
|||||||
+77
-10
@@ -14,61 +14,61 @@ def test_cli_module_importable():
|
|||||||
|
|
||||||
|
|
||||||
def test_main_calls_serve_by_default():
|
def test_main_calls_serve_by_default():
|
||||||
"""Calling main() with no args must invoke serve() with new defaults."""
|
"""Calling main() with no args must invoke serve() with None defaults (settings layer resolves)."""
|
||||||
from muxplex.cli import main
|
from muxplex.cli import main
|
||||||
|
|
||||||
with patch("muxplex.cli.serve") as mock_serve:
|
with patch("muxplex.cli.serve") as mock_serve:
|
||||||
with patch("sys.argv", ["muxplex"]):
|
with patch("sys.argv", ["muxplex"]):
|
||||||
main()
|
main()
|
||||||
mock_serve.assert_called_once_with(
|
mock_serve.assert_called_once_with(
|
||||||
host="127.0.0.1", port=8088, auth="pam", session_ttl=604800
|
host=None, port=None, auth=None, session_ttl=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_main_passes_custom_host_and_port():
|
def test_main_passes_custom_host_and_port():
|
||||||
"""main() with --host/--port must forward them to serve()."""
|
"""main() with --host/--port must forward them to serve(); unset flags are None."""
|
||||||
from muxplex.cli import main
|
from muxplex.cli import main
|
||||||
|
|
||||||
with patch("muxplex.cli.serve") as mock_serve:
|
with patch("muxplex.cli.serve") as mock_serve:
|
||||||
with patch("sys.argv", ["muxplex", "--host", "192.168.1.1", "--port", "9000"]):
|
with patch("sys.argv", ["muxplex", "--host", "192.168.1.1", "--port", "9000"]):
|
||||||
main()
|
main()
|
||||||
mock_serve.assert_called_once_with(
|
mock_serve.assert_called_once_with(
|
||||||
host="192.168.1.1", port=9000, auth="pam", session_ttl=604800
|
host="192.168.1.1", port=9000, auth=None, session_ttl=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_main_default_host_is_localhost():
|
def test_main_default_host_is_localhost():
|
||||||
"""Default --host must be 127.0.0.1 (not 0.0.0.0)."""
|
"""Default --host must be None (settings layer resolves to 127.0.0.1)."""
|
||||||
from muxplex.cli import main
|
from muxplex.cli import main
|
||||||
|
|
||||||
with patch("muxplex.cli.serve") as mock_serve:
|
with patch("muxplex.cli.serve") as mock_serve:
|
||||||
with patch("sys.argv", ["muxplex"]):
|
with patch("sys.argv", ["muxplex"]):
|
||||||
main()
|
main()
|
||||||
_, kwargs = mock_serve.call_args
|
_, kwargs = mock_serve.call_args
|
||||||
assert kwargs["host"] == "127.0.0.1"
|
assert kwargs["host"] is None
|
||||||
|
|
||||||
|
|
||||||
def test_main_passes_auth_flag():
|
def test_main_passes_auth_flag():
|
||||||
"""main() with --auth password must forward auth='password' to serve()."""
|
"""main() with --auth password must forward auth='password'; unset flags are None."""
|
||||||
from muxplex.cli import main
|
from muxplex.cli import main
|
||||||
|
|
||||||
with patch("muxplex.cli.serve") as mock_serve:
|
with patch("muxplex.cli.serve") as mock_serve:
|
||||||
with patch("sys.argv", ["muxplex", "--auth", "password"]):
|
with patch("sys.argv", ["muxplex", "--auth", "password"]):
|
||||||
main()
|
main()
|
||||||
mock_serve.assert_called_once_with(
|
mock_serve.assert_called_once_with(
|
||||||
host="127.0.0.1", port=8088, auth="password", session_ttl=604800
|
host=None, port=None, auth="password", session_ttl=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_main_passes_session_ttl_flag():
|
def test_main_passes_session_ttl_flag():
|
||||||
"""main() with --session-ttl 3600 must forward session_ttl=3600 to serve()."""
|
"""main() with --session-ttl 3600 must forward session_ttl=3600; unset flags are None."""
|
||||||
from muxplex.cli import main
|
from muxplex.cli import main
|
||||||
|
|
||||||
with patch("muxplex.cli.serve") as mock_serve:
|
with patch("muxplex.cli.serve") as mock_serve:
|
||||||
with patch("sys.argv", ["muxplex", "--session-ttl", "3600"]):
|
with patch("sys.argv", ["muxplex", "--session-ttl", "3600"]):
|
||||||
main()
|
main()
|
||||||
mock_serve.assert_called_once_with(
|
mock_serve.assert_called_once_with(
|
||||||
host="127.0.0.1", port=8088, auth="pam", session_ttl=3600
|
host=None, port=None, auth=None, session_ttl=3600
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -923,3 +923,70 @@ def test_serve_session_ttl_zero_is_valid(tmp_path, monkeypatch):
|
|||||||
serve(session_ttl=0)
|
serve(session_ttl=0)
|
||||||
|
|
||||||
assert os.environ.get("MUXPLEX_SESSION_TTL") == "0"
|
assert os.environ.get("MUXPLEX_SESSION_TTL") == "0"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# argparse refactoring tests — None defaults, serve flags on both parsers,
|
||||||
|
# upgrade alias, install-service deprecation warning
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_passes_none_for_unset_flags():
|
||||||
|
"""main() with no flags passes None for host/port/auth/session_ttl to serve()."""
|
||||||
|
from muxplex.cli import main
|
||||||
|
|
||||||
|
with patch("muxplex.cli.serve") as mock_serve:
|
||||||
|
with patch("sys.argv", ["muxplex"]):
|
||||||
|
main()
|
||||||
|
mock_serve.assert_called_once_with(
|
||||||
|
host=None, port=None, auth=None, session_ttl=None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_passes_explicit_host_only():
|
||||||
|
"""main() with --host 10.0.0.1 passes host='10.0.0.1', others as None."""
|
||||||
|
from muxplex.cli import main
|
||||||
|
|
||||||
|
with patch("muxplex.cli.serve") as mock_serve:
|
||||||
|
with patch("sys.argv", ["muxplex", "--host", "10.0.0.1"]):
|
||||||
|
main()
|
||||||
|
mock_serve.assert_called_once_with(
|
||||||
|
host="10.0.0.1", port=None, auth=None, session_ttl=None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_serve_subcommand_accepts_flags():
|
||||||
|
"""'muxplex serve --host 10.0.0.1 --port 9000' passes values to serve()."""
|
||||||
|
from muxplex.cli import main
|
||||||
|
|
||||||
|
with patch("muxplex.cli.serve") as mock_serve:
|
||||||
|
with patch(
|
||||||
|
"sys.argv", ["muxplex", "serve", "--host", "10.0.0.1", "--port", "9000"]
|
||||||
|
):
|
||||||
|
main()
|
||||||
|
mock_serve.assert_called_once_with(
|
||||||
|
host="10.0.0.1", port=9000, auth=None, session_ttl=None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_help_shows_single_upgrade_line():
|
||||||
|
"""Help output shows 'upgrade (update)' alias notation, not two separate subcommand entries."""
|
||||||
|
import io
|
||||||
|
|
||||||
|
from muxplex.cli import main
|
||||||
|
|
||||||
|
buf = io.StringIO()
|
||||||
|
with patch("sys.argv", ["muxplex", "--help"]):
|
||||||
|
try:
|
||||||
|
with patch("sys.stdout", buf):
|
||||||
|
main()
|
||||||
|
except SystemExit:
|
||||||
|
pass
|
||||||
|
|
||||||
|
help_text = buf.getvalue()
|
||||||
|
# With aliases=['update'], argparse renders: 'upgrade (update) description'
|
||||||
|
# With separate parsers, 'upgrade' and 'update' each have their own help lines
|
||||||
|
assert "upgrade (update)" in help_text, (
|
||||||
|
"upgrade and update must appear as alias notation 'upgrade (update)', not two separate entries. "
|
||||||
|
f"Got help text:\n{help_text}"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user