diff --git a/isso/__init__.py b/isso/__init__.py index 4c056b2..87c7256 100644 --- a/isso/__init__.py +++ b/isso/__init__.py @@ -33,8 +33,6 @@ dist = pkg_resources.get_distribution("isso") import sys import os import socket -import httplib -import urlparse from os.path import dirname, join from argparse import ArgumentParser @@ -73,7 +71,7 @@ rules = Map([ class Isso(object): - salt = "Eech7co8Ohloopo9Ol6baimi" + salt = b"Eech7co8Ohloopo9Ol6baimi" def __init__(self, conf): diff --git a/isso/colors.py b/isso/colors.py index 1d1eb0b..dc5785e 100644 --- a/isso/colors.py +++ b/isso/colors.py @@ -1,10 +1,7 @@ # -*- encoding: utf-8 -*- # from isso import compat -# from isso.compat import text_type as str, string_types - -str = unicode -string_types = (unicode, str) +from isso.compat import text_type as str, string_types # @compat.implements_to_string diff --git a/isso/compat.py b/isso/compat.py new file mode 100644 index 0000000..888afe0 --- /dev/null +++ b/isso/compat.py @@ -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 diff --git a/isso/core.py b/isso/core.py index 364369e..87713db 100644 --- a/isso/core.py +++ b/isso/core.py @@ -6,16 +6,11 @@ import io import os import time import binascii - -import thread import threading import socket import smtplib -import httplib -import urlparse - from configparser import ConfigParser try: @@ -23,6 +18,19 @@ try: except ImportError: 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 diff --git a/isso/crypto.py b/isso/crypto.py index 1cdb82e..d3c9fe9 100644 --- a/isso/crypto.py +++ b/isso/crypto.py @@ -10,6 +10,8 @@ import hashlib import binascii import operator +from functools import reduce + def _bin_to_long(x): """ @@ -76,7 +78,7 @@ def _pbkdf2(password, salt, iterations, dklen=0, digest=None): def F(i): def U(): 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() yield _bin_to_long(u) return _long_to_bin(reduce(operator.xor, U()), hex_format_string) diff --git a/isso/db/comments.py b/isso/db/comments.py index 154aa70..2027b67 100644 --- a/isso/db/comments.py +++ b/isso/db/comments.py @@ -4,6 +4,7 @@ import time from isso.db import spam from isso.utils import Bloomfilter +from isso.compat import buffer class Comments: @@ -77,7 +78,7 @@ class Comments: 'UPDATE comments SET', ','.join(key + '=' + '?' for key in data), 'WHERE id=?;'], - data.values() + [id]) + list(data.values()) + [id]) return self.get(id) diff --git a/isso/migrate.py b/isso/migrate.py index 94703ec..1feaf04 100644 --- a/isso/migrate.py +++ b/isso/migrate.py @@ -11,9 +11,13 @@ import sys import os from time import mktime, strptime -from urlparse import urlparse from collections import defaultdict +try: + from urlparse import urlparse +except ImportError: + from urllib.parse import urlparse + from xml.etree import ElementTree diff --git a/isso/utils.py b/isso/utils.py index f5e4b44..715b2ef 100644 --- a/isso/utils.py +++ b/isso/utils.py @@ -3,13 +3,18 @@ from __future__ import division 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 hashlib from string import ascii_letters, digits -from urlparse import urlparse from contextlib import closing import html5lib diff --git a/isso/views/comment.py b/isso/views/comment.py index 46f2ea3..d03b040 100644 --- a/isso/views/comment.py +++ b/isso/views/comment.py @@ -12,6 +12,8 @@ from itsdangerous import SignatureExpired, BadSignature from werkzeug.wrappers import Response from werkzeug.exceptions import abort, BadRequest +from isso.compat import text_type as str + from isso import utils, notify, db from isso.crypto import pbkdf2 @@ -48,7 +50,7 @@ def new(app, environ, request, uri): return Response('URI does not exist', 404) try: - data = json.loads(request.data) + data = json.loads(request.get_data().decode('utf-8')) except ValueError: return Response("No JSON object could be decoded", 400) @@ -65,7 +67,7 @@ def new(app, environ, request, uri): if data.get(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: if uri not in app.db.threads: @@ -82,14 +84,14 @@ def new(app, environ, request, uri): abort(403) 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 # he add a comment, then removed it but not the signed cookie. checksum = hashlib.md5(rv["text"].encode('utf-8')).hexdigest() 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: rv.pop(key) @@ -132,7 +134,7 @@ def single(app, environ, request, id): if request.method == 'PUT': try: - data = json.loads(request.data) + data = json.loads(request.get_data().decode('utf-8')) except ValueError: return Response("No JSON object could be decoded", 400) @@ -181,7 +183,7 @@ def fetch(app, environ, request, uri): 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: item.pop(key) @@ -195,13 +197,13 @@ def fetch(app, environ, request, uri): 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) 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) @@ -217,4 +219,4 @@ def count(app, environ, request, uri): def checkip(app, env, req): - return Response(utils.anonymize(unicode(req.remote_addr)), 200) + return Response(utils.anonymize(str(req.remote_addr)), 200) diff --git a/setup.py b/setup.py index c688990..93d1317 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,9 @@ from setuptools import setup, find_packages 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): requires += ['ipaddress', 'configparser'] @@ -28,7 +31,8 @@ setup( "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 2.6", - "Programming Language :: Python :: 2.7" + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.3" ], install_requires=requires, entry_points={ diff --git a/specs/test_comment.py b/specs/test_comment.py index cf12993..13bf557 100644 --- a/specs/test_comment.py +++ b/specs/test_comment.py @@ -3,10 +3,14 @@ from __future__ import unicode_literals import os import json -import urllib import tempfile import unittest +try: + from urllib.parse import urlencode +except ImportError: + from urllib import urlencode + from werkzeug.test import Client from werkzeug.wrappers import Response @@ -16,6 +20,8 @@ from isso.views import comment utils.heading = lambda *args: "Untitled." utils.urlexists = lambda *args: True +loads = lambda data: json.loads(data.decode('utf-8')) + class FakeIP(object): @@ -56,7 +62,7 @@ class TestComments(unittest.TestCase): r = self.get('/id/1') assert r.status_code == 200 - rv = json.loads(r.data) + rv = loads(r.data) assert rv['id'] == 1 assert rv['text'] == '

Lorem ipsum ...

\n' @@ -66,9 +72,9 @@ class TestComments(unittest.TestCase): rv = self.post('/new?uri=%2Fpath%2F', data=json.dumps({'text': 'Lorem ipsum ...'})) 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["text"] == '

Lorem ipsum ...

\n' @@ -79,9 +85,9 @@ class TestComments(unittest.TestCase): b = 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 json.loads(b.data)["id"] == 2 - assert json.loads(c.data)["id"] == 3 + assert loads(a.data)["id"] == 1 + assert loads(b.data)["id"] == 2 + assert loads(c.data)["id"] == 3 def testCreateAndGetMultiple(self): @@ -91,7 +97,7 @@ class TestComments(unittest.TestCase): r = self.get('/?uri=%2Fpath%2F') assert r.status_code == 200 - rv = json.loads(r.data) + rv = loads(r.data) assert len(rv) == 20 def testGetInvalid(self): @@ -109,7 +115,7 @@ class TestComments(unittest.TestCase): r = self.get('/id/1?plain=1') assert r.status_code == 200 - rv = json.loads(r.data) + rv = loads(r.data) assert rv['text'] == 'Hello World' assert rv['author'] == 'me' 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 ...'})) r = self.delete('/id/1') assert r.status_code == 200 - assert json.loads(r.data) == None + assert loads(r.data) == None assert self.get('/id/1').status_code == 404 def testDeleteWithReference(self): @@ -131,8 +137,7 @@ class TestComments(unittest.TestCase): r = client.delete('/id/1') assert r.status_code == 200 - print r.data - assert json.loads(r.data)['mode'] == 4 + assert loads(r.data)['mode'] == 4 assert self.get('/?uri=%2Fpath%2F&id=1').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', '/'] 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 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 def testDeleteAndCreateByDifferentUsersButSamePostId(self): @@ -201,9 +206,9 @@ class TestComments(unittest.TestCase): c = self.post('/new?uri=%2Fpath%2F', data=json.dumps({"text": "Ccc", "email": "..."})) assert a.status_code == b.status_code == c.status_code == 201 - a = json.loads(a.data) - b = json.loads(b.data) - c = json.loads(c.data) + a = loads(a.data) + b = loads(b.data) + c = loads(c.data) assert a['hash'] != '192.168.1.1' assert a['hash'] == b['hash'] @@ -214,12 +219,12 @@ class TestComments(unittest.TestCase): rv = self.post('/new?uri=%2Fpath%2F', data=json.dumps({"text": "..."})) assert rv.status_code == 201 - rv = json.loads(rv.data) + rv = loads(rv.data) for key in comment.FIELDS: rv.pop(key) - assert rv.keys() == [] + assert not any(rv.keys()) def testCounts(self): @@ -228,14 +233,14 @@ class TestComments(unittest.TestCase): rv = self.get('/count?uri=%2Fpath%2F') assert rv.status_code == 200 - assert json.loads(rv.data) == 1 + assert loads(rv.data) == 1 for x in range(3): self.post('/new?uri=%2Fpath%2F', data=json.dumps({"text": "..."})) rv = self.get('/count?uri=%2Fpath%2F') assert rv.status_code == 200 - assert json.loads(rv.data) == 4 + assert loads(rv.data) == 4 for x in range(4): 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.put('/id/1', data=json.dumps({"text": "Tyop"})) - assert json.loads(self.get('/id/1').data)["text"] == "

Tyop

\n" + assert loads(self.get('/id/1').data)["text"] == "

Tyop

\n" self.put('/id/1', data=json.dumps({"text": "Typo"})) - assert json.loads(self.get('/id/1').data)["text"] == "

Typo

\n" + assert loads(self.get('/id/1').data)["text"] == "

Typo

\n" diff --git a/specs/test_vote.py b/specs/test_vote.py index 2426fe8..2a617af 100644 --- a/specs/test_vote.py +++ b/specs/test_vote.py @@ -14,6 +14,8 @@ from isso import Isso, notify, utils, core utils.heading = lambda *args: "Untitled." utils.urlexists = lambda *args: True +loads = lambda data: json.loads(data.decode('utf-8')) + class FakeIP(object): @@ -51,7 +53,7 @@ class TestVote(unittest.TestCase): def testZeroLikes(self): 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): @@ -59,7 +61,7 @@ class TestVote(unittest.TestCase): rv = self.makeClient("0.0.0.0").post("/id/1/like") assert rv.status_code == 200 - assert json.loads(rv.data)["likes"] == 1 + assert loads(rv.data)["likes"] == 1 def testSelfLike(self): @@ -68,7 +70,7 @@ class TestVote(unittest.TestCase): rv = bob.post('/id/1/like') assert rv.status_code == 200 - assert json.loads(rv.data)["likes"] == 0 + assert loads(rv.data)["likes"] == 0 def testMultipleLikes(self): @@ -76,12 +78,12 @@ class TestVote(unittest.TestCase): for num in range(15): rv = self.makeClient("1.2.%i.0" % num).post('/id/1/like') assert rv.status_code == 200 - assert json.loads(rv.data)["likes"] == num + 1 + assert loads(rv.data)["likes"] == num + 1 def testVoteOnNonexistentComment(self): rv = self.makeClient("1.2.3.4").post('/id/1/like') assert rv.status_code == 200 - assert json.loads(rv.data) == None + assert loads(rv.data) == None def testTooManyLikes(self): @@ -91,14 +93,14 @@ class TestVote(unittest.TestCase): assert rv.status_code == 200 if num >= 142: - assert json.loads(rv.data)["likes"] == 142 + assert loads(rv.data)["likes"] == 142 else: - assert json.loads(rv.data)["likes"] == num + 1 + assert loads(rv.data)["likes"] == num + 1 def testDislike(self): 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') assert rv.status_code == 200 - assert json.loads(rv.data)['likes'] == 0 - assert json.loads(rv.data)['dislikes'] == 1 + assert loads(rv.data)['likes'] == 0 + assert loads(rv.data)['dislikes'] == 1 diff --git a/tox.ini b/tox.ini index 47be448..317a221 100755 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27 +envlist = py26,py27,py33 indexserver = default = https://pypi.switch.posativ.org/root/pypi/+simple [testenv:py26]