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 collections.abc import Callable
from typing import Any 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: class SpotBinanceClient:
@@ -56,7 +59,7 @@ class SpotBinanceClient:
response = self._call("24h ticker", self._client.ticker_24hr, symbol=symbols[0]) response = self._call("24h ticker", self._client.ticker_24hr, symbol=symbols[0])
else: else:
response = self._call("24h ticker", self._client.ticker_24hr, symbols=symbols) 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]]: def ticker_price(self, symbols: list[str] | None = None) -> list[dict[str, Any]]:
if not symbols: if not symbols:
@@ -65,7 +68,7 @@ class SpotBinanceClient:
response = self._call("ticker price", self._client.ticker_price, symbol=symbols[0]) response = self._call("ticker price", self._client.ticker_price, symbol=symbols[0])
else: else:
response = self._call("ticker price", self._client.ticker_price, symbols=symbols) 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]]: 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] 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 .audit import read_audit_log
from .binance.spot_client import SpotBinanceClient from .binance.spot_client import SpotBinanceClient
from .config import ensure_init_files, get_binance_credentials, load_config 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 .runtime import (
from .services import account_service, market_service, opportunity_service, trade_service get_runtime_paths,
install_shell_completion,
print_output,
self_upgrade,
with_spinner,
)
from .services import (
account_service,
market_service,
opportunity_service,
trade_service,
)
EPILOG = """\ EPILOG = """\
examples: examples:
@@ -294,7 +305,6 @@ _CANONICAL_SUBCOMMANDS = {
"t": "tickers", "t": "tickers",
"k": "klines", "k": "klines",
"pf": "portfolio", "pf": "portfolio",
"p": "portfolio",
} }
_COMMANDS_WITH_SUBCOMMANDS = {"account", "market", "opportunity"} _COMMANDS_WITH_SUBCOMMANDS = {"account", "market", "opportunity"}

View File

@@ -8,7 +8,12 @@ import unittest
from pathlib import Path from pathlib import Path
from unittest.mock import patch 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 from coinhunter.runtime import get_runtime_paths
@@ -89,6 +94,8 @@ class ConfigRuntimeTestCase(unittest.TestCase):
), ),
): ):
paths = get_runtime_paths() paths = get_runtime_paths()
with patch("coinhunter.config.ensure_runtime_dirs", side_effect=PermissionError("no write access")): with (
with self.assertRaisesRegex(RuntimeError, "Set COINHUNTER_HOME to a writable directory"): patch("coinhunter.config.ensure_runtime_dirs", side_effect=PermissionError("no write access")),
ensure_init_files(paths) 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") self.assertEqual(client.calls[0]["timeInForce"], "GTC")
def test_spot_market_buy_requires_quote(self): def test_spot_market_buy_requires_quote(self):
with patch.object(trade_service, "audit_event", return_value=None): with (
with self.assertRaisesRegex(RuntimeError, "requires --quote"): patch.object(trade_service, "audit_event", return_value=None),
trade_service.execute_spot_trade( self.assertRaisesRegex(RuntimeError, "requires --quote"),
):
trade_service.execute_spot_trade(
{"trading": {"dry_run_default": False}}, {"trading": {"dry_run_default": False}},
side="buy", side="buy",
symbol="BTCUSDT", symbol="BTCUSDT",
@@ -72,9 +74,11 @@ class TradeServiceTestCase(unittest.TestCase):
) )
def test_spot_market_buy_rejects_qty(self): def test_spot_market_buy_rejects_qty(self):
with patch.object(trade_service, "audit_event", return_value=None): with (
with self.assertRaisesRegex(RuntimeError, "accepts --quote only"): patch.object(trade_service, "audit_event", return_value=None),
trade_service.execute_spot_trade( self.assertRaisesRegex(RuntimeError, "accepts --quote only"),
):
trade_service.execute_spot_trade(
{"trading": {"dry_run_default": False}}, {"trading": {"dry_run_default": False}},
side="buy", side="buy",
symbol="BTCUSDT", symbol="BTCUSDT",
@@ -87,9 +91,11 @@ class TradeServiceTestCase(unittest.TestCase):
) )
def test_spot_market_sell_rejects_quote(self): def test_spot_market_sell_rejects_quote(self):
with patch.object(trade_service, "audit_event", return_value=None): with (
with self.assertRaisesRegex(RuntimeError, "accepts --qty only"): patch.object(trade_service, "audit_event", return_value=None),
trade_service.execute_spot_trade( self.assertRaisesRegex(RuntimeError, "accepts --qty only"),
):
trade_service.execute_spot_trade(
{"trading": {"dry_run_default": False}}, {"trading": {"dry_run_default": False}},
side="sell", side="sell",
symbol="BTCUSDT", symbol="BTCUSDT",