Repository: litestar-org/litestar

Issue:
Bug: `litestar.datastructures.MultiDict.copy` returns `multidict._multidict.MultiDict` instance.
### Description

Seems the object returned by `litestar.datastructures.MultiDict.copy` is actually a `multidict._multidict.MultiDict` instance.
```py
from litestar.datastructures import MultiDict

a = MultiDict()
b = a.copy()
print(type(a))
# litestar.datastructures.multi_dicts.MultiDict
print(type(b))
# multidict._multidict.MultiDict
```
Is this intentional?

### URL to code causing the issue

_No response_

### MCVE

```python
See description.
```


### Steps to reproduce

_No response_

### Screenshots

_No response_

### Logs

_No response_

### Litestar Version

2.2.1

### Platform

- [ ] Linux
- [X] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

<!-- POLAR PLEDGE BADGE START -->
---
> [!NOTE]  
> While we are open for sponsoring on [GitHub Sponsors](https://github.com/sponsors/litestar-org/) and 
> [OpenCollective](https://opencollective.com/litestar), we also utilize [Polar.sh](https://polar.sh/) to engage in pledge-based sponsorship.
>
> Check out all issues funded or available for funding [on our Polar.sh Litestar dashboard](https://polar.sh/litestar-org)
> * If you would like to see an issue prioritized, make a pledge towards it!
> * We receive the pledge once the issue is completed & verified
> * This, along with engagement in the community, helps us know which features are a priority to our users.

<a href="https://polar.sh/litestar-org/litestar/issues/2549">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/2549/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/2549/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:
@litestar-org/members curious on this.

@Alc-Alc did we hit this yesterday in our OIDC testing?
I don't think this is anything we are doing:

```python-console
>>> from multidict import MultiDict
>>> class MyMD(MultiDict): ...
... 
>>> my_md = MyMD()
>>> my_md_copy = my_md.copy()
>>> type(my_md)
<class '__main__.MyMD'>
>>> type(my_md_copy)
<class 'multidict._multidict.MultiDict'>
>>> 
```
It seems that `MultiDict.copy()` doesn't support sub-classes.
Opened an issue here: https://github.com/aio-libs/multidict/issues/901
This is how inheritance works, typically:
```pycon
>>> class MyDict(dict): ...
... 
>>> my_dict = MyDict()
>>> my_dict_copy = my_dict.copy()
>>> type(my_dict)
<class '__main__.MyDict'>
>>> type(my_dict_copy)
<class 'dict'>
```
If you want a custom class, make override the method.
> This is how inheritance works, typically:
> 
> ```
> >>> class MyDict(dict): ...
> ... 
> >>> my_dict = MyDict()
> >>> my_dict_copy = my_dict.copy()
> >>> type(my_dict)
> <class '__main__.MyDict'>
> >>> type(my_dict_copy)
> <class 'dict'>
> ```
> 
> If you want a custom class, make override the method.

Makes sense, thanks @webknjaz 

Resolving patch:
diff --git a/litestar/datastructures/multi_dicts.py b/litestar/datastructures/multi_dicts.py
--- a/litestar/datastructures/multi_dicts.py
+++ b/litestar/datastructures/multi_dicts.py
@@ -1,13 +1,17 @@
 from __future__ import annotations
 
 from abc import ABC
-from typing import Any, Generator, Generic, Iterable, Mapping, TypeVar
+from typing import TYPE_CHECKING, Any, Generator, Generic, Iterable, Mapping, TypeVar
 
 from multidict import MultiDict as BaseMultiDict
 from multidict import MultiDictProxy, MultiMapping
 
 from litestar.datastructures.upload_file import UploadFile
 
+if TYPE_CHECKING:
+    from typing_extensions import Self
+
+
 __all__ = ("FormMultiDict", "ImmutableMultiDict", "MultiDict", "MultiMixin")
 
 
@@ -40,7 +44,8 @@ class MultiDict(BaseMultiDict[T], MultiMixin[T], Generic[T]):
     """MultiDict, using :class:`MultiDict <multidict.MultiDictProxy>`."""
 
     def __init__(self, args: MultiMapping | Mapping[str, T] | Iterable[tuple[str, T]] | None = None) -> None:
-        """Initialize ``MultiDict`` from a`MultiMapping``, :class:`Mapping <typing.Mapping>` or an iterable of tuples.
+        """Initialize ``MultiDict`` from a`MultiMapping``,
+        :class:`Mapping <typing.Mapping>` or an iterable of tuples.
 
         Args:
             args: Mapping-like structure to create the ``MultiDict`` from
@@ -57,14 +62,17 @@ def immutable(self) -> ImmutableMultiDict[T]:
         """
         return ImmutableMultiDict[T](self)  # pyright: ignore
 
+    def copy(self) -> Self:
+        """Return a shallow copy"""
+        return type(self)(list(self.multi_items()))
+
 
 class ImmutableMultiDict(MultiDictProxy[T], MultiMixin[T], Generic[T]):
     """Immutable MultiDict, using class:`MultiDictProxy <multidict.MultiDictProxy>`."""
 
     def __init__(self, args: MultiMapping | Mapping[str, Any] | Iterable[tuple[str, Any]] | None = None) -> None:
-        """Initialize ``ImmutableMultiDict`` from a.
-
-        ``MultiMapping``, :class:`Mapping <typing.Mapping>` or an iterable of tuples.
+        """Initialize ``ImmutableMultiDict`` from a `MultiMapping``,
+        :class:`Mapping <typing.Mapping>` or an iterable of tuples.
 
         Args:
             args: Mapping-like structure to create the ``ImmutableMultiDict`` from
@@ -72,15 +80,17 @@ def __init__(self, args: MultiMapping | Mapping[str, Any] | Iterable[tuple[str,
         super().__init__(BaseMultiDict(args or {}))
 
     def mutable_copy(self) -> MultiDict[T]:
-        """Create a mutable copy as a.
-
-        :class:`MultiDict`
+        """Create a mutable copy as a :class:`MultiDict`
 
         Returns:
             A mutable multi dict
         """
         return MultiDict(list(self.multi_items()))
 
+    def copy(self) -> Self:  # type: ignore[override]
+        """Return a shallow copy"""
+        return type(self)(self.items())
+
 
 class FormMultiDict(ImmutableMultiDict[Any]):
     """MultiDict for form data."""


Test patch:
diff --git a/tests/unit/test_datastructures/test_multi_dicts.py b/tests/unit/test_datastructures/test_multi_dicts.py
--- a/tests/unit/test_datastructures/test_multi_dicts.py
+++ b/tests/unit/test_datastructures/test_multi_dicts.py
@@ -1,4 +1,4 @@
-from typing import Type, Union
+from __future__ import annotations
 
 import pytest
 from pytest_mock import MockerFixture
@@ -8,14 +8,14 @@
 
 
 @pytest.mark.parametrize("multi_class", [MultiDict, ImmutableMultiDict])
-def test_multi_to_dict(multi_class: Type[Union[MultiDict, ImmutableMultiDict]]) -> None:
+def test_multi_to_dict(multi_class: type[MultiDict | ImmutableMultiDict]) -> None:
     multi = multi_class([("key", "value"), ("key", "value2"), ("key2", "value3")])
 
     assert multi.dict() == {"key": ["value", "value2"], "key2": ["value3"]}
 
 
 @pytest.mark.parametrize("multi_class", [MultiDict, ImmutableMultiDict])
-def test_multi_multi_items(multi_class: Type[Union[MultiDict, ImmutableMultiDict]]) -> None:
+def test_multi_multi_items(multi_class: type[MultiDict | ImmutableMultiDict]) -> None:
     data = [("key", "value"), ("key", "value2"), ("key2", "value3")]
     multi = multi_class(data)
 
@@ -47,3 +47,11 @@ async def test_form_multi_dict_close(mocker: MockerFixture) -> None:
     await multi.close()
 
     assert close.call_count == 2
+
+
+@pytest.mark.parametrize("type_", [MultiDict, ImmutableMultiDict])
+def test_copy(type_: type[MultiDict | ImmutableMultiDict]) -> None:
+    d = type_([("foo", "bar"), ("foo", "baz")])
+    copy = d.copy()
+    assert set(d.multi_items()) == set(copy.multi_items())
+    assert isinstance(d, type_)