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
Added AnyIO support #169
Merged
Merged
Added AnyIO support #169
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
5bee158
Added anyio backend
agronholm 54be236
Merge branch 'master' into anyio
florimondmanca 22109ce
Added anyio backend
agronholm 3d08a8f
Merge branch 'anyio' of github.com:agronholm/httpcore into anyio
agronholm e59e333
Update unasync.py
agronholm d9a1b7e
Update unasync.py
agronholm 8b7714b
Merge branch 'master' into anyio
agronholm File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
import select | ||
from ssl import SSLContext | ||
from typing import Optional | ||
|
||
import anyio.abc | ||
from anyio import BrokenResourceError, EndOfStream | ||
from anyio.abc import ByteStream, SocketAttribute | ||
from anyio.streams.tls import TLSAttribute, TLSStream | ||
|
||
from .._exceptions import ( | ||
CloseError, | ||
ConnectError, | ||
ConnectTimeout, | ||
ReadError, | ||
ReadTimeout, | ||
WriteError, | ||
WriteTimeout, | ||
) | ||
from .._types import TimeoutDict | ||
from .base import AsyncBackend, AsyncLock, AsyncSemaphore, AsyncSocketStream | ||
|
||
|
||
class SocketStream(AsyncSocketStream): | ||
def __init__(self, stream: ByteStream) -> None: | ||
self.stream = stream | ||
self.read_lock = anyio.create_lock() | ||
self.write_lock = anyio.create_lock() | ||
|
||
def get_http_version(self) -> str: | ||
alpn_protocol = self.stream.extra(TLSAttribute.alpn_protocol, None) | ||
return "HTTP/2" if alpn_protocol == "h2" else "HTTP/1.1" | ||
|
||
async def start_tls( | ||
self, | ||
hostname: bytes, | ||
ssl_context: SSLContext, | ||
timeout: TimeoutDict, | ||
) -> "SocketStream": | ||
connect_timeout = timeout.get("connect") | ||
try: | ||
async with anyio.fail_after(connect_timeout): | ||
ssl_stream = await TLSStream.wrap( | ||
self.stream, | ||
ssl_context=ssl_context, | ||
hostname=hostname.decode("ascii"), | ||
) | ||
except TimeoutError: | ||
raise ConnectTimeout from None | ||
except BrokenResourceError as exc: | ||
raise ConnectError from exc | ||
|
||
return SocketStream(ssl_stream) | ||
|
||
async def read(self, n: int, timeout: TimeoutDict) -> bytes: | ||
read_timeout = timeout.get("read") | ||
async with self.read_lock: | ||
try: | ||
async with anyio.fail_after(read_timeout): | ||
return await self.stream.receive(n) | ||
except TimeoutError: | ||
raise ReadTimeout from None | ||
except BrokenResourceError as exc: | ||
raise ReadError from exc | ||
except EndOfStream: | ||
raise ReadError("Server disconnected while attempting read") from None | ||
|
||
async def write(self, data: bytes, timeout: TimeoutDict) -> None: | ||
if not data: | ||
return | ||
|
||
write_timeout = timeout.get("write") | ||
async with self.write_lock: | ||
try: | ||
async with anyio.fail_after(write_timeout): | ||
return await self.stream.send(data) | ||
except TimeoutError: | ||
raise WriteTimeout from None | ||
except BrokenResourceError as exc: | ||
raise WriteError from exc | ||
|
||
async def aclose(self) -> None: | ||
async with self.write_lock: | ||
try: | ||
await self.stream.aclose() | ||
except BrokenResourceError as exc: | ||
raise CloseError from exc | ||
|
||
def is_connection_dropped(self) -> bool: | ||
raw_socket = self.stream.extra(SocketAttribute.raw_socket) | ||
rready, _wready, _xready = select.select([raw_socket], [], [], 0) | ||
return bool(rready) | ||
|
||
|
||
class Lock(AsyncLock): | ||
def __init__(self) -> None: | ||
self._lock = anyio.create_lock() | ||
|
||
async def release(self) -> None: | ||
await self._lock.release() | ||
|
||
async def acquire(self) -> None: | ||
await self._lock.acquire() | ||
|
||
|
||
class Semaphore(AsyncSemaphore): | ||
def __init__(self, max_value: int, exc_class: type): | ||
self.max_value = max_value | ||
self.exc_class = exc_class | ||
|
||
@property | ||
def semaphore(self) -> anyio.abc.Semaphore: | ||
if not hasattr(self, "_semaphore"): | ||
self._semaphore = anyio.create_semaphore(self.max_value) | ||
return self._semaphore | ||
|
||
async def acquire(self, timeout: float = None) -> None: | ||
async with anyio.move_on_after(timeout): | ||
await self.semaphore.acquire() | ||
return | ||
|
||
raise self.exc_class() | ||
|
||
async def release(self) -> None: | ||
await self.semaphore.release() | ||
|
||
|
||
class AnyIOBackend(AsyncBackend): | ||
async def open_tcp_stream( | ||
self, | ||
hostname: bytes, | ||
port: int, | ||
ssl_context: Optional[SSLContext], | ||
timeout: TimeoutDict, | ||
*, | ||
local_address: Optional[str], | ||
) -> AsyncSocketStream: | ||
connect_timeout = timeout.get("connect") | ||
unicode_host = hostname.decode("utf-8") | ||
|
||
try: | ||
async with anyio.fail_after(connect_timeout): | ||
stream: anyio.abc.ByteStream | ||
stream = await anyio.connect_tcp( | ||
unicode_host, port, local_host=local_address | ||
) | ||
if ssl_context: | ||
stream = await TLSStream.wrap( | ||
stream, | ||
hostname=unicode_host, | ||
ssl_context=ssl_context, | ||
standard_compatible=False, | ||
) | ||
except TimeoutError: | ||
raise ConnectTimeout from None | ||
except BrokenResourceError as exc: | ||
raise ConnectError from exc | ||
|
||
return SocketStream(stream=stream) | ||
|
||
async def open_uds_stream( | ||
self, | ||
path: str, | ||
hostname: bytes, | ||
ssl_context: Optional[SSLContext], | ||
timeout: TimeoutDict, | ||
) -> AsyncSocketStream: | ||
connect_timeout = timeout.get("connect") | ||
unicode_host = hostname.decode("utf-8") | ||
|
||
try: | ||
async with anyio.fail_after(connect_timeout): | ||
stream: anyio.abc.ByteStream = await anyio.connect_unix(path) | ||
if ssl_context: | ||
stream = await TLSStream.wrap( | ||
stream, | ||
hostname=unicode_host, | ||
ssl_context=ssl_context, | ||
standard_compatible=False, | ||
) | ||
except TimeoutError: | ||
raise ConnectTimeout from None | ||
except BrokenResourceError as exc: | ||
raise ConnectError from exc | ||
|
||
return SocketStream(stream=stream) | ||
|
||
def create_lock(self) -> AsyncLock: | ||
return Lock() | ||
|
||
def create_semaphore(self, max_value: int, exc_class: type) -> AsyncSemaphore: | ||
return Semaphore(max_value, exc_class=exc_class) | ||
|
||
async def time(self) -> float: | ||
return await anyio.current_time() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might want to look into #182 some more before deciding that this trick is worth committing to. 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense for us to approach it in this order?...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure yup, #185 isn't blocking us from merging this, just wanted to clarify that this select() approach needed a bit of refining. :)