diff --git a/IPython/config/loader.py b/IPython/config/loader.py index 73741e1..679d921 100644 --- a/IPython/config/loader.py +++ b/IPython/config/loader.py @@ -19,6 +19,7 @@ import sys from IPython.external import argparse from IPython.utils.ipstruct import Struct +from IPython.utils.genutils import filefind #----------------------------------------------------------------------------- # Code @@ -84,7 +85,7 @@ class PyFileConfigLoader(FileConfigLoader): that are all caps. These attribute are added to the config Struct. """ - def __init__(self, filename, path='.'): + def __init__(self, filename, path=None): """Build a config loader for a filename and path. Parameters @@ -110,22 +111,7 @@ class PyFileConfigLoader(FileConfigLoader): def _find_file(self): """Try to find the file by searching the paths.""" - if os.path.isfile(os.path.expanduser(self.filename)): - self.full_filename = os.path.expanduser(self.filename) - return - if self.path == '.': - self.path = [os.getcwd()] - if not isinstance(path, (list, tuple)): - raise TypeError("path must be a list or tuple, got: %r" % self.path) - for p in self.path: - if p == '.': p = os.getcwd() - full_filename = os.path.expanduser(os.path.join(p, self.filename)) - if os.path.isfile(full_filename): - self.full_filename = full_filename - return - raise IOError("Config file does not exist in any " - "of the search paths: %r, %r" % \ - (self.filename, self.path)) + self.full_filename = filefind(self.filename, self.path) def _read_file_as_dict(self): self.data = {} @@ -175,6 +161,10 @@ class ArgParseConfigLoader(CommandLineConfigLoader): def _create_parser(self): self.parser = argparse.ArgumentParser(*self.args, **self.kw) self._add_arguments() + self._add_other_arguments() + + def _add_other_arguments(): + pass def _add_arguments(self): for argument in self.arguments: @@ -196,3 +186,15 @@ class ArgParseConfigLoader(CommandLineConfigLoader): if v is not NoDefault: setattr(self.config, k, v) +class IPythonArgParseConfigLoader(ArgParseConfigLoader): + + def _add_other_arguments(self): + self.parser.add_argument('--ipythondir',dest='IPYTHONDIR',type=str, + help='set to override default location of IPYTHONDIR', + default=NoDefault) + self.parser.add_argument('-p','--p',dest='PROFILE_NAME',type=str, + help='the string name of the ipython profile to be used', + default=None) + self.parser.add_argument('--debug',dest="DEBUG",action='store_true', + help='debug the application startup process', + default=NoDefault) diff --git a/IPython/core/application.py b/IPython/core/application.py index aba03a2..6487121 100644 --- a/IPython/core/application.py +++ b/IPython/core/application.py @@ -10,41 +10,6 @@ Authors: Notes ----- - -The following directories are relevant in the startup of an app: - -* The ipythondir. This has a default, but can be set by IPYTHONDIR or at - the command line. -* The current working directory. -* Another runtime directory. With some applications (engine, controller) we - need the ability to have different cluster configs. Each of these needs - to have its own config, security dir and log dir. We could simply treat - these as regular ipython dirs. - -There are number of ways in which these directories are used: - -* For config files. -* For other assets and resources needed to run. These include - plugins, magics, furls files. -* For writing various things created at runtime like logs, furl files, etc. - -Questions: - - -* Can we limit ourselves to 1 config file or do we want to have a sequence - of them like IPYTHONDIR->RUNTIMEDIR->CWD? [1] -* Do we need a debug mode that has custom exception handling and can drop - into pdb upno startup? N -* Do we need to use an OutputTrap to capture output and then present it - to a user if startup fails? N -* Do we want the location of the config file(s) to be independent of the - ipython/runtime dir or coupled to it. In other words, can the user select - a config file that is outside their runtime/ipython dir. One model is - that we could have a very strict model of IPYTHONDIR=runtimed dir= - dir used for all config. -* Do we install default config files or not? N - -* attempt needs to either clash or to die """ #----------------------------------------------------------------------------- @@ -58,9 +23,17 @@ Questions: # Imports #----------------------------------------------------------------------------- +import os import sys +import traceback + from copy import deepcopy from IPython.utils.ipstruct import Struct +from IPython.utils.genutils import get_ipython_dir, filefind +from IPython.config.loader import ( + IPythonArgParseConfigLoader, + PyFileConfigLoader +) #----------------------------------------------------------------------------- # Classes and functions @@ -72,72 +45,157 @@ class ApplicationError(Exception): class Application(object): + """Load a config, construct an app and run it. + """ - runtime_dirs = [] - default_config = Struct() - runtime_dir = '' - config_file = '' - name = '' + config_file_name = 'ipython_config.py' + name = 'ipython' + debug = False def __init__(self): pass def start(self): """Start the application.""" - self.attempt(self.create_command_line_config) - self.attempt(self.find_runtime_dirs) - self.attempt(self.create_runtime_dirs) - self.attempt(self.find_config_files) - self.attempt(self.create_file_configs) + self.attempt(self.create_default_config) + self.attempt(self.pre_load_command_line_config) + self.attempt(self.load_command_line_config, action='exit') + self.attempt(self.post_load_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.attempt(self.post_load_file_config) self.attempt(self.merge_configs) + self.attempt(self.pre_construct) self.attempt(self.construct) - self.attempt(self.start_logging) + 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.""" + self.default_config = Struct() + self.default_config.IPYTHONDIR = get_ipython_dir() + def create_command_line_config(self): - """Read the command line args and return its config object.""" - self.command_line_config = Struct() + """Create and return a command line config loader.""" + return IPythonArgParseConfigLoader(description=self.name) + + def pre_load_command_line_config(self): + """Do actions just before loading the command line config.""" + pass - def find_runtime_dirs(self): - """Find the runtime directory for this application. + def load_command_line_config(self): + """Load the command line config. - This should set self.runtime_dir. + This method also sets ``self.debug``. """ - pass - def create_runtime_dirs(self): - """Create the runtime dirs if they don't exist.""" + loader = self.create_command_line_config() + self.command_line_config = loader.load_config() + try: + self.debug = self.command_line_config.DEBUG + except AttributeError: + pass # use class default + self.log("Default config loaded:", self.default_config) + self.log("Command line config loaded:", self.command_line_config) + + def post_load_command_line_config(self): + """Do actions just after loading the command line config.""" pass - def find_config_files(self): - """Find the config file for this application.""" + 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.IPYTHONDIR + except AttributeError: + self.ipythondir = self.default_config.IPYTHONDIR + sys.path.append(os.path.abspath(self.ipythondir)) + self.log("IPYTHONDIR set to: %s" % self.ipythondir) + + def find_config_file_name(self): + """Find the config file name for this application. + + If a profile has been set at the command line, this will resolve + it. 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 self.command_line_config.PROFILE_NAME is not None: + self.profile_name = self.command_line_config.PROFILE_NAME + name_parts = self.config_file_name.split('.') + name_parts.insert(1, '_' + self.profile_name + '.') + self.config_file_name = ''.join(name_parts) + + def find_config_file_paths(self): + """Set the search paths for resolving the config file.""" + self.config_file_paths = (os.getcwd(), self.ipythondir) + + def pre_load_file_config(self): + """Do actions before the config file is loaded.""" pass - def create_file_configs(self): - self.file_configs = [Struct()] + 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. + """ + loader = PyFileConfigLoader(self.config_file_name, + self.config_file_paths) + try: + self.file_config = loader.load_config() + self.file_config.CONFIG_FILE = loader.full_filename + except IOError: + self.log("Config file not found, skipping: %s" % \ + self.config_file_name) + self.file_config = Struct() + else: + self.log("Config file loaded: %s" % loader.full_filename) + + def post_load_file_config(self): + """Do actions after the config file is loaded.""" + pass def merge_configs(self): + """Merge the default, command line and file config objects.""" config = Struct() - all_configs = self.file_configs + self.command_line_config - for c in all_configs: - config.update(c) + config.update(self.default_config) + config.update(self.command_line_config) + config.update(self.file_config) self.master_config = config + self.log("Master config created:", self.master_config) - def construct(self, config): - """Construct the main components that make up this app.""" + def pre_construct(self): + """Do actions after the config has been built, but before construct.""" pass - def start_logging(self): - """Start logging, if needed, at the last possible moment.""" + def construct(self): + """Construct the main components that make up this app.""" + self.log("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.""" - pass + self.log("Starting application...") #------------------------------------------------------------------------- # Utility methods @@ -148,14 +206,26 @@ class Application(object): print "Aborting application: ", self.name sys.exit(1) - def attempt(self, func): + def exit(self): + print "Exiting application: ", self.name + sys.exit(1) + + def attempt(self, func, action='abort'): try: func() except: - self.handle_error() - self.abort() - - def handle_error(self): - print "I am dying!" - - \ No newline at end of file + if action == 'abort': + self.print_traceback() + self.abort() + elif action == 'exit': + self.exit() + + def print_traceback(self): + print "Error in appliction startup: ", self.name + print + traceback.print_exc() + + def log(self, *args): + if self.debug: + for arg in args: + print "[%s] %s" % (self.name, arg) \ No newline at end of file diff --git a/IPython/core/component.py b/IPython/core/component.py index 853f4d2..9cf898a 100644 --- a/IPython/core/component.py +++ b/IPython/core/component.py @@ -196,7 +196,7 @@ class Component(HasTraitlets): def _config_changed(self, name, old, new): # Get all traitlets with a config_key metadata entry - traitlets = self.traitlets(config_key=lambda v: True) + traitlets = self.traitlets('config_key') for k, v in traitlets.items(): try: config_value = new[v.get_metadata('config_key')] diff --git a/IPython/core/ipmaker.py b/IPython/core/ipmaker.py index 639830f..e277821 100644 --- a/IPython/core/ipmaker.py +++ b/IPython/core/ipmaker.py @@ -93,7 +93,7 @@ def make_IPython(argv=None,user_ns=None,user_global_ns=None,debug=1, # Defaults and initialization # For developer debugging, deactivates crash handler and uses pdb. - DEVDEBUG = False + DEVDEBUG = True if argv is None: argv = sys.argv @@ -446,6 +446,8 @@ object? -> Details about 'object'. ?object also works, ?? prints more. warn('Configuration file %s not found. Ignoring request.' % (opts_all.rcfile) ) + print opts_all.rcfile, opts_all.ipythondir + # 'profiles' are a shorthand notation for config filenames profile_handled_by_legacy = False if opts_all.profile: diff --git a/IPython/utils/genutils.py b/IPython/utils/genutils.py index f1aa8de..28dcce9 100644 --- a/IPython/utils/genutils.py +++ b/IPython/utils/genutils.py @@ -528,32 +528,55 @@ def get_py_filename(name): raise IOError,'File `%s` not found.' % name #----------------------------------------------------------------------------- -def filefind(fname,alt_dirs = None): - """Return the given filename either in the current directory, if it - exists, or in a specified list of directories. - ~ expansion is done on all file and directory names. - Upon an unsuccessful search, raise an IOError exception.""" +def filefind(filename, path_dirs=None): + """Find a file by looking through a sequence of paths. - if alt_dirs is None: - try: - alt_dirs = get_home_dir() - except HomeDirError: - alt_dirs = os.getcwd() - search = [fname] + list_strings(alt_dirs) - search = map(os.path.expanduser,search) - #print 'search list for',fname,'list:',search # dbg - fname = search[0] - if os.path.isfile(fname): - return fname - for direc in search[1:]: - testname = os.path.join(direc,fname) - #print 'testname',testname # dbg + This iterates through a sequence of paths looking for a file and returns + the full, absolute path of the first occurence of the file. If no set of + path dirs is given, the filename is tested as is, after running through + :func:`expandvars` and :func:`expanduser`. Thus a simple call:: + + filefind('myfile.txt') + + will find the file in the current working dir, but:: + + filefind('~/myfile.txt') + + Will find the file in the users home directory. This function does not + automatically try any paths, such as the cwd or the user's home directory. + + Parameters + ---------- + filename : str + The filename to look for. + path_dirs : str, None or sequence of str + The sequence of paths to look for the file in. If None, the filename + need to be absolute or be in the cwd. If a string, the string is + put into a sequence and the searched. If a sequence, walk through + each element and join with ``filename``, calling :func:`expandvars` + and :func:`expanduser` before testing for existence. + + Returns + ------- + Raises :exc:`IOError` or returns absolute path to file. + """ + if path_dirs is None: + path_dirs = ("",) + elif isinstance(path_dirs, basestring): + path_dirs = (path_dirs,) + for path in path_dirs: + if path == '.': path = os.getcwd() + testname = os.path.expandvars( + os.path.expanduser( + os.path.join(path, filename))) if os.path.isfile(testname): - return testname - raise IOError,'File' + `fname` + \ - ' not found in current or supplied directories:' + `alt_dirs` + return os.path.abspath(testname) + raise IOError("File does not exist in any " + "of the search paths: %r, %r" % \ + (filename, path_dirs)) + #---------------------------------------------------------------------------- def file_read(filename): diff --git a/IPython/utils/tests/test_traitlets.py b/IPython/utils/tests/test_traitlets.py index 4edaab6..e5ad5d9 100644 --- a/IPython/utils/tests/test_traitlets.py +++ b/IPython/utils/tests/test_traitlets.py @@ -354,10 +354,13 @@ class TestHasTraitlets(TestCase): i = Int(config_key='VALUE1', other_thing='VALUE2') f = Float(config_key='VALUE3', other_thing='VALUE2') a = A() - # traitlets = a.traitlets(config_key=lambda v: True) - # self.assertEquals(traitlets, dict(i=A.i, f=A.f)) + self.assertEquals(a.traitlets(), dict(i=A.i, f=A.f)) + traitlets = a.traitlets(config_key=lambda v: True) + self.assertEquals(traitlets, dict(i=A.i, f=A.f)) traitlets = a.traitlets(config_key='VALUE1', other_thing='VALUE2') self.assertEquals(traitlets, dict(i=A.i)) + traitlets = a.traitlets('config_key') + self.assertEquals(traitlets, dict(i=A.i, f=A.f)) #----------------------------------------------------------------------------- # Tests for specific traitlet types diff --git a/IPython/utils/traitlets.py b/IPython/utils/traitlets.py index 8667809..0b25b76 100644 --- a/IPython/utils/traitlets.py +++ b/IPython/utils/traitlets.py @@ -438,7 +438,7 @@ class HasTraitlets(object): """Get a list of all the names of this classes traitlets.""" return self.traitlets(**metadata).keys() - def traitlets(self, **metadata): + def traitlets(self, *args, **metadata): """Get a list of all the traitlets of this class. The TraitletTypes returned don't know anything about the values @@ -446,9 +446,12 @@ class HasTraitlets(object): """ traitlets = dict([memb for memb in inspect.getmembers(self.__class__) if \ isinstance(memb[1], TraitletType)]) - if len(metadata) == 0: + if len(metadata) == 0 and len(args) == 0: return traitlets + for meta_name in args: + metadata[meta_name] = lambda _: True + for meta_name, meta_eval in metadata.items(): if type(meta_eval) is not FunctionType: metadata[meta_name] = _SimpleTest(meta_eval)