Repository: dbt-labs/dbt-core

Issue:
Membership operations on `node.config` object fail
### Describe the bug
When using membership operators (e.g. `"key" in node.config`) in `0.20.0`, you get an error "hasattr(): attribute name must be string". That is because this uses the `__iter__` method under the hood, which has been removed in https://github.com/dbt-labs/dbt/pull/3364/files#diff-007a86f768b23e1516e6d253d71656b96d46672fc7432dbc81d1d9445c98ca05L204-L231.

### Steps To Reproduce
I created a reproduction case in the testing suite here -> https://github.com/dbt-labs/dbt/compare/dbt-labs:f460d27...dbt-labs:8612ce4. 

### Expected behavior
This is a breaking change for folks who are doing these types of operations in their projects, I think it is worth maintaining consistency (https://getdbt.slack.com/archives/CBSQTAPLG/p1626195482486800)


Discussion:
Python, what a magical thing...

Thanks for finding & filing @kwigley! Feels like a super quick fix, and definitely worth doing.
@gshank this might be a partial parsing bug. Could you please take a look?
This isn't a partial parsing bug per se. It's because I removed the MutableMapping superclass from the config class because of buggy behavior when serializing. I believe that all we have to do is add back the __iter__ method into the BaseConfig class.

Resolving patch:
diff --git a/core/dbt/contracts/graph/model_config.py b/core/dbt/contracts/graph/model_config.py
--- a/core/dbt/contracts/graph/model_config.py
+++ b/core/dbt/contracts/graph/model_config.py
@@ -2,13 +2,13 @@
 from enum import Enum
 from itertools import chain
 from typing import (
-    Any, List, Optional, Dict, Union, Type, TypeVar
+    Any, List, Optional, Dict, Union, Type, TypeVar, Callable
 )
 from dbt.dataclass_schema import (
     dbtClassMixin, ValidationError, register_pattern,
 )
 from dbt.contracts.graph.unparsed import AdditionalPropertiesAllowed
-from dbt.exceptions import InternalException
+from dbt.exceptions import InternalException, CompilationException
 from dbt.contracts.util import Replaceable, list_str
 from dbt import hooks
 from dbt.node_types import NodeType
@@ -204,6 +204,34 @@ def __setitem__(self, key, value):
         else:
             self._extra[key] = value
 
+    def __delitem__(self, key):
+        if hasattr(self, key):
+            msg = (
+                'Error, tried to delete config key "{}": Cannot delete '
+                'built-in keys'
+            ).format(key)
+            raise CompilationException(msg)
+        else:
+            del self._extra[key]
+
+    def _content_iterator(self, include_condition: Callable[[Field], bool]):
+        seen = set()
+        for fld, _ in self._get_fields():
+            seen.add(fld.name)
+            if include_condition(fld):
+                yield fld.name
+
+        for key in self._extra:
+            if key not in seen:
+                seen.add(key)
+                yield key
+
+    def __iter__(self):
+        yield from self._content_iterator(include_condition=lambda f: True)
+
+    def __len__(self):
+        return len(self._get_fields()) + len(self._extra)
+
     @staticmethod
     def compare_key(
         unrendered: Dict[str, Any],


Test patch:
diff --git a/test/integration/043_custom_aliases_test/macros-configs/macros.sql b/test/integration/043_custom_aliases_test/macros-configs/macros.sql
--- a/test/integration/043_custom_aliases_test/macros-configs/macros.sql
+++ b/test/integration/043_custom_aliases_test/macros-configs/macros.sql
@@ -4,7 +4,7 @@
     {%- if custom_alias_name is none -%}
         {{ node.name }}
     {%- else -%}
-        custom_{{ node.config['alias'] | trim }}
+        custom_{{ node.config['alias'] if 'alias' in node.config else '' | trim }}
     {%- endif -%}
 {%- endmacro %}