fix: resolve merge conflicts and lint issues

- Merge origin/main changes (flattened buy/sell commands, --doc flag, aliases)
- Fix spinner placement for buy/sell commands
- Fix duplicate alias key 'p' in canonical subcommands
- Remove unused mypy ignore comments in spot_client.py
- Fix nested with statements in tests

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 16:59:53 +08:00
parent 4602583760
commit d629c25232
4 changed files with 45 additions and 19 deletions

View File

@@ -5,7 +5,10 @@ from __future__ import annotations
from collections.abc import Callable
from typing import Any
from requests.exceptions import RequestException, SSLError # type: ignore[import-untyped]
from requests.exceptions import ( # type: ignore[import-untyped]
RequestException,
SSLError,
)
class SpotBinanceClient:
@@ -56,7 +59,7 @@ class SpotBinanceClient:
response = self._call("24h ticker", self._client.ticker_24hr, symbol=symbols[0])
else:
response = self._call("24h ticker", self._client.ticker_24hr, symbols=symbols)
return response if isinstance(response, list) else [response] # type: ignore[no-any-return]
return response if isinstance(response, list) else [response]
def ticker_price(self, symbols: list[str] | None = None) -> list[dict[str, Any]]:
if not symbols:
@@ -65,7 +68,7 @@ class SpotBinanceClient:
response = self._call("ticker price", self._client.ticker_price, symbol=symbols[0])
else:
response = self._call("ticker price", self._client.ticker_price, symbols=symbols)
return response if isinstance(response, list) else [response] # type: ignore[no-any-return]
return response if isinstance(response, list) else [response]
def klines(self, symbol: str, interval: str, limit: int) -> list[list[Any]]:
return self._call("klines", self._client.klines, symbol=symbol, interval=interval, limit=limit) # type: ignore[no-any-return]

View File

@@ -10,8 +10,19 @@ from . import __version__
from .audit import read_audit_log
from .binance.spot_client import SpotBinanceClient
from .config import ensure_init_files, get_binance_credentials, load_config
from .runtime import get_runtime_paths, install_shell_completion, print_output, self_upgrade, with_spinner
from .services import account_service, market_service, opportunity_service, trade_service
from .runtime import (
get_runtime_paths,
install_shell_completion,
print_output,
self_upgrade,
with_spinner,
)
from .services import (
account_service,
market_service,
opportunity_service,
trade_service,
)
EPILOG = """\
examples:
@@ -294,7 +305,6 @@ _CANONICAL_SUBCOMMANDS = {
"t": "tickers",
"k": "klines",
"pf": "portfolio",
"p": "portfolio",
}
_COMMANDS_WITH_SUBCOMMANDS = {"account", "market", "opportunity"}

View File

@@ -8,7 +8,12 @@ import unittest
from pathlib import Path
from unittest.mock import patch
from coinhunter.config import ensure_init_files, get_binance_credentials, load_config, load_env_file
from coinhunter.config import (
ensure_init_files,
get_binance_credentials,
load_config,
load_env_file,
)
from coinhunter.runtime import get_runtime_paths
@@ -89,6 +94,8 @@ class ConfigRuntimeTestCase(unittest.TestCase):
),
):
paths = get_runtime_paths()
with patch("coinhunter.config.ensure_runtime_dirs", side_effect=PermissionError("no write access")):
with self.assertRaisesRegex(RuntimeError, "Set COINHUNTER_HOME to a writable directory"):
ensure_init_files(paths)
with (
patch("coinhunter.config.ensure_runtime_dirs", side_effect=PermissionError("no write access")),
self.assertRaisesRegex(RuntimeError, "Set COINHUNTER_HOME to a writable directory"),
):
ensure_init_files(paths)

View File

@@ -57,9 +57,11 @@ class TradeServiceTestCase(unittest.TestCase):
self.assertEqual(client.calls[0]["timeInForce"], "GTC")
def test_spot_market_buy_requires_quote(self):
with patch.object(trade_service, "audit_event", return_value=None):
with self.assertRaisesRegex(RuntimeError, "requires --quote"):
trade_service.execute_spot_trade(
with (
patch.object(trade_service, "audit_event", return_value=None),
self.assertRaisesRegex(RuntimeError, "requires --quote"),
):
trade_service.execute_spot_trade(
{"trading": {"dry_run_default": False}},
side="buy",
symbol="BTCUSDT",
@@ -72,9 +74,11 @@ class TradeServiceTestCase(unittest.TestCase):
)
def test_spot_market_buy_rejects_qty(self):
with patch.object(trade_service, "audit_event", return_value=None):
with self.assertRaisesRegex(RuntimeError, "accepts --quote only"):
trade_service.execute_spot_trade(
with (
patch.object(trade_service, "audit_event", return_value=None),
self.assertRaisesRegex(RuntimeError, "accepts --quote only"),
):
trade_service.execute_spot_trade(
{"trading": {"dry_run_default": False}},
side="buy",
symbol="BTCUSDT",
@@ -87,9 +91,11 @@ class TradeServiceTestCase(unittest.TestCase):
)
def test_spot_market_sell_rejects_quote(self):
with patch.object(trade_service, "audit_event", return_value=None):
with self.assertRaisesRegex(RuntimeError, "accepts --qty only"):
trade_service.execute_spot_trade(
with (
patch.object(trade_service, "audit_event", return_value=None),
self.assertRaisesRegex(RuntimeError, "accepts --qty only"),
):
trade_service.execute_spot_trade(
{"trading": {"dry_run_default": False}},
side="sell",
symbol="BTCUSDT",