From a6004e74525f2670041969e685fdfd949f29e087 Mon Sep 17 00:00:00 2001 From: radu-mocanu Date: Thu, 26 Mar 2026 18:54:26 +0200 Subject: [PATCH] feat: add structured JSON output for all CLI commands (#768) Rename ConsoleLogger to CliLogger with output_mode support. When --format json is used, all console output is collected internally and emitted as structured JSON at command exit. Errors are caught by LazyGroup.invoke() and emitted as JSON to stderr. Co-Authored-By: Claude Sonnet 4.6 --- packages/uipath/src/uipath/_cli/__init__.py | 78 ++++ .../src/uipath/_cli/_auth/_auth_service.py | 4 +- .../src/uipath/_cli/_auth/_auth_session.py | 4 +- .../src/uipath/_cli/_auth/_oidc_utils.py | 4 +- .../src/uipath/_cli/_auth/_url_utils.py | 4 +- .../uipath/_cli/_evals/_progress_reporter.py | 4 +- .../src/uipath/_cli/_push/sw_file_handler.py | 4 +- .../uipath/src/uipath/_cli/_utils/_common.py | 8 +- .../uipath/src/uipath/_cli/_utils/_console.py | 81 +++- .../uipath/src/uipath/_cli/_utils/_debug.py | 4 +- .../uipath/src/uipath/_cli/_utils/_folders.py | 4 +- .../src/uipath/_cli/_utils/_formatters.py | 58 +-- .../src/uipath/_cli/_utils/_processes.py | 4 +- .../src/uipath/_cli/_utils/_project_files.py | 4 +- .../src/uipath/_cli/_utils/_resources.py | 4 +- .../src/uipath/_cli/_utils/_service_base.py | 26 +- .../src/uipath/_cli/_utils/_uv_helpers.py | 4 +- packages/uipath/src/uipath/_cli/cli_add.py | 4 +- packages/uipath/src/uipath/_cli/cli_auth.py | 4 +- packages/uipath/src/uipath/_cli/cli_debug.py | 4 +- packages/uipath/src/uipath/_cli/cli_dev.py | 4 +- packages/uipath/src/uipath/_cli/cli_eval.py | 4 +- packages/uipath/src/uipath/_cli/cli_init.py | 4 +- packages/uipath/src/uipath/_cli/cli_invoke.py | 4 +- packages/uipath/src/uipath/_cli/cli_new.py | 4 +- packages/uipath/src/uipath/_cli/cli_pack.py | 4 +- .../uipath/src/uipath/_cli/cli_publish.py | 4 +- packages/uipath/src/uipath/_cli/cli_pull.py | 4 +- packages/uipath/src/uipath/_cli/cli_push.py | 4 +- .../uipath/src/uipath/_cli/cli_register.py | 4 +- packages/uipath/src/uipath/_cli/cli_run.py | 4 +- packages/uipath/src/uipath/_cli/cli_server.py | 4 +- .../uipath/src/uipath/_cli/middlewares.py | 4 +- .../uipath/tests/cli/test_auth_session.py | 4 +- .../uipath/tests/cli/test_console_logger.py | 369 ++++++++++++++++++ .../tests/cli/test_json_output_integration.py | 63 +++ packages/uipath/tests/cli/test_pull.py | 12 +- packages/uipath/tests/cli/test_push.py | 16 +- 38 files changed, 703 insertions(+), 124 deletions(-) create mode 100644 packages/uipath/tests/cli/test_console_logger.py create mode 100644 packages/uipath/tests/cli/test_json_output_integration.py diff --git a/packages/uipath/src/uipath/_cli/__init__.py b/packages/uipath/src/uipath/_cli/__init__.py index f271fcf29..2d5fb5106 100644 --- a/packages/uipath/src/uipath/_cli/__init__.py +++ b/packages/uipath/src/uipath/_cli/__init__.py @@ -129,6 +129,78 @@ def get_command(self, ctx, cmd_name): return _load_command(cmd_name) return None + def invoke(self, ctx): + try: + result = super().invoke(ctx) + + # After successful command execution, emit collected output + cli_ctx = ctx.obj + if isinstance(cli_ctx, CliContext) and cli_ctx.output_format not in ( + "table", + "csv", + ): + from uipath._cli._utils._console import CliLogger + + logger = CliLogger() + logger.emit() + + return result + + except (SystemExit, click.exceptions.Exit): + raise + + except click.ClickException as e: + cli_ctx = getattr(ctx, "obj", None) + if isinstance(cli_ctx, CliContext) and cli_ctx.output_format not in ( + "table", + "csv", + ): + import json as json_mod + + from uipath._cli._utils._console import CliLogger + + logger = CliLogger() + error_output = {"status": "error", "error": e.format_message()} + if logger._messages: + error_output["messages"] = list(logger._messages) + + click.echo( + json_mod.dumps(error_output, indent=2, default=str), err=True + ) + ctx.exit(1) + else: + raise + + except Exception as e: + cli_ctx = getattr(ctx, "obj", None) + if isinstance(cli_ctx, CliContext) and cli_ctx.output_format not in ( + "table", + "csv", + ): + import json as json_mod + + from uipath._cli._utils._console import CLIError, CliLogger + + logger = CliLogger() + + if isinstance(e, CLIError): + messages = e.messages + error_msg = e.message + else: + messages = list(logger._messages) + error_msg = str(e) + + error_output = {"status": "error", "error": error_msg} + if messages: + error_output["messages"] = messages + + click.echo( + json_mod.dumps(error_output, indent=2, default=str), err=True + ) + ctx.exit(1) + else: + raise + def format_help(self, ctx, formatter): format_value = _get_format_from_argv() @@ -193,6 +265,12 @@ def cli( setup_logging(should_debug=debug) + if format != "table": + from uipath._cli._utils._console import CliLogger + + logger = CliLogger() + logger.output_mode = format # "json" or "csv" + if lv: try: version = importlib.metadata.version("uipath-langchain") diff --git a/packages/uipath/src/uipath/_cli/_auth/_auth_service.py b/packages/uipath/src/uipath/_cli/_auth/_auth_service.py index c89968683..aea20c377 100644 --- a/packages/uipath/src/uipath/_cli/_auth/_auth_service.py +++ b/packages/uipath/src/uipath/_cli/_auth/_auth_service.py @@ -6,7 +6,7 @@ from uipath._cli._auth._oidc_utils import OidcUtils from uipath._cli._auth._url_utils import extract_org_tenant, resolve_domain from uipath._cli._auth._utils import get_parsed_token_data -from uipath._cli._utils._console import ConsoleLogger +from uipath._cli._utils._console import CliLogger from uipath._utils._auth import update_env_file from uipath.platform.common import ExternalApplicationService, TokenData @@ -26,7 +26,7 @@ def __init__( scope: str | None = None, ): self._force = force - self._console = ConsoleLogger() + self._console = CliLogger() self._client_id = client_id self._client_secret = client_secret self._base_url = base_url diff --git a/packages/uipath/src/uipath/_cli/_auth/_auth_session.py b/packages/uipath/src/uipath/_cli/_auth/_auth_session.py index cd51a583b..7217ce488 100644 --- a/packages/uipath/src/uipath/_cli/_auth/_auth_session.py +++ b/packages/uipath/src/uipath/_cli/_auth/_auth_session.py @@ -17,7 +17,7 @@ ) from ..._utils._auth import update_env_file -from .._utils._console import ConsoleLogger +from .._utils._console import CliLogger from ._oidc_utils import OidcUtils from ._utils import get_auth_data, get_parsed_token_data, update_auth_file @@ -41,7 +41,7 @@ def __init__( self.domain = domain self.access_token = access_token self.prt_id = prt_id - self._console = ConsoleLogger() + self._console = CliLogger() self._identity_service = IdentityService(domain) self._portal_service = PlatformPortalService(domain) self._tenants_and_organizations = None diff --git a/packages/uipath/src/uipath/_cli/_auth/_oidc_utils.py b/packages/uipath/src/uipath/_cli/_auth/_oidc_utils.py index bb0be816a..cd483055d 100644 --- a/packages/uipath/src/uipath/_cli/_auth/_oidc_utils.py +++ b/packages/uipath/src/uipath/_cli/_auth/_oidc_utils.py @@ -6,7 +6,7 @@ from uipath.platform.orchestrator import get_server_info_async -from .._utils._console import ConsoleLogger +from .._utils._console import CliLogger from ._models import AuthConfig from ._url_utils import build_service_url @@ -88,7 +88,7 @@ async def _select_config_file(domain: str) -> str: class OidcUtils: - _console = ConsoleLogger() + _console = CliLogger() @classmethod def _find_free_port(cls, candidates: list[int]): diff --git a/packages/uipath/src/uipath/_cli/_auth/_url_utils.py b/packages/uipath/src/uipath/_cli/_auth/_url_utils.py index 844968762..ea5bd180d 100644 --- a/packages/uipath/src/uipath/_cli/_auth/_url_utils.py +++ b/packages/uipath/src/uipath/_cli/_auth/_url_utils.py @@ -2,9 +2,9 @@ from typing import Tuple from urllib.parse import urlparse -from .._utils._console import ConsoleLogger +from .._utils._console import CliLogger -console = ConsoleLogger() +console = CliLogger() def resolve_domain(base_url: str | None, environment: str | None) -> str: diff --git a/packages/uipath/src/uipath/_cli/_evals/_progress_reporter.py b/packages/uipath/src/uipath/_cli/_evals/_progress_reporter.py index 7c5114516..e76cd2d91 100644 --- a/packages/uipath/src/uipath/_cli/_evals/_progress_reporter.py +++ b/packages/uipath/src/uipath/_cli/_evals/_progress_reporter.py @@ -14,7 +14,7 @@ from pydantic.alias_generators import to_camel from rich.console import Console -from uipath._cli._utils._console import ConsoleLogger +from uipath._cli._utils._console import CliLogger from uipath._utils import Endpoint, RequestSpec from uipath._utils.constants import ( ENV_EVAL_BACKEND_URL, @@ -92,7 +92,7 @@ class StudioWebProgressReporter: def __init__(self): logging.getLogger("uipath._cli.middlewares").setLevel(logging.CRITICAL) - console_logger = ConsoleLogger.get_instance() + console_logger = CliLogger.get_instance() # Use UIPATH_EVAL_BACKEND_URL for eval-specific routing if set eval_backend_url = os.getenv(ENV_EVAL_BACKEND_URL) diff --git a/packages/uipath/src/uipath/_cli/_push/sw_file_handler.py b/packages/uipath/src/uipath/_cli/_push/sw_file_handler.py index 64a0e9b0e..188017629 100644 --- a/packages/uipath/src/uipath/_cli/_push/sw_file_handler.py +++ b/packages/uipath/src/uipath/_cli/_push/sw_file_handler.py @@ -13,7 +13,7 @@ from ...platform.errors import EnrichedException from .._utils._common import get_claim_from_token -from .._utils._console import ConsoleLogger +from .._utils._console import CliLogger from .._utils._constants import ( AGENT_INITIAL_CODE_VERSION, SCHEMA_VERSION, @@ -66,7 +66,7 @@ def __init__( """ self.directory = directory self.include_uv_lock = include_uv_lock - self.console = ConsoleLogger() + self.console = CliLogger() self._studio_client = studio_client or StudioClient(project_id) self._project_structure: ProjectStructure | None = None diff --git a/packages/uipath/src/uipath/_cli/_utils/_common.py b/packages/uipath/src/uipath/_cli/_utils/_common.py index 784192ab6..dde91a51e 100644 --- a/packages/uipath/src/uipath/_cli/_utils/_common.py +++ b/packages/uipath/src/uipath/_cli/_utils/_common.py @@ -17,7 +17,7 @@ from ..._utils.constants import ENV_UIPATH_ACCESS_TOKEN from ..models.runtime_schema import EntryPoint from ..spinner import Spinner -from ._console import ConsoleLogger +from ._console import CliLogger from ._studio_project import ( NonCodedAgentProjectException, StudioClient, @@ -126,7 +126,7 @@ def determine_project_type(entrypoints: list[EntryPoint]) -> str: chosen_type = entrypoints[0].type if len(unique_types) > 1: - console = ConsoleLogger() + console = CliLogger() types_str = ", ".join(sorted(unique_types)) console.warning( f"Mixed entrypoint types detected: [{types_str}]. " @@ -141,7 +141,7 @@ async def ensure_coded_agent_project(studio_client: StudioClient): try: await studio_client.ensure_coded_agent_project_async() except NonCodedAgentProjectException: - console = ConsoleLogger() + console = CliLogger() console.error( "The targeted Studio Web project is not of type coded agent. Please check the UIPATH_PROJECT_ID environment variable." ) @@ -182,7 +182,7 @@ async def may_override_files( except (ValueError, TypeError): formatted_date = remote_metadata.last_push_date - console = ConsoleLogger() + console = CliLogger() console.warning("Your local version is behind the remote version.") console.info(f" Remote version: {remote_metadata.code_version}") console.info(f" Local version: {local_version_display}") diff --git a/packages/uipath/src/uipath/_cli/_utils/_console.py b/packages/uipath/src/uipath/_cli/_utils/_console.py index d7ac1ee05..708ee2b98 100644 --- a/packages/uipath/src/uipath/_cli/_utils/_console.py +++ b/packages/uipath/src/uipath/_cli/_utils/_console.py @@ -32,25 +32,37 @@ class LogLevel(Enum): MAGIC = "✨" -class ConsoleLogger: +class CLIError(Exception): + """Raised by CliLogger.error() in non-console output modes. + + Caught by LazyGroup.invoke() to emit structured error output. + """ + + def __init__(self, message: str, messages: list[dict[str, str]] | None = None): + self.message = message + self.messages = messages or [] + super().__init__(message) + + +class CliLogger: """A singleton wrapper class for terminal output with emoji support and spinners.""" # Class variable to hold the singleton instance - _instance: ConsoleLogger | None = None + _instance: CliLogger | None = None - def __new__(cls: Type[ConsoleLogger]) -> ConsoleLogger: - """Ensure only one instance of ConsoleLogger is created. + def __new__(cls: Type[CliLogger]) -> CliLogger: + """Ensure only one instance of CliLogger is created. Returns: - The singleton instance of ConsoleLogger + The singleton instance of CliLogger """ if cls._instance is None: - cls._instance = super(ConsoleLogger, cls).__new__(cls) + cls._instance = super(CliLogger, cls).__new__(cls) cls._instance._initialized = False return cls._instance def __init__(self): - """Initialize the ConsoleLogger (only once).""" + """Initialize the CliLogger (only once).""" # Only initialize once if not getattr(self, "_initialized", False): self._console = Console() @@ -59,6 +71,21 @@ def __init__(self): self._progress: Progress | None = None self._progress_tasks: dict[str, TaskID] = {} self._initialized = True + self._output_mode: str = "console" + self._messages: list[dict[str, str]] = [] + self._result: Any = None + + @property + def output_mode(self) -> str: + """Get the current output mode.""" + return self._output_mode + + @output_mode.setter + def output_mode(self, value: str) -> None: + """Set the output mode and reset collected state.""" + self._output_mode = value + self._messages = [] + self._result = None def _stop_spinner_if_active(self) -> None: """Internal method to stop the spinner if it's active.""" @@ -83,6 +110,11 @@ def log( level: The log level (determines the emoji) fg: Optional foreground color for the message """ + if self._output_mode != "console": + level_name = level.name.lower() + self._messages.append({"level": level_name, "message": message}) + return + # Stop any active spinner before logging self._stop_spinner_if_active() @@ -119,6 +151,9 @@ def error(self, message: str, include_traceback: bool = False) -> NoReturn: ) message = f"{message}\n{tb}" + if self._output_mode != "console": + raise CLIError(message, messages=list(self._messages)) + self.log(message, LogLevel.ERROR, "red") click.get_current_context().exit(1) @@ -216,6 +251,10 @@ def spinner(self, message: str = "") -> Iterator[None]: Yields: None """ + if self._output_mode != "console": + yield + return + try: # Stop any existing spinner before starting a new one self._stop_spinner_if_active() @@ -280,9 +319,30 @@ def evaluation_progress( finally: self._stop_progress_if_active() + def set_result(self, data: Any) -> None: + """Store structured result data for JSON output.""" + self._result = data + + def emit(self) -> None: + """Emit collected output. Only does something in non-console modes.""" + if self._output_mode == "console": + return + + import json + + output: dict[str, Any] = {"status": "success"} + if self._messages: + output["messages"] = self._messages + if self._result is not None: + output["data"] = self._result + + click.echo(json.dumps(output, indent=2, default=str)) + self._messages = [] + self._result = None + @classmethod - def get_instance(cls) -> "ConsoleLogger": - """Get the singleton instance of ConsoleLogger. + def get_instance(cls) -> "CliLogger": + """Get the singleton instance of CliLogger. Returns: The singleton instance @@ -292,6 +352,9 @@ def get_instance(cls) -> "ConsoleLogger": return cls._instance +ConsoleLogger = CliLogger + + class EvaluationProgressManager: """Manager for evaluation progress updates.""" diff --git a/packages/uipath/src/uipath/_cli/_utils/_debug.py b/packages/uipath/src/uipath/_cli/_utils/_debug.py index 9759b7a66..eee6f1061 100644 --- a/packages/uipath/src/uipath/_cli/_utils/_debug.py +++ b/packages/uipath/src/uipath/_cli/_utils/_debug.py @@ -2,9 +2,9 @@ import os -from ._console import ConsoleLogger +from ._console import CliLogger -console = ConsoleLogger() +console = CliLogger() def setup_debugging(debug: bool, debug_port: int = 5678) -> bool: diff --git a/packages/uipath/src/uipath/_cli/_utils/_folders.py b/packages/uipath/src/uipath/_cli/_utils/_folders.py index 13aa96e97..ccc15bb7b 100644 --- a/packages/uipath/src/uipath/_cli/_utils/_folders.py +++ b/packages/uipath/src/uipath/_cli/_utils/_folders.py @@ -1,8 +1,8 @@ from typing import Any, Optional, Tuple -from ._console import ConsoleLogger +from ._console import CliLogger -console = ConsoleLogger() +console = CliLogger() async def get_personal_workspace_info_async() -> Tuple[Optional[str], Optional[str]]: diff --git a/packages/uipath/src/uipath/_cli/_utils/_formatters.py b/packages/uipath/src/uipath/_cli/_utils/_formatters.py index 042ce5b25..588036eb3 100644 --- a/packages/uipath/src/uipath/_cli/_utils/_formatters.py +++ b/packages/uipath/src/uipath/_cli/_utils/_formatters.py @@ -15,36 +15,16 @@ import click -def format_output( - data: Any, - fmt: str = "table", - output: Optional[str] = None, - no_color: bool = False, -) -> None: - """Format and output data to stdout or file. +def _normalize_data(data: Any) -> Any: + """Normalize data for structured output (model_dump, list conversion). Handles: - Pydantic models (via model_dump()) - Iterators and generators (converts to list) - - Lists, dicts, primitives - - Large datasets (warns at 10k+ items) - - Args: - data: Data to format - fmt: Output format (json, table, csv) - output: Optional file path to write to - no_color: Disable colored output for table format - - Example: - >>> format_output([{"name": "bucket1"}, {"name": "bucket2"}], fmt="table") - name - -------- - bucket1 - bucket2 + - Large dataset warnings """ if isinstance(data, (Iterator, Generator)): data = list(data) - # Warn about large datasets if len(data) > 10000: click.echo( f"Warning: Loading {len(data)} items into memory for formatting. " @@ -67,6 +47,38 @@ def format_output( else: data = [item.model_dump() for item in data] + return data + + +def format_output( + data: Any, + fmt: str = "table", + output: Optional[str] = None, + no_color: bool = False, +) -> None: + """Format and output data to stdout or file. + + Handles: + - Pydantic models (via model_dump()) + - Iterators and generators (converts to list) + - Lists, dicts, primitives + - Large datasets (warns at 10k+ items) + + Args: + data: Data to format + fmt: Output format (json, table, csv) + output: Optional file path to write to + no_color: Disable colored output for table format + + Example: + >>> format_output([{"name": "bucket1"}, {"name": "bucket2"}], fmt="table") + name + -------- + bucket1 + bucket2 + """ + data = _normalize_data(data) + if hasattr(data, "__aiter__"): raise TypeError( "Async iterators not supported in CLI output. " diff --git a/packages/uipath/src/uipath/_cli/_utils/_processes.py b/packages/uipath/src/uipath/_cli/_utils/_processes.py index 610da5ec4..8657ce355 100644 --- a/packages/uipath/src/uipath/_cli/_utils/_processes.py +++ b/packages/uipath/src/uipath/_cli/_utils/_processes.py @@ -5,9 +5,9 @@ import httpx from ..._utils._ssl_context import get_httpx_client_kwargs -from ._console import ConsoleLogger +from ._console import CliLogger -console = ConsoleLogger() +console = CliLogger() odata_top_filter = 25 diff --git a/packages/uipath/src/uipath/_cli/_utils/_project_files.py b/packages/uipath/src/uipath/_cli/_utils/_project_files.py index 01ec36de5..5dbe68fe6 100644 --- a/packages/uipath/src/uipath/_cli/_utils/_project_files.py +++ b/packages/uipath/src/uipath/_cli/_utils/_project_files.py @@ -13,7 +13,7 @@ from uipath._cli.models.uipath_json_schema import PackOptions, UiPathJsonConfig from uipath.platform.common import UiPathConfig -from .._utils._console import ConsoleLogger +from .._utils._console import CliLogger from ._constants import is_binary_file from ._studio_project import ( ProjectFile, @@ -73,7 +73,7 @@ class FileInfo(BaseModel): is_binary: bool -console = ConsoleLogger() +console = CliLogger() def get_project_config(directory: str) -> dict[str, Any]: diff --git a/packages/uipath/src/uipath/_cli/_utils/_resources.py b/packages/uipath/src/uipath/_cli/_utils/_resources.py index c161e70ba..5fad8582d 100644 --- a/packages/uipath/src/uipath/_cli/_utils/_resources.py +++ b/packages/uipath/src/uipath/_cli/_utils/_resources.py @@ -1,8 +1,8 @@ import enum -from ._console import ConsoleLogger +from ._console import CliLogger -console = ConsoleLogger().get_instance() +console = CliLogger().get_instance() class Resources(str, enum.Enum): diff --git a/packages/uipath/src/uipath/_cli/_utils/_service_base.py b/packages/uipath/src/uipath/_cli/_utils/_service_base.py index 018e2e112..9ef4accf4 100644 --- a/packages/uipath/src/uipath/_cli/_utils/_service_base.py +++ b/packages/uipath/src/uipath/_cli/_utils/_service_base.py @@ -104,17 +104,25 @@ def wrapper(ctx, *args, **kwargs): # Format and output result if result is not None: - from ._formatters import format_output + from ._console import CliLogger - fmt = kwargs.get("format") or cli_ctx.output_format - output = kwargs.get("output") + logger = CliLogger() + if logger.output_mode != "console": + from ._formatters import _normalize_data - format_output( - result, - fmt=fmt, - output=output, - no_color=False, # Auto-detected for file output - ) + logger.set_result(_normalize_data(result)) + else: + from ._formatters import format_output + + fmt = kwargs.get("format") or cli_ctx.output_format + output = kwargs.get("output") + + format_output( + result, + fmt=fmt, + output=output, + no_color=False, + ) return result diff --git a/packages/uipath/src/uipath/_cli/_utils/_uv_helpers.py b/packages/uipath/src/uipath/_cli/_utils/_uv_helpers.py index 9464fb4ef..644592bcf 100644 --- a/packages/uipath/src/uipath/_cli/_utils/_uv_helpers.py +++ b/packages/uipath/src/uipath/_cli/_utils/_uv_helpers.py @@ -1,9 +1,9 @@ import os import subprocess -from .._utils._console import ConsoleLogger +from .._utils._console import CliLogger -console = ConsoleLogger() +console = CliLogger() def handle_uv_operations(directory: str) -> None: diff --git a/packages/uipath/src/uipath/_cli/cli_add.py b/packages/uipath/src/uipath/_cli/cli_add.py index 3ddfa21d7..54488bc83 100644 --- a/packages/uipath/src/uipath/_cli/cli_add.py +++ b/packages/uipath/src/uipath/_cli/cli_add.py @@ -9,11 +9,11 @@ from uipath.eval.constants import EVALS_FOLDER from ._telemetry import track_command -from ._utils._console import ConsoleLogger +from ._utils._console import CliLogger from ._utils._resources import Resources logger = logging.getLogger(__name__) -console = ConsoleLogger() +console = CliLogger() def to_pascal_case(text: str) -> str: diff --git a/packages/uipath/src/uipath/_cli/cli_auth.py b/packages/uipath/src/uipath/_cli/cli_auth.py index 753ba9e85..f244c9a70 100644 --- a/packages/uipath/src/uipath/_cli/cli_auth.py +++ b/packages/uipath/src/uipath/_cli/cli_auth.py @@ -4,9 +4,9 @@ from ._auth._auth_service import AuthService from ._utils._common import environment_options -from ._utils._console import ConsoleLogger +from ._utils._console import CliLogger -console = ConsoleLogger() +console = CliLogger() @click.command() diff --git a/packages/uipath/src/uipath/_cli/cli_debug.py b/packages/uipath/src/uipath/_cli/cli_debug.py index 44415dd26..4e4a43cc2 100644 --- a/packages/uipath/src/uipath/_cli/cli_debug.py +++ b/packages/uipath/src/uipath/_cli/cli_debug.py @@ -22,10 +22,10 @@ from uipath.tracing import LiveTrackingSpanProcessor, LlmOpsHttpExporter from ._telemetry import track_command -from ._utils._console import ConsoleLogger +from ._utils._console import CliLogger from .middlewares import Middlewares -console = ConsoleLogger() +console = CliLogger() logger = logging.getLogger(__name__) diff --git a/packages/uipath/src/uipath/_cli/cli_dev.py b/packages/uipath/src/uipath/_cli/cli_dev.py index 62740dc4b..857a3f868 100644 --- a/packages/uipath/src/uipath/_cli/cli_dev.py +++ b/packages/uipath/src/uipath/_cli/cli_dev.py @@ -3,7 +3,7 @@ import click -from uipath._cli._utils._console import ConsoleLogger +from uipath._cli._utils._console import CliLogger from uipath._cli._utils._debug import setup_debugging from uipath._cli.middlewares import Middlewares from uipath.core.tracing import UiPathTraceManager @@ -11,7 +11,7 @@ from ._telemetry import track_command -console = ConsoleLogger() +console = CliLogger() def _check_dev_dependency(interface: str) -> None: diff --git a/packages/uipath/src/uipath/_cli/cli_eval.py b/packages/uipath/src/uipath/_cli/cli_eval.py index d0bdc730c..0ff0244e2 100644 --- a/packages/uipath/src/uipath/_cli/cli_eval.py +++ b/packages/uipath/src/uipath/_cli/cli_eval.py @@ -33,10 +33,10 @@ LlmOpsHttpExporter, ) -from ._utils._console import ConsoleLogger +from ._utils._console import CliLogger logger = logging.getLogger(__name__) -console = ConsoleLogger() +console = CliLogger() class LiteralOption(click.Option): diff --git a/packages/uipath/src/uipath/_cli/cli_init.py b/packages/uipath/src/uipath/_cli/cli_init.py index f86e30f06..f80266640 100644 --- a/packages/uipath/src/uipath/_cli/cli_init.py +++ b/packages/uipath/src/uipath/_cli/cli_init.py @@ -34,14 +34,14 @@ from ..telemetry._constants import _PROJECT_KEY, _TELEMETRY_CONFIG_FILE from ._telemetry import track_command from ._utils._common import determine_project_type -from ._utils._console import ConsoleLogger +from ._utils._console import CliLogger from ._utils._constants import AGENT_INITIAL_CODE_VERSION, SCHEMA_VERSION from ._utils._project_files import read_toml_project from .middlewares import Middlewares from .models.runtime_schema import Bindings, EntryPoint from .models.uipath_json_schema import UiPathJsonConfig -console = ConsoleLogger() +console = CliLogger() logger = logging.getLogger(__name__) CONFIG_PATH = "uipath.json" diff --git a/packages/uipath/src/uipath/_cli/cli_invoke.py b/packages/uipath/src/uipath/_cli/cli_invoke.py index 726b623d1..fbb462dd0 100644 --- a/packages/uipath/src/uipath/_cli/cli_invoke.py +++ b/packages/uipath/src/uipath/_cli/cli_invoke.py @@ -9,13 +9,13 @@ from .._utils._ssl_context import get_httpx_client_kwargs from ._telemetry import track_command from ._utils._common import get_env_vars -from ._utils._console import ConsoleLogger +from ._utils._console import CliLogger from ._utils._folders import get_personal_workspace_info_async from ._utils._processes import get_release_info from .middlewares import Middlewares logger = logging.getLogger(__name__) -console = ConsoleLogger() +console = CliLogger() def _read_project_details() -> tuple[str, str]: diff --git a/packages/uipath/src/uipath/_cli/cli_new.py b/packages/uipath/src/uipath/_cli/cli_new.py index 390c581c5..7c01f433c 100644 --- a/packages/uipath/src/uipath/_cli/cli_new.py +++ b/packages/uipath/src/uipath/_cli/cli_new.py @@ -5,10 +5,10 @@ import click from ._telemetry import track_command -from ._utils._console import ConsoleLogger +from ._utils._console import CliLogger from .middlewares import Middlewares -console = ConsoleLogger() +console = CliLogger() def generate_script(target_directory): diff --git a/packages/uipath/src/uipath/_cli/cli_pack.py b/packages/uipath/src/uipath/_cli/cli_pack.py index 83cedf870..b1083672f 100644 --- a/packages/uipath/src/uipath/_cli/cli_pack.py +++ b/packages/uipath/src/uipath/_cli/cli_pack.py @@ -15,7 +15,7 @@ from ..telemetry._constants import _PROJECT_KEY, _TELEMETRY_CONFIG_FILE from ._telemetry import track_command from ._utils._common import determine_project_type -from ._utils._console import ConsoleLogger +from ._utils._console import CliLogger from ._utils._project_files import ( ensure_config_file, files_to_include, @@ -25,7 +25,7 @@ ) from ._utils._uv_helpers import handle_uv_operations -console = ConsoleLogger() +console = CliLogger() schema = "https://cloud.uipath.com/draft/2024-12/entry-point" diff --git a/packages/uipath/src/uipath/_cli/cli_publish.py b/packages/uipath/src/uipath/_cli/cli_publish.py index 246977af1..0154bbd77 100644 --- a/packages/uipath/src/uipath/_cli/cli_publish.py +++ b/packages/uipath/src/uipath/_cli/cli_publish.py @@ -8,11 +8,11 @@ from .._utils._ssl_context import get_httpx_client_kwargs from ._telemetry import track_command from ._utils._common import get_env_vars -from ._utils._console import ConsoleLogger +from ._utils._console import CliLogger from ._utils._folders import get_personal_workspace_info_async from ._utils._processes import get_release_info -console = ConsoleLogger() +console = CliLogger() def get_most_recent_package(): diff --git a/packages/uipath/src/uipath/_cli/cli_pull.py b/packages/uipath/src/uipath/_cli/cli_pull.py index 1dcdd1b8b..b63608738 100644 --- a/packages/uipath/src/uipath/_cli/cli_pull.py +++ b/packages/uipath/src/uipath/_cli/cli_pull.py @@ -9,7 +9,7 @@ from ._telemetry import track_command from ._utils._common import ensure_coded_agent_project, may_override_files -from ._utils._console import ConsoleLogger +from ._utils._console import CliLogger from ._utils._project_files import ( ProjectPullError, pull_project, @@ -17,7 +17,7 @@ from ._utils._studio_project import StudioClient from .middlewares import Middlewares -console = ConsoleLogger() +console = CliLogger() @click.command() diff --git a/packages/uipath/src/uipath/_cli/cli_push.py b/packages/uipath/src/uipath/_cli/cli_push.py index 61e46cbc6..4797d9e32 100644 --- a/packages/uipath/src/uipath/_cli/cli_push.py +++ b/packages/uipath/src/uipath/_cli/cli_push.py @@ -11,7 +11,7 @@ from ._push.sw_file_handler import SwFileHandler from ._telemetry import track_command from ._utils._common import ensure_coded_agent_project, may_override_files -from ._utils._console import ConsoleLogger +from ._utils._console import CliLogger from ._utils._project_files import ( Severity, UpdateEvent, @@ -31,7 +31,7 @@ from .models.runtime_schema import Bindings from .models.uipath_json_schema import PackOptions -console = ConsoleLogger() +console = CliLogger() def get_org_scoped_url(base_url: str) -> str: diff --git a/packages/uipath/src/uipath/_cli/cli_register.py b/packages/uipath/src/uipath/_cli/cli_register.py index 49dc0e573..d81a78611 100644 --- a/packages/uipath/src/uipath/_cli/cli_register.py +++ b/packages/uipath/src/uipath/_cli/cli_register.py @@ -8,11 +8,11 @@ ) from ._telemetry import track_command -from ._utils._console import ConsoleLogger +from ._utils._console import CliLogger from ._utils._resources import Resources logger = logging.getLogger(__name__) -console = ConsoleLogger() +console = CliLogger() @click.command() diff --git a/packages/uipath/src/uipath/_cli/cli_run.py b/packages/uipath/src/uipath/_cli/cli_run.py index 6b5152ed4..e2b90ca41 100644 --- a/packages/uipath/src/uipath/_cli/cli_run.py +++ b/packages/uipath/src/uipath/_cli/cli_run.py @@ -28,10 +28,10 @@ ) from ._telemetry import track_command -from ._utils._console import ConsoleLogger +from ._utils._console import CliLogger from .middlewares import Middlewares -console = ConsoleLogger() +console = CliLogger() @click.command() diff --git a/packages/uipath/src/uipath/_cli/cli_server.py b/packages/uipath/src/uipath/_cli/cli_server.py index c32c6de11..8820b5244 100644 --- a/packages/uipath/src/uipath/_cli/cli_server.py +++ b/packages/uipath/src/uipath/_cli/cli_server.py @@ -14,12 +14,12 @@ from aiohttp import ClientSession, UnixConnector, web from ._telemetry import track_command -from ._utils._console import ConsoleLogger +from ._utils._console import CliLogger from .cli_debug import debug from .cli_eval import eval from .cli_run import run -console = ConsoleLogger() +console = CliLogger() SOCKET_ENV_VAR = "UIPATH_SERVER_SOCKET" DEFAULT_SOCKET_PATH = "/tmp/uipath-server.sock" diff --git a/packages/uipath/src/uipath/_cli/middlewares.py b/packages/uipath/src/uipath/_cli/middlewares.py index 9b291c266..0efd7aa0d 100644 --- a/packages/uipath/src/uipath/_cli/middlewares.py +++ b/packages/uipath/src/uipath/_cli/middlewares.py @@ -4,10 +4,10 @@ from dataclasses import dataclass from typing import Any, Callable -from ._utils._console import ConsoleLogger +from ._utils._console import CliLogger logger = logging.getLogger(__name__) -console = ConsoleLogger() +console = CliLogger() @dataclass diff --git a/packages/uipath/tests/cli/test_auth_session.py b/packages/uipath/tests/cli/test_auth_session.py index 58201945b..bc149e491 100644 --- a/packages/uipath/tests/cli/test_auth_session.py +++ b/packages/uipath/tests/cli/test_auth_session.py @@ -162,7 +162,7 @@ async def test_refresh_unauthorized(self, mock_auth_config): new_callable=AsyncMock, side_effect=error, ), - patch("uipath._cli._auth._auth_session.ConsoleLogger") as mock_logger_cls, + patch("uipath._cli._auth._auth_session.CliLogger") as mock_logger_cls, ): mock_console = Mock() mock_console.error.side_effect = SystemExit(1) @@ -196,7 +196,7 @@ async def test_refresh_server_error(self, mock_auth_config): new_callable=AsyncMock, side_effect=error, ), - patch("uipath._cli._auth._auth_session.ConsoleLogger") as mock_logger_cls, + patch("uipath._cli._auth._auth_session.CliLogger") as mock_logger_cls, ): mock_console = Mock() mock_console.error.side_effect = SystemExit(1) diff --git a/packages/uipath/tests/cli/test_console_logger.py b/packages/uipath/tests/cli/test_console_logger.py new file mode 100644 index 000000000..93a39f33c --- /dev/null +++ b/packages/uipath/tests/cli/test_console_logger.py @@ -0,0 +1,369 @@ +"""Tests for CliLogger (CLI logging) current and JSON mode behavior.""" + +import json + +import click +import pytest +from click.testing import CliRunner + +from uipath._cli._utils._console import CLIError, CliLogger, LogLevel + + +@pytest.fixture(autouse=True) +def reset_console_singleton(): + """Reset CliLogger singleton between tests.""" + CliLogger._instance = None + yield + CliLogger._instance = None + + +@pytest.fixture +def console(): + return CliLogger() + + +class TestSingleton: + def test_returns_same_instance(self): + a = CliLogger() + b = CliLogger() + assert a is b + + def test_get_instance_returns_singleton(self): + instance = CliLogger.get_instance() + assert instance is CliLogger() + + def test_get_instance_creates_if_none(self): + assert CliLogger._instance is None + instance = CliLogger.get_instance() + assert instance is not None + + +class TestLogOutput: + def test_info_writes_to_stdout(self, console): + @click.command() + def cmd(): + console.info("hello world") + + runner = CliRunner() + result = runner.invoke(cmd) + assert result.exit_code == 0 + assert "hello world" in result.output + + def test_success_writes_to_stdout(self, console): + @click.command() + def cmd(): + console.success("it worked") + + runner = CliRunner() + result = runner.invoke(cmd) + assert result.exit_code == 0 + assert "it worked" in result.output + + def test_warning_writes_to_stdout(self, console): + @click.command() + def cmd(): + console.warning("be careful") + + runner = CliRunner() + result = runner.invoke(cmd) + assert result.exit_code == 0 + assert "be careful" in result.output + + def test_hint_writes_to_stdout(self, console): + @click.command() + def cmd(): + console.hint("try this") + + runner = CliRunner() + result = runner.invoke(cmd) + assert result.exit_code == 0 + assert "try this" in result.output + + def test_magic_writes_to_stdout(self, console): + @click.command() + def cmd(): + console.magic("sparkles") + + runner = CliRunner() + result = runner.invoke(cmd) + assert result.exit_code == 0 + assert "sparkles" in result.output + + +class TestErrorBehavior: + def test_error_exits_with_code_1(self, console): + @click.command() + def cmd(): + console.error("something broke") + + runner = CliRunner() + result = runner.invoke(cmd) + assert result.exit_code == 1 + + def test_error_message_in_output(self, console): + @click.command() + def cmd(): + console.error("something broke") + + runner = CliRunner() + result = runner.invoke(cmd) + assert "something broke" in result.output + + def test_error_with_traceback(self, console): + @click.command() + def cmd(): + try: + raise ValueError("root cause") + except ValueError: + console.error("wrapper message", include_traceback=True) + + runner = CliRunner() + result = runner.invoke(cmd) + assert result.exit_code == 1 + assert "wrapper message" in result.output + assert "root cause" in result.output + + +class TestSpinner: + def test_spinner_context_manager(self, console): + @click.command() + def cmd(): + with console.spinner("loading..."): + pass + console.info("done") + + runner = CliRunner() + result = runner.invoke(cmd) + assert result.exit_code == 0 + assert "done" in result.output + + def test_spinner_stops_on_exception(self, console): + @click.command() + def cmd(): + try: + with console.spinner("loading..."): + raise RuntimeError("boom") + except RuntimeError: + pass + console.info("recovered") + + runner = CliRunner() + result = runner.invoke(cmd) + assert result.exit_code == 0 + assert "recovered" in result.output + + +class TestDisplayOptions: + def test_display_options_shows_items(self, console): + @click.command() + def cmd(): + console.display_options(["alpha", "beta", "gamma"], "Pick one:") + + runner = CliRunner() + result = runner.invoke(cmd) + assert result.exit_code == 0 + assert "alpha" in result.output + assert "0:" in result.output + + +class TestLink: + def test_link_includes_url(self, console): + @click.command() + def cmd(): + console.link("Click here:", "https://example.com") + + runner = CliRunner() + result = runner.invoke(cmd) + assert result.exit_code == 0 + assert "https://example.com" in result.output + + +class TestLogLevels: + def test_info_level_has_no_emoji(self): + assert LogLevel.INFO.value == "" + + def test_warning_level_has_emoji(self): + assert LogLevel.WARNING.value == "\u26a0\ufe0f" + + def test_error_level_has_emoji(self): + assert LogLevel.ERROR.value == "\u274c" + + +class TestCliLoggerJsonMode: + """Tests for CliLogger with output_mode='json'.""" + + def test_default_output_mode_is_console(self): + logger = CliLogger() + assert logger.output_mode == "console" + + def test_set_output_mode_json(self): + logger = CliLogger() + logger.output_mode = "json" + assert logger.output_mode == "json" + + def test_info_in_json_mode_does_not_print(self): + @click.command() + def cmd(): + logger = CliLogger() + logger.output_mode = "json" + logger.info("should not appear") + + runner = CliRunner() + result = runner.invoke(cmd) + assert "should not appear" not in result.output + + def test_success_in_json_mode_stores_message(self): + @click.command() + def cmd(): + logger = CliLogger() + logger.output_mode = "json" + logger.success("it worked") + assert len(logger._messages) == 1 + assert logger._messages[0]["level"] == "success" + assert logger._messages[0]["message"] == "it worked" + + runner = CliRunner() + result = runner.invoke(cmd) + assert result.exit_code == 0 + + def test_error_in_json_mode_raises_cli_error(self): + logger = CliLogger() + logger.output_mode = "json" + with pytest.raises(CLIError) as exc_info: + logger.error("something broke") + assert exc_info.value.message == "something broke" + + def test_error_in_json_mode_with_traceback(self): + logger = CliLogger() + logger.output_mode = "json" + try: + raise ValueError("root cause") + except ValueError: + with pytest.raises(CLIError) as exc_info: + logger.error("wrapper", include_traceback=True) + assert "root cause" in exc_info.value.message + + def test_set_result_stores_data(self): + logger = CliLogger() + logger.output_mode = "json" + logger.set_result({"key": "bucket1", "name": "Production"}) + assert logger._result == {"key": "bucket1", "name": "Production"} + + def test_emit_success_json(self): + @click.command() + def cmd(): + logger = CliLogger() + logger.output_mode = "json" + logger.info("loading...") + logger.set_result({"name": "test"}) + logger.emit() + + runner = CliRunner() + result = runner.invoke(cmd) + output = json.loads(result.output) + assert output["status"] == "success" + assert output["data"] == {"name": "test"} + assert len(output["messages"]) == 1 + assert output["messages"][0]["level"] == "info" + + def test_emit_success_without_data(self): + @click.command() + def cmd(): + logger = CliLogger() + logger.output_mode = "json" + logger.success("done") + logger.emit() + + runner = CliRunner() + result = runner.invoke(cmd) + output = json.loads(result.output) + assert output["status"] == "success" + assert "data" not in output + assert len(output["messages"]) == 1 + + def test_emit_in_console_mode_is_noop(self): + @click.command() + def cmd(): + logger = CliLogger() + logger.emit() + click.echo("still works") + + runner = CliRunner() + result = runner.invoke(cmd) + assert "still works" in result.output + + def test_spinner_in_json_mode_is_noop(self): + @click.command() + def cmd(): + logger = CliLogger() + logger.output_mode = "json" + with logger.spinner("loading..."): + pass + logger.emit() + + runner = CliRunner() + result = runner.invoke(cmd) + output = json.loads(result.output) + assert output["status"] == "success" + + def test_warning_in_json_mode_stores_message(self): + @click.command() + def cmd(): + logger = CliLogger() + logger.output_mode = "json" + logger.warning("be careful") + assert logger._messages[0]["level"] == "warning" + + runner = CliRunner() + result = runner.invoke(cmd) + assert result.exit_code == 0 + + +class TestCLIError: + def test_cli_error_has_message(self): + err = CLIError("test error") + assert err.message == "test error" + assert str(err) == "test error" + + def test_cli_error_stores_collected_messages(self): + messages = [{"level": "info", "message": "step 1"}] + err = CLIError("failed", messages=messages) + assert err.messages == messages + + +class TestLazyGroupJsonOutput: + """Tests for LazyGroup JSON output integration.""" + + def test_cli_help_with_format_json_still_works(self): + """--format json --help still produces help output without crashing.""" + from uipath._cli import cli + + runner = CliRunner() + result = runner.invoke(cli, ["--format", "json", "--help"]) + assert result.exit_code == 0 + # Help output is produced (either JSON or text depending on sys.argv) + assert len(result.output) > 0 + + def test_cli_error_structure(self): + """CLIError creates proper error output structure.""" + logger = CliLogger() + logger.output_mode = "json" + logger.info("step 1") + + error = CLIError("auth failed", messages=logger._messages) + + error_output = { + "status": "error", + "error": error.message, + "messages": error.messages, + } + assert error_output["status"] == "error" + assert error_output["error"] == "auth failed" + assert len(error_output["messages"]) == 1 + + +class TestBackwardsCompat: + def test_console_logger_alias_exists(self): + from uipath._cli._utils._console import ConsoleLogger + + assert ConsoleLogger is CliLogger diff --git a/packages/uipath/tests/cli/test_json_output_integration.py b/packages/uipath/tests/cli/test_json_output_integration.py new file mode 100644 index 000000000..91dc43c64 --- /dev/null +++ b/packages/uipath/tests/cli/test_json_output_integration.py @@ -0,0 +1,63 @@ +"""Integration tests for --format json across CLI commands.""" + +import json +from unittest.mock import patch + +import pytest +from click.testing import CliRunner + +from uipath._cli import cli +from uipath._cli._utils._console import CliLogger + + +@pytest.fixture(autouse=True) +def reset_cli_logger(): + """Reset CliLogger singleton between tests.""" + CliLogger._instance = None + yield + CliLogger._instance = None + + +class TestJsonOutputIntegration: + def test_help_json(self): + """Top-level --help with --format json returns valid JSON.""" + runner = CliRunner() + # _get_format_from_argv reads sys.argv, so we must patch it + with patch("sys.argv", ["uipath", "--format", "json", "--help"]): + result = runner.invoke(cli, ["--format", "json", "--help"]) + output = json.loads(result.output) + assert "name" in output + assert "commands" in output + + def test_version_still_works_with_format_json(self): + """--version still works with --format json.""" + runner = CliRunner() + result = runner.invoke(cli, ["--format", "json", "--version"]) + assert result.exit_code == 0 + + def test_assets_list_no_auth_json_error(self): + """assets list without auth returns error exit code.""" + runner = CliRunner() + result = runner.invoke(cli, ["--format", "json", "assets", "list"]) + assert result.exit_code != 0 + + def test_buckets_list_no_auth_json_error(self): + """buckets list without auth returns error exit code.""" + runner = CliRunner() + result = runner.invoke(cli, ["--format", "json", "buckets", "list"]) + assert result.exit_code != 0 + + def test_default_format_is_table(self): + """Default --format is table (no JSON).""" + runner = CliRunner() + result = runner.invoke(cli, ["--help"]) + # Default help should NOT be JSON + assert result.exit_code == 0 + assert "UiPath CLI" in result.output + + def test_subcommand_help_with_format_json(self): + """Subcommand help with --format json doesn't crash.""" + runner = CliRunner() + result = runner.invoke(cli, ["--format", "json", "assets", "--help"]) + assert result.exit_code == 0 + assert "assets" in result.output.lower() diff --git a/packages/uipath/tests/cli/test_pull.py b/packages/uipath/tests/cli/test_pull.py index 7b3aebd1a..f42b95e4c 100644 --- a/packages/uipath/tests/cli/test_pull.py +++ b/packages/uipath/tests/cli/test_pull.py @@ -614,9 +614,7 @@ async def test_no_local_metadata_prompts_user_confirms(self, tmp_path, monkeypat ) # Mock console.confirm to return True - with patch( - "uipath._cli._utils._common.ConsoleLogger" - ) as mock_console_class: + with patch("uipath._cli._utils._common.CliLogger") as mock_console_class: mock_console = mock_console_class.return_value mock_console.confirm.return_value = True @@ -643,9 +641,7 @@ async def test_no_local_metadata_prompts_user_denies(self, tmp_path): tmp_path / ".uipath" / "metadata.json" ) - with patch( - "uipath._cli._utils._common.ConsoleLogger" - ) as mock_console_class: + with patch("uipath._cli._utils._common.CliLogger") as mock_console_class: mock_console = mock_console_class.return_value mock_console.confirm.return_value = False @@ -711,9 +707,7 @@ async def test_local_version_less_than_remote_prompts_user(self, tmp_path): with patch("uipath._cli._utils._common.UiPathConfig") as mock_config: mock_config.studio_metadata_file_path = metadata_file - with patch( - "uipath._cli._utils._common.ConsoleLogger" - ) as mock_console_class: + with patch("uipath._cli._utils._common.CliLogger") as mock_console_class: mock_console = mock_console_class.return_value mock_console.confirm.return_value = True diff --git a/packages/uipath/tests/cli/test_push.py b/packages/uipath/tests/cli/test_push.py index d2189098d..f8473cc2e 100644 --- a/packages/uipath/tests/cli/test_push.py +++ b/packages/uipath/tests/cli/test_push.py @@ -2524,9 +2524,7 @@ async def test_no_local_metadata_prompts_user_confirms(self, tmp_path): tmp_path / ".uipath" / "metadata.json" ) - with patch( - "uipath._cli._utils._common.ConsoleLogger" - ) as mock_console_class: + with patch("uipath._cli._utils._common.CliLogger") as mock_console_class: mock_console = mock_console_class.return_value mock_console.confirm.return_value = True @@ -2556,9 +2554,7 @@ async def test_no_local_metadata_prompts_user_denies(self, tmp_path): tmp_path / ".uipath" / "metadata.json" ) - with patch( - "uipath._cli._utils._common.ConsoleLogger" - ) as mock_console_class: + with patch("uipath._cli._utils._common.CliLogger") as mock_console_class: mock_console = mock_console_class.return_value mock_console.confirm.return_value = False @@ -2624,9 +2620,7 @@ async def test_local_version_less_than_remote_prompts_user(self, tmp_path): with patch("uipath._cli._utils._common.UiPathConfig") as mock_config: mock_config.studio_metadata_file_path = metadata_file - with patch( - "uipath._cli._utils._common.ConsoleLogger" - ) as mock_console_class: + with patch("uipath._cli._utils._common.CliLogger") as mock_console_class: mock_console = mock_console_class.return_value mock_console.confirm.return_value = True @@ -2684,9 +2678,7 @@ async def test_date_formatting_in_warning(self, tmp_path): tmp_path / ".uipath" / "metadata.json" ) - with patch( - "uipath._cli._utils._common.ConsoleLogger" - ) as mock_console_class: + with patch("uipath._cli._utils._common.CliLogger") as mock_console_class: mock_console = mock_console_class.return_value mock_console.confirm.return_value = True