support for Python 3.3

This commit is contained in:
Martin Zimmermann 2013-10-09 16:28:54 +02:00
parent f9133b984e
commit 61a486d2ea
13 changed files with 109 additions and 60 deletions

View File

@ -33,8 +33,6 @@ dist = pkg_resources.get_distribution("isso")
import sys import sys
import os import os
import socket import socket
import httplib
import urlparse
from os.path import dirname, join from os.path import dirname, join
from argparse import ArgumentParser from argparse import ArgumentParser
@ -73,7 +71,7 @@ rules = Map([
class Isso(object): class Isso(object):
salt = "Eech7co8Ohloopo9Ol6baimi" salt = b"Eech7co8Ohloopo9Ol6baimi"
def __init__(self, conf): def __init__(self, conf):

View File

@ -1,10 +1,7 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
# from isso import compat # from isso import compat
# from isso.compat import text_type as str, string_types from isso.compat import text_type as str, string_types
str = unicode
string_types = (unicode, str)
# @compat.implements_to_string # @compat.implements_to_string

21
isso/compat.py Normal file
View File

@ -0,0 +1,21 @@
# -*- encoding: utf-8 -*-
import sys
PY2K = sys.version_info[0] == 2
if not PY2K:
# iterkeys = lambda d: iter(d.keys())
# iteritems = lambda d: iter(d.items())
text_type = str
string_types = (str, )
buffer = memoryview
else:
# iterkeys = lambda d: d.iterkeys()
# iteritems = lambda d: d.iteritems()
text_type = unicode
string_types = (str, unicode)
buffer = buffer

View File

@ -6,16 +6,11 @@ import io
import os import os
import time import time
import binascii import binascii
import thread
import threading import threading
import socket import socket
import smtplib import smtplib
import httplib
import urlparse
from configparser import ConfigParser from configparser import ConfigParser
try: try:
@ -23,6 +18,19 @@ try:
except ImportError: except ImportError:
uwsgi = None uwsgi = None
from isso.compat import PY2K
if PY2K:
import thread
import httplib
import urlparse
else:
import _thread as thread
import http.client as httplib
import urllib.parse as urlparse
from isso import notify, colors from isso import notify, colors

View File

@ -10,6 +10,8 @@ import hashlib
import binascii import binascii
import operator import operator
from functools import reduce
def _bin_to_long(x): def _bin_to_long(x):
""" """
@ -76,7 +78,7 @@ def _pbkdf2(password, salt, iterations, dklen=0, digest=None):
def F(i): def F(i):
def U(): def U():
u = salt + struct.pack(b'>I', i) u = salt + struct.pack(b'>I', i)
for j in xrange(int(iterations)): for j in range(int(iterations)):
u = _fast_hmac(password, u, digest).digest() u = _fast_hmac(password, u, digest).digest()
yield _bin_to_long(u) yield _bin_to_long(u)
return _long_to_bin(reduce(operator.xor, U()), hex_format_string) return _long_to_bin(reduce(operator.xor, U()), hex_format_string)

View File

@ -4,6 +4,7 @@ import time
from isso.db import spam from isso.db import spam
from isso.utils import Bloomfilter from isso.utils import Bloomfilter
from isso.compat import buffer
class Comments: class Comments:
@ -77,7 +78,7 @@ class Comments:
'UPDATE comments SET', 'UPDATE comments SET',
','.join(key + '=' + '?' for key in data), ','.join(key + '=' + '?' for key in data),
'WHERE id=?;'], 'WHERE id=?;'],
data.values() + [id]) list(data.values()) + [id])
return self.get(id) return self.get(id)

View File

@ -11,9 +11,13 @@ import sys
import os import os
from time import mktime, strptime from time import mktime, strptime
from urlparse import urlparse
from collections import defaultdict from collections import defaultdict
try:
from urlparse import urlparse
except ImportError:
from urllib.parse import urlparse
from xml.etree import ElementTree from xml.etree import ElementTree

View File

@ -3,13 +3,18 @@
from __future__ import division from __future__ import division
import socket import socket
import httplib
try:
import httplib
from urlparse import urlparse
except ImportError:
import http.client as httplib
from urllib.parse import urlparse
import random import random
import hashlib import hashlib
from string import ascii_letters, digits from string import ascii_letters, digits
from urlparse import urlparse
from contextlib import closing from contextlib import closing
import html5lib import html5lib

View File

@ -12,6 +12,8 @@ from itsdangerous import SignatureExpired, BadSignature
from werkzeug.wrappers import Response from werkzeug.wrappers import Response
from werkzeug.exceptions import abort, BadRequest from werkzeug.exceptions import abort, BadRequest
from isso.compat import text_type as str
from isso import utils, notify, db from isso import utils, notify, db
from isso.crypto import pbkdf2 from isso.crypto import pbkdf2
@ -48,7 +50,7 @@ def new(app, environ, request, uri):
return Response('URI does not exist', 404) return Response('URI does not exist', 404)
try: try:
data = json.loads(request.data) data = json.loads(request.get_data().decode('utf-8'))
except ValueError: except ValueError:
return Response("No JSON object could be decoded", 400) return Response("No JSON object could be decoded", 400)
@ -65,7 +67,7 @@ def new(app, environ, request, uri):
if data.get(field): if data.get(field):
data[field] = cgi.escape(data[field]) data[field] = cgi.escape(data[field])
data['remote_addr'] = utils.anonymize(unicode(request.remote_addr)) data['remote_addr'] = utils.anonymize(str(request.remote_addr))
with app.lock: with app.lock:
if uri not in app.db.threads: if uri not in app.db.threads:
@ -82,14 +84,14 @@ def new(app, environ, request, uri):
abort(403) abort(403)
href = (app.conf.get('general', 'host').rstrip("/") + uri + "#isso-%i" % rv["id"]) href = (app.conf.get('general', 'host').rstrip("/") + uri + "#isso-%i" % rv["id"])
app.notify(title, notify.format(rv, href, utils.anonymize(unicode(request.remote_addr)))) app.notify(title, notify.format(rv, href, utils.anonymize(str(request.remote_addr))))
# save checksum of text into cookie, so mallory can't modify/delete a comment, if # save checksum of text into cookie, so mallory can't modify/delete a comment, if
# he add a comment, then removed it but not the signed cookie. # he add a comment, then removed it but not the signed cookie.
checksum = hashlib.md5(rv["text"].encode('utf-8')).hexdigest() checksum = hashlib.md5(rv["text"].encode('utf-8')).hexdigest()
rv["text"] = app.markdown(rv["text"]) rv["text"] = app.markdown(rv["text"])
rv["hash"] = pbkdf2(rv.get('email') or rv['remote_addr'], app.salt, 1000, 6) rv["hash"] = str(pbkdf2(rv.get('email') or rv['remote_addr'], app.salt, 1000, 6))
for key in set(rv.keys()) - FIELDS: for key in set(rv.keys()) - FIELDS:
rv.pop(key) rv.pop(key)
@ -132,7 +134,7 @@ def single(app, environ, request, id):
if request.method == 'PUT': if request.method == 'PUT':
try: try:
data = json.loads(request.data) data = json.loads(request.get_data().decode('utf-8'))
except ValueError: except ValueError:
return Response("No JSON object could be decoded", 400) return Response("No JSON object could be decoded", 400)
@ -181,7 +183,7 @@ def fetch(app, environ, request, uri):
for item in rv: for item in rv:
item['hash'] = pbkdf2(item['email'] or item['remote_addr'], app.salt, 1000, 6) item['hash'] = str(pbkdf2(item['email'] or item['remote_addr'], app.salt, 1000, 6))
for key in set(item.keys()) - FIELDS: for key in set(item.keys()) - FIELDS:
item.pop(key) item.pop(key)
@ -195,13 +197,13 @@ def fetch(app, environ, request, uri):
def like(app, environ, request, id): def like(app, environ, request, id):
nv = app.db.comments.vote(True, id, utils.anonymize(unicode(request.remote_addr))) nv = app.db.comments.vote(True, id, utils.anonymize(str(request.remote_addr)))
return Response(json.dumps(nv), 200) return Response(json.dumps(nv), 200)
def dislike(app, environ, request, id): def dislike(app, environ, request, id):
nv = app.db.comments.vote(False, id, utils.anonymize(unicode(request.remote_addr))) nv = app.db.comments.vote(False, id, utils.anonymize(str(request.remote_addr)))
return Response(json.dumps(nv), 200) return Response(json.dumps(nv), 200)
@ -217,4 +219,4 @@ def count(app, environ, request, uri):
def checkip(app, env, req): def checkip(app, env, req):
return Response(utils.anonymize(unicode(req.remote_addr)), 200) return Response(utils.anonymize(str(req.remote_addr)), 200)

View File

@ -7,6 +7,9 @@ from setuptools import setup, find_packages
requires = ['Jinja2>=2.7', 'werkzeug>=0.9', 'itsdangerous', 'misaka', 'html5lib'] requires = ['Jinja2>=2.7', 'werkzeug>=0.9', 'itsdangerous', 'misaka', 'html5lib']
if (3, 0) <= sys.version_info < (3, 3):
raise SystemExit("Python 3.0, 3.1 and 3.2 are not supported")
if sys.version_info < (3, 0): if sys.version_info < (3, 0):
requires += ['ipaddress', 'configparser'] requires += ['ipaddress', 'configparser']
@ -28,7 +31,8 @@ setup(
"Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
"License :: OSI Approved :: BSD License", "License :: OSI Approved :: BSD License",
"Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7" "Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.3"
], ],
install_requires=requires, install_requires=requires,
entry_points={ entry_points={

View File

@ -3,10 +3,14 @@ from __future__ import unicode_literals
import os import os
import json import json
import urllib
import tempfile import tempfile
import unittest import unittest
try:
from urllib.parse import urlencode
except ImportError:
from urllib import urlencode
from werkzeug.test import Client from werkzeug.test import Client
from werkzeug.wrappers import Response from werkzeug.wrappers import Response
@ -16,6 +20,8 @@ from isso.views import comment
utils.heading = lambda *args: "Untitled." utils.heading = lambda *args: "Untitled."
utils.urlexists = lambda *args: True utils.urlexists = lambda *args: True
loads = lambda data: json.loads(data.decode('utf-8'))
class FakeIP(object): class FakeIP(object):
@ -56,7 +62,7 @@ class TestComments(unittest.TestCase):
r = self.get('/id/1') r = self.get('/id/1')
assert r.status_code == 200 assert r.status_code == 200
rv = json.loads(r.data) rv = loads(r.data)
assert rv['id'] == 1 assert rv['id'] == 1
assert rv['text'] == '<p>Lorem ipsum ...</p>\n' assert rv['text'] == '<p>Lorem ipsum ...</p>\n'
@ -66,9 +72,9 @@ class TestComments(unittest.TestCase):
rv = self.post('/new?uri=%2Fpath%2F', data=json.dumps({'text': 'Lorem ipsum ...'})) rv = self.post('/new?uri=%2Fpath%2F', data=json.dumps({'text': 'Lorem ipsum ...'}))
assert rv.status_code == 201 assert rv.status_code == 201
assert len(filter(lambda header: header[0] == 'Set-Cookie', rv.headers)) == 1 assert any(filter(lambda header: header[0] == 'Set-Cookie', rv.headers))
rv = json.loads(rv.data) rv = loads(rv.data)
assert rv["mode"] == 1 assert rv["mode"] == 1
assert rv["text"] == '<p>Lorem ipsum ...</p>\n' assert rv["text"] == '<p>Lorem ipsum ...</p>\n'
@ -79,9 +85,9 @@ class TestComments(unittest.TestCase):
b = self.post('/new?uri=test', data=json.dumps({'text': '...'})) b = self.post('/new?uri=test', data=json.dumps({'text': '...'}))
c = self.post('/new?uri=test', data=json.dumps({'text': '...'})) c = self.post('/new?uri=test', data=json.dumps({'text': '...'}))
assert json.loads(a.data)["id"] == 1 assert loads(a.data)["id"] == 1
assert json.loads(b.data)["id"] == 2 assert loads(b.data)["id"] == 2
assert json.loads(c.data)["id"] == 3 assert loads(c.data)["id"] == 3
def testCreateAndGetMultiple(self): def testCreateAndGetMultiple(self):
@ -91,7 +97,7 @@ class TestComments(unittest.TestCase):
r = self.get('/?uri=%2Fpath%2F') r = self.get('/?uri=%2Fpath%2F')
assert r.status_code == 200 assert r.status_code == 200
rv = json.loads(r.data) rv = loads(r.data)
assert len(rv) == 20 assert len(rv) == 20
def testGetInvalid(self): def testGetInvalid(self):
@ -109,7 +115,7 @@ class TestComments(unittest.TestCase):
r = self.get('/id/1?plain=1') r = self.get('/id/1?plain=1')
assert r.status_code == 200 assert r.status_code == 200
rv = json.loads(r.data) rv = loads(r.data)
assert rv['text'] == 'Hello World' assert rv['text'] == 'Hello World'
assert rv['author'] == 'me' assert rv['author'] == 'me'
assert rv['website'] == 'http://example.com/' assert rv['website'] == 'http://example.com/'
@ -120,7 +126,7 @@ class TestComments(unittest.TestCase):
self.post('/new?uri=%2Fpath%2F', data=json.dumps({'text': 'Lorem ipsum ...'})) self.post('/new?uri=%2Fpath%2F', data=json.dumps({'text': 'Lorem ipsum ...'}))
r = self.delete('/id/1') r = self.delete('/id/1')
assert r.status_code == 200 assert r.status_code == 200
assert json.loads(r.data) == None assert loads(r.data) == None
assert self.get('/id/1').status_code == 404 assert self.get('/id/1').status_code == 404
def testDeleteWithReference(self): def testDeleteWithReference(self):
@ -131,8 +137,7 @@ class TestComments(unittest.TestCase):
r = client.delete('/id/1') r = client.delete('/id/1')
assert r.status_code == 200 assert r.status_code == 200
print r.data assert loads(r.data)['mode'] == 4
assert json.loads(r.data)['mode'] == 4
assert self.get('/?uri=%2Fpath%2F&id=1').status_code == 200 assert self.get('/?uri=%2Fpath%2F&id=1').status_code == 200
assert self.get('/?uri=%2Fpath%2F&id=2').status_code == 200 assert self.get('/?uri=%2Fpath%2F&id=2').status_code == 200
@ -175,11 +180,11 @@ class TestComments(unittest.TestCase):
paths = ['/sub/path/', '/path.html', '/sub/path.html', 'path', '/'] paths = ['/sub/path/', '/path.html', '/sub/path.html', 'path', '/']
for path in paths: for path in paths:
assert self.post('/new?' + urllib.urlencode({'uri': path}), assert self.post('/new?' + urlencode({'uri': path}),
data=json.dumps({'text': '...'})).status_code == 201 data=json.dumps({'text': '...'})).status_code == 201
for i, path in enumerate(paths): for i, path in enumerate(paths):
assert self.get('/?' + urllib.urlencode({'uri': path})).status_code == 200 assert self.get('/?' + urlencode({'uri': path})).status_code == 200
assert self.get('/id/%i' % (i + 1)).status_code == 200 assert self.get('/id/%i' % (i + 1)).status_code == 200
def testDeleteAndCreateByDifferentUsersButSamePostId(self): def testDeleteAndCreateByDifferentUsersButSamePostId(self):
@ -201,9 +206,9 @@ class TestComments(unittest.TestCase):
c = self.post('/new?uri=%2Fpath%2F', data=json.dumps({"text": "Ccc", "email": "..."})) c = self.post('/new?uri=%2Fpath%2F', data=json.dumps({"text": "Ccc", "email": "..."}))
assert a.status_code == b.status_code == c.status_code == 201 assert a.status_code == b.status_code == c.status_code == 201
a = json.loads(a.data) a = loads(a.data)
b = json.loads(b.data) b = loads(b.data)
c = json.loads(c.data) c = loads(c.data)
assert a['hash'] != '192.168.1.1' assert a['hash'] != '192.168.1.1'
assert a['hash'] == b['hash'] assert a['hash'] == b['hash']
@ -214,12 +219,12 @@ class TestComments(unittest.TestCase):
rv = self.post('/new?uri=%2Fpath%2F', data=json.dumps({"text": "..."})) rv = self.post('/new?uri=%2Fpath%2F', data=json.dumps({"text": "..."}))
assert rv.status_code == 201 assert rv.status_code == 201
rv = json.loads(rv.data) rv = loads(rv.data)
for key in comment.FIELDS: for key in comment.FIELDS:
rv.pop(key) rv.pop(key)
assert rv.keys() == [] assert not any(rv.keys())
def testCounts(self): def testCounts(self):
@ -228,14 +233,14 @@ class TestComments(unittest.TestCase):
rv = self.get('/count?uri=%2Fpath%2F') rv = self.get('/count?uri=%2Fpath%2F')
assert rv.status_code == 200 assert rv.status_code == 200
assert json.loads(rv.data) == 1 assert loads(rv.data) == 1
for x in range(3): for x in range(3):
self.post('/new?uri=%2Fpath%2F', data=json.dumps({"text": "..."})) self.post('/new?uri=%2Fpath%2F', data=json.dumps({"text": "..."}))
rv = self.get('/count?uri=%2Fpath%2F') rv = self.get('/count?uri=%2Fpath%2F')
assert rv.status_code == 200 assert rv.status_code == 200
assert json.loads(rv.data) == 4 assert loads(rv.data) == 4
for x in range(4): for x in range(4):
self.delete('/id/%i' % (x + 1)) self.delete('/id/%i' % (x + 1))
@ -247,7 +252,7 @@ class TestComments(unittest.TestCase):
self.post('/new?uri=test', data=json.dumps({"text": "Tpyo"})) self.post('/new?uri=test', data=json.dumps({"text": "Tpyo"}))
self.put('/id/1', data=json.dumps({"text": "Tyop"})) self.put('/id/1', data=json.dumps({"text": "Tyop"}))
assert json.loads(self.get('/id/1').data)["text"] == "<p>Tyop</p>\n" assert loads(self.get('/id/1').data)["text"] == "<p>Tyop</p>\n"
self.put('/id/1', data=json.dumps({"text": "Typo"})) self.put('/id/1', data=json.dumps({"text": "Typo"}))
assert json.loads(self.get('/id/1').data)["text"] == "<p>Typo</p>\n" assert loads(self.get('/id/1').data)["text"] == "<p>Typo</p>\n"

View File

@ -14,6 +14,8 @@ from isso import Isso, notify, utils, core
utils.heading = lambda *args: "Untitled." utils.heading = lambda *args: "Untitled."
utils.urlexists = lambda *args: True utils.urlexists = lambda *args: True
loads = lambda data: json.loads(data.decode('utf-8'))
class FakeIP(object): class FakeIP(object):
@ -51,7 +53,7 @@ class TestVote(unittest.TestCase):
def testZeroLikes(self): def testZeroLikes(self):
rv = self.makeClient("127.0.0.1").post("/new?uri=test", data=json.dumps({"text": "..."})) rv = self.makeClient("127.0.0.1").post("/new?uri=test", data=json.dumps({"text": "..."}))
assert json.loads(rv.data)['likes'] == json.loads(rv.data)['dislikes'] == 0 assert loads(rv.data)['likes'] == loads(rv.data)['dislikes'] == 0
def testSingleLike(self): def testSingleLike(self):
@ -59,7 +61,7 @@ class TestVote(unittest.TestCase):
rv = self.makeClient("0.0.0.0").post("/id/1/like") rv = self.makeClient("0.0.0.0").post("/id/1/like")
assert rv.status_code == 200 assert rv.status_code == 200
assert json.loads(rv.data)["likes"] == 1 assert loads(rv.data)["likes"] == 1
def testSelfLike(self): def testSelfLike(self):
@ -68,7 +70,7 @@ class TestVote(unittest.TestCase):
rv = bob.post('/id/1/like') rv = bob.post('/id/1/like')
assert rv.status_code == 200 assert rv.status_code == 200
assert json.loads(rv.data)["likes"] == 0 assert loads(rv.data)["likes"] == 0
def testMultipleLikes(self): def testMultipleLikes(self):
@ -76,12 +78,12 @@ class TestVote(unittest.TestCase):
for num in range(15): for num in range(15):
rv = self.makeClient("1.2.%i.0" % num).post('/id/1/like') rv = self.makeClient("1.2.%i.0" % num).post('/id/1/like')
assert rv.status_code == 200 assert rv.status_code == 200
assert json.loads(rv.data)["likes"] == num + 1 assert loads(rv.data)["likes"] == num + 1
def testVoteOnNonexistentComment(self): def testVoteOnNonexistentComment(self):
rv = self.makeClient("1.2.3.4").post('/id/1/like') rv = self.makeClient("1.2.3.4").post('/id/1/like')
assert rv.status_code == 200 assert rv.status_code == 200
assert json.loads(rv.data) == None assert loads(rv.data) == None
def testTooManyLikes(self): def testTooManyLikes(self):
@ -91,14 +93,14 @@ class TestVote(unittest.TestCase):
assert rv.status_code == 200 assert rv.status_code == 200
if num >= 142: if num >= 142:
assert json.loads(rv.data)["likes"] == 142 assert loads(rv.data)["likes"] == 142
else: else:
assert json.loads(rv.data)["likes"] == num + 1 assert loads(rv.data)["likes"] == num + 1
def testDislike(self): def testDislike(self):
self.makeClient("127.0.0.1").post("/new?uri=test", data=json.dumps({"text": "..."})) self.makeClient("127.0.0.1").post("/new?uri=test", data=json.dumps({"text": "..."}))
rv = self.makeClient("1.2.3.4").post('/id/1/dislike') rv = self.makeClient("1.2.3.4").post('/id/1/dislike')
assert rv.status_code == 200 assert rv.status_code == 200
assert json.loads(rv.data)['likes'] == 0 assert loads(rv.data)['likes'] == 0
assert json.loads(rv.data)['dislikes'] == 1 assert loads(rv.data)['dislikes'] == 1

View File

@ -1,5 +1,5 @@
[tox] [tox]
envlist = py26,py27 envlist = py26,py27,py33
indexserver = indexserver =
default = https://pypi.switch.posativ.org/root/pypi/+simple default = https://pypi.switch.posativ.org/root/pypi/+simple
[testenv:py26] [testenv:py26]