Repository: encode/httpx
Pull request #866 (2020-03-16 20:25:50)

Add `Content-Length` header for empty bytestream
Reference: https://github.com/encode/httpx/pull/861#discussion_r392840494


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 158577a..748ac94 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
+## 0.12.1 (March 19th, 2020)
+
+### Fixed
+
+* Resolved packaging issue, where additional files were being included.
+
 ## 0.12.0 (March 9th, 2020)
 
 The 0.12 release tightens up the API expectations for `httpx` by switching to private module names to enforce better clarity around public API.
diff --git a/README.md b/README.md
index 0be8618..574c7c3 100644
--- a/README.md
+++ b/README.md
@@ -101,6 +101,8 @@ For more advanced topics, see the [Advanced Usage](https://www.python-httpx.org/
 
 The [Developer Interface](https://www.python-httpx.org/api/) provides a comprehensive API reference.
 
+To find out about tools that integrate with HTTPX, see [Third Party Packages](https://www.python-httpx.org/third-party-packages/).
+
 ## Contribute
 
 If you want to contribute with HTTPX check out the [Contributing Guide](https://www.python-httpx.org/contributing/) to learn how to start.
diff --git a/docs/compatibility.md b/docs/compatibility.md
index 89235b1..c827f35 100644
--- a/docs/compatibility.md
+++ b/docs/compatibility.md
@@ -64,6 +64,12 @@ is generally equivalent to
 client = httpx.Client(**kwargs)
 ```
 
+## Request instantiation
+
+There is no notion of [prepared requests](https://requests.readthedocs.io/en/stable/user/advanced/#prepared-requests) in HTTPX. If you need to customize request instantiation, see [Request instances](/advanced#request-instances).
+
+Besides, `httpx.Request()` does not support the `auth`, `timeout`, `allow_redirects`, `proxies`, `verify` and `cert` parameters. However these are available in `httpx.request`, `httpx.get`, `httpx.post` etc., as well as on [`Client` instances](/advanced#client-instances).
+
 ## Mocking
 
 If you need to mock HTTPX the same way that test utilities like `responses` and `requests-mock` does for `requests`, see [RESPX](https://github.com/lundberg/respx).
diff --git a/docs/index.md b/docs/index.md
index 2bc69b5..0f93117 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -103,6 +103,8 @@ the [async support](async.md) section, or the [HTTP/2](http2.md) section.
 
 The [Developer Interface](api.md) provides a comprehensive API reference.
 
+To find out about tools that integrate with HTTPX, see [Third Party Packages](third-party-packages.md).
+
 ## Dependencies
 
 The HTTPX project relies on these excellent libraries:
diff --git a/docs/third-party-packages.md b/docs/third-party-packages.md
new file mode 100644
index 0000000..cc6a799
--- /dev/null
+++ b/docs/third-party-packages.md
@@ -0,0 +1,25 @@
+# Third Party Packages
+
+As HTTPX usage grows, there is an expanding community of developers building tools and libraries that integrate with HTTPX, or depend on HTTPX. Here are some of them.
+
+## Plugins
+
+<!-- NOTE: this list is in alphabetical order. -->
+
+### Authlib
+
+[GitHub](https://github.com/lepture/authlib) - [Documentation](https://docs.authlib.org/en/latest/)
+
+The ultimate Python library in building OAuth and OpenID Connect clients and servers. Includes an [OAuth HTTPX client](https://docs.authlib.org/en/latest/client/httpx.html).
+
+### Gidgethub
+
+[GitHub](https://github.com/brettcannon/gidgethub) - [Documentation](https://gidgethub.readthedocs.io/en/latest/index.html)
+
+An asynchronous GitHub API library. Includes [HTTPX support](https://gidgethub.readthedocs.io/en/latest/httpx.html).
+
+### RESPX
+
+[GitHub](https://github.com/lundberg/respx) - [Documentation](https://lundberg.github.io/respx/)
+
+A utility for mocking out the Python HTTPX library.
diff --git a/httpx/__version__.py b/httpx/__version__.py
index 3b4d4b9..c2a43f6 100644
--- a/httpx/__version__.py
+++ b/httpx/__version__.py
@@ -1,3 +1,3 @@
 __title__ = "httpx"
 __description__ = "A next generation HTTP client, for Python 3."
-__version__ = "0.12.0"
+__version__ = "0.12.1"
diff --git a/httpx/_content_streams.py b/httpx/_content_streams.py
index afffed3..dd6c51f 100644
--- a/httpx/_content_streams.py
+++ b/httpx/_content_streams.py
@@ -71,8 +71,6 @@ class ByteStream(ContentStream):
         self.body = body.encode("utf-8") if isinstance(body, str) else body
 
     def get_headers(self) -> typing.Dict[str, str]:
-        if not self.body:
-            return {}
         content_length = str(len(self.body))
         return {"Content-Length": content_length}
 
diff --git a/mkdocs.yml b/mkdocs.yml
index 852bb82..518736c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -18,6 +18,7 @@ nav:
     - Environment Variables: 'environment_variables.md'
     - Requests Compatibility: 'compatibility.md'
     - Developer Interface: 'api.md'
+    - Third Party Packages: 'third-party-packages.md'
     - Contributing: 'contributing.md'
 
 markdown_extensions:


Test patch:
diff --git a/tests/client/test_headers.py b/tests/client/test_headers.py
index 22b0ba1..5c4dbb4 100755
--- a/tests/client/test_headers.py
+++ b/tests/client/test_headers.py
@@ -40,6 +40,7 @@ async def test_client_header():
             "accept": "*/*",
             "accept-encoding": "gzip, deflate, br",
             "connection": "keep-alive",
+            "content-length": "0",
             "example-header": "example-value",
             "host": "example.org",
             "user-agent": f"python-httpx/{__version__}",
@@ -61,6 +62,7 @@ async def test_header_merge():
             "accept": "*/*",
             "accept-encoding": "gzip, deflate, br",
             "connection": "keep-alive",
+            "content-length": "0",
             "host": "example.org",
             "user-agent": "python-myclient/0.2.1",
             "x-auth-token": "FooBarBazToken",
@@ -82,6 +84,7 @@ async def test_header_merge_conflicting_headers():
             "accept": "*/*",
             "accept-encoding": "gzip, deflate, br",
             "connection": "keep-alive",
+            "content-length": "0",
             "host": "example.org",
             "user-agent": f"python-httpx/{__version__}",
             "x-auth-token": "BazToken",
@@ -105,6 +108,7 @@ async def test_header_update():
             "accept": "*/*",
             "accept-encoding": "gzip, deflate, br",
             "connection": "keep-alive",
+            "content-length": "0",
             "host": "example.org",
             "user-agent": f"python-httpx/{__version__}",
         }
@@ -117,6 +121,7 @@ async def test_header_update():
             "accept-encoding": "gzip, deflate, br",
             "another-header": "AThing",
             "connection": "keep-alive",
+            "content-length": "0",
             "host": "example.org",
             "user-agent": "python-myclient/0.2.1",
         }
@@ -147,6 +152,7 @@ async def test_host_with_auth_and_port_in_url():
             "accept": "*/*",
             "accept-encoding": "gzip, deflate, br",
             "connection": "keep-alive",
+            "content-length": "0",
             "host": "example.org",
             "user-agent": f"python-httpx/{__version__}",
             "authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
@@ -171,6 +177,7 @@ async def test_host_with_non_default_port_in_url():
             "accept": "*/*",
             "accept-encoding": "gzip, deflate, br",
             "connection": "keep-alive",
+            "content-length": "0",
             "host": "example.org:123",
             "user-agent": f"python-httpx/{__version__}",
             "authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
diff --git a/tests/client/test_redirects.py b/tests/client/test_redirects.py
index 190ff09..63403a6 100644
--- a/tests/client/test_redirects.py
+++ b/tests/client/test_redirects.py
@@ -279,7 +279,7 @@ async def test_body_redirect():
     response = await client.post(url, data=data)
     assert response.url == URL("https://example.org/redirect_body_target")
     assert response.json()["body"] == "Example request body"
-    assert "content-length" in response.json()["headers"]
+    assert response.json()["headers"]["content-length"] == "20"
 
 
 @pytest.mark.usefixtures("async_environment")
@@ -293,7 +293,7 @@ async def test_no_body_redirect():
     response = await client.post(url, data=data)
     assert response.url == URL("https://example.org/redirect_body_target")
     assert response.json()["body"] == ""
-    assert "content-length" not in response.json()["headers"]
+    assert response.json()["headers"]["content-length"] == "0"
 
 
 @pytest.mark.usefixtures("async_environment")
diff --git a/tests/models/test_requests.py b/tests/models/test_requests.py
index c72e0af..0461394 100644
--- a/tests/models/test_requests.py
+++ b/tests/models/test_requests.py
@@ -10,7 +10,7 @@ def test_request_repr():
 
 def test_no_content():
     request = httpx.Request("GET", "http://example.org")
-    assert "Content-Length" not in request.headers
+    assert request.headers["Content-Length"] == "0"
 
 
 def test_content_length_header():
diff --git a/tests/test_content_streams.py b/tests/test_content_streams.py
index c5eb961..84999ef 100644
--- a/tests/test_content_streams.py
+++ b/tests/test_content_streams.py
@@ -25,7 +25,9 @@ async def test_empty_content():
     async_content = b"".join([part async for part in stream])
 
     assert stream.can_replay()
-    assert stream.get_headers() == {}
+    assert stream.get_headers() == {
+        "Content-Length": "0",
+    }
     assert sync_content == b""
     assert async_content == b""
 
@@ -196,7 +198,9 @@ async def test_empty_request():
     async_content = b"".join([part async for part in stream])
 
     assert stream.can_replay()
-    assert stream.get_headers() == {}
+    assert stream.get_headers() == {
+        "Content-Length": "0",
+    }
     assert sync_content == b""
     assert async_content == b""