diff --git a/CHANGES.md b/CHANGES.md index b9d296c5..5b344836 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,8 @@ Unreleased ### Bug fixes +* The `s7` command now prints an install hint and exits when the optional `click` dependency is missing, instead of + failing with an `ImportError` traceback, so `click` stays an optional `cli` extra (#954). * `get_string` and `get_wstring` now raise `ValueError` when the buffer is shorter than the declared current length, instead of silently returning a truncated value (#945). * USERDATA responses whose last-data-unit byte is neither `0x00` nor `0x01` are rejected with `S7ProtocolError` diff --git a/pyproject.toml b/pyproject.toml index 9f6ca508..d178874d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ include = ["snap7*", "s7*"] [project.scripts] snap7-server = "snap7.server:mainloop" -s7 = "snap7.cli:main" +s7 = "snap7._console:main" [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/snap7/_console.py b/snap7/_console.py new file mode 100644 index 00000000..a450531b --- /dev/null +++ b/snap7/_console.py @@ -0,0 +1,19 @@ +""" +Console entry point for the ``s7`` command. + +The CLI depends on the optional ``click`` package (``pip install python-snap7[cli]``). This wrapper checks for it before +importing :mod:`snap7.cli`, so a plain install prints an install hint instead of an ``ImportError`` traceback. +""" + +import importlib.util +import sys + + +def main() -> None: + if importlib.util.find_spec("click") is None: + print("The s7 command requires the CLI dependencies. Install them with: pip install python-snap7[cli]", file=sys.stderr) + sys.exit(1) + + from snap7.cli import main as cli_main + + cli_main() diff --git a/tests/test_console.py b/tests/test_console.py new file mode 100644 index 00000000..8f65556b --- /dev/null +++ b/tests/test_console.py @@ -0,0 +1,35 @@ +"""Tests for the s7 console entry point wrapper.""" + +import importlib.util +from typing import Any +from unittest.mock import patch + +import pytest + +from snap7 import _console + + +@pytest.mark.client +def test_missing_click_prints_install_hint(capsys: pytest.CaptureFixture[str]) -> None: + real_find_spec = importlib.util.find_spec + + def find_spec(name: str, *args: Any, **kwargs: Any) -> Any: + if name == "click": + return None + return real_find_spec(name, *args, **kwargs) + + with patch.object(importlib.util, "find_spec", find_spec): + with pytest.raises(SystemExit) as exc_info: + _console.main() + + assert exc_info.value.code == 1 + assert "pip install python-snap7[cli]" in capsys.readouterr().err + + +@pytest.mark.client +def test_runs_cli_when_click_installed() -> None: + pytest.importorskip("click") + with patch("sys.argv", ["s7", "--help"]): + with pytest.raises(SystemExit) as exc_info: + _console.main() + assert exc_info.value.code == 0