Repository: meltano/meltano

Issue:
bug: Project files deep merge error when child dict has str value
### Meltano Version

2.12.0

### Python Version

NA

### Bug scope

Other

### Operating System

N/A

### Description

Minimal reproducible example:

```
>>> meltano.core.project_files.deep_merge({}, [{'': ''}])
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /home/will/.pyenv/versions/3.10.7/lib/python3.10/code.py:90 in runcode                           │
│                                                                                                  │
│    87 │   │                                                                                      │
│    88 │   │   """                                                                                │
│    89 │   │   try:                                                                               │
│ ❱  90 │   │   │   exec(code, self.locals)                                                        │
│    91 │   │   except SystemExit:                                                                 │
│    92 │   │   │   raise                                                                          │
│    93 │   │   except:                                                                            │
│ <console>:1 in <module>                                                                          │
│                                                                                                  │
│ /home/will/meltano/meltano/src/meltano/core/project_files.py:56 in deep_merge                    │
│                                                                                                  │
│    53 │   │   │   elif isinstance(value, Sequence):                                              │
│    54 │   │   │   # elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):    │
│    55 │   │   │   │   node = base.setdefault(key, value.__class__())                             │
│ ❱  56 │   │   │   │   node.extend(value)                                                         │
│    57 │   │   │   else:                                                                          │
│    58 │   │   │   │   base[key] = value                                                          │
│    59 │   return base                                                                            │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
AttributeError: 'str' object has no attribute 'extend'
```

The commented-out line above the line where the error is thrown is a fix.

### Code

_No response_


Discussion:

Resolving patch:
diff --git a/src/meltano/core/project_files.py b/src/meltano/core/project_files.py
--- a/src/meltano/core/project_files.py
+++ b/src/meltano/core/project_files.py
@@ -50,7 +50,7 @@ def deep_merge(parent: TMapping, children: list[TMapping]) -> TMapping:
                 # get node or create one
                 node = base.setdefault(key, value.__class__())
                 base[key] = deep_merge(node, [value])
-            elif isinstance(value, Sequence):
+            elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
                 node = base.setdefault(key, value.__class__())
                 node.extend(value)
             else:


Test patch:
diff --git a/tests/meltano/core/test_project_files.py b/tests/meltano/core/test_project_files.py
--- a/tests/meltano/core/test_project_files.py
+++ b/tests/meltano/core/test_project_files.py
@@ -32,13 +32,14 @@ def cd_temp_dir():
 
 @pytest.mark.order(0)
 @pytest.mark.parametrize(
-    "parent,children,expected",
-    [
+    ("parent", "children", "expected"),
+    (
         ({"a": 1}, [{"a": 1}], {"a": 1}),
         ({"a": 1}, [{"a": 2}], {"a": 2}),
         ({"a": 1}, [{"a": 2, "b": 2}], {"a": 2, "b": 2}),
         ({"a": [1, 2, 3]}, [{"a": [3, 4, 5]}], {"a": [1, 2, 3, 3, 4, 5]}),
-    ],
+        ({"a": "A", "b": "B"}, [{"a": "Z"}], {"a": "Z", "b": "B"}),
+    ),
 )
 def test_deep_merge(parent, children, expected):
     assert deep_merge(parent, children) == expected