application.py
390 lines
| 13.9 KiB
| text/x-python
|
PythonLexer
Brian Granger
|
r2185 | #!/usr/bin/env python | ||
# 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 | |||
Brian Granger
|
r2296 | from IPython.core import release | ||
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, | ||||
NoConfigDefault | ||||
Brian Granger
|
r2200 | ) | ||
Brian Granger
|
r2185 | |||
#----------------------------------------------------------------------------- | ||||
# Classes and functions | ||||
#----------------------------------------------------------------------------- | ||||
Brian Granger
|
r2296 | class BaseAppArgParseConfigLoader(ArgParseConfigLoader): | ||
Brian Granger
|
r2245 | """Default command line options for IPython based applications.""" | ||
def _add_other_arguments(self): | ||||
Brian Granger
|
r2322 | self.parser.add_argument('--ipython-dir', | ||
Brian Granger
|
r2328 | dest='Global.ipython_dir',type=unicode, | ||
Brian Granger
|
r2322 | help='Set to override default location of Global.ipython_dir.', | ||
Brian Granger
|
r2245 | default=NoConfigDefault, | ||
Brian Granger
|
r2322 | metavar='Global.ipython_dir') | ||
self.parser.add_argument('-p', '--profile', | ||||
Brian Granger
|
r2328 | dest='Global.profile',type=unicode, | ||
Brian Granger
|
r2245 | help='The string name of the ipython profile to be used.', | ||
default=NoConfigDefault, | ||||
metavar='Global.profile') | ||||
Brian Granger
|
r2322 | self.parser.add_argument('--log-level', | ||
Brian Granger
|
r2287 | dest="Global.log_level",type=int, | ||
Brian Granger
|
r2252 | help='Set the log level (0,10,20,30,40,50). Default is 30.', | ||
Brian Granger
|
r2299 | default=NoConfigDefault, | ||
metavar='Global.log_level') | ||||
Brian Granger
|
r2322 | self.parser.add_argument('--config-file', | ||
Brian Granger
|
r2328 | dest='Global.config_file',type=unicode, | ||
Brian Granger
|
r2245 | help='Set the config file name to override default.', | ||
default=NoConfigDefault, | ||||
metavar='Global.config_file') | ||||
Brian Granger
|
r2185 | class ApplicationError(Exception): | ||
pass | ||||
class Application(object): | ||||
Brian Granger
|
r2301 | """Load a config, construct components and set them running.""" | ||
Brian Granger
|
r2185 | |||
Brian Granger
|
r2328 | name = u'ipython' | ||
Brian Granger
|
r2296 | description = 'IPython: an enhanced interactive Python shell.' | ||
Fernando Perez
|
r2357 | |||
Brian Granger
|
r2328 | config_file_name = u'ipython_config.py' | ||
Fernando Perez
|
r2357 | # Track the default and actual separately because some messages are | ||
# only printed if we aren't using the default. | ||||
default_config_file_name = config_file_name | ||||
Brian Granger
|
r2294 | default_log_level = logging.WARN | ||
Fernando Perez
|
r2357 | # Set by --profile option | ||
profile_name = None | ||||
Fernando Perez
|
r2387 | #: User's ipython directory, typically ~/.ipython/ | ||
Fernando Perez
|
r2357 | ipython_dir = None | ||
Fernando Perez
|
r2391 | #: A reference to the argv to be used (typically ends up being sys.argv[1:]) | ||
argv = None | ||||
Fernando Perez
|
r2357 | |||
# Private attributes | ||||
_exiting = False | ||||
Fernando Perez
|
r2392 | _initialized = False | ||
Brian Granger
|
r2185 | |||
Fernando Perez
|
r2391 | def __init__(self, argv=None): | ||
self.argv = sys.argv[1:] if argv is None else argv | ||||
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): | ||
Brian Granger
|
r2187 | """Start the application.""" | ||
Fernando Perez
|
r2392 | |||
if self._initialized: | ||||
return | ||||
Brian Granger
|
r2200 | self.attempt(self.create_default_config) | ||
Brian Granger
|
r2294 | self.log_default_config() | ||
self.set_default_config_log_level() | ||||
Brian Granger
|
r2200 | self.attempt(self.pre_load_command_line_config) | ||
Brian Granger
|
r2203 | self.attempt(self.load_command_line_config, action='abort') | ||
Brian Granger
|
r2294 | self.set_command_line_config_log_level() | ||
Brian Granger
|
r2200 | self.attempt(self.post_load_command_line_config) | ||
Brian Granger
|
r2294 | self.log_command_line_config() | ||
Brian Granger
|
r2322 | self.attempt(self.find_ipython_dir) | ||
Brian Granger
|
r2303 | self.attempt(self.find_resources) | ||
Brian Granger
|
r2200 | 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) | ||||
Brian Granger
|
r2294 | self.set_file_config_log_level() | ||
Brian Granger
|
r2200 | self.attempt(self.post_load_file_config) | ||
Brian Granger
|
r2294 | self.log_file_config() | ||
Brian Granger
|
r2187 | self.attempt(self.merge_configs) | ||
Brian Granger
|
r2294 | self.log_master_config() | ||
Brian Granger
|
r2200 | self.attempt(self.pre_construct) | ||
Brian Granger
|
r2187 | self.attempt(self.construct) | ||
Brian Granger
|
r2200 | self.attempt(self.post_construct) | ||
Fernando Perez
|
r2392 | self._initialized = True | ||
def start(self): | ||||
self.initialize() | ||||
Brian Granger
|
r2187 | self.attempt(self.start_app) | ||
#------------------------------------------------------------------------- | ||||
# Various stages of Application creation | ||||
#------------------------------------------------------------------------- | ||||
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 | ||||
not a HasTraitlets or Component) are not set in this way. Instead | ||||
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
|
r2391 | return BaseAppArgParseConfigLoader(self.argv, | ||
Brian Granger
|
r2296 | description=self.description, | ||
version=release.version | ||||
) | ||||
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 | ||
Fernando Perez
|
r2357 | except AttributeError: | ||
pass | ||||
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) | ||||
config._merge(self.file_config) | ||||
config._merge(self.command_line_config) | ||||
Brian Granger
|
r2187 | self.master_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': | ||
self.abort() | ||||
elif action == 'exit': | ||||
Brian Granger
|
r2323 | self.exit(0) | ||
Brian Granger
|
r2296 | |||