diff --git a/IPython/config/loader.py b/IPython/config/loader.py index 5b01462..328cd76 100644 --- a/IPython/config/loader.py +++ b/IPython/config/loader.py @@ -298,14 +298,12 @@ NoConfigDefault = NoConfigDefault() class ArgParseConfigLoader(CommandLineConfigLoader): - # arguments = [(('-f','--file'),dict(type=str,dest='file'))] - arguments = () - - def __init__(self, argv=None, *args, **kw): + def __init__(self, argv=None, arguments=(), *args, **kw): """Create a config loader for use with argparse. - With the exception of argv, other args and kwargs arguments here are - passed onto the constructor of :class:`argparse.ArgumentParser`. + With the exception of ``argv`` and ``arguments``, other args and kwargs + arguments here are passed onto the constructor of + :class:`argparse.ArgumentParser`. Parameters ---------- @@ -313,11 +311,16 @@ class ArgParseConfigLoader(CommandLineConfigLoader): argv : optional, list If given, used to read command-line arguments from, otherwise sys.argv[1:] is used. + + arguments : optional, tuple + Description of valid command-line arguments, to be called in sequence + with parser.add_argument() to configure the parser. """ super(CommandLineConfigLoader, self).__init__() if argv == None: argv = sys.argv[1:] self.argv = argv + self.arguments = arguments self.args = args self.kw = kw diff --git a/IPython/config/tests/test_loader.py b/IPython/config/tests/test_loader.py index ea06dab..487db8b 100755 --- a/IPython/config/tests/test_loader.py +++ b/IPython/config/tests/test_loader.py @@ -66,15 +66,13 @@ class TestArgParseCL(TestCase): def test_basic(self): - class MyLoader(ArgParseConfigLoader): - arguments = ( + arguments = ( (('-f','--foo'), dict(dest='Global.foo', type=str)), (('-b',), dict(dest='MyClass.bar', type=int)), (('-n',), dict(dest='n', action='store_true')), (('Global.bam',), dict(type=str)) ) - - cl = MyLoader() + cl = ArgParseConfigLoader(arguments=arguments) config = cl.load_config('-f hi -b 10 -n wow'.split()) self.assertEquals(config.Global.foo, 'hi') self.assertEquals(config.MyClass.bar, 10) diff --git a/IPython/core/application.py b/IPython/core/application.py index c3dddb7..ad6472b 100755 --- a/IPython/core/application.py +++ b/IPython/core/application.py @@ -33,7 +33,7 @@ import logging import os import sys -from IPython.core import release +from IPython.core import release, crashhandler from IPython.utils.genutils import get_ipython_dir, get_ipython_package_dir from IPython.config.loader import ( PyFileConfigLoader, @@ -77,6 +77,29 @@ class ApplicationError(Exception): pass +app_cl_args = ( + (('--ipython-dir', ), dict( + dest='Global.ipython_dir',type=unicode, + help='Set to override default location of Global.ipython_dir.', + default=NoConfigDefault, + metavar='Global.ipython_dir') ), + (('-p', '--profile',), dict( + dest='Global.profile',type=unicode, + help='The string name of the ipython profile to be used.', + default=NoConfigDefault, + metavar='Global.profile') ), + (('--log-level',), dict( + dest="Global.log_level",type=int, + help='Set the log level (0,10,20,30,40,50). Default is 30.', + default=NoConfigDefault, + metavar='Global.log_level')), + (('--config-file',), dict( + dest='Global.config_file',type=unicode, + help='Set the config file name to override default.', + default=NoConfigDefault, + metavar='Global.config_file')), + ) + class Application(object): """Load a config, construct components and set them running.""" @@ -94,11 +117,17 @@ class Application(object): ipython_dir = None #: A reference to the argv to be used (typically ends up being sys.argv[1:]) argv = None + #: Default command line arguments. Subclasses should create a new tuple + #: that *includes* these. + cl_arguments = app_cl_args # Private attributes _exiting = False _initialized = False + # Class choices for things that will be instantiated at runtime. + _CrashHandler = crashhandler.CrashHandler + def __init__(self, argv=None): self.argv = sys.argv[1:] if argv is None else argv self.init_logger() @@ -125,39 +154,49 @@ class Application(object): if self._initialized: return - - self.attempt(self.create_default_config) + + # The first part is protected with an 'attempt' wrapper, that will log + # failures with the basic system traceback machinery. Once our crash + # handler is in place, we can let any subsequent exception propagate, + # as our handler will log it with much better detail than the default. + self.attempt(self.create_crash_handler) + self.create_default_config() self.log_default_config() self.set_default_config_log_level() - self.attempt(self.pre_load_command_line_config) - self.attempt(self.load_command_line_config, action='abort') + self.pre_load_command_line_config() + self.load_command_line_config() self.set_command_line_config_log_level() - self.attempt(self.post_load_command_line_config) + self.post_load_command_line_config() self.log_command_line_config() - self.attempt(self.find_ipython_dir) - self.attempt(self.find_resources) - self.attempt(self.find_config_file_name) - self.attempt(self.find_config_file_paths) - self.attempt(self.pre_load_file_config) - self.attempt(self.load_file_config) + self.find_ipython_dir() + self.find_resources() + self.find_config_file_name() + self.find_config_file_paths() + self.pre_load_file_config() + self.load_file_config() self.set_file_config_log_level() - self.attempt(self.post_load_file_config) + self.post_load_file_config() self.log_file_config() - self.attempt(self.merge_configs) + self.merge_configs() self.log_master_config() - self.attempt(self.pre_construct) - self.attempt(self.construct) - self.attempt(self.post_construct) + self.pre_construct() + self.construct() + self.post_construct() self._initialized = True def start(self): self.initialize() - self.attempt(self.start_app) + self.start_app() #------------------------------------------------------------------------- # Various stages of Application creation #------------------------------------------------------------------------- + def create_crash_handler(self): + """Create a crash handler, typically setting sys.excepthook to it.""" + self.crash_handler = self._CrashHandler(self, self.name) + sys.excepthook = self.crash_handler + def create_default_config(self): """Create defaults that can't be set elsewhere. @@ -185,10 +224,9 @@ class Application(object): def create_command_line_config(self): """Create and return a command line config loader.""" - return BaseAppArgParseConfigLoader(self.argv, - description=self.description, - version=release.version - ) + return ArgParseConfigLoader(self.argv, self.cl_arguments, + description=self.description, + version=release.version) def pre_load_command_line_config(self): """Do actions just before loading the command line config.""" @@ -384,7 +422,10 @@ class Application(object): raise except: if action == 'abort': + self.log.critical("Aborting application: %s" % self.name, + exc_info=True) self.abort() + raise elif action == 'exit': self.exit(0) diff --git a/IPython/core/crashhandler.py b/IPython/core/crashhandler.py index ad8d340..d0de855 100644 --- a/IPython/core/crashhandler.py +++ b/IPython/core/crashhandler.py @@ -28,10 +28,8 @@ from IPython.core import release from IPython.core import ultratb from IPython.external.Itpl import itpl -from IPython.utils.genutils import * - #**************************************************************************** -class CrashHandler: +class CrashHandler(object): """Customizable crash handlers for IPython-based systems. Instances of this class provide a __call__ method which can be used as a @@ -41,15 +39,15 @@ class CrashHandler: """ - def __init__(self,IP,app_name,contact_name,contact_email, - bug_tracker,crash_report_fname, + def __init__(self,app, app_name, contact_name=None, contact_email=None, + bug_tracker=None, crash_report_fname='CrashReport.txt', show_crash_traceback=True): """New crash handler. Inputs: - - IP: a running IPython instance, which will be queried at crash time - for internal information. + - app: a running application instance, which will be queried at crash + time for internal information. - app_name: a string containing the name of your application. @@ -77,13 +75,14 @@ class CrashHandler: """ # apply args into instance - self.IP = IP # IPython instance + self.app = app self.app_name = app_name self.contact_name = contact_name self.contact_email = contact_email self.bug_tracker = bug_tracker self.crash_report_fname = crash_report_fname self.show_crash_traceback = show_crash_traceback + self.section_sep = '\n\n'+'*'*75+'\n\n' # Hardcoded defaults, which can be overridden either by subclasses or # at runtime for the instance. @@ -124,7 +123,7 @@ $self.bug_tracker #color_scheme = 'Linux' # dbg try: - rptdir = self.IP.ipython_dir + rptdir = self.app.ipython_dir except: rptdir = os.getcwd() if not os.path.isdir(rptdir): @@ -134,7 +133,7 @@ $self.bug_tracker # properly expanded out in the user message template self.crash_report_fname = report_name TBhandler = ultratb.VerboseTB(color_scheme=color_scheme, - long_header=1) + long_header=1) traceback = TBhandler.text(etype,evalue,etb,context=31) # print traceback to screen @@ -159,70 +158,62 @@ $self.bug_tracker def make_report(self,traceback): """Return a string containing a crash report.""" - - sec_sep = '\n\n'+'*'*75+'\n\n' - + import platform + + sec_sep = self.section_sep + report = [] rpt_add = report.append rpt_add('*'*75+'\n\n'+'IPython post-mortem report\n\n') - rpt_add('IPython version: %s \n\n' % release.version) - rpt_add('BZR revision : %s \n\n' % release.revision) - rpt_add('Platform info : os.name -> %s, sys.platform -> %s' % + rpt_add('IPython version: %s \n' % release.version) + rpt_add('BZR revision : %s \n' % release.revision) + rpt_add('Platform info : os.name -> %s, sys.platform -> %s\n' % (os.name,sys.platform) ) - rpt_add(sec_sep+'Current user configuration structure:\n\n') - rpt_add(pformat(self.IP.dict())) - rpt_add(sec_sep+'Crash traceback:\n\n' + traceback) + rpt_add(' : %s\n' % platform.platform()) + rpt_add('Python info : %s\n' % sys.version) + try: - rpt_add(sec_sep+"History of session input:") - for line in self.IP.user_ns['_ih']: - rpt_add(line) - rpt_add('\n*** Last line of input (may not be in above history):\n') - rpt_add(self.IP._last_input_line+'\n') + config = pformat(self.app.config) + rpt_add(sec_sep+'Current user configuration structure:\n\n') + rpt_add(config) except: pass + rpt_add(sec_sep+'Crash traceback:\n\n' + traceback) return ''.join(report) + class IPythonCrashHandler(CrashHandler): """sys.excepthook for IPython itself, leaves a detailed report on disk.""" - def __init__(self,IP): + def __init__(self, app, app_name='IPython'): # Set here which of the IPython authors should be listed as contact AUTHOR_CONTACT = 'Fernando' # Set argument defaults - app_name = 'IPython' bug_tracker = 'https://bugs.launchpad.net/ipython/+filebug' contact_name,contact_email = release.authors[AUTHOR_CONTACT][:2] crash_report_fname = 'IPython_crash_report.txt' # Call parent constructor - CrashHandler.__init__(self,IP,app_name,contact_name,contact_email, + CrashHandler.__init__(self,app,app_name,contact_name,contact_email, bug_tracker,crash_report_fname) def make_report(self,traceback): """Return a string containing a crash report.""" - sec_sep = '\n\n'+'*'*75+'\n\n' - - report = [] + sec_sep = self.section_sep + # Start with parent report + report = [super(IPythonCrashHandler, self).make_report(traceback)] + # Add interactive-specific info we may have rpt_add = report.append - - rpt_add('*'*75+'\n\n'+'IPython post-mortem report\n\n') - rpt_add('IPython version: %s \n\n' % release.version) - rpt_add('BZR revision : %s \n\n' % release.revision) - rpt_add('Platform info : os.name -> %s, sys.platform -> %s' % - (os.name,sys.platform) ) - rpt_add(sec_sep+'Current user configuration structure:\n\n') - # rpt_add(pformat(self.IP.dict())) - rpt_add(sec_sep+'Crash traceback:\n\n' + traceback) try: rpt_add(sec_sep+"History of session input:") - for line in self.IP.user_ns['_ih']: + for line in self.app.shell.user_ns['_ih']: rpt_add(line) rpt_add('\n*** Last line of input (may not be in above history):\n') - rpt_add(self.IP._last_input_line+'\n') + rpt_add(self.app.shell._last_input_line+'\n') except: pass diff --git a/IPython/core/ipapp.py b/IPython/core/ipapp.py index af76e6e..8384838 100755 --- a/IPython/core/ipapp.py +++ b/IPython/core/ipapp.py @@ -28,6 +28,7 @@ import logging import os import sys +from IPython.core import crashhandler from IPython.core import release from IPython.core.application import Application, BaseAppArgParseConfigLoader from IPython.core.error import UsageError @@ -280,19 +281,18 @@ cl_args = ( ) -class IPythonAppCLConfigLoader(BaseAppArgParseConfigLoader): - - arguments = cl_args - - default_config_file_name = u'ipython_config.py' - class IPythonApp(Application): name = u'ipython' description = 'IPython: an enhanced interactive Python shell.' config_file_name = default_config_file_name + cl_arguments = Application.cl_arguments + cl_args + + # Private and configuration attributes + _CrashHandler = crashhandler.IPythonCrashHandler + def __init__(self, argv=None, **shell_params): """Create a new IPythonApp. @@ -309,6 +309,7 @@ class IPythonApp(Application): super(IPythonApp, self).__init__(argv) self.shell_params = shell_params + def create_default_config(self): super(IPythonApp, self).create_default_config() # Eliminate multiple lookups @@ -340,13 +341,6 @@ class IPythonApp(Application): Global.wthread = False Global.gthread = False - def create_command_line_config(self): - """Create and return a command line config loader.""" - return IPythonAppCLConfigLoader(self.argv, - description=self.description, - version=release.version - ) - def load_file_config(self): if hasattr(self.command_line_config.Global, 'quick'): if self.command_line_config.Global.quick: diff --git a/IPython/core/iplib.py b/IPython/core/iplib.py index eea73bf..5965b19 100644 --- a/IPython/core/iplib.py +++ b/IPython/core/iplib.py @@ -1167,37 +1167,14 @@ class InteractiveShell(Component, Magic): color_scheme='NoColor', tb_offset = 1) - # IPython itself shouldn't crash. This will produce a detailed - # post-mortem if it does. But we only install the crash handler for - # non-threaded shells, the threaded ones use a normal verbose reporter - # and lose the crash handler. This is because exceptions in the main - # thread (such as in GUI code) propagate directly to sys.excepthook, - # and there's no point in printing crash dumps for every user exception. - if self.isthreaded: - ipCrashHandler = ultratb.FormattedTB() - else: - from IPython.core import crashhandler - ipCrashHandler = crashhandler.IPythonCrashHandler(self) - self.set_crash_handler(ipCrashHandler) + # The instance will store a pointer to the system-wide exception hook, + # so that runtime code (such as magics) can access it. This is because + # during the read-eval loop, it may get temporarily overwritten. + self.sys_excepthook = sys.excepthook # and add any custom exception handlers the user may have specified self.set_custom_exc(*custom_exceptions) - def set_crash_handler(self, crashHandler): - """Set the IPython crash handler. - - This must be a callable with a signature suitable for use as - sys.excepthook.""" - - # Install the given crash handler as the Python exception hook - sys.excepthook = crashHandler - - # The instance will store a pointer to this, so that runtime code - # (such as magics) can access it. This is because during the - # read-eval loop, it gets temporarily overwritten (to deal with GUI - # frameworks). - self.sys_excepthook = sys.excepthook - def set_custom_exc(self,exc_tuple,handler): """set_custom_exc(exc_tuple,handler) @@ -1880,7 +1857,7 @@ class InteractiveShell(Component, Magic): if (self.SyntaxTB.last_syntax_error and self.autoedit_syntax): self.edit_syntax_error() - + # We are off again... __builtin__.__dict__['__IPYTHON__active'] -= 1 diff --git a/IPython/frontend/prefilterfrontend.py b/IPython/frontend/prefilterfrontend.py index 239435c..6cb2997 100644 --- a/IPython/frontend/prefilterfrontend.py +++ b/IPython/frontend/prefilterfrontend.py @@ -77,12 +77,6 @@ class PrefilterFrontEnd(LineFrontEndBase): """ if argv is None: argv = ['--no-banner'] - # This is a hack to avoid the IPython exception hook to trigger - # on exceptions (https://bugs.launchpad.net/bugs/337105) - # XXX: This is horrible: module-leve monkey patching -> side - # effects. - from IPython.core import iplib - iplib.InteractiveShell.isthreaded = True LineFrontEndBase.__init__(self, *args, **kwargs) self.shell.output_trap = RedirectorOutputTrap( diff --git a/IPython/frontend/tests/test_prefilterfrontend.py b/IPython/frontend/tests/test_prefilterfrontend.py index ebe100c..5e7504b 100644 --- a/IPython/frontend/tests/test_prefilterfrontend.py +++ b/IPython/frontend/tests/test_prefilterfrontend.py @@ -88,9 +88,6 @@ def isolate_ipython0(func): del user_ns[k] for k in new_globals: del user_global_ns[k] - # Undo the hack at creation of PrefilterFrontEnd - from IPython.core import iplib - iplib.InteractiveShell.isthreaded = False return out my_func.__name__ = func.__name__