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

fix: ape test -vvv was not using correct pytest verbosity #2303

Merged
merged 3 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
38 changes: 34 additions & 4 deletions src/ape/cli/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ def __init__(self):
self.logger = logger
super().__init__({})

def __repr__(self) -> str:
# Customizing this because otherwise it uses `dict` repr, which is confusing.
return f"<{self.__class__.__name__}>"

@staticmethod
def abort(msg: str, base_error: Optional[Exception] = None) -> NoReturn:
"""
Expand All @@ -56,18 +60,37 @@ def abort(msg: str, base_error: Optional[Exception] = None) -> NoReturn:


def verbosity_option(
cli_logger: Optional[ApeLogger] = None, default: Union[str, int, LogLevel] = DEFAULT_LOG_LEVEL
cli_logger: Optional[ApeLogger] = None,
default: Union[str, int, LogLevel] = DEFAULT_LOG_LEVEL,
callback: Optional[Callable] = None,
**kwargs,
) -> Callable:
"""A decorator that adds a `--verbosity, -v` option to the decorated
command.

Args:
cli_logger (:class:`~ape.logging.ApeLogger` | None): Optionally pass
a custom logger object.
default (str | int | :class:`~ape.logging.LogLevel`): The default log-level
for this command.
callback (Callable | None): A callback handler for passed-in verbosity values.
**kwargs: Additional click overrides.

Returns:
click option
"""
_logger = cli_logger or logger
kwarguments = _create_verbosity_kwargs(_logger=_logger, default=default)
kwarguments = _create_verbosity_kwargs(
_logger=_logger, default=default, callback=callback, **kwargs
)
return lambda f: click.option(*_VERBOSITY_VALUES, **kwarguments)(f)


def _create_verbosity_kwargs(
_logger: Optional[ApeLogger] = None, default: Union[str, int, LogLevel] = DEFAULT_LOG_LEVEL
_logger: Optional[ApeLogger] = None,
default: Union[str, int, LogLevel] = DEFAULT_LOG_LEVEL,
callback: Optional[Callable] = None,
**kwargs,
) -> dict:
cli_logger = _logger or logger

Expand All @@ -77,6 +100,9 @@ def set_level(ctx, param, value):
if value.startswith("LOGLEVEL."):
value = value.split(".")[-1].strip()

if callback is not None:
value = callback(ctx, param, value)

cli_logger._load_from_sys_argv(default=value)

level_names = [lvl.name for lvl in LogLevel]
Expand All @@ -89,6 +115,7 @@ def set_level(ctx, param, value):
"help": f"One of {names_str}",
"is_eager": True,
"type": Noop(),
**kwargs,
}


Expand All @@ -102,13 +129,16 @@ def ape_cli_context(
such as logging or accessing managers.

Args:
default_log_level (Union[str, int, :class:`~ape.logging.LogLevel`]): The log-level
default_log_level (str | int | :class:`~ape.logging.LogLevel`): The log-level
value to pass to :meth:`~ape.cli.options.verbosity_option`.
obj_type (Type): The context object type. Defaults to
:class:`~ape.cli.options.ApeCliContextObject`. Sub-class
the context to extend its functionality in your CLIs,
such as if you want to add additional manager classes
to the context.

Returns:
click option
"""

def decorator(f):
Expand Down
6 changes: 4 additions & 2 deletions src/ape/pytest/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ def is_module(v):
gas_tracker = GasTracker(config_wrapper)
coverage_tracker = CoverageTracker(config_wrapper)

# Enable verbose output if stdout capture is disabled
config.option.verbose = config.getoption("capture") == "no"
if not config.option.verbose:
# Enable verbose output if stdout capture is disabled
config.option.verbose = config.getoption("capture") == "no"
# else: user has already changes verbosity to an equal or higher level; avoid downgrading.

# Register the custom Ape test runner
runner = PytestApeRunner(config_wrapper, receipt_capture, gas_tracker, coverage_tracker)
Expand Down
58 changes: 50 additions & 8 deletions src/ape_test/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
from datetime import datetime, timedelta
from pathlib import Path
from subprocess import run as run_subprocess
from typing import Any

import click
import pytest
from click import Command
from watchdog import events
from watchdog.observers import Observer

Expand Down Expand Up @@ -89,23 +91,59 @@ def _validate_pytest_args(*pytest_args) -> list[str]:
# Ensure this is a pytest -v and not ape's -v.
next_arg = next(args_iter)
lvl_name = _get_level(next_arg)
if lvl_name in [x.name for x in LogLevel]:
# Ape log level found, cannot use.
continue
if not _is_ape_loglevel(lvl_name):
valid_args.append(argument)

else:
valid_args.append(argument)

return valid_args


def _is_ape_loglevel(value: Any) -> bool:
if isinstance(value, (int, LogLevel)):
return True

elif isinstance(value, str):
return (
value.upper() in [x.name for x in LogLevel]
or (value.isnumeric() and int(value) in LogLevel)
or value.lower().startswith("loglevel.")
)

return False


class ApeTestCommand(Command):
def parse_args(self, ctx, args: list[str]) -> list[str]:
num_args = len(args)
for idx, argument in enumerate(args):
if not argument.startswith("-v"):
continue
elif (idx == num_args - 1) or argument in ("-vv", "-vvv"):
# Definitely for pytest.
ctx.obj["pytest_verbosity"] = argument
args = [a for a in args if a != argument]
else:
# -v with a following arg; ensure not Ape's.
next_arg = args[idx + 1]
if not _is_ape_loglevel(next_arg):
ctx.obj["pytest_verbosity"] = "-v"
args = [a for a in args if a != argument]

return super().parse_args(ctx, args)


@click.command(
add_help_option=False, # NOTE: This allows pass-through to pytest's help
short_help="Launches pytest and runs the tests for a project",
context_settings=dict(ignore_unknown_options=True),
cls=ApeTestCommand,
)
# NOTE: Using '.value' because more performant.
@ape_cli_context(default_log_level=LogLevel.WARNING.value)
@ape_cli_context(
default_log_level=LogLevel.WARNING.value,
)
@click.option(
"-w",
"--watch",
Expand All @@ -131,6 +169,11 @@ def _validate_pytest_args(*pytest_args) -> list[str]:
)
@click.argument("pytest_args", nargs=-1, type=click.UNPROCESSED)
def cli(cli_ctx, watch, watch_folders, watch_delay, pytest_args):
pytest_arg_ls = [*pytest_args]
if pytest_verbosity := cli_ctx.get("pytest_verbosity"):
pytest_arg_ls.append(pytest_verbosity)

pytest_arg_ls = _validate_pytest_args(*pytest_arg_ls, pytest_verbosity)
if watch:
event_handler = _create_event_handler()
observer = _create_observer()
Expand All @@ -142,19 +185,18 @@ def cli(cli_ctx, watch, watch_folders, watch_delay, pytest_args):
cli_ctx.logger.warning(f"Folder '{folder}' doesn't exist or isn't a folder.")

observer.start()
pytest_args = _validate_pytest_args(*pytest_args)

try:
_run_ape_test(*pytest_args)
_run_ape_test(*pytest_arg_ls)
while True:
_run_main_loop(watch_delay, *pytest_args)
_run_main_loop(watch_delay, *pytest_arg_ls)

finally:
observer.stop()
observer.join()

else:
return_code = pytest.main([*pytest_args], ["ape_test"])
return_code = pytest.main([*pytest_arg_ls], ["ape_test"])
if return_code:
# only exit with non-zero status to make testing easier
sys.exit(return_code)
Expand Down
32 changes: 30 additions & 2 deletions tests/integration/cli/test_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,13 +233,41 @@ def test_verbosity(runner, ape_cli):


@skip_projects_except("test")
def test_vvv(runner, ape_cli):
@pytest.mark.parametrize("v_arg", ("-v", "-vv", "-vvv"))
def test_vvv(runner, ape_cli, integ_project, v_arg):
"""
Showing you can somehow use pytest's -v flag without
messing up Ape.
"""
result = runner.invoke(ape_cli, ("test", "-vvv", "--fixtures"), catch_exceptions=False)
here = integ_project.path
os.chdir(integ_project.path)
name = f"test_{v_arg.replace('-', '_')}"

TEST = f"""
def {name}():
assert True
""".lstrip()

# Have to create a new test each time to avoid .pycs issues
new_test_file = integ_project.tests_folder / f"{name}.py"
new_test_file.write_text(TEST)

try:
# NOTE: v_arg purposely at the end because testing doesn't interfere
# with click's option parsing "requires value" error.
result = runner.invoke(
ape_cli,
("test", f"tests/{new_test_file.name}::{name}", v_arg),
catch_exceptions=False,
)
finally:
new_test_file.unlink(missing_ok=True)
os.chdir(here)

assert result.exit_code == 0, result.output
# Prove `-vvv` worked via the output.
# It shows PASSED instead of the little green dot.
assert "PASSED" in result.output


@skip_projects_except("test", "with-contracts")
Expand Down
Loading