예제 #1
0
 def unprovide(self,
               requirement,
               environ,
               local_state_file,
               overrides,
               requirement_status=None):
     """Override superclass to delete the downloaded file."""
     project_dir = environ['PROJECT_DIR']
     filename = os.path.abspath(
         os.path.join(project_dir, requirement.filename))
     try:
         if os.path.isdir(filename):
             shutil.rmtree(filename)
         elif os.path.isfile(filename):
             os.remove(filename)
         else:
             return SimpleStatus(
                 success=True,
                 description=(
                     "No need to remove %s which wasn't downloaded." %
                     filename))
         return SimpleStatus(success=True,
                             description=("Removed downloaded file %s." %
                                          filename))
     except Exception as e:
         return SimpleStatus(success=False,
                             description=("Failed to remove %s: %s." %
                                          (filename, str(e))))
예제 #2
0
def build_image(path,
                tag,
                command,
                builder_image=DEFAULT_BUILDER_IMAGE,
                build_args=None):
    """Run s2i build."""

    cmd = [
        's2i', 'build', '--copy', path, builder_image, tag, '-e',
        'CMD={}'.format(command)
    ]
    if build_args is not None:
        cmd.extend(build_args)

    start_msg = '''*** {} image build starting.'''.format(tag)
    print(start_msg)
    print(' '.join(cmd))

    try:
        _ = subprocess.check_call(cmd)
        msg = '''\nDocker image {} build successful.'''.format(tag)
        return SimpleStatus(success=True, description=msg)
    except subprocess.CalledProcessError as e:
        error_msg = '''\nAn error was encountered building this docker image.'''
        return SimpleStatus(success=False,
                            description=error_msg,
                            errors=[str(e)])
    except FileNotFoundError as e:
        error_msg = """\nERROR: The source-to-image (s2i) executable was not found. It can be installed using
    conda install -c ctools source-to-image"""
        return SimpleStatus(success=False,
                            description=error_msg,
                            errors=[str(e)])
예제 #3
0
def _upload(project,
            archive_filename,
            uploaded_basename,
            site=None,
            username=None,
            token=None,
            log_level=None):
    assert not project.problems

    client = _Client(site=site,
                     username=username,
                     token=token,
                     log_level=log_level)
    try:
        json = client.upload(project.publication_info(), archive_filename,
                             uploaded_basename)
        return _UploadedStatus(json)
    except Unauthorized as e:
        return SimpleStatus(
            success=False,
            description='Please log in with the "anaconda login" command.',
            errors=["Not logged in."])
    except BinstarError as e:
        return SimpleStatus(success=False,
                            description="Upload failed.",
                            errors=[str(e)])
예제 #4
0
def set_variables(project, vars_and_values, env_spec_name=None):
    """Set variables' values in anaconda-project-local.yml.

    Returns a ``Status`` instance which evaluates to True on
    success and has an ``errors`` property (with a list of error
    strings) on failure.

    Args:
        project (Project): the project
        vars_and_values (list of tuple): key-value pairs
        env_spec_name (str): name of env spec to use

    Returns:
        ``Status`` instance
    """
    (env_prefix, status) = _prepare_env_prefix(project, env_spec_name)
    if env_prefix is None:
        return status

    local_state = LocalStateFile.load_for_directory(project.directory_path)
    var_reqs = dict()
    for req in project.find_requirements(klass=EnvVarRequirement):
        var_reqs[req.env_var] = req
    present_vars = set(var_reqs.keys())
    errors = []
    local_state_count = 0
    keyring_count = 0
    for varname, value in vars_and_values:
        if varname in present_vars:
            if var_reqs[varname].encrypted:
                # import keyring locally because it's an optional dependency
                # that prints a warning when it's needed but not found.
                from anaconda_project.internal import keyring

                keyring.set(env_prefix, varname, value)
                keyring_count = keyring_count + 1
            else:
                local_state.set_value(['variables', varname], value)
                local_state_count = local_state_count + 1
        else:
            errors.append("Variable %s does not exist in the project." %
                          varname)

    if errors:
        return SimpleStatus(success=False,
                            description="Could not set variables.",
                            errors=errors)
    else:
        if local_state_count > 0:
            local_state.save()
        if keyring_count == 0:
            description = ("Values saved in %s." % local_state.filename)
        elif local_state_count == 0:
            description = ("Values saved in the system keychain.")
        else:
            description = (
                "%d values saved in %s, %d values saved in the system keychain."
                % (local_state_count, local_state.filename, keyring_count))
        return SimpleStatus(success=True, description=description)
예제 #5
0
def test_simple_status_properties():
    good_status = SimpleStatus(success=True, description="quick brown fox", errors=["bar"])
    assert good_status
    assert good_status.status_description == "quick brown fox"
    assert good_status.errors == ["bar"]

    bad_status = SimpleStatus(success=False, description="quick brown fox", errors=["bar"])
    assert not bad_status
    assert bad_status.status_description == "quick brown fox"
    assert bad_status.errors == ["bar"]
예제 #6
0
def _remove_env_path(env_path):
    """Also used by project_ops.py to delete environment files."""
    if os.path.exists(env_path):
        try:
            shutil.rmtree(env_path)
            return SimpleStatus(success=True, description=("Deleted environment files in %s." % env_path))
        except Exception as e:
            problem = "Failed to remove environment files in {}: {}.".format(env_path, str(e))
            return SimpleStatus(success=False, description=problem)
    else:
        return SimpleStatus(success=True,
                            description=("Nothing to clean up for environment '%s'." % os.path.basename(env_path)))
예제 #7
0
def remove_service(project, prepare_result, variable_name):
    """Remove a service to anaconda-project.yml.

    Returns a ``Status`` instance which evaluates to True on
    success and has an ``errors`` property (with a list of error
    strings) on failure.

    Args:
        project (Project): the project
        prepare_result (PrepareResult): result of a previous prepare
        variable_name (str): environment variable name for the service requirement

    Returns:
        ``Status`` instance
    """
    failed = project.problems_status()
    if failed is not None:
        return failed

    requirements = [
        req for req in project.find_requirements(klass=ServiceRequirement)
        if req.service_type == variable_name or req.env_var == variable_name
    ]
    if not requirements:
        return SimpleStatus(
            success=False,
            description="Service '{}' not found in the project file.".format(
                variable_name))

    if len(requirements) > 1:
        return SimpleStatus(
            success=False,
            description=(
                "Conflicting results, found {} matches, use list-services"
                " to identify which service you want to remove").format(
                    len(requirements)))

    env_var = requirements[0].env_var

    status = prepare.unprepare(project, prepare_result, whitelist=[env_var])
    if not status:
        return status

    project.project_file.unset_value(['services', env_var])
    project.project_file.use_changes_without_saving()
    assert project.problems == []

    project.project_file.save()
    return SimpleStatus(
        success=True,
        description="Removed service '{}' from the project file.".format(
            variable_name))
예제 #8
0
def clean(project, prepare_result):
    """Blow away auto-provided state for the project.

    This should not remove any potential "user data" such as
    anaconda-project-local.yml.

    This includes a call to ``anaconda_project.prepare.unprepare``
    but also removes the entire services/ and envs/ directories
    even if they contain leftovers that we didn't prepare in the
    most recent prepare() call.

    Args:
        project (Project): the project instance
        prepare_result (PrepareResult): result of a previous prepare

    Returns:
        a ``Status`` instance

    """
    status = prepare.unprepare(project, prepare_result)
    logs = status.logs
    errors = status.errors
    if status:
        logs = logs + [status.status_description]
    else:
        errors = errors + [status.status_description]

    # we also nuke any "debris" from non-current choices, like old
    # environments or services
    def cleanup_dir(dirname):
        if os.path.isdir(dirname):
            logs.append("Removing %s." % dirname)
            try:
                shutil.rmtree(dirname)
            except Exception as e:
                errors.append("Error removing %s: %s." % (dirname, str(e)))

    cleanup_dir(os.path.join(project.directory_path, "services"))
    cleanup_dir(os.path.join(project.directory_path, "envs"))

    if status and len(errors) == 0:
        return SimpleStatus(success=True,
                            description="Cleaned.",
                            logs=logs,
                            errors=errors)
    else:
        return SimpleStatus(success=False,
                            description="Failed to clean everything up.",
                            logs=logs,
                            errors=errors)
예제 #9
0
def remove_env_spec(project, name):
    """Remove the environment spec from project directory and remove from anaconda-project.yml.

    Returns a ``Status`` subtype (it won't be a
    ``RequirementStatus`` as with some other functions, just a
    plain status).

    Args:
        project (Project): the project
        name (str): environment spec name

    Returns:
        ``Status`` instance
    """
    assert name is not None

    failed = project.problems_status()
    if failed is not None:
        return failed

    if name not in project.env_specs:
        problem = "Environment spec {} doesn't exist.".format(name)
        return SimpleStatus(success=False, description=problem)

    if len(project.env_specs) == 1:
        problem = "At least one environment spec is required; '{}' is the only one left.".format(
            name)
        return SimpleStatus(success=False, description=problem)

    env_path = project.env_specs[name].path(project.directory_path)

    # For remove_service and remove_download, we use unprepare()
    # to do the cleanup; for the environment, it's awkward to do
    # that because the env we want to remove may not be the one
    # that was prepared. So instead we share some code with the
    # CondaEnvProvider but don't try to go through the unprepare
    # machinery.
    status = _remove_env_path(env_path)
    if status:
        project.project_file.unset_value(['env_specs', name])
        project.project_file.use_changes_without_saving()
        if project.problems_status() is None:
            project.project_file.save()
        else:
            # revert and return the problems
            status = project.problems_status()
            project.project_file.load()

    return status
예제 #10
0
    def check(dirname):
        _monkeypatch_pwd(monkeypatch, dirname)

        status = SimpleStatus(success=True, description='Service added.')
        status.requirement = RedisRequirement(RequirementsRegistry(), env_var='REDIS_URL', options=dict(type='redis'))

        _monkeypatch_add_service(monkeypatch, status)

        code = _parse_args_and_run_subcommand(['anaconda-project', 'add-service', 'redis'])
        assert code == 0

        out, err = capsys.readouterr()
        assert (
            'Service added.\n' + 'Added service redis to the project file, its address will be in REDIS_URL.\n') == out
        assert '' == err
예제 #11
0
def remove_download(project, prepare_result, env_var):
    """Remove file or directory referenced by ``env_var`` from file system and the project.

    The returned ``Status`` will be an instance of ``SimpleStatus``. A False
    status will have an ``errors`` property with a list of error
    strings.

    Args:
        project (Project): the project
        prepare_result (PrepareResult): result of a previous prepare
        env_var (str): env var to store the local filename

    Returns:
        ``Status`` instance
    """
    failed = project.problems_status()
    if failed is not None:
        return failed
    # Modify the project file _in memory only_, do not save
    requirement = project.find_requirements(env_var, klass=DownloadRequirement)
    if not requirement:
        return SimpleStatus(
            success=False,
            description="Download requirement: {} not found.".format(env_var))
    assert len(requirement) == 1  # duplicate env vars aren't allowed
    requirement = requirement[0]

    status = prepare.unprepare(project, prepare_result, whitelist=[env_var])
    if status:
        project.project_file.unset_value(['downloads', env_var])
        project.project_file.use_changes_without_saving()
        assert project.problems == []
        project.project_file.save()

    return status
예제 #12
0
def remove_variables(project, vars_to_remove, env_spec_name=None):
    """Remove variables from anaconda-project.yml and unset their values in local project state.

    Returns a ``Status`` instance which evaluates to True on
    success and has an ``errors`` property (with a list of error
    strings) on failure.

    Args:
        project (Project): the project
        vars_to_remove (list of str): variable names
        env_spec_name (str): name of env spec to use

    Returns:
        ``Status`` instance
    """
    (env_prefix, status) = _prepare_env_prefix(project, env_spec_name)
    if env_prefix is None:
        return status

    local_state = LocalStateFile.load_for_directory(project.directory_path)
    for varname in vars_to_remove:
        _unset_variable(project, env_prefix, varname, local_state)
        project.project_file.unset_value(['variables', varname])
        project.project_file.save()
        local_state.save()

    return SimpleStatus(success=True,
                        description="Variables removed from the project file.")
예제 #13
0
def unset_variables(project, vars_to_unset, env_spec_name=None):
    """Unset variables' values in anaconda-project-local.yml.

    Returns a ``Status`` instance which evaluates to True on
    success and has an ``errors`` property (with a list of error
    strings) on failure.

    Args:
        project (Project): the project
        vars_to_unset (list of str): variable names
        env_spec_name (str): name of env spec to use

    Returns:
        ``Status`` instance
    """
    (env_prefix, status) = _prepare_env_prefix(project, env_spec_name)
    if env_prefix is None:
        return status

    local_state = LocalStateFile.load_for_directory(project.directory_path)
    for varname in vars_to_unset:
        _unset_variable(project, env_prefix, varname, local_state)
    local_state.save()

    return SimpleStatus(success=True, description=("Variables were unset."))
예제 #14
0
    def unprovide(self,
                  requirement,
                  environ,
                  local_state_file,
                  overrides,
                  requirement_status=None):
        """Override superclass to delete project-scoped envs directory."""
        config = self.read_config(
            requirement,
            environ,
            local_state_file,
            # future: pass in this default_env_spec_name
            default_env_spec_name='default',
            overrides=overrides)

        env_path = config.get('value', None)
        assert env_path is not None
        project_dir = environ['PROJECT_DIR']
        if not env_path.startswith(project_dir):
            return SimpleStatus(
                success=True,
                description=(
                    "Current environment is not in %s, no need to delete it." %
                    project_dir))

        return _remove_env_path(env_path)
예제 #15
0
def remove_command(project, name):
    """Remove a command from anaconda-project.yml.

    Returns a ``Status`` subtype (it won't be a
    ``RequirementStatus`` as with some other functions, just a
    plain status).

    Args:
       project (Project): the project
       name (string): name of the command to be removed

    Returns:
       a ``Status`` instance
    """
    failed = project.problems_status()
    if failed is not None:
        return failed

    if name not in project.commands:
        return SimpleStatus(
            success=False,
            description="Command: '{}' not found in project file.".format(
                name))

    command = project.commands[name]

    # if we remove a notebook, it's an error normally, we have to mark it skipped
    # TODO share this code with the no_fix function in project.py
    if command.notebook is not None:
        skipped_notebooks = project.project_file.get_value(
            ['skip_imports', 'notebooks'], default=[])
        if isinstance(skipped_notebooks, list) and \
           command.notebook not in skipped_notebooks:
            skipped_notebooks.append(command.notebook)
            project.project_file.set_value(['skip_imports', 'notebooks'],
                                           skipped_notebooks)

    project.project_file.unset_value(['commands', name])
    project.project_file.use_changes_without_saving()

    assert project.problems == []
    project.project_file.save()

    return SimpleStatus(
        success=True,
        description="Command: '{}' removed from project file.".format(name))
예제 #16
0
def set_properties(project, name=None, icon=None, description=None):
    """Set simple properties on a project.

    This doesn't support properties which require prepare()
    actions to check their effects; see other calls such as
    ``add_packages()`` for those.

    This will fail if project.problems is non-empty.

    Args:
        project (``Project``): the project instance
        name (str): Name of the project or None to leave unmodified
        icon (str): Icon for the project or None to leave unmodified
        description (str): description for the project or None to leave unmodified

    Returns:
        a ``Status`` instance indicating success or failure
    """
    failed = project.problems_status()
    if failed is not None:
        return failed

    if name is not None:
        project.project_file.set_value('name', name)

    if icon is not None:
        project.project_file.set_value('icon', icon)

    if description is not None:
        project.project_file.set_value('description', description)

    project.project_file.use_changes_without_saving()

    if len(project.problems) == 0:
        # write out the anaconda-project.yml if it looks like we're safe.
        project.project_file.save()
        return SimpleStatus(success=True,
                            description="Project properties updated.")
    else:
        # revert to previous state (after extracting project.problems)
        status = SimpleStatus(success=False,
                              description="Failed to set project properties.",
                              errors=list(project.problems))
        project.project_file.load()
        return status
예제 #17
0
 def mock_unarchive(filename, project_dir, parent_dir=None, frontend=None):
     assert frontend is not None
     frontend.info("a")
     frontend.info("b")
     frontend.error("c")
     frontend.error("d")
     return SimpleStatus(success=False,
                         description="DESC",
                         errors=['c', 'd'])
예제 #18
0
    def check(dirname):
        _monkeypatch_pwd(monkeypatch, dirname)
        _monkeypatch_add_service(monkeypatch, SimpleStatus(success=False, description='Service add FAIL.'))

        code = _parse_args_and_run_subcommand(['anaconda-project', 'add-service', 'redis'])
        assert code == 1

        out, err = capsys.readouterr()
        assert '' == out
        assert 'Service add FAIL.\n' == err
예제 #19
0
 def unprovide(self,
               requirement,
               environ,
               local_state_file,
               overrides,
               requirement_status=None):
     """Override superclass to return success always."""
     return SimpleStatus(success=True,
                         description=("Nothing to clean up for %s." %
                                      requirement.env_var))
예제 #20
0
def shutdown_service_run_state(local_state_file, service_name):
    """Run any shutdown commands from the local state file for the given service.

    Also remove the shutdown commands from the file.

    Args:
        local_state_file (LocalStateFile): local state
        service_name (str): the name of the service, usually a
            variable name, should be specific enough to uniquely
            identify the provider

    Returns:
        a `Status` instance potentially containing errors
    """
    run_states = local_state_file.get_all_service_run_states()
    if service_name not in run_states:
        return SimpleStatus(success=True,
                            description=("Nothing to do to shut down %s." %
                                         service_name))

    errors = []
    state = run_states[service_name]
    if 'shutdown_commands' in state:
        commands = state['shutdown_commands']
        for command in commands:
            code = logged_subprocess.call(command)
            if code != 0:
                errors.append(
                    "Shutting down %s, command %s failed with code %d." %
                    (service_name, repr(command), code))
    # clear out the run state once we try to shut it down
    local_state_file.set_service_run_state(service_name, dict())
    local_state_file.save()

    if errors:
        return SimpleStatus(success=False,
                            description=("Shutdown commands failed for %s." %
                                         service_name),
                            errors=errors)
    else:
        return SimpleStatus(success=True,
                            description=("Successfully shut down %s." %
                                         service_name))
예제 #21
0
    def check(dirname):
        _monkeypatch_pwd(monkeypatch, dirname)
        _monkeypatch_add_env_spec(monkeypatch, SimpleStatus(success=True, description='Environment looks good.'))

        code = _parse_args_and_run_subcommand(['anaconda-project', 'add-env-spec', '--name', 'foo'])
        assert code == 0

        out, err = capsys.readouterr()
        assert ('Environment looks good.\n' + 'Added environment foo to the project file.\n') == out
        assert '' == err
예제 #22
0
def export_env_spec(project, name, filename):
    """Export the environment spec as an environment.yml-type file.

    Returns a ``Status`` subtype (it won't be a
    ``RequirementStatus`` as with some other functions, just a
    plain status).

    Args:
        project (Project): the project
        name (str): environment spec name or None for default
        filename (str): file to export to

    Returns:
        ``Status`` instance
    """
    failed = project.problems_status()
    if failed is not None:
        return failed

    if name is None:
        name = project.default_env_spec_name
    assert name is not None

    if name not in project.env_specs:
        problem = "Environment spec {} doesn't exist.".format(name)
        return SimpleStatus(success=False, description=problem)

    spec = project.env_specs[name]

    try:
        spec.save_environment_yml(filename)
    except Exception as e:
        return SimpleStatus(success=False,
                            description="Failed to save {}: {}.".format(
                                filename, str(e)))

    return SimpleStatus(
        success=True,
        description="Exported environment spec {} to {}.".format(
            name, filename))
예제 #23
0
    def check(dirname):
        _monkeypatch_pwd(monkeypatch, dirname)
        params = _monkeypatch_remove_packages(monkeypatch, SimpleStatus(success=True, description='Installed ok.'))

        code = _parse_args_and_run_subcommand(['anaconda-project', 'remove-packages', '--env-spec', 'foo', 'bar'])
        assert code == 0

        out, err = capsys.readouterr()
        assert ('Installed ok.\n' + 'Removed packages from environment foo in project file: bar.\n') == out
        assert '' == err

        assert 1 == len(params['args'])
        assert dict(env_spec_name='foo', packages=['bar']) == params['kwargs']
예제 #24
0
def _remove_env_path(env_path, project_dir):
    """Also used by project_ops.py to delete environment files."""
    if not os.path.isdir(env_path):
        return SimpleStatus(
            success=True,
            description=("Nothing to clean up for environment '%s'." %
                         os.path.basename(env_path)))
    if not env_path.startswith(project_dir + os.sep):
        return SimpleStatus(
            success=True,
            description=(
                "Current environment is not in %s, no need to delete it." %
                project_dir))
    try:
        shutil.rmtree(env_path)
        return SimpleStatus(success=True,
                            description=("Deleted environment files in %s." %
                                         env_path))
    except Exception as e:
        problem = "Failed to remove environment files in {}: {}.".format(
            env_path, str(e))
        return SimpleStatus(success=False, description=problem)
예제 #25
0
    def check(dirname):
        _monkeypatch_pwd(monkeypatch, dirname)
        _monkeypatch_add_env_spec(monkeypatch,
                                  SimpleStatus(success=False,
                                               description='Environment variable MYDATA is not set.',
                                               logs=['This is a log message.'],
                                               errors=['This is an error message.']))

        code = _parse_args_and_run_subcommand(['anaconda-project', 'add-env-spec', '--name', 'foo'])
        assert code == 1

        out, err = capsys.readouterr()
        assert '' == out
        assert 'This is a log message.\nThis is an error message.\nEnvironment variable MYDATA is not set.\n' == err
    def check(dirname):
        _monkeypatch_pwd(monkeypatch, dirname)
        params = _monkeypatch_update(
            monkeypatch, SimpleStatus(success=True, description='Updated.'))

        code = _parse_args_and_run_subcommand(['anaconda-project', 'update'])
        assert code == 0

        out, err = capsys.readouterr()
        assert ('Updated.\n') == out
        assert '' == err

        assert 1 == len(params['args'])
        assert dict(env_spec_name=None) == params['kwargs']
예제 #27
0
    def check(dirname):
        _monkeypatch_pwd(monkeypatch, dirname)
        params = _monkeypatch_add_packages(monkeypatch, SimpleStatus(success=True, description='Installed ok.'))

        code = _parse_args_and_run_subcommand(['anaconda-project', 'add-packages', '--env-spec', 'foo', '--channel',
                                               'c1', '--channel=c2', 'a', 'b'])
        assert code == 0

        out, err = capsys.readouterr()
        assert ('Installed ok.\n' + 'Added packages to environment foo in project file: a, b.\n') == out
        assert '' == err

        assert 1 == len(params['args'])
        assert dict(env_spec_name='foo', packages=['a', 'b'], channels=['c1', 'c2']) == params['kwargs']
예제 #28
0
    def check(dirname):
        _monkeypatch_pwd(monkeypatch, dirname)
        params = _monkeypatch_add_env_spec(monkeypatch,
                                           SimpleStatus(success=True,
                                                        description='Environment looks good.'))

        code = _parse_args_and_run_subcommand(['anaconda-project', 'add-env-spec', '--name', 'foo', '--channel', 'c1',
                                               '--channel=c2', 'a', 'b'])
        assert code == 0

        out, err = capsys.readouterr()
        assert ('Environment looks good.\n' + 'Added environment foo to the project file.\n') == out
        assert '' == err

        assert 1 == len(params['args'])
        assert dict(name='foo', packages=['a', 'b'], channels=['c1', 'c2']) == params['kwargs']
예제 #29
0
def add_variables(project, vars_to_add, defaults=None):
    """Add variables in anaconda-project.yml, optionally setting their defaults.

    Returns a ``Status`` instance which evaluates to True on
    success and has an ``errors`` property (with a list of error
    strings) on failure.

    Args:
        project (Project): the project
        vars_to_add (list of str): variable names
        defaults (dict): dictionary from keys to defaults, can be empty

    Returns:
        ``Status`` instance
    """
    failed = project.problems_status()
    if failed is not None:
        return failed

    if defaults is None:
        defaults = dict()

    present_vars = {
        req.env_var
        for req in project.requirements if isinstance(req, EnvVarRequirement)
    }
    for varname in vars_to_add:
        if varname in defaults:
            # we need to update the default even if var already exists
            new_default = defaults.get(varname)
            variable_value = project.project_file.get_value(
                ['variables', varname])
            if variable_value is None or not isinstance(variable_value, dict):
                variable_value = new_default
            else:
                variable_value['default'] = new_default
            project.project_file.set_value(['variables', varname],
                                           variable_value)
        elif varname not in present_vars:
            # we are only adding the var if nonexistent and should leave
            # the default alone if it's already set
            project.project_file.set_value(['variables', varname], None)
    project.project_file.save()

    return SimpleStatus(success=True,
                        description="Variables added to the project file.")
    def check(dirname):
        _monkeypatch_pwd(monkeypatch, dirname)
        params = _monkeypatch_add_platforms(
            monkeypatch, SimpleStatus(success=True,
                                      description='Installed ok.'))

        code = _parse_args_and_run_subcommand(
            ['anaconda-project', 'add-platforms', 'a', 'b'])
        assert code == 0

        out, err = capsys.readouterr()
        assert ('Installed ok.\n' +
                'Added platforms to project file: a, b.\n') == out
        assert '' == err

        assert 1 == len(params['args'])
        assert dict(env_spec_name=None, platforms=['a',
                                                   'b']) == params['kwargs']