From b03d29b502b557dbfb6ab778eaa1a04c29878def Mon Sep 17 00:00:00 2001 From: Jon Betts Date: Fri, 30 Apr 2021 14:06:08 +0100 Subject: [PATCH 01/29] Avoid the use of the word "async" for Python 3.7+ compatibility Also some white space changes, might be line endings? --- test/autobahn_test_servers.py | 10 +++++----- ws4py/_asyncio_compat.py | 9 +++++++++ ws4py/async_websocket.py | 5 +++-- ws4py/server/tulipserver.py | 33 +++++++++++++++++---------------- 4 files changed, 34 insertions(+), 23 deletions(-) create mode 100644 ws4py/_asyncio_compat.py diff --git a/test/autobahn_test_servers.py b/test/autobahn_test_servers.py index 3e33f82..fdf8d5d 100644 --- a/test/autobahn_test_servers.py +++ b/test/autobahn_test_servers.py @@ -85,7 +85,7 @@ def run_python3_asyncio(host="127.0.0.1", port=9009): wsaccel.patch_ws4py() from ws4py.async_websocket import EchoWebSocket from ws4py.server.tulipserver import WebSocketProtocol - + loop = asyncio.get_event_loop() def start_server(): @@ -121,14 +121,14 @@ def run_autobahn_server(host="127.0.0.1", port=9003): from twisted.internet import reactor from autobahn.twisted.websocket import WebSocketServerProtocol, \ WebSocketServerFactory - + class MyServerProtocol(WebSocketServerProtocol): def onMessage(self, payload, isBinary): self.sendMessage(payload, isBinary) logger = logging.getLogger('autobahn_testsuite') logger.warning("Serving Autobahn server on %s:%s" % (host, port)) - + factory = WebSocketServerFactory("ws://%s:%d" % (host, port)) factory.protocol = MyServerProtocol @@ -142,7 +142,7 @@ def run_python_wsgi(host="127.0.0.1", port=9002): """ run_python_wsgi_async(host, port, False) -def run_python_wsgi_async(host="127.0.0.1", port=9010, async=True): +def run_python_wsgi_async(host="127.0.0.1", port=9010, async_=True): """ Runs wsgi server on python 2.x with async middleware" """ @@ -153,7 +153,7 @@ def run_python_wsgi_async(host="127.0.0.1", port=9010, async=True): from ws4py.server.wsgiutils import WebSocketWSGIApplication app = WebSocketWSGIApplication(handler_cls=EchoWebSocket) - if async: + if async_: def middleware(app): def later(environ, start_response): for part in app(environ, start_response): diff --git a/ws4py/_asyncio_compat.py b/ws4py/_asyncio_compat.py new file mode 100644 index 0000000..baec0cd --- /dev/null +++ b/ws4py/_asyncio_compat.py @@ -0,0 +1,9 @@ +"""Provide compatibility over different versions of asyncio.""" + +import asyncio + +if hasattr(asyncio, "async"): + # Compatibility for Python 3.3 and older + ensure_future = getattr(asyncio, "async") +else: + ensure_future = asyncio.ensure_future \ No newline at end of file diff --git a/ws4py/async_websocket.py b/ws4py/async_websocket.py index 9e2a4c7..177f471 100644 --- a/ws4py/async_websocket.py +++ b/ws4py/async_websocket.py @@ -19,6 +19,7 @@ import types from ws4py.websocket import WebSocket as _WebSocket +from ws4py import _asyncio_compat from ws4py.messaging import Message __all__ = ['WebSocket', 'EchoWebSocket'] @@ -84,7 +85,7 @@ def close_connection(self): def closeit(): yield from self.proto.writer.drain() self.proto.writer.close() - asyncio.async(closeit()) + _asyncio_compat.ensure_future(closeit()) def _write(self, data): """ @@ -94,7 +95,7 @@ def _write(self, data): def sendit(data): self.proto.writer.write(data) yield from self.proto.writer.drain() - asyncio.async(sendit(data)) + _asyncio_compat.ensure_future(sendit(data)) @asyncio.coroutine def run(self): diff --git a/ws4py/server/tulipserver.py b/ws4py/server/tulipserver.py index 2786c16..fdf749c 100644 --- a/ws4py/server/tulipserver.py +++ b/ws4py/server/tulipserver.py @@ -9,6 +9,7 @@ from ws4py import WS_KEY, WS_VERSION from ws4py.exc import HandshakeError from ws4py.websocket import WebSocket +from ws4py import _asyncio_compat LF = b'\n' CRLF = b'\r\n' @@ -25,7 +26,7 @@ def __init__(self, handler_cls): def _pseudo_connected(self, reader, writer): pass - + def connection_made(self, transport): """ A peer is now connected and we receive an instance @@ -40,17 +41,17 @@ def connection_made(self, transport): #self.stream.set_transport(transport) asyncio.StreamReaderProtocol.connection_made(self, transport) # Let make it concurrent for others to tag along - f = asyncio.async(self.handle_initial_handshake()) + f = _asyncio_compat.ensure_future(self.handle_initial_handshake()) f.add_done_callback(self.terminated) @property def writer(self): return self._stream_writer - + @property def reader(self): return self._stream_reader - + def terminated(self, f): if f.done() and not f.cancelled(): ex = f.exception() @@ -70,12 +71,12 @@ def close(self): transport. """ self.ws.close() - + def timeout(self): self.ws.close_connection() if self.ws.started: self.ws.closed(1002, "Peer connection timed-out") - + def connection_lost(self, exc): """ The peer connection is now, the closing @@ -88,7 +89,7 @@ def connection_lost(self, exc): self.ws.close_connection() if self.ws.started: self.ws.closed(1002, "Peer connection was lost") - + @asyncio.coroutine def handle_initial_handshake(self): """ @@ -100,15 +101,15 @@ def handle_initial_handshake(self): """ request_line = yield from self.next_line() method, uri, req_protocol = request_line.strip().split(SPACE, 2) - + # GET required if method.upper() != b'GET': raise HandshakeError('HTTP method must be a GET') - + headers = yield from self.read_headers() if req_protocol == b'HTTP/1.1' and 'Host' not in headers: raise ValueError("Missing host header") - + for key, expected_value in [('Upgrade', 'websocket'), ('Connection', 'upgrade')]: actual_value = headers.get(key, '').lower() @@ -160,7 +161,7 @@ def handle_initial_handshake(self): self.ws.protocols = ws_protocols self.ws.extensions = ws_extensions self.ws.headers = headers - + response = [req_protocol + b' 101 Switching Protocols'] response.append(b'Upgrade: websocket') response.append(b'Content-Type: text/plain') @@ -184,7 +185,7 @@ def handle_websocket(self): exchange is completed and terminated. """ yield from self.ws.run() - + @asyncio.coroutine def read_headers(self): """ @@ -198,21 +199,21 @@ def read_headers(self): if line == CRLF: break return BytesHeaderParser().parsebytes(headers) - + @asyncio.coroutine def next_line(self): """ Reads data until \r\n is met and then return all read - bytes. + bytes. """ line = yield from self.reader.readline() if not line.endswith(CRLF): raise ValueError("Missing mandatory trailing CRLF") return line - + if __name__ == '__main__': from ws4py.async_websocket import EchoWebSocket - + loop = asyncio.get_event_loop() def start_server(): From 6ffe57fd5fbf673b73cda8e4b15c359acec1b6ca Mon Sep 17 00:00:00 2001 From: Jon Betts Date: Fri, 30 Apr 2021 14:08:10 +0100 Subject: [PATCH 02/29] Remove Python compatiblity below 3.6 and add 3.6, 3.7, 3.8 and 3.9 This also fixes the tox file to run all of these tests. --- .python-version | 5 +++++ .travis.yml | 10 ++++++---- setup.py | 7 ++++--- tox.ini | 5 ++++- 4 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 .python-version diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..f00bd00 --- /dev/null +++ b/.python-version @@ -0,0 +1,5 @@ +3.9.4 +3.8.9 +3.7.10 +3.6.13 +2.7.18 diff --git a/.travis.yml b/.travis.yml index d345f1d..8c22b32 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,18 +2,20 @@ language: python python: - 2.7 - - 3.3 - - 3.5 + - 3.6 + - 3.7 + - 3.8 + - 3.9 before_install: - sudo apt-get install python-dev libevent-dev - pip install Cython install: - - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install -r requirements/py2kreqs.txt; fi + - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install -r requirements/py2kreqs.txt; fi - if [[ $TRAVIS_PYTHON_VERSION == 3* ]]; then pip install -r requirements/py3kreqs.txt; fi - python setup.py install -script: +script: - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then py.test -v; fi - if [[ $TRAVIS_PYTHON_VERSION == 3* ]]; then py.test -v; fi diff --git a/setup.py b/setup.py index c9452fa..72fc274 100644 --- a/setup.py +++ b/setup.py @@ -58,9 +58,10 @@ def find_package_modules(self, package, package_dir): 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Communications', diff --git a/tox.ini b/tox.ini index 6621a19..66ac4ba 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,10 @@ # and then run "tox" from this directory. [tox] -envlist = py27 +envlist = py27,py36,py37,py38,py39 [testenv] commands = python setup.py test +deps = + py27: -r requirements/py2kreqs.txt + {py36,py37,py38,py39}: -r requirements/py3kreqs.txt \ No newline at end of file From 74c85a1653065920547df711f69624866fc1699e Mon Sep 17 00:00:00 2001 From: Jon Betts Date: Fri, 30 Apr 2021 14:12:29 +0100 Subject: [PATCH 03/29] Prevent the log tests from leaving a file after they run --- test/test_logger.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/test/test_logger.py b/test/test_logger.py index a39e56d..5e84624 100644 --- a/test/test_logger.py +++ b/test/test_logger.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -import logging +import logging import logging.handlers as handlers import os, os.path import unittest @@ -15,27 +15,31 @@ def clean_logger(): except KeyError: pass logger.removeHandler(handler) - + class WSTestLogger(unittest.TestCase): + LOG_FILE = './my.log' + def tearDown(self): clean_logger() - + if os.path.exists(self.LOG_FILE): + os.remove(self.LOG_FILE) + def test_named_logger(self): - logger = configure_logger(stdout=False, filepath='./my.log') + logger = configure_logger(stdout=False, filepath=self.LOG_FILE) logger = logging.getLogger('ws4py') self.assertEqual(logger.getEffectiveLevel(), logging.INFO) - + def test_level(self): - logger = configure_logger(stdout=True, filepath='./my.log', + logger = configure_logger(stdout=True, filepath=self.LOG_FILE, level=logging.DEBUG) self.assertEqual(logger.getEffectiveLevel(), logging.DEBUG) for handler in logger.handlers: self.assertEqual(handler.level, logging.DEBUG) - + def test_file_logger(self): - filepath = os.path.abspath('./my.log') + filepath = os.path.abspath(self.LOG_FILE) logger = configure_logger(stdout=False, filepath=filepath) for handler in logger.handlers: if isinstance(handler, handlers.RotatingFileHandler): From 3e54d09243a42594e1561b18bb2bb5ec2b3c9d19 Mon Sep 17 00:00:00 2001 From: Jon Betts Date: Fri, 30 Apr 2021 14:53:30 +0100 Subject: [PATCH 04/29] Mention the Python changes in the CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d26ed44..89d6e57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ ## Unreleased [Full Changelog](https://github.com/Lawouach/WebSocket-for-Python/compare/0.5.1...master) +**Changes:** + + * Upgrade Python support to include 3.6, 3.7, 3.8 and 3.9 + * Drop support for Python 3.* < 3.6 (Python 2.7 remains) + ## [0.5.1](https://github.com/Lawouach/WebSocket-for-Python/tree/0.5.1) (2018-02-28) [Full Changelog](https://github.com/Lawouach/WebSocket-for-Python/compare/0.5.0...0.5.1) **Merged pull requests:** From c3149976c93d47d5e9f3fc5eed7ba5c0735739ae Mon Sep 17 00:00:00 2001 From: Yuan-Hsiang Lee Date: Sun, 21 May 2023 04:06:33 -0600 Subject: [PATCH 05/29] use build_py from setuptools (#273) --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 72fc274..47f4e79 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,10 @@ from setuptools import setup except ImportError: from distutils.core import setup -from distutils.command.build_py import build_py +try: + from setuptools.command.build_py import build_py +except ImportError: + from distutils.command.build_py import build_py class buildfor2or3(build_py): def find_package_modules(self, package, package_dir): From fefab1fbcf8814b9ae1a6523d0dd95114392cfef Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Sun, 21 May 2023 21:41:34 +0600 Subject: [PATCH 06/29] Update README.md by removing warning for now. (#274) Signed-off-by: Asif Saif Uddin --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 4645859..97c530f 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,6 @@ Read the [documentation](https://ws4py.readthedocs.org/en/latest/) for more info You can also join the [ws4py mailing-list](http://groups.google.com/group/ws4py) to discuss the library. -**WARNING**: This project is [on hiatus](https://opensource.guide/best-practices/#share-the-workload) -and [does not receive active maintainance](http://www.defuze.org/archives/409-ws4py-is-eager-for-a-new-maintainer.html). -Please be aware of this when deciding to rely on it as contributions may be slow -to make their way to a new release. If you feel like offering help in -maintaining it, [please let us know](https://groups.google.com/forum/#!forum/ws4py). ## Installation From 6cf490d44836c13c45136ac11d51b31845f14dc7 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Thu, 9 Nov 2023 13:36:45 +0600 Subject: [PATCH 07/29] Create python-package.yml (#279) Signed-off-by: Asif Saif Uddin --- .github/workflows/python-package.yml | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/python-package.yml diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..9cbf1c5 --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,40 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Python package + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pytest From b5a477e563abe03fd8d24e8a5c3f8211d95b736b Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Fri, 10 Nov 2023 16:04:48 +0600 Subject: [PATCH 08/29] try more old python versions on the CI for temporary basis (#281) * try more old python versions on the CI for temporary basis Signed-off-by: Asif Saif Uddin * downgrade ubuntu to run python 3.6 Signed-off-by: Asif Saif Uddin --------- Signed-off-by: Asif Saif Uddin --- .github/workflows/python-package.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 9cbf1c5..d011374 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -12,11 +12,11 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v4 From 9ffee997a128ea9d7b09fb1dcf4aa280d25ebdc9 Mon Sep 17 00:00:00 2001 From: ANANTHAKRISHNAN U S Date: Sun, 12 May 2024 16:45:04 +0530 Subject: [PATCH 09/29] Fix ssl.wrap_socket() is deprecated (#283) self signed certificate; no need to verify signed commit final commit --- ws4py/client/__init__.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/ws4py/client/__init__.py b/ws4py/client/__init__.py index 411638f..0c2cb5f 100644 --- a/ws4py/client/__init__.py +++ b/ws4py/client/__init__.py @@ -81,11 +81,6 @@ def __init__(self, url, protocols=None, extensions=None, self.exclude_headers = exclude_headers or [] self.exclude_headers = [x.lower() for x in self.exclude_headers] - if self.scheme == "wss": - # Prevent check_hostname requires server_hostname (ref #187) - if "cert_reqs" not in self.ssl_options: - self.ssl_options["cert_reqs"] = ssl.CERT_NONE - self._parse_url() if self.unix_socket_path: @@ -211,7 +206,15 @@ def connect(self): """ if self.scheme == "wss": # default port is now 443; upgrade self.sender to send ssl - self.sock = ssl.wrap_socket(self.sock, **self.ssl_options) + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + if self.ssl_options.get('certfile', None): + context.load_cert_chain(self.ssl_options.get('certfile'), self.ssl_options.get('keyfile')) + # Prevent check_hostname requires server_hostname (ref #187) + if "cert_reqs" not in self.ssl_options: + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + self.sock = context.wrap_socket(self.sock) self._is_secure = True self.sock.connect(self.bind_addr) From 0aefaede95208fab141dffb4c70d4fd9ca6c4c6e Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Wed, 10 Jul 2024 16:01:25 +0600 Subject: [PATCH 10/29] Update CI to run tox (#286) Signed-off-by: Asif Saif Uddin --- .github/workflows/python-package.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index d011374..2757b3d 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -27,7 +27,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install flake8 pytest + python -m pip install flake8 pytest tox if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | @@ -35,6 +35,6 @@ jobs: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest + - name: Test with tox run: | - pytest + tox -e py From 5c9fc1170d0b59cfb4111cd14eee320a081112e3 Mon Sep 17 00:00:00 2001 From: Eli Courtwright Date: Mon, 9 Sep 2024 09:59:55 -0400 Subject: [PATCH 11/29] Added tls support for validating certificates (#285) The implementation made several months ago at https://github.com/Lawouach/WebSocket-for-Python/commit/9ffee997a128ea9d7b09fb1dcf4aa280d25ebdc9 works properly when turning on wss:// URLs but fails when trying to turn on server cert validation. This fixes the issues with cert validation - I made this change on my day job's internal Gitlab, and I'm making a PR back into the public repo to contribute the fix back. --- ws4py/client/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ws4py/client/__init__.py b/ws4py/client/__init__.py index 0c2cb5f..b24ace6 100644 --- a/ws4py/client/__init__.py +++ b/ws4py/client/__init__.py @@ -206,15 +206,20 @@ def connect(self): """ if self.scheme == "wss": # default port is now 443; upgrade self.sender to send ssl - context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + protocol = getattr(ssl, 'PROTOCOL_TLS_CLIENT', ssl.PROTOCOL_TLSv1_2) # PROTOCOL_TLS_CLIENT is correct for newer Python but doesn't exist on older Pythons + context = ssl.SSLContext(protocol) if self.ssl_options.get('certfile', None): context.load_cert_chain(self.ssl_options.get('certfile'), self.ssl_options.get('keyfile')) + + if self.ssl_options.get('ca_certs'): + context.load_verify_locations(self.ssl_options['ca_certs']) + # Prevent check_hostname requires server_hostname (ref #187) if "cert_reqs" not in self.ssl_options: context.check_hostname = False context.verify_mode = ssl.CERT_NONE - self.sock = context.wrap_socket(self.sock) + self.sock = context.wrap_socket(self.sock, server_hostname=self.host) self._is_secure = True self.sock.connect(self.bind_addr) From 22f8bb7680bbd3262de44ebdfd6a9c3a8c9d31f9 Mon Sep 17 00:00:00 2001 From: Alexandre Detiste Date: Tue, 17 Sep 2024 13:00:45 +0200 Subject: [PATCH 12/29] prefer newer unittest.mock when available (#284) * prefer newer unittest.mock when available * Update test/test_client.py Signed-off-by: Asif Saif Uddin * flake8 --------- Signed-off-by: Asif Saif Uddin Co-authored-by: Asif Saif Uddin --- docs/sources/basics.rst | 2 +- example/basic/app.py | 2 +- example/droid_sensor.py | 4 +++- example/droid_sensor_cherrypy_server.py | 8 +++++--- example/websensors/templates/.cache/board.html.py | 4 ++-- example/websensors/templates/.cache/index.html.py | 4 ++-- test/test_cherrypy.py | 5 ++++- test/test_client.py | 6 ++++-- test/test_manager.py | 5 ++++- test/test_utils.py | 9 +++++++-- test/test_websocket.py | 5 ++++- ws4py/compat.py | 2 +- ws4py/server/cherrypyserver.py | 2 +- 13 files changed, 39 insertions(+), 19 deletions(-) diff --git a/docs/sources/basics.rst b/docs/sources/basics.rst index 5d8d271..3bf587a 100644 --- a/docs/sources/basics.rst +++ b/docs/sources/basics.rst @@ -34,7 +34,7 @@ necessarily need a connected socket, in fact, you don't even need a socket at al >>> def data_source(): >>> yield TextMessage(u'hello world') - >>> from mock import MagicMock + >>> from unittest.mock import MagicMock >>> source = MagicMock(side_effect=data_source) >>> ws = EchoWebSocket(sock=source) >>> ws.send(u'hello there') diff --git a/example/basic/app.py b/example/basic/app.py index 7d0745c..c81dcf1 100644 --- a/example/basic/app.py +++ b/example/basic/app.py @@ -24,7 +24,7 @@ cur_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__))) index_path = os.path.join(cur_dir, 'index.html') -index_page = file(index_path, 'r').read() +index_page = open(index_path, 'r').read() class ChatWebSocketHandler(WebSocket): def received_message(self, m): diff --git a/example/droid_sensor.py b/example/droid_sensor.py index 7f9e8af..3699a50 100644 --- a/example/droid_sensor.py +++ b/example/droid_sensor.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from __future__ import print_function + __doc__ = """ WebSocket client that pushes Android sensor metrics to the websocket server it is connected to. @@ -64,7 +66,7 @@ def run(self): continue c = lambda rad: rad * 360.0 / math.pi - print c(azimuth), c(pitch), c(roll), x, y, z + print(c(azimuth), c(pitch), c(roll), x, y, z) if self.client.terminated: break diff --git a/example/droid_sensor_cherrypy_server.py b/example/droid_sensor_cherrypy_server.py index b342540..8bd4d28 100644 --- a/example/droid_sensor_cherrypy_server.py +++ b/example/droid_sensor_cherrypy_server.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from __future__ import print_function + import os.path import cherrypy @@ -8,7 +10,7 @@ class BroadcastWebSocketHandler(WebSocket): def received_message(self, m): cherrypy.engine.publish('websocket-broadcast', str(m)) - + class Root(object): @cherrypy.expose def display(self): @@ -61,7 +63,7 @@ def index(self): """ - + if __name__ == '__main__': cherrypy.config.update({ 'server.socket_host': '0.0.0.0', @@ -69,7 +71,7 @@ def index(self): 'tools.staticdir.root': os.path.abspath(os.path.join(os.path.dirname(__file__), 'static')) } ) - print os.path.abspath(os.path.join(__file__, 'static')) + print(os.path.abspath(os.path.join(__file__, 'static'))) WebSocketPlugin(cherrypy.engine).subscribe() cherrypy.tools.websocket = WebSocketTool() diff --git a/example/websensors/templates/.cache/board.html.py b/example/websensors/templates/.cache/board.html.py index 5bd9f8b..3e73637 100644 --- a/example/websensors/templates/.cache/board.html.py +++ b/example/websensors/templates/.cache/board.html.py @@ -22,9 +22,9 @@ def render_body(context,**pageargs): # SOURCE LINE 1 __M_writer(u'\n\n\n\n \n \n \n \n Mobile remote control\n \n \n \n \n \n\n \n \n \n\n \n \n \n \n\n
\n\t \n\t \n
\n\n \n\t\n \n \n \n \n\n') return '' finally: diff --git a/example/websensors/templates/.cache/index.html.py b/example/websensors/templates/.cache/index.html.py index 28ac0d3..6f8f25a 100644 --- a/example/websensors/templates/.cache/index.html.py +++ b/example/websensors/templates/.cache/index.html.py @@ -21,9 +21,9 @@ def render_body(context,**pageargs): # SOURCE LINE 1 __M_writer(u'\n\n\n\n \n \n \n \n Shared drawing board\n \n \n \n \n \n\n \n \n\n \n \n \n \n\n
\n
\n

shared drawing board

\n
\n
\n\n
\n
\n\n \n\n
\n
\n\n \n\n \n \n \n\n') return '' finally: diff --git a/test/test_cherrypy.py b/test/test_cherrypy.py index 4d184de..326e5df 100644 --- a/test/test_cherrypy.py +++ b/test/test_cherrypy.py @@ -4,7 +4,10 @@ import time import unittest -from mock import MagicMock, call +try: + from unittest.mock import MagicMock, call +except ImportError: + from mock import MagicMock, call import cherrypy from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool diff --git a/test/test_client.py b/test/test_client.py index 7ad6a4b..dd542a6 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -4,8 +4,10 @@ import socket import time import unittest - -from mock import MagicMock, patch +try: + from unittest.mock import MagicMock, patch +except ImportError: + from mock import MagicMock, patch from ws4py import WS_KEY from ws4py.exc import HandshakeError diff --git a/test/test_manager.py b/test/test_manager.py index 126c714..9a8d347 100644 --- a/test/test_manager.py +++ b/test/test_manager.py @@ -8,7 +8,10 @@ except ImportError: from itertools import zip_longest -from mock import MagicMock, call, patch +try: + from unittest.mock import MagicMock, call, patch +except ImportError: + from mock import MagicMock, call, patch from ws4py.manager import WebSocketManager, SelectPoller,\ EPollPoller diff --git a/test/test_utils.py b/test/test_utils.py index 4f21afd..3e48aef 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -1,9 +1,14 @@ # -*- coding: utf-8 -*- import unittest +try: + from unittest.mock import MagicMock +except ImportError: + from mock import MagicMock + from ws4py import format_addresses from ws4py.websocket import WebSocket -from mock import MagicMock + class WSUtilities(unittest.TestCase): def test_format_address(self): @@ -14,7 +19,7 @@ def test_format_address(self): log = format_addresses(ws) self.assertEqual(log, "[Local => 127.0.0.1:52300 | Remote => 127.0.0.1:4800]") - + if __name__ == '__main__': suite = unittest.TestSuite() loader = unittest.TestLoader() diff --git a/test/test_websocket.py b/test/test_websocket.py index 96fb384..981d2b7 100644 --- a/test/test_websocket.py +++ b/test/test_websocket.py @@ -4,7 +4,10 @@ import socket import struct -from mock import MagicMock, call, patch +try: + from unittest.mock import MagicMock, call, patch +except ImportError: + from mock import MagicMock, call, patch from ws4py.framing import Frame, \ OPCODE_CONTINUATION, OPCODE_TEXT, \ diff --git a/ws4py/compat.py b/ws4py/compat.py index e986e33..5b8870d 100644 --- a/ws4py/compat.py +++ b/ws4py/compat.py @@ -34,7 +34,7 @@ def ord(c): else: py3k = False from urlparse import urlsplit - range = xrange + range = xrange # noqa: F821 unicode = unicode basestring = basestring ord = ord diff --git a/ws4py/server/cherrypyserver.py b/ws4py/server/cherrypyserver.py index 5b93465..49ffebf 100644 --- a/ws4py/server/cherrypyserver.py +++ b/ws4py/server/cherrypyserver.py @@ -379,4 +379,4 @@ def index(self): cherrypy.log("Handler created: %s" % repr(cherrypy.request.ws_handler)) cherrypy.quickstart(Root(), '/', config={'/': {'tools.websocket.on': True, - 'tools.websocket.handler_cls': EchoWebSocketHandler}}) + 'tools.websocket.handler_cls': EchoWebSocketHandler}}) # noqa: F821 From cb60792322c55e22896ff8955b220641895dad46 Mon Sep 17 00:00:00 2001 From: Vasily Zakharov Date: Wed, 18 Sep 2024 12:04:22 +0300 Subject: [PATCH 13/29] =?UTF-8?q?Fix=20for=20=D1=81losing=20message=20form?= =?UTF-8?q?atting=20(#289)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Issue #276 fixed * Fixed flake8 warning --------- Co-authored-by: Vasily Zakharov --- example/mocking_data_source.py | 2 +- ws4py/websocket.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/example/mocking_data_source.py b/example/mocking_data_source.py index a830475..1fd7ae2 100644 --- a/example/mocking_data_source.py +++ b/example/mocking_data_source.py @@ -30,7 +30,7 @@ def recv(self, size): current_bytes = self.remaining_bytes[:size] self.remaining_bytes = self.remaining_bytes[size:] - if self.remaining_bytes is b'': + if self.remaining_bytes == b'': self.frame = None self.remaining_bytes = None diff --git a/ws4py/websocket.py b/ws4py/websocket.py index 61f8c33..6d94650 100644 --- a/ws4py/websocket.py +++ b/ws4py/websocket.py @@ -462,7 +462,7 @@ def process(self, bytes): self.reading_buffer_size = s.parser.send(bytes) or DEFAULT_READING_SIZE if s.closing is not None: - logger.debug("Closing message received (%d) '%s'" % (s.closing.code, s.closing.reason)) + logger.debug("Closing message received (%d): %s" % (s.closing.code, s.closing.reason.decode() if isinstance(s.closing.reason, bytes) else s.closing.reason)) if not self.server_terminated: self.close(s.closing.code, s.closing.reason) else: @@ -471,7 +471,7 @@ def process(self, bytes): if s.errors: for error in s.errors: - logger.debug("Error message received (%d) '%s'" % (error.code, error.reason)) + logger.debug("Error message received (%d): %s" % (error.code, error.reason.decode() if isinstance(error.reason, bytes) else error.reason)) self.close(error.code, error.reason) s.errors = [] return False From 5252cb93170bee0edc2bdffaeef927a0f7b03b29 Mon Sep 17 00:00:00 2001 From: Vasily Zakharov Date: Sun, 3 Nov 2024 22:52:50 +0300 Subject: [PATCH 14/29] Renamed process() `bytes` argument to `data` to avoid collision with `bytes` built-in type --- ws4py/websocket.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ws4py/websocket.py b/ws4py/websocket.py index 6d94650..7f27921 100644 --- a/ws4py/websocket.py +++ b/ws4py/websocket.py @@ -440,7 +440,7 @@ def terminate(self): self.stream = None self.environ = None - def process(self, bytes): + def process(self, data): """ Takes some bytes and process them through the internal stream's parser. If a message of any kind is found, performs one of these actions: @@ -456,10 +456,10 @@ def process(self, bytes): """ s = self.stream - if not bytes and self.reading_buffer_size > 0: + if not data and self.reading_buffer_size > 0: return False - self.reading_buffer_size = s.parser.send(bytes) or DEFAULT_READING_SIZE + self.reading_buffer_size = s.parser.send(data) or DEFAULT_READING_SIZE if s.closing is not None: logger.debug("Closing message received (%d): %s" % (s.closing.code, s.closing.reason.decode() if isinstance(s.closing.reason, bytes) else s.closing.reason)) From 6a9e57b69b30fad1f985f12c1889b8add8636cf2 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Sun, 17 Nov 2024 23:50:26 +0600 Subject: [PATCH 15/29] Update python-package.yml (#295) * Update python-package.yml Signed-off-by: Asif Saif Uddin * Update python-package.yml Signed-off-by: Asif Saif Uddin --------- Signed-off-by: Asif Saif Uddin --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 2757b3d..13170d3 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -27,7 +27,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install flake8 pytest tox + python -m pip install flake8 pytest tox CherryPy gevent tornado if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | From 0c411c2f8eeedff7f7eea87450fdb0c82047f864 Mon Sep 17 00:00:00 2001 From: Sylvain Hellegouarch Date: Thu, 19 Dec 2024 10:51:04 +0100 Subject: [PATCH 16/29] advertize Python 3.10 and 3.11 Signed-off-by: Sylvain Hellegouarch --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 47f4e79..c9c2163 100644 --- a/setup.py +++ b/setup.py @@ -65,6 +65,8 @@ def find_package_modules(self, package, package_dir): 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Communications', From 750d9221331037b195abbe8ec236338fa34aae81 Mon Sep 17 00:00:00 2001 From: Sylvain Hellegouarch Date: Thu, 19 Dec 2024 10:53:32 +0100 Subject: [PATCH 17/29] test on 3.10 and 3.11. Also fix command Signed-off-by: Sylvain Hellegouarch --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 66ac4ba..d448e57 100644 --- a/tox.ini +++ b/tox.ini @@ -4,10 +4,10 @@ # and then run "tox" from this directory. [tox] -envlist = py27,py36,py37,py38,py39 +envlist = py27,py36,py37,py38,py39,py310,py311 [testenv] -commands = python setup.py test +commands = pytest deps = py27: -r requirements/py2kreqs.txt - {py36,py37,py38,py39}: -r requirements/py3kreqs.txt \ No newline at end of file + {py36,py37,py38,py39,py310,py311}: -r requirements/py3kreqs.txt \ No newline at end of file From debbb4fb78de34ea4b2f75546362038ea60efb27 Mon Sep 17 00:00:00 2001 From: Sylvain Hellegouarch Date: Thu, 19 Dec 2024 11:10:36 +0100 Subject: [PATCH 18/29] fix tox call and expose 3.12 support Signed-off-by: Sylvain Hellegouarch --- .github/workflows/python-package.yml | 2 +- setup.py | 1 + tox.ini | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 13170d3..2de8879 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -37,4 +37,4 @@ jobs: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with tox run: | - tox -e py + tox diff --git a/setup.py b/setup.py index c9c2163..d94d598 100644 --- a/setup.py +++ b/setup.py @@ -67,6 +67,7 @@ def find_package_modules(self, package, package_dir): 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Communications', diff --git a/tox.ini b/tox.ini index d448e57..b7bf449 100644 --- a/tox.ini +++ b/tox.ini @@ -4,10 +4,10 @@ # and then run "tox" from this directory. [tox] -envlist = py27,py36,py37,py38,py39,py310,py311 +envlist = py27,py36,py37,py38,py39,py310,py311,py312 [testenv] commands = pytest deps = py27: -r requirements/py2kreqs.txt - {py36,py37,py38,py39,py310,py311}: -r requirements/py3kreqs.txt \ No newline at end of file + {py36,py37,py38,py39,py310,py311,py312}: -r requirements/py3kreqs.txt \ No newline at end of file From 75f9fcdee9392a21364b633bd0f41dc289f5daee Mon Sep 17 00:00:00 2001 From: Sylvain Hellegouarch Date: Thu, 19 Dec 2024 11:12:01 +0100 Subject: [PATCH 19/29] build for 3.12 Signed-off-by: Sylvain Hellegouarch --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 2de8879..0b813d1 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 From dcb1e89d632c9c2eca1aa265076604a138997252 Mon Sep 17 00:00:00 2001 From: Sylvain Hellegouarch Date: Thu, 19 Dec 2024 11:16:29 +0100 Subject: [PATCH 20/29] Conditional tox env selector based on GH action Signed-off-by: Sylvain Hellegouarch --- .github/workflows/python-package.yml | 4 ++-- tox.ini | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 0b813d1..83baa87 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -27,7 +27,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install flake8 pytest tox CherryPy gevent tornado + python -m pip install flake8 pytest tox tox-gh-actions CherryPy gevent tornado if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | @@ -37,4 +37,4 @@ jobs: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with tox run: | - tox + tox -e diff --git a/tox.ini b/tox.ini index b7bf449..525727c 100644 --- a/tox.ini +++ b/tox.ini @@ -6,8 +6,19 @@ [tox] envlist = py27,py36,py37,py38,py39,py310,py311,py312 +[gh-actions] +python = + 2.7: py27 + 3.6: py36 + 3.7: py37 + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 + [testenv] commands = pytest deps = py27: -r requirements/py2kreqs.txt - {py36,py37,py38,py39,py310,py311,py312}: -r requirements/py3kreqs.txt \ No newline at end of file + {py36,py37,py38,py39,py310,py311,py312}: -r requirements/py3kreqs.txt From a9a0447e44aa2c744c7fa034bb4d2a4aaa5965e2 Mon Sep 17 00:00:00 2001 From: Sylvain Hellegouarch Date: Thu, 19 Dec 2024 11:19:02 +0100 Subject: [PATCH 21/29] remove unused argument Signed-off-by: Sylvain Hellegouarch --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 83baa87..6d57441 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -37,4 +37,4 @@ jobs: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with tox run: | - tox -e + tox From d2a07971a7645a6911c621f6b415211f2ae2da8f Mon Sep 17 00:00:00 2001 From: Sylvain Hellegouarch Date: Thu, 19 Dec 2024 11:29:41 +0100 Subject: [PATCH 22/29] add release workflow Signed-off-by: Sylvain Hellegouarch --- .github/workflows/release.yaml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/release.yaml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..7eef7cb --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,33 @@ +name: Release + +on: + pull_request: + branches-ignore: + - 'master' + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' + - '[0-9]+.[0-9]+.[0-9]+rc[0-9]+' + +jobs: + release-to-pypi: + runs-on: ubuntu-24.04 + environment: release + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install pypa/build + run: >- + python3 -m + pip install + build + --user + - name: Build a binary wheel and a source tarball + run: python3 -m build + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 From b833d0dee2a67f916358c26ee93d13a4374e7efc Mon Sep 17 00:00:00 2001 From: Sylvain Hellegouarch Date: Thu, 19 Dec 2024 11:41:03 +0100 Subject: [PATCH 23/29] bump to 0.6.0 Signed-off-by: Sylvain Hellegouarch --- CHANGELOG.md | 6 +++++- ws4py/__init__.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89d6e57..89bf158 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,14 @@ ## Unreleased [Full Changelog](https://github.com/Lawouach/WebSocket-for-Python/compare/0.5.1...master) +## [0.6.0](https://github.com/Lawouach/WebSocket-for-Python/tree/0.6.0) (2024-12-19) +[Full Changelog](https://github.com/Lawouach/WebSocket-for-Python/compare/0.5.1...0.6.0) **Changes:** - * Upgrade Python support to include 3.6, 3.7, 3.8 and 3.9 + * Upgrade Python support to include 3.6, 3.7, 3.8, 3.9, 3.10, 3.11 and 3.12 * Drop support for Python 3.* < 3.6 (Python 2.7 remains) + * Fix tox and github action so they work nicely together + * Add a release workflow to Pypi which uses trusted publisher ## [0.5.1](https://github.com/Lawouach/WebSocket-for-Python/tree/0.5.1) (2018-02-28) [Full Changelog](https://github.com/Lawouach/WebSocket-for-Python/compare/0.5.0...0.5.1) diff --git a/ws4py/__init__.py b/ws4py/__init__.py index 5f01c50..c36b838 100644 --- a/ws4py/__init__.py +++ b/ws4py/__init__.py @@ -30,7 +30,7 @@ import logging.handlers as handlers __author__ = "Sylvain Hellegouarch" -__version__ = "0.5.1" +__version__ = "0.6.0" __all__ = ['WS_KEY', 'WS_VERSION', 'configure_logger', 'format_addresses'] WS_KEY = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" From 3c3cc34de2ed67543e264f15ca02b753330dae27 Mon Sep 17 00:00:00 2001 From: Chad Spencer Date: Tue, 24 Dec 2024 01:53:19 -0700 Subject: [PATCH 24/29] Non-blocking read option on socket (#278) * Adds sock_timeout property to WebSocket to allow non-blocking reads Also adds test: test_run for WebSocket.py * Update ws4py/websocket.py Signed-off-by: Asif Saif Uddin * Update test/test_websocket.py Signed-off-by: Asif Saif Uddin --------- Signed-off-by: Asif Saif Uddin Co-authored-by: Asif Saif Uddin --- test/test_websocket.py | 22 ++++++++++++++++++++++ ws4py/websocket.py | 19 ++++++++++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/test/test_websocket.py b/test/test_websocket.py index 981d2b7..42ec208 100644 --- a/test/test_websocket.py +++ b/test/test_websocket.py @@ -181,6 +181,28 @@ def test_sending_ping(self): m.sendall.assert_called_once_with(tm) + @patch("ws4py.websocket.Heartbeat") + def test_run(self, mocker): + mocked_sock = MagicMock() + mocked_opened = MagicMock() + mocked_once = MagicMock(return_value=False) # False to break the loop + mocked_terminate = MagicMock() + + ws = WebSocket(sock=mocked_sock) + assert ws.sock_timeout is None + + with patch.multiple(ws, + opened=mocked_opened, + once=mocked_once, + terminate=mocked_terminate, + stream=MagicMock(), + ): + ws.run() + mocked_sock.settimeout.assert_called_with(None) + mocked_opened.assert_called() + mocked_once.assert_called() + mocked_terminate.assert_called() + if __name__ == '__main__': suite = unittest.TestSuite() loader = unittest.TestLoader() diff --git a/ws4py/websocket.py b/ws4py/websocket.py index 7f27921..69e060e 100644 --- a/ws4py/websocket.py +++ b/ws4py/websocket.py @@ -141,6 +141,17 @@ def __init__(self, sock, protocols=None, extensions=None, environ=None, heartbea "Internal buffer to get around SSL problems" self.buf = b'' + self.sock_timeout = None + """ + Used to set socket.settimeout(value): + From: https://docs.python.org/3.11/library/socket.html#socket.socket.settimeout + The value argument can be a nonnegative floating point number expressing seconds, or None. + If a non-zero value is given, subsequent socket operations will raise a timeout exception + if the timeout period value has elapsed before the operation has completed. + If zero is given, the socket is put in non-blocking mode. + If None is given, the socket is put in blocking mode. + """ + self._local_address = None self._peer_address = None @@ -515,10 +526,12 @@ def run(self): we initiate the closing of the connection with the appropiate error code. - This method is blocking and should likely be run - in a thread. + The self.sock_timeout determines whether this method + is blocking, or can timeout on reads. If a timeout + occurs, the unhandled_error function will be called + It should likely be run in a thread. """ - self.sock.setblocking(True) + self.sock.settimeout(self.sock_timeout) with Heartbeat(self, frequency=self.heartbeat_freq): s = self.stream From 4989bb7bf41a846c28e97b955fdbbd4f2eb5c7f9 Mon Sep 17 00:00:00 2001 From: Vasily Zakharov Date: Tue, 24 Dec 2024 11:57:13 +0300 Subject: [PATCH 25/29] Avoid ResourceWarning if shutdown failed for some reason (#272) * Avoiding ResourceWarning if shutdown failed for some reason. Typically it looks like this: /home/user/.local/lib/python3.8/site-packages/ws4py/websocket.py:230: ResourceWarning: unclosed * Update ws4py/websocket.py Signed-off-by: Asif Saif Uddin --------- Signed-off-by: Asif Saif Uddin Co-authored-by: Vasily Zakharov Co-authored-by: Asif Saif Uddin --- ws4py/websocket.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ws4py/websocket.py b/ws4py/websocket.py index 69e060e..4dd6259 100644 --- a/ws4py/websocket.py +++ b/ws4py/websocket.py @@ -234,11 +234,14 @@ def close_connection(self): if self.sock: try: self.sock.shutdown(socket.SHUT_RDWR) + except: + pass + try: self.sock.close() except: pass - finally: - self.sock = None + self.sock = None + def ping(self, message): """ From 6c8bc76cfffc36896784ecd6c98bec8dbb54cc18 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Wed, 7 May 2025 17:58:55 +0600 Subject: [PATCH 26/29] Update python-package.yml (#299) Signed-off-by: Asif Saif Uddin --- .github/workflows/python-package.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 6d57441..38ac371 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -12,11 +12,11 @@ on: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 From a4b059c0f42c085ceb179140e7dc52fe539d454e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=86=B2?= Date: Wed, 7 May 2025 20:02:49 +0800 Subject: [PATCH 27/29] Fix passing host to in headers (#268) * Fix passing host to in headers * Update ws4py/client/__init__.py Signed-off-by: Asif Saif Uddin * Add unit test for passing host to in headers --------- Signed-off-by: Asif Saif Uddin Co-authored-by: Asif Saif Uddin --- test/test_client.py | 9 +++++++++ ws4py/client/__init__.py | 7 ++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/test/test_client.py b/test/test_client.py index dd542a6..bdb8459 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -106,6 +106,15 @@ def test_parse_wss_scheme_with_query_string(self): self.assertEqual(c.resource, "/?token=value") self.assertEqual(c.bind_addr, ("127.0.0.1", 443)) + def test_overriding_host_from_headers(self): + c = WebSocketBaseClient(url="wss://127.0.0.1", headers=[("Host", "example123.com")]) + self.assertEqual(c.host, "127.0.0.1") + self.assertEqual(c.port, 443) + self.assertEqual(c.bind_addr, ("127.0.0.1", 443)) + for h in c.handshake_headers: + if h[0].lower() == "host": + self.assertEqual(h[1], "example123.com") + @patch('ws4py.client.socket') def test_connect_and_close(self, sock): diff --git a/ws4py/client/__init__.py b/ws4py/client/__init__.py index b24ace6..4bc7c1a 100644 --- a/ws4py/client/__init__.py +++ b/ws4py/client/__init__.py @@ -261,7 +261,6 @@ def handshake_headers(self): handshake. """ headers = [ - ('Host', '%s:%s' % (self.host, self.port)), ('Connection', 'Upgrade'), ('Upgrade', 'websocket'), ('Sec-WebSocket-Key', self.key.decode('utf-8')), @@ -274,6 +273,12 @@ def handshake_headers(self): if self.extra_headers: headers.extend(self.extra_headers) + # keep old logic if no overriding Host in headers + if not any(x for x in headers if x[0].lower() == 'host') and \ + 'host' not in self.exclude_headers: + headers.append(('Host', '%s:%s' % (self.host, self.port))) + + if not any(x for x in headers if x[0].lower() == 'origin') and \ 'origin' not in self.exclude_headers: From fc3a6afea9ba4227fd1359ca3e8e084a60dffc39 Mon Sep 17 00:00:00 2001 From: Chow Loong Jin Date: Wed, 7 May 2025 20:10:22 +0800 Subject: [PATCH 28/29] Add test for frame spillover issue (#218, #230) (#220) * Add test for frame spillover fix * Make test_websocket.test_spill_frame more readable --------- Signed-off-by: Asif Saif Uddin Co-authored-by: Ngo The Trung Co-authored-by: Asif Saif Uddin --- .travis.yml | 2 ++ test/test_websocket.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8c22b32..8746ea4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,13 @@ language: python python: - 2.7 + - 3.5 - 3.6 - 3.7 - 3.8 - 3.9 + before_install: - sudo apt-get install python-dev libevent-dev - pip install Cython diff --git a/test/test_websocket.py b/test/test_websocket.py index 42ec208..29e451f 100644 --- a/test/test_websocket.py +++ b/test/test_websocket.py @@ -5,10 +5,13 @@ import struct try: + from io import BytesIO from unittest.mock import MagicMock, call, patch except ImportError: + from StringIO import StringIO as BytesIO from mock import MagicMock, call, patch + from ws4py.framing import Frame, \ OPCODE_CONTINUATION, OPCODE_TEXT, \ OPCODE_BINARY, OPCODE_CLOSE, OPCODE_PING, OPCODE_PONG @@ -180,6 +183,25 @@ def test_sending_ping(self): ws.ping("hello") m.sendall.assert_called_once_with(tm) + def test_spill_frame(self): + data = b"hello" + buf = BytesIO(data + b"spillover") + + sock = MagicMock() + sock._ssl = object() # for WebSocket._is_secure logic + sock.recv.side_effect = buf.read + sock.pending.side_effect = lambda: buf.tell() < len(buf.getvalue()) + + ws = WebSocket(sock=sock) + ws.stream = MagicMock() + + self.assertTrue(ws._is_secure) + + ws.reading_buffer_size = len(data) + ws.once() + + ws.stream.parser.send.assert_called_once_with(data) + @patch("ws4py.websocket.Heartbeat") def test_run(self, mocker): From 6755e285a82d219f72b2593d3bd1ee73d907b128 Mon Sep 17 00:00:00 2001 From: Martin Date: Sun, 29 Mar 2026 20:06:40 +0200 Subject: [PATCH 29/29] fix(setup.py): Fix license deprecation Signed-off-by: Martin --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index d94d598..4ff6ede 100644 --- a/setup.py +++ b/setup.py @@ -49,13 +49,12 @@ def find_package_modules(self, package, package_dir): download_url = "https://pypi.python.org/pypi/ws4py", packages = ['ws4py', 'ws4py.client', 'ws4py.server'], platforms = ["any"], - license = 'BSD', + license = 'BSD-3-Clause', long_description = "WebSocket client and server library for Python 2 and 3 as well as PyPy", classifiers=[ 'Development Status :: 5 - Production/Stable', 'Framework :: CherryPy', 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2',