~sirn/fanboi2

957969e16f0949fe9573b8140b810b3b9e2c00b9 — Kridsada Thanabulpong 5 years ago 4fa4480
Switch to Python 3.5.

As libraries are dropping Python 3.2 support. This change also switched
from PyPy back to CPython since PyPy3.3 is still alpha, and Python 3.5
support is still forthcoming.

Following to this change, libraries are either deprecated, upgraded or
replaced as appropriate. The list includes:

* Replaced IPy with built-in ipaddress module.
* Replaced mock with built-in unittest.mock module.
* Replaced pg8000 with psycopg2.
* Updated Pyramid to 1.7.
* Updated Alembic to 0.8.
* Updated MarkupSafe to latest.
* Updated wtforms to latest.
* Updated coverage to latest.
* Removed Pygments version lock.

Any code that are incompatible with Python 3.5 are also updated.
M .gitignore => .gitignore +1 -0
@@ 5,6 5,7 @@ Thumbs.db
# Application runtime
static/
tmp/
.eggs/
*.egg-info/
*.sqlite
*.pyc

M .travis.yml => .travis.yml +2 -8
@@ 2,13 2,7 @@ language: python
script: nosetests

python:
  - "3.2"
  - "pypy3"

# PyPy is trying to create a ctype_config_cache, but the directory is not
# writeable. This is a hacky workaround to make it work.
before_install:
  - 'sudo bash -c "if test -n \"\$(shopt -s nullglob; echo /opt/python/pypy3-*)\"; then cd /opt/python/pypy3-*/lib_pypy/ && mkdir -p ctypes_config_cache/__pycache__ && chmod a+x ctypes_config_cache/__pycache__; fi"'
  - "3.5"

install:
  - 'pip install -e .'


@@ 17,7 11,7 @@ before_script:
  - 'psql -c "create database fanboi2;" -U postgres'

env:
  - POSTGRESQL_TEST_DATABASE=postgresql+pg8000://postgres@localhost:5432/fanboi2
  - POSTGRESQL_TEST_DATABASE=postgresql://postgres@localhost:5432/fanboi2

notifications:
  email: false

M CHANGES.rst => CHANGES.rst +4 -4
@@ 6,11 6,11 @@
- [Add] More random quotes.
- [Change] Rewrite all board templates.
- [Change] Codebase now comes with type annotation for IDE.
- [Change] Replaced `CPython 3.2 <https://www.python.org/download/releases/3.2.5/>`_ with `PyPy3 <http://pypy.org/download.html>`_.
- [Change] Codebase now uses `Python 3.5 <https://docs.python.org/3.5/whatsnew/changelog.html#python-3-5-2>`_.
- [Change] Replaced `Jinja2 <http://jinja.pocoo.org/>`_ templates with `Mako <http://www.makotemplates.org/>`_ templates.
- [Change] Pyramid views are now properly organized into modules.
- [Change] Views now use function dispatching instead of class-based dispatching one.
- [Change] Vagrant now use `FreeBSD 10.1 <https://www.freebsd.org/>`_ instead of `Ubuntu 12.04 <http://releases.ubuntu.com/precise/>`_ to match the new production stack.
- [Change] Pyramid views are now organized into modules.
- [Change] Views now use function dispatching instead of class-based dispatching.
- [Change] Vagrant now use `FreeBSD 10.3 <https://www.freebsd.org/>`_ instead of `Ubuntu 12.04 <http://releases.ubuntu.com/precise/>`_ to match the new production stack.
- [Remove] Get rid of all usage of ``pyramid.threadlocal``.
- [Remove] Production provisioning is now private.


M README.rst => README.rst +6 -7
@@ 34,17 34,17 @@ The Adventurous Way

If you don't really want to use Vagrant, you can also install everything using your preferred methods:

1. `PyPy3 2.3.1 <http://pypy.org/download.html#default-with-a-jit-compiler>`_.
2. `PostgreSQL 9.2 <http://www.postgresql.org/>`_.
3. `Redis 2.8 <http://redis.io/>`_.
1. `Python 3.5 <https://www.python.org/downloads/>`_.
2. `PostgreSQL 9.5 <http://www.postgresql.org/>`_.
3. `Redis 3.0 <http://redis.io/>`_.
4. `Memcached 1.4 <http://www.memcached.org/>`_.
5. `Node.js 4.2 <http://nodejs.org/>`_ with `Gulp`_ and `Typings`_.
5. `Node.js 6.2 <http://nodejs.org/>`_ with `Gulp`_ and `Typings`_.

After the package above are up and running, you may now setup the application::

    $ cp examples/development.ini.sample development.ini
    $ cp examples/alembic.ini.sample alembic.ini
    $ pypy setup.py develop
    $ python3.5 setup.py develop
    $ alembic upgrade head
    $ pserve development.ini



@@ 95,8 95,7 @@ Please make sure that test coverage is 100% and everything passed. It's also a g
License
-------

| Copyright (c) 2013-2015, Kridsada Thanabulpong
| All rights reserved.
Copyright (c) 2013-2016, Kridsada Thanabulpong. All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:


M Vagrantfile => Vagrantfile +10 -13
@@ 2,18 2,19 @@
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "pxfs/freebsd-10.1"
  config.vm.box = "pxfs/freebsd-10.3"
  config.vm.network :forwarded_port, :guest => 6543, :host => 6543
  config.ssh.shell = "sh"

  config.vm.provision :shell, :privileged => true, :inline => <<-EOF
    pkg install -y ca_root_nss
    pkg install -y git-lite
    pkg install -y postgresql92-server
    pkg install -y postgresql95-server
    pkg install -y node npm
    pkg install -y redis
    pkg install -y memcached
    pkg install -y bzip2 sqlite3
    pkg install -y python35

    sysrc postgresql_enable=YES
    sysrc redis_enable=YES


@@ 30,14 31,13 @@ Vagrant.configure("2") do |config|
    sh -c 'echo "host all all 127.0.0.1/32 trust" >> /usr/local/pgsql/data/pg_hba.conf'
    sh -c 'echo "host all all ::1/128 trust" >> /usr/local/pgsql/data/pg_hba.conf'
    service postgresql restart

    fetch -o - https://bootstrap.pypa.io/get-pip.py | /usr/local/bin/python3.5 -
    /usr/local/bin/pip3.5 install virtualenv
  EOF

  config.vm.provision :shell, :privileged => false, :inline => <<-EOF
    cd /tmp
    rm -rf $HOME/pypy3
    fetch -o - http://static.grid.in.th.s3.amazonaws.com/dist/pypy3-2.4.0.tar.bz2 |tar -xjf -
    mv pypy3* $HOME/pypy3
    fetch -o - https://bootstrap.pypa.io/get-pip.py |$HOME/pypy3/bin/pypy3 -
    virtualenv -p python3.5 $HOME/python3.5

    npm config set prefix $HOME/nodejs
    npm install -g gulp


@@ 46,20 46,17 @@ Vagrant.configure("2") do |config|
    echo 'EDITOR=vi; export EDITOR' > $HOME/.profile
    echo 'PAGER=more; export PAGER' >> $HOME/.profile
    echo 'ENV=$HOME/.shrc; export ENV' >> $HOME/.profile
    echo 'PATH="$HOME/nodejs/bin:$HOME/pypy3/bin:$HOME/bin:$PATH"; export PATH' >> $HOME/.profile
    echo 'PATH="$HOME/nodejs/bin:$HOME/python3.5/bin:$HOME/bin:$PATH"; export PATH' >> $HOME/.profile

    psql template1 -c "CREATE DATABASE fanboi2_development;"
    psql template1 -c "CREATE DATABASE fanboi2_test;"

    cd /vagrant
    rm -rf fanboi2.egg-info
    rm -rf node_modules

    cp examples/development.ini.sample development.ini
    cp examples/alembic.ini.sample alembic.ini

    $HOME/pypy3/bin/pip3 install -e .
    $HOME/pypy3/bin/alembic upgrade head
    $HOME/python3.5/bin/pip3 install -e .
    $HOME/python3.5/bin/alembic upgrade head

    npm install --no-bin-link
    $HOME/nodejs/bin/typings install

M examples/development.ini.sample => examples/development.ini.sample +1 -1
@@ 13,7 13,7 @@ pyramid.includes =
debugtoolbar.hosts = 0.0.0.0/0

mako.directories = fanboi2:templates
sqlalchemy.url = postgresql+pg8000://vagrant:@127.0.0.1:5432/fanboi2_development
sqlalchemy.url = postgresql://vagrant:@127.0.0.1:5432/fanboi2_development
redis.url = redis://127.0.0.1:6379/0
celery.broker = redis://127.0.0.1:6379/1


M fanboi2/__init__.py => fanboi2/__init__.py +3 -3
@@ 1,6 1,6 @@
import hashlib
from functools import lru_cache
from IPy import IP
from ipaddress import ip_address
from pyramid.config import Configurator
from pyramid.path import AssetResolver
from pyramid_beaker import session_factory_from_settings


@@ 20,8 20,8 @@ def remote_addr(request):
    :type request: pyramid.request.Request
    :rtype: str
    """
    ipaddr = IP(request.environ.get('REMOTE_ADDR', '255.255.255.255'))
    if ipaddr.iptype() == "PRIVATE":
    ipaddr = ip_address(request.environ.get('REMOTE_ADDR', '255.255.255.255'))
    if ipaddr.is_private:
        return request.environ.get('HTTP_X_FORWARDED_FOR', str(ipaddr))
    return str(ipaddr)


M fanboi2/formatters.py => fanboi2/formatters.py +2 -2
@@ 35,10 35,10 @@ class PostMarkup(Markup):
    formatting such as raw data length or shortened status.
    """

    def __init__(self, *args, **kwargs):
        super(PostMarkup, self).__init__(*args, **kwargs)
    def __new__(self, *args, **kwargs):
        self.length = None
        self.shortened = False
        return super(PostMarkup, self).__new__(self, *args, **kwargs)

    def __len__(self):
        if not self.length:

M fanboi2/tests/__init__.py => fanboi2/tests/__init__.py +1 -1
@@ 10,7 10,7 @@ from fanboi2.models import DBSession, Base, redis_conn

DATABASE_URI = os.environ.get(
    'POSTGRESQL_TEST_DATABASE',
    'postgresql+pg8000://fanboi2:@localhost:5432/fanboi2_test')
    'postgresql://fanboi2:@localhost:5432/fanboi2_test')


class DummyRedis(object):

M fanboi2/tests/test_serializers.py => fanboi2/tests/test_serializers.py +1 -1
@@ 11,7 11,7 @@ class TestJSONRenderer(RegistryMixin, unittest.TestCase):
        return initialize_renderer()

    def _makeOne(self, object, request=None):
        if request is None:
        if request is None:  # pragma: no cover
            request = testing.DummyRequest()
        renderer = self._getTargetFunction()(None)
        return json.loads(renderer(object, {'request': request}))

M fanboi2/tests/test_tasks.py => fanboi2/tests/test_tasks.py +5 -5
@@ 1,6 1,6 @@
import mock
import transaction
import unittest
import unittest.mock
from fanboi2.models import DBSession
from fanboi2.tests import ModelMixin, TaskMixin, DummyAsyncResult



@@ 67,7 67,7 @@ class TestAddTopicTask(TaskMixin, ModelMixin, unittest.TestCase):
        self.assertEqual(topic.posts[0].body, 'Hello, world!')
        self.assertEqual(result.result, ('topic', topic.id))

    @mock.patch('fanboi2.utils.Akismet.spam')
    @unittest.mock.patch('fanboi2.utils.Akismet.spam')
    def test_add_topic_spam(self, akismet):
        from fanboi2.models import Topic
        akismet.return_value = True


@@ 80,7 80,7 @@ class TestAddTopicTask(TaskMixin, ModelMixin, unittest.TestCase):
        self.assertEqual(DBSession.query(Topic).count(), 0)
        self.assertEqual(result.result, ('failure', 'spam_rejected'))

    @mock.patch('fanboi2.utils.Dnsbl.listed')
    @unittest.mock.patch('fanboi2.utils.Dnsbl.listed')
    def test_add_topic_dnsbl(self, dnsbl):
        from fanboi2.models import Topic
        dnsbl.return_value = True


@@ 117,7 117,7 @@ class TestAddPostTask(TaskMixin, ModelMixin, unittest.TestCase):
        self.assertEqual(post.bumped, True)
        self.assertEqual(result.result, ('post', post.id))

    @mock.patch('fanboi2.utils.Akismet.spam')
    @unittest.mock.patch('fanboi2.utils.Akismet.spam')
    def test_add_post_spam(self, akismet):
        import transaction
        from fanboi2.models import Post


@@ 159,7 159,7 @@ class TestAddPostTask(TaskMixin, ModelMixin, unittest.TestCase):
            board = self._makeBoard(title='Foobar', slug='foobar')
            topic = self._makeTopic(board=board, title='Hello, world!')
            topic_id = topic.id  # topic is not bound outside transaction!
        with mock.patch('fanboi2.models.DBSession.flush') as dbs:
        with unittest.mock.patch('fanboi2.models.DBSession.flush') as dbs:
            dbs.side_effect = IntegrityError(None, None, None)
            result = self._makeOne(request, topic_id, 'Hi!', True)
        self.assertEqual(dbs.call_count, 5)

M fanboi2/tests/test_utils.py => fanboi2/tests/test_utils.py +15 -15
@@ 1,5 1,5 @@
import mock
import unittest
import unittest.mock
from fanboi2.models import redis_conn
from fanboi2.tests import DummyRedis, RegistryMixin
from pyramid import testing


@@ 55,14 55,14 @@ class TestDnsBl(unittest.TestCase):
        dnsbl = self._makeOne(providers='xbl.spamhaus.org tor.ahbl.org')
        self.assertEqual(dnsbl.providers, ['xbl.spamhaus.org', 'tor.ahbl.org'])

    @mock.patch('socket.gethostbyname')
    @unittest.mock.patch('socket.gethostbyname')
    def test_listed(self, lookup_call):
        lookup_call.return_value = '127.0.0.2'
        dnsbl = self._makeOne()
        self.assertEqual(dnsbl.listed('10.0.100.254'), True)
        lookup_call.assert_called_with('254.100.0.10.xbl.spamhaus.org.')

    @mock.patch('socket.gethostbyname')
    @unittest.mock.patch('socket.gethostbyname')
    def test_listed_unlisted(self, lookup_call):
        import socket
        lookup_call.side_effect = socket.gaierror('foobar')


@@ 70,13 70,13 @@ class TestDnsBl(unittest.TestCase):
        self.assertEqual(dnsbl.listed('10.0.100.1'), False)
        lookup_call.assert_called_with('1.100.0.10.xbl.spamhaus.org.')

    @mock.patch('socket.gethostbyname')
    @unittest.mock.patch('socket.gethostbyname')
    def test_listed_invalid(self, lookup_call):
        lookup_call.return_value = '192.168.1.1'
        dnsbl = self._makeOne()
        self.assertEqual(dnsbl.listed('10.0.100.2'), False)

    @mock.patch('socket.gethostbyname')
    @unittest.mock.patch('socket.gethostbyname')
    def test_listed_malformed(self, lookup_call):
        lookup_call.return_value = 'foobarbaz'
        dnsbl = self._makeOne()


@@ 107,7 107,7 @@ class TestAkismet(RegistryMixin, unittest.TestCase):
        akismet = self._makeOne(key=None)
        self.assertEqual(akismet.key, None)

    @mock.patch('requests.post')
    @unittest.mock.patch('requests.post')
    def test_spam(self, api_call):
        api_call.return_value = self._makeResponse(b'true')
        request = self._makeRequest()


@@ 115,12 115,12 @@ class TestAkismet(RegistryMixin, unittest.TestCase):
        self.assertEqual(akismet.spam(request, 'buy viagra'), True)
        api_call.assert_called_with(
            'https://hogehoge.rest.akismet.com/1.1/comment-check',
            headers=mock.ANY,
            data=mock.ANY,
            timeout=mock.ANY,
            headers=unittest.mock.ANY,
            data=unittest.mock.ANY,
            timeout=unittest.mock.ANY,
        )

    @mock.patch('requests.post')
    @unittest.mock.patch('requests.post')
    def test_spam_ham(self, api_call):
        api_call.return_value = self._makeResponse(b'false')
        request = self._makeRequest()


@@ 128,12 128,12 @@ class TestAkismet(RegistryMixin, unittest.TestCase):
        self.assertEqual(akismet.spam(request, 'Hogehogehogehoge!'), False)
        api_call.assert_called_with(
            'https://hogehoge.rest.akismet.com/1.1/comment-check',
            headers=mock.ANY,
            data=mock.ANY,
            timeout=mock.ANY,
            headers=unittest.mock.ANY,
            data=unittest.mock.ANY,
            timeout=unittest.mock.ANY,
        )

    @mock.patch('requests.post')
    @unittest.mock.patch('requests.post')
    def test_spam_timeout(self, api_call):
        import requests
        request = self._makeRequest()


@@ 142,7 142,7 @@ class TestAkismet(RegistryMixin, unittest.TestCase):
        self.assertEqual(akismet.spam(request, 'buy viagra'), False)

    # noinspection PyTypeChecker
    @mock.patch('requests.post')
    @unittest.mock.patch('requests.post')
    def test_spam_no_key(self, api_call):
        request = self._makeRequest()
        akismet = self._makeOne(key=None)

M fanboi2/tests/test_views.py => fanboi2/tests/test_views.py +55 -55
@@ 1,5 1,5 @@
import mock
import unittest
import unittest.mock
from fanboi2.tests import ViewMixin, ModelMixin, TaskMixin, DummyAsyncResult




@@ 91,8 91,8 @@ class TestApiViews(ViewMixin, ModelMixin, TaskMixin, unittest.TestCase):
            board_topics_get(request)

    # noinspection PyUnresolvedReferences
    @mock.patch('fanboi2.utils.RateLimiter.limit')
    @mock.patch('fanboi2.tasks.add_topic.delay')
    @unittest.mock.patch('fanboi2.utils.RateLimiter.limit')
    @unittest.mock.patch('fanboi2.tasks.add_topic.delay')
    def test_board_topics_post(self, add_, limit_):
        from fanboi2.views.api import board_topics_post
        board = self._makeBoard(title='Foobar', slug='foobar')


@@ 100,21 100,21 @@ class TestApiViews(ViewMixin, ModelMixin, TaskMixin, unittest.TestCase):
        request = self._POST({'title': 'Thread thread', 'body': 'Words words'})
        request.matchdict['board'] = board.slug
        self._makeConfig(request, self._makeRegistry())
        add_.return_value = mock_response = mock.Mock(id='task-uuid')
        add_.return_value = mock_response = unittest.mock.Mock(id='task-uuid')

        response = board_topics_post(request)
        self.assertEqual(response, mock_response)
        limit_.assert_called_with(board.settings['post_delay'])
        add_.assert_called_with(
            request=mock.ANY,
            request=unittest.mock.ANY,
            board_id=board.id,
            title='Thread thread',
            body='Words words',
        )

    # noinspection PyUnresolvedReferences
    @mock.patch('fanboi2.utils.RateLimiter.limit')
    @mock.patch('fanboi2.tasks.add_topic.delay')
    @unittest.mock.patch('fanboi2.utils.RateLimiter.limit')
    @unittest.mock.patch('fanboi2.tasks.add_topic.delay')
    def test_board_topics_post_json(self, add_, limit_):
        from fanboi2.views.api import board_topics_post
        board = self._makeBoard(title='Foobar', slug='foobar')


@@ 122,13 122,13 @@ class TestApiViews(ViewMixin, ModelMixin, TaskMixin, unittest.TestCase):
        request = self._json_POST({'title': 'Thread', 'body': 'Words words'})
        request.matchdict['board'] = board.slug
        self._makeConfig(request, self._makeRegistry())
        add_.return_value = mock_response = mock.Mock(id='task-uuid')
        add_.return_value = mock_response = unittest.mock.Mock(id='task-uuid')

        response = board_topics_post(request)
        self.assertEqual(response, mock_response)
        limit_.assert_called_with(board.settings['post_delay'])
        add_.assert_called_with(
            request=mock.ANY,
            request=unittest.mock.ANY,
            board_id=board.id,
            title='Thread',
            body='Words words',


@@ 142,8 142,8 @@ class TestApiViews(ViewMixin, ModelMixin, TaskMixin, unittest.TestCase):
        with self.assertRaises(NoResultFound):
            board_topics_post(request)

    @mock.patch('fanboi2.utils.RateLimiter.limit')
    @mock.patch('fanboi2.tasks.add_topic.delay')
    @unittest.mock.patch('fanboi2.utils.RateLimiter.limit')
    @unittest.mock.patch('fanboi2.tasks.add_topic.delay')
    def test_board_topics_post_failed(self, add_, limit_):
        from fanboi2.errors import ParamsInvalidError
        from fanboi2.models import DBSession, Topic


@@ 160,9 160,9 @@ class TestApiViews(ViewMixin, ModelMixin, TaskMixin, unittest.TestCase):
        self.assertFalse(add_.called)
        self.assertEqual(DBSession.query(Topic).count(), 0)

    @mock.patch('fanboi2.utils.RateLimiter.timeleft')
    @mock.patch('fanboi2.utils.RateLimiter.limited')
    @mock.patch('fanboi2.tasks.add_topic.delay')
    @unittest.mock.patch('fanboi2.utils.RateLimiter.timeleft')
    @unittest.mock.patch('fanboi2.utils.RateLimiter.limited')
    @unittest.mock.patch('fanboi2.tasks.add_topic.delay')
    def test_board_topics_post_limited(self, add_, limited_, time_):
        from fanboi2.errors import RateLimitedError
        from fanboi2.models import DBSession, Topic


@@ 182,7 182,7 @@ class TestApiViews(ViewMixin, ModelMixin, TaskMixin, unittest.TestCase):
        self.assertTrue(time_.called)
        self.assertEqual(DBSession.query(Topic).count(), 0)

    @mock.patch('fanboi2.tasks.celery.AsyncResult')
    @unittest.mock.patch('fanboi2.tasks.celery.AsyncResult')
    def test_task_get(self, result_):
        from fanboi2.views.api import task_get
        board = self._makeBoard(title='Foobar', slug='foobar')


@@ 200,7 200,7 @@ class TestApiViews(ViewMixin, ModelMixin, TaskMixin, unittest.TestCase):
        self.assertEqual(response.object, topic)
        result_.assert_called_with('dummy')

    @mock.patch('fanboi2.tasks.celery.AsyncResult')
    @unittest.mock.patch('fanboi2.tasks.celery.AsyncResult')
    def test_task_get_failure(self, result_):
        from fanboi2.errors import SpamRejectedError
        from fanboi2.views.api import task_get


@@ 269,8 269,8 @@ class TestApiViews(ViewMixin, ModelMixin, TaskMixin, unittest.TestCase):
            topic_posts_get(request)

    # noinspection PyUnresolvedReferences
    @mock.patch('fanboi2.utils.RateLimiter.limit')
    @mock.patch('fanboi2.tasks.add_post.delay')
    @unittest.mock.patch('fanboi2.utils.RateLimiter.limit')
    @unittest.mock.patch('fanboi2.tasks.add_post.delay')
    def test_topic_posts_post(self, add_, limit_):
        from fanboi2.views.api import topic_posts_post
        board = self._makeBoard(title='Foobar', slug='foobar')


@@ 280,21 280,21 @@ class TestApiViews(ViewMixin, ModelMixin, TaskMixin, unittest.TestCase):
        request = self._POST({'body': 'Words words'})
        request.matchdict['topic'] = topic.id
        self._makeConfig(request, self._makeRegistry())
        add_.return_value = mock_response = mock.Mock(id='task-uuid')
        add_.return_value = mock_response = unittest.mock.Mock(id='task-uuid')

        response = topic_posts_post(request)
        self.assertEqual(response, mock_response)
        limit_.assert_called_with(board.settings['post_delay'])
        add_.assert_called_with(
            request=mock.ANY,
            request=unittest.mock.ANY,
            topic_id=topic.id,
            body='Words words',
            bumped=False,
        )

    # noinspection PyUnresolvedReferences
    @mock.patch('fanboi2.utils.RateLimiter.limit')
    @mock.patch('fanboi2.tasks.add_post.delay')
    @unittest.mock.patch('fanboi2.utils.RateLimiter.limit')
    @unittest.mock.patch('fanboi2.tasks.add_post.delay')
    def test_topic_posts_post_json(self, add_, limit_):
        from fanboi2.views.api import topic_posts_post
        board = self._makeBoard(title='Foobar', slug='foobar')


@@ 304,13 304,13 @@ class TestApiViews(ViewMixin, ModelMixin, TaskMixin, unittest.TestCase):
        request = self._json_POST({'body': 'Words words'})
        request.matchdict['topic'] = topic.id
        self._makeConfig(request, self._makeRegistry())
        add_.return_value = mock_response = mock.Mock(id='task-uuid')
        add_.return_value = mock_response = unittest.mock.Mock(id='task-uuid')

        response = topic_posts_post(request)
        self.assertEqual(response, mock_response)
        limit_.assert_called_with(board.settings['post_delay'])
        add_.assert_called_with(
            request=mock.ANY,
            request=unittest.mock.ANY,
            topic_id=topic.id,
            body='Words words',
            bumped=False,


@@ 324,8 324,8 @@ class TestApiViews(ViewMixin, ModelMixin, TaskMixin, unittest.TestCase):
        with self.assertRaises(NoResultFound):
            topic_posts_post(request)

    @mock.patch('fanboi2.utils.RateLimiter.limit')
    @mock.patch('fanboi2.tasks.add_post.delay')
    @unittest.mock.patch('fanboi2.utils.RateLimiter.limit')
    @unittest.mock.patch('fanboi2.tasks.add_post.delay')
    def test_topic_posts_post_failed(self, add_, limit_):
        from fanboi2.errors import ParamsInvalidError
        from fanboi2.models import DBSession, Post


@@ 345,9 345,9 @@ class TestApiViews(ViewMixin, ModelMixin, TaskMixin, unittest.TestCase):
        self.assertFalse(add_.called)
        self.assertEqual(DBSession.query(Post).count(), post_count)

    @mock.patch('fanboi2.utils.RateLimiter.timeleft')
    @mock.patch('fanboi2.utils.RateLimiter.limited')
    @mock.patch('fanboi2.tasks.add_post.delay')
    @unittest.mock.patch('fanboi2.utils.RateLimiter.timeleft')
    @unittest.mock.patch('fanboi2.utils.RateLimiter.limited')
    @unittest.mock.patch('fanboi2.tasks.add_post.delay')
    def test_topic_posts_post_limited(self, add_, limited_, time_):
        from fanboi2.errors import RateLimitedError
        from fanboi2.models import DBSession, Post


@@ 539,7 539,7 @@ class TestPageViews(ViewMixin, unittest.TestCase):
        self.assertSAEqual(response['board'], board)

    # noinspection PyUnresolvedReferences
    @mock.patch('fanboi2.tasks.celery.AsyncResult')
    @unittest.mock.patch('fanboi2.tasks.celery.AsyncResult')
    def test_board_new_get_task(self, result_):
        from fanboi2.views.pages import board_new_get
        board = self._makeBoard(title='Foobar', slug='foobar')


@@ 560,7 560,7 @@ class TestPageViews(ViewMixin, unittest.TestCase):
        self.assertEqual(response.location, location)
        result_.assert_called_with('dummy')

    @mock.patch('fanboi2.tasks.celery.AsyncResult')
    @unittest.mock.patch('fanboi2.tasks.celery.AsyncResult')
    def test_board_new_get_task_wait(self, result_):
        from fanboi2.views.pages import board_new_get
        board = self._makeBoard(title='Foobar', slug='foobar')


@@ 575,7 575,7 @@ class TestPageViews(ViewMixin, unittest.TestCase):
        board_new_get(request)
        result_.assert_called_with('dummy')

    @mock.patch('fanboi2.tasks.celery.AsyncResult')
    @unittest.mock.patch('fanboi2.tasks.celery.AsyncResult')
    def test_board_new_get_spam_rejected(self, result_):
        from fanboi2.views.pages import board_new_get
        board = self._makeBoard(title='Foobar', slug='foobar')


@@ 593,7 593,7 @@ class TestPageViews(ViewMixin, unittest.TestCase):
        result_.assert_called_with('dummy')
        self.assertEqual(response.status, '422 Unprocessable Entity')

    @mock.patch('fanboi2.tasks.celery.AsyncResult')
    @unittest.mock.patch('fanboi2.tasks.celery.AsyncResult')
    def test_board_new_get_dnsbl_rejected(self, result_):
        from fanboi2.views.pages import board_new_get
        board = self._makeBoard(title='Foobar', slug='foobar')


@@ 620,8 620,8 @@ class TestPageViews(ViewMixin, unittest.TestCase):
            board_new_get(request)

    # noinspection PyUnresolvedReferences
    @mock.patch('fanboi2.utils.RateLimiter.limit')
    @mock.patch('fanboi2.tasks.add_topic.delay')
    @unittest.mock.patch('fanboi2.utils.RateLimiter.limit')
    @unittest.mock.patch('fanboi2.tasks.add_topic.delay')
    def test_board_new_post(self, add_, limit_):
        from fanboi2.views.pages import board_new_post
        board = self._makeBoard(title='Foobar', slug='foobar')


@@ 630,20 630,20 @@ class TestPageViews(ViewMixin, unittest.TestCase):
        request.matchdict['board'] = board.slug
        config = self._makeConfig(request, self._makeRegistry())
        config.add_route('board_new', '/{board}/new')
        add_.return_value = mock.Mock(id='task-uuid')
        add_.return_value = unittest.mock.Mock(id='task-uuid')

        response = board_new_post(self._make_csrf(request))
        self.assertEqual(response.location, '/foobar/new?task=task-uuid')
        limit_.assert_called_with(board.settings['post_delay'])
        add_.assert_called_with(
            request=mock.ANY,
            request=unittest.mock.ANY,
            board_id=board.id,
            title='Thread thread',
            body='Words words',
        )

    @mock.patch('fanboi2.utils.RateLimiter.limit')
    @mock.patch('fanboi2.tasks.add_topic.delay')
    @unittest.mock.patch('fanboi2.utils.RateLimiter.limit')
    @unittest.mock.patch('fanboi2.tasks.add_topic.delay')
    def test_board_new_post_failed(self, add_, limit_):
        from fanboi2.models import DBSession, Topic
        from fanboi2.views.pages import board_new_post


@@ 662,9 662,9 @@ class TestPageViews(ViewMixin, unittest.TestCase):
            'body': ['This field is required.']
        })

    @mock.patch('fanboi2.utils.RateLimiter.timeleft')
    @mock.patch('fanboi2.utils.RateLimiter.limited')
    @mock.patch('fanboi2.tasks.add_topic.delay')
    @unittest.mock.patch('fanboi2.utils.RateLimiter.timeleft')
    @unittest.mock.patch('fanboi2.utils.RateLimiter.limited')
    @unittest.mock.patch('fanboi2.tasks.add_topic.delay')
    def test_board_new_post_limited(self, add_, limited_, time_):
        from fanboi2.models import DBSession, Topic
        from fanboi2.views.pages import board_new_post


@@ 728,7 728,7 @@ class TestPageViews(ViewMixin, unittest.TestCase):
        self.assertSAEqual(response['posts'], [post])

    # noinspection PyUnresolvedReferences
    @mock.patch('fanboi2.tasks.celery.AsyncResult')
    @unittest.mock.patch('fanboi2.tasks.celery.AsyncResult')
    def test_topic_show_get_task(self, result_):
        from fanboi2.views.pages import topic_show_get
        board = self._makeBoard(title='Foobar', slug='foobar')


@@ 753,7 753,7 @@ class TestPageViews(ViewMixin, unittest.TestCase):
        self.assertEqual(response.location, location)
        result_.assert_called_with('dummy')

    @mock.patch('fanboi2.tasks.celery.AsyncResult')
    @unittest.mock.patch('fanboi2.tasks.celery.AsyncResult')
    def test_topic_show_get_task_wait(self, result_):
        from fanboi2.views.pages import topic_show_get
        board = self._makeBoard(title='Foobar', slug='foobar')


@@ 772,7 772,7 @@ class TestPageViews(ViewMixin, unittest.TestCase):
        topic_show_get(request)
        result_.assert_called_with('dummy')

    @mock.patch('fanboi2.tasks.celery.AsyncResult')
    @unittest.mock.patch('fanboi2.tasks.celery.AsyncResult')
    def test_topic_show_get_spam_rejected(self, result_):
        from fanboi2.views.pages import topic_show_get
        board = self._makeBoard(title='Foobar', slug='foobar')


@@ 794,7 794,7 @@ class TestPageViews(ViewMixin, unittest.TestCase):
        result_.assert_called_with('dummy')
        self.assertEqual(response.status, '422 Unprocessable Entity')

    @mock.patch('fanboi2.tasks.celery.AsyncResult')
    @unittest.mock.patch('fanboi2.tasks.celery.AsyncResult')
    def test_topic_show_get_status_rejected(self, result_):
        from fanboi2.views.pages import topic_show_get
        board = self._makeBoard(title='Foobar', slug='foobar')


@@ 840,8 840,8 @@ class TestPageViews(ViewMixin, unittest.TestCase):
            topic_show_get(request)

    # noinspection PyUnresolvedReferences
    @mock.patch('fanboi2.utils.RateLimiter.limit')
    @mock.patch('fanboi2.tasks.add_post.delay')
    @unittest.mock.patch('fanboi2.utils.RateLimiter.limit')
    @unittest.mock.patch('fanboi2.tasks.add_post.delay')
    def test_topic_show_post(self, add_, limit_):
        from fanboi2.views.pages import topic_show_post
        board = self._makeBoard(title='Foobar', slug='foobar')


@@ 853,14 853,14 @@ class TestPageViews(ViewMixin, unittest.TestCase):
        request.matchdict['topic'] = topic.id
        config = self._makeConfig(request, self._makeRegistry())
        config.add_route('topic', '/{board}/{topic}')
        add_.return_value = mock.Mock(id='task-uuid')
        add_.return_value = unittest.mock.Mock(id='task-uuid')

        response = topic_show_post(self._make_csrf(request))
        location = '/%s/%s?task=task-uuid' % (board.slug, topic.id)
        self.assertEqual(response.location, location)
        limit_.assert_called_with(board.settings['post_delay'])
        add_.assert_called_with(
            request=mock.ANY,
            request=unittest.mock.ANY,
            topic_id=topic.id,
            body='Words words',
            bumped=False,


@@ 899,8 899,8 @@ class TestPageViews(ViewMixin, unittest.TestCase):
        with self.assertRaises(HTTPNotFound):
            topic_show_post(self._make_csrf(request))

    @mock.patch('fanboi2.utils.RateLimiter.limit')
    @mock.patch('fanboi2.tasks.add_post.delay')
    @unittest.mock.patch('fanboi2.utils.RateLimiter.limit')
    @unittest.mock.patch('fanboi2.tasks.add_post.delay')
    def test_topic_show_post_failed(self, add_, limit_):
        from fanboi2.models import DBSession, Post
        from fanboi2.views.pages import topic_show_post


@@ 923,9 923,9 @@ class TestPageViews(ViewMixin, unittest.TestCase):
            'body': ['This field is required.']
        })

    @mock.patch('fanboi2.utils.RateLimiter.timeleft')
    @mock.patch('fanboi2.utils.RateLimiter.limited')
    @mock.patch('fanboi2.tasks.add_post.delay')
    @unittest.mock.patch('fanboi2.utils.RateLimiter.timeleft')
    @unittest.mock.patch('fanboi2.utils.RateLimiter.limited')
    @unittest.mock.patch('fanboi2.tasks.add_post.delay')
    def test_board_show_post_limited(self, add_, limited_, time_):
        from fanboi2.models import DBSession, Post
        from fanboi2.views.pages import topic_show_post

M fanboi2/utils.py => fanboi2/utils.py +3 -2
@@ 1,7 1,7 @@
import hashlib
import requests
import socket
from IPy import IP
from ipaddress import ip_interface, ip_network
from .models import redis_conn
from .version import __VERSION__



@@ 48,7 48,8 @@ class Dnsbl(object) :
                try:
                    check = '.'.join(reversed(ip_address.split('.')))
                    res = socket.gethostbyname("%s.%s." % (check, provider))
                    if IP(res).make_net('255.0.0.0') == IP('127.0.0.0/8'):
                    ipaddr = ip_interface("%s/255.0.0.0" % (res,))
                    if ipaddr.network == ip_network('127.0.0.0/8'):
                        return True
                except (socket.gaierror, ValueError):
                    continue

M fanboi2/views/api.py => fanboi2/views/api.py +1 -1
@@ 22,7 22,7 @@ def _get_params(request):
    if request.content_type.startswith('application/json'):
        try:
            params = MultiDict(request.json_body)
        except ValueError:
        except ValueError:  # pragma: no cover
            pass
    return params


M setup.cfg => setup.cfg +3 -0
@@ 5,6 5,9 @@ cover-package=fanboi2
with-coverage=1
cover-erase=1

[coverage:report]
show_missing = true

[compile_catalog]
directory = fanboi2/locale
domain = fanboi2

M setup.py => setup.py +7 -12
@@ 8,7 8,7 @@ changes = open(os.path.join(here, 'CHANGES.rst')).read()
requires = [

    # Pyramid
    'pyramid >=1.5, <1.6',
    'pyramid >=1.7, <1.8',
    'pyramid_mako',
    'pyramid_tm',
    'pyramid_debugtoolbar',


@@ 16,33 16,28 @@ requires = [
    'waitress',

    # Backend
    'sqlalchemy >=0.9, <0.10',
    'alembic >=0.6.2, <0.7',
    'sqlalchemy >=1.0, <1.1',
    'alembic >=0.8, <0.9',
    'celery >=3.1, <3.2',
    'transaction',
    'pg8000',
    'psycopg2',
    'zope.sqlalchemy',
    'redis',
    'dogpile.cache',
    'python3-memcached',
    'pytz',
    'IPy',
    'requests',

    # Frontend
    'isodate',
    'MarkupSafe',
    'wtforms',

    # Tests
    'nose',
    'coverage <4.0', # 4.0 drops Python 3.2 support
    'mock',

    # Python 3.2 compatible
    'MarkupSafe==0.15', # https://github.com/mitsuhiko/markupsafe/pull/13
    'wtforms==1.0.3', # https://bitbucket.org/simplecodes/wtforms/issue/153/
    'coverage',

    # To be deprecate.
    'Pygments==1.6',
    'Markdown==2.5.2',

    ]