From 1cde2d060454253414072552eb74561df5bdcf23 Mon Sep 17 00:00:00 2001 From: Patryk Czajka Date: Tue, 11 Jun 2024 10:43:52 +0200 Subject: [PATCH] Refactor: factorise plugins app implementation (#1166) * Add e2e test * SnowTyperCreator class * connection * cortex * git * notebooks * snowpark * sql * stage * streamlit * object.stage * object * add typing * update release notes * fix decorator * Refactor: replace SnowTyper implementation * fix mock * refactor * fix test_adds_init_command * refactor: renames * Add comment * fix integration tests --- RELEASE-NOTES.md | 2 +- .../api/commands/project_initialisation.py | 7 +- src/snowflake/cli/api/commands/snow_typer.py | 77 ++++++++++++++++++- .../cli/plugins/connection/commands.py | 4 +- .../cli/plugins/connection/plugin_spec.py | 2 +- src/snowflake/cli/plugins/cortex/commands.py | 4 +- .../cli/plugins/cortex/plugin_spec.py | 2 +- src/snowflake/cli/plugins/git/commands.py | 4 +- src/snowflake/cli/plugins/git/plugin_spec.py | 2 +- .../cli/plugins/nativeapp/commands.py | 4 +- .../cli/plugins/nativeapp/plugin_spec.py | 2 +- .../cli/plugins/nativeapp/version/commands.py | 4 +- .../cli/plugins/notebook/commands.py | 4 +- .../cli/plugins/notebook/plugin_spec.py | 2 +- src/snowflake/cli/plugins/object/__init__.py | 11 --- .../cli/plugins/object/command_aliases.py | 12 +-- src/snowflake/cli/plugins/object/commands.py | 4 +- .../cli/plugins/object/plugin_spec.py | 4 +- .../object_stage_deprecated/commands.py | 4 +- .../object_stage_deprecated/plugin_spec.py | 6 +- .../cli/plugins/snowpark/__init__.py | 4 - .../cli/plugins/snowpark/commands.py | 6 +- .../cli/plugins/snowpark/package/commands.py | 4 +- .../cli/plugins/snowpark/plugin_spec.py | 4 +- src/snowflake/cli/plugins/spcs/__init__.py | 4 +- .../cli/plugins/spcs/compute_pool/commands.py | 4 +- .../plugins/spcs/image_registry/commands.py | 4 +- .../plugins/spcs/image_repository/commands.py | 4 +- .../cli/plugins/spcs/jobs/commands.py | 6 +- src/snowflake/cli/plugins/spcs/plugin_spec.py | 2 +- .../cli/plugins/spcs/services/commands.py | 4 +- src/snowflake/cli/plugins/sql/commands.py | 4 +- src/snowflake/cli/plugins/sql/plugin_spec.py | 2 +- src/snowflake/cli/plugins/stage/commands.py | 4 +- .../cli/plugins/stage/plugin_spec.py | 4 +- .../cli/plugins/streamlit/commands.py | 4 +- .../cli/plugins/streamlit/plugin_spec.py | 2 +- tests/api/commands/test_snow_typer.py | 25 +++--- tests/test_project_initialisation.py | 6 +- .../__snapshots__/test_error_handling.ambr | 10 +++ tests_e2e/config/malformatted_config.toml | 5 -- tests_e2e/test_error_handling.py | 28 +++++++ .../test_override_by_external_plugins.py | 4 +- 43 files changed, 203 insertions(+), 102 deletions(-) create mode 100644 tests_e2e/__snapshots__/test_error_handling.ambr delete mode 100755 tests_e2e/config/malformatted_config.toml diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index a8a0e26ab..4f0157f59 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -13,9 +13,9 @@ * Template variables can now be used anywhere in the the project definition file. ## Fixes and improvements +* Fixed error handling for malformatted `config.toml` * Fixed ZIP packaging of Snowpark project dependencies containing implicit namespace packages like `snowflake`. - # v2.4.0 ## Backward incompatibility diff --git a/src/snowflake/cli/api/commands/project_initialisation.py b/src/snowflake/cli/api/commands/project_initialisation.py index 7a661ee1b..6cb38ab2c 100644 --- a/src/snowflake/cli/api/commands/project_initialisation.py +++ b/src/snowflake/cli/api/commands/project_initialisation.py @@ -2,7 +2,7 @@ from typing import Optional -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.constants import TEMPLATES_PATH from snowflake.cli.api.output.types import CommandResult, MessageResult from snowflake.cli.api.secure_path import SecurePath @@ -16,7 +16,10 @@ def _create_project_template(template_name: str, project_directory: str): def add_init_command( - app: SnowTyper, project_type: str, template: str, help_message: Optional[str] = None + app: SnowTyperFactory, + project_type: str, + template: str, + help_message: Optional[str] = None, ): @app.command() def init( diff --git a/src/snowflake/cli/api/commands/snow_typer.py b/src/snowflake/cli/api/commands/snow_typer.py index 4f9b28fed..5fd8d54cf 100644 --- a/src/snowflake/cli/api/commands/snow_typer.py +++ b/src/snowflake/cli/api/commands/snow_typer.py @@ -1,8 +1,9 @@ from __future__ import annotations +import dataclasses import logging from functools import wraps -from typing import Callable, Optional +from typing import Any, Callable, Dict, List, Optional, Tuple import typer from snowflake.cli.api.commands.decorators import ( @@ -111,3 +112,77 @@ def post_execute(): log.debug("Executing command post execution callback") flush_telemetry() + + +@dataclasses.dataclass +class SnowTyperCommandData: + """ + Class for storing data of commands to be registered in SnowTyper instances created by SnowTyperFactory. + """ + + func: Callable + args: Tuple[Any, ...] + kwargs: Dict[str, Any] + + +class SnowTyperFactory: + """ + SnowTyper factory. Usage is similar to SnowTyper, except that create_instance() + creates actual SnowTyper instance. + """ + + def __init__( + self, + /, + name: Optional[str] = None, + help: Optional[str] = None, # noqa: A002 + short_help: Optional[str] = None, + is_hidden: Optional[Callable[[], bool]] = None, + deprecated: bool = False, + ): + self.name = name + self.help = help + self.short_help = short_help + self.is_hidden = is_hidden + self.deprecated = deprecated + self.commands_to_register: List[SnowTyperCommandData] = [] + self.subapps_to_register: List[SnowTyperFactory] = [] + self.callbacks_to_register: List[Callable] = [] + + def create_instance(self) -> SnowTyper: + app = SnowTyper( + name=self.name, + help=self.help, + short_help=self.short_help, + hidden=self.is_hidden() if self.is_hidden else False, + deprecated=self.deprecated, + ) + # register commands + for command in self.commands_to_register: + app.command(*command.args, **command.kwargs)(command.func) + # register callbacks + for callback in self.callbacks_to_register: + app.callback()(callback) + # add subgroups + for subapp in self.subapps_to_register: + app.add_typer(subapp.create_instance()) + return app + + def command(self, *args, **kwargs): + def decorator(command): + self.commands_to_register.append( + SnowTyperCommandData(command, args=args, kwargs=kwargs) + ) + return command + + return decorator + + def add_typer(self, snow_typer: SnowTyperFactory) -> None: + self.subapps_to_register.append(snow_typer) + + def callback(self): + def decorator(callback): + self.callbacks_to_register.append(callback) + return callback + + return decorator diff --git a/src/snowflake/cli/plugins/connection/commands.py b/src/snowflake/cli/plugins/connection/commands.py index b2e555e14..67f562828 100644 --- a/src/snowflake/cli/plugins/connection/commands.py +++ b/src/snowflake/cli/plugins/connection/commands.py @@ -10,7 +10,7 @@ from snowflake.cli.api.commands.flags import ( PLAIN_PASSWORD_MSG, ) -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.config import ( ConnectionConfig, add_connection, @@ -32,7 +32,7 @@ from snowflake.connector import ProgrammingError from snowflake.connector.config_manager import CONFIG_MANAGER -app = SnowTyper( +app = SnowTyperFactory( name="connection", help="Manages connections to Snowflake.", ) diff --git a/src/snowflake/cli/plugins/connection/plugin_spec.py b/src/snowflake/cli/plugins/connection/plugin_spec.py index 87221d6f6..fc1cd01e6 100644 --- a/src/snowflake/cli/plugins/connection/plugin_spec.py +++ b/src/snowflake/cli/plugins/connection/plugin_spec.py @@ -12,5 +12,5 @@ def command_spec(): return CommandSpec( parent_command_path=SNOWCLI_ROOT_COMMAND_PATH, command_type=CommandType.COMMAND_GROUP, - typer_instance=commands.app, + typer_instance=commands.app.create_instance(), ) diff --git a/src/snowflake/cli/plugins/cortex/commands.py b/src/snowflake/cli/plugins/cortex/commands.py index 4a2ba04ee..295d3fb51 100644 --- a/src/snowflake/cli/plugins/cortex/commands.py +++ b/src/snowflake/cli/plugins/cortex/commands.py @@ -9,7 +9,7 @@ from click import UsageError from snowflake.cli.api.cli_global_context import cli_context from snowflake.cli.api.commands.flags import readable_file_option -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.output.types import ( CollectionResult, CommandResult, @@ -26,7 +26,7 @@ Text, ) -app = SnowTyper( +app = SnowTyperFactory( name="cortex", help="Provides access to Snowflake Cortex.", ) diff --git a/src/snowflake/cli/plugins/cortex/plugin_spec.py b/src/snowflake/cli/plugins/cortex/plugin_spec.py index ce27955d4..b9aafe198 100644 --- a/src/snowflake/cli/plugins/cortex/plugin_spec.py +++ b/src/snowflake/cli/plugins/cortex/plugin_spec.py @@ -12,5 +12,5 @@ def command_spec(): return CommandSpec( parent_command_path=SNOWCLI_ROOT_COMMAND_PATH, command_type=CommandType.COMMAND_GROUP, - typer_instance=commands.app, + typer_instance=commands.app.create_instance(), ) diff --git a/src/snowflake/cli/plugins/git/commands.py b/src/snowflake/cli/plugins/git/commands.py index a1a8853ac..89df8985a 100644 --- a/src/snowflake/cli/plugins/git/commands.py +++ b/src/snowflake/cli/plugins/git/commands.py @@ -12,7 +12,7 @@ identifier_argument, like_option, ) -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.console.console import cli_console from snowflake.cli.api.constants import ObjectType from snowflake.cli.api.output.types import CollectionResult, CommandResult, QueryResult @@ -26,7 +26,7 @@ from snowflake.cli.plugins.stage.commands import get from snowflake.cli.plugins.stage.manager import OnErrorType -app = SnowTyper( +app = SnowTyperFactory( name="git", help="Manages git repositories in Snowflake.", ) diff --git a/src/snowflake/cli/plugins/git/plugin_spec.py b/src/snowflake/cli/plugins/git/plugin_spec.py index f5aead910..6e6547712 100644 --- a/src/snowflake/cli/plugins/git/plugin_spec.py +++ b/src/snowflake/cli/plugins/git/plugin_spec.py @@ -12,5 +12,5 @@ def command_spec(): return CommandSpec( parent_command_path=SNOWCLI_ROOT_COMMAND_PATH, command_type=CommandType.COMMAND_GROUP, - typer_instance=commands.app, + typer_instance=commands.app.create_instance(), ) diff --git a/src/snowflake/cli/plugins/nativeapp/commands.py b/src/snowflake/cli/plugins/nativeapp/commands.py index 98d73322d..82b6c04ad 100644 --- a/src/snowflake/cli/plugins/nativeapp/commands.py +++ b/src/snowflake/cli/plugins/nativeapp/commands.py @@ -9,7 +9,7 @@ from snowflake.cli.api.commands.decorators import ( with_project_definition, ) -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.output.types import ( CollectionResult, CommandResult, @@ -37,7 +37,7 @@ ) from snowflake.cli.plugins.nativeapp.version.commands import app as versions_app -app = SnowTyper( +app = SnowTyperFactory( name="app", help="Manages a Snowflake Native App", ) diff --git a/src/snowflake/cli/plugins/nativeapp/plugin_spec.py b/src/snowflake/cli/plugins/nativeapp/plugin_spec.py index 8227d6809..55c956281 100644 --- a/src/snowflake/cli/plugins/nativeapp/plugin_spec.py +++ b/src/snowflake/cli/plugins/nativeapp/plugin_spec.py @@ -12,5 +12,5 @@ def command_spec(): return CommandSpec( parent_command_path=SNOWCLI_ROOT_COMMAND_PATH, command_type=CommandType.COMMAND_GROUP, - typer_instance=commands.app, + typer_instance=commands.app.create_instance(), ) diff --git a/src/snowflake/cli/plugins/nativeapp/version/commands.py b/src/snowflake/cli/plugins/nativeapp/version/commands.py index 1efe24ef5..8c1380646 100644 --- a/src/snowflake/cli/plugins/nativeapp/version/commands.py +++ b/src/snowflake/cli/plugins/nativeapp/version/commands.py @@ -9,7 +9,7 @@ from snowflake.cli.api.commands.decorators import ( with_project_definition, ) -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.output.types import CommandResult, MessageResult, QueryResult from snowflake.cli.plugins.nativeapp.common_flags import ForceOption, InteractiveOption from snowflake.cli.plugins.nativeapp.policy import ( @@ -23,7 +23,7 @@ NativeAppVersionDropProcessor, ) -app = SnowTyper( +app = SnowTyperFactory( name="version", help="Manages versions defined in an application package", ) diff --git a/src/snowflake/cli/plugins/notebook/commands.py b/src/snowflake/cli/plugins/notebook/commands.py index b11c4f877..6b9a27000 100644 --- a/src/snowflake/cli/plugins/notebook/commands.py +++ b/src/snowflake/cli/plugins/notebook/commands.py @@ -2,13 +2,13 @@ import typer from snowflake.cli.api.commands.flags import identifier_argument -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.output.types import MessageResult from snowflake.cli.plugins.notebook.manager import NotebookManager from snowflake.cli.plugins.notebook.types import NotebookName, NotebookStagePath from typing_extensions import Annotated -app = SnowTyper( +app = SnowTyperFactory( name="notebook", help="Manages notebooks in Snowflake.", ) diff --git a/src/snowflake/cli/plugins/notebook/plugin_spec.py b/src/snowflake/cli/plugins/notebook/plugin_spec.py index edcaef756..891c8ee6b 100644 --- a/src/snowflake/cli/plugins/notebook/plugin_spec.py +++ b/src/snowflake/cli/plugins/notebook/plugin_spec.py @@ -12,5 +12,5 @@ def command_spec(): return CommandSpec( parent_command_path=SNOWCLI_ROOT_COMMAND_PATH, command_type=CommandType.COMMAND_GROUP, - typer_instance=commands.app, + typer_instance=commands.app.create_instance(), ) diff --git a/src/snowflake/cli/plugins/object/__init__.py b/src/snowflake/cli/plugins/object/__init__.py index cf5630e02..e69de29bb 100644 --- a/src/snowflake/cli/plugins/object/__init__.py +++ b/src/snowflake/cli/plugins/object/__init__.py @@ -1,11 +0,0 @@ -from snowflake.cli.api.commands.snow_typer import SnowTyper -from snowflake.cli.plugins.object.commands import app as show_app -from snowflake.cli.plugins.stage.commands import app as stage_app - -app = SnowTyper( - name="object", - help="Manages Snowflake objects like warehouses and stages", -) - -app.add_typer(stage_app) # type: ignore -app.add_typer(show_app) # type: ignore diff --git a/src/snowflake/cli/plugins/object/command_aliases.py b/src/snowflake/cli/plugins/object/command_aliases.py index d4db7de20..8aaafe534 100644 --- a/src/snowflake/cli/plugins/object/command_aliases.py +++ b/src/snowflake/cli/plugins/object/command_aliases.py @@ -4,7 +4,7 @@ import typer from click import ClickException -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.constants import ObjectType from snowflake.cli.plugins.object.commands import ( ScopeOption, @@ -16,7 +16,7 @@ def add_object_command_aliases( - app: SnowTyper, + app: SnowTyperFactory, object_type: ObjectType, name_argument: typer.Argument, like_option: Optional[typer.Option], @@ -31,7 +31,7 @@ def add_object_command_aliases( @app.command("list", requires_connection=True) def list_cmd(like: str = like_option, **options): # type: ignore - list_( + return list_( object_type=object_type.value.cli_name, like=like, scope=ScopeOption.default, @@ -46,7 +46,7 @@ def list_cmd( scope: Tuple[str, str] = scope_option, # type: ignore **options, ): - list_( + return list_( object_type=object_type.value.cli_name, like=like, scope=scope, @@ -59,7 +59,7 @@ def list_cmd( @app.command("drop", requires_connection=True) def drop_cmd(name: str = name_argument, **options): - drop( + return drop( object_type=object_type.value.cli_name, object_name=name, **options, @@ -71,7 +71,7 @@ def drop_cmd(name: str = name_argument, **options): @app.command("describe", requires_connection=True) def describe_cmd(name: str = name_argument, **options): - describe( + return describe( object_type=object_type.value.cli_name, object_name=name, **options, diff --git a/src/snowflake/cli/plugins/object/commands.py b/src/snowflake/cli/plugins/object/commands.py index 1ceb30c50..8087524cb 100644 --- a/src/snowflake/cli/plugins/object/commands.py +++ b/src/snowflake/cli/plugins/object/commands.py @@ -5,13 +5,13 @@ import typer from click import ClickException from snowflake.cli.api.commands.flags import like_option -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.constants import SUPPORTED_OBJECTS, VALID_SCOPES from snowflake.cli.api.output.types import QueryResult from snowflake.cli.api.project.util import is_valid_identifier from snowflake.cli.plugins.object.manager import ObjectManager -app = SnowTyper( +app = SnowTyperFactory( name="object", help="Manages Snowflake objects like warehouses and stages", ) diff --git a/src/snowflake/cli/plugins/object/plugin_spec.py b/src/snowflake/cli/plugins/object/plugin_spec.py index 8681ee949..bd7d84934 100644 --- a/src/snowflake/cli/plugins/object/plugin_spec.py +++ b/src/snowflake/cli/plugins/object/plugin_spec.py @@ -4,7 +4,7 @@ CommandType, plugin_hook_impl, ) -from snowflake.cli.plugins.object.commands import app as object_app +from snowflake.cli.plugins.object import commands @plugin_hook_impl @@ -12,5 +12,5 @@ def command_spec(): return CommandSpec( parent_command_path=SNOWCLI_ROOT_COMMAND_PATH, command_type=CommandType.COMMAND_GROUP, - typer_instance=object_app, + typer_instance=commands.app.create_instance(), ) diff --git a/src/snowflake/cli/plugins/object_stage_deprecated/commands.py b/src/snowflake/cli/plugins/object_stage_deprecated/commands.py index 351c8fffb..de5c20a0a 100644 --- a/src/snowflake/cli/plugins/object_stage_deprecated/commands.py +++ b/src/snowflake/cli/plugins/object_stage_deprecated/commands.py @@ -6,7 +6,7 @@ from snowflake.cli.api.commands.flags import ( PatternOption, ) -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.console import cli_console from snowflake.cli.api.plugins.command import CommandPath from snowflake.cli.plugins.stage.commands import ( @@ -23,7 +23,7 @@ f" Please use `{CommandPath(['stage'])}` instead." ) -app = SnowTyper(name="stage", help="Manages stages.", deprecated=True) +app = SnowTyperFactory(name="stage", help="Manages stages.", deprecated=True) @app.callback() diff --git a/src/snowflake/cli/plugins/object_stage_deprecated/plugin_spec.py b/src/snowflake/cli/plugins/object_stage_deprecated/plugin_spec.py index da1f8a6c7..9be1d5e65 100644 --- a/src/snowflake/cli/plugins/object_stage_deprecated/plugin_spec.py +++ b/src/snowflake/cli/plugins/object_stage_deprecated/plugin_spec.py @@ -6,9 +6,7 @@ CommandType, plugin_hook_impl, ) -from snowflake.cli.plugins.object_stage_deprecated.commands import ( - app as stage_deprecated_app, -) +from snowflake.cli.plugins.object_stage_deprecated import commands @plugin_hook_impl @@ -16,5 +14,5 @@ def command_spec(): return CommandSpec( parent_command_path=CommandPath(["object"]), command_type=CommandType.COMMAND_GROUP, - typer_instance=stage_deprecated_app, + typer_instance=commands.app.create_instance(), ) diff --git a/src/snowflake/cli/plugins/snowpark/__init__.py b/src/snowflake/cli/plugins/snowpark/__init__.py index 83b16cf80..e69de29bb 100644 --- a/src/snowflake/cli/plugins/snowpark/__init__.py +++ b/src/snowflake/cli/plugins/snowpark/__init__.py @@ -1,4 +0,0 @@ -from snowflake.cli.plugins.snowpark.commands import app -from snowflake.cli.plugins.snowpark.package.commands import app as package_app - -app.add_typer(package_app) diff --git a/src/snowflake/cli/plugins/snowpark/commands.py b/src/snowflake/cli/plugins/snowpark/commands.py index 80819b044..149adc915 100644 --- a/src/snowflake/cli/plugins/snowpark/commands.py +++ b/src/snowflake/cli/plugins/snowpark/commands.py @@ -18,7 +18,7 @@ like_option, ) from snowflake.cli.api.commands.project_initialisation import add_init_command -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.constants import ( DEFAULT_SIZE_LIMIT_MB, DEPLOYMENT_STAGE, @@ -63,6 +63,7 @@ AnacondaPackages, AnacondaPackagesManager, ) +from snowflake.cli.plugins.snowpark.package.commands import app as package_app from snowflake.cli.plugins.snowpark.snowpark_package_paths import SnowparkPackagePaths from snowflake.cli.plugins.snowpark.snowpark_shared import ( AllowSharedLibrariesOption, @@ -79,10 +80,11 @@ log = logging.getLogger(__name__) -app = SnowTyper( +app = SnowTyperFactory( name="snowpark", help="Manages procedures and functions.", ) +app.add_typer(package_app) ObjectTypeArgument = typer.Argument( help="Type of Snowpark object", diff --git a/src/snowflake/cli/plugins/snowpark/package/commands.py b/src/snowflake/cli/plugins/snowpark/package/commands.py index fb1cd30c3..8d22e1871 100644 --- a/src/snowflake/cli/plugins/snowpark/package/commands.py +++ b/src/snowflake/cli/plugins/snowpark/package/commands.py @@ -10,7 +10,7 @@ from snowflake.cli.api.commands.flags import ( deprecated_flag_callback, ) -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.output.types import CommandResult, MessageResult from snowflake.cli.api.secure_path import SecurePath from snowflake.cli.plugins.snowpark.models import ( @@ -37,7 +37,7 @@ ) from snowflake.cli.plugins.snowpark.zipper import zip_dir -app = SnowTyper( +app = SnowTyperFactory( name="package", help="Manages custom Python packages for Snowpark", ) diff --git a/src/snowflake/cli/plugins/snowpark/plugin_spec.py b/src/snowflake/cli/plugins/snowpark/plugin_spec.py index c56142b97..4dcf869fa 100644 --- a/src/snowflake/cli/plugins/snowpark/plugin_spec.py +++ b/src/snowflake/cli/plugins/snowpark/plugin_spec.py @@ -4,7 +4,7 @@ CommandType, plugin_hook_impl, ) -from snowflake.cli.plugins.snowpark import app as snowpark_app +from snowflake.cli.plugins.snowpark import commands @plugin_hook_impl @@ -12,5 +12,5 @@ def command_spec(): return CommandSpec( parent_command_path=SNOWCLI_ROOT_COMMAND_PATH, command_type=CommandType.COMMAND_GROUP, - typer_instance=snowpark_app, + typer_instance=commands.app.create_instance(), ) diff --git a/src/snowflake/cli/plugins/spcs/__init__.py b/src/snowflake/cli/plugins/spcs/__init__.py index d0fd45174..80b668c95 100644 --- a/src/snowflake/cli/plugins/spcs/__init__.py +++ b/src/snowflake/cli/plugins/spcs/__init__.py @@ -1,4 +1,4 @@ -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.plugins.spcs.compute_pool.commands import ( app as compute_pools_app, ) @@ -9,7 +9,7 @@ from snowflake.cli.plugins.spcs.jobs.commands import app as jobs_app from snowflake.cli.plugins.spcs.services.commands import app as services_app -app = SnowTyper( +app = SnowTyperFactory( name="spcs", help="Manages Snowpark Container Services compute pools, services, image registries, and image repositories.", ) diff --git a/src/snowflake/cli/plugins/spcs/compute_pool/commands.py b/src/snowflake/cli/plugins/spcs/compute_pool/commands.py index 20984a514..157e02a56 100644 --- a/src/snowflake/cli/plugins/spcs/compute_pool/commands.py +++ b/src/snowflake/cli/plugins/spcs/compute_pool/commands.py @@ -9,7 +9,7 @@ OverrideableOption, like_option, ) -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.constants import ObjectType from snowflake.cli.api.output.types import CommandResult, SingleQueryResult from snowflake.cli.api.project.util import is_valid_object_name @@ -22,7 +22,7 @@ ) from snowflake.cli.plugins.spcs.compute_pool.manager import ComputePoolManager -app = SnowTyper( +app = SnowTyperFactory( name="compute-pool", help="Manages Snowpark Container Services compute pools.", short_help="Manages compute pools.", diff --git a/src/snowflake/cli/plugins/spcs/image_registry/commands.py b/src/snowflake/cli/plugins/spcs/image_registry/commands.py index f73dccf5c..9000a8e99 100644 --- a/src/snowflake/cli/plugins/spcs/image_registry/commands.py +++ b/src/snowflake/cli/plugins/spcs/image_registry/commands.py @@ -1,10 +1,10 @@ -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.output.types import MessageResult, ObjectResult from snowflake.cli.plugins.spcs.image_registry.manager import ( RegistryManager, ) -app = SnowTyper( +app = SnowTyperFactory( name="image-registry", help="Manages Snowpark Container Services image registries.", short_help="Manages image registries.", diff --git a/src/snowflake/cli/plugins/spcs/image_repository/commands.py b/src/snowflake/cli/plugins/spcs/image_repository/commands.py index 43b437685..6eb59a08e 100644 --- a/src/snowflake/cli/plugins/spcs/image_repository/commands.py +++ b/src/snowflake/cli/plugins/spcs/image_repository/commands.py @@ -11,7 +11,7 @@ ReplaceOption, like_option, ) -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.console import cli_console from snowflake.cli.api.constants import ObjectType from snowflake.cli.api.output.types import ( @@ -27,7 +27,7 @@ from snowflake.cli.plugins.spcs.image_registry.manager import RegistryManager from snowflake.cli.plugins.spcs.image_repository.manager import ImageRepositoryManager -app = SnowTyper( +app = SnowTyperFactory( name="image-repository", help="Manages Snowpark Container Services image repositories.", short_help="Manages image repositories.", diff --git a/src/snowflake/cli/plugins/spcs/jobs/commands.py b/src/snowflake/cli/plugins/spcs/jobs/commands.py index 94dd99b8b..22815ae3d 100644 --- a/src/snowflake/cli/plugins/spcs/jobs/commands.py +++ b/src/snowflake/cli/plugins/spcs/jobs/commands.py @@ -2,15 +2,15 @@ from pathlib import Path import typer -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.output.types import CommandResult, SingleQueryResult from snowflake.cli.plugins.spcs.common import print_log_lines from snowflake.cli.plugins.spcs.jobs.manager import JobManager -app = SnowTyper( +app = SnowTyperFactory( name="job", help="Manages Snowpark jobs.", - hidden=True, + is_hidden=lambda: True, ) diff --git a/src/snowflake/cli/plugins/spcs/plugin_spec.py b/src/snowflake/cli/plugins/spcs/plugin_spec.py index bf0e7f4e5..2e13e7c03 100644 --- a/src/snowflake/cli/plugins/spcs/plugin_spec.py +++ b/src/snowflake/cli/plugins/spcs/plugin_spec.py @@ -12,5 +12,5 @@ def command_spec(): return CommandSpec( parent_command_path=SNOWCLI_ROOT_COMMAND_PATH, command_type=CommandType.COMMAND_GROUP, - typer_instance=app, + typer_instance=app.create_instance(), ) diff --git a/src/snowflake/cli/plugins/spcs/services/commands.py b/src/snowflake/cli/plugins/spcs/services/commands.py index 499dcfd5d..60191dbdb 100644 --- a/src/snowflake/cli/plugins/spcs/services/commands.py +++ b/src/snowflake/cli/plugins/spcs/services/commands.py @@ -11,7 +11,7 @@ OverrideableOption, like_option, ) -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.constants import ObjectType from snowflake.cli.api.output.types import ( CommandResult, @@ -31,7 +31,7 @@ ) from snowflake.cli.plugins.spcs.services.manager import ServiceManager -app = SnowTyper( +app = SnowTyperFactory( name="service", help="Manages Snowpark Container Services services.", short_help="Manages services.", diff --git a/src/snowflake/cli/plugins/sql/commands.py b/src/snowflake/cli/plugins/sql/commands.py index 24f53ed3f..736511f59 100644 --- a/src/snowflake/cli/plugins/sql/commands.py +++ b/src/snowflake/cli/plugins/sql/commands.py @@ -8,12 +8,12 @@ parse_key_value_variables, project_definition_option, ) -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.output.types import CommandResult, MultipleResults, QueryResult from snowflake.cli.plugins.sql.manager import SqlManager # simple Typer with defaults because it won't become a command group as it contains only one command -app = SnowTyper() +app = SnowTyperFactory() def _parse_key_value(key_value_str: str): diff --git a/src/snowflake/cli/plugins/sql/plugin_spec.py b/src/snowflake/cli/plugins/sql/plugin_spec.py index 94c549a21..8bbc62de4 100644 --- a/src/snowflake/cli/plugins/sql/plugin_spec.py +++ b/src/snowflake/cli/plugins/sql/plugin_spec.py @@ -12,5 +12,5 @@ def command_spec(): return CommandSpec( parent_command_path=SNOWCLI_ROOT_COMMAND_PATH, command_type=CommandType.SINGLE_COMMAND, - typer_instance=commands.app, + typer_instance=commands.app.create_instance(), ) diff --git a/src/snowflake/cli/plugins/stage/commands.py b/src/snowflake/cli/plugins/stage/commands.py index ddd562e1d..750914a7a 100644 --- a/src/snowflake/cli/plugins/stage/commands.py +++ b/src/snowflake/cli/plugins/stage/commands.py @@ -13,7 +13,7 @@ VariablesOption, like_option, ) -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.console import cli_console from snowflake.cli.api.constants import ObjectType from snowflake.cli.api.output.types import ( @@ -31,7 +31,7 @@ from snowflake.cli.plugins.stage.diff import DiffResult, compute_stage_diff from snowflake.cli.plugins.stage.manager import OnErrorType, StageManager -app = SnowTyper( +app = SnowTyperFactory( name="stage", help="Manages stages.", ) diff --git a/src/snowflake/cli/plugins/stage/plugin_spec.py b/src/snowflake/cli/plugins/stage/plugin_spec.py index 319a1c255..be314837f 100644 --- a/src/snowflake/cli/plugins/stage/plugin_spec.py +++ b/src/snowflake/cli/plugins/stage/plugin_spec.py @@ -4,7 +4,7 @@ CommandType, plugin_hook_impl, ) -from snowflake.cli.plugins.stage.commands import app as stage_app +from snowflake.cli.plugins.stage import commands @plugin_hook_impl @@ -12,5 +12,5 @@ def command_spec(): return CommandSpec( parent_command_path=SNOWCLI_ROOT_COMMAND_PATH, command_type=CommandType.COMMAND_GROUP, - typer_instance=stage_app, + typer_instance=commands.app.create_instance(), ) diff --git a/src/snowflake/cli/plugins/streamlit/commands.py b/src/snowflake/cli/plugins/streamlit/commands.py index 6bebd4edb..563879e06 100644 --- a/src/snowflake/cli/plugins/streamlit/commands.py +++ b/src/snowflake/cli/plugins/streamlit/commands.py @@ -13,7 +13,7 @@ ) from snowflake.cli.api.commands.flags import ReplaceOption, like_option from snowflake.cli.api.commands.project_initialisation import add_init_command -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.constants import ObjectType from snowflake.cli.api.identifiers import FQN from snowflake.cli.api.output.types import ( @@ -28,7 +28,7 @@ ) from snowflake.cli.plugins.streamlit.manager import StreamlitManager -app = SnowTyper( +app = SnowTyperFactory( name="streamlit", help="Manages a Streamlit app in Snowflake.", ) diff --git a/src/snowflake/cli/plugins/streamlit/plugin_spec.py b/src/snowflake/cli/plugins/streamlit/plugin_spec.py index e31aec26e..11f1cea59 100644 --- a/src/snowflake/cli/plugins/streamlit/plugin_spec.py +++ b/src/snowflake/cli/plugins/streamlit/plugin_spec.py @@ -12,5 +12,5 @@ def command_spec(): return CommandSpec( parent_command_path=SNOWCLI_ROOT_COMMAND_PATH, command_type=CommandType.COMMAND_GROUP, - typer_instance=commands.app, + typer_instance=commands.app.create_instance(), ) diff --git a/tests/api/commands/test_snow_typer.py b/tests/api/commands/test_snow_typer.py index 2083b4f1c..b9193516e 100644 --- a/tests/api/commands/test_snow_typer.py +++ b/tests/api/commands/test_snow_typer.py @@ -4,7 +4,7 @@ import pytest import typer -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyper, SnowTyperFactory from snowflake.cli.api.output.types import MessageResult from typer.testing import CliRunner @@ -36,6 +36,9 @@ def exception_handler(err): if exception_handler: exception_handler(err) + def create_instance(self): + return self + return _CustomTyper @@ -69,7 +72,7 @@ def cmd_with_connection_options(name: str = typer.Argument()): def cmd_witch_enabled_switch(): return MessageResult("Enabled") - return app + return app.create_instance() @pytest.fixture @@ -140,24 +143,26 @@ def test_pre_callback_error_path(cli): def test_command_without_any_options(cli, snapshot): - result = cli(app_factory(SnowTyper))(["simple_cmd", "--help"]) + result = cli(app_factory(SnowTyperFactory))(["simple_cmd", "--help"]) assert result.output == snapshot def test_command_with_global_options(cli, snapshot): - result = cli(app_factory(SnowTyper))(["cmd_with_global_options", "--help"]) + result = cli(app_factory(SnowTyperFactory))(["cmd_with_global_options", "--help"]) assert result.output == snapshot def test_command_with_connection_options(cli, snapshot): - result = cli(app_factory(SnowTyper))(["cmd_with_connection_options", "--help"]) + result = cli(app_factory(SnowTyperFactory))( + ["cmd_with_connection_options", "--help"] + ) assert result.output == snapshot def test_enabled_command_is_visible(cli, snapshot): global _ENABLED_FLAG _ENABLED_FLAG = True - result = cli(app_factory(SnowTyper))(["switchable_cmd", "--help"]) + result = cli(app_factory(SnowTyperFactory))(["switchable_cmd", "--help"]) assert result.exit_code == 0 assert result.output == snapshot @@ -165,28 +170,28 @@ def test_enabled_command_is_visible(cli, snapshot): def test_enabled_command_is_not_visible(cli, snapshot): global _ENABLED_FLAG _ENABLED_FLAG = False - result = cli(app_factory(SnowTyper))(["switchable_cmd", "--help"]) + result = cli(app_factory(SnowTyperFactory))(["switchable_cmd", "--help"]) assert result.exit_code == 2 assert result.output == snapshot @mock.patch("snowflake.cli.app.telemetry.log_command_usage") def test_snow_typer_pre_execute_sends_telemetry(mock_log_command_usage, cli): - result = cli(app_factory(SnowTyper))(["simple_cmd", "Norma"]) + result = cli(app_factory(SnowTyperFactory))(["simple_cmd", "Norma"]) assert result.exit_code == 0 mock_log_command_usage.assert_called_once_with() @mock.patch("snowflake.cli.app.telemetry.flush_telemetry") def test_snow_typer_post_execute_sends_telemetry(mock_flush_telemetry, cli): - result = cli(app_factory(SnowTyper))(["simple_cmd", "Norma"]) + result = cli(app_factory(SnowTyperFactory))(["simple_cmd", "Norma"]) assert result.exit_code == 0 mock_flush_telemetry.assert_called_once_with() @mock.patch("snowflake.cli.app.printing.print_result") def test_snow_typer_result_callback_sends_telemetry(mock_print_result, cli): - result = cli(app_factory(SnowTyper))(["simple_cmd", "Norma"]) + result = cli(app_factory(SnowTyperFactory))(["simple_cmd", "Norma"]) assert result.exit_code == 0 assert mock_print_result.call_count == 1 assert mock_print_result.call_args.args[0].message == "hello Norma" diff --git a/tests/test_project_initialisation.py b/tests/test_project_initialisation.py index 267aaf126..5fe9bbfe3 100644 --- a/tests/test_project_initialisation.py +++ b/tests/test_project_initialisation.py @@ -3,14 +3,14 @@ from unittest import mock from snowflake.cli.api.commands.project_initialisation import add_init_command -from snowflake.cli.api.commands.snow_typer import SnowTyper +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.secure_path import SecurePath from typer.testing import CliRunner @mock.patch.object(SecurePath, "copy") def test_adds_init_command(mock_copy): - app = SnowTyper() + app = SnowTyperFactory() runner = CliRunner() with TemporaryDirectory() as tmp_templates: @@ -25,7 +25,7 @@ def test_adds_init_command(mock_copy): Path(tmp_templates), ): add_init_command(app, "my_project_type", template="my_template") - result = runner.invoke(app, ["my_dir"]) + result = runner.invoke(app.create_instance(), ["my_dir"]) assert result.exit_code == 0 assert result.output == "Initialized the new project in my_dir/\n" diff --git a/tests_e2e/__snapshots__/test_error_handling.ambr b/tests_e2e/__snapshots__/test_error_handling.ambr new file mode 100644 index 000000000..307757923 --- /dev/null +++ b/tests_e2e/__snapshots__/test_error_handling.ambr @@ -0,0 +1,10 @@ +# serializer version: 1 +# name: test_corrupted_config_in_default_location + ''' + ╭─ Error ──────────────────────────────────────────────────────────────────────╮ + │ Configuration file seems to be corrupted. Key "demo" already exists. at line │ + │ 2 col 18 │ + ╰──────────────────────────────────────────────────────────────────────────────╯ + + ''' +# --- diff --git a/tests_e2e/config/malformatted_config.toml b/tests_e2e/config/malformatted_config.toml deleted file mode 100755 index 606ed17ec..000000000 --- a/tests_e2e/config/malformatted_config.toml +++ /dev/null @@ -1,5 +0,0 @@ -[connections.dev] -[connections.spcs] -[connections.integration] -schema = "public" -schema = "public" diff --git a/tests_e2e/test_error_handling.py b/tests_e2e/test_error_handling.py index 93950f887..777048b69 100644 --- a/tests_e2e/test_error_handling.py +++ b/tests_e2e/test_error_handling.py @@ -1,5 +1,6 @@ import os import subprocess +from pathlib import Path import pytest @@ -49,3 +50,30 @@ def test_error_traceback_disabled_without_debug(snowcli, test_root_path): assert result_debug.returncode == 1 assert not result_debug.stdout assert traceback_msg in result_debug.stderr + + +@pytest.mark.e2e +def test_corrupted_config_in_default_location( + snowcli, temp_dir, isolate_default_config_location, test_root_path, snapshot +): + default_config = Path(temp_dir) / "config.toml" + default_config.write_text("[connections.demo]\n[connections.demo]") + default_config.chmod(0o600) + # corrupted config should produce human-friendly error + result_err = subprocess.run( + [snowcli, "connection", "list"], + capture_output=True, + text=True, + ) + assert result_err.returncode == 1 + assert result_err.stderr == snapshot + + # corrupted config in default location should not influence one passed with --config-file flag + healthy_config = test_root_path / "config" / "config.toml" + result_healthy = subprocess.run( + [snowcli, "--config-file", healthy_config, "connection", "list"], + capture_output=True, + text=True, + ) + assert result_healthy.returncode == 0, result_healthy.stderr + assert "dev" in result_healthy.stdout and "integration" in result_healthy.stdout diff --git a/tests_integration/plugin/test_override_by_external_plugins.py b/tests_integration/plugin/test_override_by_external_plugins.py index af70c6443..3737c18e6 100644 --- a/tests_integration/plugin/test_override_by_external_plugins.py +++ b/tests_integration/plugin/test_override_by_external_plugins.py @@ -12,8 +12,8 @@ def test_override_build_in_commands(runner, test_root_path, _install_plugin, cap result = runner.invoke(["--config-file", config_path, "connection", "list"]) assert ( - caplog.messages[0] - == "Cannot register plugin [override]: Cannot add command [snow connection list] because it already exists." + "Cannot register plugin [override]: Cannot add command [snow connection list] because it already exists." + in caplog.messages ) assert result.output == dedent( """\