Ejemplo n.º 1
0
def diff(name: str) -> ListLike:
    """Get the difference between history yaml and local conda environment"""
    env = Environment.read(name=name)
    env_reader = EnvIO(env_directory=USER_ENVS_DIR / name)
    version_diff_pkges, new_pkges, missing_pkges = History.history_diff(
        env_name=name, env=env, env_reader=env_reader)
    return missing_pkges + version_diff_pkges + new_pkges
Ejemplo n.º 2
0
def merge_conflicting_changes(env: Environment):
    """Reconciles packages between local and remote"""
    remote_dir = env.local_io.get_remote_dir()
    remote_io = EnvIO(env_directory=remote_dir)
    remote_history = remote_io.get_history()

    local_history = env.history
    update_conda_environment(env_dir=remote_dir)
    if _r_env_needs_updating(local_history=local_history,
                             remote_history=remote_history):
        update_r_environment(name=env.name, env_dir=remote_dir)
    EnvIO.overwrite_local(local_io=env.local_io, remote_io=remote_io)
    new_env = Environment(name=env.name, history=remote_history)
    new_env.validate()
    extra_logs = []
    for log in local_history.logs:
        if log not in set(remote_history.logs):
            extra_logs.append(log)
    for log in extra_logs:
        new_env = _update_from_extra_log(env=new_env,
                                         history=local_history,
                                         log=log)

    new_env.validate()
    new_env.export()

    env.history = new_env.history
    logger.info("Successfully updated the environment.")
    return new_env
Ejemplo n.º 3
0
 def __init__(self, name: str, history: Optional[History] = None):
     self.name = name
     self.history = history
     self.local_io = EnvIO(env_directory=USER_ENVS_DIR / name)
     self.dependencies = {}
     if self.history:
         self.dependencies = get_dependencies(name=self.name)
         if self.history.packages.get("r"):
             self.dependencies["r"] = get_r_dependencies(name=self.name)
Ejemplo n.º 4
0
def setup_remote(
    name: str,
    remote_dir: Optional[PathLike] = None,
    if_missing: bool = False,
    yes: bool = False,
) -> None:
    """setup remote directory for push and pull"""
    if not remote_dir:
        remote_dir = infer_remote_dir(check_history_exists=False)
    env_io = EnvIO(env_directory=USER_ENVS_DIR / name)
    env_io.set_remote_dir(remote_dir=remote_dir, if_missing=if_missing, yes=yes)
Ejemplo n.º 5
0
def test_replace_existing_env_success(mocker):
    mocker.patch("conda_env_tracker.env.delete_conda_environment")
    mocker.patch("conda_env_tracker.env.prompt_yes_no").return_value = True
    env_name = "conda_env_tracker-test-create-history"
    create_cmd = f"conda create --name {env_name} pandas"
    channels = ["conda-forge", "main"]
    packages = Packages.from_specs("pandas")
    action = "pandas=0.23=py_36"

    mocker.patch(
        "conda_env_tracker.env.get_all_existing_environment").return_value = [
            env_name
        ]
    mocker.patch("conda_env_tracker.env.conda_create",
                 mocker.Mock(return_value=create_cmd))
    mocker.patch(
        "conda_env_tracker.env.get_dependencies",
        mocker.Mock(return_value={
            "conda": {
                "pandas": Package("pandas", "*", "0.23=py_36")
            },
            "pip": {},
        }),
    )
    mocker.patch(
        "conda_env_tracker.history.debug.get_pip_version",
        mocker.Mock(return_value="18.1"),
    )
    mocker.patch("conda_env_tracker.env.get_conda_channels",
                 mocker.Mock(return_value=channels))
    Environment.create(name=env_name, packages=packages)

    writer = EnvIO(env_directory=USER_ENVS_DIR / env_name)
    history = writer.get_history()
    channel_string = "--override-channels --strict-channel-priority " + " ".join(
        "--channel " + channel for channel in channels)
    assert history.actions == [
        f"conda create --name {env_name} {action} {channel_string}"
    ]
    assert history.packages == {
        "conda": {
            "pandas": Package.from_spec("pandas")
        }
    }
    assert history.channels == channels
    assert history.logs == [create_cmd]

    env_dir = Path(USER_ENVS_DIR) / env_name
    shutil.rmtree(env_dir)
Ejemplo n.º 6
0
def get_env_name(infer: bool = False) -> str:
    """Get the name of the environment either from current conda environment, from remote directory,
    or inferred from git repo.
    """
    # pylint: disable=redefined-outer-name
    if not infer:
        return get_active_conda_env_name()
    try:
        remote_dir = infer_remote_dir()
        history = EnvIO(env_directory=remote_dir).get_history()
        return history.name
    except CondaEnvTrackerHistoryNotFoundError as err:
        raise CondaEnvTrackerHistoryNotFoundError(
            f"Cannot infer name from history, often resolved by passing the name argument. Full error: {str(err)}"
        )
Ejemplo n.º 7
0
 def remove(self, yes=False) -> None:
     """Remove the environment and history."""
     delete_conda_environment(name=self.name)
     try:
         remote_io = EnvIO(self.local_io.get_remote_dir())
     except CondaEnvTrackerRemoteError:
         remote_io = None
     self.local_io.delete_all()
     if remote_io and (
         yes
         or prompt_yes_no(
             prompt_msg=f"Do you want to remove remote files in dir: {remote_io.env_dir}?"
         )
     ):
         remote_io.delete_all()
Ejemplo n.º 8
0
def setup_env(mocker, request):
    """Setup and teardown for tests with parameterized inputs. Even yes is passed in commands like create and install,
    the option itself don't show up in actions and logs as it provide no additional value for reproducibility."""
    env_dir = USER_ENVS_DIR / ENV_NAME
    mocker.patch(
        "conda_env_tracker.history.debug.get_pip_version",
        mocker.Mock(return_value="18.1"),
    )
    mocker.patch("conda_env_tracker.env.get_all_existing_environment")
    mocker.patch("conda_env_tracker.main._ask_user_to_sync")
    mocker.patch("conda_env_tracker.gateways.conda.run_command")
    initial_conda_packages = {
        "pandas": Package("pandas", "pandas", "0.23", "py_36")
    }
    get_package_mock = mocker.patch(
        "conda_env_tracker.env.get_dependencies",
        mocker.Mock(return_value={
            "conda": initial_conda_packages,
            "pip": {}
        }),
    )
    condarc_channels = ["conda-forge", "main"]
    mocker.patch(
        "conda_env_tracker.env.get_conda_channels",
        mocker.Mock(return_value=condarc_channels),
    )
    if "channels" in request.param["input"]:
        channels = request.param["input"]["channels"]
    else:
        channels = condarc_channels
    id_mock = mocker.patch("conda_env_tracker.history.history.uuid4")
    id_mock.return_value = "my_unique_id"
    env = main.create(name=ENV_NAME, **request.param["input"])
    env_io = EnvIO(env_directory=env_dir)
    yield {
        "channels": channels,
        "env": env,
        "env_io": env_io,
        "expected": request.param["expected"],
        "get_package_mock": get_package_mock,
        "initial_conda_packages": initial_conda_packages,
        "id": id_mock,
    }
    if env_dir.exists():
        shutil.rmtree(env_dir)
Ejemplo n.º 9
0
 def __init__(
     self,
     name: str,
     history: Optional[History] = None,
     dependencies: Optional[dict] = None,
 ):
     self.name = name
     self.history = history
     self.local_io = EnvIO(env_directory=USER_ENVS_DIR / name)
     if dependencies:
         self.dependencies = dependencies
     else:
         self.dependencies = {}
         if self.history:
             self.dependencies = get_dependencies(name=self.name)
             if self.history.packages.get("r"):
                 self.dependencies["r"] = get_r_dependencies(name=self.name)
     if history:
         self.history.packages.update_versions(dependencies=self.dependencies)
Ejemplo n.º 10
0
def pull(env: Environment, yes: bool = False) -> Environment:
    """Pull history from remote to local"""
    try:
        remote_dir = env.local_io.get_remote_dir()
    except CondaEnvTrackerRemoteError as remote_err:
        raise CondaEnvTrackerPullError(str(remote_err))
    remote_io = EnvIO(env_directory=remote_dir)
    remote_history = remote_io.get_history()

    local_history = env.history

    if _create_env_with_new_id(env=env, remote_history=remote_history,
                               yes=yes):
        return replace_local_with_remote(
            env=env,
            remote_dir=remote_dir,
            remote_io=remote_io,
            remote_history=remote_history,
        )
    if _nothing_to_pull(local_history=local_history,
                        remote_history=remote_history):
        logger.info("Nothing to pull.")
        return env
    if _local_needs_update(local_history=local_history,
                           remote_history=remote_history,
                           yes=yes):
        return replace_local_with_remote(
            env=env,
            remote_dir=remote_dir,
            remote_io=remote_io,
            remote_history=remote_history,
        )

    if not yes and not prompt_yes_no(prompt_msg=(
            "Remote and local have different packages, do you want to overwrite "
            "with remote and append local")):
        logger.info("Exiting without updating local environment.")
        raise CondaEnvTrackerPullError(
            "Conflicting Packages in remote and local; user elected not to update"
        )

    return merge_conflicting_changes(env=env)
Ejemplo n.º 11
0
def push(env: Environment) -> Environment:
    """Handle push to remote"""
    remote_dir = env.local_io.get_remote_dir()
    remote_io = EnvIO(env_directory=remote_dir)
    remote_history = remote_io.get_history()

    if remote_history == env.history:
        logger.info("Nothing to push.")
        return env

    if remote_history and (not is_ordered_subset(
            set=env.history.actions, subset=remote_history.actions) or
                           not is_ordered_subset(set=env.history.logs,
                                                 subset=remote_history.logs)):
        raise CondaEnvTrackerPushError(
            PUSH_ERROR_STR.format(remote_dir=remote_dir,
                                  local_dir=USER_ENVS_DIR / env.name))
    env.local_io.copy_environment(remote_dir)
    logger.info(f"Successfully push {env.name} to {remote_dir}")
    return env
Ejemplo n.º 12
0
def _remote_dir_is_set(name: str) -> bool:
    """Check if the remote directory has been setup for the cet environment."""
    env_io = EnvIO(env_directory=USER_ENVS_DIR / name)
    return env_io.is_remote_dir_set()
Ejemplo n.º 13
0
def pull(env: Environment, yes: bool = False) -> Environment:
    """Pull history from remote to local"""
    remote_dir = env.local_io.get_remote_dir()
    remote_io = EnvIO(env_directory=remote_dir)
    remote_history = remote_io.get_history()

    local_history = env.history

    _check_for_errors(local_history=local_history, remote_history=remote_history)

    if _nothing_to_pull(local_history=local_history, remote_history=remote_history):
        logger.info("Nothing to pull.")
        return env
    if _local_needs_update(local_history=local_history, remote_history=remote_history):
        update(
            env=env,
            remote_dir=remote_dir,
            remote_io=remote_io,
            remote_history=remote_history,
        )
        return env
    if _actions_in_different_order(
        local_history=local_history, remote_history=remote_history
    ):
        if not yes and not prompt_yes_no(
            prompt_msg="Remote environment has same packages but in different order, "
            "Should we overwrite local with remote environment"
        ):
            logger.info("Exiting without updating local environment.")
            return env
        update(
            env=env,
            remote_dir=remote_dir,
            remote_io=remote_io,
            remote_history=remote_history,
        )
        return env
    if not yes and not prompt_yes_no(
        prompt_msg=(
            "Remote and local have different packages, do you want to overwrite "
            "with remote and append local"
        )
    ):
        logger.info("Exiting without updating local environment.")
        return env
    update_conda_environment(env_dir=remote_dir)
    if _r_env_needs_updating(
        local_history=local_history, remote_history=remote_history
    ):
        update_r_environment(name=env.name, env_dir=remote_dir)
    EnvIO.overwrite_local(local_io=env.local_io, remote_io=remote_io)
    new_env = Environment(name=env.name, history=remote_history)
    new_env.validate()
    extra_logs = []
    for log in local_history.logs:
        if log not in set(remote_history.logs):
            extra_logs.append(log)
    for log in extra_logs:
        new_env = _update_from_extra_log(env=new_env, history=local_history, log=log)
    new_env = _update_r_packages(
        env=new_env, local_history=local_history, remote_history=remote_history
    )

    new_env.validate()
    new_env.export()

    env.history = new_env.history
    logger.info("Successfully updated the environment.")
    return new_env
Ejemplo n.º 14
0
    def create(
        cls,
        name: str,
        packages: Packages,
        channels: ListLike = None,
        yes: bool = False,
        strict_channel_priority: bool = True,
    ):
        """Creating a conda environment from a list of packages."""
        if name == "base":
            raise CondaEnvTrackerCondaError(
                "Environment can not be created using default name base"
            )

        if name in get_all_existing_environment():
            message = (
                f"This environment {name} already exists. Would you like to replace it"
            )
            if prompt_yes_no(prompt_msg=message, default=False):
                delete_conda_environment(name=name)
                local_io = EnvIO(env_directory=USER_ENVS_DIR / name)
                if local_io.env_dir.exists():
                    local_io.delete_all()
            else:
                raise CondaEnvTrackerCondaError(f"Environment {name} already exists")
        logger.debug(f"creating conda env {name}")

        conda_create(
            name=name,
            packages=packages,
            channels=channels,
            yes=yes,
            strict_channel_priority=strict_channel_priority,
        )
        create_cmd = get_conda_create_command(
            name=name,
            packages=packages,
            channels=channels,
            strict_channel_priority=strict_channel_priority,
        )
        specs = Actions.get_package_specs(
            packages=packages, dependencies=get_dependencies(name=name)["conda"]
        )

        if not channels:
            channels = get_conda_channels()

        dependencies = get_dependencies(name=name)

        history = History.create(
            name=name,
            channels=Channels(channels),
            packages=PackageRevision.create(packages, dependencies=dependencies),
            logs=Logs(create_cmd),
            actions=Actions.create(
                name=name,
                specs=specs,
                channels=Channels(channels),
                strict_channel_priority=strict_channel_priority,
            ),
            diff=Diff.create(packages=packages, dependencies=dependencies),
            debug=Debug.create(name=name),
        )
        env = cls(name=name, history=history, dependencies=dependencies)
        env.export()

        return env
Ejemplo n.º 15
0
 def read(cls, name: str):
     """read the environment from history file"""
     reader = EnvIO(env_directory=USER_ENVS_DIR / name)
     history = reader.get_history()
     return cls(name=name, history=history)
def test_remote_end_to_end(end_to_end_setup, mocker):
    """Test setup, create, install, pull and push feature of conda_env_tracker"""
    env = end_to_end_setup["env"]
    remote_dir = end_to_end_setup["remote_dir"]
    channels = end_to_end_setup["channels"]
    channel_command = end_to_end_setup["channel_command"]

    setup_remote(name=env.name, remote_dir=remote_dir)

    local_io = EnvIO(env_directory=USER_ENVS_DIR / env.name)
    assert str(local_io.get_remote_dir()) == str(remote_dir)
    assert not list(remote_dir.glob("*"))

    push(name=env.name)
    remote_io = EnvIO(env_directory=remote_dir)
    assert remote_io.get_history() == local_io.get_history()
    assert remote_io.get_environment() == local_io.get_environment()

    env = conda_install(name=env.name, specs=["pytest"], yes=True)
    assert env.local_io.get_history() != remote_io.get_history()
    env = push(name=env.name)
    assert env.local_io.get_history() == remote_io.get_history()

    log_mock = mocker.patch("conda_env_tracker.pull.logging.Logger.info")
    env = pull(name=env.name)
    log_mock.assert_called_once_with("Nothing to pull.")

    remove_package_from_history(env, "pytest")

    conda_dependencies = env.dependencies["conda"]
    assert env.local_io.get_history() != remote_io.get_history()
    assert env.history.packages == {
        "conda": {
            "python":
            Package(
                "python",
                "python=3.6",
                version=conda_dependencies["python"].version,
                build=conda_dependencies["python"].build,
            ),
            "colorama":
            Package(
                "colorama",
                "colorama",
                version=conda_dependencies["colorama"].version,
                build=conda_dependencies["colorama"].build,
            ),
        }
    }

    env = pull(name=env.name)

    assert env.local_io.get_history() == remote_io.get_history()
    assert remote_io.get_environment() == local_io.get_environment()

    remove_package_from_history(env, "pytest")
    assert env.local_io.get_history() != remote_io.get_history()

    env = conda_install(name=env.name,
                        specs=["pytest-cov"],
                        channels=("main", ),
                        yes=True)
    env = pull(name=env.name, yes=True)

    conda_dependencies = env.dependencies["conda"]
    assert env.history.packages == {
        "conda": {
            "python":
            Package(
                "python",
                "python=3.6",
                version=conda_dependencies["python"].version,
                build=conda_dependencies["python"].build,
            ),
            "colorama":
            Package(
                "colorama",
                "colorama",
                version=conda_dependencies["colorama"].version,
                build=conda_dependencies["colorama"].build,
            ),
            "pytest":
            Package(
                "pytest",
                "pytest",
                version=conda_dependencies["pytest"].version,
                build=conda_dependencies["pytest"].build,
            ),
            "pytest-cov":
            Package(
                "pytest-cov",
                "pytest-cov",
                version=conda_dependencies["pytest-cov"].version,
                build=conda_dependencies["pytest-cov"].build,
            ),
        }
    }

    log_list = [
        f"conda create --name {env.name} python=3.6 colorama {channel_command}",
        f"conda install --name {env.name} pytest",
        f"conda install --name {env.name} pytest-cov --channel main",
    ]
    assert env.history.logs == log_list

    actual_env = (env.local_io.env_dir / "environment.yml").read_text()
    conda_dependencies = get_dependencies(name=env.name)["conda"]
    expected_env = [f"name: {env.name}", "channels:"]
    for channel in channels + ["nodefaults"]:
        expected_env.append(f"  - {channel}")
    expected_env = ("\n".join(expected_env + [
        "dependencies:",
        "  - python=" + conda_dependencies["python"].version,
        "  - colorama=" + conda_dependencies["colorama"].version,
        "  - pytest=" + conda_dependencies["pytest"].version,
        "  - pytest-cov=" + conda_dependencies["pytest-cov"].version,
    ]) + "\n")
    assert actual_env == expected_env

    expected_debug = 3 * [{
        "platform": get_platform_name(),
        "conda_version": CONDA_VERSION,
        "pip_version": get_pip_version(name=env.name),
        "timestamp": str(date.today()),
    }]
    for i in range(len(env.history.debug)):
        for key, val in expected_debug[i].items():
            if key == "timestamp":
                assert env.history.debug[i][key].startswith(val)
            else:
                assert env.history.debug[i][key] == val