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/
docs/_build
__pycache__/
.tox/
*.swp
*.pyc

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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