diff --git a/.gitignore b/.gitignore index b8252fc..1c527d5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ dist/ docs/_build __pycache__/ +.tox/ *.swp - +*.pyc diff --git a/channels/__init__.py b/channels/__init__.py index 923d05c..dde140b 100644 --- a/channels/__init__.py +++ b/channels/__init__.py @@ -2,8 +2,10 @@ __version__ = "0.8" # Load backends, using settings if available (else falling back to a default) DEFAULT_CHANNEL_BACKEND = "default" -from .backends import BackendManager -from django.conf import settings + +from .backends import BackendManager # isort:skip +from django.conf import settings # isort:skip + channel_backends = BackendManager( getattr(settings, "CHANNEL_BACKENDS", { DEFAULT_CHANNEL_BACKEND: { @@ -16,4 +18,4 @@ channel_backends = BackendManager( default_app_config = 'channels.apps.ChannelsConfig' # Promote channel to top-level (down here to avoid circular import errs) -from .channel import Channel, Group +from .channel import Channel, Group # NOQA isort:skip diff --git a/channels/apps.py b/channels/apps.py index 2d31d8d..389fc99 100644 --- a/channels/apps.py +++ b/channels/apps.py @@ -1,10 +1,11 @@ from django.apps import AppConfig + class ChannelsConfig(AppConfig): name = "channels" verbose_name = "Channels" - + def ready(self): # Do django monkeypatches from .hacks import monkeypatch_django diff --git a/channels/auth.py b/channels/auth.py index 261010c..d0c786a 100644 --- a/channels/auth.py +++ b/channels/auth.py @@ -1,6 +1,7 @@ import functools from django.contrib import auth + from .decorators import channel_session, http_session diff --git a/channels/backends/__init__.py b/channels/backends/__init__.py index 5582b73..e52d24f 100644 --- a/channels/backends/__init__.py +++ b/channels/backends/__init__.py @@ -13,15 +13,18 @@ class BackendManager(object): def __init__(self, backend_configs): self.configs = backend_configs self.backends = {} - + def make_backend(self, name): # Load the backend class try: backend_class = import_string(self.configs[name]['BACKEND']) except KeyError: raise InvalidChannelBackendError("No BACKEND specified for %s" % name) - except ImportError as e: - raise InvalidChannelBackendError("Cannot import BACKEND %r specified for %s" % (self.configs[name]['BACKEND'], name)) + except ImportError: + raise InvalidChannelBackendError( + "Cannot import BACKEND %r specified for %s" % (self.configs[name]['BACKEND'], name) + ) + # Initialise and pass config instance = backend_class(**{k.lower(): v for k, v in self.configs[name].items() if k != "BACKEND"}) instance.alias = name diff --git a/channels/backends/base.py b/channels/backends/base.py index 9baaf6c..fbf9c5f 100644 --- a/channels/backends/base.py +++ b/channels/backends/base.py @@ -1,4 +1,5 @@ import time + from channels.consumer_registry import ConsumerRegistry diff --git a/channels/backends/database.py b/channels/backends/database.py index d1514fa..d0b7f69 100644 --- a/channels/backends/database.py +++ b/channels/backends/database.py @@ -1,9 +1,8 @@ -import time -import json import datetime +import json from django.apps.registry import Apps -from django.db import models, connections, DEFAULT_DB_ALIAS, IntegrityError +from django.db import DEFAULT_DB_ALIAS, IntegrityError, connections, models from django.utils.functional import cached_property from django.utils.timezone import now @@ -39,6 +38,7 @@ class DatabaseChannelBackend(BaseChannelBackend): channel = models.CharField(max_length=200, db_index=True) content = models.TextField() expiry = models.DateTimeField(db_index=True) + class Meta: apps = Apps() app_label = "channels" @@ -60,6 +60,7 @@ class DatabaseChannelBackend(BaseChannelBackend): group = models.CharField(max_length=200) channel = models.CharField(max_length=200) expiry = models.DateTimeField(db_index=True) + class Meta: apps = Apps() app_label = "channels" @@ -81,6 +82,7 @@ class DatabaseChannelBackend(BaseChannelBackend): class Lock(models.Model): channel = models.CharField(max_length=200, unique=True) expiry = models.DateTimeField(db_index=True) + class Meta: apps = Apps() app_label = "channels" @@ -93,9 +95,9 @@ class DatabaseChannelBackend(BaseChannelBackend): def send(self, channel, message): self.channel_model.objects.create( - channel = channel, - content = json.dumps(message), - expiry = now() + datetime.timedelta(seconds=self.expiry) + channel=channel, + content=json.dumps(message), + expiry=now() + datetime.timedelta(seconds=self.expiry) ) def receive_many(self, channels): @@ -125,9 +127,9 @@ class DatabaseChannelBackend(BaseChannelBackend): seconds (expiry defaults to message expiry if not provided). """ self.group_model.objects.update_or_create( - group = group, - channel = channel, - defaults = {"expiry": now() + datetime.timedelta(seconds=expiry or self.expiry)}, + group=group, + channel=channel, + defaults={"expiry": now() + datetime.timedelta(seconds=expiry or self.expiry)}, ) def group_discard(self, group, channel): @@ -152,8 +154,8 @@ class DatabaseChannelBackend(BaseChannelBackend): # We rely on the UNIQUE constraint for only-one-thread-wins on locks try: self.lock_model.objects.create( - channel = channel, - expiry = now() + datetime.timedelta(seconds=expiry or self.expiry), + channel=channel, + expiry=now() + datetime.timedelta(seconds=expiry or self.expiry), ) except IntegrityError: return False diff --git a/channels/backends/memory.py b/channels/backends/memory.py index 9fff16c..0cb28ba 100644 --- a/channels/backends/memory.py +++ b/channels/backends/memory.py @@ -1,12 +1,14 @@ -import time import json +import time from collections import deque + from .base import BaseChannelBackend queues = {} groups = {} locks = set() + class InMemoryChannelBackend(BaseChannelBackend): """ In-memory channel implementation. Intended only for use with threading, diff --git a/channels/backends/redis_py.py b/channels/backends/redis_py.py index f48f799..0cfbea9 100644 --- a/channels/backends/redis_py.py +++ b/channels/backends/redis_py.py @@ -1,12 +1,11 @@ -import time -import json -import datetime -import math -import redis -import random import binascii +import json +import math +import random +import time import uuid +import redis from django.utils import six from .base import BaseChannelBackend @@ -64,7 +63,11 @@ class RedisChannelBackend(BaseChannelBackend): def send(self, channel, message): # if channel is no str (=> bytes) convert it if not isinstance(channel, str): - channel = channel.decode('utf-8') + channel = channel.decode("utf-8") + # Write out message into expiring key (avoids big items in list) + # TODO: Use extended set, drop support for older redis? + key = self.prefix + uuid.uuid4().hex + # Pick a connection to the right server - consistent for response # channels, random for normal channels if channel.startswith("!"): @@ -72,9 +75,7 @@ class RedisChannelBackend(BaseChannelBackend): connection = self.connection(index) else: connection = self.connection(None) - # Write out message into expiring key (avoids big items in list) - # TODO: Use extended set, drop support for older redis? - key = self.prefix + uuid.uuid4().hex + connection.set( key, json.dumps(message), diff --git a/channels/channel.py b/channels/channel.py index 0e047b5..33112d8 100644 --- a/channels/channel.py +++ b/channels/channel.py @@ -1,7 +1,7 @@ import random import string -from channels import channel_backends, DEFAULT_CHANNEL_BACKEND +from channels import DEFAULT_CHANNEL_BACKEND, channel_backends class Channel(object): @@ -81,7 +81,7 @@ class Group(object): self.channel_backend.group_discard(self.name, channel) def channels(self): - self.channel_backend.group_channels(self.name) + return self.channel_backend.group_channels(self.name) def send(self, content): if not isinstance(content, dict): diff --git a/channels/consumer_registry.py b/channels/consumer_registry.py index 359fcd7..5102e7a 100644 --- a/channels/consumer_registry.py +++ b/channels/consumer_registry.py @@ -1,6 +1,8 @@ import importlib -from django.utils import six + from django.core.exceptions import ImproperlyConfigured +from django.utils import six + from .utils import name_that_thing diff --git a/channels/decorators.py b/channels/decorators.py index 9a0d084..b123d86 100644 --- a/channels/decorators.py +++ b/channels/decorators.py @@ -18,7 +18,10 @@ def linearize(func): def inner(message, *args, **kwargs): # Make sure there's a reply channel if not message.reply_channel: - raise ValueError("No reply_channel sent to consumer; @no_overlap can only be used on messages containing it.") + raise ValueError( + "No reply_channel sent to consumer; @no_overlap can only be used on messages containing it." + ) + # Get the lock, or re-queue locked = message.channel_backend.lock_channel(message.reply_channel) if not locked: @@ -43,10 +46,14 @@ def channel_session(func): def inner(message, *args, **kwargs): # Make sure there's a reply_channel if not message.reply_channel: - raise ValueError("No reply_channel sent to consumer; @channel_session can only be used on messages containing it.") + raise ValueError( + "No reply_channel sent to consumer; @no_overlap can only be used on messages containing it." + ) + # Make sure there's NOT a channel_session already if hasattr(message, "channel_session"): raise ValueError("channel_session decorator wrapped inside another channel_session decorator") + # Turn the reply_channel into a valid session key length thing. # We take the last 24 bytes verbatim, as these are the random section, # and then hash the remaining ones onto the start, and add a prefix diff --git a/channels/hacks.py b/channels/hacks.py index e782eb6..b8050cb 100644 --- a/channels/hacks.py +++ b/channels/hacks.py @@ -1,8 +1,9 @@ +from django.core.handlers.base import BaseHandler from django.http.request import HttpRequest from django.http.response import HttpResponseBase -from django.core.handlers.base import BaseHandler -from .request import encode_request, decode_request -from .response import encode_response, decode_response, ResponseLater + +from .request import decode_request, encode_request +from .response import ResponseLater, decode_response, encode_response def monkeypatch_django(): diff --git a/channels/interfaces/websocket_asyncio.py b/channels/interfaces/websocket_asyncio.py index b0d852a..899bfcf 100644 --- a/channels/interfaces/websocket_asyncio.py +++ b/channels/interfaces/websocket_asyncio.py @@ -1,9 +1,11 @@ -import asyncio import time -from autobahn.asyncio.websocket import WebSocketServerProtocol, WebSocketServerFactory +import asyncio +from autobahn.asyncio.websocket import ( + WebSocketServerFactory, WebSocketServerProtocol, +) -from .websocket_autobahn import get_protocol, get_factory +from .websocket_autobahn import get_factory, get_protocol class WebsocketAsyncioInterface(object): diff --git a/channels/interfaces/websocket_autobahn.py b/channels/interfaces/websocket_autobahn.py index 7517b8c..1445ad1 100644 --- a/channels/interfaces/websocket_autobahn.py +++ b/channels/interfaces/websocket_autobahn.py @@ -1,6 +1,6 @@ import time -from channels import Channel, channel_backends, DEFAULT_CHANNEL_BACKEND +from channels import DEFAULT_CHANNEL_BACKEND, Channel, channel_backends def get_protocol(base): diff --git a/channels/interfaces/websocket_twisted.py b/channels/interfaces/websocket_twisted.py index e6055f1..e8122a7 100644 --- a/channels/interfaces/websocket_twisted.py +++ b/channels/interfaces/websocket_twisted.py @@ -1,9 +1,11 @@ import time -from autobahn.twisted.websocket import WebSocketServerProtocol, WebSocketServerFactory +from autobahn.twisted.websocket import ( + WebSocketServerFactory, WebSocketServerProtocol, +) from twisted.internet import reactor -from .websocket_autobahn import get_protocol, get_factory +from .websocket_autobahn import get_factory, get_protocol class WebsocketTwistedInterface(object): diff --git a/channels/interfaces/wsgi.py b/channels/interfaces/wsgi.py index b856b93..004e6ea 100644 --- a/channels/interfaces/wsgi.py +++ b/channels/interfaces/wsgi.py @@ -1,6 +1,7 @@ import django from django.core.handlers.wsgi import WSGIHandler from django.http import HttpResponse + from channels import Channel diff --git a/channels/management/commands/runserver.py b/channels/management/commands/runserver.py index 25be5d8..6fa54d6 100644 --- a/channels/management/commands/runserver.py +++ b/channels/management/commands/runserver.py @@ -1,8 +1,9 @@ import threading -from django.core.management.commands.runserver import Command as RunserverCommand +from django.core.management.commands.runserver import \ + Command as RunserverCommand -from channels import channel_backends, DEFAULT_CHANNEL_BACKEND +from channels import DEFAULT_CHANNEL_BACKEND, channel_backends from channels.adapters import UrlConsumer from channels.interfaces.wsgi import WSGIInterface from channels.log import setup_logger diff --git a/channels/management/commands/runworker.py b/channels/management/commands/runworker.py index 721e42f..2c157bc 100644 --- a/channels/management/commands/runworker.py +++ b/channels/management/commands/runworker.py @@ -1,6 +1,7 @@ + from django.core.management import BaseCommand, CommandError -from channels import channel_backends, DEFAULT_CHANNEL_BACKEND +from channels import DEFAULT_CHANNEL_BACKEND, channel_backends from channels.log import setup_logger from channels.worker import Worker diff --git a/channels/management/commands/runwsserver.py b/channels/management/commands/runwsserver.py index 72e3cb7..54f2f0c 100644 --- a/channels/management/commands/runwsserver.py +++ b/channels/management/commands/runwsserver.py @@ -1,6 +1,6 @@ from django.core.management import BaseCommand, CommandError -from channels import channel_backends, DEFAULT_CHANNEL_BACKEND +from channels import DEFAULT_CHANNEL_BACKEND, channel_backends from channels.log import setup_logger @@ -23,7 +23,7 @@ class Command(BaseCommand): # Run the interface port = int(options.get("port", None) or 9000) try: - import asyncio + import asyncio # NOQA except ImportError: from channels.interfaces.websocket_twisted import WebsocketTwistedInterface self.logger.info("Running Twisted/Autobahn WebSocket interface server") diff --git a/channels/request.py b/channels/request.py index 2af4098..4060335 100644 --- a/channels/request.py +++ b/channels/request.py @@ -1,7 +1,6 @@ from django.http import HttpRequest -from django.utils.datastructures import MultiValueDict from django.http.request import QueryDict -from django.conf import settings +from django.utils.datastructures import MultiValueDict def encode_request(request): diff --git a/channels/response.py b/channels/response.py index 4b253d7..f484da7 100644 --- a/channels/response.py +++ b/channels/response.py @@ -24,9 +24,9 @@ def decode_response(value): Decodes a response JSONish value to a HttpResponse object. """ response = HttpResponse( - content = value['content'], - content_type = value['content_type'], - status = value['status'], + content=value['content'], + content_type=value['content_type'], + status=value['status'], ) for cookie in value['cookies']: response.cookies.load(cookie) diff --git a/channels/tests/settings.py b/channels/tests/settings.py new file mode 100644 index 0000000..c472c7e --- /dev/null +++ b/channels/tests/settings.py @@ -0,0 +1,9 @@ +SECRET_KEY = 'cat' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + } +} + +MIDDLEWARE_CLASSES = [] diff --git a/channels/tests/test_backends.py b/channels/tests/test_backends.py index c2cbba6..b25750f 100644 --- a/channels/tests/test_backends.py +++ b/channels/tests/test_backends.py @@ -1,10 +1,11 @@ +# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.test import TestCase -from ..channel import Channel + from ..backends.database import DatabaseChannelBackend -from ..backends.redis_py import RedisChannelBackend from ..backends.memory import InMemoryChannelBackend +from ..backends.redis_py import RedisChannelBackend class MemoryBackendTests(TestCase): diff --git a/runtests.py b/runtests.py new file mode 100644 index 0000000..1d60d79 --- /dev/null +++ b/runtests.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +import os +import sys + +import django +from django.conf import settings +from django.test.utils import get_runner + +if __name__ == "__main__": + os.environ['DJANGO_SETTINGS_MODULE'] = "channels.tests.settings" + django.setup() + TestRunner = get_runner(settings) + test_runner = TestRunner() + failures = test_runner.run_tests(["channels.tests"]) + sys.exit(bool(failures)) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..445b946 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,12 @@ +[flake8] +exclude = venv/*,tox/*,docs/*,testproject/* +ignore = E123,E128,E402,W503,E731,W601 +max-line-length = 119 + +[isort] +combine_as_imports = true +default_section = THIRDPARTY +include_trailing_comma = true +known_first_party = channels +multi_line_output = 5 +not_skip = __init__.py diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..f42ff19 --- /dev/null +++ b/tox.ini @@ -0,0 +1,24 @@ +[tox] +skipsdist = True +envlist = + {py27}-django-{17,18,19} + {py35}-django-{18,19} + {py27,py35}-flake8 + isort + +[testenv] +setenv = + PYTHONPATH = {toxinidir}:{toxinidir} +deps = + six + redis==2.10.5 + flake8: flake8 + isort: isort + django-16: Django>=1.6,<1.7 + django-17: Django>=1.7,<1.8 + django-18: Django>=1.8,<1.9 + django-19: Django==1.9b1 +commands = + flake8: flake8 + isort: isort -c -rc channels + django: python {toxinidir}/runtests.py