Skip to content

Commit

Permalink
feat: Add --no-install option to meltano add (meltano#6578)
Browse files Browse the repository at this point in the history
* Add `--no-install` flag for `meltano add`

* Test `--no-install`

* Reference `--no-install` in `meltano add` docs

* Fix lint

* Update docs/src/_reference/command-line-interface.md

Co-authored-by: Taylor A. Murphy <taylor@meltano.com>

* Reset project context in test to avoid errors related to plugins having already been installed

Co-authored-by: Taylor A. Murphy <taylor@meltano.com>
Co-authored-by: Edgar R. M <edgarrm358@gmail.com>
  • Loading branch information
3 people committed Aug 10, 2022
1 parent f89fc4d commit e4cf960
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 5 deletions.
13 changes: 12 additions & 1 deletion docs/src/_reference/command-line-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Specifically, it will:
3. Store the plugin definition in the `./plugins` directory (see [Lock Artifacts](/concepts/plugins#lock-artifacts))
4. Assuming a valid `pip_url` is specified, install the new plugin using [`meltano install <type> <name>`](#install), which will:
1. Create a dedicated [Python virtual environment](https://docs.python.org/3/glossary.html#term-virtual-environment) for the plugin inside the [`.meltano` directory](/concepts/project#meltano-directory) at `.meltano/<type>s/<name>/venv`, e.g. `.meltano/extractors/tap-gitlab/venv`
2. Install the plugin's [pip package](https://pip.pypa.io/en/stable/) into the virtual environment using `pip install <pip_url>`
2. Install the plugin's [pip package](https://pip.pypa.io/en/stable/) into the virtual environment using `pip install <pip_url>` (given `--no-install` is not provided)

(Some plugin types have slightly different or additional behavior; refer to the [plugin type documentation](/concepts/plugins#types) for more details.)

Expand Down Expand Up @@ -80,6 +80,15 @@ meltano add <type> <name> --inherit-from <existing-name>
meltano add extractor tap-ga--client-foo --inherit-from tap-google-analytics
```

By default, `meltano add` will attempt to install the plugin after adding it. Use `--no-install` to skip this behavior:

```bash
meltano add <type> <name> --no-install

# For example:
meltano add extractor tap-spotify --no-install
```

#### Parameters

- `--custom`: Add a [custom plugin](/concepts/plugins#custom-plugins). The command will prompt you for the package's [base plugin description](/concepts/plugins#project-plugins) metadata.
Expand All @@ -90,6 +99,8 @@ meltano add extractor tap-ga--client-foo --inherit-from tap-google-analytics

- `--variant=<variant>`: Add a specific (non-default) [variant](/concepts/plugins#variants) of the identified [discoverable plugin](/concepts/plugins#discoverable-plugins).

- `--no-install`: Do not install the plugin after adding it to the project.

## `config`

Enables you to manage the [configuration](/guide/configuration) of Meltano itself or any of its plugins, as well as [plugin extras](#how-to-use-plugin-extras).
Expand Down
14 changes: 10 additions & 4 deletions src/meltano/cli/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@
is_flag=True,
help="Add a custom plugin. The command will prompt you for the package's base plugin description metadata.",
)
@click.option(
"--no-install",
is_flag=True,
help="Do not install the plugin after adding it to the project.",
)
@pass_project()
@click.pass_context
def add(
Expand Down Expand Up @@ -140,11 +145,12 @@ def add(
)
tracker.track_command_event(CliEvent.inflight)

success = install_plugins(project, plugins, reason=PluginInstallReason.ADD)
if not flags.get("no_install"):
success = install_plugins(project, plugins, reason=PluginInstallReason.ADD)

if not success:
tracker.track_command_event(CliEvent.failed)
raise CliError("Failed to install plugin(s)")
if not success:
tracker.track_command_event(CliEvent.failed)
raise CliError("Failed to install plugin(s)")

_print_plugins(plugins)
tracker.track_command_event(CliEvent.completed)
Expand Down
95 changes: 95 additions & 0 deletions tests/meltano/cli/test_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from meltano.core.plugin.error import PluginNotFoundError
from meltano.core.plugin.project_plugin import ProjectPlugin
from meltano.core.plugin_install_service import PluginInstallReason
from meltano.core.project import Project
from meltano.core.project_init_service import ProjectInitService


class TestCliAdd:
Expand All @@ -27,6 +29,13 @@ def patch_hub(self, meltano_hub_service: MeltanoHubService):
):
yield

@pytest.fixture
def reset_project_context(
self, project: Project, project_init_service: ProjectInitService
):
shutil.rmtree(".", ignore_errors=True)
project_init_service.create_files(project)

@pytest.mark.parametrize(
"plugin_type,plugin_name,default_variant,required_plugin_refs",
[
Expand Down Expand Up @@ -521,3 +530,89 @@ def test_add_custom_variant(
plugin_variant = plugin_def.variants[0]

assert plugin.variant == plugin_variant.name == "personal"

@pytest.mark.parametrize(
"plugin_type,plugin_name,default_variant,required_plugin_refs",
[
(PluginType.EXTRACTORS, "tap-carbon-intensity", "meltano", []),
(PluginType.LOADERS, "target-sqlite", "meltanolabs", []),
(PluginType.TRANSFORMS, "tap-carbon-intensity", "meltano", []),
(
PluginType.ORCHESTRATORS,
"airflow",
Variant.ORIGINAL_NAME,
[PluginRef(PluginType.FILES, "airflow")],
),
],
ids=[
"single-extractor",
"single-loader",
"transform-and-related",
"orchestrator-and-required",
],
)
def test_add_no_install(
self,
plugin_type,
plugin_name,
default_variant,
required_plugin_refs,
project,
cli_runner,
project_plugins_service,
reset_project_context,
):
# ensure the plugin is not present
with pytest.raises(PluginNotFoundError):
project_plugins_service.find_plugin(plugin_name, plugin_type=plugin_type)

with mock.patch("meltano.cli.add.install_plugins") as install_plugin_mock:
install_plugin_mock.return_value = True
res = cli_runner.invoke(
cli, ["add", plugin_type.singular, plugin_name, "--no-install"]
)

if plugin_type is PluginType.TRANSFORMS:
assert res.exit_code == 1, res.stdout
assert isinstance(res.exception, CliError)
assert "Dependencies not met:" in str(res.exception)
else:
assert res.exit_code == 0, res.stdout
assert f"Added {plugin_type.descriptor} '{plugin_name}'" in res.stdout

plugin = project_plugins_service.find_plugin(plugin_name, plugin_type)
assert plugin
assert plugin.variant == default_variant

# check plugin lock file is added
plugins_dir = project.root_dir("plugins")
assert plugins_dir.joinpath(
f"{plugin_type}/{plugin_name}--{default_variant}.lock"
).exists()

for required_plugin_ref in required_plugin_refs:
if (required_plugin_ref._type) == PluginType.FILES and (
required_plugin_ref.name == "dbt"
):
# file bundles with no managed files are added but do not appear in meltano.yml
assert (
f"Adding required file bundle '{required_plugin_ref.name}'"
in res.stdout
)
else:
plugin = project_plugins_service.get_plugin(required_plugin_ref)
assert plugin

assert (
f"Added required {plugin.type.descriptor} '{plugin.name}'"
in res.stdout
)

# check required plugin lock files are added
assert list(
plugins_dir.glob(
f"{required_plugin_ref._type}/{required_plugin_ref.name}--*.lock"
)
)

install_plugin_mock.assert_not_called()

0 comments on commit e4cf960

Please sign in to comment.