application.py
488 lines
| 18.4 KiB
| text/x-python
|
PythonLexer
Brian Granger
|
r2185 | # encoding: utf-8 | ||
""" | ||||
Brian Granger
|
r2301 | An application for IPython. | ||
All top-level applications should use the classes in this module for | ||||
handling configuration and creating componenets. | ||||
The job of an :class:`Application` is to create the master configuration | ||||
object and then create the components, passing the config to them. | ||||
Brian Granger
|
r2185 | |||
Authors: | ||||
* Brian Granger | ||||
* Fernando Perez | ||||
Notes | ||||
----- | ||||
""" | ||||
#----------------------------------------------------------------------------- | ||||
# Copyright (C) 2008-2009 The IPython Development Team | ||||
# | ||||
# Distributed under the terms of the BSD License. The full license is in | ||||
# the file COPYING, distributed as part of this software. | ||||
#----------------------------------------------------------------------------- | ||||
#----------------------------------------------------------------------------- | ||||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
Brian Granger
|
r2252 | import logging | ||
Brian Granger
|
r2200 | import os | ||
Brian Granger
|
r2185 | import sys | ||
Brian Granger
|
r2245 | |||
Fernando Perez
|
r2403 | from IPython.core import release, crashhandler | ||
Fernando Perez
|
r2357 | from IPython.utils.genutils import get_ipython_dir, get_ipython_package_dir | ||
Brian Granger
|
r2200 | from IPython.config.loader import ( | ||
Brian Granger
|
r2245 | PyFileConfigLoader, | ||
ArgParseConfigLoader, | ||||
Config, | ||||
Brian Granger
|
r2200 | ) | ||
Brian Granger
|
r2185 | |||
#----------------------------------------------------------------------------- | ||||
# Classes and functions | ||||
#----------------------------------------------------------------------------- | ||||
class ApplicationError(Exception): | ||||
pass | ||||
Fernando Perez
|
r2403 | app_cl_args = ( | ||
Fernando Perez
|
r2427 | (('--ipython-dir', ), dict( | ||
dest='Global.ipython_dir',type=unicode, | ||||
help= | ||||
"""Set to override default location of the IPython directory | ||||
IPYTHON_DIR, stored as Global.ipython_dir. This can also be specified | ||||
through the environment variable IPYTHON_DIR.""", | ||||
metavar='Global.ipython_dir') ), | ||||
(('-p', '--profile',), dict( | ||||
dest='Global.profile',type=unicode, | ||||
help= | ||||
"""The string name of the ipython profile to be used. Assume that your | ||||
config file is ipython_config-<name>.py (looks in current dir first, | ||||
then in IPYTHON_DIR). This is a quick way to keep and load multiple | ||||
config files for different tasks, especially if include your basic one | ||||
in your more specialized ones. You can keep a basic | ||||
IPYTHON_DIR/ipython_config.py file and then have other 'profiles' which | ||||
include this one and load extra things for particular tasks.""", | ||||
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.', | ||||
metavar='Global.log_level')), | ||||
(('--config-file',), dict( | ||||
dest='Global.config_file',type=unicode, | ||||
help= | ||||
"""Set the config file name to override default. Normally IPython | ||||
loads ipython_config.py (from current directory) or | ||||
IPYTHON_DIR/ipython_config.py. If the loading of your config file | ||||
fails, IPython starts with a bare bones configuration (no modules | ||||
loaded at all).""", | ||||
metavar='Global.config_file')), | ||||
Fernando Perez
|
r2403 | ) | ||
Brian Granger
|
r2185 | class Application(object): | ||
Fernando Perez
|
r2439 | """Load a config, construct components and set them running. | ||
The configuration of an application can be done via four different Config | ||||
objects, which are loaded and ultimately merged into a single one used from | ||||
that point on by the app. These are: | ||||
1. default_config: internal defaults, implemented in code. | ||||
2. file_config: read from the filesystem. | ||||
3. command_line_config: read from the system's command line flags. | ||||
4. constructor_config: passed parametrically to the constructor. | ||||
During initialization, 3 is actually read before 2, since at the | ||||
command-line one may override the location of the file to be read. But the | ||||
above is the order in which the merge is made. | ||||
There is a final config object can be created and passed to the | ||||
constructor: override_config. If it exists, this completely overrides the | ||||
configs 2-4 above (the default is still used to ensure that all needed | ||||
fields at least are created). This makes it easier to create | ||||
parametrically (e.g. in testing or sphinx plugins) objects with a known | ||||
configuration, that are unaffected by whatever arguments may be present in | ||||
sys.argv or files in the user's various directories. | ||||
Brian Granger
|
r2200 | """ | ||
Brian Granger
|
r2185 | |||
Brian Granger
|
r2328 | name = u'ipython' | ||
Brian Granger
|
r2296 | description = 'IPython: an enhanced interactive Python shell.' | ||
Fernando Perez
|
r2427 | #: usage message printed by argparse. If None, auto-generate | ||
usage = None | ||||
Brian Granger
|
r2328 | config_file_name = u'ipython_config.py' | ||
Fernando Perez
|
r2439 | #: Track the default and actual separately because some messages are | ||
#: only printed if we aren't using the default. | ||||
Fernando Perez
|
r2357 | default_config_file_name = config_file_name | ||
Brian Granger
|
r2294 | default_log_level = logging.WARN | ||
Fernando Perez
|
r2439 | #: Set by --profile option | ||
Fernando Perez
|
r2357 | profile_name = None | ||
Fernando Perez
|
r2387 | #: User's ipython directory, typically ~/.ipython/ | ||
Fernando Perez
|
r2357 | ipython_dir = None | ||
Fernando Perez
|
r2439 | #: internal defaults, implemented in code. | ||
default_config = None | ||||
#: read from the filesystem | ||||
file_config = None | ||||
#: read from the system's command line flags | ||||
command_line_config = None | ||||
#: passed parametrically to the constructor. | ||||
constructor_config = None | ||||
#: final override, if given supercedes file/command/constructor configs | ||||
override_config = None | ||||
Fernando Perez
|
r2391 | #: A reference to the argv to be used (typically ends up being sys.argv[1:]) | ||
argv = None | ||||
Fernando Perez
|
r2403 | #: Default command line arguments. Subclasses should create a new tuple | ||
#: that *includes* these. | ||||
cl_arguments = app_cl_args | ||||
Fernando Perez
|
r2357 | |||
Fernando Perez
|
r2439 | #: extra arguments computed by the command-line loader | ||
extra_args = None | ||||
Fernando Perez
|
r2357 | # Private attributes | ||
_exiting = False | ||||
Fernando Perez
|
r2392 | _initialized = False | ||
Brian Granger
|
r2185 | |||
Fernando Perez
|
r2403 | # Class choices for things that will be instantiated at runtime. | ||
_CrashHandler = crashhandler.CrashHandler | ||||
Fernando Perez
|
r2439 | def __init__(self, argv=None, constructor_config=None, override_config=None): | ||
Fernando Perez
|
r2391 | self.argv = sys.argv[1:] if argv is None else argv | ||
Fernando Perez
|
r2439 | self.constructor_config = constructor_config | ||
self.override_config = override_config | ||||
Brian Granger
|
r2252 | self.init_logger() | ||
def init_logger(self): | ||||
self.log = logging.getLogger(self.__class__.__name__) | ||||
# This is used as the default until the command line arguments are read. | ||||
Brian Granger
|
r2294 | self.log.setLevel(self.default_log_level) | ||
Brian Granger
|
r2252 | self._log_handler = logging.StreamHandler() | ||
self._log_formatter = logging.Formatter("[%(name)s] %(message)s") | ||||
self._log_handler.setFormatter(self._log_formatter) | ||||
self.log.addHandler(self._log_handler) | ||||
def _set_log_level(self, level): | ||||
self.log.setLevel(level) | ||||
def _get_log_level(self): | ||||
return self.log.level | ||||
log_level = property(_get_log_level, _set_log_level) | ||||
Brian Granger
|
r2185 | |||
Fernando Perez
|
r2392 | def initialize(self): | ||
Fernando Perez
|
r2439 | """Initialize the application. | ||
Loads all configuration information and sets all application state, but | ||||
does not start any relevant processing (typically some kind of event | ||||
loop). | ||||
Once this method has been called, the application is flagged as | ||||
initialized and the method becomes a no-op.""" | ||||
Fernando Perez
|
r2392 | |||
if self._initialized: | ||||
return | ||||
Fernando Perez
|
r2403 | |||
# 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) | ||||
Fernando Perez
|
r2439 | |||
# Configuration phase | ||||
# Default config (internally hardwired in application code) | ||||
Fernando Perez
|
r2403 | self.create_default_config() | ||
Brian Granger
|
r2294 | self.log_default_config() | ||
self.set_default_config_log_level() | ||||
Fernando Perez
|
r2439 | |||
if self.override_config is None: | ||||
# Command-line config | ||||
self.pre_load_command_line_config() | ||||
self.load_command_line_config() | ||||
self.set_command_line_config_log_level() | ||||
self.post_load_command_line_config() | ||||
self.log_command_line_config() | ||||
# Find resources needed for filesystem access, using information from | ||||
# the above two | ||||
Fernando Perez
|
r2403 | self.find_ipython_dir() | ||
self.find_resources() | ||||
self.find_config_file_name() | ||||
self.find_config_file_paths() | ||||
Fernando Perez
|
r2439 | |||
if self.override_config is None: | ||||
# File-based config | ||||
self.pre_load_file_config() | ||||
self.load_file_config() | ||||
self.set_file_config_log_level() | ||||
self.post_load_file_config() | ||||
self.log_file_config() | ||||
# Merge all config objects into a single one the app can then use | ||||
Fernando Perez
|
r2403 | self.merge_configs() | ||
Brian Granger
|
r2294 | self.log_master_config() | ||
Fernando Perez
|
r2439 | |||
# Construction phase | ||||
Fernando Perez
|
r2403 | self.pre_construct() | ||
self.construct() | ||||
self.post_construct() | ||||
Fernando Perez
|
r2439 | |||
# Done, flag as such and | ||||
Fernando Perez
|
r2392 | self._initialized = True | ||
Brian Granger
|
r2187 | def start(self): | ||
"""Start the application.""" | ||||
Fernando Perez
|
r2392 | self.initialize() | ||
Fernando Perez
|
r2403 | self.start_app() | ||
Brian Granger
|
r2187 | |||
#------------------------------------------------------------------------- | ||||
# Various stages of Application creation | ||||
#------------------------------------------------------------------------- | ||||
Fernando Perez
|
r2403 | 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 | ||||
Brian Granger
|
r2200 | def create_default_config(self): | ||
Brian Granger
|
r2253 | """Create defaults that can't be set elsewhere. | ||
For the most part, we try to set default in the class attributes | ||||
of Components. But, defaults the top-level Application (which is | ||||
Dav Clark
|
r2384 | not a HasTraits or Component) are not set in this way. Instead | ||
Brian Granger
|
r2253 | we set them here. The Global section is for variables like this that | ||
don't belong to a particular component. | ||||
""" | ||||
Fernando Perez
|
r2362 | c = Config() | ||
c.Global.ipython_dir = get_ipython_dir() | ||||
c.Global.log_level = self.log_level | ||||
self.default_config = c | ||||
Brian Granger
|
r2294 | |||
def log_default_config(self): | ||||
Brian Granger
|
r2252 | self.log.debug('Default config loaded:') | ||
self.log.debug(repr(self.default_config)) | ||||
Brian Granger
|
r2200 | |||
Brian Granger
|
r2294 | def set_default_config_log_level(self): | ||
try: | ||||
self.log_level = self.default_config.Global.log_level | ||||
except AttributeError: | ||||
# Fallback to the default_log_level class attribute | ||||
pass | ||||
Brian Granger
|
r2187 | def create_command_line_config(self): | ||
Brian Granger
|
r2200 | """Create and return a command line config loader.""" | ||
Fernando Perez
|
r2403 | return ArgParseConfigLoader(self.argv, self.cl_arguments, | ||
description=self.description, | ||||
Fernando Perez
|
r2427 | version=release.version, | ||
usage=self.usage, | ||||
) | ||||
Brian Granger
|
r2200 | |||
def pre_load_command_line_config(self): | ||||
"""Do actions just before loading the command line config.""" | ||||
pass | ||||
Brian Granger
|
r2187 | |||
Brian Granger
|
r2200 | def load_command_line_config(self): | ||
Brian Granger
|
r2294 | """Load the command line config.""" | ||
Brian Granger
|
r2200 | loader = self.create_command_line_config() | ||
self.command_line_config = loader.load_config() | ||||
Brian Granger
|
r2253 | self.extra_args = loader.get_extra_args() | ||
Brian Granger
|
r2252 | |||
Brian Granger
|
r2294 | def set_command_line_config_log_level(self): | ||
Brian Granger
|
r2200 | try: | ||
Brian Granger
|
r2252 | self.log_level = self.command_line_config.Global.log_level | ||
Brian Granger
|
r2200 | except AttributeError: | ||
Brian Granger
|
r2294 | pass | ||
Brian Granger
|
r2200 | |||
def post_load_command_line_config(self): | ||||
"""Do actions just after loading the command line config.""" | ||||
Brian Granger
|
r2185 | pass | ||
Brian Granger
|
r2294 | def log_command_line_config(self): | ||
self.log.debug("Command line config loaded:") | ||||
self.log.debug(repr(self.command_line_config)) | ||||
Brian Granger
|
r2322 | def find_ipython_dir(self): | ||
Brian Granger
|
r2200 | """Set the IPython directory. | ||
Fernando Perez
|
r2357 | This sets ``self.ipython_dir``, but the actual value that is passed to | ||
the application is kept in either ``self.default_config`` or | ||||
Brian Granger
|
r2322 | ``self.command_line_config``. This also adds ``self.ipython_dir`` to | ||
Fernando Perez
|
r2357 | ``sys.path`` so config files there can be referenced by other config | ||
Brian Granger
|
r2200 | files. | ||
""" | ||||
try: | ||||
Brian Granger
|
r2322 | self.ipython_dir = self.command_line_config.Global.ipython_dir | ||
Brian Granger
|
r2200 | except AttributeError: | ||
Brian Granger
|
r2322 | self.ipython_dir = self.default_config.Global.ipython_dir | ||
sys.path.append(os.path.abspath(self.ipython_dir)) | ||||
if not os.path.isdir(self.ipython_dir): | ||||
os.makedirs(self.ipython_dir, mode=0777) | ||||
self.log.debug("IPYTHON_DIR set to: %s" % self.ipython_dir) | ||||
Brian Granger
|
r2200 | |||
Brian Granger
|
r2303 | def find_resources(self): | ||
"""Find other resources that need to be in place. | ||||
Things like cluster directories need to be in place to find the | ||||
config file. These happen right after the IPython directory has | ||||
been set. | ||||
""" | ||||
pass | ||||
Brian Granger
|
r2200 | |||
def find_config_file_name(self): | ||||
"""Find the config file name for this application. | ||||
Brian Granger
|
r2294 | This must set ``self.config_file_name`` to the filename of the | ||
config file to use (just the filename). The search paths for the | ||||
config file are set in :meth:`find_config_file_paths` and then passed | ||||
to the config file loader where they are resolved to an absolute path. | ||||
Fernando Perez
|
r2357 | If a profile has been set at the command line, this will resolve it. | ||
Brian Granger
|
r2200 | """ | ||
Brian Granger
|
r2203 | try: | ||
Brian Granger
|
r2245 | self.config_file_name = self.command_line_config.Global.config_file | ||
Brian Granger
|
r2203 | except AttributeError: | ||
pass | ||||
try: | ||||
Brian Granger
|
r2245 | self.profile_name = self.command_line_config.Global.profile | ||
Brian Granger
|
r2203 | except AttributeError: | ||
pass | ||||
Fernando Perez
|
r2357 | else: | ||
Brian Granger
|
r2200 | name_parts = self.config_file_name.split('.') | ||
Brian Granger
|
r2328 | name_parts.insert(1, u'_' + self.profile_name + u'.') | ||
Brian Granger
|
r2200 | self.config_file_name = ''.join(name_parts) | ||
def find_config_file_paths(self): | ||||
Brian Granger
|
r2294 | """Set the search paths for resolving the config file. | ||
This must set ``self.config_file_paths`` to a sequence of search | ||||
paths to pass to the config file loader. | ||||
""" | ||||
Fernando Perez
|
r2357 | # Include our own profiles directory last, so that users can still find | ||
# our shipped copies of builtin profiles even if they don't have them | ||||
# in their local ipython directory. | ||||
prof_dir = os.path.join(get_ipython_package_dir(), 'config', 'profile') | ||||
Fernando Perez
|
r2358 | self.config_file_paths = (os.getcwd(), self.ipython_dir, prof_dir) | ||
Brian Granger
|
r2200 | |||
def pre_load_file_config(self): | ||||
"""Do actions before the config file is loaded.""" | ||||
Brian Granger
|
r2185 | pass | ||
Brian Granger
|
r2200 | def load_file_config(self): | ||
"""Load the config file. | ||||
This tries to load the config file from disk. If successful, the | ||||
``CONFIG_FILE`` config variable is set to the resolved config file | ||||
location. If not successful, an empty config is used. | ||||
""" | ||||
Fernando Perez
|
r2357 | self.log.debug("Attempting to load config file: %s" % | ||
self.config_file_name) | ||||
Brian Granger
|
r2245 | loader = PyFileConfigLoader(self.config_file_name, | ||
path=self.config_file_paths) | ||||
Brian Granger
|
r2200 | try: | ||
self.file_config = loader.load_config() | ||||
Brian Granger
|
r2245 | self.file_config.Global.config_file = loader.full_filename | ||
Brian Granger
|
r2200 | except IOError: | ||
Brian Granger
|
r2257 | # Only warn if the default config file was NOT being used. | ||
if not self.config_file_name==self.default_config_file_name: | ||||
Fernando Perez
|
r2357 | self.log.warn("Config file not found, skipping: %s" % | ||
Brian Granger
|
r2257 | self.config_file_name, exc_info=True) | ||
Brian Granger
|
r2252 | self.file_config = Config() | ||
except: | ||||
Fernando Perez
|
r2357 | self.log.warn("Error loading config file: %s" % | ||
Brian Granger
|
r2328 | self.config_file_name, exc_info=True) | ||
Brian Granger
|
r2245 | self.file_config = Config() | ||
Brian Granger
|
r2294 | |||
def set_file_config_log_level(self): | ||||
Brian Granger
|
r2270 | # We need to keeep self.log_level updated. But we only use the value | ||
# of the file_config if a value was not specified at the command | ||||
Brian Granger
|
r2294 | # line, because the command line overrides everything. | ||
Brian Granger
|
r2270 | if not hasattr(self.command_line_config.Global, 'log_level'): | ||
try: | ||||
self.log_level = self.file_config.Global.log_level | ||||
except AttributeError: | ||||
pass # Use existing value | ||||
Brian Granger
|
r2200 | |||
def post_load_file_config(self): | ||||
"""Do actions after the config file is loaded.""" | ||||
pass | ||||
Brian Granger
|
r2185 | |||
Brian Granger
|
r2294 | def log_file_config(self): | ||
if hasattr(self.file_config.Global, 'config_file'): | ||||
Fernando Perez
|
r2357 | self.log.debug("Config file loaded: %s" % | ||
self.file_config.Global.config_file) | ||||
Brian Granger
|
r2294 | self.log.debug(repr(self.file_config)) | ||
Brian Granger
|
r2187 | def merge_configs(self): | ||
Brian Granger
|
r2200 | """Merge the default, command line and file config objects.""" | ||
Brian Granger
|
r2245 | config = Config() | ||
config._merge(self.default_config) | ||||
Fernando Perez
|
r2439 | if self.override_config is None: | ||
config._merge(self.file_config) | ||||
config._merge(self.command_line_config) | ||||
if self.constructor_config is not None: | ||||
config._merge(self.constructor_config) | ||||
else: | ||||
config._merge(self.override_config) | ||||
# XXX fperez - propose to Brian we rename master_config to simply | ||||
# config, I think this is going to be heavily used in examples and | ||||
# application code and the name is shorter/easier to find/remember. | ||||
# For now, just alias it... | ||||
Brian Granger
|
r2187 | self.master_config = config | ||
Fernando Perez
|
r2439 | self.config = config | ||
Brian Granger
|
r2294 | |||
def log_master_config(self): | ||||
Brian Granger
|
r2252 | self.log.debug("Master config created:") | ||
self.log.debug(repr(self.master_config)) | ||||
Brian Granger
|
r2185 | |||
Brian Granger
|
r2200 | def pre_construct(self): | ||
"""Do actions after the config has been built, but before construct.""" | ||||
Brian Granger
|
r2185 | pass | ||
Brian Granger
|
r2200 | def construct(self): | ||
"""Construct the main components that make up this app.""" | ||||
Brian Granger
|
r2252 | self.log.debug("Constructing components for application") | ||
Brian Granger
|
r2200 | |||
def post_construct(self): | ||||
"""Do actions after construct, but before starting the app.""" | ||||
Brian Granger
|
r2185 | pass | ||
def start_app(self): | ||||
"""Actually start the app.""" | ||||
Brian Granger
|
r2252 | self.log.debug("Starting application") | ||
Brian Granger
|
r2185 | |||
Brian Granger
|
r2187 | #------------------------------------------------------------------------- | ||
# Utility methods | ||||
#------------------------------------------------------------------------- | ||||
Brian Granger
|
r2185 | def abort(self): | ||
"""Abort the starting of the application.""" | ||||
Brian Granger
|
r2303 | if self._exiting: | ||
pass | ||||
else: | ||||
self.log.critical("Aborting application: %s" % self.name, exc_info=True) | ||||
self._exiting = True | ||||
sys.exit(1) | ||||
Brian Granger
|
r2185 | |||
Brian Granger
|
r2323 | def exit(self, exit_status=0): | ||
Brian Granger
|
r2303 | if self._exiting: | ||
pass | ||||
else: | ||||
self.log.debug("Exiting application: %s" % self.name) | ||||
self._exiting = True | ||||
Brian Granger
|
r2323 | sys.exit(exit_status) | ||
Brian Granger
|
r2200 | |||
def attempt(self, func, action='abort'): | ||||
Brian Granger
|
r2185 | try: | ||
func() | ||||
Brian Granger
|
r2245 | except SystemExit: | ||
Brian Granger
|
r2303 | raise | ||
Brian Granger
|
r2185 | except: | ||
Brian Granger
|
r2200 | if action == 'abort': | ||
Fernando Perez
|
r2403 | self.log.critical("Aborting application: %s" % self.name, | ||
exc_info=True) | ||||
Brian Granger
|
r2200 | self.abort() | ||
Fernando Perez
|
r2403 | raise | ||
Brian Granger
|
r2200 | elif action == 'exit': | ||
Brian Granger
|
r2323 | self.exit(0) | ||