Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Preliminary Python 3.x support, using 2to3 #52

Open
wants to merge 16 commits into from

3 participants

@necaris

Add:

  • support for running 2to3 as part of the build process
  • a custom 2to3 fixer to ensure tests are patched too
  • minor changes to modules to ensure they work with Python 3
  • metadata to declare Python 3 support

The existing test suite passes under Python 3.2 on Linux (Fedora 17, x86_64)

@pythonmobile

were these added to the codebase yet?

@necaris

It doesn't look like the pull request has been merged, if that's what you mean?

Note that this doesn't actually make things work under Python 3, it just makes them work enough to be importable. If this is something more people need I could put some more effort into making it work, but for my current project I've simply started a port since backward compatibility is a bit more work (especially as there are a lot of issues with str / bytes boundaries in this project).

@pythonmobile

necaris: I am having this wierd problem that when I use flask openid with one machine, When I click the login button, I get an internal server error. I cant pin point the error. Any ideas how to do this?

@mitchellrj

@necaris It may be better to just add the use_2to3 flag to the setup.py and let distribute handle the rest as appropriate. Better still, hand-tune the Python 3 adaptations so that Python < 2.6 is still supported.

@necaris

@mitchellrj cheers for the tip -- unfortunately this patch doesn't actually come close to making it work on Python 3, it just fixes the most glaring of the SyntaxErrors you get even after 2to3 runs.

Since this package works separately with text and bytestrings, and the boundaries between them aren't well-defined, I found it too painful to try and make all of the code upgradable with 2to3 while remaining compatible with older versions of Python. I'm keeping this pull request open just in case there's some interest from upstream, but I've moved most of my effort over to actually porting the library (https://pypi.python.org/pypi/python3-openid/3.0.1) and maybe slowly cleaning it up and modernizing things.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
0  custom_fixers/__init__.py
No changes.
View
17 custom_fixers/fix_file_builtin.py
@@ -0,0 +1,17 @@
+from lib2to3.fixer_base import BaseFix
+from lib2to3.fixer_util import is_probably_builtin
+from lib2to3.pgen2 import token
+
+
+class FixFileBuiltin(BaseFix):
+
+ _accept_type = token.NAME
+
+ def match(self, node):
+ if node.value == 'file' and is_probably_builtin(node):
+ return True
+ return False
+
+ def transform(self, node, results):
+ node.value = 'open'
+ node.changed()
View
2  openid/__init__.py
@@ -45,7 +45,7 @@
# Parse the version info
try:
- version_info = map(int, __version__.split('.'))
+ version_info = tuple(map(int, __version__.split('.')))
except ValueError:
version_info = (None, None, None)
else:
View
22 openid/consumer/consumer.py
@@ -345,7 +345,7 @@ def begin(self, user_url, anonymous=False):
service = disco.getNextService(self._discover)
except fetchers.HTTPFetchingError, why:
raise DiscoveryFailure(
- 'Error fetching XRDS document: %s' % (why[0],), None)
+ 'Error fetching XRDS document: %s' % (why.why,), None)
if service is None:
raise DiscoveryFailure(
@@ -646,7 +646,7 @@ def _complete_id_res(self, message, endpoint, return_to):
try:
return self._doIdRes(message, endpoint, return_to)
except (ProtocolError, DiscoveryFailure), why:
- return FailureResponse(endpoint, why[0])
+ return FailureResponse(endpoint, why)
def _completeInvalid(self, message, endpoint, _):
mode = message.getArg(OPENID_NS, 'mode', '<No mode set>')
@@ -663,7 +663,7 @@ def _checkReturnTo(self, message, return_to):
try:
self._verifyReturnToArgs(message.toPostArgs())
except ProtocolError, why:
- logging.exception("Verifying return_to arguments: %s" % (why[0],))
+ logging.exception("Verifying return_to arguments: %s" % (why,))
return False
# Check the return_to base URL against the one in the message.
@@ -770,7 +770,7 @@ def _idResCheckNonce(self, message, endpoint):
try:
timestamp, salt = splitNonce(nonce)
except ValueError, why:
- raise ProtocolError('Malformed nonce: %s' % (why[0],))
+ raise ProtocolError('Malformed nonce: %s' % (why,))
if (self.store is not None and
not self.store.useNonce(server_url, timestamp, salt)):
@@ -1262,18 +1262,18 @@ def _requestAssociation(self, endpoint, assoc_type, session_type):
try:
response = self._makeKVPost(args, endpoint.server_url)
except fetchers.HTTPFetchingError, why:
- logging.exception('openid.associate request failed: %s' % (why[0],))
+ logging.exception('openid.associate request failed: %s' % (why,))
return None
try:
assoc = self._extractAssociation(response, assoc_session)
except KeyError, why:
logging.exception('Missing required parameter in response from %s: %s'
- % (endpoint.server_url, why[0]))
+ % (endpoint.server_url, why))
return None
except ProtocolError, why:
logging.exception('Protocol error parsing response from %s: %s' % (
- endpoint.server_url, why[0]))
+ endpoint.server_url, why))
return None
else:
return assoc
@@ -1395,7 +1395,7 @@ def _extractAssociation(self, assoc_response, assoc_session):
try:
expires_in = int(expires_in_str)
except ValueError, why:
- raise ProtocolError('Invalid expires_in field: %s' % (why[0],))
+ raise ProtocolError('Invalid expires_in field: %s' % (why,))
# OpenID 1 has funny association session behaviour.
if assoc_response.isOpenID1():
@@ -1434,7 +1434,7 @@ def _extractAssociation(self, assoc_response, assoc_session):
secret = assoc_session.extractSecret(assoc_response)
except ValueError, why:
fmt = 'Malformed response for %s session: %s'
- raise ProtocolError(fmt % (assoc_session.session_type, why[0]))
+ raise ProtocolError(fmt % (assoc_session.session_type, why))
return Association.fromExpiresIn(
expires_in, assoc_handle, secret, assoc_type)
@@ -1669,9 +1669,9 @@ def htmlMarkup(self, realm, return_to=None, immediate=False,
@returns: str
"""
- return oidutil.autoSubmitHTML(self.formMarkup(realm,
+ return oidutil.autoSubmitHTML(self.formMarkup(realm,
return_to,
- immediate,
+ immediate,
form_tag_attrs))
def shouldSendRedirect(self):
View
4 openid/consumer/discover.py
@@ -79,7 +79,7 @@ def supportsType(self, type_uri):
I consider C{/server} endpoints to implicitly support C{/signon}.
"""
return (
- (type_uri in self.type_uris) or
+ (type_uri in self.type_uris) or
(type_uri == OPENID_2_0_TYPE and self.isOPIdentifier())
)
@@ -306,7 +306,7 @@ def normalizeURL(url):
try:
normalized = urinorm.urinorm(url)
except ValueError, why:
- raise DiscoveryFailure('Normalizing identifier: %s' % (why[0],), None)
+ raise DiscoveryFailure('Normalizing identifier: %s' % (why,), None)
else:
return urlparse.urldefrag(normalized)[0]
View
7 openid/cryptutil.py
@@ -102,10 +102,15 @@ def reversed(seq):
def longToBinary(l):
if l == 0:
return '\x00'
+ encoded = pickle.encode_long(l)
+ if not isinstance(encoded, unicode):
+ encoded = unicode(encoded, encoding="utf-8")
- return ''.join(reversed(pickle.encode_long(l)))
+ return ''.join(reversed(encoded))
def binaryToLong(s):
+ if not isinstance(s, unicode):
+ s = unicode(s, encoding="utf-8")
return pickle.decode_long(''.join(reversed(s)))
else:
# We have pycrypto
View
2  openid/extensions/sreg.py
@@ -159,7 +159,7 @@ def getSRegNS(message):
except KeyError, why:
# An alias for the string 'sreg' already exists, but it's
# defined for something other than simple registration
- raise SRegNamespaceError(why[0])
+ raise SRegNamespaceError(why)
# we know that sreg_ns_uri defined, because it's defined in the
# else clause of the loop as well, so disable the warning
View
11 openid/fetchers.py
@@ -8,6 +8,15 @@
'HTTPError']
import urllib2
+try:
+ urllib_version = urllib2.__version__
+except:
+ # Python 3 -- the urllib2 import is rewritten by 2to3 into
+ # `import urllib.request, urllib.error, urllib.parse', but
+ # 2to3 doesn't know which of them the __version__ should be
+ # read from.
+ urllib_version = urllib.request.__version__
+
import time
import cStringIO
import sys
@@ -200,7 +209,7 @@ def fetch(self, url, body=None, headers=None):
headers.setdefault(
'User-Agent',
- "%s Python-urllib/%s" % (USER_AGENT, urllib2.__version__,))
+ "%s Python-urllib/%s" % (USER_AGENT, urllib_version,))
req = urllib2.Request(url, data=body, headers=headers)
try:
View
14 openid/oidutil.py
@@ -9,10 +9,16 @@
import binascii
import sys
-import urlparse
import logging
-from urllib import urlencode
+try:
+ import urlparse
+ from urllib import urlencode
+except ImportError:
+ # Python 3.x
+ import urllib.parse as urlparse
+ from urllib.parse import urlencode
+
elementtree_modules = [
'lxml.etree',
@@ -166,7 +172,7 @@ def fromBase64(s):
return binascii.a2b_base64(s)
except binascii.Error, why:
# Convert to a common exception type
- raise ValueError(why[0])
+ raise ValueError(str(why))
class Symbol(object):
"""This class implements an object that compares equal to others
@@ -185,6 +191,6 @@ def __ne__(self, other):
def __hash__(self):
return hash((self.__class__, self.name))
-
+
def __repr__(self):
return '<Symbol %s>' % (self.name,)
View
8 openid/server/server.py
@@ -441,7 +441,7 @@ def fromMessage(klass, message, op_endpoint=UNUSED):
session = session_class.fromMessage(message)
except ValueError, why:
raise ProtocolError(message, 'Error parsing %s session: %s' %
- (session_class.session_type, why[0]))
+ (session_class.session_type,))
assoc_type = message.getArg(OPENID_NS, 'assoc_type', 'HMAC-SHA1')
if assoc_type not in session.allowed_assoc_types:
@@ -1505,13 +1505,13 @@ class Server(object):
associations I can make and how.
@type negotiator: L{openid.association.SessionNegotiator}
"""
-
+
def __init__(
self,
store,
op_endpoint=None,
- signatoryClass=Signatory,
- encoderClass=SigningEncoder,
+ signatoryClass=Signatory,
+ encoderClass=SigningEncoder,
decoderClass=Decoder):
"""A new L{Server}.
View
2  openid/store/sqlstore.py
@@ -350,7 +350,7 @@ def useNonce(self, *args, **kwargs):
try:
return super(SQLiteStore, self).useNonce(*args, **kwargs)
except self.exceptions.OperationalError, why:
- if re.match('^columns .* are not unique$', why[0]):
+ if re.match('^columns .* are not unique$', str(why)):
return False
else:
raise
View
189 openid/test/__init__.py
@@ -0,0 +1,189 @@
+import os.path
+import sys
+import warnings
+import unittest
+
+test_modules = [
+ 'cryptutil',
+ 'oidutil',
+ 'dh',
+ ]
+
+
+def fixpath():
+ try:
+ d = os.path.dirname(__file__)
+ except NameError:
+ d = os.path.dirname(sys.argv[0])
+ parent = os.path.normpath(os.path.join(d, '..'))
+ if parent not in sys.path:
+ print ("putting %s in sys.path" % (parent,))
+ sys.path.insert(0, parent)
+
+
+def otherTests():
+ suite = unittest.TestSuite()
+ for module_name in test_modules:
+ module_name = 'openid.test.' + module_name
+ try:
+ test_mod = __import__(module_name, {}, {}, [None])
+ except ImportError:
+ print ('Failed to import test %r' % (module_name,))
+ else:
+ suite.addTest(unittest.FunctionTestCase(test_mod.test))
+
+ return suite
+
+
+def pyUnitTests():
+ pyunit_module_names = [
+ 'server',
+ 'consumer',
+ 'message',
+ 'symbol',
+ 'etxrd',
+ 'xri',
+ 'xrires',
+ 'association_response',
+ 'auth_request',
+ 'negotiation',
+ 'verifydisco',
+ 'sreg',
+ 'ax',
+ 'pape',
+ 'pape_draft2',
+ 'pape_draft5',
+ 'rpverify',
+ 'extension',
+ ]
+
+ pyunit_modules = [
+ __import__('openid.test.test_%s' % (name,), {}, {}, ['unused'])
+ for name in pyunit_module_names
+ ]
+
+ try:
+ from openid.test import test_examples
+ except ImportError:
+ e = sys.exc_info[1]
+ if 'twill' in str(e):
+ warnings.warn("Could not import twill; skipping test_examples.")
+ else:
+ raise
+ else:
+ pyunit_modules.append(test_examples)
+
+ # Some modules have data-driven tests, and they use custom methods
+ # to build the test suite:
+ custom_module_names = [
+ 'kvform',
+ 'linkparse',
+ 'oidutil',
+ 'storetest',
+ 'test_accept',
+ 'test_association',
+ 'test_discover',
+ 'test_fetchers',
+ 'test_htmldiscover',
+ 'test_nonce',
+ 'test_openidyadis',
+ 'test_parsehtml',
+ 'test_urinorm',
+ 'test_yadis_discover',
+ 'trustroot',
+ ]
+
+ loader = unittest.TestLoader()
+ s = unittest.TestSuite()
+
+ for m in pyunit_modules:
+ s.addTest(loader.loadTestsFromModule(m))
+
+ for name in custom_module_names:
+ m = __import__('openid.test.%s' % (name,), {}, {}, ['unused'])
+ try:
+ s.addTest(m.pyUnitTests())
+ except AttributeError:
+ # because the AttributeError doesn't actually say which
+ # object it was.
+ print ("Error loading tests from %s:" % (name,))
+ raise
+
+ return s
+
+
+def splitDir(d, count):
+ # in python2.4 and above, it's easier to spell this as
+ # d.rsplit(os.sep, count)
+ for i in xrange(count):
+ d = os.path.dirname(d)
+ return d
+
+
+def _import_djopenid():
+ """Import djopenid from examples/
+
+ It's not in sys.path, and I don't really want to put it in sys.path.
+ """
+ import types
+ thisfile = os.path.abspath(sys.modules[__name__].__file__)
+ topDir = splitDir(thisfile, 2)
+ djdir = os.path.join(topDir, 'examples', 'djopenid')
+
+ djinit = os.path.join(djdir, '__init__.py')
+
+ djopenid = types.ModuleType('djopenid')
+ if sys.version_info[0] >= 3:
+ with open(djinit, 'r') as f:
+ exec(compile(f.read(), "__init__.py"), "exec", djinit.__dict__)
+ else:
+ execfile(djinit, djopenid.__dict__)
+ djopenid.__file__ = djinit
+
+ # __path__ is the magic that makes child modules of the djopenid package
+ # importable. New feature in python 2.3, see PEP 302.
+ djopenid.__path__ = [djdir]
+ sys.modules['djopenid'] = djopenid
+
+
+def django_tests():
+ """Runs tests from examples/djopenid.
+
+ @returns: number of failed tests.
+ """
+ import os
+ # Django uses this to find out where its settings are.
+ os.environ['DJANGO_SETTINGS_MODULE'] = 'djopenid.settings'
+
+ _import_djopenid()
+
+ try:
+ import django.test.simple
+ except ImportError:
+ warnings.warn("django.test.simple not found; "
+ "django examples not tested.")
+ return 0
+
+ import djopenid.server.models, djopenid.consumer.models
+ print ("Testing Django examples:")
+
+ # These tests do get put in to a pyunit test suite, so we could run them
+ # with the other pyunit tests, but django also establishes a test database
+ # for them, so we let it do that thing instead.
+ return django.test.simple.run_tests([djopenid.server.models,
+ djopenid.consumer.models])
+
+try:
+ bool
+except NameError:
+ def bool(x):
+ return not not x
+
+
+def test_suite():
+ fixpath()
+ all_suite = unittest.TestSuite()
+ all_suite.addTests(otherTests())
+ all_suite.addTests(pyUnitTests())
+ all_suite.addTests(unittest.FunctionTestCase(django_tests))
+ return all_suite
View
2  openid/test/discoverdata.py
@@ -87,7 +87,7 @@ def generateSample(test_name, base_url,
template = getData(filename, test_name)
except IOError, why:
import errno
- if why[0] == errno.ENOENT:
+ if int(why) == errno.ENOENT:
raise KeyError(filename)
else:
raise
View
2  openid/test/storetest.py
@@ -269,7 +269,7 @@ def test_mysql():
try:
conn = MySQLdb.connect(user=db_user, passwd=db_passwd, host = db_host)
except MySQLdb.OperationalError, why:
- if why[0] == 2005:
+ if int(why) == 2005:
print ('Skipping MySQL store test (cannot connect '
'to test server on host %r)' % (db_host,))
return
View
14 openid/test/test_consumer.py
@@ -788,7 +788,7 @@ def test(self):
try:
self.consumer._idResCheckForFields(message)
except ProtocolError, why:
- self.failUnless(why[0].startswith('Missing required'))
+ self.failUnless(str(why).startswith('Missing required'))
else:
self.fail('Expected an error, but none occurred')
return test
@@ -799,7 +799,7 @@ def test(self):
try:
self.consumer._idResCheckForFields(message)
except ProtocolError, why:
- self.failUnless(why[0].endswith('not signed'))
+ self.failUnless(str(why).endswith('not signed'))
else:
self.fail('Expected an error, but none occurred')
return test
@@ -1477,8 +1477,8 @@ def test():
try:
self.consumer.begin('unused in this test')
except DiscoveryFailure, why:
- self.failUnless(why[0].startswith('Error fetching'))
- self.failIf(why[0].find('Unit test') == -1)
+ self.failUnless(str(why).startswith('Error fetching'))
+ self.failIf(str(why).find('Unit test') == -1)
else:
self.fail('Expected DiscoveryFailure')
@@ -1493,8 +1493,8 @@ def test():
try:
self.consumer.begin(url)
except DiscoveryFailure, why:
- self.failUnless(why[0].startswith('No usable OpenID'))
- self.failIf(why[0].find(url) == -1)
+ self.failUnless(str(why).startswith('No usable OpenID'))
+ self.failIf(str(why).find(url) == -1)
else:
self.fail('Expected DiscoveryFailure')
@@ -1771,7 +1771,7 @@ def discoverAndVerify(claimed_id, to_match_endpoints):
self.failUnless(str(e), text)
else:
self.fail("expected ProtocolError, %r returned." % (r,))
-
+
def test_foreignDelegate(self):
text = "verify failed"
View
2  openid/test/test_fetchers.py
@@ -90,7 +90,7 @@ def run_fetcher_tests(server):
try:
exc_fetchers.append(klass())
except RuntimeError, why:
- if why[0].startswith('Cannot find %s library' % (library_name,)):
+ if str(why).startswith('Cannot find %s library' % (library_name,)):
try:
__import__(library_name)
except ImportError:
View
2  openid/test/test_parsehtml.py
@@ -18,7 +18,7 @@ def runTest(self):
try:
p.feed(self.case)
except ParseDone, why:
- found = why[0]
+ found = str(why)
# make sure we protect outselves against accidental bogus
# test cases
View
4 openid/urinorm.py
@@ -138,7 +138,7 @@ def remove_dot_segments(path):
def urinorm(uri):
if isinstance(uri, unicode):
- uri = _escapeme_re.sub(_pct_escape_unicode, uri).encode('ascii')
+ uri = _escapeme_re.sub(_pct_escape_unicode, uri).encode('ascii').decode()
illegal_mo = uri_illegal_char_re.search(uri)
if illegal_mo:
@@ -171,7 +171,7 @@ def urinorm(uri):
if '%' in host:
host = host.lower()
host = pct_encoded_re.sub(_pct_encoded_replace, host)
- host = unicode(host, 'utf-8').encode('idna')
+ host = unicode(host, 'utf-8').encode('idna').decode()
else:
host = host.lower()
View
5 openid/yadis/parsehtml.py
@@ -185,7 +185,10 @@ def findHTMLMeta(stream):
chunks.append(stream.read())
break
except ParseDone, why:
- uri = why[0]
+ if hasattr(why, 'args'):
+ uri = why.args[0]
+ else:
+ uri = why[0]
if uri is None:
# Parse finished, but we may need the rest of the file
chunks.append(stream.read())
View
11 setup.py
@@ -6,6 +6,15 @@
except ImportError:
from distutils.core import setup
+# Python 3 support
+extra = {}
+if sys.version_info >= (3, 0):
+ extra.update(
+ use_2to3=True,
+ use_2to3_fixers=['custom_fixers']
+ )
+
+
if 'sdist' in sys.argv:
os.system('./admin/makedoc')
@@ -41,9 +50,11 @@
"License :: OSI Approved :: Apache Software License",
"Operating System :: POSIX",
"Programming Language :: Python",
+ "Programming Language :: Python :: 3",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: System :: Systems Administration :: Authentication/Directory",
],
+ **extra
)
View
2  tox.ini
@@ -4,7 +4,7 @@
# and then run "tox" from this directory.
[tox]
-envlist = py25, py26, py27, pypy
+envlist = py25, py26, py27, pypy, py32
[testenv]
commands = ./run_tests.sh
Something went wrong with that request. Please try again.