Commits

Gael Pasgrimaud committed 1b96f74

py3 compat. only 4 errors remaining

Comments (0)

Files changed (18)

 parts/
 develop-eggs/
 docs/_build/
-buildout.cfg
-bootstrap.py
 .installed.cfg
 *.jar
 *.swp

bootstrap-py3k.py

+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+"""
+
+import os, shutil, sys, tempfile, textwrap
+try:
+    import urllib.request as urllib2
+except ImportError:
+    import urllib2
+import subprocess
+from optparse import OptionParser
+
+if sys.platform == 'win32':
+    def quote(c):
+        if ' ' in c:
+            return '"%s"' % c # work around spawn lamosity on windows
+        else:
+            return c
+else:
+    quote = str
+
+# See zc.buildout.easy_install._has_broken_dash_S for motivation and comments.
+stdout, stderr = subprocess.Popen(
+    [sys.executable, '-S', '-c',
+     'try:\n'
+     '    import pickle\n'
+     'except ImportError:\n'
+     '    print(1)\n'
+     'else:\n'
+     '    print(0)\n'],
+    stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
+has_broken_dash_S = bool(int(stdout.strip()))
+
+# In order to be more robust in the face of system Pythons, we want to
+# run without site-packages loaded.  This is somewhat tricky, in
+# particular because Python 2.6's distutils imports site, so starting
+# with the -S flag is not sufficient.  However, we'll start with that:
+if not has_broken_dash_S and 'site' in sys.modules:
+    # We will restart with python -S.
+    args = sys.argv[:]
+    args[0:0] = [sys.executable, '-S']
+    args = list(map(quote, args))
+    os.execv(sys.executable, args)
+
+# Now we are running with -S.  We'll get the clean sys.path, import site
+# because distutils will do it later, and then reset the path and clean
+# out any namespace packages from site-packages that might have been
+# loaded by .pth files.
+clean_path = sys.path[:]
+import site
+sys.path[:] = clean_path
+for k, v in list(sys.modules.items()):
+    if k in ('setuptools', 'pkg_resources') or (
+        hasattr(v, '__path__') and
+        len(v.__path__)==1 and
+        not os.path.exists(os.path.join(v.__path__[0],'__init__.py'))):
+        # This is a namespace package.  Remove it.
+        sys.modules.pop(k)
+
+is_jython = sys.platform.startswith('java')
+
+setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py'
+distribute_source = 'http://python-distribute.org/distribute_setup.py'
+
+# parsing arguments
+def normalize_to_url(option, opt_str, value, parser):
+    if value:
+        if '://' not in value: # It doesn't smell like a URL.
+            value = 'file://%s' % (
+                urllib2.pathname2url(
+                    os.path.abspath(os.path.expanduser(value))),)
+        if opt_str == '--download-base' and not value.endswith('/'):
+            # Download base needs a trailing slash to make the world happy.
+            value += '/'
+    else:
+        value = None
+    name = opt_str[2:].replace('-', '_')
+    setattr(parser.values, name, value)
+
+usage = '''\
+[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
+
+Bootstraps a buildout-based project.
+
+Simply run this script in a directory containing a buildout.cfg, using the
+Python that you want bin/buildout to use.
+
+Note that by using --setup-source and --download-base to point to
+local resources, you can keep this script from going over the network.
+'''
+
+parser = OptionParser(usage=usage)
+parser.add_option("-v", "--version", dest="version",
+                          help="use a specific zc.buildout version")
+parser.add_option("--setup-version", dest="setup_version",
+                  help="The version of setuptools or distribute to use.")
+parser.add_option("-d", "--distribute",
+                   action="store_true", dest="use_distribute",
+                   default= sys.version_info[0] >= 3,
+                   help="Use Distribute rather than Setuptools.")
+parser.add_option("--setup-source", action="callback", dest="setup_source",
+                  callback=normalize_to_url, nargs=1, type="string",
+                  help=("Specify a URL or file location for the setup file. "
+                        "If you use Setuptools, this will default to " +
+                        setuptools_source + "; if you use Distribute, this "
+                        "will default to " + distribute_source +"."))
+parser.add_option("--download-base", action="callback", dest="download_base",
+                  callback=normalize_to_url, nargs=1, type="string",
+                  help=("Specify a URL or directory for downloading "
+                        "zc.buildout and either Setuptools or Distribute. "
+                        "Defaults to PyPI."))
+parser.add_option("--eggs",
+                  help=("Specify a directory for storing eggs.  Defaults to "
+                        "a temporary directory that is deleted when the "
+                        "bootstrap script completes."))
+parser.add_option("-t", "--accept-buildout-test-releases",
+                  dest='accept_buildout_test_releases',
+                  action="store_true",
+                  default=sys.version_info[0] > 2,
+                  help=("Normally, if you do not specify a --version, the "
+                        "bootstrap script and buildout gets the newest "
+                        "*final* versions of zc.buildout and its recipes and "
+                        "extensions for you.  If you use this flag, "
+                        "bootstrap and buildout will get the newest releases "
+                        "even if they are alphas or betas."))
+parser.add_option("-c", None, action="store", dest="config_file",
+                   help=("Specify the path to the buildout configuration "
+                         "file to be used."))
+
+options, args = parser.parse_args()
+
+# if -c was provided, we push it back into args for buildout's main function
+if options.config_file is not None:
+    args += ['-c', options.config_file]
+
+if options.eggs:
+    eggs_dir = os.path.abspath(os.path.expanduser(options.eggs))
+else:
+    eggs_dir = tempfile.mkdtemp()
+
+if options.setup_source is None:
+    if options.use_distribute:
+        options.setup_source = distribute_source
+    else:
+        options.setup_source = setuptools_source
+
+if options.accept_buildout_test_releases:
+    args.append('buildout:accept-buildout-test-releases=true')
+args.append('bootstrap')
+
+try:
+    import pkg_resources
+    import setuptools # A flag.  Sometimes pkg_resources is installed alone.
+    if not hasattr(pkg_resources, '_distribute'):
+        raise ImportError
+except ImportError:
+    ez_code = urllib2.urlopen(
+        options.setup_source).read().replace('\r\n'.encode(), '\n'.encode())
+    ez = {}
+    exec(ez_code, ez)
+    setup_args = dict(to_dir=eggs_dir, download_delay=0)
+    if options.download_base:
+        setup_args['download_base'] = options.download_base
+    if options.setup_version:
+        setup_args['version'] = options.setup_version
+    if options.use_distribute:
+        setup_args['no_fake'] = True
+    ez['use_setuptools'](**setup_args)
+    if 'pkg_resources' in sys.modules:
+        if sys.version_info[0] >= 3:
+            import imp
+            reload_ = imp.reload
+        else:
+            reload_ = reload
+
+        reload_(sys.modules['pkg_resources'])
+    import pkg_resources
+    # This does not (always?) update the default working set.  We will
+    # do it.
+    for path in sys.path:
+        if path not in pkg_resources.working_set.entries:
+            pkg_resources.working_set.add_entry(path)
+
+cmd = [quote(sys.executable),
+       '-c',
+       quote('from setuptools.command.easy_install import main; main()'),
+       '-mqNxd',
+       quote(eggs_dir)]
+
+if not has_broken_dash_S:
+    cmd.insert(1, '-S')
+
+find_links = options.download_base
+if not find_links:
+    find_links = os.environ.get('bootstrap-testing-find-links')
+if find_links:
+    cmd.extend(['-f', quote(find_links)])
+
+if options.use_distribute:
+    setup_requirement = 'distribute'
+else:
+    setup_requirement = 'setuptools'
+ws = pkg_resources.working_set
+setup_requirement_path = ws.find(
+    pkg_resources.Requirement.parse(setup_requirement)).location
+env = dict(
+    os.environ,
+    PYTHONPATH=setup_requirement_path)
+
+requirement = 'zc.buildout'
+version = options.version
+if version is None and not options.accept_buildout_test_releases:
+    # Figure out the most recent final version of zc.buildout.
+    import setuptools.package_index
+    _final_parts = '*final-', '*final'
+    def _final_version(parsed_version):
+        for part in parsed_version:
+            if (part[:1] == '*') and (part not in _final_parts):
+                return False
+        return True
+    index = setuptools.package_index.PackageIndex(
+        search_path=[setup_requirement_path])
+    if find_links:
+        index.add_find_links((find_links,))
+    req = pkg_resources.Requirement.parse(requirement)
+    if index.obtain(req) is not None:
+        best = []
+        bestv = None
+        for dist in index[req.project_name]:
+            distv = dist.parsed_version
+            if _final_version(distv):
+                if bestv is None or distv > bestv:
+                    best = [dist]
+                    bestv = distv
+                elif distv == bestv:
+                    best.append(dist)
+        if best:
+            best.sort()
+            version = best[-1].version
+if version:
+    requirement = '=='.join((requirement, version))
+cmd.append(requirement)
+
+if is_jython:
+    import subprocess
+    exitcode = subprocess.Popen(cmd, env=env).wait()
+else: # Windows prefers this, apparently; otherwise we would prefer subprocess
+    exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env]))
+if exitcode != 0:
+    sys.stdout.flush()
+    sys.stderr.flush()
+    print("An error occurred when trying to install zc.buildout. "
+          "Look above this message for any errors that "
+          "were output by easy_install.")
+    sys.exit(exitcode)
+
+ws.add_entry(eggs_dir)
+ws.require(requirement)
+import zc.buildout.buildout
+zc.buildout.buildout.main(args)
+if not options.eggs: # clean up temporary egg directory
+    shutil.rmtree(eggs_dir)
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+"""
+
+import os, shutil, sys, tempfile, textwrap, urllib, urllib2, subprocess
+from optparse import OptionParser
+
+if sys.platform == 'win32':
+    def quote(c):
+        if ' ' in c:
+            return '"%s"' % c # work around spawn lamosity on windows
+        else:
+            return c
+else:
+    quote = str
+
+# See zc.buildout.easy_install._has_broken_dash_S for motivation and comments.
+stdout, stderr = subprocess.Popen(
+    [sys.executable, '-Sc',
+     'try:\n'
+     '    import ConfigParser\n'
+     'except ImportError:\n'
+     '    print 1\n'
+     'else:\n'
+     '    print 0\n'],
+    stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
+has_broken_dash_S = bool(int(stdout.strip()))
+
+# In order to be more robust in the face of system Pythons, we want to
+# run without site-packages loaded.  This is somewhat tricky, in
+# particular because Python 2.6's distutils imports site, so starting
+# with the -S flag is not sufficient.  However, we'll start with that:
+if not has_broken_dash_S and 'site' in sys.modules:
+    # We will restart with python -S.
+    args = sys.argv[:]
+    args[0:0] = [sys.executable, '-S']
+    args = map(quote, args)
+    os.execv(sys.executable, args)
+# Now we are running with -S.  We'll get the clean sys.path, import site
+# because distutils will do it later, and then reset the path and clean
+# out any namespace packages from site-packages that might have been
+# loaded by .pth files.
+clean_path = sys.path[:]
+import site
+sys.path[:] = clean_path
+for k, v in sys.modules.items():
+    if k in ('setuptools', 'pkg_resources') or (
+        hasattr(v, '__path__') and
+        len(v.__path__)==1 and
+        not os.path.exists(os.path.join(v.__path__[0],'__init__.py'))):
+        # This is a namespace package.  Remove it.
+        sys.modules.pop(k)
+
+is_jython = sys.platform.startswith('java')
+
+setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py'
+distribute_source = 'http://python-distribute.org/distribute_setup.py'
+
+# parsing arguments
+def normalize_to_url(option, opt_str, value, parser):
+    if value:
+        if '://' not in value: # It doesn't smell like a URL.
+            value = 'file://%s' % (
+                urllib.pathname2url(
+                    os.path.abspath(os.path.expanduser(value))),)
+        if opt_str == '--download-base' and not value.endswith('/'):
+            # Download base needs a trailing slash to make the world happy.
+            value += '/'
+    else:
+        value = None
+    name = opt_str[2:].replace('-', '_')
+    setattr(parser.values, name, value)
+
+usage = '''\
+[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
+
+Bootstraps a buildout-based project.
+
+Simply run this script in a directory containing a buildout.cfg, using the
+Python that you want bin/buildout to use.
+
+Note that by using --setup-source and --download-base to point to
+local resources, you can keep this script from going over the network.
+'''
+
+parser = OptionParser(usage=usage)
+parser.add_option("-v", "--version", dest="version",
+                          help="use a specific zc.buildout version")
+parser.add_option("-d", "--distribute",
+                   action="store_true", dest="use_distribute", default=False,
+                   help="Use Distribute rather than Setuptools.")
+parser.add_option("--setup-source", action="callback", dest="setup_source",
+                  callback=normalize_to_url, nargs=1, type="string",
+                  help=("Specify a URL or file location for the setup file. "
+                        "If you use Setuptools, this will default to " +
+                        setuptools_source + "; if you use Distribute, this "
+                        "will default to " + distribute_source +"."))
+parser.add_option("--download-base", action="callback", dest="download_base",
+                  callback=normalize_to_url, nargs=1, type="string",
+                  help=("Specify a URL or directory for downloading "
+                        "zc.buildout and either Setuptools or Distribute. "
+                        "Defaults to PyPI."))
+parser.add_option("--eggs",
+                  help=("Specify a directory for storing eggs.  Defaults to "
+                        "a temporary directory that is deleted when the "
+                        "bootstrap script completes."))
+parser.add_option("-t", "--accept-buildout-test-releases",
+                  dest='accept_buildout_test_releases',
+                  action="store_true", default=False,
+                  help=("Normally, if you do not specify a --version, the "
+                        "bootstrap script and buildout gets the newest "
+                        "*final* versions of zc.buildout and its recipes and "
+                        "extensions for you.  If you use this flag, "
+                        "bootstrap and buildout will get the newest releases "
+                        "even if they are alphas or betas."))
+parser.add_option("-c", None, action="store", dest="config_file",
+                   help=("Specify the path to the buildout configuration "
+                         "file to be used."))
+
+options, args = parser.parse_args()
+
+# if -c was provided, we push it back into args for buildout's main function
+if options.config_file is not None:
+    args += ['-c', options.config_file]
+
+if options.eggs:
+    eggs_dir = os.path.abspath(os.path.expanduser(options.eggs))
+else:
+    eggs_dir = tempfile.mkdtemp()
+
+if options.setup_source is None:
+    if options.use_distribute:
+        options.setup_source = distribute_source
+    else:
+        options.setup_source = setuptools_source
+
+if options.accept_buildout_test_releases:
+    args.append('buildout:accept-buildout-test-releases=true')
+args.append('bootstrap')
+
+try:
+    import pkg_resources
+    import setuptools # A flag.  Sometimes pkg_resources is installed alone.
+    if not hasattr(pkg_resources, '_distribute'):
+        raise ImportError
+except ImportError:
+    ez_code = urllib2.urlopen(
+        options.setup_source).read().replace('\r\n', '\n')
+    ez = {}
+    exec ez_code in ez
+    setup_args = dict(to_dir=eggs_dir, download_delay=0)
+    if options.download_base:
+        setup_args['download_base'] = options.download_base
+    if options.use_distribute:
+        setup_args['no_fake'] = True
+    ez['use_setuptools'](**setup_args)
+    if 'pkg_resources' in sys.modules:
+        reload(sys.modules['pkg_resources'])
+    import pkg_resources
+    # This does not (always?) update the default working set.  We will
+    # do it.
+    for path in sys.path:
+        if path not in pkg_resources.working_set.entries:
+            pkg_resources.working_set.add_entry(path)
+
+cmd = [quote(sys.executable),
+       '-c',
+       quote('from setuptools.command.easy_install import main; main()'),
+       '-mqNxd',
+       quote(eggs_dir)]
+
+if not has_broken_dash_S:
+    cmd.insert(1, '-S')
+
+find_links = options.download_base
+if not find_links:
+    find_links = os.environ.get('bootstrap-testing-find-links')
+if find_links:
+    cmd.extend(['-f', quote(find_links)])
+
+if options.use_distribute:
+    setup_requirement = 'distribute'
+else:
+    setup_requirement = 'setuptools'
+ws = pkg_resources.working_set
+setup_requirement_path = ws.find(
+    pkg_resources.Requirement.parse(setup_requirement)).location
+env = dict(
+    os.environ,
+    PYTHONPATH=setup_requirement_path)
+
+requirement = 'zc.buildout'
+version = options.version
+if version is None and not options.accept_buildout_test_releases:
+    # Figure out the most recent final version of zc.buildout.
+    import setuptools.package_index
+    _final_parts = '*final-', '*final'
+    def _final_version(parsed_version):
+        for part in parsed_version:
+            if (part[:1] == '*') and (part not in _final_parts):
+                return False
+        return True
+    index = setuptools.package_index.PackageIndex(
+        search_path=[setup_requirement_path])
+    if find_links:
+        index.add_find_links((find_links,))
+    req = pkg_resources.Requirement.parse(requirement)
+    if index.obtain(req) is not None:
+        best = []
+        bestv = None
+        for dist in index[req.project_name]:
+            distv = dist.parsed_version
+            if _final_version(distv):
+                if bestv is None or distv > bestv:
+                    best = [dist]
+                    bestv = distv
+                elif distv == bestv:
+                    best.append(dist)
+        if best:
+            best.sort()
+            version = best[-1].version
+if version:
+    requirement = '=='.join((requirement, version))
+cmd.append(requirement)
+
+if is_jython:
+    import subprocess
+    exitcode = subprocess.Popen(cmd, env=env).wait()
+else: # Windows prefers this, apparently; otherwise we would prefer subprocess
+    exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env]))
+if exitcode != 0:
+    sys.stdout.flush()
+    sys.stderr.flush()
+    print ("An error occurred when trying to install zc.buildout. "
+           "Look above this message for any errors that "
+           "were output by easy_install.")
+    sys.exit(exitcode)
+
+ws.add_entry(eggs_dir)
+ws.require(requirement)
+import zc.buildout.buildout
+zc.buildout.buildout.main(args)
+if not options.eggs: # clean up temporary egg directory
+    shutil.rmtree(eggs_dir)

buildout-py3k.cfg

+[buildout]
+newest = false
+prefer-final = true
+parts = eggs
+develop = . ../webob
+
+[eggs]
+recipe = zc.recipe.egg
+eggs =
+    webtest
+    webob
+    nose
+    six
+interpreter = python
+[buildout]
+newest = false
+extensions = gp.vcsdevelop
+parts = eggs
+requirements=requirements.txt
+develop = .
+
+[eggs]
+recipe = z3c.recipe.scripts
+eggs =
+    ${buildout:requirements-eggs}
+    webtest
+    nose
+interpreter = python
       maintainer_email='gael@gawel.org',
       url='http://webtest.pythonpaste.org/',
       license='MIT',
-      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+      packages=find_packages(exclude=[
+          'ez_setup', 'examples', 'tests',
+          'bootstrap', 'bootstrap-py3k',
+      ]),
       include_package_data=True,
       zip_safe=False,
       install_requires=[
 # -*- coding: utf-8 -*-
+import sys
+
 try:
     # py < 2.7
     import unnitest2 as unittest
     unicode()
 except NameError:
     u = str
+    b = bytes
 else:
+    def b(value):
+        return str(value)
     def u(value):
         return unicode(value, 'utf-8')
 
-def raises(exc, func, *args, **kw):
-    try:
-        func(*args, **kw)
-    except exc:
-        pass
-    else:
-        raise AssertionError(
-            "Expected exception %s from %s"
-            % (exc, func))
 
+if sys.version_info[:1] < (2, 6):
+    def assertIn(self, x, y, c=None):
+        assert x in y
+
+    unittest.TestCase.assertIn = assertIn

tests/test_click.py

 import webtest
 from webtest.app import _parse_attrs
 from webob import Request
+from webtest.compat import to_bytes
+from webtest.compat import PY3
 from tests.compat import unittest
 from tests.compat import u
 
 
 def links_app(environ, start_response):
     req = Request(environ)
-    status = "200 OK"
+    status = to_bytes("200 OK")
     responses = {
        '/': """
             <html>
     }
 
     utf8_paths = ['/utf8/']
-    body = responses[req.path_info]
+    body = responses[u(req.path_info)]
     headers = [
         ('Content-Type', 'text/html'),
         ('Content-Length', str(len(body)))
         headers[0] = ('Content-Type', 'text/html; charset=utf-8')
 
     start_response(status, headers)
-    return [body]
+    return [to_bytes(body)]
 
 class TestClick(unittest.TestCase):
 
         app = webtest.TestApp(links_app, use_unicode=False)
         resp = app.get('/utf8/')
         self.assertEqual(resp.charset, 'utf-8')
-        self.assertIn(u("Тестовая страница").encode('utf8'), resp)
-        self.assertIn(u("Тестовая страница"), resp)
-        self.assertIn('This is foo.', resp.click(u('Менделеев').encode('utf8')))
+        if not PY3:
+            # No need to deal with that in Py3
+            self.assertIn(u("Тестовая страница").encode('utf8'), resp)
+            self.assertIn(u("Тестовая страница"), resp)
+            target = u('Менделеев').encode('utf8')
+            self.assertIn('This is foo.', resp.click(target))
 
-        # should skip the img tag
-        anchor_re = u(".*title='Поэт'.*").encode('utf8')
-        self.assertIn('This is baz.', resp.click(anchor=anchor_re))
+            # should skip the img tag
+            anchor = u(".*title='Поэт'.*")
+            anchor_re = anchor.encode('utf8')
+            self.assertIn('This is baz.', resp.click(anchor=anchor_re))
 
 
     def test_click_u(self):

tests/test_cookie.py

 import webtest
 from webob import Request
 from tests.compat import unittest
+from webtest.compat import to_bytes
 
 
 def cookie_app(environ, start_response):
     req = Request(environ)
-    status = '200 OK'
+    status = to_bytes("200 OK")
     body = '<html><body><a href="/go/">go</a></body></html>'
     headers = [
         ('Content-Type', 'text/html'),
             ('Set-Cookie', 'foo="bar;baz"'),
         ])
     start_response(status, headers)
-    return [body]
+    return [to_bytes(body)]
 
 def cookie_app2(environ, start_response):
-    status = '200 OK'
+    status = to_bytes("200 OK")
     body = ''
     headers = [
         ('Content-Type', 'text/html'),
         ('Set-Cookie', 'foo="bar;baz"'),
     ]
     start_response(status, headers)
-    return [body]
+    return [to_bytes(body)]
 
 class TestCookies(unittest.TestCase):
 

tests/test_file_upload.py

 import os.path
 import struct
 from tests.compat import unittest
-
+from webtest.compat import to_bytes
+from webtest.compat import to_string
+from webtest.compat import join_bytes
+from webtest.compat import binary_type
+from webtest.compat import PY3
 from webob import Request
 import webtest
 
 def single_upload_file_app(environ, start_response):
     req = Request(environ)
-    status = "200 OK"
+    status = to_bytes("200 OK")
     if req.method == "GET":
-        body =\
+        body = to_bytes(
 """
 <html>
     <head><title>form page</title></head>
         </form>
     </body>
 </html>
-"""
+""")
     else:
         uploaded_files = req.POST.getall("file-field")
-        body_head =\
+        body_head = to_bytes(
 """
 <html>
     <head><title>display page</title></head>
     <body>
-"""
+""")
 
         file_parts = []
         for uploaded_file in uploaded_files:
             file_parts.append(\
 """        <p>You selected '%(filename)s'</p>
         <p>with contents: '%(value)s'</p>
-""" % dict(filename=str(uploaded_file.filename),
-           value=str(uploaded_file.value)))
+""" % dict(filename=to_string(uploaded_file.filename),
+           value=to_string(uploaded_file.value)))
 
-        body_foot =\
+        body_foot = to_bytes(
 """    </body>
 </html>
-"""
-        body = body_head + "".join(file_parts) + body_foot
+""")
+        body = body_head + join_bytes("", file_parts) + body_foot
     headers = [
         ('Content-Type', 'text/html; charset=utf-8'),
         ('Content-Length', str(len(body)))]
     start_response(status, headers)
-    assert(isinstance(body, str))
+    assert(isinstance(body, binary_type))
     return [body]
 
 
 def upload_binary_app(environ, start_response):
     req = Request(environ)
-    status = "200 OK"
+    status = to_bytes("200 OK")
     if req.method == "GET":
-        body ="""
+        body = to_bytes("""
 <html>
     <head><title>form page</title></head>
     <body>
         </form>
     </body>
 </html>
-"""
+""")
     else:
         uploaded_files = req.POST.getall("binary-file-field")
         data = [str(n) for n in struct.unpack('255h', uploaded_files[0].value)]
-        body = """
+        body = to_bytes("""
 <html>
     <head><title>display page</title></head>
     <body>
         %s
     </body>
 </html>
-""" % ','.join(data)
+""" % join_bytes(',', data))
     headers = [
         ('Content-Type', 'text/html; charset=utf-8'),
         ('Content-Length', str(len(body)))]
 
 def multiple_upload_file_app(environ, start_response):
     req = Request(environ)
-    status = "200 OK"
+    status = to_bytes("200 OK")
     if req.method == "GET":
-        body =\
+        body = to_bytes(
 """
 <html>
     <head><title>form page</title></head>
         </form>
     </body>
 </html>
-"""
+""")
     else:
         uploaded_file_1 = req.POST.get("file-field-1")
         uploaded_file_2 = req.POST.get("file-field-2")
         uploaded_files = [uploaded_file_1, uploaded_file_2]
 
-        body_head =\
+        body_head = to_bytes(
 """
 <html>
     <head><title>display page</title></head>
     <body>
-"""
+""")
 
         file_parts = []
         for uploaded_file in uploaded_files:
-            print (str(uploaded_file.filename), type(uploaded_file.value))
+            print (to_bytes(uploaded_file.filename), type(uploaded_file.value))
             file_parts.append(
 """
         <p>You selected '%(filename)s'</p>
         <p>with contents: '%(value)s'</p>
-""" % dict(filename=str(uploaded_file.filename),
-           value=str(uploaded_file.value)))
+""" % dict(filename=to_string(uploaded_file.filename),
+           value=to_string(uploaded_file.value)))
 
-        body_foot =\
+        body_foot = to_bytes(
 """    </body>
 </html>
-"""
-        body = body_head + "".join(file_parts) + body_foot
+""")
+        body = body_head + join_bytes("", file_parts) + body_foot
     headers = [
         ('Content-Type', 'text/html; charset=utf-8'),
         ('Content-Length', str(len(body)))]
     def test_file_upload_with_filename_only(self):
         uploaded_file_name = \
             os.path.join(os.path.dirname(__file__), "__init__.py")
-        uploaded_file_contents = file(uploaded_file_name).read()
+        uploaded_file_contents = open(uploaded_file_name).read()
+        if PY3:
+            uploaded_file_contents = to_bytes(uploaded_file_contents)
 
         app = webtest.TestApp(single_upload_file_app)
         res = app.get('/')
         single_form.set("file-field", (uploaded_file_name,))
         display = single_form.submit("button")
         self.assertIn("<p>You selected '%s'</p>" % uploaded_file_name, display, display)
-        self.assertIn("<p>with contents: '%s'</p>" % uploaded_file_contents, display, \
+        self.assertIn("<p>with contents: '%s'</p>" % to_string(uploaded_file_contents), display, \
             display)
 
 
     def test_file_upload_with_filename_and_contents(self):
         uploaded_file_name = \
             os.path.join(os.path.dirname(__file__), "__init__.py")
-        uploaded_file_contents = file(uploaded_file_name).read()
+        uploaded_file_contents = open(uploaded_file_name).read()
+        if PY3:
+            uploaded_file_contents = to_bytes(uploaded_file_contents)
 
         app = webtest.TestApp(single_upload_file_app)
         res = app.get('/')
                         (uploaded_file_name, uploaded_file_contents))
         display = single_form.submit("button")
         self.assertIn("<p>You selected '%s'</p>" % uploaded_file_name, display, display)
-        self.assertIn("<p>with contents: '%s'</p>" % uploaded_file_contents, display, \
+        self.assertIn("<p>with contents: '%s'</p>" % to_string(uploaded_file_contents), display, \
             display)
 
 
     def test_multiple_file_uploads_with_filename_and_contents(self):
         uploaded_file1_name = \
             os.path.join(os.path.dirname(__file__), "__init__.py")
-        uploaded_file1_contents = file(uploaded_file1_name).read()
-        uploaded_file2_name = \
-            os.path.join(os.path.dirname(__file__), "test_input.py")
-        uploaded_file2_contents = file(uploaded_file2_name).read()
+        uploaded_file1_contents = open(uploaded_file1_name).read()
+        if PY3:
+            uploaded_file1_contents = to_bytes(uploaded_file1_contents)
+        uploaded_file2_name = __file__
+        uploaded_file2_contents = open(uploaded_file2_name).read()
+        if PY3:
+            uploaded_file2_contents = to_bytes(uploaded_file2_contents)
 
         app = webtest.TestApp(multiple_upload_file_app)
         res = app.get('/')
         single_form.set("file-field-2", (uploaded_file2_name, uploaded_file2_contents))
         display = single_form.submit("button")
         self.assertIn("<p>You selected '%s'</p>" % uploaded_file1_name, display, display)
-        self.assertIn("<p>with contents: '%s'</p>" % uploaded_file1_contents, display, \
+        self.assertIn("<p>with contents: '%s'</p>" % to_string(uploaded_file1_contents), display, \
             display)
         self.assertIn("<p>You selected '%s'</p>" % uploaded_file2_name, display, display)
-        self.assertIn("<p>with contents: '%s'</p>" % uploaded_file2_contents, display, \
+        self.assertIn("<p>with contents: '%s'</p>" % to_string(uploaded_file2_contents), display, \
             display)

tests/test_input.py

 # -*- coding: utf-8 -*-
 from webob import Request
 import webtest
+from webtest.compat import to_bytes
 from tests.compat import unittest
 from tests.compat import u
 
 
 def input_app(environ, start_response):
     req = Request(environ)
-    status = "200 OK"
-    body =\
+    status = to_bytes("200 OK")
+    body = to_bytes(
 """
 <html>
     <head><title>form page</title></head>
         </form>
     </body>
 </html>
-"""
+""")
     headers = [
         ('Content-Type', 'text/html'),
         ('Content-Length', str(len(body)))]
 
 def input_app_without_default(environ, start_response):
     req = Request(environ)
-    status = "200 OK"
-    body =\
+    status = to_bytes("200 OK")
+    body = to_bytes(
 """
 <html>
     <head><title>form page</title></head>
         </form>
     </body>
 </html>
-"""
+""")
     headers = [
         ('Content-Type', 'text/html'),
         ('Content-Length', str(len(body)))]
 
 def input_unicode_app(environ, start_response):
     req = Request(environ)
-    status = "200 OK"
+    status = to_bytes("200 OK")
     body =\
 u("""
 <html>

tests/test_script_name.py

 # -*- coding: utf-8 -*-
 import webtest
-from webob import Request, Response, exc
+from webob import Request, Response
 from tests.compat import unittest
+from webtest.compat import to_bytes
 
 def application(environ, start_response):
     req = Request(environ)
     if req.path_info == '/redirect':
         req.path_info = '/path'
-        resp = exc.HTTPFound(location=req.path)
+        resp = Response()
+        resp.status = '302 Found'
+        resp.location = req.path
     else:
         resp = Response()
-        resp.body = '<html><body><a href="%s">link</a></body></html>' % req.path
+        resp.body = to_bytes('<html><body><a href="%s">link</a></body></html>' % req.path)
     return resp(environ, start_response)
 
 class TestScriptName(unittest.TestCase):

tests/test_select.py

 # -*- coding: utf-8 -*-
 from webob import Request
 import webtest
+from webtest.compat import binary_type
+from webtest.compat import to_bytes
 from tests.compat import unittest
 from tests.compat import u
 
     req = Request(environ)
     status = "200 OK"
     if req.method == "GET":
-        body =\
+        body = to_bytes(
 """
 <html>
     <head><title>form page</title></head>
         </form>
     </body>
 </html>
-"""
+""")
     else:
         select_type = req.POST.get("button")
         if select_type == "single":
             selection = req.POST.get("single")
         elif select_type == "multiple":
             selection = ", ".join(req.POST.getall("multiple"))
-        body =\
+        body = to_bytes(
 """
 <html>
     <head><title>display page</title></head>
         <p>You selected %(selection)s</p>
     </body>
 </html>
-""" % locals()
+""" % locals())
 
     headers = [
         ('Content-Type', 'text/html; charset=utf-8'),
         ('Content-Length', str(len(body)))]
-    start_response(status, headers)
-    return [body.encode('utf8')]
+    start_response(to_bytes(status), headers)
+    return [body]
 
 def select_app_without_default(environ, start_response):
     req = Request(environ)
     status = "200 OK"
     if req.method == "GET":
-        body =\
+        body = to_bytes(
 """
 <html>
     <head><title>form page</title></head>
         </form>
     </body>
 </html>
-"""
+""")
     else:
         select_type = req.POST.get("button")
         if select_type == "single":
             selection = req.POST.get("single")
         elif select_type == "multiple":
             selection = ", ".join(req.POST.getall("multiple"))
-        body =\
+        body = to_bytes(
 """
 <html>
     <head><title>display page</title></head>
         <p>You selected %(selection)s</p>
     </body>
 </html>
-""" % locals()
+""" % locals())
 
     headers = [
         ('Content-Type', 'text/html; charset=utf-8'),
         ('Content-Length', str(len(body)))]
-    start_response(status, headers)
-    return [body.encode('utf8')]
+    start_response(to_bytes(status), headers)
+    return [body]
 
 
 def select_app_unicode(environ, start_response):
     headers = [
         ('Content-Type', 'text/html; charset=utf-8'),
         ('Content-Length', str(len(body)))]
-    start_response(status, headers)
-    if not isinstance(body, str):
-        raise AssertionError('Body is not str')
+    start_response(to_bytes(status), headers)
+    if not isinstance(body, binary_type):
+        raise AssertionError('Body is not %s' % binary_type)
     return [body]
 
 class TestSelect(unittest.TestCase):

tests/test_testing.py

 import webtest
 from webtest.debugapp import debug_app
+from webtest.compat import to_bytes
 from tests.compat import unittest
 
 class TestTesting(unittest.TestCase):
         self.assertEqual(res.status_int, 200)
         self.assertEqual(res.headers['content-type'], 'text/plain')
         self.assertTrue(res.content_length > 0)
-        self.assertEqual(res.body, '')
+        self.assertEqual(res.body, to_bytes(''))
 
     def test_get_params(self):
         res = self.app.post('/', params=dict(a=1))
 from webtest.compat import urlparse
 from webtest.compat import print_stderr
 from webtest.compat import StringIO
+from webtest.compat import BytesIO
 from webtest.compat import SimpleCookie, CookieError
 from webtest.compat import cookie_quote
 from webtest.compat import urlencode
+from webtest.compat import splittype
+from webtest.compat import splithost
 from webtest.compat import string_types
+from webtest.compat import binary_type
 from webtest.compat import text_type
+from webtest.compat import to_string
+from webtest.compat import to_bytes
+from webtest.compat import join_bytes
+from webtest.compat import PY3
 from webob import Request, Response
-from webtest import lint
+
+if PY3:
+    from webtest import lint3 as lint
+else:
+    from webtest import lint
 
 __all__ = ['TestApp', 'TestRequest']
 
     def testbody(self):
         if getattr(self, '_use_unicode', True) and self.charset:
             return self.unicode_body
+        if PY3:
+            return to_string(self.body)
         return self.body
 
     _tag_re = re.compile(r'<(/?)([:a-z0-9_\-]*)(.*?)>', re.S | re.I)
             "You can only follow redirect responses (not %s)"
             % self.status)
         location = self.headers['location']
-        type, rest = urllib.splittype(location)
-        host, path = urllib.splithost(rest)
+        type, rest = splittype(location)
+        host, path = splithost(rest)
         # @@: We should test that it's not a remote redirect
         return self.test_app.get(location, **kw)
 
         href_pat = _make_pattern(href_pattern)
         html_pat = _make_pattern(html_pattern)
 
+        body = self.testbody
+
         _tag_re = re.compile(r'<%s\s+(.*?)>(.*?)</%s>' % (tag, tag),
                              re.I + re.S)
         _script_re = re.compile(r'<script.*?>.*?</script>', re.I | re.S)
         bad_spans = []
-        for match in _script_re.finditer(self.testbody):
+        for match in _script_re.finditer(body):
             bad_spans.append((match.start(), match.end()))
 
         def printlog(s):
 
         found_links = []
         total_links = 0
-        for match in _tag_re.finditer(self.testbody):
+        for match in _tag_re.finditer(body):
             found_bad = False
             for bad_start, bad_end in bad_spans:
                 if (match.start() > bad_start
             % method)
 
         # encode unicode strings for the outside world
-        if getattr(self, '_use_unicode', False):
+        if not PY3 and getattr(self, '_use_unicode', False):
             def to_str(s):
                 if isinstance(s, text_type):
                     return s.encode(self.charset)
             method = self.test_app.post
         return method(href, **args)
 
-    _normal_body_regex = re.compile(r'[ \n\r\t]+')
+    _normal_body_regex = re.compile(to_bytes(r'[ \n\r\t]+'))
 
     _normal_body = None
 
     def normal_body__get(self):
         if self._normal_body is None:
             self._normal_body = self._normal_body_regex.sub(
-                ' ', self.body)
+                to_bytes(' '), self.body)
         return self._normal_body
 
     normal_body = property(normal_body__get,
                 s = s.__unicode__()
             else:
                 s = str(s)
+        # PY3 Workaround.
+        # We don't want to search for str when we have no charset
+        if isinstance(s, text_type) and not self.charset:
+            s = to_bytes(s)
         if isinstance(s, text_type):
             body = self.unicode_body
             normal_body = self.unicode_normal_body
                     "Body contains bad string %r" % no_s)
 
     def __str__(self):
-        simple_body = '\n'.join([l for l in self.body.splitlines()
+        simple_body = '\n'.join([l for l in self.testbody.splitlines()
                                  if l.strip()])
         headers = [(self._normalize_header_name(n), v)
                    for n, v in self.headerlist
                    if n.lower() != 'content-length']
         headers.sort()
         return 'Response: %s\n%s\n%s' % (
-            self.status,
+            to_string(self.status),
             '\n'.join(['%s: %s' % (n, v) for n, v in headers]),
             simple_body)
 
             location = ' location: %s' % self.location
         else:
             location = ''
-        return ('<' + self.status + ct + location + body + '>')
+        return ('<' + to_string(self.status) + ct + location + body + '>')
 
     def html(self):
         """
         if hasattr(params, 'items'):
             params = urlencode(params.items(), doseq=True)
         if upload_files or \
-                (content_type and content_type.startswith('multipart')):
+                (content_type and to_string(content_type).startswith('multipart')):
             params = cgi.parse_qsl(params, keep_blank_values=True)
             content_type, params = self.encode_multipart(
                 params, upload_files or ())
             environ['CONTENT_TYPE'] = content_type
         environ['CONTENT_LENGTH'] = str(len(params))
         environ['REQUEST_METHOD'] = method
-        environ['wsgi.input'] = StringIO(params)
+        environ['wsgi.input'] = BytesIO(to_bytes(params))
         url = self._remove_fragment(url)
         req = self.RequestClass.blank(url, environ)
         if headers:
             lines.append(value)
         lines.append('--' + boundary + '--')
         lines.append('')
-        body = '\r\n'.join(lines)
+        body = join_bytes('\r\n', lines)
         content_type = 'multipart/form-data; boundary=%s' % boundary
         return content_type, body
 
                 filename = os.path.join(self.relative_to, filename)
             f = open(filename, 'rb')
             content = f.read()
+            if PY3 and isinstance(content, text_type):
+                # we want bytes
+                content = content.encode(f.encoding)
             f.close()
             return (file_info[0], filename, content)
         elif len(file_info) == 3:
+            content = file_info[2]
+            if not isinstance(content, binary_type):
+                raise ValueError('File content must be %s not %s'
+                        % (binary_type, type(content)))
             return file_info
         else:
             raise ValueError(
         res.app = app
         res.test_app = self
         # We do this to make sure the app_iter is exausted:
-        res.body
+        try:
+            res.body
+        except TypeError:
+            pass
         res.errors = errors.getvalue()
         total_time = end_time - start_time
         for name, value in req.environ['paste.testing_variables'].items():
         __tracebackhide__ = True
         if status == '*':
             return
+        res_status = to_string(res.status)
         if (isinstance(status, string_types)
             and '*' in status):
-            if re.match(fnmatch.translate(status), res.status, re.I):
+            if re.match(fnmatch.translate(status), res_status, re.I):
                 return
         if isinstance(status, (list, tuple)):
             if res.status_int not in status:
                 raise AppError(
                     "Bad response: %s (not one of %s for %s)\n%s"
-                    % (res.status, ', '.join(map(str, status)),
+                    % (res_status, ', '.join(map(str, status)),
                        res.request.url, res.body))
             return
         if status is None:
                 return
             raise AppError(
                 "Bad response: %s (not 200 OK or 3xx redirect for %s)\n%s"
-                % (res.status, res.request.url,
+                % (res_status, res.request.url,
                    res.body))
         if status != res.status_int:
             raise AppError(
-                "Bad response: %s (not %s)" % (res.status, status))
+                "Bad response: %s (not %s)" % (res_status, status))
 
     def _check_errors(self, res):
         errors = res.errors

webtest/compat.py

     string_types = (str,)
     text_type = str
     binary_type = bytes
-    DictType = dict
-    StringType = bytes
-    TupleType = tuple
-    ListType = list
     from io import StringIO
+    from io import BytesIO
     from urllib.parse import urlencode
+    from urllib.parse import splittype
+    from urllib.parse import splithost
     import urllib.parse as urlparse
     from http.client import HTTPConnection
     from http.client import CannotSendRequest
     from http.server import SimpleHTTPRequestHandler
     from http.cookies import SimpleCookie, CookieError
     from http.cookies import _quote as cookie_quote
+
+    def to_bytes(s):
+        if isinstance(s, bytes):
+            return s
+        return s.encode('ISO-8859-1')
+
+    def to_string(s):
+        if isinstance(s, str):
+            return s
+        return str(s, 'ISO-8859-1')
+
+    def join_bytes(sep, l):
+        l = [to_bytes(e) for e in l]
+        return to_bytes(sep).join(l)
+
 else:
     PY3 = False
     string_types = basestring
     text_type = unicode
     binary_type = str
-    from types import DictType, StringType, TupleType, ListType
+    from urllib import splittype
+    from urllib import splithost
     from urllib import urlencode
     from httplib import HTTPConnection
     from httplib import CannotSendRequest
         from cStringIO import StringIO
     except ImportError:
         from StringIO import StringIO
+    BytesIO = StringIO
     import urlparse
 
+    def to_bytes(s):
+        return str(s)
 
+    def to_string(s):
+        return str(s)
+
+    def join_bytes(sep, l):
+        l = [e for e in l]
+        return sep.join(l)
 
 def print_stderr(value):
     if PY3:

webtest/debugapp.py

 from webob import Request, Response
-try:
-    sorted
-except NameError:
-    from webtest import sorted
+from webtest.compat import to_bytes
 
 __all__ = ['debug_app']
 
+
 def debug_app(environ, start_response):
     req = Request(environ)
     if req.path_info == '/form.html' and req.method == 'GET':
         resp = Response(content_type='text/html')
-        resp.body = '''<html><body>
+        resp.body = to_bytes('''<html><body>
         <form action="/form-submit" method="POST">
             <input type="text" name="name">
             <input type="submit" name="submit" value="Submit!">
-        </form></body></html>'''
+        </form></body></html>''')
         return resp(environ, start_response)
 
     if 'error' in req.GET:
     else:
         req_body = ''
     if req_body:
-        parts.append('-- Body ----------\n')
+        parts.append(to_bytes('-- Body ----------\n'))
         parts.append(req_body)
-    body = ''.join(parts)
+    body = to_bytes('').join([to_bytes(p) for p in parts])
 
     if status[:3] in ('204', '304') and not req_body:
-        body = ''
+        body = to_bytes('')
 
     headers = [
         ('Content-Type', 'text/plain'),
             header_name = name[len('header-'):]
             headers.append((header_name, value))
 
-    start_response(status, headers)
+    start_response(to_bytes(str(status)), headers)
     if req.method == 'HEAD':
-        return ['']
+        return [to_bytes('')]
     return [body]
 
 def make_debug_app(global_conf):
+# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
+# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
+# Also licenced under the Apache License, 2.0: http://opensource.org/licenses/apache2.0.php
+# Licensed to PSF under a Contributor Agreement
+"""
+Middleware to check for obedience to the WSGI specification.
+
+Some of the things this checks:
+
+* Signature of the application and start_response (including that
+  keyword arguments are not used).
+
+* Environment checks:
+
+  - Environment is a dictionary (and not a subclass).
+
+  - That all the required keys are in the environment: REQUEST_METHOD,
+    SERVER_NAME, SERVER_PORT, wsgi.version, wsgi.input, wsgi.errors,
+    wsgi.multithread, wsgi.multiprocess, wsgi.run_once
+
+  - That HTTP_CONTENT_TYPE and HTTP_CONTENT_LENGTH are not in the
+    environment (these headers should appear as CONTENT_LENGTH and
+    CONTENT_TYPE).
+
+  - Warns if QUERY_STRING is missing, as the cgi module acts
+    unpredictably in that case.
+
+  - That CGI-style variables (that don't contain a .) have
+    (non-unicode) string values
+
+  - That wsgi.version is a tuple
+
+  - That wsgi.url_scheme is 'http' or 'https' (@@: is this too
+    restrictive?)
+
+  - Warns if the REQUEST_METHOD is not known (@@: probably too
+    restrictive).
+
+  - That SCRIPT_NAME and PATH_INFO are empty or start with /
+
+  - That at least one of SCRIPT_NAME or PATH_INFO are set.
+
+  - That CONTENT_LENGTH is a positive integer.
+
+  - That SCRIPT_NAME is not '/' (it should be '', and PATH_INFO should
+    be '/').
+
+  - That wsgi.input has the methods read, readline, readlines, and
+    __iter__
+
+  - That wsgi.errors has the methods flush, write, writelines
+
+* The status is a string, contains a space, starts with an integer,
+  and that integer is in range (> 100).
+
+* That the headers is a list (not a subclass, not another kind of
+  sequence).
+
+* That the items of the headers are tuples of strings.
+
+* That there is no 'status' header (that is used in CGI, but not in
+  WSGI).
+
+* That the headers don't contain newlines or colons, end in _ or -, or
+  contain characters codes below 037.
+
+* That Content-Type is given if there is content (CGI often has a
+  default content type, but WSGI does not).
+
+* That no Content-Type is given when there is no content (@@: is this
+  too restrictive?)
+
+* That the exc_info argument to start_response is a tuple or None.
+
+* That all calls to the writer are with strings, and no other methods
+  on the writer are accessed.
+
+* That wsgi.input is used properly:
+
+  - .read() is called with zero or one argument
+
+  - That it returns a string
+
+  - That readline, readlines, and __iter__ return strings
+
+  - That .close() is not called
+
+  - No other methods are provided
+
+* That wsgi.errors is used properly:
+
+  - .write() and .writelines() is called with a string
+
+  - That .close() is not called, and no other methods are provided.
+
+* The response iterator:
+
+  - That it is not a string (it should be a list of a single string; a
+    string will work, but perform horribly).
+
+  - That .next() returns a string
+
+  - That the iterator is not iterated over until start_response has
+    been called (that can signal either a server or application
+    error).
+
+  - That .close() is called (doesn't raise exception, only prints to
+    sys.stderr, because we only know it isn't called when the object
+    is garbage collected).
+"""
+from __future__ import unicode_literals
+
+import re
+import sys
+import warnings
+from webtest.compat import PY3
+from webtest.compat import to_string
+
+header_re = re.compile(r'^[a-zA-Z][a-zA-Z0-9\-_]*$')
+bad_header_value_re = re.compile(r'[\000-\037]')
+
+METADATA_TYPE = (str, bytes)
+
+class WSGIWarning(Warning):
+    """
+    Raised in response to WSGI-spec-related warnings
+    """
+
+def middleware(application, global_conf=None):
+
+    """
+    When applied between a WSGI server and a WSGI application, this
+    middleware will check for WSGI compliancy on a number of levels.
+    This middleware does not modify the request or response in any
+    way, but will throw an AssertionError if anything seems off
+    (except for a failure to close the application iterator, which
+    will be printed to stderr -- there's no way to throw an exception
+    at that point).
+    """
+
+    def lint_app(*args, **kw):
+        assert len(args) == 2, "Two arguments required"
+        assert not kw, "No keyword arguments allowed"
+        environ, start_response = args
+
+        check_environ(environ)
+
+        # We use this to check if the application returns without
+        # calling start_response:
+        start_response_started = []
+
+        def start_response_wrapper(*args, **kw):
+            assert len(args) == 2 or len(args) == 3, (
+                "Invalid number of arguments: %s" % args)
+            assert not kw, "No keyword arguments allowed"
+            status = args[0]
+            headers = args[1]
+            if len(args) == 3:
+                exc_info = args[2]
+            else:
+                exc_info = None
+
+            check_status(status)
+            check_headers(headers)
+            check_content_type(status, headers)
+            check_exc_info(exc_info)
+
+            start_response_started.append(None)
+            return WriteWrapper(start_response(*args))
+
+        environ['wsgi.input'] = InputWrapper(environ['wsgi.input'])
+        environ['wsgi.errors'] = ErrorWrapper(environ['wsgi.errors'])
+
+        iterator = application(environ, start_response_wrapper)
+        assert iterator is not None and iterator != False, (
+            "The application must return an iterator, if only an empty list")
+
+        check_iterator(iterator)
+
+        return IteratorWrapper(iterator, start_response_started)
+
+    return lint_app
+
+class InputWrapper(object):
+
+    def __init__(self, wsgi_input):
+        self.input = wsgi_input
+
+    def read(self, *args):
+        assert len(args) <= 1
+        v = self.input.read(*args)
+        assert type(v) is bytes
+        return v
+
+    def readline(self, *args):
+        v = self.input.readline(*args)
+        assert type(v) is bytes
+        return v
+
+    def readlines(self, *args):
+        assert len(args) <= 1
+        lines = self.input.readlines(*args)
+        assert type(lines) is type([])
+        for line in lines:
+            assert type(line) is bytes
+        return lines
+
+    def __iter__(self):
+        while 1:
+            line = self.readline()
+            if not line:
+                return
+            yield line
+
+    def close(self):
+        assert 0, "input.close() must not be called"
+
+class ErrorWrapper(object):
+
+    def __init__(self, wsgi_errors):
+        self.errors = wsgi_errors
+
+    def write(self, s):
+        assert type(s) is bytes
+        self.errors.write(s)
+
+    def flush(self):
+        self.errors.flush()
+
+    def writelines(self, seq):
+        for line in seq:
+            self.write(line)
+
+    def close(self):
+        assert 0, "errors.close() must not be called"
+
+class WriteWrapper(object):
+
+    def __init__(self, wsgi_writer):
+        self.writer = wsgi_writer
+
+    def __call__(self, s):
+        assert type(s) is bytes
+        self.writer(s)
+
+class PartialIteratorWrapper(object):
+
+    def __init__(self, wsgi_iterator):
+        self.iterator = wsgi_iterator
+
+    def __iter__(self):
+        # We want to make sure __iter__ is called
+        return IteratorWrapper(self.iterator)
+
+class IteratorWrapper(object):
+
+    def __init__(self, wsgi_iterator, check_start_response):
+        self.original_iterator = wsgi_iterator
+        self.iterator = iter(wsgi_iterator)
+        self.closed = False
+        self.check_start_response = check_start_response
+
+    def __iter__(self):
+        return self
+
+    def next(self):
+        assert not self.closed, (
+            "Iterator read after closed")
+        try:
+            v = next(self.iterator)
+        except NameError:
+            v = self.iterator.next()
+        if self.check_start_response is not None:
+            assert self.check_start_response, (
+                "The application returns and we started iterating over its body, but start_response has not yet been called")
+            self.check_start_response = None
+        assert isinstance(v, bytes), (
+            "Iterator %r returned a non-%r object: %r"
+            % (self.iterator, bytes, v))
+        return v
+
+    __next__ = next
+
+    def close(self):
+        self.closed = True
+        if hasattr(self.original_iterator, 'close'):
+            self.original_iterator.close()
+
+    def __del__(self):
+        if not self.closed:
+            sys.stderr.write(
+                "Iterator garbage collected without being closed")
+        assert self.closed, (
+            "Iterator garbage collected without being closed")
+
+def check_environ(environ):
+    assert type(environ) is dict, (
+        "Environment is not of the right type: %r (environment: %r)"
+        % (type(environ), environ))
+
+    for key in ['REQUEST_METHOD', 'SERVER_NAME', 'SERVER_PORT',
+                'wsgi.version', 'wsgi.input', 'wsgi.errors',
+                'wsgi.multithread', 'wsgi.multiprocess',
+                'wsgi.run_once']:
+        assert key in environ, (
+            "Environment missing required key: %r" % key)
+
+    for key in ['HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH']:
+        assert key not in environ, (
+            "Environment should not have the key: %s "
+            "(use %s instead)" % (key, key[5:]))
+
+    if 'QUERY_STRING' not in environ:
+        warnings.warn(
+            'QUERY_STRING is not in the WSGI environment; the cgi '
+            'module will use sys.argv when this variable is missing, '
+            'so application errors are more likely',
+            WSGIWarning)
+
+    for key in environ:
+        if '.' in key:
+            # Extension, we don't care about its type
+            continue
+        assert type(environ[key]) in METADATA_TYPE, (
+            "Environmental variable %s is not a string: %r (value: %r)"
+            % (key, type(environ[key]), environ[key]))
+
+    assert type(environ['wsgi.version']) is tuple, (
+        "wsgi.version should be a tuple (%r)" % environ['wsgi.version'])
+    assert environ['wsgi.url_scheme'] in ('http', 'https'), (
+        "wsgi.url_scheme unknown: %r" % environ['wsgi.url_scheme'])
+
+    check_input(environ['wsgi.input'])
+    check_errors(environ['wsgi.errors'])
+
+    # @@: these need filling out:
+    if environ['REQUEST_METHOD'] not in (
+        'GET', 'HEAD', 'POST', 'OPTIONS','PUT','DELETE','TRACE'):
+        warnings.warn(
+            "Unknown REQUEST_METHOD: %r" % environ['REQUEST_METHOD'],
+            WSGIWarning)
+
+    assert (not environ.get('SCRIPT_NAME')
+            or environ['SCRIPT_NAME'].startswith('/')), (
+        "SCRIPT_NAME doesn't start with /: %r" % environ['SCRIPT_NAME'])
+    assert (not environ.get('PATH_INFO')
+            or environ['PATH_INFO'].startswith('/')), (
+        "PATH_INFO doesn't start with /: %r" % environ['PATH_INFO'])
+    if environ.get('CONTENT_LENGTH'):
+        assert int(environ['CONTENT_LENGTH']) >= 0, (
+            "Invalid CONTENT_LENGTH: %r" % environ['CONTENT_LENGTH'])
+
+    if not environ.get('SCRIPT_NAME'):
+        assert 'PATH_INFO' in environ, (
+            "One of SCRIPT_NAME or PATH_INFO are required (PATH_INFO "
+            "should at least be '/' if SCRIPT_NAME is empty)")
+    assert environ.get('SCRIPT_NAME') != '/', (
+        "SCRIPT_NAME cannot be '/'; it should instead be '', and "
+        "PATH_INFO should be '/'")
+
+def check_input(wsgi_input):
+    for attr in ['read', 'readline', 'readlines', '__iter__']:
+        assert hasattr(wsgi_input, attr), (
+            "wsgi.input (%r) doesn't have the attribute %s"
+            % (wsgi_input, attr))
+
+def check_errors(wsgi_errors):
+    for attr in ['flush', 'write', 'writelines']:
+        assert hasattr(wsgi_errors, attr), (
+            "wsgi.errors (%r) doesn't have the attribute %s"
+            % (wsgi_errors, attr))
+
+def check_status(status):
+    assert type(status) in METADATA_TYPE , (
+        "Status must be a %s (not %r)" % (METADATA_TYPE, status))
+    # Implicitly check that we can turn it into an integer:
+    status_code = status.split(None, 1)[0]
+    assert len(status_code) == 3, (
+        "Status codes must be three characters: %r" % status_code)
+    status_int = int(status_code)
+    assert status_int >= 100, "Status code is invalid: %r" % status_int
+    if len(status) < 4 or status[3] != ' ':
+        warnings.warn(
+            "The status string (%r) should be a three-digit integer "
+            "followed by a single space and a status explanation"
+            % status, WSGIWarning)
+
+def check_headers(headers):
+    assert type(headers) is list, (
+        "Headers (%r) must be of type list: %r"
+        % (headers, type(headers)))
+    for item in headers:
+        assert type(item) is tuple, (
+            "Individual headers (%r) must be of type tuple: %r"
+            % (item, type(item)))
+        assert len(item) == 2
+        name, value = item
+        if type(name) is str:
+            try:
+                name.encode('latin1')
+            except UnicodeEncodeError:
+                raise AssertionError((
+                    "Headers name must be latin1 string or bytes."
+                    "%r is not a valid latin1 string" % (name,)))
+        str_name = to_string(name)
+        assert str_name.lower() != 'status', (
+            "The Status header cannot be used; it conflicts with CGI "
+            "script, and HTTP status is not given through headers "
+            "(value: %r)." % value)
+        assert '\n' not in str_name and ':' not in str_name, (
+            "Header names may not contain ':' or '\\n': %r" % name)
+        assert header_re.search(str_name), "Bad header name: %r" % name
+        assert not str_name.endswith('-') and not str_name.endswith('_'), (
+            "Names may not end in '-' or '_': %r" % name)
+        if type(value) is str:
+            try:
+                value.encode('latin1')
+            except UnicodeEncodeError:
+                raise AssertionError((
+                    "Headers values must be latin1 string or bytes."
+                    "%r is not a valid latin1 string" % (value,)))
+        str_value = to_string(value)
+        assert not bad_header_value_re.search(str_value), (
+            "Bad header value: %r (bad char: %r)"
+            % (str_value, bad_header_value_re.search(str_value).group(0)))
+
+def check_content_type(status, headers):
+    code = int(status.split(None, 1)[0])
+    # @@: need one more person to verify this interpretation of RFC 2616
+    #     http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+    NO_MESSAGE_BODY = (201, 204, 304)
+    NO_MESSAGE_TYPE = (204, 304)
+    length = None
+    for name, value in headers:
+        str_name = to_string(name)
+        if str_name.lower() == 'content-length' and value.isdigit():
+            length = int(value)
+    for name, value in headers:
+        str_name = to_string(name)
+        if str_name.lower() == 'content-type':
+            if code not in NO_MESSAGE_TYPE:
+                return
+            elif length == 0:
+                warnings.warn(("Content-Type header found in a %s response, "
+                               "which not return content.") % code,
+                               WSGIWarning)
+                return
+            else:
+                assert 0, (("Content-Type header found in a %s response, "
+                            "which must not return content.") % code)
+    if code not in NO_MESSAGE_BODY:
+        assert 0, "No Content-Type header found in headers (%s)" % headers
+
+def check_exc_info(exc_info):
+    assert exc_info is None or type(exc_info) is tuple, (
+        "exc_info (%r) is not a tuple: %r" % (exc_info, type(exc_info)))
+    # More exc_info checks?
+
+def check_iterator(iterator):
+    # Technically a bytes is legal, which is why it's a really bad
+    # idea, because it may cause the response to be returned
+    # character-by-character
+    assert not isinstance(iterator, bytes), (
+        "You should not return a bytes as your application iterator, "
+        "instead return a single-item list containing that string.")
+    assert not isinstance(iterator, str), (
+        "You should not return a string as your application iterator, "
+        "instead return a single-item list containing bytes.")
+
+def make_middleware(application, global_conf):
+    # @@: global_conf should be taken out of the middleware function,
+    # and isolated here
+    return middleware(application)
+
+make_middleware.__doc__ = __doc__
+
+__all__ = ['middleware', 'make_middleware']