Skip to content

Commit

Permalink
Removed the 'inmanta module install' command. (Issue #6717, PR #6737)
Browse files Browse the repository at this point in the history
# Description

Update the pip config used by inmanta module install to behave as follows:
1. raise an error stating that the command is no longer supported.
2. Offer the suggestion to run pip install -e . to install a module in development mode
3. Offer the suggestion to run inmanta module build followed by pip install if the user really does not want an editable install.
4. The inmanta module build command should use use-system-config=true. It is assumed safe because it only installs build dependencies.

closes #6717

# Self Check:

Strike through any lines that are not applicable (`~~line~~`) then check the box

- [x] Attached issue to pull request
- [x] Changelog entry
- [x] Type annotations are present
- [x] Code is clear and sufficiently documented
- [x] No (preventable) type errors (check using make mypy or make mypy-diff)
- [x] Sufficient test cases (reproduces the bug/tests the requested feature)
- [x] Correct, in line with design
- [x] End user documentation is included or an issue is created for end-user documentation (add ref to issue here: )
- [ ] If this PR fixes a race condition in the test suite, also push the fix to the relevant stable branche(s) (see [test-fixes](https://internal.inmanta.com/development/core/tasks/build-master.html#test-fixes) for more info)
  • Loading branch information
FloLey authored and inmantaci committed Nov 22, 2023
1 parent c36e331 commit e5d8522
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 82 deletions.
10 changes: 10 additions & 0 deletions changelogs/unreleased/6717-update-module-install.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
description: Removed the 'inmanta module install' command.
change-type: major
issue-nr: 6717
destination-branches: [master]
sections:
deprecation-note: "{{description}}. As an alternative to the now-removed 'inmanta module install' command,
users should follow the updated procedure for module installation:
The new method involves using the 'inmanta module build' command followed by 'pip install ./dist/<dist-package>' to
build a module from source and install the distribution package, respectively. Alternatively, use 'pip install -e .'
to install the module in editable mode"
2 changes: 1 addition & 1 deletion docs/model_developers/developer_getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ For a v2 module, use the v2 cookiecutter template, then install the module:
pip install cookiecutter
cookiecutter https://github.com/inmanta/inmanta-module-template.git
inmanta module install -e ./<module-name>
pip install -e ./<module-name>
This will install a Python package with the name ``inmanta-module-<module-name>`` in the active environment.

Expand Down
4 changes: 2 additions & 2 deletions docs/model_developers/modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ Setting up the dev environment
##############################
To set up the development environment for a project, activate your development Python environment and
install the project with ``inmanta project install``. To set up the environment for a single v2 module,
run ``inmanta module install -e`` instead.
run ``pip install -e .`` instead.

The following subsections explain any additional steps you need to take if you want to make changes
to one of the dependent modules as well.
Expand All @@ -295,7 +295,7 @@ v2 modules
----------
All other modules are v2 and have been installed by ``inmanta project install`` into the active Python
environment. If you want to be able to make changes to one of these modules, the easiest way is to
check out the module repo separately and run ``inmanta module install -e <path>`` on it, overwriting the published
check out the module repo separately and run ``pip install -e <path>`` on it, overwriting the published
package that was installed previously. This will install the module in editable form: any changes you make
to the checked out files will be picked up by the compiler. You can also do this prior to installing the
project, in which case the pre-installed module will remain installed in editable form when you install
Expand Down
10 changes: 6 additions & 4 deletions src/inmanta/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -833,8 +833,9 @@ def path_for(self, name: str) -> Optional[str]:
raise InvalidModuleException(
f"Invalid module at {pkg_installation_dir}: found module package but it has no {ModuleV2.MODULE_FILE}. "
"This occurs when you install or build modules from source incorrectly. "
"Always use the `inmanta module install` and `inmanta module build` commands to "
"respectively install and build modules from source. Make sure to uninstall the broken package first."
"Always use the `inmanta module build` command followed by `pip install ./dist/<dist-package>` to "
"respectively build a module from source and install the distribution package. "
"Make sure to uninstall the broken package first."
)

@classmethod
Expand Down Expand Up @@ -3334,8 +3335,9 @@ def __init__(
if not os.path.exists(os.path.join(self.model_dir, "_init.cf")):
raise InvalidModuleException(
f"The module at {path} contains no _init.cf file. This occurs when you install or build modules from source"
" incorrectly. Always use the `inmanta module install` and `inmanta module build` commands to respectively"
" install and build modules from source. Make sure to uninstall the broken package first."
" incorrectly. Always use the `inmanta module build` command followed by `pip install ./dist/<dist-package>` "
"to respectively build a module from source and install the distribution package. "
"Make sure to uninstall the broken package first."
)

@classmethod
Expand Down
55 changes: 16 additions & 39 deletions src/inmanta/moduletool.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@
from inmanta.ast import CompilerException
from inmanta.command import CLIException, ShowUsageException
from inmanta.const import CF_CACHE_DIR, MAX_UPDATE_ATTEMPT
from inmanta.env import PipConfig
from inmanta.module import (
DummyProject,
FreezeOperator,
Expand All @@ -62,7 +61,6 @@
InvalidMetadata,
InvalidModuleException,
Module,
ModuleGeneration,
ModuleLike,
ModuleMetadata,
ModuleMetadataFileNotFound,
Expand Down Expand Up @@ -600,17 +598,13 @@ def modules_parser_config(cls, parser: ArgumentParser, parent_parsers: abc.Seque
install: ArgumentParser = subparser.add_parser(
"install",
parents=parent_parsers,
help="Install a module in the active Python environment.",
help="This command is no longer supported.",
description="""
Install a module in the active Python environment. Only works for v2 modules: v1 modules can only be installed in the context
of a project.
The 'inmanta module install' command is no longer supported. Instead, use one of the following approaches:
This command might reinstall Python packages in the development venv if the currently installed versions are not compatible
with the dependencies specified by the installed module.
Like `pip install`, this command does not reinstall a module for which the same version is already installed, except in editable
mode.
""".strip(),
1. To install a module in editable mode, use 'pip install -e .'.
2. For a non-editable installation, first run 'inmanta module build' followed by 'pip install ./dist/<dist-package>'.
""".strip(),
)
install.add_argument("-e", "--editable", action="store_true", help="Install in editable mode.")
install.add_argument("path", nargs="?", help="The path to the module.")
Expand Down Expand Up @@ -1025,33 +1019,16 @@ def show_bool(b: bool) -> str:

def install(self, editable: bool = False, path: Optional[str] = None) -> None:
"""
Install a module in the active Python environment. Only works for v2 modules: v1 modules can only be installed in the
context of a project.
This command is no longer supported.
Use 'pip install -e .' to install a module in editable mode.
Use 'inmanta module build' followed by 'pip install ./dist/<dist-package>' for non-editable install.
"""

# TODO: refine behavior
pip_config = PipConfig(use_system_config=True)

def install(install_path: str) -> None:
try:
env.process_env.install_for_config(
requirements=[], paths=[env.LocalPackagePath(path=install_path, editable=editable)], config=pip_config
)
except env.ConflictingRequirements as e:
raise InvalidModuleException("Module installation failed due to conflicting dependencies") from e

module_path: str = os.path.abspath(path) if path is not None else os.getcwd()
module: Module = self.construct_module(None, module_path)
if editable:
if module.GENERATION == ModuleGeneration.V1:
raise ModuleVersionException(
"Can not install v1 modules in editable mode. You can upgrade your module with `inmanta module v1tov2`."
)
install(module_path)
else:
with tempfile.TemporaryDirectory() as build_dir:
build_artifact: str = self.build(module_path, build_dir)
install(build_artifact)
raise CLIException(
"The 'inmanta module install' command is no longer supported. "
"For editable mode installation, use 'pip install -e .'. "
"For a regular installation, first run 'inmanta module build' and then 'pip install ./dist/<dist-package>'.",
exitcode=1,
)

def status(self, module: Optional[str] = None) -> None:
"""
Expand Down Expand Up @@ -1614,7 +1591,7 @@ def __init__(self, module_path: str) -> None:

def build(self, output_directory: str, dev_build: bool = False, byte_code: bool = False) -> str:
"""
Build the module and return the path to the build artifact.
Build the module using the pip system config and return the path to the build artifact.
:param byte_code: When set to true, only bytecode will be included. This also results in a binary wheel
"""
Expand Down Expand Up @@ -1783,7 +1760,7 @@ def _get_isolated_env_builder(self) -> DefaultIsolatedEnv:

def _build_v2_module(self, build_path: str, output_directory: str) -> str:
"""
Build v2 module using PEP517 package builder.
Build v2 module using the pip system config and using PEP517 package builder.
"""
try:
with self._get_isolated_env_builder() as env:
Expand Down
70 changes: 47 additions & 23 deletions tests/moduletool/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
Contact: code@inmanta.com
"""
import argparse
import logging
import os
import re
Expand All @@ -33,8 +32,9 @@

from inmanta import compiler, const, env, loader, module
from inmanta.ast import CompilerException
from inmanta.command import CLIException
from inmanta.config import Config
from inmanta.env import CommandRunner, ConflictingRequirements
from inmanta.env import CommandRunner, ConflictingRequirements, PipConfig
from inmanta.module import InmantaModuleRequirement, InstallMode, ModuleLoadingException, ModuleNotFoundException
from inmanta.moduletool import DummyProject, ModuleConverter, ModuleTool, ProjectTool
from moduletool.common import BadModProvider, install_project
Expand All @@ -44,17 +44,26 @@
LOGGER = logging.getLogger(__name__)


def run_module_install(module_path: str, editable: bool, set_path_argument: bool) -> None:
def run_module_install(module_path: str, editable: bool = False) -> None:
"""
Install the Inmanta module (v2) into the active environment using the `inmanta module install` command.
:param module_path: Path to the inmanta module
:param editable: Install the module in editable mode (pip install -e).
:param set_path_argument: If true provide the module_path via the path argument, otherwise the module path is set via cwd.
"""
if not set_path_argument:
os.chdir(module_path)
ModuleTool().execute("install", argparse.Namespace(editable=editable, path=module_path if set_path_argument else None))
if editable:
env.process_env.install_for_config(
requirements=[],
paths=[env.LocalPackagePath(path=module_path, editable=True)],
config=PipConfig(use_system_config=True),
)
else:
mod_artifact_path = ModuleTool().build(path=module_path)
env.process_env.install_for_config(
requirements=[],
paths=[env.LocalPackagePath(path=mod_artifact_path)],
config=PipConfig(use_system_config=True),
)


def test_bad_checkout(git_modules_dir, modules_repo):
Expand Down Expand Up @@ -206,11 +215,9 @@ def test_dev_checkout(git_modules_dir, modules_repo):


@pytest.mark.parametrize_any("editable", [True, False])
@pytest.mark.parametrize_any("set_path_argument", [True, False])
def test_module_install(snippetcompiler_clean, modules_v2_dir: str, editable: bool, set_path_argument: bool) -> None:
def test_module_install(snippetcompiler_clean, modules_v2_dir: str, editable: bool) -> None:
"""
Install a simple v2 module with the `inmanta module install` command. Make sure the command works with all possible values
for its options.
Make sure it is possible to install a module in both non-editable and editable mode
"""
# activate snippetcompiler's venv
snippetcompiler_clean.setup_for_snippet("")
Expand All @@ -222,7 +229,7 @@ def is_installed(name: str, only_editable: bool = False) -> bool:
return name in env.process_env.get_installed_packages(only_editable=only_editable)

assert not is_installed(python_module_name)
run_module_install(module_path, editable, set_path_argument)
run_module_install(module_path, editable)
assert is_installed(python_module_name, True) == editable
if not editable:
assert is_installed(python_module_name, False)
Expand All @@ -231,7 +238,7 @@ def is_installed(name: str, only_editable: bool = False) -> bool:
@pytest.mark.slowtest
def test_module_install_conflicting_requirements(tmpdir: py.path.local, snippetcompiler_clean, modules_v2_dir: str) -> None:
"""
Verify that the module tool's install command raises an appropriate exception when a module has conflicting dependencies.
Verify that installing a module raises an appropriate exception when a module has conflicting dependencies.
"""
# activate snippetcompiler's venv
snippetcompiler_clean.setup_for_snippet("")
Expand Down Expand Up @@ -259,10 +266,13 @@ def test_module_install_conflicting_requirements(tmpdir: py.path.local, snippetc
new_requirements=[InmantaModuleRequirement.parse(name) for name in ("modone", "modtwo")],
)

with pytest.raises(module.InvalidModuleException) as exc_info:
run_module_install(module_path, False, True)
assert isinstance(exc_info.value.__cause__, ConflictingRequirements)
assert "caused by:" in exc_info.value.format_trace()
with pytest.raises(ConflictingRequirements) as exc_info:
run_module_install(module_path, False)
assert (
"ERROR: Cannot install inmanta-module-modone==1.2.3 and "
"inmanta-module-modtwo==1.2.3 because these package versions "
"have conflicting dependencies."
) in exc_info.value.format_trace()


@pytest.mark.parametrize_any("dev", [True, False])
Expand Down Expand Up @@ -290,7 +300,7 @@ def test_module_install_version(
new_version=module_version,
)
os.chdir(project.path)
ModuleTool().install(editable=True, path=module_path)
run_module_install(module_path)

# check version
mod: module.Module = ModuleTool().get_module(module_name)
Expand Down Expand Up @@ -324,7 +334,7 @@ def new_files_exist() -> Iterator[bool]:
)

# install module
ModuleTool().install(editable=False, path=module_path)
run_module_install(module_path)

assert not any(new_files_exist())

Expand All @@ -334,7 +344,7 @@ def new_files_exist() -> Iterator[bool]:
open(os.path.join(model_dir, "newmod.cf"), "w").close()
open(os.path.join(module_path, const.PLUGINS_PACKAGE, module_name, "newmod.py"), "w").close()
module_from_template(module_path, new_version=version.Version("2.0.0"), in_place=True)
ModuleTool().install(editable=False, path=module_path)
run_module_install(module_path)

assert all(new_files_exist())

Expand Down Expand Up @@ -363,7 +373,7 @@ def test_3322_module_install_deep_data_files(tmpdir: py.path.local, snippetcompi
snippetcompiler_clean.setup_for_snippet("")

# install module: non-editable mode
ModuleTool().install(editable=False, path=module_path)
run_module_install(module_path)

assert os.path.exists(
os.path.join(
Expand Down Expand Up @@ -407,7 +417,7 @@ def model_file_installed() -> bool:
snippetcompiler_clean.setup_for_snippet("")

# install module: non-editable mode
ModuleTool().install(editable=False, path=module_path)
run_module_install(module_path)
assert model_file_installed()

# remove model file and reinstall
Expand All @@ -417,10 +427,24 @@ def model_file_installed() -> bool:
new_version=version.Version("2.0.0"),
in_place=True,
)
ModuleTool().install(editable=False, path=module_path)
run_module_install(module_path)
assert not model_file_installed()


def test_module_install_exception() -> None:
"""
Verify that the "inmanta module install" commands raises an exception
"""

with pytest.raises(
CLIException,
match="The 'inmanta module install' command is no longer supported. For editable mode "
"installation, use 'pip install -e .'. For a regular installation, first run 'inmanta module "
"build' and then 'pip install ./dist/<dist-package>'.",
):
ModuleTool().execute("install", [])


def test_project_install_logs(
snippetcompiler_clean,
tmpdir: py.path.local,
Expand Down
8 changes: 7 additions & 1 deletion tests/test_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

import utils
from inmanta import const, env, loader, moduletool
from inmanta.env import PipConfig
from inmanta.loader import ModuleSource, SourceInfo
from inmanta.module import Project

Expand Down Expand Up @@ -283,7 +284,12 @@ def test_plugin_module_finder(
str(venv_module_dir),
new_content_init_py="where = 'venv'",
)
moduletool.ModuleTool().install(path=str(venv_module_dir))
mod_artifact_path = moduletool.ModuleTool().build(path=str(venv_module_dir))
env.process_env.install_for_config(
requirements=[],
paths=[env.LocalPackagePath(path=mod_artifact_path)],
config=PipConfig(use_system_config=True),
)

module_to_reload: Optional[ModuleType] = None
if reload:
Expand Down
10 changes: 5 additions & 5 deletions tests/test_module_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,13 @@
import pytest
from pkg_resources import Requirement

from inmanta import compiler, const, loader, plugins, resources
from inmanta import compiler, const, env, loader, plugins, resources
from inmanta.ast import CompilerException
from inmanta.const import CF_CACHE_DIR
from inmanta.env import ConflictingRequirements, LocalPackagePath, PackageNotFound, PipConfig, process_env
from inmanta.module import (
DummyProject,
InmantaModuleRequirement,
InvalidModuleException,
ModuleLoadingException,
ModuleNotFoundException,
ModuleV1,
Expand Down Expand Up @@ -616,10 +615,11 @@ def test_project_requirements_dont_overwrite_core_requirements_source(
jinja2_version_before = active_env.get_installed_packages()["Jinja2"].base_version

# Install the module
with pytest.raises(InvalidModuleException) as e:
ModuleTool().install(editable=False, path=module_path)
mod_artifact_path = ModuleTool().build(path=module_path)
with pytest.raises(ConflictingRequirements) as e:
env.process_env.install_from_source([env.LocalPackagePath(path=mod_artifact_path, editable=False)])

assert ("Module installation failed due to conflicting dependencies") in str(e.value.msg)
assert ("these package versions have conflicting dependencies") in str(e.value.msg)

jinja2_version_after = active_env.get_installed_packages()["Jinja2"].base_version
assert jinja2_version_before == jinja2_version_after
Expand Down

0 comments on commit e5d8522

Please sign in to comment.