[Python-modules-commits] [python-websockets] 01/04: Import python-websockets_3.0.orig.tar.gz
Piotr Ożarowski
piotr at moszumanska.debian.org
Mon Jan 11 22:20:52 UTC 2016
This is an automated email from the git hooks/post-receive script.
piotr pushed a commit to branch master
in repository python-websockets.
commit 03f3128bbb2866b94a0de5977eb10109ecc3e1eb
Author: Piotr Ożarowski <piotr at debian.org>
Date: Sun Dec 27 10:24:26 2015 +0100
Import python-websockets_3.0.orig.tar.gz
---
PKG-INFO | 5 +-
README | 3 +-
setup.cfg | 2 +-
websockets.egg-info/PKG-INFO | 5 +-
websockets.egg-info/SOURCES.txt | 3 +
websockets.egg-info/requires.txt | 3 +-
websockets/client.py | 26 +-
websockets/compatibility.py | 8 +
websockets/exceptions.py | 49 +++-
websockets/framing.py | 7 +
websockets/handshake.py | 5 +
websockets/http.py | 5 +
websockets/protocol.py | 202 +++++++++++---
websockets/py35_client.py | 21 ++
websockets/py35_test_client_server.py | 33 +++
websockets/server.py | 41 ++-
websockets/test_client_server.py | 23 +-
websockets/test_handshake.py | 2 +
websockets/test_protocol.py | 510 +++++++++++++++++++++-------------
websockets/uri.py | 3 +
websockets/version.py | 2 +-
21 files changed, 685 insertions(+), 273 deletions(-)
diff --git a/PKG-INFO b/PKG-INFO
index 255f2f3..a8b75be 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: websockets
-Version: 2.6
+Version: 3.0
Summary: An implementation of the WebSocket Protocol (RFC 6455)
Home-page: https://github.com/aaugustin/websockets
Author: Aymeric Augustin
@@ -19,7 +19,7 @@ Description: ``websockets`` is a library for developing WebSocket servers_ and c
3.4 or Python 3.3 with the ``asyncio`` module, which is available with ``pip
install asyncio``.
- Documentation is available at http://aaugustin.github.io/websockets/.
+ Documentation is available on `Read the Docs`_.
Bug reports, patches and suggestions welcome! Just open an issue_ or send a
`pull request`_.
@@ -29,6 +29,7 @@ Description: ``websockets`` is a library for developing WebSocket servers_ and c
.. _RFC 6455: http://tools.ietf.org/html/rfc6455
.. _Autobahn Testsuite: https://github.com/aaugustin/websockets/blob/master/compliance/README.rst
.. _PEP 3156: http://www.python.org/dev/peps/pep-3156/
+ .. _Read the Docs: https://websockets.readthedocs.org/
.. _issue: https://github.com/aaugustin/websockets/issues/new
.. _pull request: https://github.com/aaugustin/websockets/compare/
diff --git a/README b/README
index ec3c42c..01f357c 100644
--- a/README
+++ b/README
@@ -13,7 +13,7 @@ Installation is as simple as ``pip install websockets``. It requires Python ≥
3.4 or Python 3.3 with the ``asyncio`` module, which is available with ``pip
install asyncio``.
-Documentation is available at http://aaugustin.github.io/websockets/.
+Documentation is available on `Read the Docs`_.
Bug reports, patches and suggestions welcome! Just open an issue_ or send a
`pull request`_.
@@ -23,5 +23,6 @@ Bug reports, patches and suggestions welcome! Just open an issue_ or send a
.. _RFC 6455: http://tools.ietf.org/html/rfc6455
.. _Autobahn Testsuite: https://github.com/aaugustin/websockets/blob/master/compliance/README.rst
.. _PEP 3156: http://www.python.org/dev/peps/pep-3156/
+.. _Read the Docs: https://websockets.readthedocs.org/
.. _issue: https://github.com/aaugustin/websockets/issues/new
.. _pull request: https://github.com/aaugustin/websockets/compare/
diff --git a/setup.cfg b/setup.cfg
index 94f9067..5bc163c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -10,6 +10,6 @@ lines_after_imports = 2
[egg_info]
tag_build =
-tag_svn_revision = 0
tag_date = 0
+tag_svn_revision = 0
diff --git a/websockets.egg-info/PKG-INFO b/websockets.egg-info/PKG-INFO
index 255f2f3..a8b75be 100644
--- a/websockets.egg-info/PKG-INFO
+++ b/websockets.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: websockets
-Version: 2.6
+Version: 3.0
Summary: An implementation of the WebSocket Protocol (RFC 6455)
Home-page: https://github.com/aaugustin/websockets
Author: Aymeric Augustin
@@ -19,7 +19,7 @@ Description: ``websockets`` is a library for developing WebSocket servers_ and c
3.4 or Python 3.3 with the ``asyncio`` module, which is available with ``pip
install asyncio``.
- Documentation is available at http://aaugustin.github.io/websockets/.
+ Documentation is available on `Read the Docs`_.
Bug reports, patches and suggestions welcome! Just open an issue_ or send a
`pull request`_.
@@ -29,6 +29,7 @@ Description: ``websockets`` is a library for developing WebSocket servers_ and c
.. _RFC 6455: http://tools.ietf.org/html/rfc6455
.. _Autobahn Testsuite: https://github.com/aaugustin/websockets/blob/master/compliance/README.rst
.. _PEP 3156: http://www.python.org/dev/peps/pep-3156/
+ .. _Read the Docs: https://websockets.readthedocs.org/
.. _issue: https://github.com/aaugustin/websockets/issues/new
.. _pull request: https://github.com/aaugustin/websockets/compare/
diff --git a/websockets.egg-info/SOURCES.txt b/websockets.egg-info/SOURCES.txt
index a3ada05..43435b4 100644
--- a/websockets.egg-info/SOURCES.txt
+++ b/websockets.egg-info/SOURCES.txt
@@ -5,11 +5,14 @@ setup.cfg
setup.py
websockets/__init__.py
websockets/client.py
+websockets/compatibility.py
websockets/exceptions.py
websockets/framing.py
websockets/handshake.py
websockets/http.py
websockets/protocol.py
+websockets/py35_client.py
+websockets/py35_test_client_server.py
websockets/server.py
websockets/test_client_server.py
websockets/test_framing.py
diff --git a/websockets.egg-info/requires.txt b/websockets.egg-info/requires.txt
index 64f133c..bbb4934 100644
--- a/websockets.egg-info/requires.txt
+++ b/websockets.egg-info/requires.txt
@@ -1,4 +1,3 @@
-
[:python_version=="3.3"]
-asyncio
\ No newline at end of file
+asyncio
diff --git a/websockets/client.py b/websockets/client.py
index d3677c3..ea74461 100644
--- a/websockets/client.py
+++ b/websockets/client.py
@@ -1,5 +1,6 @@
"""
The :mod:`websockets.client` module defines a simple WebSocket client API.
+
"""
__all__ = ['connect', 'WebSocketClientProtocol']
@@ -21,8 +22,8 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
This class inherits most of its methods from
:class:`~websockets.protocol.WebSocketCommonProtocol`.
- """
+ """
is_client = True
state = CONNECTING
@@ -39,6 +40,7 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
If provided, ``extra_headers`` sets additional HTTP request headers.
It must be a mapping or an iterable of (name, value) pairs.
+
"""
headers = []
set_header = lambda k, v: headers.append((k, v))
@@ -98,7 +100,7 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
@asyncio.coroutine
def connect(uri, *,
- loop=None, klass=WebSocketClientProtocol,
+ loop=None, klass=WebSocketClientProtocol, legacy_recv=False,
origin=None, subprotocols=None, extra_headers=None,
**kwds):
"""
@@ -116,7 +118,7 @@ def connect(uri, *,
* ``origin`` sets the Origin HTTP header
* ``subprotocols`` is a list of supported subprotocols in order of
- decreasing preference
+ decreasing preference
* ``extra_headers`` sets additional HTTP request headers – it can be a
mapping or an iterable of (name, value) pairs
@@ -125,6 +127,10 @@ def connect(uri, *,
It raises :exc:`~websockets.uri.InvalidURI` if ``uri`` is invalid and
:exc:`~websockets.handshake.InvalidHandshake` if the handshake fails.
+
+ On Python 3.5, it can be used as a asynchronous context manager. In that
+ case, the connection is closed when exiting the context.
+
"""
if loop is None:
loop = asyncio.get_event_loop()
@@ -136,7 +142,8 @@ def connect(uri, *,
raise ValueError("connect() received a SSL context for a ws:// URI. "
"Use a wss:// URI to enable TLS.")
factory = lambda: klass(
- host=wsuri.host, port=wsuri.port, secure=wsuri.secure, loop=loop)
+ host=wsuri.host, port=wsuri.port, secure=wsuri.secure,
+ loop=loop, legacy_recv=legacy_recv)
transport, protocol = yield from loop.create_connection(
factory, wsuri.host, wsuri.port, **kwds)
@@ -150,3 +157,14 @@ def connect(uri, *,
raise
return protocol
+
+
+try:
+ from .py35_client import Connect
+except SyntaxError: # pragma: no cover
+ pass
+else:
+ Connect.__wrapped__ = connect
+ # Copy over docstring to support building documentation on Python 3.5.
+ Connect.__doc__ = connect.__doc__
+ connect = Connect
diff --git a/websockets/compatibility.py b/websockets/compatibility.py
new file mode 100644
index 0000000..90afea9
--- /dev/null
+++ b/websockets/compatibility.py
@@ -0,0 +1,8 @@
+import asyncio
+
+
+# Replace with BaseEventLoop.create_task when dropping Python < 3.4.2.
+try: # pragma: no cover
+ asyncio_ensure_future = asyncio.ensure_future # Python ≥ 3.5
+except AttributeError: # pragma: no cover
+ asyncio_ensure_future = asyncio.async # Python < 3.5
diff --git a/websockets/exceptions.py b/websockets/exceptions.py
index 258780e..8824df0 100644
--- a/websockets/exceptions.py
+++ b/websockets/exceptions.py
@@ -1,28 +1,63 @@
__all__ = [
'InvalidHandshake', 'InvalidOrigin', 'InvalidState', 'InvalidURI',
- 'PayloadTooBig', 'WebSocketProtocolError',
+ 'ConnectionClosed', 'PayloadTooBig', 'WebSocketProtocolError',
]
class InvalidHandshake(Exception):
- """Exception raised when a handshake request or response is invalid."""
+ """
+ Exception raised when a handshake request or response is invalid.
+
+ """
class InvalidOrigin(InvalidHandshake):
- """Exception raised when the origin in a handshake request is forbidden."""
+ """
+ Exception raised when the origin in a handshake request is forbidden.
+
+ """
class InvalidState(Exception):
- """Exception raised when an operation is forbidden in the current state."""
+ """
+ Exception raised when an operation is forbidden in the current state.
+
+ """
+
+
+class ConnectionClosed(InvalidState):
+ """
+ Exception raised when trying to read or write on a closed connection.
+
+ Provides the connection close code and reason in its ``code`` and
+ ``reason`` attributes respectively.
+
+ """
+ def __init__(self, code, reason):
+ self.code = code
+ self.reason = reason
+ message = 'WebSocket connection is closed: '
+ message += 'code = {}, '.format(code) if code else 'no code, '
+ message += 'reason = {}.'.format(reason) if reason else 'no reason.'
+ super().__init__(message)
class InvalidURI(Exception):
- """Exception raised when an URI isn't a valid websocket URI."""
+ """
+ Exception raised when an URI isn't a valid websocket URI.
+
+ """
class PayloadTooBig(Exception):
- """Exception raised when a frame's payload exceeds the maximum size."""
+ """
+ Exception raised when a frame's payload exceeds the maximum size.
+
+ """
class WebSocketProtocolError(Exception):
- """Internal exception raised when the remote side breaks the protocol."""
+ """
+ Internal exception raised when the remote side breaks the protocol.
+
+ """
diff --git a/websockets/framing.py b/websockets/framing.py
index 7714a86..544ac27 100644
--- a/websockets/framing.py
+++ b/websockets/framing.py
@@ -6,6 +6,7 @@ It deals with a single frame at a time. Anything that depends on the sequence
of frames is implemented in :mod:`websockets.protocol`.
.. _section 5 of RFC 6455: http://tools.ietf.org/html/rfc6455#section-5
+
"""
import asyncio
@@ -52,6 +53,7 @@ Frame.__doc__ = """WebSocket frame.
Only these three fields are needed by higher level code. The MASK bit, payload
length and masking-key are handled on the fly by :func:`read_frame` and
:func:`write_frame`.
+
"""
@@ -72,6 +74,7 @@ def read_frame(reader, mask, *, max_size=None):
This function validates the frame before returning it and raises
:exc:`~websockets.exceptions.WebSocketProtocolError` if it contains
incorrect values.
+
"""
# Read the header
data = yield from reader(2)
@@ -119,6 +122,7 @@ def write_frame(frame, writer, mask):
This function validates the frame before sending it and raises
:exc:`~websockets.exceptions.WebSocketProtocolError` if it contains
incorrect values.
+
"""
check_frame(frame)
output = io.BytesIO()
@@ -153,6 +157,7 @@ def check_frame(frame):
"""
Raise :exc:`~websockets.exceptions.WebSocketProtocolError` if the frame
contains incorrect values.
+
"""
if frame.opcode in (OP_CONT, OP_TEXT, OP_BINARY):
return
@@ -174,6 +179,7 @@ def parse_close(data):
Raise :exc:`~websockets.exceptions.WebSocketProtocolError` or
:exc:`UnicodeDecodeError` if the data is invalid.
+
"""
length = len(data)
if length == 0:
@@ -193,5 +199,6 @@ def serialize_close(code, reason):
Serialize the data for a close frame.
This is the reverse of :func:`parse_close`.
+
"""
return struct.pack('!H', code) + reason.encode('utf-8')
diff --git a/websockets/handshake.py b/websockets/handshake.py
index 322d555..e2ed644 100644
--- a/websockets/handshake.py
+++ b/websockets/handshake.py
@@ -31,6 +31,7 @@ To open a connection, a client must:
:func:`build_request`,
- Read the response, check that the status code is 101, and check the headers
with :func:`check_response`.
+
"""
__all__ = [
@@ -53,6 +54,7 @@ def build_request(set_header):
Build a handshake request to send to the server.
Return the ``key`` which must be passed to :func:`check_response`.
+
"""
rand = bytes(random.getrandbits(8) for _ in range(16))
key = base64.b64encode(rand).decode()
@@ -77,6 +79,7 @@ def check_request(get_header):
request and doesn't perform Host and Origin checks. These controls are
usually performed earlier in the HTTP request handling code. They're the
responsibility of the caller.
+
"""
try:
assert get_header('Upgrade').lower() == 'websocket'
@@ -97,6 +100,7 @@ def build_response(set_header, key):
Build a handshake response to send to the client.
``key`` comes from :func:`check_request`.
+
"""
set_header('Upgrade', 'WebSocket')
set_header('Connection', 'Upgrade')
@@ -117,6 +121,7 @@ def check_response(get_header, key):
This function doesn't verify that the response is an HTTP/1.1 or higher
response with a 101 status code. These controls are the responsibility of
the caller.
+
"""
try:
assert get_header('Upgrade').lower() == 'websocket'
diff --git a/websockets/http.py b/websockets/http.py
index 6e1972f..1452d0a 100644
--- a/websockets/http.py
+++ b/websockets/http.py
@@ -4,6 +4,7 @@ merely adequate for the WebSocket handshake messages.
These functions cannot be imported from :mod:`websockets`; they must be
imported from :mod:`websockets.http`.
+
"""
__all__ = ['read_request', 'read_response', 'USER_AGENT']
@@ -37,6 +38,7 @@ def read_request(stream):
Raise an exception if the request isn't well formatted.
The request is assumed not to contain a body.
+
"""
request_line, headers = yield from read_message(stream)
method, path, version = request_line[:-2].decode().split(None, 2)
@@ -58,6 +60,7 @@ def read_response(stream):
Raise an exception if the request isn't well formatted.
The response is assumed not to contain a body.
+
"""
status_line, headers = yield from read_message(stream)
version, status, reason = status_line[:-2].decode().split(None, 2)
@@ -75,6 +78,7 @@ def read_message(stream):
and ``headers`` is a :class:`~email.message.Message`.
The message is assumed not to contain a body.
+
"""
start_line = yield from read_line(stream)
header_lines = io.BytesIO()
@@ -94,6 +98,7 @@ def read_message(stream):
def read_line(stream):
"""
Read a single line from ``stream``.
+
"""
line = yield from stream.readline()
if len(line) > MAX_LINE:
diff --git a/websockets/protocol.py b/websockets/protocol.py
index 00dfa37..51cfd62 100644
--- a/websockets/protocol.py
+++ b/websockets/protocol.py
@@ -3,6 +3,7 @@ The :mod:`websockets.protocol` module handles WebSocket control and data
frames as specified in `sections 4 to 8 of RFC 6455`_.
.. _sections 4 to 8 of RFC 6455: http://tools.ietf.org/html/rfc6455#section-4
+
"""
__all__ = ['WebSocketCommonProtocol']
@@ -15,7 +16,9 @@ import logging
import random
import struct
-from .exceptions import InvalidState, PayloadTooBig, WebSocketProtocolError
+from .compatibility import asyncio_ensure_future
+from .exceptions import (ConnectionClosed, InvalidState, PayloadTooBig,
+ WebSocketProtocolError)
from .framing import *
from .handshake import *
@@ -56,7 +59,8 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
The ``max_size`` parameter enforces the maximum size for incoming messages
in bytes. The default value is 1MB. ``None`` disables the limit. If a
message larger than the maximum size is received, :meth:`recv()` will
- return ``None`` and the connection will be closed with status code 1009.
+ raise :exc:`~websockets.exceptions.ConnectionClosed` and the connection
+ will be closed with status code 1009.
Once the handshake is complete, request and response HTTP headers are
available:
@@ -71,8 +75,8 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
Once the connection is closed, the status code is available in the
:attr:`close_code` attribute and the reason in :attr:`close_reason`.
- """
+ """
# There are only two differences between the client-side and the server-
# side behavior: masking the payload and closing the underlying TCP
# connection. This class implements the server-side behavior by default.
@@ -83,7 +87,8 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
def __init__(self, *,
host=None, port=None, secure=None,
- timeout=10, max_size=2 ** 20, loop=None):
+ timeout=10, max_size=2 ** 20, loop=None,
+ legacy_recv=False):
self.host = host
self.port = port
self.secure = secure
@@ -94,6 +99,8 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
# attribute of StreamReaderProtocol, inherited from FlowControlMixin.
self.loop = loop
+ self.legacy_recv = legacy_recv
+
stream_reader = asyncio.StreamReader(loop=loop)
super().__init__(stream_reader, self.client_connected, loop)
@@ -134,10 +141,6 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
if self.state == OPEN:
self.opening_handshake.set_result(True)
- @property
- def state_name(self):
- return ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'][self.state]
-
# Public API
@property
@@ -145,8 +148,9 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
"""
Local address of the connection.
- The address is a ``(host, port)`` tuple or ``None`` if the connection
- hasn't been established yet.
+ This is a ``(host, port)`` tuple or ``None`` if the connection hasn't
+ been established yet.
+
"""
if self.writer is None:
return None
@@ -157,8 +161,9 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
"""
Remote address of the connection.
- The address is a ``(host, port)`` tuple or ``None`` if the connection
- hasn't been established yet.
+ This is a ``(host, port)`` tuple or ``None`` if the connection hasn't
+ been established yet.
+
"""
if self.writer is None:
return None
@@ -169,22 +174,41 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
"""
This property is ``True`` when the connection is usable.
- It may be used to handle disconnections gracefully.
+ It may be used to detect disconnections but this is discouraged per
+ the EAFP_ principle. When ``open`` is ``False``, using the connection
+ raises a :exc:`~websockets.exceptions.ConnectionClosed` exception.
+
+ .. _EAFP: https://docs.python.org/3/glossary.html#term-eafp
+
"""
return self.state == OPEN
+ @property
+ def state_name(self):
+ """
+ Current connection state, as a string.
+
+ Possible states are defined in the WebSocket specification:
+ CONNECTING, OPEN, CLOSING, or CLOSED.
+
+ To check if the connection is open, use :attr:`open` instead.
+
+ """
+ return ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'][self.state]
+
@asyncio.coroutine
def close(self, code=1000, reason=''):
"""
This coroutine performs the closing handshake.
It waits for the other end to complete the handshake. It doesn't do
- anything once the connection is closed.
+ anything once the connection is closed. Thus it's idemptotent.
- It's usually safe to wrap this coroutine in :func:`~asyncio.async`
+ It's safe to wrap this coroutine in :func:`~asyncio.ensure_future`
since errors during connection termination aren't particularly useful.
``code`` must be an :class:`int` and ``reason`` a :class:`str`.
+
"""
if self.state == OPEN:
# 7.1.2. Start the WebSocket Closing Handshake
@@ -211,18 +235,31 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
It returns a :class:`str` for a text frame and :class:`bytes` for a
binary frame.
- When the end of the message stream is reached, or when a protocol
- error occurs, :meth:`recv` returns ``None``, indicating that the
- connection is closed.
+ When the end of the message stream is reached, :meth:`recv` raises
+ :exc:`~websockets.exceptions.ConnectionClosed`. This can happen after
+ a normal connection closure, a protocol error or a network failure.
+
+ .. versionchanged:: 3.0
+
+ :meth:`recv` used to return ``None`` instead. Refer to the
+ changelog for details.
+
"""
+ # Don't yield from self.ensure_open() here because messages could be
+ # available in the queue even if the connection is closed.
+
# Return any available message
try:
return self.messages.get_nowait()
except asyncio.queues.QueueEmpty:
pass
+ # Don't yield from self.ensure_open() here because messages could be
+ # received before the closing frame even if the connection is closing.
+
# Wait for a message until the connection is closed
- next_message = asyncio.async(self.messages.get(), loop=self.loop)
+ next_message = asyncio_ensure_future(
+ self.messages.get(), loop=self.loop)
try:
done, pending = yield from asyncio.wait(
[next_message, self.worker],
@@ -232,23 +269,27 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
next_message.cancel()
raise
+ # Now there's no need to yield from self.ensure_open(). Either a
+ # message was received or the connection was closed.
+
if next_message in done:
return next_message.result()
else:
next_message.cancel()
+ if not self.legacy_recv:
+ raise ConnectionClosed(self.close_code, self.close_reason)
@asyncio.coroutine
def send(self, data):
"""
This coroutine sends a message.
- It sends a :class:`str` as a text frame and :class:`bytes` as a binary
- frame.
+ It sends :class:`str` as a text frame and :class:`bytes` as a binary
+ frame. It raises a :exc:`TypeError` for other inputs.
- It raises a :exc:`TypeError` for other inputs and
- :exc:`~websockets.exceptions.InvalidState` once the connection is
- closed.
"""
+ yield from self.ensure_open()
+
if isinstance(data, str):
opcode = 1
data = data.encode('utf-8')
@@ -256,6 +297,7 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
opcode = 2
else:
raise TypeError("data must be bytes or str")
+
yield from self.write_frame(opcode, data)
@asyncio.coroutine
@@ -267,11 +309,23 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
corresponding pong is received and which you may ignore if you don't
want to wait.
- A ping may serve as a keepalive.
+ A ping may serve as a keepalive or as a check that the remote endpoint
+ received all messages up to this point, with ``yield from ws.ping()``.
+
+ By default, the ping contains four random bytes. The content may be
+ overridden with the optional ``data`` argument which must be of type
+ :class:`str` (which will be encoded to UTF-8) or :class:`bytes`.
+
"""
+ yield from self.ensure_open()
+
+ if data is not None:
+ data = self.encode_data(data)
+
# Protect against duplicates if a payload is explicitly set.
if data in self.pings:
raise ValueError("Already waiting for a pong with the same data")
+
# Generate a unique random payload otherwise.
while data is None or data in self.pings:
data = struct.pack('!I', random.getrandbits(32))
@@ -286,11 +340,52 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
This coroutine sends a pong.
An unsolicited pong may serve as a unidirectional heartbeat.
+
+ The content may be overridden with the optional ``data`` argument
+ which must be of type :class:`str` (which will be encoded to UTF-8) or
+ :class:`bytes`.
+
"""
+ yield from self.ensure_open()
+
+ data = self.encode_data(data)
+
yield from self.write_frame(OP_PONG, data)
# Private methods - no guarantees.
+ def encode_data(self, data):
+ # Expect str or bytes, return bytes.
+ if isinstance(data, str):
+ return data.encode('utf-8')
+ elif isinstance(data, bytes):
+ return data
+ else:
+ raise TypeError("data must be bytes or str")
+
+ @asyncio.coroutine
+ def ensure_open(self):
+ # Raise a suitable exception if the connection isn't open.
+ # Handle cases from the most common to the least common.
+
+ if self.state == OPEN:
+ return
+
+ if self.state == CLOSED:
+ raise ConnectionClosed(self.close_code, self.close_reason)
+
+ # If the closing handshake is in progress, let it complete to get the
+ # proper close status and code. As an safety measure, the timeout is
+ # longer than the worst case (2 * self.timeout) but not unlimited.
+ if self.state == CLOSING:
+ yield from asyncio.wait_for(
+ self.worker, 3 * self.timeout, loop=self.loop)
+ raise ConnectionClosed(self.close_code, self.close_reason)
+
+ # Control may only reach this point in buggy third-party subclasses.
+ assert self.state == CONNECTING
+ raise InvalidState("WebSocket connection isn't established yet.")
+
@asyncio.coroutine
def run(self):
# This coroutine guarantees that the connection is closed at exit.
@@ -375,6 +470,7 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
# 6.2. Receiving Data
while True:
frame = yield from self.read_frame(max_size)
+
# 5.5. Control Frames
if frame.opcode == OP_CLOSE:
# Make sure the close frame is valid before echoing it.
@@ -382,13 +478,14 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
if self.state == OPEN:
# 7.1.3. The WebSocket Closing Handshake is Started
yield from self.write_frame(OP_CLOSE, frame.data)
- if not self.closing_handshake.done():
- self.close_code, self.close_reason = code, reason
- self.closing_handshake.set_result(True)
+ self.close_code, self.close_reason = code, reason
+ self.closing_handshake.set_result(True)
return
+
elif frame.opcode == OP_PING:
# Answer pings.
yield from self.pong(frame.data)
+
elif frame.opcode == OP_PONG:
# Do not acknowledge pings on unsolicited pongs.
if frame.data in self.pings:
@@ -398,6 +495,7 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
ping_id, waiter = self.pings.popitem(0)
if not waiter.cancelled():
waiter.set_result(None)
+
# 5.6. Data Frames
else:
return frame
@@ -413,10 +511,11 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
@asyncio.coroutine
def write_frame(self, opcode, data=b''):
- # This may happen if a user attempts to write on a closed connection.
- if self.state != OPEN:
+ # Defensive assertion for protocol compliance.
+ if self.state != OPEN: # pragma: no cover
raise InvalidState("Cannot write to a WebSocket "
"in the {} state".format(self.state_name))
+
# Make sure no other frame will be sent after a close frame. Do this
# before yielding control to avoid sending more than one close frame.
if opcode == OP_CLOSE:
@@ -426,12 +525,32 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
logger.debug("%s >> %s", side, frame)
is_masked = self.is_client
write_frame(frame, self.writer.write, is_masked)
+
+ # Backport of the combined logic of:
+ # https://github.com/python/asyncio/pull/280
+ # https://github.com/python/asyncio/pull/291
+ # Remove when dropping support for Python < 3.6.
+ transport = self.writer._transport
+ if transport is not None: # pragma: no cover
+ # PR 291 added the is_closing method to transports shortly after
+ # PR 280 fixed the bug we're trying to work around in this block.
+ if not hasattr(transport, 'is_closing'):
+ # This emulates what is_closing would return if it existed.
+ try:
+ is_closing = transport._closing
+ except AttributeError:
+ is_closing = transport._closed
+ if is_closing:
+ yield
+
try:
# Handle flow control automatically.
yield from self.writer.drain()
- except ConnectionResetError:
+ except ConnectionError:
# Terminate the connection if the socket died.
yield from self.fail_connection(1006)
+ # And raise an exception, since the frame couldn't be sent.
+ raise ConnectionClosed(self.close_code, self.close_reason)
@asyncio.coroutine
def close_connection(self, force=False):
@@ -476,7 +595,7 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
logger.info("Failing the WebSocket connection: %d %s", code, reason)
if self.state == OPEN:
if code == 1006:
- # Don't send a close frame is the connection is broken. Set
+ # Don't send a close frame if the connection is broken. Set
# the state to CLOSING to allow close_connection to proceed.
self.state = CLOSING
else:
@@ -493,7 +612,21 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
self.reader = reader
self.writer = writer
# Start the task that handles incoming messages.
- self.worker = asyncio.async(self.run(), loop=self.loop)
+ self.worker = asyncio_ensure_future(self.run(), loop=self.loop)
+
+ def eof_received(self):
+ super().eof_received()
+ # Since Python 3.5, StreamReaderProtocol.eof_received() returns True
+ # to leave the transport open (http://bugs.python.org/issue24539).
+ # This is inappropriate for websockets for at least three reasons.
+ # 1. The use case is to read data until EOF with self.reader.read(-1).
+ # Since websockets is a TLV protocol, this never happens.
+ # 2. It doesn't work on SSL connections. A falsy value must be
+ # returned to have the same behavior on SSL and plain connections.
+ # 3. The websockets protocol has its own closing handshake. Endpoints
+ # close the TCP connection after sending a Close frame.
+ # As a consequence we revert to the previous, more useful behavior.
+ return
def connection_lost(self, exc):
# 7.1.4. The WebSocket Connection is Closed
@@ -503,4 +636,7 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
self.closing_handshake.set_result(False)
if not self.connection_closed.done():
self.connection_closed.set_result(None)
+ # Close the transport in case close_connection() wasn't executed.
+ if self.writer is not None:
+ self.writer.close()
super().connection_lost(exc)
diff --git a/websockets/py35_client.py b/websockets/py35_client.py
new file mode 100644
index 0000000..5ab7af0
--- /dev/null
+++ b/websockets/py35_client.py
@@ -0,0 +1,21 @@
+class Connect:
+ """
+ This class wraps :func:`~websockets.client.connect` on Python ≥ 3.5.
+
+ This allows using it as an asynchronous context manager.
+
+ """
+ def __init__(self, *args, **kwargs):
+ self.client = self.__class__.__wrapped__(*args, **kwargs)
+
+ async def __aenter__(self):
+ self.websocket = await self
+ return self.websocket
+
+ async def __aexit__(self, exc_type, exc_value, traceback):
+ await self.websocket.close()
+
+ def __await__(self):
+ return (yield from self.client)
+
+ __iter__ = __await__
diff --git a/websockets/py35_test_client_server.py b/websockets/py35_test_client_server.py
new file mode 100644
index 0000000..0b40df9
--- /dev/null
+++ b/websockets/py35_test_client_server.py
@@ -0,0 +1,33 @@
+# Tests containing Python 3.5+ syntax, extracted from test_client_server.py.
+# To avoid test discovery, this module's name must not start with test_.
+
+import asyncio
+
+from .client import *
+from .server import *
+from .test_client_server import handler
+
+
+class ClientServerContextManager:
+
+ def setUp(self):
+ self.loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(self.loop)
+
+ def tearDown(self):
+ self.loop.close()
+
+ def test_basic(self):
+ server = serve(handler, 'localhost', 8642)
+ self.server = self.loop.run_until_complete(server)
+
+ async def basic():
+ async with connect('ws://localhost:8642/') as client:
+ await client.send("Hello!")
+ reply = await client.recv()
+ self.assertEqual(reply, "Hello!")
+
+ self.loop.run_until_complete(basic())
+
+ self.server.close()
+ self.loop.run_until_complete(self.server.wait_closed())
diff --git a/websockets/server.py b/websockets/server.py
index 94135d1..763d6ff 100644
--- a/websockets/server.py
+++ b/websockets/server.py
@@ -1,5 +1,6 @@
"""
The :mod:`websockets.server` module defines a simple WebSocket server API.
+
"""
__all__ = ['serve', 'WebSocketServerProtocol']
@@ -9,6 +10,7 @@ import collections.abc
import email.message
import logging
+from .compatibility import asyncio_ensure_future
from .exceptions import InvalidHandshake, InvalidOrigin
from .handshake import build_response, check_request
from .http import USER_AGENT, read_request
@@ -27,8 +29,8 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
For the sake of simplicity, it doesn't rely on a full HTTP implementation.
Its support for HTTP responses is very limited.
- """
+ """
state = CONNECTING
def __init__(self, ws_handler, ws_server, *,
@@ -42,7 +44,13 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
def connection_made(self, transport):
super().connection_made(transport)
- self.handler_task = asyncio.async(self.handler(), loop=self.loop)
+ # Register the connection with the server when creating the handler
+ # task. (Registering at the beginning of the handler coroutine would
+ # create a race condition between the creation of the task, which
+ # schedules its execution, and the moment the handler starts running.)
+ self.ws_server.register(self)
+ self.handler_task = asyncio_ensure_future(
+ self.handler(), loop=self.loop)
@asyncio.coroutine
def handler(self):
@@ -86,6 +94,13 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
except Exception: # pragma: no cover
pass
... 980 lines suppressed ...
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-websockets.git
More information about the Python-modules-commits
mailing list