Example #1
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
Example #2
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)
Example #3
0
def update(
    env: Environment, remote_dir: PathLike, remote_io: EnvIO, remote_history: History
):
    """Update the environment and history."""
    update_conda_environment(env_dir=remote_dir)
    if _r_env_needs_updating(env.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)
    env.replace_history(history=remote_history)
    env.validate()
    logger.info("Successfully updated the environment.")
Example #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)
Example #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)
Example #6
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()
Example #7
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
Example #8
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)
Example #9
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)
Example #10
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
Example #11
0
def validate_remote_if_missing(
    env_io: EnvIO, remote_dir: PathLike, yes: bool = False, if_missing: bool = False
):
    """Validate if remote dir exists"""
    try:
        current_remote_dir = env_io.get_remote_dir()
        if not yes and if_missing and remote_dir != current_remote_dir:
            sys.exit(
                f"Current remote directory ({current_remote_dir}) differs from new ({remote_dir}) and [--if-missing] flag was set."
            )
    except CondaEnvTrackerRemoteError:
        pass
Example #12
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)}"
        )
Example #13
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)
Example #14
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()
Example #15
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
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
Example #17
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
Example #18
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)
Example #19
0
class Environment:
    """Class representing a conda environment."""

    sources = ["conda", "pip", "r"]

    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)

    @classmethod
    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

    @classmethod
    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)

    @classmethod
    def infer(cls, name: str, packages: Packages, channels: ListLike = None):
        """create conda_env_tracker environment by inferring to existing conda environment"""
        if name == "base":
            raise CondaEnvTrackerCondaError(
                "Environment can not be created using default name base"
            )

        if name not in get_all_existing_environment():
            raise CondaEnvTrackerCondaError(
                f"Environment {name} can not be inferred, does not exist"
            )

        dependencies = get_dependencies(name=name)
        if "r-base" in dependencies["conda"]:
            dependencies["r"] = get_r_dependencies(name=name)

        user_packages = {"conda": Packages(), "pip": Packages()}
        for package in packages:
            if package.name in dependencies.get("conda", Packages()):
                user_packages["conda"].append(package)
            elif package.name in dependencies.get("pip", Packages()):
                user_packages["pip"].append(package)
            else:
                raise CondaEnvTrackerCondaError(
                    f"Environment {name} does not have {package.spec} installed"
                )

        conda_create_cmd = get_conda_create_command(
            name, user_packages["conda"], channels
        )

        specs = Actions.get_package_specs(
            packages=user_packages["conda"], dependencies=dependencies["conda"]
        )

        history = History.create(
            name=name,
            channels=Channels(channels),
            packages=PackageRevision.create(
                user_packages["conda"], dependencies=dependencies
            ),
            logs=Logs(conda_create_cmd),
            actions=Actions.create(name=name, specs=specs, channels=Channels(channels)),
            diff=Diff.create(
                packages=user_packages["conda"], dependencies=dependencies
            ),
            debug=Debug.create(name=name),
        )

        env = cls(name=name, history=history, dependencies=dependencies)
        if user_packages["pip"]:
            handler = PipHandler(env=env)
            handler.update_history_install(packages=user_packages["pip"])
            env = handler.env
        env.export()

        return env

    def export(self) -> None:
        """Export the conda environment and history."""
        self.local_io.write_history_file(history=self.history)
        self._export_packages()
        self._export_install_r_if_necessary()

    def rebuild(self) -> None:
        """Rebuild the conda environment."""
        logger.debug('If struggling to use an environment try "conda clean --all".')
        delete_conda_environment(name=self.name)
        update_conda_environment(env_dir=self.local_io.env_dir)
        self.update_dependencies()

    def remove(self, yes=False) -> None:
        """Remove the environment and history."""
        if yes or prompt_yes_no(
            f"Are you sure you want to remove the {self.name} environment",
            default=False,
        ):
            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()

    def replace_history(self, history: History) -> None:
        """Replace with a new history."""
        self.history = history
        self.update_dependencies()

    def validate(self) -> None:
        """Check that all packages are installed correctly."""
        if self.history.packages.get("r"):
            self.update_dependencies(update_r_dependencies=True)
        else:
            self.update_dependencies()
        packages = Packages()
        for source in self.sources:
            packages += [
                package for package in self.history.packages.get(source, {}).values()
            ]
        self.validate_packages(packages)

    def append_channels(self, channels: ListLike) -> None:
        """Append channels to the list of channels in the history."""
        for channel in channels:
            if channel not in self.history.channels:
                self.history.channels.append(channel)
        self.local_io.write_history_file(history=self.history)

    def update_dependencies(self, update_r_dependencies=False):
        """Update the list of all conda, pip, and R dependencies installed."""
        self.dependencies = get_dependencies(name=self.name)
        if update_r_dependencies:
            self.dependencies["r"] = get_r_dependencies(name=self.name)

    def validate_packages(
        self, installed_packages: Packages = None, source: str = "conda"
    ):
        """Raise an error if a package was not installed correctly. If this command removes a package that
        was previously specified by the user, then warn that it has been removed and remove it from the history.
        """
        removed = []
        for package in self.history.packages.get(source, {}):
            if package not in self.dependencies.get(source, {}):
                removed.append(package)
        installed_names = set()
        if installed_packages:
            installed_names = {package.name for package in installed_packages}
        for package in removed:
            if package in installed_names:
                raise CondaEnvTrackerInstallError(
                    f'Package "{package}" was not installed.'
                )
            logger.warning(f'Package "{package}" was removed during the last command.')
            self.history.packages[source].pop(package)

    def _export_packages(self) -> None:
        """Export a conda env yaml file with only the packages with versions for switching platforms.

        Adding nodefaults to the channel list prevents conda env update statements from using the channels
        in the users .condarc file.
        """
        conda_environment = {
            "name": self.name,
            "channels": self.history.channels.export(),
        }
        if "nodefaults" not in conda_environment["channels"]:
            conda_environment["channels"].append("nodefaults")
        conda_environment["dependencies"] = []
        for package in self.history.packages.get("conda"):
            version = self.dependencies["conda"][package].version
            conda_environment["dependencies"].append(f"{package}={version}")
        conda_environment = self._add_pip_dependencies(conda_environment)
        contents = yaml.dump(conda_environment, default_flow_style=False)
        self.local_io.export_packages(contents=contents)

    def _add_pip_dependencies(self, conda_environment: Dict) -> Dict:
        """Add the pip dependencies to the environment yaml dict."""
        if self.history.packages.get("pip") and conda_environment.get("dependencies"):
            pip = {"pip": []}
            for name, package in self.history.packages["pip"].items():
                if package.spec_is_custom():
                    pip["pip"].append(package.spec)
                else:
                    version = self.dependencies["pip"][name].version
                    pip["pip"].append(f"{name}=={version}")
            conda_environment["dependencies"].append(pip)
        return conda_environment

    def _export_install_r_if_necessary(self) -> None:
        """Export an install.R file that can be used to install the same R packages and versions."""
        if self.history.packages.get("r"):
            install_r = export_install_r(
                packages=Packages(
                    [package for package in self.history.packages["r"].values()]
                )
            )
            self.local_io.export_install_r(install_r)
        else:
            self.local_io.delete_install_r()