diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..af40312 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,6 @@ +[run] +source = src + +[report] +exclude_lines = + pragma: no cover diff --git a/.gitignore b/.gitignore index 5149542..6bacfe5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ build/ dist/ *.egg-info/ .tox/ +.coverage +htmlcov/ diff --git a/.travis.yml b/.travis.yml index 02dca11..f9678cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,21 @@ language: python +sudo: false python: - 2.7 + - pypy-5.4.1 + - 3.4 + - 3.5 + - 3.6 install: - - pip install . + - pip install -U pip setuptools + - pip install -U coverage coveralls + - pip install -U -e .[test] script: - - python setup.py test -q + - coverage run -m zope.testrunner --test-path=src --auto-color --auto-progress notifications: email: false +after_success: + - coveralls +cache: pip +before_cache: + - rm -f $HOME/.cache/pip/log/debug.log diff --git a/CHANGES.txt b/CHANGES.rst similarity index 57% rename from CHANGES.txt rename to CHANGES.rst index daf85af..4a5a626 100644 --- a/CHANGES.txt +++ b/CHANGES.rst @@ -1,14 +1,19 @@ -======= -CHANGES -======= +========= + CHANGES +========= + +4.0.0 (unreleased) +================== + +- Add support for Python 3.4, 3.5, 3.6 and PyPy. 3.4.1 (2008-02-02) ------------------- +================== - Fix of 599 error on conflict error in request see: http://mail.zope.org/pipermail/zope-dev/2008-January/030844.html 3.4.0 (2007-10-23) ------------------- +================== - Initial release independent of the main Zope tree. diff --git a/MANIFEST.in b/MANIFEST.in index ca417b9..f194c19 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,10 @@ include *.py include *.txt +include *.rst include buildout.cfg +include tox.ini +include .coveragerc +exclude .coverage +include .travis.yml +recursive-include src *.pt +recursive-include src *.zcml diff --git a/README.txt b/README.rst similarity index 100% rename from README.txt rename to README.rst diff --git a/setup.py b/setup.py index 49c8043..66d6ab7 100644 --- a/setup.py +++ b/setup.py @@ -18,26 +18,35 @@ ############################################################################## """Setup for zope.app.debug package -$Id$ """ import os from setuptools import setup, find_packages def read(*rnames): - return open(os.path.join(os.path.dirname(__file__), *rnames)).read() + with open(os.path.join(os.path.dirname(__file__), *rnames)) as f: + return f.read() + +tests_require = [ + 'zope.component', + 'zope.principalregistry', + 'zope.site', + 'zope.traversing', + 'zope.testing', + 'zope.testrunner', +] setup(name='zope.app.debug', - version = '3.4.2dev', + version='4.0.0.dev0', author='Zope Corporation and Contributors', author_email='zope-dev@zope.org', description='Zope Debug Mode', long_description=( - read('README.txt') + read('README.rst') + '\n\n' + - read('CHANGES.txt') + read('CHANGES.rst') ), - keywords = "zope3 webdav", - classifiers = [ + keywords="zope3 webdav", + classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Intended Audience :: Developers', @@ -46,18 +55,32 @@ def read(*rnames): 'Natural Language :: English', 'Operating System :: OS Independent', 'Topic :: Internet :: WWW/HTTP', - 'Framework :: Zope3'], - url='http://cheeseshop.python.org/pypi/zope.app.debug', + 'Framework :: Zope3', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + ], + url='http://github.com/zopefoundation/zope.app.debug', license='ZPL 2.1', packages=find_packages('src'), - package_dir = {'': 'src'}, + package_dir={'': 'src'}, namespace_packages=['zope', 'zope.app'], - extras_require = dict(test=['zope.app.testing']), - install_requires=['setuptools', - 'zope.publisher', - 'zope.app.appsetup', - 'zope.app.publication' - ], - include_package_data = True, - zip_safe = False, - ) + extras_require={ + 'test': tests_require, + }, + tests_require=tests_require, + install_requires=[ + 'setuptools', + 'zope.publisher', + 'zope.app.appsetup', + 'zope.app.publication' + ], + include_package_data=True, + zip_safe=False, +) diff --git a/src/zope/__init__.py b/src/zope/__init__.py index 2e2033b..2cdb0e4 100644 --- a/src/zope/__init__.py +++ b/src/zope/__init__.py @@ -1,7 +1 @@ -# this is a namespace package -try: - import pkg_resources - pkg_resources.declare_namespace(__name__) -except ImportError: - import pkgutil - __path__ = pkgutil.extend_path(__path__, __name__) +__import__('pkg_resources').declare_namespace(__name__) # pragma: no cover diff --git a/src/zope/app/__init__.py b/src/zope/app/__init__.py index 2e2033b..2cdb0e4 100644 --- a/src/zope/app/__init__.py +++ b/src/zope/app/__init__.py @@ -1,7 +1 @@ -# this is a namespace package -try: - import pkg_resources - pkg_resources.declare_namespace(__name__) -except ImportError: - import pkgutil - __path__ = pkgutil.extend_path(__path__, __name__) +__import__('pkg_resources').declare_namespace(__name__) # pragma: no cover diff --git a/src/zope/app/debug/debug.py b/src/zope/app/debug/debug.py index dd8b7c6..c9c0271 100644 --- a/src/zope/app/debug/debug.py +++ b/src/zope/app/debug/debug.py @@ -13,13 +13,21 @@ ############################################################################## """Code to initialize the application server -$Id$ """ +from __future__ import print_function __docformat__ = 'restructuredtext' -import base64, time -import urllib -from StringIO import StringIO +import base64 +import time +try: + import urllib.parse as urllib +except ImportError: + import urllib +import sys + +from pdb import Pdb +from io import BytesIO + from zope.publisher.publish import publish as _publish, debug_call from zope.publisher.browser import TestRequest, setDefaultSkin from zope.app.publication.browser import BrowserPublication @@ -27,7 +35,9 @@ class Debugger(object): - def __init__(self, db=None, config_file=None): + pdb = Pdb + + def __init__(self, db=None, config_file=None, stdout=None): if db is None and config_file is None: db = 'Data.fs' config_file = 'site.zcml' @@ -35,12 +45,13 @@ def __init__(self, db=None, config_file=None): if config_file is not None: config(config_file) self.db = database(db) + self.stdout = stdout + @classmethod def fromDatabase(cls, db): inst = cls.__new__(cls) inst.db = db return inst - fromDatabase = classmethod(fromDatabase) def root(self): """Get the top-level application object @@ -60,13 +71,13 @@ def _request(self, env = {} - if type(stdin) is str: - stdin = StringIO(stdin) + if isinstance(stdin, bytes): + stdin = BytesIO(stdin) - p=path.split('?') - if len(p)==1: + p = path.split('?') + if len(p) == 1: env['PATH_INFO'] = p[0] - elif len(p)==2: + elif len(p) == 2: env['PATH_INFO'], env['QUERY_STRING'] = p else: raise ValueError("Too many ?s in path", path) @@ -77,7 +88,10 @@ def _request(self, env.update(environment) if basic: - env['HTTP_AUTHORIZATION']="Basic %s" % base64.encodestring(basic) + basic_bytes = basic.encode('ascii') if not isinstance(basic, bytes) else basic + basic64_bytes = base64.b64encode(basic_bytes) + basic64 = basic64_bytes.decode('ascii').strip() + env['HTTP_AUTHORIZATION'] = "Basic %s" % basic64 pub = publication(self.db) @@ -89,8 +103,6 @@ def _request(self, setDefaultSkin(request) request.setPublication(pub) if form: - # This requires that request class has an attribute 'form' - # (BrowserRequest has, TestRequest hasn't) request.form.update(form) return request @@ -105,13 +117,14 @@ def publish(self, path='/', stdin='', *args, **kw): request = _publish(request) getStatus = getattr(request.response, 'getStatus', lambda: None) - headers = request.response.getHeaders() - headers.sort() - print 'Status %s\r\n%s\r\n\r\n%s' % ( - request.response.getStatusString(), - '\r\n'.join([("%s: %s" % h) for h in headers]), - request.response.consumeBody(), - ) + headers = sorted(request.response.getHeaders()) + + print( + 'Status %s\r\n%s\r\n\r\n%s' % ( + request.response.getStatusString(), + '\r\n'.join([("%s: %s" % h) for h in headers]), + request.response.consumeBody(), + ), file=self.stdout or sys.stdout) return time.time()-t, time.clock()-c, getStatus() def run(self, *args, **kw): @@ -125,42 +138,49 @@ def run(self, *args, **kw): return time.time()-t, time.clock()-c, getStatus() def debug(self, *args, **kw): + out = self.stdout or sys.stdout + + class ZopePdb(self.Pdb): + done_pub = False + done_ob = False + + def do_pub(self, arg): + if self.done_pub: + print('pub already done.', file=out) + return + + self.do_s('') + self.do_s('') + self.do_c('') + self.done_pub = True + + def do_ob(self, arg): + if self.done_ob: + print('ob already done.', file=out) + return + + self.do_pub('') + self.do_c('') + self.done_ob = True - import pdb - - class Pdb(pdb.Pdb): - def do_pub(self,arg): - if hasattr(self,'done_pub'): - print 'pub already done.' - else: - self.do_s('') - self.do_s('') - self.do_c('') - self.done_pub=1 - def do_ob(self,arg): - if hasattr(self,'done_ob'): - print 'ob already done.' - else: - self.do_pub('') - self.do_c('') - self.done_ob=1 - - db=Pdb() + dbg = ZopePdb() request = self._request(*args, **kw) - fbreak(db, _publish) - fbreak(db, debug_call) + fbreak(dbg, _publish) + fbreak(dbg, debug_call) - print '* Type c to jump to published object call.' - db.runcall(_publish, request) + print('* Type c to jump to published object call.', + file=out) + dbg.runcall(_publish, request) + return dbg def fbreak(db, meth): try: - meth = meth.im_func + meth = meth.__func__ except AttributeError: pass - code = meth.func_code + code = meth.__code__ lineno = getlineno(code) filename = code.co_filename db.set_break(filename,lineno) diff --git a/src/zope/app/debug/tests/__init__.py b/src/zope/app/debug/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/zope/app/debug/tests/ftesting.zcml b/src/zope/app/debug/tests/ftesting.zcml new file mode 100644 index 0000000..ba206e5 --- /dev/null +++ b/src/zope/app/debug/tests/ftesting.zcml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/zope/app/debug/tests/test_debug.py b/src/zope/app/debug/tests/test_debug.py new file mode 100644 index 0000000..5080e16 --- /dev/null +++ b/src/zope/app/debug/tests/test_debug.py @@ -0,0 +1,171 @@ +############################################################################## +# +# Copyright (c) 2017 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. +# +############################################################################## + +import unittest + +if str is bytes: + from io import BytesIO as StringIO +else: + from io import StringIO + + +from ZODB.MappingStorage import MappingStorage +from ZODB.DB import DB +from zope.testing import cleanup +from zope.app.debug.debug import Debugger + +from zope.configuration import xmlconfig + +from zope.component.testlayer import ZCMLFileLayer +from zope.security.management import endInteraction + +import zope.app.debug.tests + +DebugLayer = ZCMLFileLayer(zope.app.debug.tests) + +class FolderView(object): + + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self): + return u"Hi" + +class TestDebugger(unittest.TestCase): + + layer = DebugLayer + + def setUp(self): + endInteraction() + self.storage = MappingStorage() + self.db = DB(self.storage) + + def test_from_database(self): + dbg = Debugger.fromDatabase(self.db) + self.assertIs(self.db, dbg.db) + + def test_construct(self): + self.assertRaises(IOError, Debugger) + dbg = Debugger(self.db) + self.assertIs(self.db, dbg.db) + + def test_root(self): + # Only happens from the constructor + dbg = Debugger.fromDatabase(self.db) + self.assertRaises(KeyError, dbg.root) + dbg = Debugger(self.db) + self.assertIsNotNone(dbg.root()) + + def test_debug(self): + out = StringIO() + dbg = Debugger(self.db, stdout=out) + + class Pdb(object): + + def __init__(self): + self.breaks = set() + self.calls = [] + self.s = [] + self.c = [] + + def set_break(self, filename, lineno): + self.breaks.add(filename) + + def runcall(self, *args): + self.calls.append(args) + + def do_s(self, arg): + self.s.append(arg) + + def do_c(self, arg): + self.c.append(arg) + + + dbg.Pdb = Pdb + + pdb = dbg.debug() + + self.assertIn('to jump to published object call', + out.getvalue()) + self.assertEqual(1, len(pdb.breaks)) + self.assertEqual(1, len(pdb.calls)) + + out.seek(0) + + pdb.do_ob('') + self.assertNotIn('already done', out.getvalue()) + self.assertEqual(['', ''], pdb.s) + self.assertEqual(['', ''], pdb.c) + + pdb.do_pub('') + self.assertIn('pub already done', out.getvalue()) + + pdb.do_ob('') + self.assertIn('ob already done', out.getvalue()) + + def test_run(self): + dbg = Debugger(self.db) + time, clock, status = dbg.run() + self.assertGreaterEqual(time, 0) + self.assertGreaterEqual(clock, 0) + self.assertEqual(200, status) + + def test_publish(self): + stdout = StringIO() + dbg = Debugger(self.db, stdout=stdout) + time, clock, status = dbg.publish() + self.assertGreaterEqual(time, 0) + self.assertGreaterEqual(clock, 0) + self.assertEqual(200, status) + + self.assertIn('Content-Type', stdout.getvalue()) + + def test_request(self): + dbg = Debugger(self.db) + # One query string + req = dbg._request(path='/?q=1') + + # Too many query strings + self.assertEqual(req['QUERY_STRING'], 'q=1') + self.assertRaises(ValueError, dbg._request, path="/?q=1?q=2") + + # Environment + req = dbg._request(environment={'k': 42}) + self.assertEqual(req['k'], 42) + + req = dbg._request(basic="foo:bar") + self.assertFalse(req.has_key('HTTP_AUTHORIZATION')) + self.assertEqual(req._auth, 'Basic Zm9vOmJhcg==') + + # Form + req = dbg._request(form={'k': 42}) + self.assertEqual(req.form['k'], 42) + + # Request factory + + class Request(object): + pub = None + def __init__(self, *args): + pass + + def setPublication(self, p): + self.pub = p + + req = dbg._request(request=Request) + self.assertIsInstance(req, Request) + self.assertIsNotNone(req.pub) + +def test_suite(): + return unittest.defaultTestLoader.loadTestsFromName(__name__) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..89b6219 --- /dev/null +++ b/tox.ini @@ -0,0 +1,9 @@ +[tox] +envlist = + py27, pypy, py34, py35, py36 + +[testenv] +commands = + zope-testrunner --test-path=src [] +deps = + .[test]