Repository: dbt-labs/dbt-core

Issue:
[Bug] `cli.make_context()` is not idempotent when passing the same `args`
### Is this a new bug in dbt-core?

- [X] I believe this is a new bug in dbt-core
- [X] I have searched the existing issues, and I could not find an existing issue for this bug

### Current Behavior

Calling `cli.make_context()` twice with the same `args` leads to a failing test upon the 2nd invocation.

### Expected Behavior

I'd expect `cli.make_context()` to be [idempotent](https://sketchplanations.com/idempotence) like _looking_ at a slice of cake rather than _eating_ a slice of cake. e.g., I should be able to call it with the same `args` object repeatedly and get the same result.

### Steps To Reproduce

Add the following test to [`tests/unit/test_cli_flags.py`](https://github.com/dbt-labs/dbt-core/blob/952cca8c58e5eda247196f7bf9d6794e72f1005b/tests/unit/test_cli_flags.py):

```python
    @pytest.mark.parametrize(
        "flag_value",
        [
            (None),
            ("my_target"),
        ],
    )
    def test_target_settings(self, flag_value):
        cli_args = ["--target", flag_value]
        subcommand = ["run"]

        # CLI parameters _before_ subcommand
        parent_context_a = self.make_dbt_context("parent_context_a", cli_args)
        child_context_a = self.make_dbt_context("child_context_a", subcommand, parent_context_a)
        flags_a = Flags(child_context_a)

        # CLI parameters _after_ subcommand
        parent_context_b = self.make_dbt_context("parent_context_b", subcommand)
        child_context_b = self.make_dbt_context(
            "child_context_b", cli_args, parent_context_b
        )
        flags_b = Flags(child_context_b)

        assert flags_a.TARGET == flags_b.TARGET
        assert flags_a.TARGET == flag_value
        assert flags_b.TARGET == flag_value
```

Then run it like this:
```shell
python3 -m pytest tests/unit/test_cli_flags.py::TestFlags::test_target_settings
```

### Relevant log output

```shell
Specify one of these sub-commands and you can find more help from there.
========================================== short test summary info ==========================================
FAILED tests/unit/test_cli_flags.py::TestFlags::test_target_settings[None] - click.exceptions.Exit: 0
FAILED tests/unit/test_cli_flags.py::TestFlags::test_target_settings[my_target] - click.exceptions.Exit: 0
============================================= 2 failed in 0.42s =============================================
```


### Environment

```markdown
- OS:
- Python:
- dbt:
```


### Which database adapter are you using with dbt?

_No response_

### Additional Context

If the following edit is made to the code example above, then it will work:
```python
        parent_context_a = self.make_dbt_context("parent_context_a", cli_args.copy())
```

Alternatively, an equivalent change can be made [here](https://github.com/dbt-labs/dbt-core/blob/952cca8c58e5eda247196f7bf9d6794e72f1005b/tests/unit/test_cli_flags.py#L21) instead:
```python
    def make_dbt_context(
        self, context_name: str, args: List[str], parent: Optional[click.Context] = None
    ) -> click.Context:
        ctx = cli.make_context(context_name, args.copy(), parent)
        return ctx
```

We can see an example within the `click` source code [here](https://github.com/pallets/click/blob/cab9483a30379f9b8e3ddb72d5a4e88f88d517b6/src/click/shell_completion.py#L547) of making a copy of the `args` when calling `cli.make_context`:
```python
    ctx = cli.make_context(prog_name, args.copy(), **ctx_args)
```

So that makes it seem like someone else has bumped into a similar issue before.

My guess is that Click's [`make_context`](https://github.com/pallets/click/blob/cab9483a30379f9b8e3ddb72d5a4e88f88d517b6/src/click/core.py#L1099) is mutating the `args` list which is creates an issue if is re-used a second time in a subsequent call to `make_context`.

If so, then the best solution would be if `make_context` didn't mutate the list.

But in the meantime, it would behoove us to make a copy of any `args` variables being passed into `cli.make_context`.


Discussion:
If I had to guess, the relevant place within `click` where the args can be mutated is [here](https://github.com/pallets/click/blob/cab9483a30379f9b8e3ddb72d5a4e88f88d517b6/src/click/parser.py#L217).

So the problem _might_ be solved at the root if it were updated to this:
```python
        self.rargs = rargs.copy()
```

After this state-holding object makes its own copy of the args list, then it can safely mutate it with impunity 😎

<details>
<summary>
Code trace
</summary>

- https://github.com/pallets/click/blob/cab9483a30379f9b8e3ddb72d5a4e88f88d517b6/src/click/core.py#L1099-L1133
- https://github.com/pallets/click/blob/cab9483a30379f9b8e3ddb72d5a4e88f88d517b6/src/click/core.py#L1136-L1142
- https://github.com/pallets/click/blob/cab9483a30379f9b8e3ddb72d5a4e88f88d517b6/src/click/core.py#L1136-L1142
- https://github.com/pallets/click/blob/cab9483a30379f9b8e3ddb72d5a4e88f88d517b6/src/click/parser.py#L295-L304
- https://github.com/pallets/click/blob/cab9483a30379f9b8e3ddb72d5a4e88f88d517b6/src/click/parser.py#L213-L217
- 💥 https://github.com/pallets/click/blob/cab9483a30379f9b8e3ddb72d5a4e88f88d517b6/src/click/parser.py#L324-L326
</details>


Resolving patch:
diff --git a/core/dbt/cli/main.py b/core/dbt/cli/main.py
--- a/core/dbt/cli/main.py
+++ b/core/dbt/cli/main.py
@@ -54,7 +54,7 @@ def __init__(
 
     def invoke(self, args: List[str], **kwargs) -> dbtRunnerResult:
         try:
-            dbt_ctx = cli.make_context(cli.name, args)
+            dbt_ctx = cli.make_context(cli.name, args.copy())
             dbt_ctx.obj = {
                 "manifest": self.manifest,
                 "callbacks": self.callbacks,


Test patch:
diff --git a/tests/functional/dbt_runner/test_dbt_runner.py b/tests/functional/dbt_runner/test_dbt_runner.py
--- a/tests/functional/dbt_runner/test_dbt_runner.py
+++ b/tests/functional/dbt_runner/test_dbt_runner.py
@@ -94,6 +94,12 @@ def test_pass_in_manifest(self, project, dbt):
         assert result.success
         assert len(FACTORY.adapters) == 1
 
+    def test_pass_in_args_variable(self, dbt):
+        args = ["--log-format", "text"]
+        args_before = args.copy()
+        dbt.invoke(args)
+        assert args == args_before
+
 
 class TestDbtRunnerQueryComments:
     @pytest.fixture(scope="class")
diff --git a/tests/unit/test_cli_flags.py b/tests/unit/test_cli_flags.py
--- a/tests/unit/test_cli_flags.py
+++ b/tests/unit/test_cli_flags.py
@@ -18,7 +18,7 @@ class TestFlags:
     def make_dbt_context(
         self, context_name: str, args: List[str], parent: Optional[click.Context] = None
     ) -> click.Context:
-        ctx = cli.make_context(context_name, args, parent)
+        ctx = cli.make_context(context_name, args.copy(), parent)
         return ctx
 
     @pytest.fixture(scope="class")
@@ -29,6 +29,13 @@ def run_context(self) -> click.Context:
     def project_flags(self) -> ProjectFlags:
         return ProjectFlags()
 
+    def test_cli_args_unmodified(self):
+        args = ["--target", "my_target"]
+        args_before = args.copy()
+        self.make_dbt_context("context", args)
+
+        assert args == args_before
+
     def test_which(self, run_context):
         flags = Flags(run_context)
         assert flags.WHICH == "run"