[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