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)])
def set_variables(project, vars_and_values, env_spec_name=None): """Set variables' values in kapsel-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: 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)
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)))
def remove_service(project, prepare_result, variable_name): """Remove a service to kapsel.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))
def clean(project, prepare_result): """Blow away auto-provided state for the project. This should not remove any potential "user data" such as kapsel-local.yml. This includes a call to ``conda_kapsel.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)
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))))
def check(dirname): _monkeypatch_pwd(monkeypatch, dirname) status = SimpleStatus(success=True, description='Service added.') status.requirement = RedisRequirement(PluginRegistry(), env_var='REDIS_URL', options=dict(type='redis')) _monkeypatch_add_service(monkeypatch, status) code = _parse_args_and_run_subcommand(['conda-kapsel', '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
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
def remove_variables(project, vars_to_remove, env_spec_name=None): """Remove variables from kapsel.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.")
def unset_variables(project, vars_to_unset, env_spec_name=None): """Unset variables' values in kapsel-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."))
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 kapsel.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
def remove_env_spec(project, name): """Remove the environment spec from project directory and remove from kapsel.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() assert project.problems == [] project.project_file.save() return status
def add_command(project, name, command_type, command, env_spec_name=None): """Add a command to kapsel.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): name of the command command_type (str): choice of `bokeh_app`, `notebook`, `unix` or `windows` command command (str): the command line or filename itself env_spec_name (str): env spec to use with this command Returns: a ``Status`` instance """ if command_type not in ALL_COMMAND_TYPES: raise ValueError("Invalid command type " + command_type + " choose from " + repr(ALL_COMMAND_TYPES)) name = name.strip() failed = project.problems_status() if failed is not None: return failed command_dict = project.project_file.get_value(['commands', name]) if command_dict is None: command_dict = dict() project.project_file.set_value(['commands', name], command_dict) command_dict[command_type] = command if env_spec_name is None: if 'env_spec' not in command_dict: # make it explicit for clarity command_dict['env_spec'] = project.default_env_spec_name # if env_spec is set, leave it alone; this way people can # modify commands via command line without specifying the # env_spec every time. else: command_dict['env_spec'] = env_spec_name project.project_file.use_changes_without_saving() failed = project.problems_status(description="Unable to add the command.") if failed is not None: # reset, maybe someone added conflicting command line types or something project.project_file.load() return failed else: project.project_file.save() return SimpleStatus(success=True, description="Command added to project file.")
def check(dirname): _monkeypatch_pwd(monkeypatch, dirname) status = SimpleStatus(success=True, description='Service added.') status.requirement = RedisRequirement(PluginRegistry(), env_var='REDIS_URL', options=dict(type='redis')) _monkeypatch_add_service(monkeypatch, status) code = _parse_args_and_run_subcommand( ['conda-kapsel', '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
def check(dirname): _monkeypatch_pwd(monkeypatch, dirname) _monkeypatch_add_service( monkeypatch, SimpleStatus(success=False, description='Service add FAIL.')) code = _parse_args_and_run_subcommand( ['conda-kapsel', 'add-service', 'redis']) assert code == 1 out, err = capsys.readouterr() assert '' == out assert 'Service add FAIL.\n' == err
def remove_command(project, name): """Remove a command from kapsel.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 command.auto_generated: return SimpleStatus( success=False, description="Cannot remove auto-generated command: '{}'.".format( name)) 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))
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( ['conda-kapsel', '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
def problems_status(self, description=None): """Get a ``Status`` describing project problems, or ``None`` if no problems.""" if len(self.problems) > 0: errors = [] for problem in self.problems: errors.append(problem) if description is None: description = "Unable to load the project." return SimpleStatus(success=False, description=description, logs=[], errors=errors) else: return None
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 = 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))
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( ['conda-kapsel', '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 add_variables(project, vars_to_add, defaults=None): """Add variables in kapsel.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 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)
def check(dirname): _monkeypatch_pwd(monkeypatch, dirname) params = _monkeypatch_remove_packages( monkeypatch, SimpleStatus(success=True, description='Installed ok.')) code = _parse_args_and_run_subcommand( ['conda-kapsel', 'remove-packages', 'bar']) assert code == 0 out, err = capsys.readouterr() assert ('Installed ok.\n' + 'Removed packages from project file: bar.\n') == out assert '' == err assert 1 == len(params['args']) assert dict(env_spec_name=None, packages=['bar']) == params['kwargs']
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([ 'conda-kapsel', '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']
def check(dirname): _monkeypatch_pwd(monkeypatch, dirname) params = _monkeypatch_add_packages( monkeypatch, SimpleStatus(success=True, description='Installed ok.')) code = _parse_args_and_run_subcommand([ 'conda-kapsel', 'add-packages', '--channel', 'c1', '--channel=c2', 'a', 'b' ]) assert code == 0 out, err = capsys.readouterr() assert ('Installed ok.\n' + 'Added packages to project file: a, b.\n') == out assert '' == err assert 1 == len(params['args']) assert dict(env_spec_name=None, packages=['a', 'b'], channels=['c1', 'c2']) == params['kwargs']
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))
def add_service(project, service_type, variable_name=None): """Add a service to kapsel.yml. The returned ``Status`` should be a ``RequirementStatus`` for the service requirement if it evaluates to True (on success), but may be another subtype of ``Status`` on failure. A False status will have an ``errors`` property with a list of error strings. Args: project (Project): the project service_type (str): which kind of service variable_name (str): environment variable name (None for default) Returns: ``Status`` instance """ failed = project.problems_status() if failed is not None: return failed known_types = project.plugin_registry.list_service_types() found = None for known in known_types: if known.name == service_type: found = known break if found is None: return SimpleStatus( success=False, description="Unable to add service.", logs=[], errors=[ "Unknown service type '%s', we know about: %s" % (service_type, ", ".join(map(lambda s: s.name, known_types))) ]) if variable_name is None: variable_name = found.default_variable assert len(known_types ) == 1 # when this fails, see change needed in the loop below requirement_already_exists = False existing_requirements = project.find_requirements(env_var=variable_name) if len(existing_requirements) > 0: requirement = existing_requirements[0] if isinstance(requirement, ServiceRequirement): assert requirement.service_type == service_type # when the above assertion fails, add the second known type besides # redis in test_project_ops.py::test_add_service_already_exists_with_different_type # and then uncomment the below code. # if requirement.service_type != service_type: # return SimpleStatus(success=False, description="Unable to add service.", logs=[], # errors=["Service %s already exists but with type '%s'" % # (variable_name, requirement.service_type)]) # else: requirement_already_exists = True else: return SimpleStatus( success=False, description="Unable to add service.", logs=[], errors=["Variable %s is already in use." % variable_name]) if not requirement_already_exists: project.project_file.set_value(['services', variable_name], service_type) return _commit_requirement_if_it_works(project, variable_name)
def mock_upload(*args, **kwargs): params['args'] = args params['kwargs'] = kwargs return SimpleStatus(success=True, description="Yay", logs=['Hello'])
def update_command(project, name, command_type=None, command=None, new_name=None): """Update attributes of a command in kapsel.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): name of the command command_type (str or None): choice of `bokeh_app`, `notebook`, `unix` or `windows` command command (str or None): the command line or filename itself; command_type must also be specified Returns: a ``Status`` instance """ # right now update_command can be called "pointlessly" (with # no new command), this is because in theory it might let you # update other properties too, when/if commands have more # properties. if command_type is None and new_name is None: return SimpleStatus(success=True, description=("Nothing to change about command %s" % name)) if command_type not in (list(ALL_COMMAND_TYPES) + [None]): raise ValueError("Invalid command type " + command_type + " choose from " + repr(ALL_COMMAND_TYPES)) if command is None and command_type is not None: raise ValueError( "If specifying the command_type, must also specify the command") failed = project.problems_status() if failed is not None: return failed if name not in project.commands: return SimpleStatus(success=False, description="Failed to update command.", errors=[("No command '%s' found." % name)]) command_object = project.commands[name] if command_object.auto_generated: return SimpleStatus( success=False, description="Failed to update command.", errors=[("Autogenerated command '%s' can't be modified." % name)]) command_dict = project.project_file.get_value(['commands', name]) assert command_dict is not None if new_name: project.project_file.unset_value(['commands', name]) project.project_file.set_value(['commands', new_name], command_dict) existing_types = set(command_dict.keys()) conflicting_types = existing_types - set([command_type]) # 'unix' and 'windows' don't conflict with one another if command_type == 'unix': conflicting_types = conflicting_types - set(['windows']) elif command_type == 'windows': conflicting_types = conflicting_types - set(['unix']) if command_type is not None: for conflicting in conflicting_types: del command_dict[conflicting] command_dict[command_type] = command project.project_file.use_changes_without_saving() failed = project.problems_status(description="Unable to add the command.") if failed is not None: # reset, maybe someone added a nonexistent bokeh app or something project.project_file.load() return failed else: project.project_file.save() return SimpleStatus(success=True, description="Command updated in project file.")
def _update_env_spec(project, name, packages, channels, create): failed = project.problems_status() if failed is not None: return failed if packages is None: packages = [] if channels is None: channels = [] if not create and (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) if name is None: env_dict = project.project_file.root else: env_dict = project.project_file.get_value(['env_specs', name]) if env_dict is None: env_dict = dict() project.project_file.set_value(['env_specs', name], env_dict) # packages may be a "CommentedSeq" and we don't want to lose the comments, # so don't convert this thing to a regular list. old_packages = env_dict.get('packages', []) old_packages_set = set(parse_spec(dep).name for dep in old_packages) bad_specs = [] updated_specs = [] new_specs = [] for dep in packages: if dep in old_packages: # no-op adding the EXACT same thing (don't move it around) continue parsed = parse_spec(dep) if parsed is None: bad_specs.append(dep) else: if parsed.name in old_packages_set: updated_specs.append((parsed.name, dep)) else: new_specs.append(dep) if len(bad_specs) > 0: bad_specs_string = ", ".join(bad_specs) return SimpleStatus(success=False, description="Could not add packages.", errors=[("Bad package specifications: %s." % bad_specs_string)]) # remove everything that we are changing the spec for def replace_spec(old): name = parse_spec(old).name for (replaced_name, new_spec) in updated_specs: if replaced_name == name: return new_spec return old _map_inplace(replace_spec, old_packages) # add all the new ones for added in new_specs: old_packages.append(added) env_dict['packages'] = old_packages # channels may be a "CommentedSeq" and we don't want to lose the comments, # so don't convert this thing to a regular list. new_channels = env_dict.get('channels', []) old_channels_set = set(new_channels) for channel in channels: if channel not in old_channels_set: new_channels.append(channel) env_dict['channels'] = new_channels status = _commit_requirement_if_it_works(project, CondaEnvRequirement, env_spec_name=name) return status
def remove_packages(project, env_spec_name, packages): """Attempt to remove packages from an environment in kapsel.yml. If the env_spec_name is None rather than an env name, packages are removed from the global packages section (from all environments). The returned ``Status`` should be a ``RequirementStatus`` for the environment requirement if it evaluates to True (on success), but may be another subtype of ``Status`` on failure. A False status will have an ``errors`` property with a list of error strings. Args: project (Project): the project env_spec_name (str): environment spec name or None for all environment specs packages (list of str): packages to remove Returns: ``Status`` instance """ # This is sort of one big ugly. What we SHOULD be able to do # is simply remove the package from kapsel.yml then re-run # prepare, and if the packages aren't pulled in as deps of # something else, they get removed. This would work if our # approach was to always force the env to exactly the env # we'd have created from scratch, given our env config. # But that isn't our approach right now. # # So what we do right now is remove the package from the env, # and then remove it from kapsel.yml, and then see if we can # still prepare the project. failed = project.problems_status() if failed is not None: return failed assert packages is not None assert len(packages) > 0 if env_spec_name is None: envs = project.env_specs.values() unaffected_envs = [] else: env = project.env_specs.get(env_spec_name, None) if env is None: problem = "Environment spec {} doesn't exist.".format( env_spec_name) return SimpleStatus(success=False, description=problem) else: envs = [env] unaffected_envs = list(project.env_specs.values()) unaffected_envs.remove(env) assert len(unaffected_envs) == (len(project.env_specs) - 1) assert len(envs) > 0 conda = conda_manager.new_conda_manager() for env in envs: prefix = env.path(project.directory_path) try: if os.path.isdir(prefix): conda.remove_packages(prefix, packages) except conda_manager.CondaManagerError: pass # ignore errors; not all the envs will exist or have the package installed perhaps def envs_to_their_dicts(envs): env_dicts = [] for env in envs: env_dict = project.project_file.get_value(['env_specs', env.name]) if env_dict is not None: # it can be None for the default environment (which doesn't have to be listed) env_dicts.append(env_dict) return env_dicts env_dicts = envs_to_their_dicts(envs) env_dicts.append(project.project_file.root) unaffected_env_dicts = envs_to_their_dicts(unaffected_envs) assert len(env_dicts) > 0 previous_global_deps = set(project.project_file.root.get('packages', [])) for env_dict in env_dicts: # packages may be a "CommentedSeq" and we don't want to lose the comments, # so don't convert this thing to a regular list. old_packages = env_dict.get('packages', []) removed_set = set(packages) _filter_inplace(lambda dep: dep not in removed_set, old_packages) env_dict['packages'] = old_packages # if we removed any deps from global, add them to the # individual envs that were not supposed to be affected. new_global_deps = set(project.project_file.root.get('packages', [])) removed_from_global = (previous_global_deps - new_global_deps) for env_dict in unaffected_env_dicts: # old_packages may be a "CommentedSeq" and we don't want to lose the comments, # so don't convert this thing to a regular list. old_packages = env_dict.get('packages', []) old_packages.extend(list(removed_from_global)) env_dict['packages'] = old_packages status = _commit_requirement_if_it_works(project, CondaEnvRequirement, env_spec_name=env_spec_name) return status