Repository: pantsbuild/pants

Issue:
Add a config option under [pytest]
**Is your feature request related to a problem? Please describe.**
Actually, we can't specify a path to a conf file for the pytest tool.

**Describe the solution you'd like**
Add a `config` option under `[pytest]` (`pants.toml`).

**Describe alternatives you've considered**
Put `pytest.ini` in the root directory of my project. But for consistency, I would prefer to have all my conf files in one place.

**Additional context**
Pytest does support providing a conf file using `-c`.



Discussion:
Great! Adding this is pretty trivial.

In `pytest.py`, you'll add an option like this:

https://github.com/pantsbuild/pants/blob/29750e6ea4cc23ea59ff4a027d587f19f2a63304/src/python/pants/backend/python/lint/black/subsystem.py#L48-L60

Also update the help message for `config_discovery`, and update the method `config_request`, like this:

https://github.com/pantsbuild/pants/blob/29750e6ea4cc23ea59ff4a027d587f19f2a63304/src/python/pants/backend/python/lint/black/subsystem.py#L61-L83

You'll need to update the `argv` in `pytest_runner.py`, similar to this:

https://github.com/pantsbuild/pants/blob/29750e6ea4cc23ea59ff4a027d587f19f2a63304/src/python/pants/backend/python/lint/black/rules.py#L96

And then, update the test for config files to be something like this:

https://github.com/pantsbuild/pants/blob/29750e6ea4cc23ea59ff4a027d587f19f2a63304/src/python/pants/backend/python/lint/black/rules_integration_test.py#L141-L157
Hey @erjac77, any interest in implementing the above? 
Yes, I'm on it!

Resolving patch:
diff --git a/src/python/pants/backend/python/goals/pytest_runner.py b/src/python/pants/backend/python/goals/pytest_runner.py
--- a/src/python/pants/backend/python/goals/pytest_runner.py
+++ b/src/python/pants/backend/python/goals/pytest_runner.py
@@ -330,6 +330,7 @@ async def setup_pytest_for_target(
         VenvPexProcess(
             pytest_runner_pex,
             argv=(
+                *(("-c", pytest.config) if pytest.config else ()),
                 *request.prepend_argv,
                 *pytest.args,
                 *coverage_args,
diff --git a/src/python/pants/backend/python/subsystems/pytest.py b/src/python/pants/backend/python/subsystems/pytest.py
--- a/src/python/pants/backend/python/subsystems/pytest.py
+++ b/src/python/pants/backend/python/subsystems/pytest.py
@@ -37,7 +37,7 @@
     TransitiveTargetsRequest,
 )
 from pants.engine.unions import UnionRule
-from pants.option.option_types import ArgsListOption, BoolOption, IntOption, StrOption
+from pants.option.option_types import ArgsListOption, BoolOption, FileOption, IntOption, StrOption
 from pants.util.docutil import bin_name, doc_url, git_url
 from pants.util.logging import LogLevel
 from pants.util.memo import memoized_method
@@ -134,15 +134,30 @@ class PyTest(PythonToolBase):
             """
         ),
     )
+    config = FileOption(
+        default=None,
+        advanced=True,
+        help=lambda cls: softwrap(
+            f"""
+            Path to a config file understood by Pytest
+            (https://docs.pytest.org/en/latest/reference/customize.html#configuration-file-formats).
+            Setting this option will disable `[{cls.options_scope}].config_discovery`. Use
+            this option if the config is located in a non-standard location.
+            """
+        ),
+    )
     config_discovery = BoolOption(
         default=True,
         advanced=True,
-        help=softwrap(
-            """
+        help=lambda cls: softwrap(
+            f"""
             If true, Pants will include all relevant Pytest config files (e.g. `pytest.ini`)
             during runs. See
             https://docs.pytest.org/en/stable/customize.html#finding-the-rootdir for where
             config files should be located for Pytest to discover them.
+
+            Use `[{cls.options_scope}].config` instead if your config is in a
+            non-standard location.
             """
         ),
     )
@@ -165,6 +180,8 @@ def config_request(self, dirs: Iterable[str]) -> ConfigFilesRequest:
             check_content[os.path.join(d, "setup.cfg")] = b"[tool:pytest]"
 
         return ConfigFilesRequest(
+            specified=self.config,
+            specified_option_name=f"[{self.options_scope}].config",
             discovery=self.config_discovery,
             check_existence=check_existence,
             check_content=check_content,


Test patch:
diff --git a/src/python/pants/backend/python/goals/pytest_runner_integration_test.py b/src/python/pants/backend/python/goals/pytest_runner_integration_test.py
--- a/src/python/pants/backend/python/goals/pytest_runner_integration_test.py
+++ b/src/python/pants/backend/python/goals/pytest_runner_integration_test.py
@@ -270,10 +270,14 @@ def test_ignore_me():
     assert "collected 2 items / 1 deselected / 1 selected" in result.stdout
 
 
-def test_config_file(rule_runner: RuleRunner) -> None:
+@pytest.mark.parametrize(
+    "config_path,extra_args",
+    (["pytest.ini", []], ["custom_config.ini", ["--pytest-config=custom_config.ini"]]),
+)
+def test_config_file(rule_runner: RuleRunner, config_path: str, extra_args: list[str]) -> None:
     rule_runner.write_files(
         {
-            "pytest.ini": dedent(
+            config_path: dedent(
                 """\
                 [pytest]
                 addopts = -s
@@ -289,7 +293,7 @@ def test():
         }
     )
     tgt = rule_runner.get_target(Address(PACKAGE, relative_file_path="tests.py"))
-    result = run_pytest(rule_runner, tgt)
+    result = run_pytest(rule_runner, tgt, extra_args=extra_args)
     assert result.exit_code == 0
     assert "All good!" in result.stdout and "Captured" not in result.stdout