Merge pull request #29 from ekmartin/tests_and_py3

Run tests with tox and add flake8/isort
This commit is contained in:
Andrew Godwin 2015-11-09 12:25:06 +00:00
commit 0c03ea7780
27 changed files with 147 additions and 56 deletions

3
.gitignore vendored
View File

@ -2,5 +2,6 @@
dist/ dist/
docs/_build docs/_build
__pycache__/ __pycache__/
.tox/
*.swp *.swp
*.pyc

View File

@ -2,8 +2,10 @@ __version__ = "0.8"
# Load backends, using settings if available (else falling back to a default) # Load backends, using settings if available (else falling back to a default)
DEFAULT_CHANNEL_BACKEND = "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( channel_backends = BackendManager(
getattr(settings, "CHANNEL_BACKENDS", { getattr(settings, "CHANNEL_BACKENDS", {
DEFAULT_CHANNEL_BACKEND: { DEFAULT_CHANNEL_BACKEND: {
@ -16,4 +18,4 @@ channel_backends = BackendManager(
default_app_config = 'channels.apps.ChannelsConfig' default_app_config = 'channels.apps.ChannelsConfig'
# Promote channel to top-level (down here to avoid circular import errs) # 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

View File

@ -1,5 +1,6 @@
from django.apps import AppConfig from django.apps import AppConfig
class ChannelsConfig(AppConfig): class ChannelsConfig(AppConfig):
name = "channels" name = "channels"

View File

@ -1,6 +1,7 @@
import functools import functools
from django.contrib import auth from django.contrib import auth
from .decorators import channel_session, http_session from .decorators import channel_session, http_session

View File

@ -20,8 +20,11 @@ class BackendManager(object):
backend_class = import_string(self.configs[name]['BACKEND']) backend_class = import_string(self.configs[name]['BACKEND'])
except KeyError: except KeyError:
raise InvalidChannelBackendError("No BACKEND specified for %s" % name) raise InvalidChannelBackendError("No BACKEND specified for %s" % name)
except ImportError as e: except ImportError:
raise InvalidChannelBackendError("Cannot import BACKEND %r specified for %s" % (self.configs[name]['BACKEND'], name)) raise InvalidChannelBackendError(
"Cannot import BACKEND %r specified for %s" % (self.configs[name]['BACKEND'], name)
)
# Initialise and pass config # Initialise and pass config
instance = backend_class(**{k.lower(): v for k, v in self.configs[name].items() if k != "BACKEND"}) instance = backend_class(**{k.lower(): v for k, v in self.configs[name].items() if k != "BACKEND"})
instance.alias = name instance.alias = name

View File

@ -1,4 +1,5 @@
import time import time
from channels.consumer_registry import ConsumerRegistry from channels.consumer_registry import ConsumerRegistry

View File

@ -1,9 +1,8 @@
import time
import json
import datetime import datetime
import json
from django.apps.registry import Apps 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.functional import cached_property
from django.utils.timezone import now from django.utils.timezone import now
@ -39,6 +38,7 @@ class DatabaseChannelBackend(BaseChannelBackend):
channel = models.CharField(max_length=200, db_index=True) channel = models.CharField(max_length=200, db_index=True)
content = models.TextField() content = models.TextField()
expiry = models.DateTimeField(db_index=True) expiry = models.DateTimeField(db_index=True)
class Meta: class Meta:
apps = Apps() apps = Apps()
app_label = "channels" app_label = "channels"
@ -60,6 +60,7 @@ class DatabaseChannelBackend(BaseChannelBackend):
group = models.CharField(max_length=200) group = models.CharField(max_length=200)
channel = models.CharField(max_length=200) channel = models.CharField(max_length=200)
expiry = models.DateTimeField(db_index=True) expiry = models.DateTimeField(db_index=True)
class Meta: class Meta:
apps = Apps() apps = Apps()
app_label = "channels" app_label = "channels"
@ -81,6 +82,7 @@ class DatabaseChannelBackend(BaseChannelBackend):
class Lock(models.Model): class Lock(models.Model):
channel = models.CharField(max_length=200, unique=True) channel = models.CharField(max_length=200, unique=True)
expiry = models.DateTimeField(db_index=True) expiry = models.DateTimeField(db_index=True)
class Meta: class Meta:
apps = Apps() apps = Apps()
app_label = "channels" app_label = "channels"

View File

@ -1,12 +1,14 @@
import time
import json import json
import time
from collections import deque from collections import deque
from .base import BaseChannelBackend from .base import BaseChannelBackend
queues = {} queues = {}
groups = {} groups = {}
locks = set() locks = set()
class InMemoryChannelBackend(BaseChannelBackend): class InMemoryChannelBackend(BaseChannelBackend):
""" """
In-memory channel implementation. Intended only for use with threading, In-memory channel implementation. Intended only for use with threading,

View File

@ -1,12 +1,11 @@
import time
import json
import datetime
import math
import redis
import random
import binascii import binascii
import json
import math
import random
import time
import uuid import uuid
import redis
from django.utils import six from django.utils import six
from .base import BaseChannelBackend from .base import BaseChannelBackend
@ -64,7 +63,11 @@ class RedisChannelBackend(BaseChannelBackend):
def send(self, channel, message): def send(self, channel, message):
# if channel is no str (=> bytes) convert it # if channel is no str (=> bytes) convert it
if not isinstance(channel, str): 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 # Pick a connection to the right server - consistent for response
# channels, random for normal channels # channels, random for normal channels
if channel.startswith("!"): if channel.startswith("!"):
@ -72,9 +75,7 @@ class RedisChannelBackend(BaseChannelBackend):
connection = self.connection(index) connection = self.connection(index)
else: else:
connection = self.connection(None) 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( connection.set(
key, key,
json.dumps(message), json.dumps(message),

View File

@ -1,7 +1,7 @@
import random import random
import string import string
from channels import channel_backends, DEFAULT_CHANNEL_BACKEND from channels import DEFAULT_CHANNEL_BACKEND, channel_backends
class Channel(object): class Channel(object):
@ -81,7 +81,7 @@ class Group(object):
self.channel_backend.group_discard(self.name, channel) self.channel_backend.group_discard(self.name, channel)
def channels(self): def channels(self):
self.channel_backend.group_channels(self.name) return self.channel_backend.group_channels(self.name)
def send(self, content): def send(self, content):
if not isinstance(content, dict): if not isinstance(content, dict):

View File

@ -1,6 +1,8 @@
import importlib import importlib
from django.utils import six
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.utils import six
from .utils import name_that_thing from .utils import name_that_thing

View File

@ -18,7 +18,10 @@ def linearize(func):
def inner(message, *args, **kwargs): def inner(message, *args, **kwargs):
# Make sure there's a reply channel # Make sure there's a reply channel
if not message.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 # Get the lock, or re-queue
locked = message.channel_backend.lock_channel(message.reply_channel) locked = message.channel_backend.lock_channel(message.reply_channel)
if not locked: if not locked:
@ -43,10 +46,14 @@ def channel_session(func):
def inner(message, *args, **kwargs): def inner(message, *args, **kwargs):
# Make sure there's a reply_channel # Make sure there's a reply_channel
if not message.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 # Make sure there's NOT a channel_session already
if hasattr(message, "channel_session"): if hasattr(message, "channel_session"):
raise ValueError("channel_session decorator wrapped inside another channel_session decorator") raise ValueError("channel_session decorator wrapped inside another channel_session decorator")
# Turn the reply_channel into a valid session key length thing. # Turn the reply_channel into a valid session key length thing.
# We take the last 24 bytes verbatim, as these are the random section, # 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 # and then hash the remaining ones onto the start, and add a prefix

View File

@ -1,8 +1,9 @@
from django.core.handlers.base import BaseHandler
from django.http.request import HttpRequest from django.http.request import HttpRequest
from django.http.response import HttpResponseBase from django.http.response import HttpResponseBase
from django.core.handlers.base import BaseHandler
from .request import encode_request, decode_request from .request import decode_request, encode_request
from .response import encode_response, decode_response, ResponseLater from .response import ResponseLater, decode_response, encode_response
def monkeypatch_django(): def monkeypatch_django():

View File

@ -1,9 +1,11 @@
import asyncio
import time 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): class WebsocketAsyncioInterface(object):

View File

@ -1,6 +1,6 @@
import time import time
from channels import Channel, channel_backends, DEFAULT_CHANNEL_BACKEND from channels import DEFAULT_CHANNEL_BACKEND, Channel, channel_backends
def get_protocol(base): def get_protocol(base):

View File

@ -1,9 +1,11 @@
import time import time
from autobahn.twisted.websocket import WebSocketServerProtocol, WebSocketServerFactory from autobahn.twisted.websocket import (
WebSocketServerFactory, WebSocketServerProtocol,
)
from twisted.internet import reactor from twisted.internet import reactor
from .websocket_autobahn import get_protocol, get_factory from .websocket_autobahn import get_factory, get_protocol
class WebsocketTwistedInterface(object): class WebsocketTwistedInterface(object):

View File

@ -1,6 +1,7 @@
import django import django
from django.core.handlers.wsgi import WSGIHandler from django.core.handlers.wsgi import WSGIHandler
from django.http import HttpResponse from django.http import HttpResponse
from channels import Channel from channels import Channel

View File

@ -1,8 +1,9 @@
import threading 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.adapters import UrlConsumer
from channels.interfaces.wsgi import WSGIInterface from channels.interfaces.wsgi import WSGIInterface
from channels.log import setup_logger from channels.log import setup_logger

View File

@ -1,6 +1,7 @@
from django.core.management import BaseCommand, CommandError 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.log import setup_logger
from channels.worker import Worker from channels.worker import Worker

View File

@ -1,6 +1,6 @@
from django.core.management import BaseCommand, CommandError 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.log import setup_logger
@ -23,7 +23,7 @@ class Command(BaseCommand):
# Run the interface # Run the interface
port = int(options.get("port", None) or 9000) port = int(options.get("port", None) or 9000)
try: try:
import asyncio import asyncio # NOQA
except ImportError: except ImportError:
from channels.interfaces.websocket_twisted import WebsocketTwistedInterface from channels.interfaces.websocket_twisted import WebsocketTwistedInterface
self.logger.info("Running Twisted/Autobahn WebSocket interface server") self.logger.info("Running Twisted/Autobahn WebSocket interface server")

View File

@ -1,7 +1,6 @@
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.datastructures import MultiValueDict
from django.http.request import QueryDict from django.http.request import QueryDict
from django.conf import settings from django.utils.datastructures import MultiValueDict
def encode_request(request): def encode_request(request):

View File

@ -0,0 +1,9 @@
SECRET_KEY = 'cat'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
}
}
MIDDLEWARE_CLASSES = []

View File

@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.test import TestCase from django.test import TestCase
from ..channel import Channel
from ..backends.database import DatabaseChannelBackend from ..backends.database import DatabaseChannelBackend
from ..backends.redis_py import RedisChannelBackend
from ..backends.memory import InMemoryChannelBackend from ..backends.memory import InMemoryChannelBackend
from ..backends.redis_py import RedisChannelBackend
class MemoryBackendTests(TestCase): class MemoryBackendTests(TestCase):

15
runtests.py Normal file
View File

@ -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))

12
setup.cfg Normal file
View File

@ -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

24
tox.ini Normal file
View File

@ -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