Repository: dbt-labs/dbt-core
Pull request: fix: removing dependency from dependencies.yml
resolves https://github.com/dbt-labs/dbt-core/issues/7745

<!---
  Include the number of the issue addressed by this PR above if applicable.
  PRs for code changes without an associated issue *will not be merged*.
  See CONTRIBUTING.md for more information.
-->

### Description

<!---
  Describe the Pull Request here. Add any references and info to help reviewers
  understand your changes. Include any tradeoffs you considered.
-->

### Checklist

- [x] I have read [the contributing guide](https://github.com/dbt-labs/dbt-core/blob/main/CONTRIBUTING.md) and understand what's expected of me
- [x] I have signed the [CLA](https://docs.getdbt.com/docs/contributor-license-agreements)
- [x] I have run this code in development and it appears to resolve the stated issue
- [x] This PR includes tests, or tests are not required/relevant for this PR
- [x] I have [opened an issue to add/update docs](https://github.com/dbt-labs/docs.getdbt.com/issues/new/choose), or docs changes are not required/relevant for this PR
- [x] I have run `changie new` to [create a changelog entry](https://github.com/dbt-labs/dbt-core/blob/main/CONTRIBUTING.md#adding-a-changelog-entry)


Problem statement:
[CT-2614] [Bug] Removing project dependency from dependencies.yml between runs causes RuntimeError
Going from: 
```
-- dependencies.yml
projects:
  - name: project_a
```
to: 
```
-- dependencies.yml
projects: []
```

causes: 
```python
  File "/Users/michelleark/src/dbt-core/core/dbt/parser/manifest.py", line 791, in build_public_nodes
    for project_name, publication in self.manifest.publications.items():
RuntimeError: dictionary changed size during iteration
```

Hints from issue discussion:

Commit message: changelog entry

Patch to review:
diff --git a/.changes/unreleased/Fixes-20230605-124425.yaml b/.changes/unreleased/Fixes-20230605-124425.yaml
new file mode 100644
index 00000000000..09c23e3f296
--- /dev/null
+++ b/.changes/unreleased/Fixes-20230605-124425.yaml
@@ -0,0 +1,6 @@
+kind: Fixes
+body: fix RuntimeError when removing project dependency from dependencies.yml
+time: 2023-06-05T12:44:25.978022-04:00
+custom:
+  Author: michelleark
+  Issue: "7743"
diff --git a/core/dbt/parser/manifest.py b/core/dbt/parser/manifest.py
index bc6c9233388..2f5855f77ef 100644
--- a/core/dbt/parser/manifest.py
+++ b/core/dbt/parser/manifest.py
@@ -784,14 +784,15 @@ def build_public_nodes(self) -> bool:
             for project in self.manifest.project_dependencies.projects:
                 project_dependency_names.append(project.name)
 
-        # clean up previous publications that are no longer specified
-        # and save previous publications, for later removal of references
-        saved_manifest_publications: MutableMapping[str, PublicationConfig] = {}
+        # Save previous publications, for later removal of references
+        saved_manifest_publications: MutableMapping[str, PublicationConfig] = deepcopy(
+            self.manifest.publications
+        )
         if self.manifest.publications:
             for project_name, publication in self.manifest.publications.items():
                 if project_name not in project_dependency_names:
                     remove_dependent_project_references(self.manifest, publication)
-                    self.manifest.publications.pop(project_name)
+                    saved_manifest_publications.pop(project_name)
                     fire_event(
                         PublicationArtifactChanged(
                             action="removed",
@@ -802,7 +803,8 @@ def build_public_nodes(self) -> bool:
                         )
                     )
                     public_nodes_changed = True
-            saved_manifest_publications = self.manifest.publications
+
+            # clean up previous publications that are no longer specified
             self.manifest.publications = {}
             # Empty public_nodes since we're re-generating them all
             self.manifest.public_nodes = {}
diff --git a/tests/functional/partial_parsing/fixtures.py b/tests/functional/partial_parsing/fixtures.py
index 11ff492443d..0df8ccb350c 100644
--- a/tests/functional/partial_parsing/fixtures.py
+++ b/tests/functional/partial_parsing/fixtures.py
@@ -1233,3 +1233,57 @@
 
 
 """
+
+dependencies_yml = """
+projects:
+  - name: marketing
+"""
+
+empty_dependencies_yml = """
+projects: []
+"""
+
+marketing_pub_json = """
+{
+  "project_name": "marketing",
+  "metadata": {
+    "dbt_schema_version": "https://schemas.getdbt.com/dbt/publication/v1.json",
+    "dbt_version": "1.5.0",
+    "generated_at": "2023-04-13T17:17:58.128706Z",
+    "invocation_id": "56e3126f-78c7-470c-8eb0-c94af7c3eaac",
+    "env": {},
+    "adapter_type": "postgres",
+    "quoting": {
+      "database": true,
+      "schema": true,
+      "identifier": true
+    }
+  },
+  "public_models": {
+    "model.marketing.fct_one": {
+      "name": "fct_one",
+      "package_name": "marketing",
+      "unique_id": "model.marketing.fct_one",
+      "relation_name": "\\"dbt\\".\\"test_schema\\".\\"fct_one\\"",
+      "database": "dbt",
+      "schema": "test_schema",
+      "identifier": "fct_one",
+      "version": null,
+      "latest_version": null,
+      "public_node_dependencies": [],
+      "generated_at": "2023-04-13T17:17:58.128706Z"
+    },
+    "model.marketing.fct_two": {
+      "name": "fct_two",
+      "package_name": "marketing",
+      "unique_id": "model.marketing.fct_two",
+      "relation_name": "\\"dbt\\".\\"test_schema\\".\\"fct_two\\"",
+      "version": null,
+      "latest_version": null,
+      "public_node_dependencies": ["model.test.fct_one"],
+      "generated_at": "2023-04-13T17:17:58.128706Z"
+    }
+  },
+  "dependencies": []
+}
+"""
diff --git a/tests/functional/partial_parsing/test_partial_parsing.py b/tests/functional/partial_parsing/test_partial_parsing.py
index 748747311c7..4ee2b0efea9 100644
--- a/tests/functional/partial_parsing/test_partial_parsing.py
+++ b/tests/functional/partial_parsing/test_partial_parsing.py
@@ -69,11 +69,17 @@
     groups_schema_yml_one_group_model_in_group2,
     groups_schema_yml_two_groups_private_orders_valid_access,
     groups_schema_yml_two_groups_private_orders_invalid_access,
+    dependencies_yml,
+    empty_dependencies_yml,
+    marketing_pub_json,
 )
 
 from dbt.exceptions import CompilationError, ParsingError, DuplicateVersionedUnversionedError
 from dbt.contracts.files import ParseFileType
 from dbt.contracts.results import TestStatus
+from dbt.contracts.publication import PublicationArtifact
+
+import json
 import re
 import os
 
@@ -804,3 +810,26 @@ def test_pp_groups(self, project):
         )
         with pytest.raises(ParsingError):
             results = run_dbt(["--partial-parse", "run"])
+
+
+class TestDependencies:
+    @pytest.fixture(scope="class")
+    def models(self):
+        return {"orders.sql": orders_sql}
+
+    @pytest.fixture(scope="class")
+    def marketing_publication(self):
+        return PublicationArtifact.from_dict(json.loads(marketing_pub_json))
+
+    def test_dependencies(self, project, marketing_publication):
+        # initial run with dependencies
+        write_file(dependencies_yml, "dependencies.yml")
+        run_dbt(["run"], publications=[marketing_publication])
+        manifest = get_manifest(project.project_root)
+        assert len(manifest.project_dependencies.projects) == 1
+
+        # remove dependencies
+        write_file(empty_dependencies_yml, "dependencies.yml")
+        run_dbt(["run"], publications=[marketing_publication])
+        manifest = get_manifest(project.project_root)
+        assert len(manifest.project_dependencies.projects) == 0


Code review comments:
diff_hunk: @@ -804,3 +810,26 @@ def test_pp_groups(self, project):
         )
         with pytest.raises(ParsingError):
             results = run_dbt(["--partial-parse", "run"])
+
+
+class TestDependencies:
+    @pytest.fixture(scope="class")
+    def models(self):
+        return {"orders.sql": orders_sql}
+
+    @pytest.fixture(scope="class")
+    def marketing_publication(self):
+        return PublicationArtifact.from_dict(json.loads(marketing_pub_json))
+
+    def test_dependencies(self, project, marketing_publication):
+        # initial run with dependencies
+        write_file(dependencies_yml, "dependencies.yml")
+        run_dbt(["run"], publications=[marketing_publication])
path: tests/functional/partial_parsing/test_partial_parsing.py
text: @author:
will change this to "parse"