Skip to content

Commit

Permalink
svn merge ipython-debug-shell
Browse files Browse the repository at this point in the history
  • Loading branch information
janwijbrand committed May 1, 2012
2 parents daf2cdb + 33e1c87 commit b0a1290
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 12 deletions.
5 changes: 3 additions & 2 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ Changes
1.2 (unreleased)
================

- Nothing changed yet.

- Added new IPython-based interactive debugger which is used
automatically when IPython is available. Otherwise the gdb-style
debugger is provided.

1.1 (2010-10-26)
================
Expand Down
2 changes: 1 addition & 1 deletion buildout.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ interpreter = python

[test]
recipe = zc.recipe.testrunner
eggs = grokcore.startup
eggs = grokcore.startup [test]
defaults = ['--tests-pattern', '^f?tests$', '-v']
7 changes: 6 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ def read(*rnames):
'zope.interface',
'zope.testing',
'zope.security',
'zope.securitypolicy',
]

debug_requires = [
'IPython',
]

setup(
Expand Down Expand Up @@ -50,7 +55,7 @@ def read(*rnames):
'zope.app.debug',
],
tests_require = tests_require,
extras_require = dict(test=tests_require),
extras_require = dict(test=tests_require, debug=debug_requires),
entry_points={
'paste.app_factory': [
'main = grokcore.startup:application_factory',
Expand Down
28 changes: 26 additions & 2 deletions src/grokcore/startup/README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -260,11 +260,34 @@ API Documentation
>>> import shutil
>>> shutil.rmtree(temp_dir)

``interactive_debug_prompt(zope_conf_path)``
``get_debugger(zope_conf_path)``
--------------------------------------------

Get an interactive console with a debugging shell started.

`grokcore.startup` provides two different debuggers currently: a
plain one based on `zope.app.debug` and a more powerful `IPython`_
debugger. The IPython debugger is automatically enabled if you have
IPython available in the environment.

You can explicitly enable the IPython_ debugger by stating::

grokcore.startup [debug]

in the install requirements of your `setup.py`, probably adding only
``[debug]`` to an already existing entry for
`grokcore.startup`. Don't forget to rerun `buildout` afterwards.

You can explicitly require one or the other debugger by calling::

grokcore.startup.startup.interactive_debug_prompt(zope_conf)

or::

grokcore.startup.debug.ipython_debug_prompt(zope_conf)

in the ``[interactive_debugger]`` section of your ``buildout.cfg``.

>>> import zope.app.appsetup.appsetup
>>> # Ugh - allow a reconfiguration of an app.
>>> zope.app.appsetup.appsetup._configured = False
Expand Down Expand Up @@ -313,7 +336,7 @@ API Documentation
... pprint(__file__)
... pprint(__name__)""")
>>>
>>> sys.argv = ['interactive_debug_prompt', script]
>>> sys.argv = ['get_debugger', script]
>>> from grokcore.startup import interactive_debug_prompt
>>> try:
... interactive_debug_prompt(zope_conf=zopeconf)
Expand Down Expand Up @@ -347,3 +370,4 @@ API Documentation
.. _WSGI: http://www.wsgi.org/wsgi/
.. _WSGIPublisherApplication: http://apidoc.zope.org/++apidoc++/Code/zope/app/wsgi/WSGIPublisherApplication/index.html
.. _zc.buildout: http://pypi.python.org/pypi/zc.buildout
.. _ipython: http://ipython.org/
8 changes: 5 additions & 3 deletions src/grokcore/startup/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#
##############################################################################
# Make this a package.
from grokcore.startup.startup import (application_factory,
debug_application_factory,
interactive_debug_prompt)
from grokcore.startup.startup import (
application_factory,
debug_application_factory,
interactive_debug_prompt,
)
209 changes: 209 additions & 0 deletions src/grokcore/startup/debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
##############################################################################
#
# Copyright (c) 2006-2012 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 sys
import os.path
import textwrap

import transaction
import zope.app.wsgi
import zope.app.debug
from pprint import pprint
from zope.securitypolicy.zopepolicy import settingsForObject
from zope.component import getUtility, getMultiAdapter

from IPython.frontend.terminal.embed import InteractiveShellEmbed
shell = InteractiveShellEmbed()


PATH_SEP = '/'

class GrokDebug(object):

def __init__(self, zope_conf='parts/etc/zope.debug.conf'):
db = zope.app.wsgi.config(zope_conf)
debugger = zope.app.debug.Debugger.fromDatabase(db)
self.app = debugger
self.root = debugger.root()
self.context = self.root

def get_start_context(self, path):
if path.startswith(PATH_SEP):
context = self.root
else:
# relative path
context = self.context
return context

def _get_object_names(self, context):
return [obj.__name__ for obj in context.values()]

def ns(self):
"""Return namespace dictionary.
To be used for updating namespace of commands available
for user in shell.
"""
return dict(lsg=self.ls,
cdg=self.cd,
pwdg=self.pwd,
app=self.app,
root=self.root,
ctx=self.ctx,
sec=self.get_security_settings,
gu=getUtility,
gma=getMultiAdapter,
sync=self.sync,
pby=self.providedBy,
commit=transaction.commit)

def update_ns(self):
shell.user_ns.update(self.ns())

def get_security_settings(self, path):
pprint(settingsForObject(get_context_by_path(
self.get_start_context(path), path)))

def sync(self):
self.root._p_jar.sync()

def ls(self, path=None):
"""List objects.
This command is bound to `lsg` in IPython shell.
Without `path` parameter list objects in current container,
which is available as `ctx` from IPython shell.
`path` can be relative or absolute.
To use autocompletion of path command should be invoked
with prepended semicolon in ipython shell as
;lsg /path
"""
if path is None:
return self._get_object_names(self.context)

context = get_context_by_path(self.get_start_context(path), path)
return self._get_object_names(context)


def cd(self, path):
"""cd to specified path.
Bound to `cdg` in IPython shell.
`path` can be relative or absolute.
To use autocompletion of path command should be invoked
with prepended semicolon in ipython shell as
;cdg /path
"""
if path.strip() == '..':
self.context = self.context.__parent__
self.update_ns()
return self.pwd

# cd
self.context = get_context_by_path(self.get_start_context(path), path)
self.update_ns()
return self.pwd

@property
def pwd(self):
"""Print absolute path to current context object.
Bound to `pwdg` in IPython shell
"""
res = []
obj = self.context
while obj is not None:
name = obj.__name__
if name is not None and name:
res.append(name)
obj = obj.__parent__

if not res:
return PATH_SEP

res = PATH_SEP.join(reversed(res))
if not res.startswith(PATH_SEP):
return PATH_SEP + res

return res

@property
def ctx(self):
"""Return current context object.
Bound to `ctx` in IPython shell
"""
return self.context

def providedBy(self, obj=None):
if not obj:
obj = self.ctx
return list(zope.interface.providedBy(obj))

def get_context_by_path(context, path):
for name in (p for p in path.split(PATH_SEP) if p):
context = context[name]
return context

def path_completer(self, event):
"""TAB path completer for `cdg` and `lsg` commands."""
relpath = event.symbol

context = grokd.get_start_context(relpath)

# ends with '/'
if relpath.endswith(PATH_SEP):
context = get_context_by_path(context, relpath)
return [relpath+obj.__name__ for obj in context.values()]

head, tail = os.path.split(relpath)
if head and not head.endswith(PATH_SEP):
head += PATH_SEP
context = get_context_by_path(context, head)

return [head+obj.__name__ for obj in context.values()
if obj.__name__.startswith(tail)]


def ipython_debug_prompt(zope_conf):
grokd = GrokDebug(zope_conf)
banner = textwrap.dedent(
"""\
IPython shell for Grok.
Bound object names:
-------------------
root
ctx
Bound command names:
--------------------
cdg / ;cdg
lsg / ;lsg
sec / ;sec
gu / ;gu
gma / ;gma
pby (providedBy)
pwdg
sync
commit
""")

shell.user_ns.update(grokd.ns())
shell.banner2=banner
shell.set_hook('complete_command', path_completer, re_key='.*cdg|.*lsg')
shell(local_ns=grokd.ns())
15 changes: 12 additions & 3 deletions src/grokcore/startup/startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ def do_not_reraise_exception(context):
# Return the created application
return app

def interactive_debug_prompt(
zope_conf=os.path.join('parts', 'etc', 'zope.conf')):

def _classic_debug_prompt(zope_conf):
db = zope.app.wsgi.config(zope_conf)
debugger = zope.app.debug.Debugger.fromDatabase(db)
globals_ = {
Expand Down Expand Up @@ -67,3 +65,14 @@ def interactive_debug_prompt(
"The 'app' variable contains the Debugger, 'app.publish(path)' "
"simulates a request.")
code.interact(banner=banner, local=globals_)

def _ipython_debug_prompt(zope_conf):
from grokcore.startup.debug import ipython_debug_prompt
return ipython_debug_prompt(zope_conf)

def interactive_debug_prompt(zope_conf):
try:
import IPython
except ImportError:
return _classic_debug_prompt(zope_conf)
return _ipython_debug_prompt(zope_conf)

0 comments on commit b0a1290

Please sign in to comment.