Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: encode/httpcore
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 1.0.0
Choose a base ref
...
head repository: encode/httpcore
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 1.0.1
Choose a head ref
  • 10 commits
  • 7 files changed
  • 5 contributors

Commits on Oct 12, 2023

  1. Ensure pool timeout is applied, even after attempts leading to Connec…

    …tionNotAvailable (#823)
    
    * add failing tests
    
    * fix pooltimeout
    
    * Revert "add failing tests"
    
    This reverts commit cacc248.
    
    * mark `if timeout < 0` as not covered, since the test is reverted
    
    * Update CHANGELOG.md
    valsteen authored Oct 12, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    f8ff1c4 View commit details

Commits on Oct 14, 2023

  1. typo in changelog (#827)

    stonebig authored Oct 14, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    7fce352 View commit details

Commits on Nov 1, 2023

  1. Bump pytest from 7.4.0 to 7.4.3 (#836)

    Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.0 to 7.4.3.
    - [Release notes](https://github.com/pytest-dev/pytest/releases)
    - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
    - [Commits](pytest-dev/pytest@7.4.0...7.4.3)
    
    ---
    updated-dependencies:
    - dependency-name: pytest
      dependency-type: direct:production
      update-type: version-update:semver-patch
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Nov 1, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    f8dcb5f View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    e039dd1 View commit details
  3. Bump black from 23.7.0 to 23.10.1 (#835)

    Bumps [black](https://github.com/psf/black) from 23.7.0 to 23.10.1.
    - [Release notes](https://github.com/psf/black/releases)
    - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
    - [Commits](psf/black@23.7.0...23.10.1)
    
    ---
    updated-dependencies:
    - dependency-name: black
      dependency-type: direct:production
      update-type: version-update:semver-minor
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Nov 1, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    29f172d View commit details
  4. Bump trio-typing from 0.8.0 to 0.9.0 (#838)

    Bumps [trio-typing](https://github.com/python-trio/trio-typing) from 0.8.0 to 0.9.0.
    - [Commits](python-trio/trio-typing@v0.8.0...v0.9.0)
    
    ---
    updated-dependencies:
    - dependency-name: trio-typing
      dependency-type: direct:production
      update-type: version-update:semver-minor
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Nov 1, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    aea3a6b View commit details
  5. Bump mkdocs-material from 9.4.2 to 9.4.7 (#837)

    Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.4.2 to 9.4.7.
    - [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
    - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
    - [Commits](squidfunk/mkdocs-material@9.4.2...9.4.7)
    
    ---
    updated-dependencies:
    - dependency-name: mkdocs-material
      dependency-type: direct:production
      update-type: version-update:semver-patch
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Nov 1, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    d664b32 View commit details

Commits on Nov 2, 2023

  1. Fix synchronous TLS-in-TLS streams (#840)

    * Fix synchronous TLS-in-TLS
    
    * changelog
    karpetrosyan authored Nov 2, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    66aa1c1 View commit details
  2. Raise a neater RuntimeError when the correct async deps are not insta…

    …lled. (#826)
    
    * Raise a neater RuntimeError when the correct async deps are not installed
    
    * Run scripts/unasync
    
    * Update CHANGELOG
    tomchristie authored Nov 2, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    08b3eee View commit details

Commits on Nov 3, 2023

  1. Version 1.0.1 (#842)

    * Version 1.0.1
    
    * Update CHANGELOG.md
    tomchristie authored Nov 3, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    9c818e9 View commit details
Showing with 72 additions and 80 deletions.
  1. +7 −1 CHANGELOG.md
  2. +1 −1 httpcore/__init__.py
  3. +17 −3 httpcore/_async/connection_pool.py
  4. +0 −6 httpcore/_backends/sync.py
  5. +17 −3 httpcore/_sync/connection_pool.py
  6. +25 −61 httpcore/_synchronization.py
  7. +5 −5 requirements.txt
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -4,7 +4,13 @@ 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/).

## 1.0.0 (November 6th, 2023)
## 1.0.1 (November 3rd, 2023)

- Fix pool timeout to account for the total time spent retrying. (#823)
- Raise a neater RuntimeError when the correct async deps are not installed. (#826)
- Add support for synchronous TLS-in-TLS streams. (#840)

## 1.0.0 (October 6th, 2023)

From version 1.0 our async support is now optional, as the package has minimal dependencies by default.

2 changes: 1 addition & 1 deletion httpcore/__init__.py
Original file line number Diff line number Diff line change
@@ -130,7 +130,7 @@ def __init__(self, *args, **kwargs): # type: ignore
"WriteError",
]

__version__ = "1.0.0"
__version__ = "1.0.1"


__locals = locals()
20 changes: 17 additions & 3 deletions httpcore/_async/connection_pool.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import ssl
import sys
import time
from types import TracebackType
from typing import AsyncIterable, AsyncIterator, Iterable, List, Optional, Type

from .._backends.auto import AutoBackend
from .._backends.base import SOCKET_OPTION, AsyncNetworkBackend
from .._exceptions import ConnectionNotAvailable, UnsupportedProtocol
from .._exceptions import ConnectionNotAvailable, PoolTimeout, UnsupportedProtocol
from .._models import Origin, Request, Response
from .._synchronization import AsyncEvent, AsyncLock, AsyncShieldCancellation
from .connection import AsyncHTTPConnection
@@ -220,15 +221,20 @@ async def handle_async_request(self, request: Request) -> Response:
)

status = RequestStatus(request)
timeouts = request.extensions.get("timeout", {})
timeout = timeouts.get("pool", None)

if timeout is not None:
deadline = time.monotonic() + timeout
else:
deadline = float("inf")

async with self._pool_lock:
self._requests.append(status)
await self._close_expired_connections()
await self._attempt_to_acquire_connection(status)

while True:
timeouts = request.extensions.get("timeout", {})
timeout = timeouts.get("pool", None)
try:
connection = await status.wait_for_connection(timeout=timeout)
except BaseException as exc:
@@ -263,6 +269,10 @@ async def handle_async_request(self, request: Request) -> Response:
else:
break

timeout = deadline - time.monotonic()
if timeout < 0:
raise PoolTimeout # pragma: nocover

# When we return the response, we wrap the stream in a special class
# that handles notifying the connection pool once the response
# has been released.
@@ -316,6 +326,10 @@ async def aclose(self) -> None:
self._requests = []

async def __aenter__(self) -> "AsyncConnectionPool":
# Acquiring the pool lock here ensures that we have the
# correct dependencies installed as early as possible.
async with self._pool_lock:
pass
return self

async def __aexit__(
6 changes: 0 additions & 6 deletions httpcore/_backends/sync.py
Original file line number Diff line number Diff line change
@@ -145,12 +145,6 @@ def start_tls(
server_hostname: typing.Optional[str] = None,
timeout: typing.Optional[float] = None,
) -> NetworkStream:
if isinstance(self._sock, ssl.SSLSocket): # pragma: no cover
raise RuntimeError(
"Attempted to add a TLS layer on top of the existing "
"TLS stream, which is not supported by httpcore package"
)

exc_map: ExceptionMapping = {
socket.timeout: ConnectTimeout,
OSError: ConnectError,
20 changes: 17 additions & 3 deletions httpcore/_sync/connection_pool.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import ssl
import sys
import time
from types import TracebackType
from typing import Iterable, Iterator, Iterable, List, Optional, Type

from .._backends.sync import SyncBackend
from .._backends.base import SOCKET_OPTION, NetworkBackend
from .._exceptions import ConnectionNotAvailable, UnsupportedProtocol
from .._exceptions import ConnectionNotAvailable, PoolTimeout, UnsupportedProtocol
from .._models import Origin, Request, Response
from .._synchronization import Event, Lock, ShieldCancellation
from .connection import HTTPConnection
@@ -220,15 +221,20 @@ def handle_request(self, request: Request) -> Response:
)

status = RequestStatus(request)
timeouts = request.extensions.get("timeout", {})
timeout = timeouts.get("pool", None)

if timeout is not None:
deadline = time.monotonic() + timeout
else:
deadline = float("inf")

with self._pool_lock:
self._requests.append(status)
self._close_expired_connections()
self._attempt_to_acquire_connection(status)

while True:
timeouts = request.extensions.get("timeout", {})
timeout = timeouts.get("pool", None)
try:
connection = status.wait_for_connection(timeout=timeout)
except BaseException as exc:
@@ -263,6 +269,10 @@ def handle_request(self, request: Request) -> Response:
else:
break

timeout = deadline - time.monotonic()
if timeout < 0:
raise PoolTimeout # pragma: nocover

# When we return the response, we wrap the stream in a special class
# that handles notifying the connection pool once the response
# has been released.
@@ -316,6 +326,10 @@ def close(self) -> None:
self._requests = []

def __enter__(self) -> "ConnectionPool":
# Acquiring the pool lock here ensures that we have the
# correct dependencies installed as early as possible.
with self._pool_lock:
pass
return self

def __exit__(
86 changes: 25 additions & 61 deletions httpcore/_synchronization.py
Original file line number Diff line number Diff line change
@@ -24,13 +24,23 @@ def current_async_library() -> str:
try:
import sniffio
except ImportError: # pragma: nocover
return "asyncio"

environment = sniffio.current_async_library()
environment = "asyncio"
else:
environment = sniffio.current_async_library()

if environment not in ("asyncio", "trio"): # pragma: nocover
raise RuntimeError("Running under an unsupported async environment.")

if environment == "asyncio" and anyio is None: # pragma: nocover
raise RuntimeError(
"Running with asyncio requires installation of 'httpcore[asyncio]'."
)

if environment == "trio" and trio is None: # pragma: nocover
raise RuntimeError(
"Running with trio requires installation of 'httpcore[trio]'."
)

return environment


@@ -45,16 +55,8 @@ def setup(self) -> None:
"""
self._backend = current_async_library()
if self._backend == "trio":
if trio is None: # pragma: nocover
raise RuntimeError(
"Running with trio requires installation of 'httpcore[trio]'."
)
self._trio_lock = trio.Lock()
else:
if anyio is None: # pragma: nocover
raise RuntimeError(
"Running with asyncio requires installation of 'httpcore[asyncio]'."
)
elif self._backend == "asyncio":
self._anyio_lock = anyio.Lock()

async def __aenter__(self) -> "AsyncLock":
@@ -63,7 +65,7 @@ async def __aenter__(self) -> "AsyncLock":

if self._backend == "trio":
await self._trio_lock.acquire()
else:
elif self._backend == "asyncio":
await self._anyio_lock.acquire()

return self
@@ -76,7 +78,7 @@ async def __aexit__(
) -> None:
if self._backend == "trio":
self._trio_lock.release()
else:
elif self._backend == "asyncio":
self._anyio_lock.release()


@@ -91,16 +93,8 @@ def setup(self) -> None:
"""
self._backend = current_async_library()
if self._backend == "trio":
if trio is None: # pragma: nocover
raise RuntimeError(
"Running with trio requires installation of 'httpcore[trio]'."
)
self._trio_event = trio.Event()
else:
if anyio is None: # pragma: nocover
raise RuntimeError(
"Running with asyncio requires installation of 'httpcore[asyncio]'."
)
elif self._backend == "asyncio":
self._anyio_event = anyio.Event()

def set(self) -> None:
@@ -109,30 +103,20 @@ def set(self) -> None:

if self._backend == "trio":
self._trio_event.set()
else:
elif self._backend == "asyncio":
self._anyio_event.set()

async def wait(self, timeout: Optional[float] = None) -> None:
if not self._backend:
self.setup()

if self._backend == "trio":
if trio is None: # pragma: nocover
raise RuntimeError(
"Running with trio requires installation of 'httpcore[trio]'."
)

trio_exc_map: ExceptionMapping = {trio.TooSlowError: PoolTimeout}
timeout_or_inf = float("inf") if timeout is None else timeout
with map_exceptions(trio_exc_map):
with trio.fail_after(timeout_or_inf):
await self._trio_event.wait()
else:
if anyio is None: # pragma: nocover
raise RuntimeError(
"Running with asyncio requires installation of 'httpcore[asyncio]'."
)

elif self._backend == "asyncio":
anyio_exc_map: ExceptionMapping = {TimeoutError: PoolTimeout}
with map_exceptions(anyio_exc_map):
with anyio.fail_after(timeout):
@@ -151,20 +135,10 @@ def setup(self) -> None:
"""
self._backend = current_async_library()
if self._backend == "trio":
if trio is None: # pragma: nocover
raise RuntimeError(
"Running with trio requires installation of 'httpcore[trio]'."
)

self._trio_semaphore = trio.Semaphore(
initial_value=self._bound, max_value=self._bound
)
else:
if anyio is None: # pragma: nocover
raise RuntimeError(
"Running with asyncio requires installation of 'httpcore[asyncio]'."
)

elif self._backend == "asyncio":
self._anyio_semaphore = anyio.Semaphore(
initial_value=self._bound, max_value=self._bound
)
@@ -175,13 +149,13 @@ async def acquire(self) -> None:

if self._backend == "trio":
await self._trio_semaphore.acquire()
else:
elif self._backend == "asyncio":
await self._anyio_semaphore.acquire()

async def release(self) -> None:
if self._backend == "trio":
self._trio_semaphore.release()
else:
elif self._backend == "asyncio":
self._anyio_semaphore.release()


@@ -201,24 +175,14 @@ def __init__(self) -> None:
self._backend = current_async_library()

if self._backend == "trio":
if trio is None: # pragma: nocover
raise RuntimeError(
"Running with trio requires installation of 'httpcore[trio]'."
)

self._trio_shield = trio.CancelScope(shield=True)
else:
if anyio is None: # pragma: nocover
raise RuntimeError(
"Running with asyncio requires installation of 'httpcore[asyncio]'."
)

elif self._backend == "asyncio":
self._anyio_shield = anyio.CancelScope(shield=True)

def __enter__(self) -> "AsyncShieldCancellation":
if self._backend == "trio":
self._trio_shield.__enter__()
else:
elif self._backend == "asyncio":
self._anyio_shield.__enter__()
return self

@@ -230,7 +194,7 @@ def __exit__(
) -> None:
if self._backend == "trio":
self._trio_shield.__exit__(exc_type, exc_value, traceback)
else:
elif self._backend == "asyncio":
self._anyio_shield.__exit__(exc_type, exc_value, traceback)


10 changes: 5 additions & 5 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -3,8 +3,8 @@
# Docs
mkdocs==1.5.3
mkdocs-autorefs==0.5.0
mkdocs-material==9.4.2
mkdocs-material-extensions==1.2
mkdocs-material==9.4.7
mkdocs-material-extensions==1.3
mkdocstrings[python-legacy]==0.22.0
jinja2==3.1.2

@@ -13,13 +13,13 @@ build==1.0.3
twine

# Tests & Linting
black==23.7.0
black==23.10.1
coverage[toml]==7.3.0
ruff==0.0.291
mypy==1.5.1
trio-typing==0.8.0
trio-typing==0.9.0
types-certifi==2021.10.8.3
pytest==7.4.0
pytest==7.4.3
pytest-httpbin==2.0.0
pytest-trio==0.7.0
werkzeug<2.1 # See: https://github.com/postmanlabs/httpbin/issues/673