Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Treat warnings as errors #1687

Merged
merged 19 commits into from
Jun 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e8076b5
Treat warnings as errors
tomchristie Jun 15, 2021
3881ee2
Defensive programming in Client.__del__ to avoid possible warnings on…
tomchristie Jun 15, 2021
94721ed
Linting
tomchristie Jun 15, 2021
6e4784f
Ignore linting getattr errors in __del__
tomchristie Jun 15, 2021
05389e8
getattr requires a default
tomchristie Jun 15, 2021
62f2d9b
Tighten up closing of auth_flow generators
tomchristie Jun 15, 2021
eb6bd86
Switch multipart test to open file in a context manager
tomchristie Jun 15, 2021
3a32f04
Merge branch 'master' into pytest-treat-warnings-as-errors
j178 Jun 16, 2021
ecc1dd9
Ignore warnings on uvicorn
tomchristie Jun 16, 2021
f0a306c
Merge branch 'pytest-treat-warnings-as-errors' of https://github.com/…
tomchristie Jun 16, 2021
25392a2
Drop -Werror from addopts
j178 Jun 16, 2021
f4aebce
Warings specified entirely in 'filterwarnings' section
tomchristie Jun 16, 2021
f9dba46
Merge branch 'pytest-treat-warnings-as-errors' of https://github.com/…
tomchristie Jun 16, 2021
dcbf4d7
Use ssl.PROTOCOL_TLS_CLIENT instead of deprecated ssl.PROTOCOL_TLS
tomchristie Jun 16, 2021
ed9aabf
Push 'check_hostname = False' above 'context.verify_mode = ssl.CERT_N…
tomchristie Jun 16, 2021
c433d74
Introduce set_minimum_tls_version_1_2 compatible across different pyt…
tomchristie Jun 16, 2021
76992dc
Commenting
tomchristie Jun 16, 2021
7561f7f
Add missing annotation
tomchristie Jun 16, 2021
e6594a2
Exclude _compat from coverage
tomchristie Jun 16, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 56 additions & 44 deletions httpx/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,32 +897,35 @@ def _send_handling_auth(
history: typing.List[Response],
) -> Response:
auth_flow = auth.sync_auth_flow(request)
request = next(auth_flow)
try:
request = next(auth_flow)

for hook in self._event_hooks["request"]:
hook(request)
for hook in self._event_hooks["request"]:
hook(request)

while True:
response = self._send_handling_redirects(
request,
timeout=timeout,
allow_redirects=allow_redirects,
history=history,
)
try:
while True:
response = self._send_handling_redirects(
request,
timeout=timeout,
allow_redirects=allow_redirects,
history=history,
)
try:
next_request = auth_flow.send(response)
except StopIteration:
return response
try:
next_request = auth_flow.send(response)
except StopIteration:
return response

response.history = list(history)
response.read()
request = next_request
history.append(response)
response.history = list(history)
response.read()
request = next_request
history.append(response)

except Exception as exc:
response.close()
raise exc
except Exception as exc:
response.close()
raise exc
finally:
auth_flow.close()

def _send_handling_redirects(
self,
Expand Down Expand Up @@ -1244,7 +1247,10 @@ def __exit__(
transport.__exit__(exc_type, exc_value, traceback)

def __del__(self) -> None:
if self._state == ClientState.OPENED:
# We use 'getattr' here, to manage the case where '__del__()' is called
# on a partically initiallized instance that raised an exception during
# the call to '__init__()'.
if getattr(self, "_state", None) == ClientState.OPENED: # noqa: B009
self.close()


Expand Down Expand Up @@ -1588,32 +1594,35 @@ async def _send_handling_auth(
history: typing.List[Response],
) -> Response:
auth_flow = auth.async_auth_flow(request)
request = await auth_flow.__anext__()
try:
request = await auth_flow.__anext__()

for hook in self._event_hooks["request"]:
await hook(request)
for hook in self._event_hooks["request"]:
await hook(request)

while True:
response = await self._send_handling_redirects(
request,
timeout=timeout,
allow_redirects=allow_redirects,
history=history,
)
try:
while True:
response = await self._send_handling_redirects(
request,
timeout=timeout,
allow_redirects=allow_redirects,
history=history,
)
try:
next_request = await auth_flow.asend(response)
except StopAsyncIteration:
return response
try:
next_request = await auth_flow.asend(response)
except StopAsyncIteration:
return response

response.history = list(history)
await response.aread()
request = next_request
history.append(response)
response.history = list(history)
await response.aread()
request = next_request
history.append(response)

except Exception as exc:
await response.aclose()
raise exc
except Exception as exc:
await response.aclose()
raise exc
finally:
await auth_flow.aclose()

async def _send_handling_redirects(
self,
Expand Down Expand Up @@ -1942,7 +1951,10 @@ async def __aexit__(
await proxy.__aexit__(exc_type, exc_value, traceback)

def __del__(self) -> None:
if self._state == ClientState.OPENED:
# We use 'getattr' here, to manage the case where '__del__()' is called
# on a partically initiallized instance that raised an exception during
# the call to '__init__()'.
if getattr(self, "_state", None) == ClientState.OPENED: # noqa: B009
# Unlike the sync case, we cannot silently close the client when
# it is garbage collected, because `.aclose()` is an async operation,
# but `__del__` is not.
Expand Down
21 changes: 20 additions & 1 deletion httpx/_compat.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
"""
The _compat module is used for code which requires branching between different
Python environments. It is excluded from the code coverage checks.
"""
import ssl
import sys

# `contextlib.asynccontextmanager` exists from Python 3.7 onwards.
# For 3.6 we require the `async_generator` package for a backported version.
try:
from contextlib import asynccontextmanager # type: ignore
except ImportError: # pragma: no cover
except ImportError:
from async_generator import asynccontextmanager # type: ignore # noqa


def set_minimum_tls_version_1_2(context: ssl.SSLContext) -> None:
if sys.version_info >= (3, 10):
context.minimum_version = ssl.TLSVersion.TLSv1_2
else:
# These become deprecated in favor of 'context.minimum_version'
# from Python 3.10 onwards.
context.options |= ssl.OP_NO_SSLv2
context.options |= ssl.OP_NO_SSLv3
context.options |= ssl.OP_NO_TLSv1
context.options |= ssl.OP_NO_TLSv1_1
10 changes: 4 additions & 6 deletions httpx/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import certifi

from ._compat import set_minimum_tls_version_1_2
from ._models import URL, Headers
from ._types import CertTypes, HeaderTypes, TimeoutTypes, URLTypes, VerifyTypes
from ._utils import get_ca_bundle_from_env, get_logger
Expand Down Expand Up @@ -90,8 +91,8 @@ def load_ssl_context_no_verify(self) -> ssl.SSLContext:
Return an SSL context for unverified connections.
"""
context = self._create_default_ssl_context()
context.verify_mode = ssl.CERT_NONE
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
self._load_client_certs(context)
return context

Expand Down Expand Up @@ -153,11 +154,8 @@ def _create_default_ssl_context(self) -> ssl.SSLContext:
Creates the default SSLContext object that's used for both verified
and unverified connections.
"""
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
context.options |= ssl.OP_NO_SSLv2
context.options |= ssl.OP_NO_SSLv3
context.options |= ssl.OP_NO_TLSv1
context.options |= ssl.OP_NO_TLSv1_1
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
set_minimum_tls_version_1_2(context)
context.options |= ssl.OP_NO_COMPRESSION
context.set_ciphers(DEFAULT_CIPHERS)

Expand Down
5 changes: 4 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ combine_as_imports = True

[tool:pytest]
addopts = -rxXs
filterwarnings =
error
default:::uvicorn
markers =
copied_from(source, changes=None): mark test as copied from somewhere else, along with a description of changes made to accodomate e.g. our test setup

[coverage:run]
omit = venv/*
omit = venv/*, httpx/_compat.py
include = httpx/*, tests/*
57 changes: 29 additions & 28 deletions tests/test_multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,34 +107,35 @@ def test_multipart_encode(tmp_path: typing.Any) -> None:
"e": True,
"f": "",
}
files = {"file": ("name.txt", open(path, "rb"))}

with mock.patch("os.urandom", return_value=os.urandom(16)):
boundary = os.urandom(16).hex()

headers, stream = encode_request(data=data, files=files)
assert isinstance(stream, typing.Iterable)

content = (
'--{0}\r\nContent-Disposition: form-data; name="a"\r\n\r\n1\r\n'
'--{0}\r\nContent-Disposition: form-data; name="b"\r\n\r\nC\r\n'
'--{0}\r\nContent-Disposition: form-data; name="c"\r\n\r\n11\r\n'
'--{0}\r\nContent-Disposition: form-data; name="c"\r\n\r\n22\r\n'
'--{0}\r\nContent-Disposition: form-data; name="c"\r\n\r\n33\r\n'
'--{0}\r\nContent-Disposition: form-data; name="d"\r\n\r\n\r\n'
'--{0}\r\nContent-Disposition: form-data; name="e"\r\n\r\ntrue\r\n'
'--{0}\r\nContent-Disposition: form-data; name="f"\r\n\r\n\r\n'
'--{0}\r\nContent-Disposition: form-data; name="file";'
' filename="name.txt"\r\n'
"Content-Type: text/plain\r\n\r\n<file content>\r\n"
"--{0}--\r\n"
"".format(boundary).encode("ascii")
)
assert headers == {
"Content-Type": f"multipart/form-data; boundary={boundary}",
"Content-Length": str(len(content)),
}
assert content == b"".join(stream)
with open(path, "rb") as input_file:
files = {"file": ("name.txt", input_file)}

with mock.patch("os.urandom", return_value=os.urandom(16)):
boundary = os.urandom(16).hex()

headers, stream = encode_request(data=data, files=files)
assert isinstance(stream, typing.Iterable)

content = (
'--{0}\r\nContent-Disposition: form-data; name="a"\r\n\r\n1\r\n'
'--{0}\r\nContent-Disposition: form-data; name="b"\r\n\r\nC\r\n'
'--{0}\r\nContent-Disposition: form-data; name="c"\r\n\r\n11\r\n'
'--{0}\r\nContent-Disposition: form-data; name="c"\r\n\r\n22\r\n'
'--{0}\r\nContent-Disposition: form-data; name="c"\r\n\r\n33\r\n'
'--{0}\r\nContent-Disposition: form-data; name="d"\r\n\r\n\r\n'
'--{0}\r\nContent-Disposition: form-data; name="e"\r\n\r\ntrue\r\n'
'--{0}\r\nContent-Disposition: form-data; name="f"\r\n\r\n\r\n'
'--{0}\r\nContent-Disposition: form-data; name="file";'
' filename="name.txt"\r\n'
"Content-Type: text/plain\r\n\r\n<file content>\r\n"
"--{0}--\r\n"
"".format(boundary).encode("ascii")
)
assert headers == {
"Content-Type": f"multipart/form-data; boundary={boundary}",
"Content-Length": str(len(content)),
}
assert content == b"".join(stream)


def test_multipart_encode_unicode_file_contents() -> None:
Expand Down