Skip to content

Commit 9836360

Browse files
committed
Merge branch 'development'
2 parents 2c44f4c + 768be99 commit 9836360

File tree

10 files changed

+249
-35
lines changed

10 files changed

+249
-35
lines changed

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Websocket Server
22
=======================
33

4-
[![CircleCI](https://circleci.com/gh/Pithikos/python-websocket-server/tree/master.svg?style=svg)](https://circleci.com/gh/Pithikos/python-websocket-server/tree/master)
4+
[![CircleCI](https://circleci.com/gh/Pithikos/python-websocket-server/tree/master.svg?style=svg)](https://circleci.com/gh/Pithikos/python-websocket-server/tree/master) [![PyPI version](https://badge.fury.io/py/websocket-server.svg)](https://badge.fury.io/py/websocket-server)
55

66
A minimal Websockets Server in Python with no external dependencies.
77

@@ -18,11 +18,9 @@ websocket server for prototyping, testing or for making a GUI for your applicati
1818
Installation
1919
=======================
2020

21-
You can use the project in three ways.
21+
Install with pip
2222

23-
1. Copy/paste the *websocket_server.py* file in your project and use it directly
24-
2. `pip install git+https://github.com/Pithikos/python-websocket-server` (latest code)
25-
3. `pip install websocket-server` (might not be up-to-date)
23+
pip install websocket-server
2624

2725
For coding details have a look at the [*server.py*](https://github.com/Pithikos/python-websocket-server/blob/master/server.py) example and the [API](https://github.com/Pithikos/python-websocket-server#api).
2826

@@ -61,7 +59,7 @@ The WebsocketServer can be initialized with the below parameters.
6159

6260
*`key`* - If using SSL, this is the path to the key.
6361

64-
*`cert`* - If using SSL, this is the path to the certificate.
62+
*`cert`* - If using SSL, this is the path to the certificate.
6563

6664

6765
### Properties
@@ -80,6 +78,9 @@ The WebsocketServer can be initialized with the below parameters.
8078
| `set_fn_message_received()` | Sets a callback function that will be called when a `client` sends a message | function | None |
8179
| `send_message()` | Sends a `message` to a specific `client`. The message is a simple string. | client, message | None |
8280
| `send_message_to_all()` | Sends a `message` to **all** connected clients. The message is a simple string. | message | None |
81+
| `shutdown_gracefully()` | Shutdown server by sending a websocket CLOSE handshake to all connected clients. | None | None |
82+
| `shutdown_abruptly()` | Shutdown server without sending any websocket CLOSE handshake. | None | None |
83+
8384

8485

8586
### Callback functions

docs/release-workflow.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Release notes
2+
-------------
3+
4+
Releases are marked on master branch with tags. The upload to pypi is automated as long as a merge
5+
from development comes with a tag.
6+
7+
General flow
8+
9+
1. Get in dev branch
10+
2. Update VERSION in setup.py and releases.txt file
11+
3. Make a commit
12+
4. Merge development into master (`git merge --no-ff development`)
13+
4. Add corresponding version as a new tag (`git tag <new_version>`) e.g. git tag v0.3.0
14+
5. Push everything (`git push --tags && git push`)

docs/release.md

Lines changed: 0 additions & 12 deletions
This file was deleted.

releases.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
0.4
2+
- Python 2 and 3 support
3+
4+
0.5.1
5+
- SSL support
6+
- Drop Python 2 support
7+
8+
0.5.4
9+
- Add API for shutting down server (abruptly & gracefully)

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
tox>=3.24.0
33
IPython
44
pytest
5-
websocket-client
5+
websocket-client>=1.1.1
66
twine

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from distutils.command.install import install
1313

1414

15-
VERSION = '0.5.1'
15+
VERSION = '0.5.4'
1616

1717

1818
def get_tag_version():

tests/test_server.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from utils import session, client_session, server
2+
from time import sleep
3+
4+
import websocket
5+
import pytest
6+
7+
8+
def test_send_close(client_session):
9+
"""
10+
Ensure client stops receiving data once we send_close (socket is still open)
11+
"""
12+
client, server = client_session
13+
assert client.received_messages == []
14+
15+
server.send_message_to_all("test1")
16+
sleep(0.5)
17+
assert client.received_messages == ["test1"]
18+
19+
# After CLOSE, client should not be receiving any messages
20+
server.clients[-1]["handler"].send_close()
21+
sleep(0.5)
22+
server.send_message_to_all("test2")
23+
sleep(0.5)
24+
assert client.received_messages == ["test1"]
25+
26+
27+
def test_shutdown_gracefully(client_session):
28+
client, server = client_session
29+
assert client.ws.sock and client.ws.sock.connected
30+
assert server.socket.fileno() > 0
31+
32+
server.shutdown_gracefully()
33+
sleep(0.5)
34+
35+
# Ensure all parties disconnected
36+
assert not client.ws.sock
37+
assert server.socket.fileno() == -1
38+
assert not server.clients
39+
40+
41+
def test_shutdown_abruptly(client_session):
42+
client, server = client_session
43+
assert client.ws.sock and client.ws.sock.connected
44+
assert server.socket.fileno() > 0
45+
46+
server.shutdown_abruptly()
47+
sleep(0.5)
48+
49+
# Ensure server socket died
50+
assert server.socket.fileno() == -1
51+
52+
# Ensure client handler terminated
53+
assert server.received_messages == []
54+
assert client.errors == []
55+
client.ws.send("1st msg after server shutdown")
56+
sleep(0.5)
57+
58+
# Note the message is received since the client handler
59+
# will terminate only once it has received the last message
60+
# and break out of the keep_alive loop. Any consecutive messages
61+
# will not be received though.
62+
assert server.received_messages == ["1st msg after server shutdown"]
63+
assert len(client.errors) == 1
64+
assert isinstance(client.errors[0], websocket._exceptions.WebSocketConnectionClosedException)
65+
66+
# Try to send 2nd message
67+
with pytest.raises(websocket._exceptions.WebSocketConnectionClosedException):
68+
client.ws.send("2nd msg after server shutdown")
69+
70+
71+
def test_client_closes_gracefully(session):
72+
client, server = session
73+
assert client.connected
74+
assert server.clients
75+
old_client_handler = server.clients[0]["handler"]
76+
client.close()
77+
assert not client.connected
78+
79+
# Ensure server closed connection.
80+
# We test this by having the server trying to send
81+
# data to the client
82+
assert not server.clients
83+
with pytest.raises(BrokenPipeError):
84+
old_client_handler.connection.send(b"test")

tests/test_text_messages.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# -*- coding: utf-8 -*-
21
from utils import session, server
32

43

tests/utils.py

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,65 @@
11
import logging
2+
from time import sleep
23
from threading import Thread
34

45
import pytest
5-
from websocket import create_connection # websocket-client
6+
import websocket # websocket-client
67

78
import _bootstrap_
89
from websocket_server import WebsocketServer
910

1011

12+
class TestClient():
13+
def __init__(self, port, threaded=True):
14+
self.received_messages = []
15+
self.closes = []
16+
self.opens = []
17+
self.errors = []
18+
19+
websocket.enableTrace(True)
20+
self.ws = websocket.WebSocketApp(f"ws://localhost:{port}/",
21+
on_open=self.on_open,
22+
on_message=self.on_message,
23+
on_error=self.on_error,
24+
on_close=self.on_close)
25+
if threaded:
26+
self.thread = Thread(target=self.ws.run_forever)
27+
self.thread.daemon = True
28+
self.thread.start()
29+
else:
30+
self.ws.run_forever()
31+
32+
def on_message(self, ws, message):
33+
self.received_messages.append(message)
34+
print(f"TestClient: on_message: {message}")
35+
36+
def on_error(self, ws, error):
37+
self.errors.append(error)
38+
print(f"TestClient: on_error: {error}")
39+
40+
def on_close(self, ws, close_status_code, close_msg):
41+
self.closes.append((close_status_code, close_msg))
42+
print(f"TestClient: on_close: {close_status_code} - {close_msg}")
43+
44+
def on_open(self, ws):
45+
self.opens.append(ws)
46+
print("TestClient: on_open")
47+
48+
49+
class TestServer(WebsocketServer):
50+
def __init__(self, *args, **kwargs):
51+
super().__init__(*args, **kwargs)
52+
self.received_messages = []
53+
self.set_fn_message_received(self.handle_received_message)
54+
55+
def handle_received_message(self, client, server, message):
56+
self.received_messages.append(message)
57+
58+
1159
@pytest.fixture(scope='function')
1260
def server():
1361
""" Returns the response of a server after"""
14-
s = WebsocketServer(0, loglevel=logging.DEBUG)
62+
s = TestServer(0, loglevel=logging.DEBUG)
1563
server_thread = Thread(target=s.run_forever)
1664
server_thread.daemon = True
1765
server_thread.start()
@@ -21,6 +69,21 @@ def server():
2169

2270
@pytest.fixture
2371
def session(server):
24-
ws = create_connection("ws://{}:{}".format(*server.server_address))
25-
yield ws, server
26-
ws.close()
72+
"""
73+
Gives a simple connection to a server
74+
"""
75+
conn = websocket.create_connection("ws://{}:{}".format(*server.server_address))
76+
yield conn, server
77+
conn.close()
78+
79+
80+
@pytest.fixture
81+
def client_session(server):
82+
"""
83+
Gives a TestClient instance connected to a server
84+
"""
85+
client = TestClient(port=server.port)
86+
sleep(1)
87+
assert client.ws.sock and client.ws.sock.connected
88+
yield client, server
89+
client.ws.close()

0 commit comments

Comments
 (0)