application.py
468 lines
| 17.2 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 | ||||
Brian Granger
|
r2731 | object and then create the configurable objects, 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 | ||
Brian Granger
|
r2498 | from IPython.utils.path 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 | ||||
Brian Granger
|
r2501 | class BaseAppConfigLoader(ArgParseConfigLoader): | ||
"""Default command line options for IPython based applications.""" | ||||
def _add_ipython_dir(self, parser): | ||||
"""Add the --ipython-dir option to the parser.""" | ||||
paa = parser.add_argument | ||||
paa('--ipython-dir', | ||||
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') | ||||
def _add_log_level(self, parser): | ||||
"""Add the --log-level option to the parser.""" | ||||
paa = parser.add_argument | ||||
paa('--log-level', | ||||
dest="Global.log_level",type=int, | ||||
help='Set the log level (0,10,20,30,40,50). Default is 30.', | ||||
metavar='Global.log_level') | ||||
Thomas Kluyver
|
r3440 | |||
def _add_version(self, parser): | ||||
"""Add the --version option to the parser.""" | ||||
parser.add_argument('--version', action="version", | ||||
version=self.version) | ||||
Brian Granger
|
r2501 | |||
def _add_arguments(self): | ||||
self._add_ipython_dir(self.parser) | ||||
self._add_log_level(self.parser) | ||||
Thomas Kluyver
|
r3457 | try: # Old versions of argparse don't have a version action | ||
self._add_version(self.parser) | ||||
except Exception: | ||||
pass | ||||
Brian Granger
|
r2501 | |||
Fernando Perez
|
r2403 | |||
Brian Granger
|
r2185 | class Application(object): | ||
Brian Granger
|
r2731 | """Load a config, construct configurables and set them running. | ||
Fernando Perez
|
r2439 | |||
Brian Granger
|
r2501 | The configuration of an application can be done via three different Config | ||
objects, which are loaded and ultimately merged into a single one used | ||||
from that point on by the app. These are: | ||||
Fernando Perez
|
r2439 | |||
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. | ||||
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. | ||||
""" | ||||
Brian Granger
|
r2185 | |||
Brian Granger
|
r2328 | name = u'ipython' | ||
Brian Granger
|
r2296 | description = 'IPython: an enhanced interactive Python shell.' | ||
Brian Granger
|
r2501 | #: Usage message printed by argparse. If None, auto-generate | ||
Fernando Perez
|
r2427 | usage = None | ||
Brian Granger
|
r2501 | #: The command line config loader. Subclass of ArgParseConfigLoader. | ||
command_line_loader = BaseAppConfigLoader | ||||
Brian Granger
|
r2511 | #: The name of the config file to load, determined at runtime | ||
config_file_name = None | ||||
Brian Granger
|
r2501 | #: The name of the default config file. Track separately from the actual | ||
#: name because some logic happens only if we aren't using the default. | ||||
Brian Granger
|
r2511 | default_config_file_name = u'ipython_config.py' | ||
Brian Granger
|
r2294 | default_log_level = logging.WARN | ||
Fernando Perez
|
r2439 | #: Set by --profile option | ||
Fernando Perez
|
r2357 | profile_name = None | ||
MinRK
|
r3351 | #: User's ipython directory, typically ~/.ipython or ~/.config/ipython/ | ||
Fernando Perez
|
r2357 | ipython_dir = None | ||
Brian Granger
|
r2501 | #: Internal defaults, implemented in code. | ||
Fernando Perez
|
r2439 | default_config = None | ||
Brian Granger
|
r2501 | #: Read from the filesystem. | ||
Fernando Perez
|
r2439 | file_config = None | ||
Brian Granger
|
r2501 | #: Read from the system's command line flags. | ||
Fernando Perez
|
r2439 | command_line_config = None | ||
Brian Granger
|
r2731 | #: The final config that will be passed to the main object. | ||
Brian Granger
|
r2501 | master_config = None | ||
Fernando Perez
|
r2391 | #: A reference to the argv to be used (typically ends up being sys.argv[1:]) | ||
argv = None | ||||
Fernando Perez
|
r2439 | #: extra arguments computed by the command-line loader | ||
extra_args = None | ||||
Brian Granger
|
r2506 | #: The class to use as the crash handler. | ||
crash_handler_class = crashhandler.CrashHandler | ||||
Fernando Perez
|
r2439 | |||
Fernando Perez
|
r2357 | # Private attributes | ||
_exiting = False | ||||
Fernando Perez
|
r2392 | _initialized = False | ||
Brian Granger
|
r2185 | |||
Brian Granger
|
r2501 | def __init__(self, argv=None): | ||
Fernando Perez
|
r2391 | 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): | ||
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 | |||
Brian Granger
|
r2501 | # 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() | ||||
Fernando Perez
|
r2439 | |||
# 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 | |||
Brian Granger
|
r2501 | # 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() | ||||
Fernando Perez
|
r2439 | |||
# 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 | ||
def start(self): | ||||
Fernando Perez
|
r2439 | """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.""" | ||||
Brian Granger
|
r2506 | self.crash_handler = self.crash_handler_class(self) | ||
Fernando Perez
|
r2403 | 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 | ||||
Brian Granger
|
r2731 | of Configurables. But, defaults the top-level Application (which is | ||
not a HasTraits or Configurables) are not set in this way. Instead | ||||
Brian Granger
|
r2253 | we set them here. The Global section is for variables like this that | ||
Brian Granger
|
r2731 | don't belong to a particular configurable. | ||
Brian Granger
|
r2253 | """ | ||
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.""" | ||
Brian Granger
|
r2501 | return self.command_line_loader( | ||
self.argv, | ||||
description=self.description, | ||||
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 | ||||
Brian Granger
|
r2511 | else: | ||
return | ||||
Brian Granger
|
r2203 | |||
try: | ||||
Brian Granger
|
r2245 | self.profile_name = self.command_line_config.Global.profile | ||
Fernando Perez
|
r2357 | except AttributeError: | ||
Brian Granger
|
r2511 | # Just use the default as there is no profile | ||
self.config_file_name = self.default_config_file_name | ||||
Fernando Perez
|
r2357 | else: | ||
Brian Granger
|
r2511 | # Use the default config file name and profile name if set | ||
# to determine the used config file name. | ||||
name_parts = self.default_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') | ||||
Thomas Kluyver
|
r3451 | self.config_file_paths = (os.getcwdu(), 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 | ||
Thomas Kluyver
|
r3451 | def load_file_config(self, suppress_errors=True): | ||
Brian Granger
|
r2200 | """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. | ||||
Thomas Kluyver
|
r3458 | |||
By default, errors in loading config are handled, and a warning | ||||
printed on screen. For testing, the suppress_errors option is set | ||||
to False, so errors will make tests fail. | ||||
Brian Granger
|
r2200 | """ | ||
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: | ||||
Thomas Kluyver
|
r3451 | if not suppress_errors: # For testing purposes | ||
raise | ||||
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) | ||||
Brian Granger
|
r2501 | config._merge(self.file_config) | ||
config._merge(self.command_line_config) | ||||
Fernando Perez
|
r2439 | # 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): | ||
Brian Granger
|
r2731 | """Construct the main objects that make up this app.""" | ||
self.log.debug("Constructing main objects 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
|
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 | |||
Brian Granger
|
r2506 | def attempt(self, func): | ||
Brian Granger
|
r2185 | try: | ||
func() | ||||
Brian Granger
|
r2245 | except SystemExit: | ||
Brian Granger
|
r2303 | raise | ||
Brian Granger
|
r2185 | except: | ||
Brian Granger
|
r2506 | self.log.critical("Aborting application: %s" % self.name, | ||
exc_info=True) | ||||
self.exit(0) | ||||
Brian Granger
|
r2296 | |||