diff --git a/sqlmesh/cli/main.py b/sqlmesh/cli/main.py index 57e42d8a72..f3e1d54ba2 100644 --- a/sqlmesh/cli/main.py +++ b/sqlmesh/cli/main.py @@ -13,6 +13,7 @@ from sqlmesh.cli.example_project import ProjectTemplate, init_example_project from sqlmesh.core.analytics import cli_analytics from sqlmesh.core.console import configure_console, get_console +from sqlmesh.utils import Verbosity from sqlmesh.core.config import load_configs from sqlmesh.core.context import Context from sqlmesh.utils.date import TimeLike @@ -442,7 +443,10 @@ def diff(ctx: click.Context, environment: t.Optional[str] = None) -> None: @error_handler @cli_analytics def plan( - ctx: click.Context, verbose: bool, environment: t.Optional[str] = None, **kwargs: t.Any + ctx: click.Context, + verbose: int, + environment: t.Optional[str] = None, + **kwargs: t.Any, ) -> None: """Apply local changes to the target environment.""" context = ctx.obj @@ -450,7 +454,8 @@ def plan( select_models = kwargs.pop("select_model") or None allow_destructive_models = kwargs.pop("allow_destructive_model") or None backfill_models = kwargs.pop("backfill_model") or None - setattr(get_console(), "verbose", verbose) + setattr(get_console(), "verbosity", Verbosity(verbose)) + context.plan( environment, restate_models=restate_models, @@ -643,7 +648,7 @@ def create_test( def test( obj: Context, k: t.List[str], - verbose: bool, + verbose: int, preserve_fixtures: bool, tests: t.List[str], ) -> None: @@ -651,7 +656,7 @@ def test( result = obj.test( match_patterns=k, tests=tests, - verbose=verbose, + verbosity=Verbosity(verbose), preserve_fixtures=preserve_fixtures, ) if not result.wasSuccessful(): @@ -703,13 +708,13 @@ def fetchdf(ctx: click.Context, sql: str) -> None: @click.pass_obj @error_handler @cli_analytics -def info(obj: Context, skip_connection: bool, verbose: bool) -> None: +def info(obj: Context, skip_connection: bool, verbose: int) -> None: """ Print information about a SQLMesh project. Includes counts of project models and macros and connection tests for the data warehouse. """ - obj.print_info(skip_connection=skip_connection, verbose=verbose) + obj.print_info(skip_connection=skip_connection, verbosity=Verbosity(verbose)) @cli.command("ui") @@ -904,7 +909,11 @@ def rewrite(obj: Context, sql: str, read: str = "", write: str = "") -> None: @error_handler @cli_analytics def prompt( - ctx: click.Context, prompt: str, evaluate: bool, temperature: float, verbose: bool + ctx: click.Context, + prompt: str, + evaluate: bool, + temperature: float, + verbose: int, ) -> None: """Uses LLM to generate a SQL query from a prompt.""" from sqlmesh.integrations.llm import LLMIntegration @@ -915,7 +924,7 @@ def prompt( context.models.values(), context.engine_adapter.dialect, temperature=temperature, - verbose=verbose, + verbosity=Verbosity(verbose), ) query = llm_integration.query(prompt) diff --git a/sqlmesh/cli/options.py b/sqlmesh/cli/options.py index 869cd46e19..7a26b237cc 100644 --- a/sqlmesh/cli/options.py +++ b/sqlmesh/cli/options.py @@ -51,6 +51,6 @@ verbose = click.option( "-v", "--verbose", - is_flag=True, - help="Verbose output.", + count=True, + help="Verbose output. Use -vv for very verbose output.", ) diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 9c889e8df6..d46d1a3ef5 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -36,6 +36,7 @@ ) from sqlmesh.core.test import ModelTest from sqlmesh.utils import rich as srich +from sqlmesh.utils import Verbosity from sqlmesh.utils.concurrency import NodeExecutionFailedError from sqlmesh.utils.date import time_like_to_str, to_date, yesterday_ds from sqlmesh.utils.errors import ( @@ -310,10 +311,13 @@ def show_row_diff( def print_environments(self, environments_summary: t.Dict[str, int]) -> None: """Prints all environment names along with expiry datetime.""" - def _limit_model_names(self, tree: Tree, verbose: bool = False) -> Tree: + def _limit_model_names(self, tree: Tree, verbosity: Verbosity = Verbosity.DEFAULT) -> Tree: """Trim long indirectly modified model lists below threshold.""" modified_length = len(tree.children) - if not verbose and modified_length > self.INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD: + if ( + verbosity < Verbosity.VERY_VERBOSE + and modified_length > self.INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD + ): tree.children = [ tree.children[0], Tree(f".... {modified_length-2} more ...."), @@ -516,7 +520,7 @@ class TerminalConsole(Console): def __init__( self, console: t.Optional[RichConsole] = None, - verbose: bool = False, + verbosity: Verbosity = Verbosity.DEFAULT, dialect: DialectType = None, ignore_warnings: bool = False, **kwargs: t.Any, @@ -548,7 +552,7 @@ def __init__( self.loading_status: t.Dict[uuid.UUID, Status] = {} - self.verbose = verbose + self.verbosity = verbosity self.dialect = dialect self.ignore_warnings = ignore_warnings @@ -673,7 +677,7 @@ def start_creation_progress( def update_creation_progress(self, snapshot: SnapshotInfoLike) -> None: """Update the snapshot creation progress.""" if self.creation_progress is not None and self.creation_task is not None: - if self.verbose: + if self.verbosity >= Verbosity.VERBOSE: self.creation_progress.live.console.print( f"{snapshot.display_name(self.environment_naming_info, self.default_catalog, dialect=self.dialect)} [green]created[/green]" ) @@ -749,7 +753,7 @@ def start_promotion_progress( def update_promotion_progress(self, snapshot: SnapshotInfoLike, promoted: bool) -> None: """Update the snapshot promotion progress.""" if self.promotion_progress is not None and self.promotion_task is not None: - if self.verbose: + if self.verbosity >= Verbosity.VERBOSE: action_str = "[green]promoted[/green]" if promoted else "[yellow]demoted[/yellow]" self.promotion_progress.live.console.print( f"{snapshot.display_name(self.environment_naming_info, self.default_catalog, dialect=self.dialect)} {action_str}" @@ -971,7 +975,7 @@ def _show_summary_tree_for( added_tree.add( f"[added]{snapshot.display_name(environment_naming_info, default_catalog, dialect=self.dialect)}" ) - tree.add(self._limit_model_names(added_tree, self.verbose)) + tree.add(self._limit_model_names(added_tree, self.verbosity)) if removed_snapshot_ids: removed_tree = Tree("[bold][removed]Removed:") for s_id in sorted(removed_snapshot_ids): @@ -979,7 +983,7 @@ def _show_summary_tree_for( removed_tree.add( f"[removed]{snapshot_table_info.display_name(environment_naming_info, default_catalog, dialect=self.dialect)}" ) - tree.add(self._limit_model_names(removed_tree, self.verbose)) + tree.add(self._limit_model_names(removed_tree, self.verbosity)) if modified_snapshot_ids: direct = Tree("[bold][direct]Directly Modified:") indirect = Tree("[bold][indirect]Indirectly Modified:") @@ -1007,7 +1011,7 @@ def _show_summary_tree_for( if direct.children: tree.add(direct) if indirect.children: - tree.add(self._limit_model_names(indirect, self.verbose)) + tree.add(self._limit_model_names(indirect, self.verbosity)) if metadata.children: tree.add(metadata) self._print(tree) @@ -1077,7 +1081,7 @@ def _prompt_categorize( f"[indirect]{child_snapshot.display_name(plan.environment_naming_info, default_catalog, dialect=self.dialect)}" ) if indirect_tree: - indirect_tree = self._limit_model_names(indirect_tree, self.verbose) + indirect_tree = self._limit_model_names(indirect_tree, self.verbosity) self._print(tree) if not no_prompts: @@ -1107,7 +1111,7 @@ def _show_categorized_snapshots(self, plan: Plan, default_catalog: t.Optional[st f"[indirect]{child_snapshot.display_name(plan.environment_naming_info, default_catalog, dialect=self.dialect)} ({child_category_str})" ) if indirect_tree: - indirect_tree = self._limit_model_names(indirect_tree, self.verbose) + indirect_tree = self._limit_model_names(indirect_tree, self.verbosity) elif context_diff.metadata_updated(snapshot.name): tree = Tree( f"\n[bold][metadata]Metadata Updated: {snapshot.display_name(plan.environment_naming_info, default_catalog, dialect=self.dialect)}" @@ -1144,7 +1148,7 @@ def _show_missing_dates(self, plan: Plan, default_catalog: t.Optional[str]) -> N ) if backfill: - backfill = self._limit_model_names(backfill, self.verbose) + backfill = self._limit_model_names(backfill, self.verbosity) self._print(backfill) def _prompt_effective_from( @@ -1993,7 +1997,10 @@ def show_model_difference_summary( self._print("\n**Added Models:**") added_models = sorted(added_snapshot_models) list_length = len(added_models) - if not self.verbose and list_length > self.INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD: + if ( + self.verbosity < Verbosity.VERY_VERBOSE + and list_length > self.INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD + ): self._print(added_models[0]) self._print(f"- `.... {list_length-2} more ....`\n") self._print(added_models[-1]) @@ -2017,7 +2024,10 @@ def show_model_difference_summary( self._print("\n**Removed Models:**") removed_models = sorted(removed_model_snapshot_table_infos) list_length = len(removed_models) - if not self.verbose and list_length > self.INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD: + if ( + self.verbosity < Verbosity.VERY_VERBOSE + and list_length > self.INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD + ): self._print(removed_models[0]) self._print(f"- `.... {list_length-2} more ....`\n") self._print(removed_models[-1]) @@ -2062,7 +2072,7 @@ def show_model_difference_summary( indirectly_modified = sorted(indirectly_modified) modified_length = len(indirectly_modified) if ( - not self.verbose + self.verbosity < Verbosity.VERY_VERBOSE and modified_length > self.INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD ): self._print( @@ -2106,7 +2116,10 @@ def _show_missing_dates(self, plan: Plan, default_catalog: t.Optional[str]) -> N ) length = len(snapshots) - if not self.verbose and length > self.INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD: + if ( + self.verbosity < Verbosity.VERY_VERBOSE + and length > self.INDIRECTLY_MODIFIED_DISPLAY_THRESHOLD + ): self._print(snapshots[0]) self._print(f"- `.... {length-2} more ....`\n") self._print(snapshots[-1]) @@ -2135,7 +2148,7 @@ def _show_categorized_snapshots(self, plan: Plan, default_catalog: t.Optional[st f"[indirect]{child_snapshot.display_name(plan.environment_naming_info, default_catalog, dialect=self.dialect)} ({child_category_str})" ) if indirect_tree: - indirect_tree = self._limit_model_names(indirect_tree, self.verbose) + indirect_tree = self._limit_model_names(indirect_tree, self.verbosity) elif context_diff.metadata_updated(snapshot.name): tree = Tree( f"[bold][metadata]Metadata Updated: {snapshot.display_name(plan.environment_naming_info, default_catalog, dialect=self.dialect)}" @@ -2364,7 +2377,7 @@ def __init__( ) -> None: self.console: RichConsole = console or srich.console self.dialect = dialect - self.verbose = False + self.verbosity = Verbosity.DEFAULT self.ignore_warnings = ignore_warnings def _write(self, msg: t.Any, *args: t.Any, **kwargs: t.Any) -> None: diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index f45130ac61..66d78138bc 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -114,7 +114,7 @@ run_tests, ) from sqlmesh.core.user import User -from sqlmesh.utils import UniqueKeyDict +from sqlmesh.utils import UniqueKeyDict, Verbosity from sqlmesh.utils.dag import DAG from sqlmesh.utils.date import TimeLike, now_ds, to_timestamp, format_tz_datetime from sqlmesh.utils.errors import ( @@ -1741,16 +1741,13 @@ def test( self, match_patterns: t.Optional[t.List[str]] = None, tests: t.Optional[t.List[str]] = None, - verbose: bool = False, + verbosity: Verbosity = Verbosity.DEFAULT, preserve_fixtures: bool = False, stream: t.Optional[t.TextIO] = None, ) -> ModelTextTestResult: """Discover and run model tests""" - if verbose: + if verbosity >= Verbosity.VERBOSE: pd.set_option("display.max_columns", None) - verbosity = 2 - else: - verbosity = 1 if tests: result = run_model_tests( @@ -1954,7 +1951,9 @@ def create_external_models(self, strict: bool = False) -> None: ) @python_api_analytics - def print_info(self, skip_connection: bool = False, verbose: bool = False) -> None: + def print_info( + self, skip_connection: bool = False, verbosity: Verbosity = Verbosity.DEFAULT + ) -> None: """Prints information about connections, models, macros, etc. to the console.""" self.console.log_status_update(f"Models: {len(self.models)}") self.console.log_status_update(f"Macros: {len(self._macros) - len(macro.get_registry())}") @@ -1962,7 +1961,7 @@ def print_info(self, skip_connection: bool = False, verbose: bool = False) -> No if skip_connection: return - if verbose: + if verbosity >= Verbosity.VERBOSE: self.console.log_status_update("") print_config(self.config.get_connection(self.gateway), self.console, "Connection") print_config( @@ -2072,9 +2071,11 @@ def clear_caches(self) -> None: for path in self.configs: rmtree(path / c.CACHE) - def _run_tests(self, verbose: bool = False) -> t.Tuple[unittest.result.TestResult, str]: + def _run_tests( + self, verbosity: Verbosity = Verbosity.DEFAULT + ) -> t.Tuple[unittest.result.TestResult, str]: test_output_io = StringIO() - result = self.test(stream=test_output_io, verbose=verbose) + result = self.test(stream=test_output_io, verbosity=verbosity) return result, test_output_io.getvalue() def _run_plan_tests( diff --git a/sqlmesh/core/test/__init__.py b/sqlmesh/core/test/__init__.py index a09c24ab00..5c5cb36380 100644 --- a/sqlmesh/core/test/__init__.py +++ b/sqlmesh/core/test/__init__.py @@ -14,7 +14,7 @@ load_model_test_file as load_model_test_file, ) from sqlmesh.core.test.result import ModelTextTestResult as ModelTextTestResult -from sqlmesh.utils import UniqueKeyDict +from sqlmesh.utils import UniqueKeyDict, Verbosity if t.TYPE_CHECKING: from sqlmesh.core.config.loader import C @@ -26,7 +26,7 @@ def run_tests( config: C, gateway: t.Optional[str] = None, dialect: str | None = None, - verbosity: int = 1, + verbosity: Verbosity = Verbosity.DEFAULT, preserve_fixtures: bool = False, stream: t.TextIO | None = None, default_catalog: str | None = None, @@ -73,7 +73,9 @@ def run_tests( result = t.cast( ModelTextTestResult, unittest.TextTestRunner( - stream=stream, verbosity=verbosity, resultclass=ModelTextTestResult + stream=stream, + verbosity=2 if verbosity >= Verbosity.VERBOSE else 1, + resultclass=ModelTextTestResult, ).run(unittest.TestSuite(tests)), ) finally: @@ -89,7 +91,7 @@ def run_model_tests( config: C, gateway: t.Optional[str] = None, dialect: str | None = None, - verbosity: int = 1, + verbosity: Verbosity = Verbosity.DEFAULT, patterns: list[str] | None = None, preserve_fixtures: bool = False, stream: t.TextIO | None = None, diff --git a/sqlmesh/integrations/github/cicd/controller.py b/sqlmesh/integrations/github/cicd/controller.py index 381116d9ad..b4364d6439 100644 --- a/sqlmesh/integrations/github/cicd/controller.py +++ b/sqlmesh/integrations/github/cicd/controller.py @@ -30,7 +30,7 @@ ) from sqlmesh.core.user import User from sqlmesh.integrations.github.cicd.config import GithubCICDBotConfig -from sqlmesh.utils import word_characters_only +from sqlmesh.utils import word_characters_only, Verbosity from sqlmesh.utils.concurrency import NodeExecutionFailedError from sqlmesh.utils.date import now from sqlmesh.utils.errors import ( @@ -476,7 +476,7 @@ def run_tests(self) -> t.Tuple[unittest.result.TestResult, str]: """ Run tests for the PR """ - return self._context._run_tests(verbose=True) + return self._context._run_tests(verbosity=Verbosity.VERBOSE) def _get_or_create_comment(self, header: str = BOT_HEADER_MSG) -> IssueComment: comment = seq_get( diff --git a/sqlmesh/integrations/llm.py b/sqlmesh/integrations/llm.py index eb1c7148d5..a44ec79997 100644 --- a/sqlmesh/integrations/llm.py +++ b/sqlmesh/integrations/llm.py @@ -5,6 +5,7 @@ from langchain import LLMChain, PromptTemplate from langchain.chat_models import ChatOpenAI +from sqlmesh.utils import Verbosity from sqlmesh.core.model import Model _QUERY_PROMPT_TEMPLATE = """Given an input request, create a syntactically correct {dialect} SQL query. @@ -25,13 +26,15 @@ def __init__( models: t.Iterable[Model], dialect: str, temperature: float = 0.7, - verbose: bool = False, + verbosity: Verbosity = Verbosity.DEFAULT, ): query_prompt_template = PromptTemplate.from_template(_QUERY_PROMPT_TEMPLATE).partial( dialect=dialect, table_info=_to_table_info(models) ) llm = ChatOpenAI(temperature=temperature) # type: ignore - self._query_chain = LLMChain(llm=llm, prompt=query_prompt_template, verbose=verbose) + self._query_chain = LLMChain( + llm=llm, prompt=query_prompt_template, verbose=verbosity >= Verbosity.VERBOSE + ) def query(self, prompt: str) -> str: result = self._query_chain.predict(input=prompt).strip() diff --git a/sqlmesh/magics.py b/sqlmesh/magics.py index e6bf530822..54a532978f 100644 --- a/sqlmesh/magics.py +++ b/sqlmesh/magics.py @@ -24,7 +24,6 @@ from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring from IPython.utils.process import arg_split from rich.jupyter import JupyterRenderable - from sqlmesh.cli.example_project import ProjectTemplate, init_example_project from sqlmesh.core import analytics from sqlmesh.core import constants as c @@ -34,7 +33,7 @@ from sqlmesh.core.dialect import format_model_expressions, parse from sqlmesh.core.model import load_sql_based_model from sqlmesh.core.test import ModelTestMetadata, get_all_model_tests -from sqlmesh.utils import sqlglot_dialects, yaml +from sqlmesh.utils import sqlglot_dialects, yaml, Verbosity from sqlmesh.utils.errors import MagicError, MissingContextException, SQLMeshError logger = logging.getLogger(__name__) @@ -437,12 +436,21 @@ def test(self, context: Context, line: str, test_def_raw: t.Optional[str] = None action="store_true", help="Output text differences for the rendered versions of the models and standalone audits", ) + @argument( + "--verbose", + "-v", + action="count", + default=0, + help="Verbose output. Use -vv for very verbose.", + ) @line_magic @pass_sqlmesh_context def plan(self, context: Context, line: str) -> None: """Goes through a set of prompts to both establish a plan and apply it""" args = parse_argstring(self.plan, line) + setattr(context.console, "verbosity", Verbosity(args.verbose)) + context.plan( args.environment, start=args.start, @@ -962,7 +970,13 @@ def create_test(self, context: Context, line: str) -> None: type=str, help="Only run tests that match the pattern of substring.", ) - @argument("--verbose", "-v", action="store_true", help="Verbose output.") + @argument( + "--verbose", + "-v", + action="count", + default=0, + help="Verbose output. Use -vv for very verbose.", + ) @argument( "--preserve-fixtures", action="store_true", @@ -973,10 +987,11 @@ def create_test(self, context: Context, line: str) -> None: def run_test(self, context: Context, line: str) -> None: """Run unit test(s).""" args = parse_argstring(self.run_test, line) + context.test( match_patterns=args.pattern, tests=args.tests, - verbose=args.verbose, + verbosity=Verbosity(args.verbose), preserve_fixtures=args.preserve_fixtures, ) @@ -1003,13 +1018,19 @@ def audit(self, context: Context, line: str) -> None: help="Skip the connection test.", default=False, ) - @argument("--verbose", "-v", action="store_true", help="Verbose output.") + @argument( + "--verbose", + "-v", + action="count", + default=0, + help="Verbose output. Use -vv for very verbose.", + ) @line_magic @pass_sqlmesh_context def info(self, context: Context, line: str) -> None: """Display SQLMesh project information.""" args = parse_argstring(self.info, line) - context.print_info(skip_connection=args.skip_connection, verbose=args.verbose) + context.print_info(skip_connection=args.skip_connection, verbosity=Verbosity(args.verbose)) @magic_arguments() @line_magic diff --git a/sqlmesh/utils/__init__.py b/sqlmesh/utils/__init__.py index c5e2540b37..51624408cd 100644 --- a/sqlmesh/utils/__init__.py +++ b/sqlmesh/utils/__init__.py @@ -16,6 +16,7 @@ from collections import defaultdict from contextlib import contextmanager from copy import deepcopy +from enum import IntEnum from functools import lru_cache, reduce, wraps from pathlib import Path @@ -338,3 +339,11 @@ def type_is_known(d_type: t.Union[exp.DataType, exp.ColumnDef]) -> bool: def columns_to_types_all_known(columns_to_types: t.Dict[str, exp.DataType]) -> bool: """Checks that all column types are known and not NULL.""" return all(type_is_known(expression) for expression in columns_to_types.values()) + + +class Verbosity(IntEnum): + """Verbosity levels for SQLMesh output.""" + + DEFAULT = 0 + VERBOSE = 1 + VERY_VERBOSE = 2 diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index b4b8df06e1..80f3a4a493 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -274,6 +274,30 @@ def test_plan_verbose(runner, tmp_path): assert "sqlmesh_example.seed_model promoted" in result.output +def test_plan_very_verbose(runner, tmp_path, copy_to_temp_path): + temp_path = copy_to_temp_path("examples/sushi") + + # Input: `y` to apply and backfill + result = runner.invoke( + cli, + ["--log-file-dir", temp_path[0], "--paths", temp_path[0], "plan", "-v"], + input="y\n", + ) + assert result.exit_code == 0 + # models needing backfill list is still abbreviated with regular VERBOSE, so this should not be present + assert "sushi.customers: [full refresh]" not in result.output + + # Input: `y` to apply and backfill + result = runner.invoke( + cli, + ["--log-file-dir", temp_path[0], "--paths", temp_path[0], "plan", "-vv"], + input="y\n", + ) + assert result.exit_code == 0 + # models needing backfill list is complete with VERY_VERBOSE, so this should be present + assert "sushi.customers: [full refresh]" in result.output + + def test_plan_dev(runner, tmp_path): create_example_project(tmp_path) diff --git a/web/server/api/endpoints/commands.py b/web/server/api/endpoints/commands.py index 8a03c2eb57..6655d3ce55 100644 --- a/web/server/api/endpoints/commands.py +++ b/web/server/api/endpoints/commands.py @@ -7,7 +7,7 @@ import pandas as pd from fastapi import APIRouter, Body, Depends, Request, Response from starlette.status import HTTP_204_NO_CONTENT - +from sqlmesh.core.console import Verbosity from sqlmesh.core.context import Context from sqlmesh.core.snapshot.definition import SnapshotChangeCategory from sqlmesh.core.test import ModelTest @@ -136,14 +136,16 @@ async def render( @router.get("/test") async def test( test: t.Optional[str] = None, - verbose: bool = False, + verbosity: Verbosity = Verbosity.DEFAULT, context: Context = Depends(get_loaded_context), ) -> models.TestResult: """Run one or all model tests""" test_output = io.StringIO() try: result = context.test( - tests=[str(context.path / test)] if test else None, verbose=verbose, stream=test_output + tests=[str(context.path / test)] if test else None, + verbosity=verbosity, + stream=test_output, ) except Exception: import traceback