You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

websocket.py 3.8KB

5 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import json
  2. from urllib.parse import unquote, urlparse
  3. from asgiref.testing import ApplicationCommunicator
  4. class WebsocketCommunicator(ApplicationCommunicator):
  5. """
  6. ApplicationCommunicator subclass that has WebSocket shortcut methods.
  7. It will construct the scope for you, so you need to pass the application
  8. (uninstantiated) along with the initial connection parameters.
  9. """
  10. def __init__(self, application, path, headers=None, subprotocols=None):
  11. if not isinstance(path, str):
  12. raise TypeError("Expected str, got {}".format(type(path)))
  13. parsed = urlparse(path)
  14. self.scope = {
  15. "type": "websocket",
  16. "path": unquote(parsed.path),
  17. "query_string": parsed.query.encode("utf-8"),
  18. "headers": headers or [],
  19. "subprotocols": subprotocols or [],
  20. }
  21. super().__init__(application, self.scope)
  22. async def connect(self, timeout=1):
  23. """
  24. Trigger the connection code.
  25. On an accepted connection, returns (True, <chosen-subprotocol>)
  26. On a rejected connection, returns (False, <close-code>)
  27. """
  28. await self.send_input({"type": "websocket.connect"})
  29. response = await self.receive_output(timeout)
  30. if response["type"] == "websocket.close":
  31. return (False, response.get("code", 1000))
  32. else:
  33. return (True, response.get("subprotocol", None))
  34. async def send_to(self, text_data=None, bytes_data=None):
  35. """
  36. Sends a WebSocket frame to the application.
  37. """
  38. # Make sure we have exactly one of the arguments
  39. assert bool(text_data) != bool(
  40. bytes_data
  41. ), "You must supply exactly one of text_data or bytes_data"
  42. # Send the right kind of event
  43. if text_data:
  44. assert isinstance(text_data, str), "The text_data argument must be a str"
  45. await self.send_input({"type": "websocket.receive", "text": text_data})
  46. else:
  47. assert isinstance(
  48. bytes_data, bytes
  49. ), "The bytes_data argument must be bytes"
  50. await self.send_input({"type": "websocket.receive", "bytes": bytes_data})
  51. async def send_json_to(self, data):
  52. """
  53. Sends JSON data as a text frame
  54. """
  55. await self.send_to(text_data=json.dumps(data))
  56. async def receive_from(self, timeout=1):
  57. """
  58. Receives a data frame from the view. Will fail if the connection
  59. closes instead. Returns either a bytestring or a unicode string
  60. depending on what sort of frame you got.
  61. """
  62. response = await self.receive_output(timeout)
  63. # Make sure this is a send message
  64. assert response["type"] == "websocket.send"
  65. # Make sure there's exactly one key in the response
  66. assert ("text" in response) != (
  67. "bytes" in response
  68. ), "The response needs exactly one of 'text' or 'bytes'"
  69. # Pull out the right key and typecheck it for our users
  70. if "text" in response:
  71. assert isinstance(response["text"], str), "Text frame payload is not str"
  72. return response["text"]
  73. else:
  74. assert isinstance(
  75. response["bytes"], bytes
  76. ), "Binary frame payload is not bytes"
  77. return response["bytes"]
  78. async def receive_json_from(self, timeout=1):
  79. """
  80. Receives a JSON text frame payload and decodes it
  81. """
  82. payload = await self.receive_from(timeout)
  83. assert isinstance(payload, str), "JSON data is not a text frame"
  84. return json.loads(payload)
  85. async def disconnect(self, code=1000, timeout=1):
  86. """
  87. Closes the socket
  88. """
  89. await self.send_input({"type": "websocket.disconnect", "code": code})
  90. await self.wait(timeout)