##// END OF EJS Templates
Finished refactoring ipcontroller to be a proper application....
Finished refactoring ipcontroller to be a proper application. * The :command:`ipcontroller` script has been refactored into a proper IPython application that uses the new config system. This also introduces the idea of an application directory, which for :command:`ipcontroller` is :file:`cluster_<profile>` by default. This cluster directory has the config file, the log directory and the security directory for the controller and engine.

File last commit:

r2297:9dca1484
r2297:9dca1484
Show More
application.py
452 lines | 17.1 KiB | text/x-python | PythonLexer
#!/usr/bin/env python
# encoding: utf-8
"""
An application for IPython
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
#-----------------------------------------------------------------------------
import logging
import os
import sys
import traceback
from copy import deepcopy
from IPython.core import release
from IPython.utils.genutils import get_ipython_dir, filefind
from IPython.config.loader import (
PyFileConfigLoader,
ArgParseConfigLoader,
Config,
NoConfigDefault
)
#-----------------------------------------------------------------------------
# Classes and functions
#-----------------------------------------------------------------------------
class BaseAppArgParseConfigLoader(ArgParseConfigLoader):
"""Default command line options for IPython based applications."""
def _add_other_arguments(self):
self.parser.add_argument('-ipythondir', '--ipython-dir',
dest='Global.ipythondir',type=str,
help='Set to override default location of Global.ipythondir.',
default=NoConfigDefault,
metavar='Global.ipythondir')
self.parser.add_argument('-p','-profile', '--profile',
dest='Global.profile',type=str,
help='The string name of the ipython profile to be used.',
default=NoConfigDefault,
metavar='Global.profile')
self.parser.add_argument('-log_level', '--log-level',
dest="Global.log_level",type=int,
help='Set the log level (0,10,20,30,40,50). Default is 30.',
default=NoConfigDefault)
self.parser.add_argument('-config_file', '--config-file',
dest='Global.config_file',type=str,
help='Set the config file name to override default.',
default=NoConfigDefault,
metavar='Global.config_file')
class ApplicationError(Exception):
pass
class Application(object):
"""Load a config, construct an app and run it.
"""
name = 'ipython'
description = 'IPython: an enhanced interactive Python shell.'
config_file_name = 'ipython_config.py'
default_log_level = logging.WARN
def __init__(self):
self.init_logger()
self.default_config_file_name = self.config_file_name
def init_logger(self):
self.log = logging.getLogger(self.__class__.__name__)
# This is used as the default until the command line arguments are read.
self.log.setLevel(self.default_log_level)
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)
def start(self):
"""Start the application."""
self.attempt(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.set_command_line_config_log_level()
self.attempt(self.post_load_command_line_config)
self.log_command_line_config()
self.attempt(self.find_ipythondir)
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.set_file_config_log_level()
self.attempt(self.post_load_file_config)
self.log_file_config()
self.attempt(self.merge_configs)
self.log_master_config()
self.attempt(self.pre_construct)
self.attempt(self.construct)
self.attempt(self.post_construct)
self.attempt(self.start_app)
#-------------------------------------------------------------------------
# Various stages of Application creation
#-------------------------------------------------------------------------
def create_default_config(self):
"""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.
"""
self.default_config = Config()
self.default_config.Global.ipythondir = get_ipython_dir()
self.default_config.Global.log_level = self.log_level
def log_default_config(self):
self.log.debug('Default config loaded:')
self.log.debug(repr(self.default_config))
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
def create_command_line_config(self):
"""Create and return a command line config loader."""
return BaseAppArgParseConfigLoader(
description=self.description,
version=release.version
)
def pre_load_command_line_config(self):
"""Do actions just before loading the command line config."""
pass
def load_command_line_config(self):
"""Load the command line config."""
loader = self.create_command_line_config()
self.command_line_config = loader.load_config()
self.extra_args = loader.get_extra_args()
def set_command_line_config_log_level(self):
try:
self.log_level = self.command_line_config.Global.log_level
except AttributeError:
pass
def post_load_command_line_config(self):
"""Do actions just after loading the command line config."""
pass
def log_command_line_config(self):
self.log.debug("Command line config loaded:")
self.log.debug(repr(self.command_line_config))
def find_ipythondir(self):
"""Set the IPython directory.
This sets ``self.ipythondir``, but the actual value that is passed
to the application is kept in either ``self.default_config`` or
``self.command_line_config``. This also added ``self.ipythondir`` to
``sys.path`` so config files there can be references by other config
files.
"""
try:
self.ipythondir = self.command_line_config.Global.ipythondir
except AttributeError:
self.ipythondir = self.default_config.Global.ipythondir
sys.path.append(os.path.abspath(self.ipythondir))
if not os.path.isdir(self.ipythondir):
os.makedirs(self.ipythondir, mode=0777)
self.log.debug("IPYTHONDIR set to: %s" % self.ipythondir)
def find_config_file_name(self):
"""Find the config file name for this application.
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.
If a profile has been set at the command line, this will resolve
it.
"""
try:
self.config_file_name = self.command_line_config.Global.config_file
except AttributeError:
pass
try:
self.profile_name = self.command_line_config.Global.profile
name_parts = self.config_file_name.split('.')
name_parts.insert(1, '_' + self.profile_name + '.')
self.config_file_name = ''.join(name_parts)
except AttributeError:
pass
def find_config_file_paths(self):
"""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.
"""
self.config_file_paths = (os.getcwd(), self.ipythondir)
def pre_load_file_config(self):
"""Do actions before the config file is loaded."""
pass
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.
"""
self.log.debug("Attempting to load config file: <%s>" % self.config_file_name)
loader = PyFileConfigLoader(self.config_file_name,
path=self.config_file_paths)
try:
self.file_config = loader.load_config()
self.file_config.Global.config_file = loader.full_filename
except IOError:
# Only warn if the default config file was NOT being used.
if not self.config_file_name==self.default_config_file_name:
self.log.warn("Config file not found, skipping: <%s>" % \
self.config_file_name, exc_info=True)
self.file_config = Config()
except:
self.log.warn("Error loading config file: <%s>" % \
self.config_file_name, exc_info=True)
self.file_config = Config()
def set_file_config_log_level(self):
# 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
# line, because the command line overrides everything.
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
def post_load_file_config(self):
"""Do actions after the config file is loaded."""
pass
def log_file_config(self):
if hasattr(self.file_config.Global, 'config_file'):
self.log.debug("Config file loaded: <%s>" % self.file_config.Global.config_file)
self.log.debug(repr(self.file_config))
def merge_configs(self):
"""Merge the default, command line and file config objects."""
config = Config()
config._merge(self.default_config)
config._merge(self.file_config)
config._merge(self.command_line_config)
self.master_config = config
def log_master_config(self):
self.log.debug("Master config created:")
self.log.debug(repr(self.master_config))
def pre_construct(self):
"""Do actions after the config has been built, but before construct."""
pass
def construct(self):
"""Construct the main components that make up this app."""
self.log.debug("Constructing components for application")
def post_construct(self):
"""Do actions after construct, but before starting the app."""
pass
def start_app(self):
"""Actually start the app."""
self.log.debug("Starting application")
#-------------------------------------------------------------------------
# Utility methods
#-------------------------------------------------------------------------
def abort(self):
"""Abort the starting of the application."""
self.log.critical("Aborting application: %s" % self.name, exc_info=True)
sys.exit(1)
def exit(self):
self.log.critical("Aborting application: %s" % self.name)
sys.exit(1)
def attempt(self, func, action='abort'):
try:
func()
except SystemExit:
self.exit()
except:
if action == 'abort':
self.abort()
elif action == 'exit':
self.exit()
class AppWithDirArgParseConfigLoader(ArgParseConfigLoader):
"""Default command line options for IPython based applications."""
def _add_other_arguments(self):
self.parser.add_argument('-ipythondir', '--ipython-dir',
dest='Global.ipythondir',type=str,
help='Set to override default location of Global.ipythondir.',
default=NoConfigDefault,
metavar='Global.ipythondir')
self.parser.add_argument('-p','-profile', '--profile',
dest='Global.profile',type=str,
help='The string name of the profile to be used. This determines '
'the name of the application dir: basename_<profile>. The basename is '
'determined by the particular application. The default profile '
'is named "default". This convention is used if the -app_dir '
'option is not used.',
default=NoConfigDefault,
metavar='Global.profile')
self.parser.add_argument('-log_level', '--log-level',
dest="Global.log_level",type=int,
help='Set the log level (0,10,20,30,40,50). Default is 30.',
default=NoConfigDefault)
self.parser.add_argument('-app_dir', '--app-dir',
dest='Global.app_dir',type=str,
help='Set the application dir where everything for this '
'application will be found (including the config file). This '
'overrides the logic used by the profile option.',
default=NoConfigDefault,
metavar='Global.app_dir')
class ApplicationWithDir(Application):
"""An application that puts everything into a application directory.
Instead of looking for things in the ipythondir, this type of application
will use its own private directory called the "application directory"
for things like config files, log files, etc.
The application directory is resolved as follows:
* If the ``--app-dir`` option is given, it is used.
* If ``--app-dir`` is not given, the application directory is resolve using
``app_dir_basename`` and ``profile`` as ``<app_dir_basename>_<profile>``.
The search path for this directory is then i) cwd if it is found there
and ii) in ipythondir otherwise.
The config file for the application is to be put in the application
dir and named the value of the ``config_file_name`` class attribute.
"""
# The basename used for the application dir: <app_dir_basename>_<profile>
app_dir_basename = 'cluster'
def create_default_config(self):
super(ApplicationWithDir, self).create_default_config()
self.default_config.Global.profile = 'default'
# The application dir. This is empty initially so the default is to
# try to resolve this using the profile.
self.default_config.Global.app_dir = ''
def create_command_line_config(self):
"""Create and return a command line config loader."""
return AppWithDirArgParseConfigLoader(
description=self.description,
version=release.version
)
def find_config_file_name(self):
"""Find the config file name for this application."""
self.find_app_dir()
self.create_app_dir()
def find_app_dir(self):
"""This resolves the app directory.
This method must set ``self.app_dir`` to the location of the app
dir.
"""
# Instead, first look for an explicit app_dir
try:
self.app_dir = self.command_line_config.Global.app_dir
except AttributeError:
self.app_dir = self.default_config.Global.app_dir
self.app_dir = os.path.expandvars(os.path.expanduser(self.app_dir))
if not self.app_dir:
# Then look for a profile
try:
self.profile = self.command_line_config.Global.profile
except AttributeError:
self.profile = self.default_config.Global.profile
app_dir_name = self.app_dir_basename + '_' + self.profile
try_this = os.path.join(os.getcwd(), app_dir_name)
if os.path.isdir(try_this):
self.app_dir = try_this
else:
self.app_dir = os.path.join(self.ipythondir, app_dir_name)
# These have to be set because they could be different from the one
# that we just computed. Because command line has the highest
# priority, this will always end up in the master_config.
self.default_config.Global.app_dir = self.app_dir
self.command_line_config.Global.app_dir = self.app_dir
def create_app_dir(self):
"""Make sure that the app dir exists."""
if not os.path.isdir(self.app_dir):
os.makedirs(self.app_dir, mode=0777)
def find_config_file_paths(self):
"""Set the search paths for resolving the config file."""
self.config_file_paths = (self.app_dir,)