Skip to content

Commit

Permalink
fix: -vvv actually works nw
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey committed Sep 27, 2024
1 parent 9430667 commit 533914c
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 15 deletions.
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
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
33 changes: 30 additions & 3 deletions tests/integration/cli/test_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,14 +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)
breakpoint()
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

0 comments on commit 533914c

Please sign in to comment.