Repository: encode/starlette

Issue:
Unable to merge MutableHeaders with dict
### Checklist

- [x] The bug is reproducible against the latest release and/or `master`.
- [x] There are no similar issues or pull requests to fix it yet.

### Describe the bug

Unable to merge `MutableHeaders` with `dict`.

### To reproduce

```python3
from starlette.datastructures import MutableHeaders

response = MutableHeaders()
response |= {"X-Header": "string"}
```
### Expected behavior

```python3
>>> response
MutableHeaders({'x-header': 'string'})
```

### Actual behavior

Raised exception:
```
TypeError: unsupported operand type(s) for |=: 'MutableHeaders' and 'dict'
```

### Environment

- OS: Ubuntu 21.04
- Python version: 3.9.6
- Starlette version: 0.15.0

### Additional context

Method `MutableHeaders.update()` can resolve the issue but I think comfortable is used to operators `|` (for `merge`) and `|=` (for `update`).

It's comfortable if you have many headers like:

```python3
...

headers = {
    "X-Header-1": "string-1",
    "X-Header-1": "string-2",
    "X-Header-3": "string-3",
    "X-Header-...": "string-...",
    "X-Header-N": "string-N",
}

response.headers |= headers  # equals to: response.headers.update(headers)

return response

...
```
It's like [Dictionary Merge & Update Operators in Python 3.9](https://docs.python.org/3/whatsnew/3.9.html#dictionary-merge-update-operators).


What do you think about it? Thank you.


Discussion:

Resolving patch:
diff --git a/starlette/datastructures.py b/starlette/datastructures.py
--- a/starlette/datastructures.py
+++ b/starlette/datastructures.py
@@ -618,6 +618,19 @@ def __delitem__(self, key: str) -> None:
         for idx in reversed(pop_indexes):
             del self._list[idx]
 
+    def __ior__(self, other: typing.Mapping) -> "MutableHeaders":
+        if not isinstance(other, typing.Mapping):
+            raise TypeError(f"Expected a mapping but got {other.__class__.__name__}")
+        self.update(other)
+        return self
+
+    def __or__(self, other: typing.Mapping) -> "MutableHeaders":
+        if not isinstance(other, typing.Mapping):
+            raise TypeError(f"Expected a mapping but got {other.__class__.__name__}")
+        new = self.mutablecopy()
+        new.update(other)
+        return new
+
     @property
     def raw(self) -> typing.List[typing.Tuple[bytes, bytes]]:
         return self._list
@@ -636,7 +649,7 @@ def setdefault(self, key: str, value: str) -> str:
         self._list.append((set_key, set_value))
         return value
 
-    def update(self, other: dict) -> None:
+    def update(self, other: typing.Mapping) -> None:
         for key, val in other.items():
             self[key] = val
 


Test patch:
diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py
--- a/tests/test_datastructures.py
+++ b/tests/test_datastructures.py
@@ -162,6 +162,50 @@ def test_mutable_headers():
     assert h.raw == [(b"b", b"4")]
 
 
+def test_mutable_headers_merge():
+    h = MutableHeaders()
+    h = h | MutableHeaders({"a": "1"})
+    assert isinstance(h, MutableHeaders)
+    assert dict(h) == {"a": "1"}
+    assert h.items() == [("a", "1")]
+    assert h.raw == [(b"a", b"1")]
+
+
+def test_mutable_headers_merge_dict():
+    h = MutableHeaders()
+    h = h | {"a": "1"}
+    assert isinstance(h, MutableHeaders)
+    assert dict(h) == {"a": "1"}
+    assert h.items() == [("a", "1")]
+    assert h.raw == [(b"a", b"1")]
+
+
+def test_mutable_headers_update():
+    h = MutableHeaders()
+    h |= MutableHeaders({"a": "1"})
+    assert isinstance(h, MutableHeaders)
+    assert dict(h) == {"a": "1"}
+    assert h.items() == [("a", "1")]
+    assert h.raw == [(b"a", b"1")]
+
+
+def test_mutable_headers_update_dict():
+    h = MutableHeaders()
+    h |= {"a": "1"}
+    assert isinstance(h, MutableHeaders)
+    assert dict(h) == {"a": "1"}
+    assert h.items() == [("a", "1")]
+    assert h.raw == [(b"a", b"1")]
+
+
+def test_mutable_headers_merge_not_mapping():
+    h = MutableHeaders()
+    with pytest.raises(TypeError):
+        h |= {"not_mapping"}  # type: ignore
+    with pytest.raises(TypeError):
+        h | {"not_mapping"}  # type: ignore
+
+
 def test_headers_mutablecopy():
     h = Headers(raw=[(b"a", b"123"), (b"a", b"456"), (b"b", b"789")])
     c = h.mutablecopy()