Repository: encode/httpx

Issue:
Change QueryParams(key=None) to not emit a parameter
This one I know is [`requests` behaviour](https://github.com/psf/requests/blob/9a2e5df000691ad28613524ca7f80aa28a94a8d5/requests/models.py#L101):

Take a dict:
```py
>>> from httpx import QueryParams
>>> params = {"thing_one": 1, "thing_two": [1,2,3], "thing_three": None}
>>> QueryParams(params)
QueryParams('thing_one=1&thing_two=%5B1%2C+2%2C+3%5D&thing_three=None')
```
Expected:
```py
QueryParams('thing_one=1&thing_two=%5B1%2C+2%2C+3%5D')
```
Adding `if v is not None` [here](https://github.com/encode/httpx/blob/master/httpx/models.py#L263-L264) will do it.

Discussion:
tomchristie: Later follow ups:

* Allow `typing.Union[PrimitiveData, typing.List[PrimitiveData]]` for eg. `{a: [1, 2, 3]}` => `a=1&a=2&a=3`
* Use same style for urlencoded and multipart data.

Patch:
diff --git a/httpx/__init__.py b/httpx/__init__.py
index 8de5276..fd17441 100644
--- a/httpx/__init__.py
+++ b/httpx/__init__.py
@@ -34,6 +34,8 @@ from .interfaces import (
     AsyncDispatcher,
     BaseReader,
     BaseWriter,
+    BaseBackgroundManager,
+    BasePoolSemaphore,
     ConcurrencyBackend,
     Dispatcher,
     Protocol,
diff --git a/httpx/models.py b/httpx/models.py
index f98447c..df5d071 100644
--- a/httpx/models.py
+++ b/httpx/models.py
@@ -34,14 +34,17 @@ from .utils import (
     is_known_encoding,
     normalize_header_key,
     normalize_header_value,
+    str_query_param
 )
 
+PrimitiveData = typing.Union[str, int, float, bool, type(None)]
+
 URLTypes = typing.Union["URL", str]
 
 QueryParamTypes = typing.Union[
     "QueryParams",
-    typing.Mapping[str, str],
-    typing.List[typing.Tuple[typing.Any, typing.Any]],
+    typing.Mapping[str, PrimitiveData],
+    typing.List[typing.Tuple[str, PrimitiveData]],
     str,
 ]
 
@@ -268,8 +271,8 @@ class QueryParams(typing.Mapping[str, str]):
         else:
             items = value.items()  # type: ignore
 
-        self._list = [(str(k), str(v)) for k, v in items]
-        self._dict = {str(k): str(v) for k, v in items}
+        self._list = [(str(k), str_query_param(v)) for k, v in items]
+        self._dict = {str(k): str_query_param(v) for k, v in 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]
diff --git a/httpx/utils.py b/httpx/utils.py
index 3d0d660..e96335f 100644
--- a/httpx/utils.py
+++ b/httpx/utils.py
@@ -20,6 +20,21 @@ def normalize_header_value(value: typing.AnyStr, encoding: str = None) -> bytes:
     return value.encode(encoding or "ascii")
 
 
+def str_query_param(value: typing.Union[str, int, float, bool, type(None)]) -> str:
+    """
+    Coerce a primitive data type into a string value for query params.
+
+    Note that we prefer JSON-style 'true'/'false' for boolean values here.
+    """
+    if value is True:
+        return "true"
+    elif value is False:
+        return "false"
+    elif value is None:
+        return ""
+    return str(value)
+
+
 def is_known_encoding(encoding: str) -> bool:
     """
     Return `True` if `encoding` is a known codec.


Test patch:
diff --git a/tests/models/test_queryparams.py b/tests/models/test_queryparams.py
index 983b09f..fbb559f 100644
--- a/tests/models/test_queryparams.py
+++ b/tests/models/test_queryparams.py
@@ -31,3 +31,23 @@ def test_queryparams():
 
     q = QueryParams([("a", "123"), ("a", "456")])
     assert QueryParams(q) == q
+
+
+def test_queryparam_types():
+    q = QueryParams({"a": True})
+    assert str(q) == "a=true"
+
+    q = QueryParams({"a": False})
+    assert str(q) == "a=false"
+
+    q = QueryParams({"a": ""})
+    assert str(q) == "a="
+
+    q = QueryParams({"a": None})
+    assert str(q) == "a="
+
+    q = QueryParams({"a": 1.23})
+    assert str(q) == "a=1.23"
+
+    q = QueryParams({"a": 123})
+    assert str(q) == "a=123"