From 7039da15909a40d7aa8fae5036196b3acba15c7b Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 4 Jan 2022 15:34:01 +0900 Subject: [PATCH 1/5] Add typings --- .gitignore | 1 + Makefile | 1 + mypy.ini | 4 + pylintrc | 6 +- requirements-dev.in | 2 + requirements-dev.txt | 50 +++-- setup.py | 3 + trio_websocket/__init__.py | 36 ++-- trio_websocket/_impl.py | 425 +++++++++++++++++++++++++------------ trio_websocket/py.typed | 0 10 files changed, 353 insertions(+), 175 deletions(-) create mode 100644 mypy.ini create mode 100644 trio_websocket/py.typed diff --git a/.gitignore b/.gitignore index c7c6cf2..c00764f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist examples/fake.* trio_websocket.egg-info venv +.mypy_cache/ diff --git a/Makefile b/Makefile index b496fff..935d27c 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ test: lint: $(PYTHON) -m pylint trio_websocket/ tests/ autobahn/ examples/ + $(PYTHON) -m mypy trio_websocket/ publish: rm -fr build dist .egg trio_websocket.egg-info diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..bfd9eee --- /dev/null +++ b/mypy.ini @@ -0,0 +1,4 @@ +[mypy] +strict = True +python_version = 3.6 +show_error_codes = True diff --git a/pylintrc b/pylintrc index fea31de..b252b14 100644 --- a/pylintrc +++ b/pylintrc @@ -21,7 +21,11 @@ disable=duplicate-code, unused-argument, unused-variable, wrong-spelling-in-comment, - wrong-spelling-in-docstring + wrong-spelling-in-docstring, + # needed for explicit re-exports + useless-import-alias, + # false positives with generics + unsubscriptable-object [REPORTS] score=no diff --git a/requirements-dev.in b/requirements-dev.in index 6a288ca..41190a2 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -11,3 +11,5 @@ sphinx_rtd_theme trio>=0.14.0 trustme twine +mypy +trio-typing diff --git a/requirements-dev.txt b/requirements-dev.txt index 0a07647..fb2e290 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -43,8 +43,10 @@ coverage[toml]==6.2 # pytest-cov coveralls==3.3.1 # via -r requirements-dev.in -cryptography==3.3.2 - # via trustme +cryptography==3.2.1 + # via + # secretstorage + # trustme docopt==0.6.2 # via coveralls docutils==0.17.1 @@ -75,7 +77,11 @@ iniconfig==1.1.1 # via pytest isort==5.10.1 # via pylint -jinja2==3.0.3 +jeepney==0.7.1 + # via + # keyring + # secretstorage +jinja2==2.11.2 # via sphinx keyring==23.4.1 # via twine @@ -85,7 +91,13 @@ markupsafe==2.0.1 # via jinja2 mccabe==0.6.1 # via pylint -outcome==1.1.0 +mypy-extensions==0.4.3 + # via + # mypy + # trio-typing +mypy==0.910 + # via -r requirements-dev.in +outcome==1.0.1 # via # pytest-trio # trio @@ -137,10 +149,15 @@ requests==2.27.1 # twine requests-toolbelt==0.9.1 # via twine -rfc3986==1.5.0 - # via twine -six==1.16.0 - # via bleach +secretstorage==3.3.1 + # via keyring +six==1.15.0 + # via + # astroid + # bleach + # cryptography + # packaging + # readme-renderer sniffio==1.2.0 # via trio snowballstemmer==2.2.0 @@ -170,6 +187,8 @@ sphinxcontrib-trio==1.1.2 # via -r requirements-dev.in toml==0.10.2 # via + # mypy + # pep517 # pylint # pytest tomli==1.2.3 @@ -178,24 +197,23 @@ tomli==1.2.3 # pep517 tqdm==4.62.3 # via twine -trio==0.19.0 +trio-typing==0.6.0 + # via -r requirements-dev.in +trio==0.17.0 # via # -r requirements-dev.in # pytest-trio + # trio-typing # trio-websocket (setup.py) trustme==0.9.0 # via -r requirements-dev.in twine==3.7.1 # via -r requirements-dev.in -typed-ast==1.5.1 - # via astroid typing-extensions==4.0.1 # via - # astroid - # immutables - # importlib-metadata - # pylint -urllib3==1.26.8 + # mypy + # trio-typing +urllib3==1.26.2 # via requests webencodings==0.5.1 # via bleach diff --git a/setup.py b/setup.py index ab84d41..7d325ac 100644 --- a/setup.py +++ b/setup.py @@ -34,6 +34,7 @@ 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', + 'Typing :: Typed', ], python_requires=">=3.6", keywords='websocket client server trio', @@ -43,6 +44,8 @@ 'trio>=0.11', 'wsproto>=0.14', ], + package_data={'trio_websocket': ['py.typed']}, + zip_safe=False, project_urls={ 'Bug Reports': 'https://github.com/HyperionGray/trio-websocket/issues', 'Source': 'https://github.com/HyperionGray/trio-websocket', diff --git a/trio_websocket/__init__.py b/trio_websocket/__init__.py index 82ca0ae..94cb559 100644 --- a/trio_websocket/__init__.py +++ b/trio_websocket/__init__.py @@ -1,20 +1,20 @@ from ._impl import ( - CloseReason, - ConnectionClosed, - ConnectionRejected, - ConnectionTimeout, - connect_websocket, - connect_websocket_url, - DisconnectionTimeout, - Endpoint, - HandshakeError, - open_websocket, - open_websocket_url, - WebSocketConnection, - WebSocketRequest, - WebSocketServer, - wrap_client_stream, - wrap_server_stream, - serve_websocket, + CloseReason as CloseReason, + ConnectionClosed as ConnectionClosed, + ConnectionRejected as ConnectionRejected, + ConnectionTimeout as ConnectionTimeout, + connect_websocket as connect_websocket, + connect_websocket_url as connect_websocket_url, + DisconnectionTimeout as DisconnectionTimeout, + Endpoint as Endpoint, + HandshakeError as HandshakeError, + open_websocket as open_websocket, + open_websocket_url as open_websocket_url, + WebSocketConnection as WebSocketConnection, + WebSocketRequest as WebSocketRequest, + WebSocketServer as WebSocketServer, + wrap_client_stream as wrap_client_stream, + wrap_server_stream as wrap_server_stream, + serve_websocket as serve_websocket, ) -from ._version import __version__ +from ._version import __version__ as __version__ diff --git a/trio_websocket/_impl.py b/trio_websocket/_impl.py index 3199e09..d23b8da 100644 --- a/trio_websocket/_impl.py +++ b/trio_websocket/_impl.py @@ -7,6 +7,9 @@ import random import ssl import struct +import types +import typing +from typing import Optional, Union import urllib.parse from typing import List @@ -29,6 +32,8 @@ ) import wsproto.utilities +if typing.TYPE_CHECKING: + from trio_typing import TaskStatus CONN_TIMEOUT = 60 # default connect & disconnect timeout, in seconds MESSAGE_QUEUE_SIZE = 1 @@ -36,6 +41,8 @@ RECEIVE_BYTES = 4 * 2 ** 10 # 4 KiB logger = logging.getLogger('trio-websocket') +T = typing.TypeVar('T') + class _preserve_current_exception: """A context manager which should surround an ``__exit__`` or @@ -49,17 +56,22 @@ class _preserve_current_exception: """ __slots__ = ("_armed",) - def __init__(self): + def __init__(self) -> None: self._armed = False - def __enter__(self): + def __enter__(self) -> None: self._armed = sys.exc_info()[1] is not None - def __exit__(self, ty, value, tb): + def __exit__( + self, + ty: Optional[typing.Type[BaseException]], + value: Optional[BaseException], + tb: Optional[types.TracebackType] + ) -> Optional[bool]: if value is None or not self._armed: return False - def remove_cancels(exc): + def remove_cancels(exc: BaseException) -> Optional[BaseException]: return None if isinstance(exc, trio.Cancelled) else exc return trio.MultiError.filter(remove_cancels, value) is None @@ -124,9 +136,18 @@ async def open_websocket(host, port, resource, *, use_ssl, subprotocols=None, raise DisconnectionTimeout from None -async def connect_websocket(nursery, host, port, resource, *, use_ssl, - subprotocols=None, extra_headers=None, - message_queue_size=MESSAGE_QUEUE_SIZE, max_message_size=MAX_MESSAGE_SIZE): +async def connect_websocket( + nursery: trio.Nursery, + host: str, + port: int, + resource: str, + *, + use_ssl: Union[bool, ssl.SSLContext], + subprotocols: Optional[typing.Iterable[str]] = None, + extra_headers: Optional[typing.List[typing.Tuple[bytes, bytes]]] = None, + message_queue_size: int = MESSAGE_QUEUE_SIZE, + max_message_size: int = MAX_MESSAGE_SIZE +) -> 'WebSocketConnection': ''' Return an open WebSocket client connection to a host. @@ -157,7 +178,7 @@ async def connect_websocket(nursery, host, port, resource, *, use_ssl, :rtype: WebSocketConnection ''' if use_ssl is True: - ssl_context = ssl.create_default_context() + ssl_context: Optional[ssl.SSLContext] = ssl.create_default_context() elif use_ssl is False: ssl_context = None elif isinstance(use_ssl, ssl.SSLContext): @@ -168,7 +189,7 @@ async def connect_websocket(nursery, host, port, resource, *, use_ssl, logger.debug('Connecting to ws%s://%s:%d%s', '' if ssl_context is None else 's', host, port, resource) if ssl_context is None: - stream = await trio.open_tcp_stream(host, port) + stream: trio.abc.Stream = await trio.open_tcp_stream(host, port) else: stream = await trio.open_ssl_over_tcp_stream(host, port, ssl_context=ssl_context, https_compatible=True) @@ -188,10 +209,17 @@ async def connect_websocket(nursery, host, port, resource, *, use_ssl, return connection -def open_websocket_url(url, ssl_context=None, *, subprotocols=None, - extra_headers=None, - message_queue_size=MESSAGE_QUEUE_SIZE, max_message_size=MAX_MESSAGE_SIZE, - connect_timeout=CONN_TIMEOUT, disconnect_timeout=CONN_TIMEOUT): +def open_websocket_url( + url: str, + ssl_context: Optional[ssl.SSLContext] = None, + *, + subprotocols: Optional[typing.Iterator[str]] = None, + extra_headers: Optional[typing.List[typing.Tuple[bytes, bytes]]] = None, + message_queue_size: int = MESSAGE_QUEUE_SIZE, + max_message_size: int = MAX_MESSAGE_SIZE, + connect_timeout: float = CONN_TIMEOUT, + disconnect_timeout: float = CONN_TIMEOUT +) -> typing.AsyncContextManager['WebSocketConnection']: ''' Open a WebSocket client connection to a URL. @@ -222,17 +250,24 @@ def open_websocket_url(url, ssl_context=None, *, subprotocols=None, client-side timeout (:exc:`ConnectionTimeout`, :exc:`DisconnectionTimeout`), or server rejection (:exc:`ConnectionRejected`) during handshakes. ''' - host, port, resource, ssl_context = _url_to_host(url, ssl_context) - return open_websocket(host, port, resource, use_ssl=ssl_context, + host, port, resource, ssl_context_ = _url_to_host(url, ssl_context) + return open_websocket(host, port, resource, use_ssl=ssl_context_, subprotocols=subprotocols, extra_headers=extra_headers, message_queue_size=message_queue_size, max_message_size=max_message_size, connect_timeout=connect_timeout, disconnect_timeout=disconnect_timeout) -async def connect_websocket_url(nursery, url, ssl_context=None, *, - subprotocols=None, extra_headers=None, - message_queue_size=MESSAGE_QUEUE_SIZE, max_message_size=MAX_MESSAGE_SIZE): +async def connect_websocket_url( + nursery: trio.Nursery, + url: str, + ssl_context: Optional[ssl.SSLContext] = None, + *, + subprotocols: Optional[typing.Iterable[str]] = None, + extra_headers: Optional[typing.List[typing.Tuple[bytes, bytes]]] = None, + message_queue_size: int = MESSAGE_QUEUE_SIZE, + max_message_size: int = MAX_MESSAGE_SIZE +) -> 'WebSocketConnection': ''' Return an open WebSocket client connection to a URL. @@ -259,14 +294,17 @@ async def connect_websocket_url(nursery, url, ssl_context=None, *, then the connection is closed with code 1009 (Message Too Big). :rtype: WebSocketConnection ''' - host, port, resource, ssl_context = _url_to_host(url, ssl_context) + host, port, resource, ssl_context_ = _url_to_host(url, ssl_context) return await connect_websocket(nursery, host, port, resource, - use_ssl=ssl_context, subprotocols=subprotocols, + use_ssl=ssl_context_, subprotocols=subprotocols, extra_headers=extra_headers, message_queue_size=message_queue_size, max_message_size=max_message_size) -def _url_to_host(url, ssl_context): +def _url_to_host( + url: str, + ssl_context: Optional[ssl.SSLContext] +) -> typing.Tuple[str, int, str, Union[bool, ssl.SSLContext]]: ''' Convert a WebSocket URL to a (host,port,resource) tuple. @@ -278,19 +316,22 @@ def _url_to_host(url, ssl_context): :type ssl_context: ssl.SSLContext or None :returns: A tuple of ``(host, port, resource, ssl_context)``. ''' + ssl_context_: Union[bool, None, ssl.SSLContext] = ssl_context url = str(url) # For backward compat with isinstance(url, yarl.URL). parts = urllib.parse.urlsplit(url) if parts.scheme not in ('ws', 'wss'): raise ValueError('WebSocket URL scheme must be "ws:" or "wss:"') - if ssl_context is None: - ssl_context = parts.scheme == 'wss' + if ssl_context_ is None: + ssl_context_ = parts.scheme == 'wss' elif parts.scheme == 'ws': raise ValueError('SSL context must be None for ws: URL scheme') host = parts.hostname + if host is None: + raise ValueError('WebSocket URL must contain a host') if parts.port is not None: port = parts.port else: - port = 443 if ssl_context else 80 + port = 443 if ssl_context_ else 80 path_qs = parts.path # RFC 7230, Section 5.3.1: # If the target URI's path component is empty, the client MUST @@ -299,12 +340,19 @@ def _url_to_host(url, ssl_context): path_qs = '/' if '?' in url: path_qs += '?' + parts.query - return host, port, path_qs, ssl_context - - -async def wrap_client_stream(nursery, stream, host, resource, *, - subprotocols=None, extra_headers=None, - message_queue_size=MESSAGE_QUEUE_SIZE, max_message_size=MAX_MESSAGE_SIZE): + return host, port, path_qs, ssl_context_ + + +async def wrap_client_stream( + nursery: trio.Nursery, + stream: trio.abc.Stream, + host: str, + resource: str, *, + subprotocols: Optional[typing.Iterable[str]] = None, + extra_headers: Optional[typing.List[typing.Tuple[bytes, bytes]]] = None, + message_queue_size: int = MESSAGE_QUEUE_SIZE, + max_message_size: int = MAX_MESSAGE_SIZE +) -> 'WebSocketConnection': ''' Wrap an arbitrary stream in a WebSocket connection. @@ -341,8 +389,12 @@ async def wrap_client_stream(nursery, stream, host, resource, *, return connection -async def wrap_server_stream(nursery, stream, - message_queue_size=MESSAGE_QUEUE_SIZE, max_message_size=MAX_MESSAGE_SIZE): +async def wrap_server_stream( + nursery: trio.Nursery, + stream: trio.abc.Stream, + message_queue_size: int = MESSAGE_QUEUE_SIZE, + max_message_size: int = MAX_MESSAGE_SIZE +) -> 'WebSocketRequest': ''' Wrap an arbitrary stream in a server-side WebSocket. @@ -368,10 +420,19 @@ async def wrap_server_stream(nursery, stream, return request -async def serve_websocket(handler, host, port, ssl_context, *, - handler_nursery=None, message_queue_size=MESSAGE_QUEUE_SIZE, - max_message_size=MAX_MESSAGE_SIZE, connect_timeout=CONN_TIMEOUT, - disconnect_timeout=CONN_TIMEOUT, task_status=trio.TASK_STATUS_IGNORED): +async def serve_websocket( + handler: typing.Callable[['WebSocketRequest'], typing.Awaitable[object]], + host: Optional[str], + port: int, + ssl_context: Optional[ssl.SSLContext], + *, + handler_nursery: Optional[trio.Nursery] = None, + message_queue_size: int = MESSAGE_QUEUE_SIZE, + max_message_size: int = MAX_MESSAGE_SIZE, + connect_timeout: float = CONN_TIMEOUT, + disconnect_timeout: float = CONN_TIMEOUT, + task_status: 'TaskStatus[WebSocketServer]' = trio.TASK_STATUS_IGNORED +) -> typing.NoReturn: ''' Serve a WebSocket over TCP. @@ -407,6 +468,10 @@ async def serve_websocket(handler, host, port, ssl_context, *, :param task_status: Part of Trio nursery start protocol. :returns: This function runs until cancelled. ''' + open_tcp_listeners: Union[ + partial[typing.Coroutine[typing.Any, typing.Any, typing.Sequence[trio.SSLListener]]], + partial[typing.Coroutine[typing.Any, typing.Any, typing.Sequence[trio.SocketListener]]] + ] if ssl_context is None: open_tcp_listeners = partial(trio.open_tcp_listeners, port, host=host) else: @@ -437,7 +502,7 @@ class ConnectionClosed(Exception): A WebSocket operation cannot be completed because the connection is closed or in the process of closing. ''' - def __init__(self, reason): + def __init__(self, reason: 'CloseReason') -> None: ''' Constructor. @@ -447,7 +512,7 @@ def __init__(self, reason): super().__init__() self.reason = reason - def __repr__(self): + def __repr__(self) -> str: ''' Return representation. ''' return f'{self.__class__.__name__}<{self.reason}>' @@ -457,7 +522,12 @@ class ConnectionRejected(HandshakeError): A WebSocket connection could not be established because the server rejected the connection attempt. ''' - def __init__(self, status_code, headers, body): + def __init__( + self, + status_code: int, + headers: Optional[typing.Tuple[typing.Tuple[bytes, bytes], ...]], + body: Optional[bytes] + ) -> None: ''' Constructor. @@ -472,14 +542,14 @@ def __init__(self, status_code, headers, body): #: an optional ``bytes`` response body self.body = body - def __repr__(self): + def __repr__(self) -> str: ''' Return representation. ''' return f'{self.__class__.__name__}' class CloseReason: ''' Contains information about why a WebSocket was closed. ''' - def __init__(self, code, reason): + def __init__(self, code: int, reason: Optional[str]) -> None: ''' Constructor. @@ -501,34 +571,34 @@ def __init__(self, code, reason): self._reason = reason @property - def code(self): + def code(self) -> int: ''' (Read-only) The numeric close code. ''' return self._code @property - def name(self): + def name(self) -> str: ''' (Read-only) The human-readable close code. ''' return self._name @property - def reason(self): + def reason(self) -> Optional[str]: ''' (Read-only) An arbitrary reason string. ''' return self._reason - def __repr__(self): + def __repr__(self) -> str: ''' Show close code, name, and reason. ''' return f'{self.__class__.__name__}' \ f'' -class Future: +class Future(typing.Generic[T]): ''' Represents a value that will be available in the future. ''' - def __init__(self): + def __init__(self) -> None: ''' Constructor. ''' - self._value = None + self._value: Optional[T] = None self._value_event = trio.Event() - def set_value(self, value): + def set_value(self, value: T) -> None: ''' Set a value, which will notify any waiters. @@ -537,13 +607,14 @@ def set_value(self, value): self._value = value self._value_event.set() - async def wait_value(self): + async def wait_value(self) -> T: ''' Wait for this future to have a value, then return it. :returns: The value set by ``set_value()``. ''' await self._value_event.wait() + assert self._value is not None return self._value @@ -554,7 +625,7 @@ class WebSocketRequest: The server may modify the handshake or leave it as is. The server should call ``accept()`` to finish the handshake and obtain a connection object. ''' - def __init__(self, connection, event): + def __init__(self, connection: 'WebSocketConnection', event: wsproto.events.Request) -> None: ''' Constructor. @@ -565,7 +636,7 @@ def __init__(self, connection, event): self._event = event @property - def headers(self): + def headers(self) -> typing.List[typing.Tuple[bytes, bytes]]: ''' HTTP headers represented as a list of (name, value) pairs. @@ -574,7 +645,7 @@ def headers(self): return self._event.extra_headers @property - def path(self): + def path(self) -> str: ''' The requested URL path. @@ -583,7 +654,7 @@ def path(self): return self._event.target @property - def proposed_subprotocols(self): + def proposed_subprotocols(self) -> typing.Tuple[str, ...]: ''' A tuple of protocols proposed by the client. @@ -592,7 +663,7 @@ def proposed_subprotocols(self): return tuple(self._event.subprotocols) @property - def local(self): + def local(self) -> Union['Endpoint', str]: ''' The connection's local endpoint. @@ -601,7 +672,7 @@ def local(self): return self._connection.local @property - def remote(self): + def remote(self) -> Union['Endpoint', str]: ''' The connection's remote endpoint. @@ -609,7 +680,12 @@ def remote(self): ''' return self._connection.remote - async def accept(self, *, subprotocol=None, extra_headers=None): + async def accept( + self, + *, + subprotocol: Optional[str] = None, + extra_headers: Optional[typing.List[typing.Tuple[bytes, bytes]]] = None + ) -> 'WebSocketConnection': ''' Accept the request and return a connection object. @@ -625,7 +701,13 @@ async def accept(self, *, subprotocol=None, extra_headers=None): await self._connection._accept(self._event, subprotocol, extra_headers) return self._connection - async def reject(self, status_code, *, extra_headers=None, body=None): + async def reject( + self, + status_code: int, + *, + extra_headers: Optional[typing.List[typing.Tuple[bytes, bytes]]] = None, + body: Optional[bytes] = None + ) -> None: ''' Reject the handshake. @@ -643,11 +725,11 @@ async def reject(self, status_code, *, extra_headers=None, body=None): await self._connection._reject(status_code, extra_headers, body) -def _get_stream_endpoint(stream, *, local): +def _get_stream_endpoint(stream: trio.abc.Stream, *, local: bool) -> Union['Endpoint', str]: ''' Construct an endpoint from a stream. - :param trio.Stream stream: + :param trio.abc.Stream stream: :param bool local: If true, return local endpoint. Otherwise return remote. :returns: An endpoint instance or ``repr()`` for streams that cannot be represented as an endpoint. @@ -657,11 +739,14 @@ def _get_stream_endpoint(stream, *, local): if isinstance(stream, trio.SocketStream): socket = stream.socket elif isinstance(stream, trio.SSLStream): - socket = stream.transport_stream.socket + if isinstance(stream.transport_stream, trio.SocketStream): + socket = stream.transport_stream.socket + else: + socket = None is_ssl = True if socket: addr, port, *_ = socket.getsockname() if local else socket.getpeername() - endpoint = Endpoint(addr, port, is_ssl) + endpoint: Union[Endpoint, str] = Endpoint(addr, port, is_ssl) else: endpoint = repr(stream) return endpoint @@ -702,33 +787,33 @@ def __init__(self, stream, ws_connection, *, host=None, path=None, ``len()``. If a message is received that is larger than this size, then the connection is closed with code 1009 (Message Too Big). ''' - self._close_reason = None + self._close_reason: Optional[CloseReason] = None self._id = next(self.__class__.CONNECTION_ID) self._stream = stream self._stream_lock = trio.StrictFIFOLock() self._wsproto = ws_connection self._message_size = 0 - self._message_parts = [] # type: List[bytes|str] + self._message_parts = [] # type: typing.List[Union[bytes, str]] self._max_message_size = max_message_size self._reader_running = True if ws_connection.client: self._initial_request = Request(host=host, target=path, subprotocols=client_subprotocols, - extra_headers=client_extra_headers or []) + extra_headers=client_extra_headers or []) # type: ignore[call-arg] else: self._initial_request = None self._path = path - self._subprotocol = None - self._handshake_headers = None - self._reject_status = None - self._reject_headers = None + self._subprotocol: Optional[str] = None + self._handshake_headers: Optional[typing.Tuple[typing.Tuple[bytes, bytes], ...]] = None + self._reject_status: Optional[int] = None + self._reject_headers: Optional[typing.Tuple[typing.Tuple[bytes, bytes], ...]] = None self._reject_body = b'' - self._send_channel, self._recv_channel = trio.open_memory_channel( + self._send_channel, self._recv_channel = trio.open_memory_channel[Union[str, bytes]]( message_queue_size) - self._pings = OrderedDict() + self._pings: OrderedDict[bytes, trio.Event] = OrderedDict() # Set when the server has received a connection request event. This # future is never set on client connections. - self._connection_proposal = Future() + self._connection_proposal: Optional[Future[WebSocketRequest]] = Future() # Set once the WebSocket open handshake takes place, i.e. # ConnectionRequested for server or ConnectedEstablished for client. self._open_handshake = trio.Event() @@ -740,7 +825,7 @@ def __init__(self, stream, ws_connection, *, host=None, path=None, self._for_testing_peer_closed_connection = trio.Event() @property - def closed(self): + def closed(self) -> Optional['CloseReason']: ''' (Read-only) The reason why the connection was closed, or ``None`` if the connection is still open. @@ -750,17 +835,17 @@ def closed(self): return self._close_reason @property - def is_client(self): + def is_client(self) -> bool: ''' (Read-only) Is this a client instance? ''' return self._wsproto.client @property - def is_server(self): + def is_server(self) -> bool: ''' (Read-only) Is this a server instance? ''' return not self._wsproto.client @property - def local(self): + def local(self) -> Union['Endpoint', str]: ''' The local endpoint of the connection. @@ -769,7 +854,7 @@ def local(self): return _get_stream_endpoint(self._stream, local=True) @property - def remote(self): + def remote(self) -> Union['Endpoint', str]: ''' The remote endpoint of the connection. @@ -778,17 +863,18 @@ def remote(self): return _get_stream_endpoint(self._stream, local=False) @property - def path(self): + def path(self) -> str: ''' The requested URL path. For clients, this is set when the connection is instantiated. For servers, it is set after the handshake completes. :rtype: str ''' - return self._path + # TODO: make sure self._path is not none before this + return self._path # type: ignore[return-value] @property - def subprotocol(self): + def subprotocol(self) -> Optional[str]: ''' (Read-only) The negotiated subprotocol, or ``None`` if there is no subprotocol. @@ -800,17 +886,18 @@ def subprotocol(self): return self._subprotocol @property - def handshake_headers(self): + def handshake_headers(self) -> typing.Tuple[typing.Tuple[bytes, bytes], ...]: ''' The HTTP headers that were sent by the remote during the handshake, stored as 2-tuples containing key/value pairs. Header keys are always lower case. - :rtype: tuple[tuple[str,str]] + :rtype: tuple[tuple[bytes,bytes]] ''' - return self._handshake_headers + # TODO: make sure self._handshake_headers is not none before this + return self._handshake_headers # type: ignore[return-value] - async def aclose(self, code=1000, reason=None): # pylint: disable=arguments-differ + async def aclose(self, code: int = 1000, reason: Optional[str] = None) -> None: # pylint: disable=arguments-differ ''' Close the WebSocket connection. @@ -828,14 +915,16 @@ async def aclose(self, code=1000, reason=None): # pylint: disable=arguments-dif with _preserve_current_exception(): await self._aclose(code, reason) - async def _aclose(self, code=1000, reason=None): + async def _aclose(self, code: int = 1000, reason: Optional[str] = None) -> None: if self._close_reason: # Per AsyncResource interface, calling aclose() on a closed resource # should succeed. return try: if self._wsproto.state == ConnectionState.OPEN: - await self._send(CloseConnection(code=code, reason=reason)) + # dataclasses and 3.6 + await self._send(CloseConnection(code=code, # type: ignore[call-arg] + reason=reason)) elif self._wsproto.state in (ConnectionState.CONNECTING, ConnectionState.REJECTING): self._close_handshake.set() @@ -849,7 +938,7 @@ async def _aclose(self, code=1000, reason=None): # stream is closed. await self._close_stream() - async def get_message(self): + async def get_message(self) -> Union[str, bytes]: ''' Receive the next WebSocket message. @@ -869,10 +958,11 @@ async def get_message(self): try: message = await self._recv_channel.receive() except (trio.ClosedResourceError, trio.EndOfChannel): - raise ConnectionClosed(self._close_reason) from None + # TODO: make sure self._close_reason is always non-optional + raise ConnectionClosed(self._close_reason) from None # type: ignore[arg-type] return message - async def ping(self, payload=None): + async def ping(self, payload: Optional[bytes] = None) -> None: ''' Send WebSocket ping to remote endpoint and wait for a correspoding pong. @@ -900,10 +990,11 @@ async def ping(self, payload=None): payload = struct.pack('!I', random.getrandbits(32)) event = trio.Event() self._pings[payload] = event - await self._send(Ping(payload=payload)) + # dataclasses and 3.6 + await self._send(Ping(payload=payload)) # type: ignore[call-arg] await event.wait() - async def pong(self, payload=None): + async def pong(self, payload: Optional[bytes] = None) -> None: ''' Send an unsolicted pong. @@ -914,9 +1005,10 @@ async def pong(self, payload=None): ''' if self._close_reason: raise ConnectionClosed(self._close_reason) - await self._send(Pong(payload=payload)) + # dataclasses and 3.6 + await self._send(Pong(payload=payload)) # type: ignore[call-arg] - async def send_message(self, message): + async def send_message(self, message: Union[str, bytes]) -> None: ''' Send a WebSocket message. @@ -926,20 +1018,28 @@ async def send_message(self, message): ''' if self._close_reason: raise ConnectionClosed(self._close_reason) + event: Union[TextMessage, BytesMessage] if isinstance(message, str): - event = TextMessage(data=message) + # dataclasses and 3.6 + event = TextMessage(data=message) # type: ignore[call-arg] elif isinstance(message, bytes): - event = BytesMessage(data=message) + # dataclasses and 3.6 + event = BytesMessage(data=message) # type: ignore[call-arg] else: raise ValueError('message must be str or bytes') await self._send(event) - def __str__(self): + def __str__(self) -> str: ''' Connection ID and type. ''' type_ = 'client' if self.is_client else 'server' return f'{type_}-{self._id}' - async def _accept(self, request, subprotocol, extra_headers): + async def _accept( + self, + request: Request, + subprotocol: Optional[str], + extra_headers: typing.List[typing.Tuple[bytes, bytes]] + ) -> None: ''' Accept the handshake. @@ -953,11 +1053,17 @@ async def _accept(self, request, subprotocol, extra_headers): ''' self._subprotocol = subprotocol self._path = request.target - await self._send(AcceptConnection(subprotocol=self._subprotocol, + # dataclasses and 3.6 + await self._send(AcceptConnection(subprotocol=self._subprotocol, # type: ignore[call-arg] extra_headers=extra_headers)) self._open_handshake.set() - async def _reject(self, status_code, headers, body): + async def _reject( + self, + status_code: int, + headers: typing.List[typing.Tuple[bytes, bytes]], + body: bytes + ) -> None: ''' Reject the handshake. @@ -969,17 +1075,19 @@ async def _reject(self, status_code, headers, body): :param bytes body: An optional response body. ''' if body: - headers.append(('Content-length', str(len(body)).encode('ascii'))) - reject_conn = RejectConnection(status_code=status_code, headers=headers, - has_body=bool(body)) + headers.append((b'Content-length', str(len(body)).encode('ascii'))) + # dataclasses and 3.6 + reject_conn = RejectConnection(status_code=status_code, # type: ignore[call-arg] + headers=headers, has_body=bool(body)) await self._send(reject_conn) if body: - reject_body = RejectData(data=body) + # dataclasses and 3.6 + reject_body = RejectData(data=body) # type: ignore[call-arg] await self._send(reject_body) self._close_reason = CloseReason(1006, 'Rejected WebSocket handshake') self._close_handshake.set() - async def _abort_web_socket(self): + async def _abort_web_socket(self) -> None: ''' If a stream is closed outside of this class, e.g. due to network conditions or because some other code closed our stream object, then we @@ -988,7 +1096,8 @@ async def _abort_web_socket(self): ''' close_reason = wsframeproto.CloseReason.ABNORMAL_CLOSURE if self._wsproto.state == ConnectionState.OPEN: - self._wsproto.send(CloseConnection(code=close_reason.value)) + # dataclasses and 3.6 + self._wsproto.send(CloseConnection(code=close_reason.value)) # type: ignore[call-arg] if self._close_reason is None: await self._close_web_socket(close_reason) self._reader_running = False @@ -996,7 +1105,7 @@ async def _abort_web_socket(self): # (e.g. self.aclose()) to resume. self._close_handshake.set() - async def _close_stream(self): + async def _close_stream(self) -> None: ''' Close the TCP connection. ''' self._reader_running = False try: @@ -1006,7 +1115,7 @@ async def _close_stream(self): # This means the TCP connection is already dead. pass - async def _close_web_socket(self, code, reason=None): + async def _close_web_socket(self, code: int, reason: Optional[str] = None) -> None: ''' Mark the WebSocket as closed. Close the message channel so that if any tasks are suspended in get_message(), they will wake up with a @@ -1017,7 +1126,7 @@ async def _close_web_socket(self, code, reason=None): logger.debug('%s websocket closed %r', self, exc) await self._send_channel.aclose() - async def _get_request(self): + async def _get_request(self) -> 'WebSocketRequest': ''' Return a proposal for a WebSocket handshake. @@ -1035,7 +1144,7 @@ async def _get_request(self): self._connection_proposal = None return proposal - async def _handle_request_event(self, event): + async def _handle_request_event(self, event: Request) -> None: ''' Handle a connection request. @@ -1045,9 +1154,12 @@ async def _handle_request_event(self, event): :param event: ''' proposal = WebSocketRequest(self, event) + if self._connection_proposal is None: + raise Exception('No proposal available. Did you call this method' + ' multiple times or at the wrong time?') self._connection_proposal.set_value(proposal) - async def _handle_accept_connection_event(self, event): + async def _handle_accept_connection_event(self, event: AcceptConnection) -> None: ''' Handle an AcceptConnection event. @@ -1057,7 +1169,7 @@ async def _handle_accept_connection_event(self, event): self._handshake_headers = tuple(event.extra_headers) self._open_handshake.set() - async def _handle_reject_connection_event(self, event): + async def _handle_reject_connection_event(self, event: RejectConnection) -> None: ''' Handle a RejectConnection event. @@ -1069,7 +1181,7 @@ async def _handle_reject_connection_event(self, event): raise ConnectionRejected(self._reject_status, self._reject_headers, body=None) - async def _handle_reject_data_event(self, event): + async def _handle_reject_data_event(self, event: RejectData) -> None: ''' Handle a RejectData event. @@ -1077,10 +1189,14 @@ async def _handle_reject_data_event(self, event): ''' self._reject_body += event.data if event.body_finished: - raise ConnectionRejected(self._reject_status, self._reject_headers, - body=self._reject_body) + raise ConnectionRejected( + # TODO: is _reject_status guaranteed to be non-optional? + self._reject_status, # type: ignore[arg-type] + self._reject_headers, + body=self._reject_body + ) - async def _handle_close_connection_event(self, event): + async def _handle_close_connection_event(self, event: CloseConnection) -> None: ''' Handle a close event. @@ -1099,7 +1215,7 @@ async def _handle_close_connection_event(self, event): if self.is_server: await self._close_stream() - async def _handle_message_event(self, event): + async def _handle_message_event(self, event: Union[BytesMessage, TextMessage]) -> None: ''' Handle a message event. @@ -1113,12 +1229,14 @@ async def _handle_message_event(self, event): self._message_size = 0 self._message_parts = [] self._close_reason = CloseReason(1009, err) - await self._send(CloseConnection(code=1009, reason=err)) + # >=3.7 wsproto uses dataclasses which mypy configured for 3.6 doesn't understand + await self._send(CloseConnection(code=1009, reason=err)) # type: ignore[call-arg] await self._recv_channel.aclose() self._reader_running = False elif event.message_finished: - msg = (b'' if isinstance(event, BytesMessage) else '') \ - .join(self._message_parts) + base: Union[bytes, str] = b'' if isinstance(event, BytesMessage) else '' + # mypy doesn't realize we only use bytes to join bytes, etc. + msg = base.join(self._message_parts) # type: ignore[arg-type] self._message_size = 0 self._message_parts = [] try: @@ -1129,7 +1247,7 @@ async def _handle_message_event(self, event): # and there's no useful cleanup that we can do here. pass - async def _handle_ping_event(self, event): + async def _handle_ping_event(self, event: Ping) -> None: ''' Handle a PingReceived event. @@ -1141,7 +1259,7 @@ async def _handle_ping_event(self, event): logger.debug('%s ping %r', self, event.payload) await self._send(event.response()) - async def _handle_pong_event(self, event): + async def _handle_pong_event(self, event: Pong) -> None: ''' Handle a PongReceived event. @@ -1157,22 +1275,26 @@ async def _handle_pong_event(self, event): ''' payload = bytes(event.payload) try: - event = self._pings[payload] + event_ = self._pings[payload] except KeyError: # We received a pong that doesn't match any in-flight pongs. Nothing # we can do with it, so ignore it. return while self._pings: - key, event = self._pings.popitem(0) + key, event_ = self._pings.popitem(False) skipped = ' [skipped] ' if payload != key else ' ' logger.debug('%s pong%s%r', self, skipped, key) - event.set() + event_.set() if payload == key: break - async def _reader_task(self): + async def _reader_task(self) -> None: ''' A background task that reads network data and generates events. ''' - handlers = { + handlers: typing.Dict[ + typing.Type[wsproto.events.Event], + # callables are contravariant, so cannot say it takes events.Event + typing.Callable[[typing.Any], typing.Awaitable[None]] + ] = { AcceptConnection: self._handle_accept_connection_event, BytesMessage: self._handle_message_event, CloseConnection: self._handle_close_connection_event, @@ -1233,7 +1355,7 @@ async def _reader_task(self): logger.debug('%s reader task finished', self) - async def _send(self, event): + async def _send(self, event: wsproto.events.Event) -> None: ''' Send an to the remote WebSocket. @@ -1250,12 +1372,18 @@ async def _send(self, event): await self._stream.send_all(data) except (trio.BrokenResourceError, trio.ClosedResourceError): await self._abort_web_socket() - raise ConnectionClosed(self._close_reason) from None + # TODO: is self._close_reason guaranteed to be created? + raise ConnectionClosed(self._close_reason) from None # type: ignore[arg-type] class Endpoint: ''' Represents a connection endpoint. ''' - def __init__(self, address, port, is_ssl): + def __init__( + self, + address: object, + port: int, + is_ssl: bool + ) -> None: #: IP address :class:`ipaddress.ip_address` self.address = ip_address(address) #: TCP port @@ -1264,7 +1392,7 @@ def __init__(self, address, port, is_ssl): self.is_ssl = is_ssl @property - def url(self): + def url(self) -> str: ''' Return a URL representation of a TCP endpoint, e.g. ``ws://127.0.0.1:80``. ''' scheme = 'wss' if self.is_ssl else 'ws' @@ -1277,7 +1405,7 @@ def url(self): return f'{scheme}://{self.address}{port_str}' return f'{scheme}://[{self.address}]{port_str}' - def __repr__(self): + def __repr__(self) -> str: ''' Return endpoint info as string. ''' return f'Endpoint(address="{self.address}", port={self.port}, is_ssl={self.is_ssl})' @@ -1291,10 +1419,17 @@ class WebSocketServer: instance and starts some background tasks, ''' - def __init__(self, handler, listeners, *, handler_nursery=None, - message_queue_size=MESSAGE_QUEUE_SIZE, - max_message_size=MAX_MESSAGE_SIZE, connect_timeout=CONN_TIMEOUT, - disconnect_timeout=CONN_TIMEOUT): + def __init__( + self, + handler: typing.Callable[[WebSocketRequest], typing.Awaitable[object]], + listeners: typing.Sequence[trio.abc.Listener[typing.Any]], + *, + handler_nursery: Optional[trio.Nursery] = None, + message_queue_size: int = MESSAGE_QUEUE_SIZE, + max_message_size: int = MAX_MESSAGE_SIZE, + connect_timeout: float = CONN_TIMEOUT, + disconnect_timeout: float = CONN_TIMEOUT + ) -> None: ''' Constructor. @@ -1326,7 +1461,7 @@ def __init__(self, handler, listeners, *, handler_nursery=None, self._disconnect_timeout = disconnect_timeout @property - def port(self): + def port(self) -> int: """Returns the requested or kernel-assigned port number. In the case of kernel-assigned port (requested with port=0 in the @@ -1347,7 +1482,7 @@ def port(self): raise RuntimeError(f'This socket does not have a port: {repr(listener)}') from None @property - def listeners(self): + def listeners(self) -> typing.List[Union[str, Endpoint]]: ''' Return a list of listener metadata. Each TCP listener is represented as an ``Endpoint`` instance. Other listener types are represented by their @@ -1362,7 +1497,13 @@ def listeners(self): if isinstance(listener, trio.SocketListener): socket = listener.socket elif isinstance(listener, trio.SSLListener): - socket = listener.transport_listener.socket + listener = listener.transport_listener + + if isinstance(listener, trio.SocketListener): + socket = listener.socket + else: + socket = None + is_ssl = True if socket: sockname = socket.getsockname() @@ -1371,7 +1512,11 @@ def listeners(self): listeners.append(repr(listener)) return listeners - async def run(self, *, task_status=trio.TASK_STATUS_IGNORED): + async def run( + self, + *, + task_status: 'TaskStatus[WebSocketServer]' = trio.TASK_STATUS_IGNORED + ) -> typing.NoReturn: ''' Start serving incoming connections requests. @@ -1392,7 +1537,7 @@ async def run(self, *, task_status=trio.TASK_STATUS_IGNORED): task_status.started(self) await trio.sleep_forever() - async def _handle_connection(self, stream): + async def _handle_connection(self, stream: trio.abc.Stream) -> None: ''' Handle an incoming connection by spawning a connection background task and a handler task inside a new nursery. diff --git a/trio_websocket/py.typed b/trio_websocket/py.typed new file mode 100644 index 0000000..e69de29 From edd97a4b398ebe4e28536dea9238f01ffafcbeb0 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 4 Jan 2022 15:51:00 +0900 Subject: [PATCH 2/5] Appease CI --- Makefile | 2 +- trio_websocket/_impl.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 935d27c..4c3c14b 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ test: lint: $(PYTHON) -m pylint trio_websocket/ tests/ autobahn/ examples/ - $(PYTHON) -m mypy trio_websocket/ + ! $(PYTHON) --version | grep -q 'PyPy' && $(PYTHON) -m mypy trio_websocket/ publish: rm -fr build dist .egg trio_websocket.egg-info diff --git a/trio_websocket/_impl.py b/trio_websocket/_impl.py index d23b8da..62ad212 100644 --- a/trio_websocket/_impl.py +++ b/trio_websocket/_impl.py @@ -799,7 +799,7 @@ def __init__(self, stream, ws_connection, *, host=None, path=None, if ws_connection.client: self._initial_request = Request(host=host, target=path, subprotocols=client_subprotocols, - extra_headers=client_extra_headers or []) # type: ignore[call-arg] + extra_headers=client_extra_headers or []) else: self._initial_request = None self._path = path From 71e1587b0435e02f123e89fb8295d4e3307a93d3 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 4 Jan 2022 16:01:11 +0900 Subject: [PATCH 3/5] Mypy doesn't work on PyPy --- requirements-dev.in | 2 +- requirements-dev.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev.in b/requirements-dev.in index 41190a2..09bdfe5 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -11,5 +11,5 @@ sphinx_rtd_theme trio>=0.14.0 trustme twine -mypy +mypy; platform.python_implementation == 'CPython' trio-typing diff --git a/requirements-dev.txt b/requirements-dev.txt index fb2e290..cf32702 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -95,7 +95,7 @@ mypy-extensions==0.4.3 # via # mypy # trio-typing -mypy==0.910 +mypy==0.910 ; platform_python_implementation == "CPython" # via -r requirements-dev.in outcome==1.0.1 # via From fc8f8a3d732b40f5d4b181f663b2f8bfe50fad48 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 4 Jan 2022 16:08:58 +0900 Subject: [PATCH 4/5] Shell is not my preferred language --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4c3c14b..de9fb6f 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ test: lint: $(PYTHON) -m pylint trio_websocket/ tests/ autobahn/ examples/ - ! $(PYTHON) --version | grep -q 'PyPy' && $(PYTHON) -m mypy trio_websocket/ + if ! $(PYTHON) --version | grep -q 'PyPy'; then $(PYTHON) -m mypy trio_websocket/; fi publish: rm -fr build dist .egg trio_websocket.egg-info From 2039b132f6c482e459aa03b17eea804670f91aa9 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 30 Aug 2022 11:34:50 +0900 Subject: [PATCH 5/5] Update dependencies I will re-"update" them on 3.6 in a while, but in the meantime have this --- requirements-dev.txt | 148 +++++++++++++++++++++---------------------- 1 file changed, 72 insertions(+), 76 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index cf32702..3b14882 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,52 +1,52 @@ # -# This file is autogenerated by pip-compile with python 3.6 +# This file is autogenerated by pip-compile with python 3.8 # To update, run: # # pip-compile --output-file=requirements-dev.txt requirements-dev.in setup.py # alabaster==0.7.12 # via sphinx -astroid==2.9.3 +astroid==2.12.5 # via pylint async-generator==1.10 # via # pytest-trio # trio # trio-websocket (setup.py) -attrs==21.4.0 +attrs==22.1.0 # via # -r requirements-dev.in # outcome # pytest # trio -babel==2.9.1 +babel==2.10.3 # via sphinx -bleach==4.1.0 +bleach==5.0.1 # via readme-renderer -certifi==2021.10.8 +build==0.8.0 + # via pip-tools +certifi==2022.6.15 # via requests -cffi==1.15.0 +cffi==1.15.1 # via cryptography -charset-normalizer==2.0.10 +charset-normalizer==2.1.1 # via requests -click==8.0.3 +click==8.1.3 # via pip-tools -colorama==0.4.4 - # via twine -contextvars==2.4 - # via - # sniffio - # trio -coverage[toml]==6.2 +commonmark==0.9.1 + # via rich +coverage[toml]==6.4.4 # via # coveralls # pytest-cov coveralls==3.3.1 # via -r requirements-dev.in -cryptography==3.2.1 +cryptography==37.0.4 # via # secretstorage # trustme +dill==0.3.5.1 + # via pylint docopt==0.6.2 # via coveralls docutils==0.17.1 @@ -54,65 +54,60 @@ docutils==0.17.1 # readme-renderer # sphinx # sphinx-rtd-theme -h11==0.12.0 +h11==0.13.0 # via wsproto idna==3.3 # via # requests # trio # trustme -imagesize==1.3.0 +imagesize==1.4.1 # via sphinx -immutables==0.16 - # via contextvars -importlib-metadata==4.8.3 +importlib-metadata==4.12.0 # via - # click # keyring - # pep517 - # pluggy - # pytest + # sphinx # twine iniconfig==1.1.1 # via pytest isort==5.10.1 # via pylint -jeepney==0.7.1 +jeepney==0.8.0 # via # keyring # secretstorage -jinja2==2.11.2 +jinja2==3.1.2 # via sphinx -keyring==23.4.1 +keyring==23.8.2 # via twine lazy-object-proxy==1.7.1 # via astroid -markupsafe==2.0.1 +markupsafe==2.1.1 # via jinja2 -mccabe==0.6.1 +mccabe==0.7.0 # via pylint +mypy==0.971 ; platform_python_implementation == "CPython" + # via -r requirements-dev.in mypy-extensions==0.4.3 # via # mypy # trio-typing -mypy==0.910 ; platform_python_implementation == "CPython" - # via -r requirements-dev.in -outcome==1.0.1 +outcome==1.2.0 # via # pytest-trio # trio packaging==21.3 # via - # bleach + # build # pytest # sphinx -pep517==0.12.0 - # via pip-tools -pip-tools==6.4.0 +pep517==0.13.0 + # via build +pip-tools==6.8.0 # via -r requirements-dev.in -pkginfo==1.8.2 +pkginfo==1.8.3 # via twine -platformdirs==2.4.0 +platformdirs==2.5.2 # via pylint pluggy==1.0.0 # via pytest @@ -120,15 +115,16 @@ py==1.11.0 # via pytest pycparser==2.21 # via cffi -pygments==2.11.2 +pygments==2.13.0 # via # readme-renderer + # rich # sphinx -pylint==2.12.2 +pylint==2.15.0 # via -r requirements-dev.in -pyparsing==3.0.6 +pyparsing==3.0.9 # via packaging -pytest==6.2.5 +pytest==7.1.2 # via # -r requirements-dev.in # pytest-cov @@ -137,11 +133,11 @@ pytest-cov==3.0.0 # via -r requirements-dev.in pytest-trio==0.7.0 # via -r requirements-dev.in -pytz==2021.3 +pytz==2022.2.1 # via babel -readme-renderer==32.0 +readme-renderer==37.0 # via twine -requests==2.27.1 +requests==2.28.1 # via # coveralls # requests-toolbelt @@ -149,22 +145,21 @@ requests==2.27.1 # twine requests-toolbelt==0.9.1 # via twine -secretstorage==3.3.1 +rfc3986==2.0.0 + # via twine +rich==12.5.1 + # via twine +secretstorage==3.3.3 # via keyring -six==1.15.0 - # via - # astroid - # bleach - # cryptography - # packaging - # readme-renderer +six==1.16.0 + # via bleach sniffio==1.2.0 # via trio snowballstemmer==2.2.0 # via sphinx sortedcontainers==2.4.0 # via trio -sphinx==4.3.2 +sphinx==5.1.1 # via # -r requirements-dev.in # sphinx-rtd-theme @@ -185,48 +180,49 @@ sphinxcontrib-serializinghtml==1.1.5 # via sphinx sphinxcontrib-trio==1.1.2 # via -r requirements-dev.in -toml==0.10.2 +tomli==2.0.1 # via + # build + # coverage # mypy # pep517 # pylint # pytest -tomli==1.2.3 - # via - # coverage - # pep517 -tqdm==4.62.3 - # via twine -trio-typing==0.6.0 - # via -r requirements-dev.in -trio==0.17.0 +tomlkit==0.11.4 + # via pylint +trio==0.21.0 # via # -r requirements-dev.in # pytest-trio # trio-typing # trio-websocket (setup.py) +trio-typing==0.7.0 + # via -r requirements-dev.in trustme==0.9.0 # via -r requirements-dev.in -twine==3.7.1 +twine==4.0.1 # via -r requirements-dev.in -typing-extensions==4.0.1 +typing-extensions==4.3.0 # via + # astroid # mypy + # pylint + # rich # trio-typing -urllib3==1.26.2 - # via requests +urllib3==1.26.12 + # via + # requests + # twine webencodings==0.5.1 # via bleach wheel==0.37.1 # via pip-tools -wrapt==1.13.3 +wrapt==1.14.1 # via astroid -wsproto==0.15.0 +wsproto==1.2.0 # via trio-websocket (setup.py) -zipp==3.6.0 - # via - # importlib-metadata - # pep517 +zipp==3.8.1 + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # pip