environment: improve spack remove matching (#39390)
search for equivalent specs, not for equal strings when selecting a spec to remove.
This commit is contained in:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							ecb7ad493f
						
					
				
				
					commit
					86216cc36e
				
			@@ -2664,6 +2664,26 @@ def __init__(self, manifest_dir: Union[pathlib.Path, str]) -> None:
 | 
			
		||||
        self.yaml_content = with_defaults_added
 | 
			
		||||
        self.changed = False
 | 
			
		||||
 | 
			
		||||
    def _all_matches(self, user_spec: str) -> List[str]:
 | 
			
		||||
        """Maps the input string to the first equivalent user spec in the manifest,
 | 
			
		||||
        and returns it.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            user_spec: user spec to be found
 | 
			
		||||
 | 
			
		||||
        Raises:
 | 
			
		||||
            ValueError: if no equivalent match is found
 | 
			
		||||
        """
 | 
			
		||||
        result = []
 | 
			
		||||
        for yaml_spec_str in self.pristine_configuration["specs"]:
 | 
			
		||||
            if Spec(yaml_spec_str) == Spec(user_spec):
 | 
			
		||||
                result.append(yaml_spec_str)
 | 
			
		||||
 | 
			
		||||
        if not result:
 | 
			
		||||
            raise ValueError(f"cannot find a spec equivalent to {user_spec}")
 | 
			
		||||
 | 
			
		||||
        return result
 | 
			
		||||
 | 
			
		||||
    def add_user_spec(self, user_spec: str) -> None:
 | 
			
		||||
        """Appends the user spec passed as input to the list of root specs.
 | 
			
		||||
 | 
			
		||||
@@ -2684,8 +2704,9 @@ def remove_user_spec(self, user_spec: str) -> None:
 | 
			
		||||
            SpackEnvironmentError: when the user spec is not in the list
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            self.pristine_configuration["specs"].remove(user_spec)
 | 
			
		||||
            self.configuration["specs"].remove(user_spec)
 | 
			
		||||
            for key in self._all_matches(user_spec):
 | 
			
		||||
                self.pristine_configuration["specs"].remove(key)
 | 
			
		||||
                self.configuration["specs"].remove(key)
 | 
			
		||||
        except ValueError as e:
 | 
			
		||||
            msg = f"cannot remove {user_spec} from {self}, no such spec exists"
 | 
			
		||||
            raise SpackEnvironmentError(msg) from e
 | 
			
		||||
 
 | 
			
		||||
@@ -97,8 +97,10 @@ def remove(self, spec):
 | 
			
		||||
            msg += "Either %s is not in %s or %s is " % (spec, self.name, spec)
 | 
			
		||||
            msg += "expanded from a matrix and cannot be removed directly."
 | 
			
		||||
            raise SpecListError(msg)
 | 
			
		||||
        assert len(remove) == 1
 | 
			
		||||
        self.yaml_list.remove(remove[0])
 | 
			
		||||
 | 
			
		||||
        # Remove may contain more than one string representation of the same spec
 | 
			
		||||
        for item in remove:
 | 
			
		||||
            self.yaml_list.remove(item)
 | 
			
		||||
 | 
			
		||||
        # invalidate cache variables when we change the list
 | 
			
		||||
        self._expanded_list = None
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,11 @@
 | 
			
		||||
 | 
			
		||||
import spack.environment as ev
 | 
			
		||||
import spack.spec
 | 
			
		||||
from spack.environment.environment import SpackEnvironmentViewError, _error_on_nonempty_view_dir
 | 
			
		||||
from spack.environment.environment import (
 | 
			
		||||
    EnvironmentManifestFile,
 | 
			
		||||
    SpackEnvironmentViewError,
 | 
			
		||||
    _error_on_nonempty_view_dir,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
pytestmark = pytest.mark.not_on_windows("Envs are not supported on windows")
 | 
			
		||||
 | 
			
		||||
@@ -623,3 +627,66 @@ def test_requires_on_virtual_and_potential_providers(
 | 
			
		||||
        assert mpileaks.satisfies("^mpich2")
 | 
			
		||||
        assert mpileaks["mpi"].satisfies("mpich2")
 | 
			
		||||
        assert not mpileaks.satisfies(f"^{possible_mpi_spec}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.regression("39387")
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
    "spec_str", ["mpileaks +opt", "mpileaks  +opt   ~shared", "mpileaks  ~shared   +opt"]
 | 
			
		||||
)
 | 
			
		||||
def test_manifest_file_removal_works_if_spec_is_not_normalized(tmp_path, spec_str):
 | 
			
		||||
    """Tests that we can remove a spec from a manifest file even if its string
 | 
			
		||||
    representation is not normalized.
 | 
			
		||||
    """
 | 
			
		||||
    manifest = tmp_path / "spack.yaml"
 | 
			
		||||
    manifest.write_text(
 | 
			
		||||
        f"""\
 | 
			
		||||
spack:
 | 
			
		||||
  specs:
 | 
			
		||||
  - {spec_str}
 | 
			
		||||
"""
 | 
			
		||||
    )
 | 
			
		||||
    s = spack.spec.Spec(spec_str)
 | 
			
		||||
    spack_yaml = EnvironmentManifestFile(tmp_path)
 | 
			
		||||
    # Doing a round trip str -> Spec -> str normalizes the representation
 | 
			
		||||
    spack_yaml.remove_user_spec(str(s))
 | 
			
		||||
    spack_yaml.flush()
 | 
			
		||||
 | 
			
		||||
    assert spec_str not in manifest.read_text()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.regression("39387")
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
    "duplicate_specs,expected_number",
 | 
			
		||||
    [
 | 
			
		||||
        # Swap variants, versions, etc. add spaces
 | 
			
		||||
        (["foo +bar ~baz", "foo ~baz    +bar"], 3),
 | 
			
		||||
        (["foo @1.0 ~baz %gcc", "foo ~baz @1.0%gcc"], 3),
 | 
			
		||||
        # Item 1 and 3 are exactly the same
 | 
			
		||||
        (["zlib +shared", "zlib      +shared", "zlib +shared"], 4),
 | 
			
		||||
    ],
 | 
			
		||||
)
 | 
			
		||||
def test_removing_spec_from_manifest_with_exact_duplicates(
 | 
			
		||||
    duplicate_specs, expected_number, tmp_path
 | 
			
		||||
):
 | 
			
		||||
    """Tests that we can remove exact duplicates from a manifest file.
 | 
			
		||||
 | 
			
		||||
    Note that we can't get in a state with duplicates using only CLI, but this might happen
 | 
			
		||||
    on user edited spack.yaml files.
 | 
			
		||||
    """
 | 
			
		||||
    manifest = tmp_path / "spack.yaml"
 | 
			
		||||
    manifest.write_text(
 | 
			
		||||
        f"""\
 | 
			
		||||
    spack:
 | 
			
		||||
      specs: [{", ".join(duplicate_specs)} , "zlib"]
 | 
			
		||||
    """
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    with ev.Environment(tmp_path) as env:
 | 
			
		||||
        assert len(env.user_specs) == expected_number
 | 
			
		||||
        env.remove(duplicate_specs[0])
 | 
			
		||||
        env.write()
 | 
			
		||||
 | 
			
		||||
    assert "+shared" not in manifest.read_text()
 | 
			
		||||
    assert "zlib" in manifest.read_text()
 | 
			
		||||
    with ev.Environment(tmp_path) as env:
 | 
			
		||||
        assert len(env.user_specs) == 1
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user