Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(anta.cli)!: Cleaner entrypoint #909

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions anta/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright (c) 2023-2024 Arista Networks, Inc.
# Use of this source code is governed by the Apache License 2.0
# that can be found in the LICENSE file.
"""ANTA CLI."""

from __future__ import annotations

import sys
from typing import Callable

# Note: need to separate this file from _main to be able to fail on the import.
from anta import __DEBUG__

try:
from anta.cli import cli

except ImportError as exc:
if exc.name == "click":

def build_cli(exception: Exception) -> Callable[[], None]:
"""Build CLI function using the caught exception."""

def wrap() -> None:
"""Error message if any CLI dependency is missing."""
print(
"The ANTA command line client could not run because the required "
"dependencies were not installed.\nMake sure you've installed "
"everything with: pip install 'anta[cli]'"
)
if __DEBUG__:
print(f"The caught exception was: {exception}")

sys.exit(1)

return wrap

cli = build_cli(exc)
else:
# if this is not click re-raise the original Exception
raise

if __name__ == "__main__":
cli()
78 changes: 54 additions & 24 deletions anta/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,67 @@

from __future__ import annotations

import logging
import pathlib
import sys
from typing import Callable

from anta import __DEBUG__
import click

# Note: need to separate this file from _main to be able to fail on the import.
try:
from ._main import anta, cli
from anta import GITHUB_SUGGESTION, __version__
from anta.cli.check import check as check_command
from anta.cli.debug import debug as debug_command
from anta.cli.exec import _exec as exec_command
from anta.cli.get import get as get_command
from anta.cli.nrfu import nrfu as nrfu_command
from anta.cli.utils import AliasedGroup, ExitCode
from anta.logger import Log, LogLevel, anta_log_exception, setup_logging

except ImportError as exc:
logger = logging.getLogger(__name__)

def build_cli(exception: Exception) -> Callable[[], None]:
"""Build CLI function using the caught exception."""

def wrap() -> None:
"""Error message if any CLI dependency is missing."""
print(
"The ANTA command line client could not run because the required "
"dependencies were not installed.\nMake sure you've installed "
"everything with: pip install 'anta[cli]'"
)
if __DEBUG__:
print(f"The caught exception was: {exception}")
@click.group(cls=AliasedGroup)
@click.pass_context
@click.help_option(allow_from_autoenv=False)
@click.version_option(__version__, allow_from_autoenv=False)
@click.option(
"--log-file",
help="Send the logs to a file. If logging level is DEBUG, only INFO or higher will be sent to stdout.",
show_envvar=True,
type=click.Path(file_okay=True, dir_okay=False, writable=True, path_type=pathlib.Path),
)
@click.option(
"--log-level",
"-l",
help="ANTA logging level",
default=logging.getLevelName(logging.INFO),
show_envvar=True,
show_default=True,
type=click.Choice(
[Log.CRITICAL, Log.ERROR, Log.WARNING, Log.INFO, Log.DEBUG],
case_sensitive=False,
),
)
def anta(ctx: click.Context, log_level: LogLevel, log_file: pathlib.Path) -> None:
"""Arista Network Test Automation (ANTA) CLI."""
ctx.ensure_object(dict)
setup_logging(log_level, log_file)

sys.exit(1)

return wrap
anta.add_command(nrfu_command)
anta.add_command(check_command)
anta.add_command(exec_command)
anta.add_command(get_command)
anta.add_command(debug_command)

cli = build_cli(exc)

__all__ = ["cli", "anta"]

if __name__ == "__main__":
cli()
def cli() -> None:
"""Entrypoint for pyproject.toml."""
try:
anta(obj={}, auto_envvar_prefix="ANTA")
except Exception as exc: # noqa: BLE001
anta_log_exception(
exc,
f"Uncaught Exception when running ANTA CLI\n{GITHUB_SUGGESTION}",
logger,
)
sys.exit(ExitCode.INTERNAL_ERROR)
71 changes: 0 additions & 71 deletions anta/cli/_main.py

This file was deleted.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ Homepage = "https://anta.arista.com"
Contributing = "https://anta.arista.com/main/contribution/"

[project.scripts]
anta = "anta.cli:cli"
anta = "anta.__main__:cli"

################################
# Tools
Expand Down Expand Up @@ -406,7 +406,7 @@ runtime-evaluated-base-classes = ["pydantic.BaseModel", "anta.models.AntaTest.In
"anta/cli/exec/utils.py" = [
"SLF001", # TODO: some private members, lets try to fix
]
"anta/cli/__init__.py" = [
"anta/__main__.py" = [
"T201", # Allow print statements
]
"anta/cli/*" = [
Expand Down
2 changes: 1 addition & 1 deletion tests/units/cli/get/test__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from typing import TYPE_CHECKING

from anta.cli._main import anta
from anta.cli import anta
from anta.cli.utils import ExitCode

if TYPE_CHECKING:
Expand Down
2 changes: 1 addition & 1 deletion tests/units/cli/get/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import requests
from cvprac.cvp_client_errors import CvpApiError

from anta.cli._main import anta
from anta.cli import anta
from anta.cli.utils import ExitCode

if TYPE_CHECKING:
Expand Down
75 changes: 42 additions & 33 deletions tests/units/cli/test__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,60 @@

from __future__ import annotations

import sys
from importlib import reload
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING
from unittest.mock import patch

import pytest

import anta.cli
from anta.cli import anta, cli
from anta.cli.utils import ExitCode

if TYPE_CHECKING:
from types import ModuleType
from click.testing import CliRunner

builtins_import = __import__

def test_anta(click_runner: CliRunner) -> None:
"""Test anta main entrypoint."""
result = click_runner.invoke(anta)
assert result.exit_code == ExitCode.OK
assert "Usage" in result.output

# Tried to achieve this with mock
# http://materials-scientist.com/blog/2021/02/11/mocking-failing-module-import-python/
def import_mock(name: str, *args: Any) -> ModuleType: # noqa: ANN401
"""Mock."""
if name == "click":
msg = "No module named 'click'"
raise ModuleNotFoundError(msg)
return builtins_import(name, *args)

def test_anta_help(click_runner: CliRunner) -> None:
"""Test anta --help."""
result = click_runner.invoke(anta, ["--help"])
assert result.exit_code == ExitCode.OK
assert "Usage" in result.output

def test_cli_error_missing(capsys: pytest.CaptureFixture[Any]) -> None:
"""Test ANTA errors out when anta[cli] was not installed."""
with patch.dict(sys.modules) as sys_modules, patch("builtins.__import__", import_mock):
del sys_modules["anta.cli._main"]
reload(anta.cli)

with pytest.raises(SystemExit) as e_info:
anta.cli.cli()
def test_anta_exec_help(click_runner: CliRunner) -> None:
"""Test anta exec --help."""
result = click_runner.invoke(anta, ["exec", "--help"])
assert result.exit_code == ExitCode.OK
assert "Usage: anta exec" in result.output

captured = capsys.readouterr()
assert "The ANTA command line client could not run because the required dependencies were not installed." in captured.out
assert "Make sure you've installed everything with: pip install 'anta[cli]'" in captured.out
assert e_info.value.code == 1

# setting ANTA_DEBUG
with pytest.raises(SystemExit) as e_info, patch("anta.cli.__DEBUG__", new=True):
anta.cli.cli()
def test_anta_debug_help(click_runner: CliRunner) -> None:
"""Test anta debug --help."""
result = click_runner.invoke(anta, ["debug", "--help"])
assert result.exit_code == ExitCode.OK
assert "Usage: anta debug" in result.output

captured = capsys.readouterr()
assert "The ANTA command line client could not run because the required dependencies were not installed." in captured.out
assert "Make sure you've installed everything with: pip install 'anta[cli]'" in captured.out
assert "The caught exception was:" in captured.out
assert e_info.value.code == 1

def test_anta_get_help(click_runner: CliRunner) -> None:
"""Test anta get --help."""
result = click_runner.invoke(anta, ["get", "--help"])
assert result.exit_code == ExitCode.OK
assert "Usage: anta get" in result.output


def test_uncaught_failure_anta(caplog: pytest.LogCaptureFixture) -> None:
"""Test uncaught failure when running ANTA cli."""
with (
pytest.raises(SystemExit) as e_info,
patch("anta.cli.anta", side_effect=ZeroDivisionError()),
):
cli()
assert "CRITICAL" in caplog.text
assert "Uncaught Exception when running ANTA CLI" in caplog.text
assert e_info.value.code == 1
Loading
Loading