Repository: pypa/hatch

Issue:
`run` positive filtering for multiple variables can't match
For this exact example: https://hatch.pypa.io/1.5/cli/reference/#hatch-run

```toml
[[tool.hatch.envs.test.matrix]]
python = ["39", "310"]
version = ["42", "3.14", "9000"]
```

Both of these:
```bash
hatch run +py=310 test:pytest
hatch run +py=310 -version=9000 test:pytest
```
Produce `No environments were selected`.

I need to instead run
```bash
hatch run +py=310 +version= test:pytest
```
to get anything to match.

So I am claiming that either there's a bug or the example is wrong.

If it is a bug, I am suspecting this:
https://github.com/pypa/hatch/blob/fc8b37bb3dccc869b26500c8d6462fd9e617f30a/src/hatch/cli/env/run.py#L23-L24

If the matrix has a variable that is not one of the specified `+` values, the environment can never match.

Changing `break` to `pass` made it work.

Discussion:

Patch:
diff --git a/docs/history.md b/docs/history.md
index 59961e3a..29b266a6 100644
--- a/docs/history.md
+++ b/docs/history.md
@@ -32,6 +32,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 - Environments now respect dynamically defined project dependencies
 - The `dep hash` and all `dep show` commands now respect dynamically defined project dependencies
 - The `env show`, `dep hash`, and all `dep show` commands now honor context formatting
+- Fix matrix variable inclusion filtering of the `run` and `env run` commands when there are multiple possible variables
 - Build environment compatibility is now checked before use
 - Decreasing verbosity now has no affect on output that should always be displayed
 - Handle more edge cases in the `setuptools` migration script
diff --git a/src/hatch/cli/env/run.py b/src/hatch/cli/env/run.py
index 00080356..c0324e4b 100644
--- a/src/hatch/cli/env/run.py
+++ b/src/hatch/cli/env/run.py
@@ -13,20 +13,25 @@ def parse_variable_spec(spec):
 def select_matrix_environments(environments, included_variables, excluded_variables):
     selected_environments = []
     for env_name, variables in environments.items():
+        included = set(variables)
+        excluded = set()
+
         for variable, value in variables.items():
             if variable in excluded_variables:
                 excluded_values = excluded_variables[variable]
                 if not excluded_values or value in excluded_values:
+                    excluded.add(variable)
                     break
 
             if included_variables:
                 if variable not in included_variables:
-                    break
+                    included.remove(variable)
                 else:
                     included_values = included_variables[variable]
                     if included_values and value not in included_values:
-                        break
-        else:
+                        included.remove(variable)
+
+        if included and not excluded:
             selected_environments.append(env_name)
 
     return selected_environments


Test patch:
diff --git a/tests/cli/run/test_run.py b/tests/cli/run/test_run.py
index 1ac2fa97..9f664c9d 100644
--- a/tests/cli/run/test_run.py
+++ b/tests/cli/run/test_run.py
@@ -1562,6 +1562,69 @@ def test_matrix_variable_selection_include_none(hatch, helpers, temp_dir, config
     )
 
 
+def test_matrix_variable_selection_inclusion_multiple_variables(hatch, helpers, temp_dir, config_file):
+    config_file.model.template.plugins['default']['tests'] = False
+    config_file.save()
+
+    project_name = 'My.App'
+
+    with temp_dir.as_cwd():
+        result = hatch('new', project_name)
+
+    assert result.exit_code == 0, result.output
+
+    project_path = temp_dir / 'my-app'
+    data_path = temp_dir / 'data'
+    data_path.mkdir()
+
+    project = Project(project_path)
+    helpers.update_project_environment(project, 'default', {'skip-install': True, **project.config.envs['default']})
+    helpers.update_project_environment(
+        project, 'test', {'matrix': [{'version1': ['9000', '42'], 'version2': ['3.14']}]}
+    )
+
+    with project_path.as_cwd(env_vars={ConfigEnvVars.DATA: str(data_path)}):
+        result = hatch(
+            'run',
+            '+version1=9000',
+            'test:python',
+            '-c',
+            "import os,sys;open('test.txt', 'a').write(sys.executable+os.linesep[-1])",
+        )
+
+    assert result.exit_code == 0, result.output
+    assert result.output == helpers.dedent(
+        """
+        ──────────────────────────────── test.9000-3.14 ────────────────────────────────
+        Creating environment: test.9000-3.14
+        Checking dependencies
+        """
+    )
+    output_file = project_path / 'test.txt'
+    assert output_file.is_file()
+
+    env_data_path = data_path / 'env' / 'virtual'
+    assert env_data_path.is_dir()
+
+    project_data_path = env_data_path / project_path.name
+    assert project_data_path.is_dir()
+
+    storage_dirs = list(project_data_path.iterdir())
+    assert len(storage_dirs) == 1
+
+    storage_path = storage_dirs[0]
+    assert len(storage_path.name) == 8
+
+    env_dirs = list(storage_path.iterdir())
+    assert len(env_dirs) == 1
+
+    env_path = env_dirs[0]
+    assert env_path.name == 'test.9000-3.14'
+
+    python_path = str(output_file.read_text()).strip()
+    assert str(env_path) in python_path
+
+
 def test_context_formatting_recursion(hatch, helpers, temp_dir, config_file):
     config_file.model.template.plugins['default']['tests'] = False
     config_file.save()