Repository: encode/starlette

Issue:
QueryParams unexpected bahavior
Hey, in QueryParams when calling the `.keys()` it returns the keys from the `self._list` which can return duplicate keys if a key have multiple values. Same for `.values()` and `.items()`, it returns all the values, while the expected behavior is to return for each key the last value.

```python
>>> q = QueryParams(query_string="a=1&a=2")
>>> q.keys() # ['a', 'a'] the expected ['a']
>>> q.values() # [1, 2] the expected [2]
>>> q.items() # (a, 1) (a, 2) the expected (a, 2)
```
I can easily fix those, but I suggest to create a MultiValueDict that can be subclassed by other classes. 

So what do you think, should I keep it that way and fix the behavior or add a MultiDict ?

** In the QueryParams implementation, the items are stored in both a dict and a list, is it more efficient than django's `Mapping[str, List]` or just because back then there were more important things to do.


Discussion:
Let's treat this as two different issues.

1. Yup, perfectly happy to have the single-value variants instead.
2. Perhaps we'll have a base class too, but let's discuss that separately, later.
okie dokie :D 
@tomchristie you want `.keys()` to return a `generator`, a `dict_keys`  or a `list` ?
I'd suggest just starting with a list, as it currently is.
QueryParams have a limited size, so that'll be just fine.
I'd be okay with considering later pull requests that use dictkeys, but it'd depend on the additional complexity it adds.

Resolving patch:
diff --git a/starlette/datastructures.py b/starlette/datastructures.py
--- a/starlette/datastructures.py
+++ b/starlette/datastructures.py
@@ -231,7 +231,7 @@ class QueryParams(typing.Mapping[str, str]):
 
     def __init__(
         self,
-        params: typing.Mapping[str, str] = None,
+        params: typing.Union["QueryParams", typing.Mapping[str, str]] = None,
         items: typing.List[typing.Tuple[str, str]] = None,
         query_string: str = None,
         scope: Scope = None,
@@ -241,7 +241,10 @@ def __init__(
             assert items is None, "Cannot set both 'params' and 'items'"
             assert query_string is None, "Cannot set both 'params' and 'query_string'"
             assert scope is None, "Cannot set both 'params' and 'scope'"
-            _items = list(params.items())
+            if isinstance(params, QueryParams):
+                _items = list(params.multi_items())
+            else:
+                _items = list(params.items())
         elif items is not None:
             assert query_string is None, "Cannot set both 'items' and 'query_string'"
             assert scope is None, "Cannot set both 'items' and 'scope'"
@@ -252,26 +255,28 @@ def __init__(
         elif scope is not None:
             _items = parse_qsl(scope["query_string"].decode("latin-1"))
 
-        self._dict = {k: v for k, v in reversed(_items)}
+        self._dict = {k: v for k, v in _items}
         self._list = _items
 
     def getlist(self, key: typing.Any) -> typing.List[str]:
         return [item_value for item_key, item_value in self._list if item_key == key]
 
     def keys(self) -> typing.List[str]:  # type: ignore
-        return [key for key, value in self._list]
+        return list(self._dict.keys())
 
     def values(self) -> typing.List[str]:  # type: ignore
-        return [value for key, value in self._list]
+        return list(self._dict.values())
 
     def items(self) -> typing.List[typing.Tuple[str, str]]:  # type: ignore
+        return list(self._dict.items())
+
+    def multi_items(self) -> typing.List[typing.Tuple[str, str]]:
         return list(self._list)
 
     def get(self, key: typing.Any, default: typing.Any = None) -> typing.Any:
         if key in self._dict:
             return self._dict[key]
-        else:
-            return default
+        return default
 
     def __getitem__(self, key: typing.Any) -> str:
         return self._dict[key]
@@ -283,7 +288,7 @@ def __iter__(self) -> typing.Iterator[typing.Any]:
         return iter(self.keys())
 
     def __len__(self) -> int:
-        return len(self._list)
+        return len(self._dict)
 
     def __eq__(self, other: typing.Any) -> bool:
         if not isinstance(other, QueryParams):


Test patch:
diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py
--- a/tests/test_datastructures.py
+++ b/tests/test_datastructures.py
@@ -172,15 +172,16 @@ def test_queryparams():
     assert "a" in q
     assert "A" not in q
     assert "c" not in q
-    assert q["a"] == "123"
-    assert q.get("a") == "123"
+    assert q["a"] == "456"
+    assert q.get("a") == "456"
     assert q.get("nope", default=None) is None
     assert q.getlist("a") == ["123", "456"]
-    assert q.keys() == ["a", "a", "b"]
-    assert q.values() == ["123", "456", "789"]
-    assert q.items() == [("a", "123"), ("a", "456"), ("b", "789")]
-    assert list(q) == ["a", "a", "b"]
-    assert dict(q) == {"a": "123", "b": "789"}
+    assert q.keys() == ["a", "b"]
+    assert q.values() == ["456", "789"]
+    assert q.items() == [("a", "456"), ("b", "789")]
+    assert len(q) == 2
+    assert list(q) == ["a", "b"]
+    assert dict(q) == {"a": "456", "b": "789"}
     assert str(q) == "a=123&a=456&b=789"
     assert repr(q) == "QueryParams(query_string='a=123&a=456&b=789')"
     assert QueryParams({"a": "123", "b": "456"}) == QueryParams(
@@ -193,4 +194,10 @@ def test_queryparams():
         {"b": "456", "a": "123"}
     )
     assert QueryParams() == QueryParams({})
+    assert QueryParams(items=[("a", "123"), ("a", "456")]) == QueryParams(
+        query_string="a=123&a=456"
+    )
     assert QueryParams({"a": "123", "b": "456"}) != "invalid"
+
+    q = QueryParams(items=[("a", "123"), ("a", "456")])
+    assert QueryParams(q) == q