##// END OF EJS Templates
Refactored the command line config system and other aspects of config....
Brian Granger -
Show More
@@ -1,386 +1,370 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A simple configuration system.
2 """A simple configuration system.
3
3
4 Authors
4 Authors
5 -------
5 -------
6 * Brian Granger
6 * Brian Granger
7 * Fernando Perez
7 * Fernando Perez
8 """
8 """
9
9
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11 # Copyright (C) 2008-2009 The IPython Development Team
11 # Copyright (C) 2008-2009 The IPython Development Team
12 #
12 #
13 # Distributed under the terms of the BSD License. The full license is in
13 # Distributed under the terms of the BSD License. The full license is in
14 # the file COPYING, distributed as part of this software.
14 # the file COPYING, distributed as part of this software.
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16
16
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 # Imports
18 # Imports
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20
20
21 import __builtin__
21 import __builtin__
22 import os
22 import os
23 import sys
23 import sys
24
24
25 from IPython.external import argparse
25 from IPython.external import argparse
26 from IPython.utils.path import filefind
26 from IPython.utils.path import filefind
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Exceptions
29 # Exceptions
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32
32
33 class ConfigError(Exception):
33 class ConfigError(Exception):
34 pass
34 pass
35
35
36
36
37 class ConfigLoaderError(ConfigError):
37 class ConfigLoaderError(ConfigError):
38 pass
38 pass
39
39
40 #-----------------------------------------------------------------------------
40 #-----------------------------------------------------------------------------
41 # Argparse fix
41 # Argparse fix
42 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
43
43
44 # Unfortunately argparse by default prints help messages to stderr instead of
44 # Unfortunately argparse by default prints help messages to stderr instead of
45 # stdout. This makes it annoying to capture long help screens at the command
45 # stdout. This makes it annoying to capture long help screens at the command
46 # line, since one must know how to pipe stderr, which many users don't know how
46 # line, since one must know how to pipe stderr, which many users don't know how
47 # to do. So we override the print_help method with one that defaults to
47 # to do. So we override the print_help method with one that defaults to
48 # stdout and use our class instead.
48 # stdout and use our class instead.
49
49
50 class ArgumentParser(argparse.ArgumentParser):
50 class ArgumentParser(argparse.ArgumentParser):
51 """Simple argparse subclass that prints help to stdout by default."""
51 """Simple argparse subclass that prints help to stdout by default."""
52
52
53 def print_help(self, file=None):
53 def print_help(self, file=None):
54 if file is None:
54 if file is None:
55 file = sys.stdout
55 file = sys.stdout
56 return super(ArgumentParser, self).print_help(file)
56 return super(ArgumentParser, self).print_help(file)
57
57
58 print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__
58 print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__
59
59
60 #-----------------------------------------------------------------------------
60 #-----------------------------------------------------------------------------
61 # Config class for holding config information
61 # Config class for holding config information
62 #-----------------------------------------------------------------------------
62 #-----------------------------------------------------------------------------
63
63
64
64
65 class Config(dict):
65 class Config(dict):
66 """An attribute based dict that can do smart merges."""
66 """An attribute based dict that can do smart merges."""
67
67
68 def __init__(self, *args, **kwds):
68 def __init__(self, *args, **kwds):
69 dict.__init__(self, *args, **kwds)
69 dict.__init__(self, *args, **kwds)
70 # This sets self.__dict__ = self, but it has to be done this way
70 # This sets self.__dict__ = self, but it has to be done this way
71 # because we are also overriding __setattr__.
71 # because we are also overriding __setattr__.
72 dict.__setattr__(self, '__dict__', self)
72 dict.__setattr__(self, '__dict__', self)
73
73
74 def _merge(self, other):
74 def _merge(self, other):
75 to_update = {}
75 to_update = {}
76 for k, v in other.items():
76 for k, v in other.items():
77 if not self.has_key(k):
77 if not self.has_key(k):
78 to_update[k] = v
78 to_update[k] = v
79 else: # I have this key
79 else: # I have this key
80 if isinstance(v, Config):
80 if isinstance(v, Config):
81 # Recursively merge common sub Configs
81 # Recursively merge common sub Configs
82 self[k]._merge(v)
82 self[k]._merge(v)
83 else:
83 else:
84 # Plain updates for non-Configs
84 # Plain updates for non-Configs
85 to_update[k] = v
85 to_update[k] = v
86
86
87 self.update(to_update)
87 self.update(to_update)
88
88
89 def _is_section_key(self, key):
89 def _is_section_key(self, key):
90 if key[0].upper()==key[0] and not key.startswith('_'):
90 if key[0].upper()==key[0] and not key.startswith('_'):
91 return True
91 return True
92 else:
92 else:
93 return False
93 return False
94
94
95 def has_key(self, key):
95 def has_key(self, key):
96 if self._is_section_key(key):
96 if self._is_section_key(key):
97 return True
97 return True
98 else:
98 else:
99 return dict.has_key(self, key)
99 return dict.has_key(self, key)
100
100
101 def _has_section(self, key):
101 def _has_section(self, key):
102 if self._is_section_key(key):
102 if self._is_section_key(key):
103 if dict.has_key(self, key):
103 if dict.has_key(self, key):
104 return True
104 return True
105 return False
105 return False
106
106
107 def copy(self):
107 def copy(self):
108 return type(self)(dict.copy(self))
108 return type(self)(dict.copy(self))
109
109
110 def __copy__(self):
110 def __copy__(self):
111 return self.copy()
111 return self.copy()
112
112
113 def __deepcopy__(self, memo):
113 def __deepcopy__(self, memo):
114 import copy
114 import copy
115 return type(self)(copy.deepcopy(self.items()))
115 return type(self)(copy.deepcopy(self.items()))
116
116
117 def __getitem__(self, key):
117 def __getitem__(self, key):
118 # Because we use this for an exec namespace, we need to delegate
118 # Because we use this for an exec namespace, we need to delegate
119 # the lookup of names in __builtin__ to itself. This means
119 # the lookup of names in __builtin__ to itself. This means
120 # that you can't have section or attribute names that are
120 # that you can't have section or attribute names that are
121 # builtins.
121 # builtins.
122 try:
122 try:
123 return getattr(__builtin__, key)
123 return getattr(__builtin__, key)
124 except AttributeError:
124 except AttributeError:
125 pass
125 pass
126 if self._is_section_key(key):
126 if self._is_section_key(key):
127 try:
127 try:
128 return dict.__getitem__(self, key)
128 return dict.__getitem__(self, key)
129 except KeyError:
129 except KeyError:
130 c = Config()
130 c = Config()
131 dict.__setitem__(self, key, c)
131 dict.__setitem__(self, key, c)
132 return c
132 return c
133 else:
133 else:
134 return dict.__getitem__(self, key)
134 return dict.__getitem__(self, key)
135
135
136 def __setitem__(self, key, value):
136 def __setitem__(self, key, value):
137 # Don't allow names in __builtin__ to be modified.
137 # Don't allow names in __builtin__ to be modified.
138 if hasattr(__builtin__, key):
138 if hasattr(__builtin__, key):
139 raise ConfigError('Config variable names cannot have the same name '
139 raise ConfigError('Config variable names cannot have the same name '
140 'as a Python builtin: %s' % key)
140 'as a Python builtin: %s' % key)
141 if self._is_section_key(key):
141 if self._is_section_key(key):
142 if not isinstance(value, Config):
142 if not isinstance(value, Config):
143 raise ValueError('values whose keys begin with an uppercase '
143 raise ValueError('values whose keys begin with an uppercase '
144 'char must be Config instances: %r, %r' % (key, value))
144 'char must be Config instances: %r, %r' % (key, value))
145 else:
145 else:
146 dict.__setitem__(self, key, value)
146 dict.__setitem__(self, key, value)
147
147
148 def __getattr__(self, key):
148 def __getattr__(self, key):
149 try:
149 try:
150 return self.__getitem__(key)
150 return self.__getitem__(key)
151 except KeyError, e:
151 except KeyError, e:
152 raise AttributeError(e)
152 raise AttributeError(e)
153
153
154 def __setattr__(self, key, value):
154 def __setattr__(self, key, value):
155 try:
155 try:
156 self.__setitem__(key, value)
156 self.__setitem__(key, value)
157 except KeyError, e:
157 except KeyError, e:
158 raise AttributeError(e)
158 raise AttributeError(e)
159
159
160 def __delattr__(self, key):
160 def __delattr__(self, key):
161 try:
161 try:
162 dict.__delitem__(self, key)
162 dict.__delitem__(self, key)
163 except KeyError, e:
163 except KeyError, e:
164 raise AttributeError(e)
164 raise AttributeError(e)
165
165
166
166
167 #-----------------------------------------------------------------------------
167 #-----------------------------------------------------------------------------
168 # Config loading classes
168 # Config loading classes
169 #-----------------------------------------------------------------------------
169 #-----------------------------------------------------------------------------
170
170
171
171
172 class ConfigLoader(object):
172 class ConfigLoader(object):
173 """A object for loading configurations from just about anywhere.
173 """A object for loading configurations from just about anywhere.
174
174
175 The resulting configuration is packaged as a :class:`Struct`.
175 The resulting configuration is packaged as a :class:`Struct`.
176
176
177 Notes
177 Notes
178 -----
178 -----
179 A :class:`ConfigLoader` does one thing: load a config from a source
179 A :class:`ConfigLoader` does one thing: load a config from a source
180 (file, command line arguments) and returns the data as a :class:`Struct`.
180 (file, command line arguments) and returns the data as a :class:`Struct`.
181 There are lots of things that :class:`ConfigLoader` does not do. It does
181 There are lots of things that :class:`ConfigLoader` does not do. It does
182 not implement complex logic for finding config files. It does not handle
182 not implement complex logic for finding config files. It does not handle
183 default values or merge multiple configs. These things need to be
183 default values or merge multiple configs. These things need to be
184 handled elsewhere.
184 handled elsewhere.
185 """
185 """
186
186
187 def __init__(self):
187 def __init__(self):
188 """A base class for config loaders.
188 """A base class for config loaders.
189
189
190 Examples
190 Examples
191 --------
191 --------
192
192
193 >>> cl = ConfigLoader()
193 >>> cl = ConfigLoader()
194 >>> config = cl.load_config()
194 >>> config = cl.load_config()
195 >>> config
195 >>> config
196 {}
196 {}
197 """
197 """
198 self.clear()
198 self.clear()
199
199
200 def clear(self):
200 def clear(self):
201 self.config = Config()
201 self.config = Config()
202
202
203 def load_config(self):
203 def load_config(self):
204 """Load a config from somewhere, return a :class:`Config` instance.
204 """Load a config from somewhere, return a :class:`Config` instance.
205
205
206 Usually, this will cause self.config to be set and then returned.
206 Usually, this will cause self.config to be set and then returned.
207 However, in most cases, :meth:`ConfigLoader.clear` should be called
207 However, in most cases, :meth:`ConfigLoader.clear` should be called
208 to erase any previous state.
208 to erase any previous state.
209 """
209 """
210 self.clear()
210 self.clear()
211 return self.config
211 return self.config
212
212
213
213
214 class FileConfigLoader(ConfigLoader):
214 class FileConfigLoader(ConfigLoader):
215 """A base class for file based configurations.
215 """A base class for file based configurations.
216
216
217 As we add more file based config loaders, the common logic should go
217 As we add more file based config loaders, the common logic should go
218 here.
218 here.
219 """
219 """
220 pass
220 pass
221
221
222
222
223 class PyFileConfigLoader(FileConfigLoader):
223 class PyFileConfigLoader(FileConfigLoader):
224 """A config loader for pure python files.
224 """A config loader for pure python files.
225
225
226 This calls execfile on a plain python file and looks for attributes
226 This calls execfile on a plain python file and looks for attributes
227 that are all caps. These attribute are added to the config Struct.
227 that are all caps. These attribute are added to the config Struct.
228 """
228 """
229
229
230 def __init__(self, filename, path=None):
230 def __init__(self, filename, path=None):
231 """Build a config loader for a filename and path.
231 """Build a config loader for a filename and path.
232
232
233 Parameters
233 Parameters
234 ----------
234 ----------
235 filename : str
235 filename : str
236 The file name of the config file.
236 The file name of the config file.
237 path : str, list, tuple
237 path : str, list, tuple
238 The path to search for the config file on, or a sequence of
238 The path to search for the config file on, or a sequence of
239 paths to try in order.
239 paths to try in order.
240 """
240 """
241 super(PyFileConfigLoader, self).__init__()
241 super(PyFileConfigLoader, self).__init__()
242 self.filename = filename
242 self.filename = filename
243 self.path = path
243 self.path = path
244 self.full_filename = ''
244 self.full_filename = ''
245 self.data = None
245 self.data = None
246
246
247 def load_config(self):
247 def load_config(self):
248 """Load the config from a file and return it as a Struct."""
248 """Load the config from a file and return it as a Struct."""
249 self.clear()
249 self.clear()
250 self._find_file()
250 self._find_file()
251 self._read_file_as_dict()
251 self._read_file_as_dict()
252 self._convert_to_config()
252 self._convert_to_config()
253 return self.config
253 return self.config
254
254
255 def _find_file(self):
255 def _find_file(self):
256 """Try to find the file by searching the paths."""
256 """Try to find the file by searching the paths."""
257 self.full_filename = filefind(self.filename, self.path)
257 self.full_filename = filefind(self.filename, self.path)
258
258
259 def _read_file_as_dict(self):
259 def _read_file_as_dict(self):
260 """Load the config file into self.config, with recursive loading."""
260 """Load the config file into self.config, with recursive loading."""
261 # This closure is made available in the namespace that is used
261 # This closure is made available in the namespace that is used
262 # to exec the config file. This allows users to call
262 # to exec the config file. This allows users to call
263 # load_subconfig('myconfig.py') to load config files recursively.
263 # load_subconfig('myconfig.py') to load config files recursively.
264 # It needs to be a closure because it has references to self.path
264 # It needs to be a closure because it has references to self.path
265 # and self.config. The sub-config is loaded with the same path
265 # and self.config. The sub-config is loaded with the same path
266 # as the parent, but it uses an empty config which is then merged
266 # as the parent, but it uses an empty config which is then merged
267 # with the parents.
267 # with the parents.
268 def load_subconfig(fname):
268 def load_subconfig(fname):
269 loader = PyFileConfigLoader(fname, self.path)
269 loader = PyFileConfigLoader(fname, self.path)
270 try:
270 try:
271 sub_config = loader.load_config()
271 sub_config = loader.load_config()
272 except IOError:
272 except IOError:
273 # Pass silently if the sub config is not there. This happens
273 # Pass silently if the sub config is not there. This happens
274 # when a user us using a profile, but not the default config.
274 # when a user us using a profile, but not the default config.
275 pass
275 pass
276 else:
276 else:
277 self.config._merge(sub_config)
277 self.config._merge(sub_config)
278
278
279 # Again, this needs to be a closure and should be used in config
279 # Again, this needs to be a closure and should be used in config
280 # files to get the config being loaded.
280 # files to get the config being loaded.
281 def get_config():
281 def get_config():
282 return self.config
282 return self.config
283
283
284 namespace = dict(load_subconfig=load_subconfig, get_config=get_config)
284 namespace = dict(load_subconfig=load_subconfig, get_config=get_config)
285 execfile(self.full_filename, namespace)
285 execfile(self.full_filename, namespace)
286
286
287 def _convert_to_config(self):
287 def _convert_to_config(self):
288 if self.data is None:
288 if self.data is None:
289 ConfigLoaderError('self.data does not exist')
289 ConfigLoaderError('self.data does not exist')
290
290
291
291
292 class CommandLineConfigLoader(ConfigLoader):
292 class CommandLineConfigLoader(ConfigLoader):
293 """A config loader for command line arguments.
293 """A config loader for command line arguments.
294
294
295 As we add more command line based loaders, the common logic should go
295 As we add more command line based loaders, the common logic should go
296 here.
296 here.
297 """
297 """
298
298
299
299
300 class ArgParseConfigLoader(CommandLineConfigLoader):
300 class ArgParseConfigLoader(CommandLineConfigLoader):
301
301
302 def __init__(self, argv=None, arguments=(), *parser_args, **parser_kw):
302 def __init__(self, argv=None, *parser_args, **parser_kw):
303 """Create a config loader for use with argparse.
303 """Create a config loader for use with argparse.
304
304
305 Parameters
305 Parameters
306 ----------
306 ----------
307
307
308 argv : optional, list
308 argv : optional, list
309 If given, used to read command-line arguments from, otherwise
309 If given, used to read command-line arguments from, otherwise
310 sys.argv[1:] is used.
310 sys.argv[1:] is used.
311
311
312 arguments : optional, tuple
313 A tuple of two element tuples each having the form (args, kwargs).
314 Each such pair is passed to parser.add_argument(*args, **kwargs)
315 in sequence to configure the parser.
316
317 parser_args : tuple
312 parser_args : tuple
318 A tuple of positional arguments that will be passed to the
313 A tuple of positional arguments that will be passed to the
319 constructor of :class:`argparse.ArgumentParser`.
314 constructor of :class:`argparse.ArgumentParser`.
320
315
321 parser_kw : dict
316 parser_kw : dict
322 A tuple of keyword arguments that will be passed to the
317 A tuple of keyword arguments that will be passed to the
323 constructor of :class:`argparse.ArgumentParser`.
318 constructor of :class:`argparse.ArgumentParser`.
324 """
319 """
325 super(CommandLineConfigLoader, self).__init__()
320 super(CommandLineConfigLoader, self).__init__()
326 if argv == None:
321 if argv == None:
327 argv = sys.argv[1:]
322 argv = sys.argv[1:]
328 self.argv = argv
323 self.argv = argv
329 self.arguments = arguments
330 self.parser_args = parser_args
324 self.parser_args = parser_args
331 kwargs = dict(argument_default=argparse.SUPPRESS)
325 kwargs = dict(argument_default=argparse.SUPPRESS)
332 kwargs.update(parser_kw)
326 kwargs.update(parser_kw)
333 self.parser_kw = kwargs
327 self.parser_kw = kwargs
334
328
335 def load_config(self, args=None):
329 def load_config(self, args=None):
336 """Parse command line arguments and return as a Struct.
330 """Parse command line arguments and return as a Struct.
337
331
338 Parameters
332 Parameters
339 ----------
333 ----------
340
334
341 args : optional, list
335 args : optional, list
342 If given, a list with the structure of sys.argv[1:] to parse
336 If given, a list with the structure of sys.argv[1:] to parse
343 arguments from. If not given, the instance's self.argv attribute
337 arguments from. If not given, the instance's self.argv attribute
344 (given at construction time) is used."""
338 (given at construction time) is used."""
345 self.clear()
339 self.clear()
346 if args is None:
340 if args is None:
347 args = self.argv
341 args = self.argv
348 self._create_parser()
342 self._create_parser()
349 self._parse_args(args)
343 self._parse_args(args)
350 self._convert_to_config()
344 self._convert_to_config()
351 return self.config
345 return self.config
352
346
353 def get_extra_args(self):
347 def get_extra_args(self):
354 if hasattr(self, 'extra_args'):
348 if hasattr(self, 'extra_args'):
355 return self.extra_args
349 return self.extra_args
356 else:
350 else:
357 return []
351 return []
358
352
359 def _create_parser(self):
353 def _create_parser(self):
360 self.parser = ArgumentParser(*self.parser_args, **self.parser_kw)
354 self.parser = ArgumentParser(*self.parser_args, **self.parser_kw)
361 self._add_arguments()
355 self._add_arguments()
362 self._add_other_arguments()
363
356
364 def _add_arguments(self):
357 def _add_arguments(self):
365 for argument in self.arguments:
358 raise NotImplementedError("subclasses must implement _add_arguments")
366 # Remove any defaults in case people add them. We can't have
367 # command line default because all default are determined by
368 # traited class attributes.
369 argument[1].pop('default', None)
370 self.parser.add_argument(*argument[0], **argument[1])
371
372 def _add_other_arguments(self):
373 """Meant for subclasses to add their own arguments."""
374 pass
375
359
376 def _parse_args(self, args):
360 def _parse_args(self, args):
377 """self.parser->self.parsed_data"""
361 """self.parser->self.parsed_data"""
378 self.parsed_data, self.extra_args = self.parser.parse_known_args(args)
362 self.parsed_data, self.extra_args = self.parser.parse_known_args(args)
379
363
380 def _convert_to_config(self):
364 def _convert_to_config(self):
381 """self.parsed_data->self.config"""
365 """self.parsed_data->self.config"""
382 for k, v in vars(self.parsed_data).items():
366 for k, v in vars(self.parsed_data).items():
383 exec_str = 'self.config.' + k + '= v'
367 exec_str = 'self.config.' + k + '= v'
384 exec exec_str in locals(), globals()
368 exec exec_str in locals(), globals()
385
369
386
370
@@ -1,178 +1,174 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # encoding: utf-8
2 # encoding: utf-8
3 """
3 """
4 Tests for IPython.config.loader
4 Tests for IPython.config.loader
5
5
6 Authors:
6 Authors:
7
7
8 * Brian Granger
8 * Brian Granger
9 * Fernando Perez (design help)
9 * Fernando Perez (design help)
10 """
10 """
11
11
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13 # Copyright (C) 2008-2009 The IPython Development Team
13 # Copyright (C) 2008-2009 The IPython Development Team
14 #
14 #
15 # Distributed under the terms of the BSD License. The full license is in
15 # Distributed under the terms of the BSD License. The full license is in
16 # the file COPYING, distributed as part of this software.
16 # the file COPYING, distributed as part of this software.
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20 # Imports
20 # Imports
21 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
22
22
23 import os
23 import os
24 from tempfile import mkstemp
24 from tempfile import mkstemp
25 from unittest import TestCase
25 from unittest import TestCase
26
26
27 from IPython.config.loader import (
27 from IPython.config.loader import (
28 Config,
28 Config,
29 PyFileConfigLoader,
29 PyFileConfigLoader,
30 ArgParseConfigLoader,
30 ArgParseConfigLoader,
31 ConfigError
31 ConfigError
32 )
32 )
33
33
34 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
35 # Actual tests
35 # Actual tests
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37
37
38
38
39 pyfile = """
39 pyfile = """
40 c = get_config()
40 c = get_config()
41 c.a = 10
41 c.a = 10
42 c.b = 20
42 c.b = 20
43 c.Foo.Bar.value = 10
43 c.Foo.Bar.value = 10
44 c.Foo.Bam.value = range(10)
44 c.Foo.Bam.value = range(10)
45 c.D.C.value = 'hi there'
45 c.D.C.value = 'hi there'
46 """
46 """
47
47
48 class TestPyFileCL(TestCase):
48 class TestPyFileCL(TestCase):
49
49
50 def test_basic(self):
50 def test_basic(self):
51 fd, fname = mkstemp('.py')
51 fd, fname = mkstemp('.py')
52 f = os.fdopen(fd, 'w')
52 f = os.fdopen(fd, 'w')
53 f.write(pyfile)
53 f.write(pyfile)
54 f.close()
54 f.close()
55 # Unlink the file
55 # Unlink the file
56 cl = PyFileConfigLoader(fname)
56 cl = PyFileConfigLoader(fname)
57 config = cl.load_config()
57 config = cl.load_config()
58 self.assertEquals(config.a, 10)
58 self.assertEquals(config.a, 10)
59 self.assertEquals(config.b, 20)
59 self.assertEquals(config.b, 20)
60 self.assertEquals(config.Foo.Bar.value, 10)
60 self.assertEquals(config.Foo.Bar.value, 10)
61 self.assertEquals(config.Foo.Bam.value, range(10))
61 self.assertEquals(config.Foo.Bam.value, range(10))
62 self.assertEquals(config.D.C.value, 'hi there')
62 self.assertEquals(config.D.C.value, 'hi there')
63
63
64
64 class MyLoader1(ArgParseConfigLoader):
65 arguments = (
65 def _add_arguments(self):
66 (('-f','--foo'), dict(dest='Global.foo', type=str)),
66 p = self.parser
67 (('-b',), dict(dest='MyClass.bar', type=int)),
67 p.add_argument('-f', '--foo', dest='Global.foo', type=str)
68 (('-n',), dict(dest='n', action='store_true')),
68 p.add_argument('-b', dest='MyClass.bar', type=int)
69 (('Global.bam',), dict(type=str))
69 p.add_argument('-n', dest='n', action='store_true')
70 )
70 p.add_argument('Global.bam', type=str)
71
72 class MyLoader2(ArgParseConfigLoader):
73 def _add_arguments(self):
74 subparsers = self.parser.add_subparsers(dest='subparser_name')
75 subparser1 = subparsers.add_parser('1')
76 subparser1.add_argument('-x',dest='Global.x')
77 subparser2 = subparsers.add_parser('2')
78 subparser2.add_argument('y')
71
79
72 class TestArgParseCL(TestCase):
80 class TestArgParseCL(TestCase):
73
81
74 def test_basic(self):
82 def test_basic(self):
75 cl = ArgParseConfigLoader(arguments=arguments)
83 cl = MyLoader1()
76 config = cl.load_config('-f hi -b 10 -n wow'.split())
84 config = cl.load_config('-f hi -b 10 -n wow'.split())
77 self.assertEquals(config.Global.foo, 'hi')
85 self.assertEquals(config.Global.foo, 'hi')
78 self.assertEquals(config.MyClass.bar, 10)
86 self.assertEquals(config.MyClass.bar, 10)
79 self.assertEquals(config.n, True)
87 self.assertEquals(config.n, True)
80 self.assertEquals(config.Global.bam, 'wow')
88 self.assertEquals(config.Global.bam, 'wow')
81 config = cl.load_config(['wow'])
89 config = cl.load_config(['wow'])
82 self.assertEquals(config.keys(), ['Global'])
90 self.assertEquals(config.keys(), ['Global'])
83 self.assertEquals(config.Global.keys(), ['bam'])
91 self.assertEquals(config.Global.keys(), ['bam'])
84 self.assertEquals(config.Global.bam, 'wow')
92 self.assertEquals(config.Global.bam, 'wow')
85
93
86 def test_add_arguments(self):
94 def test_add_arguments(self):
87
95 cl = MyLoader2()
88 class MyLoader(ArgParseConfigLoader):
89 def _add_arguments(self):
90 subparsers = self.parser.add_subparsers(dest='subparser_name')
91 subparser1 = subparsers.add_parser('1')
92 subparser1.add_argument('-x',dest='Global.x')
93 subparser2 = subparsers.add_parser('2')
94 subparser2.add_argument('y')
95
96 cl = MyLoader()
97 config = cl.load_config('2 frobble'.split())
96 config = cl.load_config('2 frobble'.split())
98 self.assertEquals(config.subparser_name, '2')
97 self.assertEquals(config.subparser_name, '2')
99 self.assertEquals(config.y, 'frobble')
98 self.assertEquals(config.y, 'frobble')
100 config = cl.load_config('1 -x frobble'.split())
99 config = cl.load_config('1 -x frobble'.split())
101 self.assertEquals(config.subparser_name, '1')
100 self.assertEquals(config.subparser_name, '1')
102 self.assertEquals(config.Global.x, 'frobble')
101 self.assertEquals(config.Global.x, 'frobble')
103
102
104 def test_argv(self):
103 def test_argv(self):
105 cl = ArgParseConfigLoader(
104 cl = MyLoader1(argv='-f hi -b 10 -n wow'.split())
106 argv='-f hi -b 10 -n wow'.split(),
107 arguments=arguments
108 )
109 config = cl.load_config()
105 config = cl.load_config()
110 self.assertEquals(config.Global.foo, 'hi')
106 self.assertEquals(config.Global.foo, 'hi')
111 self.assertEquals(config.MyClass.bar, 10)
107 self.assertEquals(config.MyClass.bar, 10)
112 self.assertEquals(config.n, True)
108 self.assertEquals(config.n, True)
113 self.assertEquals(config.Global.bam, 'wow')
109 self.assertEquals(config.Global.bam, 'wow')
114
110
115
111
116 class TestConfig(TestCase):
112 class TestConfig(TestCase):
117
113
118 def test_setget(self):
114 def test_setget(self):
119 c = Config()
115 c = Config()
120 c.a = 10
116 c.a = 10
121 self.assertEquals(c.a, 10)
117 self.assertEquals(c.a, 10)
122 self.assertEquals(c.has_key('b'), False)
118 self.assertEquals(c.has_key('b'), False)
123
119
124 def test_auto_section(self):
120 def test_auto_section(self):
125 c = Config()
121 c = Config()
126 self.assertEquals(c.has_key('A'), True)
122 self.assertEquals(c.has_key('A'), True)
127 self.assertEquals(c._has_section('A'), False)
123 self.assertEquals(c._has_section('A'), False)
128 A = c.A
124 A = c.A
129 A.foo = 'hi there'
125 A.foo = 'hi there'
130 self.assertEquals(c._has_section('A'), True)
126 self.assertEquals(c._has_section('A'), True)
131 self.assertEquals(c.A.foo, 'hi there')
127 self.assertEquals(c.A.foo, 'hi there')
132 del c.A
128 del c.A
133 self.assertEquals(len(c.A.keys()),0)
129 self.assertEquals(len(c.A.keys()),0)
134
130
135 def test_merge_doesnt_exist(self):
131 def test_merge_doesnt_exist(self):
136 c1 = Config()
132 c1 = Config()
137 c2 = Config()
133 c2 = Config()
138 c2.bar = 10
134 c2.bar = 10
139 c2.Foo.bar = 10
135 c2.Foo.bar = 10
140 c1._merge(c2)
136 c1._merge(c2)
141 self.assertEquals(c1.Foo.bar, 10)
137 self.assertEquals(c1.Foo.bar, 10)
142 self.assertEquals(c1.bar, 10)
138 self.assertEquals(c1.bar, 10)
143 c2.Bar.bar = 10
139 c2.Bar.bar = 10
144 c1._merge(c2)
140 c1._merge(c2)
145 self.assertEquals(c1.Bar.bar, 10)
141 self.assertEquals(c1.Bar.bar, 10)
146
142
147 def test_merge_exists(self):
143 def test_merge_exists(self):
148 c1 = Config()
144 c1 = Config()
149 c2 = Config()
145 c2 = Config()
150 c1.Foo.bar = 10
146 c1.Foo.bar = 10
151 c1.Foo.bam = 30
147 c1.Foo.bam = 30
152 c2.Foo.bar = 20
148 c2.Foo.bar = 20
153 c2.Foo.wow = 40
149 c2.Foo.wow = 40
154 c1._merge(c2)
150 c1._merge(c2)
155 self.assertEquals(c1.Foo.bam, 30)
151 self.assertEquals(c1.Foo.bam, 30)
156 self.assertEquals(c1.Foo.bar, 20)
152 self.assertEquals(c1.Foo.bar, 20)
157 self.assertEquals(c1.Foo.wow, 40)
153 self.assertEquals(c1.Foo.wow, 40)
158 c2.Foo.Bam.bam = 10
154 c2.Foo.Bam.bam = 10
159 c1._merge(c2)
155 c1._merge(c2)
160 self.assertEquals(c1.Foo.Bam.bam, 10)
156 self.assertEquals(c1.Foo.Bam.bam, 10)
161
157
162 def test_deepcopy(self):
158 def test_deepcopy(self):
163 c1 = Config()
159 c1 = Config()
164 c1.Foo.bar = 10
160 c1.Foo.bar = 10
165 c1.Foo.bam = 30
161 c1.Foo.bam = 30
166 c1.a = 'asdf'
162 c1.a = 'asdf'
167 c1.b = range(10)
163 c1.b = range(10)
168 import copy
164 import copy
169 c2 = copy.deepcopy(c1)
165 c2 = copy.deepcopy(c1)
170 self.assertEquals(c1, c2)
166 self.assertEquals(c1, c2)
171 self.assert_(c1 is not c2)
167 self.assert_(c1 is not c2)
172 self.assert_(c1.Foo is not c2.Foo)
168 self.assert_(c1.Foo is not c2.Foo)
173
169
174 def test_builtin(self):
170 def test_builtin(self):
175 c1 = Config()
171 c1 = Config()
176 exec 'foo = True' in c1
172 exec 'foo = True' in c1
177 self.assertEquals(c1.foo, True)
173 self.assertEquals(c1.foo, True)
178 self.assertRaises(ConfigError, setattr, c1, 'ValueError', 10)
174 self.assertRaises(ConfigError, setattr, c1, 'ValueError', 10)
@@ -1,489 +1,463 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 An application for IPython.
3 An application for IPython.
4
4
5 All top-level applications should use the classes in this module for
5 All top-level applications should use the classes in this module for
6 handling configuration and creating componenets.
6 handling configuration and creating componenets.
7
7
8 The job of an :class:`Application` is to create the master configuration
8 The job of an :class:`Application` is to create the master configuration
9 object and then create the components, passing the config to them.
9 object and then create the components, passing the config to them.
10
10
11 Authors:
11 Authors:
12
12
13 * Brian Granger
13 * Brian Granger
14 * Fernando Perez
14 * Fernando Perez
15
15
16 Notes
16 Notes
17 -----
17 -----
18 """
18 """
19
19
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21 # Copyright (C) 2008-2009 The IPython Development Team
21 # Copyright (C) 2008-2009 The IPython Development Team
22 #
22 #
23 # Distributed under the terms of the BSD License. The full license is in
23 # Distributed under the terms of the BSD License. The full license is in
24 # the file COPYING, distributed as part of this software.
24 # the file COPYING, distributed as part of this software.
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26
26
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28 # Imports
28 # Imports
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30
30
31 import logging
31 import logging
32 import os
32 import os
33 import sys
33 import sys
34
34
35 from IPython.core import release, crashhandler
35 from IPython.core import release, crashhandler
36 from IPython.utils.path import get_ipython_dir, get_ipython_package_dir
36 from IPython.utils.path import get_ipython_dir, get_ipython_package_dir
37 from IPython.config.loader import (
37 from IPython.config.loader import (
38 PyFileConfigLoader,
38 PyFileConfigLoader,
39 ArgParseConfigLoader,
39 ArgParseConfigLoader,
40 Config,
40 Config,
41 )
41 )
42
42
43 #-----------------------------------------------------------------------------
43 #-----------------------------------------------------------------------------
44 # Classes and functions
44 # Classes and functions
45 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
46
46
47 class ApplicationError(Exception):
47 class ApplicationError(Exception):
48 pass
48 pass
49
49
50
50
51 app_cl_args = (
51 class BaseAppConfigLoader(ArgParseConfigLoader):
52 (('--ipython-dir', ), dict(
52 """Default command line options for IPython based applications."""
53 dest='Global.ipython_dir',type=unicode,
53
54 help=
54 def _add_ipython_dir(self, parser):
55 """Set to override default location of the IPython directory
55 """Add the --ipython-dir option to the parser."""
56 IPYTHON_DIR, stored as Global.ipython_dir. This can also be specified
56 paa = parser.add_argument
57 through the environment variable IPYTHON_DIR.""",
57 paa('--ipython-dir',
58 metavar='Global.ipython_dir') ),
58 dest='Global.ipython_dir',type=unicode,
59 (('-p', '--profile',), dict(
59 help=
60 dest='Global.profile',type=unicode,
60 """Set to override default location of the IPython directory
61 help=
61 IPYTHON_DIR, stored as Global.ipython_dir. This can also be
62 """The string name of the ipython profile to be used. Assume that your
62 specified through the environment variable IPYTHON_DIR.""",
63 config file is ipython_config-<name>.py (looks in current dir first,
63 metavar='Global.ipython_dir')
64 then in IPYTHON_DIR). This is a quick way to keep and load multiple
64
65 config files for different tasks, especially if include your basic one
65 def _add_log_level(self, parser):
66 in your more specialized ones. You can keep a basic
66 """Add the --log-level option to the parser."""
67 IPYTHON_DIR/ipython_config.py file and then have other 'profiles' which
67 paa = parser.add_argument
68 include this one and load extra things for particular tasks.""",
68 paa('--log-level',
69 metavar='Global.profile') ),
69 dest="Global.log_level",type=int,
70 (('--log-level',), dict(
70 help='Set the log level (0,10,20,30,40,50). Default is 30.',
71 dest="Global.log_level",type=int,
71 metavar='Global.log_level')
72 help='Set the log level (0,10,20,30,40,50). Default is 30.',
72
73 metavar='Global.log_level')),
73 def _add_arguments(self):
74 (('--config-file',), dict(
74 self._add_ipython_dir(self.parser)
75 dest='Global.config_file',type=unicode,
75 self._add_log_level(self.parser)
76 help=
76
77 """Set the config file name to override default. Normally IPython
78 loads ipython_config.py (from current directory) or
79 IPYTHON_DIR/ipython_config.py. If the loading of your config file
80 fails, IPython starts with a bare bones configuration (no modules
81 loaded at all).""",
82 metavar='Global.config_file')),
83 )
84
77
85 class Application(object):
78 class Application(object):
86 """Load a config, construct components and set them running.
79 """Load a config, construct components and set them running.
87
80
88 The configuration of an application can be done via four different Config
81 The configuration of an application can be done via three different Config
89 objects, which are loaded and ultimately merged into a single one used from
82 objects, which are loaded and ultimately merged into a single one used
90 that point on by the app. These are:
83 from that point on by the app. These are:
91
84
92 1. default_config: internal defaults, implemented in code.
85 1. default_config: internal defaults, implemented in code.
93 2. file_config: read from the filesystem.
86 2. file_config: read from the filesystem.
94 3. command_line_config: read from the system's command line flags.
87 3. command_line_config: read from the system's command line flags.
95 4. constructor_config: passed parametrically to the constructor.
96
88
97 During initialization, 3 is actually read before 2, since at the
89 During initialization, 3 is actually read before 2, since at the
98 command-line one may override the location of the file to be read. But the
90 command-line one may override the location of the file to be read. But the
99 above is the order in which the merge is made.
91 above is the order in which the merge is made.
100
101 There is a final config object can be created and passed to the
102 constructor: override_config. If it exists, this completely overrides the
103 configs 2-4 above (the default is still used to ensure that all needed
104 fields at least are created). This makes it easier to create
105 parametrically (e.g. in testing or sphinx plugins) objects with a known
106 configuration, that are unaffected by whatever arguments may be present in
107 sys.argv or files in the user's various directories.
108 """
92 """
109
93
110 name = u'ipython'
94 name = u'ipython'
111 description = 'IPython: an enhanced interactive Python shell.'
95 description = 'IPython: an enhanced interactive Python shell.'
112 #: usage message printed by argparse. If None, auto-generate
96 #: Usage message printed by argparse. If None, auto-generate
113 usage = None
97 usage = None
98 #: The command line config loader. Subclass of ArgParseConfigLoader.
99 command_line_loader = BaseAppConfigLoader
100 #: The name of the config file to load.
114 config_file_name = u'ipython_config.py'
101 config_file_name = u'ipython_config.py'
115 #: Track the default and actual separately because some messages are
102 #: The name of the default config file. Track separately from the actual
116 #: only printed if we aren't using the default.
103 #: name because some logic happens only if we aren't using the default.
117 default_config_file_name = config_file_name
104 default_config_file_name = config_file_name
118 default_log_level = logging.WARN
105 default_log_level = logging.WARN
119 #: Set by --profile option
106 #: Set by --profile option
120 profile_name = None
107 profile_name = None
121 #: User's ipython directory, typically ~/.ipython/
108 #: User's ipython directory, typically ~/.ipython/
122 ipython_dir = None
109 ipython_dir = None
123 #: internal defaults, implemented in code.
110 #: Internal defaults, implemented in code.
124 default_config = None
111 default_config = None
125 #: read from the filesystem
112 #: Read from the filesystem.
126 file_config = None
113 file_config = None
127 #: read from the system's command line flags
114 #: Read from the system's command line flags.
128 command_line_config = None
115 command_line_config = None
129 #: passed parametrically to the constructor.
116 #: The final config that will be passed to the component.
130 constructor_config = None
117 master_config = None
131 #: final override, if given supercedes file/command/constructor configs
132 override_config = None
133 #: A reference to the argv to be used (typically ends up being sys.argv[1:])
118 #: A reference to the argv to be used (typically ends up being sys.argv[1:])
134 argv = None
119 argv = None
135 #: Default command line arguments. Subclasses should create a new tuple
136 #: that *includes* these.
137 cl_arguments = app_cl_args
138
139 #: extra arguments computed by the command-line loader
120 #: extra arguments computed by the command-line loader
140 extra_args = None
121 extra_args = None
141
122
142 # Private attributes
123 # Private attributes
143 _exiting = False
124 _exiting = False
144 _initialized = False
125 _initialized = False
145
126
146 # Class choices for things that will be instantiated at runtime.
127 # Class choices for things that will be instantiated at runtime.
147 _CrashHandler = crashhandler.CrashHandler
128 _CrashHandler = crashhandler.CrashHandler
148
129
149 def __init__(self, argv=None, constructor_config=None, override_config=None):
130 def __init__(self, argv=None):
150 self.argv = sys.argv[1:] if argv is None else argv
131 self.argv = sys.argv[1:] if argv is None else argv
151 self.constructor_config = constructor_config
152 self.override_config = override_config
153 self.init_logger()
132 self.init_logger()
154
133
155 def init_logger(self):
134 def init_logger(self):
156 self.log = logging.getLogger(self.__class__.__name__)
135 self.log = logging.getLogger(self.__class__.__name__)
157 # This is used as the default until the command line arguments are read.
136 # This is used as the default until the command line arguments are read.
158 self.log.setLevel(self.default_log_level)
137 self.log.setLevel(self.default_log_level)
159 self._log_handler = logging.StreamHandler()
138 self._log_handler = logging.StreamHandler()
160 self._log_formatter = logging.Formatter("[%(name)s] %(message)s")
139 self._log_formatter = logging.Formatter("[%(name)s] %(message)s")
161 self._log_handler.setFormatter(self._log_formatter)
140 self._log_handler.setFormatter(self._log_formatter)
162 self.log.addHandler(self._log_handler)
141 self.log.addHandler(self._log_handler)
163
142
164 def _set_log_level(self, level):
143 def _set_log_level(self, level):
165 self.log.setLevel(level)
144 self.log.setLevel(level)
166
145
167 def _get_log_level(self):
146 def _get_log_level(self):
168 return self.log.level
147 return self.log.level
169
148
170 log_level = property(_get_log_level, _set_log_level)
149 log_level = property(_get_log_level, _set_log_level)
171
150
172 def initialize(self):
151 def initialize(self):
173 """Initialize the application.
152 """Initialize the application.
174
153
175 Loads all configuration information and sets all application state, but
154 Loads all configuration information and sets all application state, but
176 does not start any relevant processing (typically some kind of event
155 does not start any relevant processing (typically some kind of event
177 loop).
156 loop).
178
157
179 Once this method has been called, the application is flagged as
158 Once this method has been called, the application is flagged as
180 initialized and the method becomes a no-op."""
159 initialized and the method becomes a no-op."""
181
160
182 if self._initialized:
161 if self._initialized:
183 return
162 return
184
163
185 # The first part is protected with an 'attempt' wrapper, that will log
164 # The first part is protected with an 'attempt' wrapper, that will log
186 # failures with the basic system traceback machinery. Once our crash
165 # failures with the basic system traceback machinery. Once our crash
187 # handler is in place, we can let any subsequent exception propagate,
166 # handler is in place, we can let any subsequent exception propagate,
188 # as our handler will log it with much better detail than the default.
167 # as our handler will log it with much better detail than the default.
189 self.attempt(self.create_crash_handler)
168 self.attempt(self.create_crash_handler)
190
169
191 # Configuration phase
170 # Configuration phase
192 # Default config (internally hardwired in application code)
171 # Default config (internally hardwired in application code)
193 self.create_default_config()
172 self.create_default_config()
194 self.log_default_config()
173 self.log_default_config()
195 self.set_default_config_log_level()
174 self.set_default_config_log_level()
196
175
197 if self.override_config is None:
176 # Command-line config
198 # Command-line config
177 self.pre_load_command_line_config()
199 self.pre_load_command_line_config()
178 self.load_command_line_config()
200 self.load_command_line_config()
179 self.set_command_line_config_log_level()
201 self.set_command_line_config_log_level()
180 self.post_load_command_line_config()
202 self.post_load_command_line_config()
181 self.log_command_line_config()
203 self.log_command_line_config()
204
182
205 # Find resources needed for filesystem access, using information from
183 # Find resources needed for filesystem access, using information from
206 # the above two
184 # the above two
207 self.find_ipython_dir()
185 self.find_ipython_dir()
208 self.find_resources()
186 self.find_resources()
209 self.find_config_file_name()
187 self.find_config_file_name()
210 self.find_config_file_paths()
188 self.find_config_file_paths()
211
189
212 if self.override_config is None:
190 # File-based config
213 # File-based config
191 self.pre_load_file_config()
214 self.pre_load_file_config()
192 self.load_file_config()
215 self.load_file_config()
193 self.set_file_config_log_level()
216 self.set_file_config_log_level()
194 self.post_load_file_config()
217 self.post_load_file_config()
195 self.log_file_config()
218 self.log_file_config()
219
196
220 # Merge all config objects into a single one the app can then use
197 # Merge all config objects into a single one the app can then use
221 self.merge_configs()
198 self.merge_configs()
222 self.log_master_config()
199 self.log_master_config()
223
200
224 # Construction phase
201 # Construction phase
225 self.pre_construct()
202 self.pre_construct()
226 self.construct()
203 self.construct()
227 self.post_construct()
204 self.post_construct()
228
205
229 # Done, flag as such and
206 # Done, flag as such and
230 self._initialized = True
207 self._initialized = True
231
208
232 def start(self):
209 def start(self):
233 """Start the application."""
210 """Start the application."""
234 self.initialize()
211 self.initialize()
235 self.start_app()
212 self.start_app()
236
213
237 #-------------------------------------------------------------------------
214 #-------------------------------------------------------------------------
238 # Various stages of Application creation
215 # Various stages of Application creation
239 #-------------------------------------------------------------------------
216 #-------------------------------------------------------------------------
240
217
241 def create_crash_handler(self):
218 def create_crash_handler(self):
242 """Create a crash handler, typically setting sys.excepthook to it."""
219 """Create a crash handler, typically setting sys.excepthook to it."""
243 self.crash_handler = self._CrashHandler(self, self.name)
220 self.crash_handler = self._CrashHandler(self, self.name)
244 sys.excepthook = self.crash_handler
221 sys.excepthook = self.crash_handler
245
222
246 def create_default_config(self):
223 def create_default_config(self):
247 """Create defaults that can't be set elsewhere.
224 """Create defaults that can't be set elsewhere.
248
225
249 For the most part, we try to set default in the class attributes
226 For the most part, we try to set default in the class attributes
250 of Components. But, defaults the top-level Application (which is
227 of Components. But, defaults the top-level Application (which is
251 not a HasTraitlets or Component) are not set in this way. Instead
228 not a HasTraitlets or Component) are not set in this way. Instead
252 we set them here. The Global section is for variables like this that
229 we set them here. The Global section is for variables like this that
253 don't belong to a particular component.
230 don't belong to a particular component.
254 """
231 """
255 c = Config()
232 c = Config()
256 c.Global.ipython_dir = get_ipython_dir()
233 c.Global.ipython_dir = get_ipython_dir()
257 c.Global.log_level = self.log_level
234 c.Global.log_level = self.log_level
258 self.default_config = c
235 self.default_config = c
259
236
260 def log_default_config(self):
237 def log_default_config(self):
261 self.log.debug('Default config loaded:')
238 self.log.debug('Default config loaded:')
262 self.log.debug(repr(self.default_config))
239 self.log.debug(repr(self.default_config))
263
240
264 def set_default_config_log_level(self):
241 def set_default_config_log_level(self):
265 try:
242 try:
266 self.log_level = self.default_config.Global.log_level
243 self.log_level = self.default_config.Global.log_level
267 except AttributeError:
244 except AttributeError:
268 # Fallback to the default_log_level class attribute
245 # Fallback to the default_log_level class attribute
269 pass
246 pass
270
247
271 def create_command_line_config(self):
248 def create_command_line_config(self):
272 """Create and return a command line config loader."""
249 """Create and return a command line config loader."""
273 return ArgParseConfigLoader(self.argv, self.cl_arguments,
250 return self.command_line_loader(
274 description=self.description,
251 self.argv,
275 version=release.version,
252 description=self.description,
276 usage=self.usage,
253 version=release.version,
277 )
254 usage=self.usage
255 )
278
256
279 def pre_load_command_line_config(self):
257 def pre_load_command_line_config(self):
280 """Do actions just before loading the command line config."""
258 """Do actions just before loading the command line config."""
281 pass
259 pass
282
260
283 def load_command_line_config(self):
261 def load_command_line_config(self):
284 """Load the command line config."""
262 """Load the command line config."""
285 loader = self.create_command_line_config()
263 loader = self.create_command_line_config()
286 self.command_line_config = loader.load_config()
264 self.command_line_config = loader.load_config()
287 self.extra_args = loader.get_extra_args()
265 self.extra_args = loader.get_extra_args()
288
266
289 def set_command_line_config_log_level(self):
267 def set_command_line_config_log_level(self):
290 try:
268 try:
291 self.log_level = self.command_line_config.Global.log_level
269 self.log_level = self.command_line_config.Global.log_level
292 except AttributeError:
270 except AttributeError:
293 pass
271 pass
294
272
295 def post_load_command_line_config(self):
273 def post_load_command_line_config(self):
296 """Do actions just after loading the command line config."""
274 """Do actions just after loading the command line config."""
297 pass
275 pass
298
276
299 def log_command_line_config(self):
277 def log_command_line_config(self):
300 self.log.debug("Command line config loaded:")
278 self.log.debug("Command line config loaded:")
301 self.log.debug(repr(self.command_line_config))
279 self.log.debug(repr(self.command_line_config))
302
280
303 def find_ipython_dir(self):
281 def find_ipython_dir(self):
304 """Set the IPython directory.
282 """Set the IPython directory.
305
283
306 This sets ``self.ipython_dir``, but the actual value that is passed to
284 This sets ``self.ipython_dir``, but the actual value that is passed to
307 the application is kept in either ``self.default_config`` or
285 the application is kept in either ``self.default_config`` or
308 ``self.command_line_config``. This also adds ``self.ipython_dir`` to
286 ``self.command_line_config``. This also adds ``self.ipython_dir`` to
309 ``sys.path`` so config files there can be referenced by other config
287 ``sys.path`` so config files there can be referenced by other config
310 files.
288 files.
311 """
289 """
312
290
313 try:
291 try:
314 self.ipython_dir = self.command_line_config.Global.ipython_dir
292 self.ipython_dir = self.command_line_config.Global.ipython_dir
315 except AttributeError:
293 except AttributeError:
316 self.ipython_dir = self.default_config.Global.ipython_dir
294 self.ipython_dir = self.default_config.Global.ipython_dir
317 sys.path.append(os.path.abspath(self.ipython_dir))
295 sys.path.append(os.path.abspath(self.ipython_dir))
318 if not os.path.isdir(self.ipython_dir):
296 if not os.path.isdir(self.ipython_dir):
319 os.makedirs(self.ipython_dir, mode=0777)
297 os.makedirs(self.ipython_dir, mode=0777)
320 self.log.debug("IPYTHON_DIR set to: %s" % self.ipython_dir)
298 self.log.debug("IPYTHON_DIR set to: %s" % self.ipython_dir)
321
299
322 def find_resources(self):
300 def find_resources(self):
323 """Find other resources that need to be in place.
301 """Find other resources that need to be in place.
324
302
325 Things like cluster directories need to be in place to find the
303 Things like cluster directories need to be in place to find the
326 config file. These happen right after the IPython directory has
304 config file. These happen right after the IPython directory has
327 been set.
305 been set.
328 """
306 """
329 pass
307 pass
330
308
331 def find_config_file_name(self):
309 def find_config_file_name(self):
332 """Find the config file name for this application.
310 """Find the config file name for this application.
333
311
334 This must set ``self.config_file_name`` to the filename of the
312 This must set ``self.config_file_name`` to the filename of the
335 config file to use (just the filename). The search paths for the
313 config file to use (just the filename). The search paths for the
336 config file are set in :meth:`find_config_file_paths` and then passed
314 config file are set in :meth:`find_config_file_paths` and then passed
337 to the config file loader where they are resolved to an absolute path.
315 to the config file loader where they are resolved to an absolute path.
338
316
339 If a profile has been set at the command line, this will resolve it.
317 If a profile has been set at the command line, this will resolve it.
340 """
318 """
341
319
342 try:
320 try:
343 self.config_file_name = self.command_line_config.Global.config_file
321 self.config_file_name = self.command_line_config.Global.config_file
344 except AttributeError:
322 except AttributeError:
345 pass
323 pass
346
324
347 try:
325 try:
348 self.profile_name = self.command_line_config.Global.profile
326 self.profile_name = self.command_line_config.Global.profile
349 except AttributeError:
327 except AttributeError:
350 pass
328 pass
351 else:
329 else:
352 name_parts = self.config_file_name.split('.')
330 name_parts = self.config_file_name.split('.')
353 name_parts.insert(1, u'_' + self.profile_name + u'.')
331 name_parts.insert(1, u'_' + self.profile_name + u'.')
354 self.config_file_name = ''.join(name_parts)
332 self.config_file_name = ''.join(name_parts)
355
333
356 def find_config_file_paths(self):
334 def find_config_file_paths(self):
357 """Set the search paths for resolving the config file.
335 """Set the search paths for resolving the config file.
358
336
359 This must set ``self.config_file_paths`` to a sequence of search
337 This must set ``self.config_file_paths`` to a sequence of search
360 paths to pass to the config file loader.
338 paths to pass to the config file loader.
361 """
339 """
362 # Include our own profiles directory last, so that users can still find
340 # Include our own profiles directory last, so that users can still find
363 # our shipped copies of builtin profiles even if they don't have them
341 # our shipped copies of builtin profiles even if they don't have them
364 # in their local ipython directory.
342 # in their local ipython directory.
365 prof_dir = os.path.join(get_ipython_package_dir(), 'config', 'profile')
343 prof_dir = os.path.join(get_ipython_package_dir(), 'config', 'profile')
366 self.config_file_paths = (os.getcwd(), self.ipython_dir, prof_dir)
344 self.config_file_paths = (os.getcwd(), self.ipython_dir, prof_dir)
367
345
368 def pre_load_file_config(self):
346 def pre_load_file_config(self):
369 """Do actions before the config file is loaded."""
347 """Do actions before the config file is loaded."""
370 pass
348 pass
371
349
372 def load_file_config(self):
350 def load_file_config(self):
373 """Load the config file.
351 """Load the config file.
374
352
375 This tries to load the config file from disk. If successful, the
353 This tries to load the config file from disk. If successful, the
376 ``CONFIG_FILE`` config variable is set to the resolved config file
354 ``CONFIG_FILE`` config variable is set to the resolved config file
377 location. If not successful, an empty config is used.
355 location. If not successful, an empty config is used.
378 """
356 """
379 self.log.debug("Attempting to load config file: %s" %
357 self.log.debug("Attempting to load config file: %s" %
380 self.config_file_name)
358 self.config_file_name)
381 loader = PyFileConfigLoader(self.config_file_name,
359 loader = PyFileConfigLoader(self.config_file_name,
382 path=self.config_file_paths)
360 path=self.config_file_paths)
383 try:
361 try:
384 self.file_config = loader.load_config()
362 self.file_config = loader.load_config()
385 self.file_config.Global.config_file = loader.full_filename
363 self.file_config.Global.config_file = loader.full_filename
386 except IOError:
364 except IOError:
387 # Only warn if the default config file was NOT being used.
365 # Only warn if the default config file was NOT being used.
388 if not self.config_file_name==self.default_config_file_name:
366 if not self.config_file_name==self.default_config_file_name:
389 self.log.warn("Config file not found, skipping: %s" %
367 self.log.warn("Config file not found, skipping: %s" %
390 self.config_file_name, exc_info=True)
368 self.config_file_name, exc_info=True)
391 self.file_config = Config()
369 self.file_config = Config()
392 except:
370 except:
393 self.log.warn("Error loading config file: %s" %
371 self.log.warn("Error loading config file: %s" %
394 self.config_file_name, exc_info=True)
372 self.config_file_name, exc_info=True)
395 self.file_config = Config()
373 self.file_config = Config()
396
374
397 def set_file_config_log_level(self):
375 def set_file_config_log_level(self):
398 # We need to keeep self.log_level updated. But we only use the value
376 # We need to keeep self.log_level updated. But we only use the value
399 # of the file_config if a value was not specified at the command
377 # of the file_config if a value was not specified at the command
400 # line, because the command line overrides everything.
378 # line, because the command line overrides everything.
401 if not hasattr(self.command_line_config.Global, 'log_level'):
379 if not hasattr(self.command_line_config.Global, 'log_level'):
402 try:
380 try:
403 self.log_level = self.file_config.Global.log_level
381 self.log_level = self.file_config.Global.log_level
404 except AttributeError:
382 except AttributeError:
405 pass # Use existing value
383 pass # Use existing value
406
384
407 def post_load_file_config(self):
385 def post_load_file_config(self):
408 """Do actions after the config file is loaded."""
386 """Do actions after the config file is loaded."""
409 pass
387 pass
410
388
411 def log_file_config(self):
389 def log_file_config(self):
412 if hasattr(self.file_config.Global, 'config_file'):
390 if hasattr(self.file_config.Global, 'config_file'):
413 self.log.debug("Config file loaded: %s" %
391 self.log.debug("Config file loaded: %s" %
414 self.file_config.Global.config_file)
392 self.file_config.Global.config_file)
415 self.log.debug(repr(self.file_config))
393 self.log.debug(repr(self.file_config))
416
394
417 def merge_configs(self):
395 def merge_configs(self):
418 """Merge the default, command line and file config objects."""
396 """Merge the default, command line and file config objects."""
419 config = Config()
397 config = Config()
420 config._merge(self.default_config)
398 config._merge(self.default_config)
421 if self.override_config is None:
399 config._merge(self.file_config)
422 config._merge(self.file_config)
400 config._merge(self.command_line_config)
423 config._merge(self.command_line_config)
401
424 if self.constructor_config is not None:
425 config._merge(self.constructor_config)
426 else:
427 config._merge(self.override_config)
428 # XXX fperez - propose to Brian we rename master_config to simply
402 # XXX fperez - propose to Brian we rename master_config to simply
429 # config, I think this is going to be heavily used in examples and
403 # config, I think this is going to be heavily used in examples and
430 # application code and the name is shorter/easier to find/remember.
404 # application code and the name is shorter/easier to find/remember.
431 # For now, just alias it...
405 # For now, just alias it...
432 self.master_config = config
406 self.master_config = config
433 self.config = config
407 self.config = config
434
408
435 def log_master_config(self):
409 def log_master_config(self):
436 self.log.debug("Master config created:")
410 self.log.debug("Master config created:")
437 self.log.debug(repr(self.master_config))
411 self.log.debug(repr(self.master_config))
438
412
439 def pre_construct(self):
413 def pre_construct(self):
440 """Do actions after the config has been built, but before construct."""
414 """Do actions after the config has been built, but before construct."""
441 pass
415 pass
442
416
443 def construct(self):
417 def construct(self):
444 """Construct the main components that make up this app."""
418 """Construct the main components that make up this app."""
445 self.log.debug("Constructing components for application")
419 self.log.debug("Constructing components for application")
446
420
447 def post_construct(self):
421 def post_construct(self):
448 """Do actions after construct, but before starting the app."""
422 """Do actions after construct, but before starting the app."""
449 pass
423 pass
450
424
451 def start_app(self):
425 def start_app(self):
452 """Actually start the app."""
426 """Actually start the app."""
453 self.log.debug("Starting application")
427 self.log.debug("Starting application")
454
428
455 #-------------------------------------------------------------------------
429 #-------------------------------------------------------------------------
456 # Utility methods
430 # Utility methods
457 #-------------------------------------------------------------------------
431 #-------------------------------------------------------------------------
458
432
459 def abort(self):
433 def abort(self):
460 """Abort the starting of the application."""
434 """Abort the starting of the application."""
461 if self._exiting:
435 if self._exiting:
462 pass
436 pass
463 else:
437 else:
464 self.log.critical("Aborting application: %s" % self.name, exc_info=True)
438 self.log.critical("Aborting application: %s" % self.name, exc_info=True)
465 self._exiting = True
439 self._exiting = True
466 sys.exit(1)
440 sys.exit(1)
467
441
468 def exit(self, exit_status=0):
442 def exit(self, exit_status=0):
469 if self._exiting:
443 if self._exiting:
470 pass
444 pass
471 else:
445 else:
472 self.log.debug("Exiting application: %s" % self.name)
446 self.log.debug("Exiting application: %s" % self.name)
473 self._exiting = True
447 self._exiting = True
474 sys.exit(exit_status)
448 sys.exit(exit_status)
475
449
476 def attempt(self, func, action='abort'):
450 def attempt(self, func, action='abort'):
477 try:
451 try:
478 func()
452 func()
479 except SystemExit:
453 except SystemExit:
480 raise
454 raise
481 except:
455 except:
482 if action == 'abort':
456 if action == 'abort':
483 self.log.critical("Aborting application: %s" % self.name,
457 self.log.critical("Aborting application: %s" % self.name,
484 exc_info=True)
458 exc_info=True)
485 self.abort()
459 self.abort()
486 raise
460 raise
487 elif action == 'exit':
461 elif action == 'exit':
488 self.exit(0)
462 self.exit(0)
489
463
@@ -1,223 +1,224 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """sys.excepthook for IPython itself, leaves a detailed report on disk.
2 """sys.excepthook for IPython itself, leaves a detailed report on disk.
3
3
4
4
5 Authors
5 Authors
6 -------
6 -------
7 - Fernando Perez <Fernando.Perez@berkeley.edu>
7 - Fernando Perez <Fernando.Perez@berkeley.edu>
8 """
8 """
9
9
10 #*****************************************************************************
10 #*****************************************************************************
11 # Copyright (C) 2008-2009 The IPython Development Team
11 # Copyright (C) 2008-2009 The IPython Development Team
12 # Copyright (C) 2001-2007 Fernando Perez. <fperez@colorado.edu>
12 # Copyright (C) 2001-2007 Fernando Perez. <fperez@colorado.edu>
13 #
13 #
14 # Distributed under the terms of the BSD License. The full license is in
14 # Distributed under the terms of the BSD License. The full license is in
15 # the file COPYING, distributed as part of this software.
15 # the file COPYING, distributed as part of this software.
16 #*****************************************************************************
16 #*****************************************************************************
17
17
18 #****************************************************************************
18 #****************************************************************************
19 # Required modules
19 # Required modules
20
20
21 # From the standard library
21 # From the standard library
22 import os
22 import os
23 import sys
23 import sys
24 from pprint import pformat
24 from pprint import pformat
25
25
26 # Our own
26 # Our own
27 from IPython.core import release
27 from IPython.core import release
28 from IPython.core import ultratb
28 from IPython.core import ultratb
29 from IPython.utils.sysinfo import sys_info
29 from IPython.utils.sysinfo import sys_info
30
30
31 from IPython.external.Itpl import itpl
31 from IPython.external.Itpl import itpl
32
32
33 #****************************************************************************
33 #****************************************************************************
34
34
35 class CrashHandler(object):
35 class CrashHandler(object):
36 """Customizable crash handlers for IPython-based systems.
36 """Customizable crash handlers for IPython-based systems.
37
37
38 Instances of this class provide a __call__ method which can be used as a
38 Instances of this class provide a __call__ method which can be used as a
39 sys.excepthook, i.e., the __call__ signature is:
39 sys.excepthook, i.e., the __call__ signature is:
40
40
41 def __call__(self,etype, evalue, etb)
41 def __call__(self,etype, evalue, etb)
42
42
43 """
43 """
44
44
45 def __init__(self,app, app_name, contact_name=None, contact_email=None,
45 def __init__(self,app, app_name, contact_name=None, contact_email=None,
46 bug_tracker=None, crash_report_fname='CrashReport.txt',
46 bug_tracker=None, crash_report_fname='CrashReport.txt',
47 show_crash_traceback=True, call_pdb=False):
47 show_crash_traceback=True, call_pdb=False):
48 """New crash handler.
48 """New crash handler.
49
49
50 Inputs:
50 Inputs:
51
51
52 - app: a running application instance, which will be queried at crash
52 - app: a running application instance, which will be queried at crash
53 time for internal information.
53 time for internal information.
54
54
55 - app_name: a string containing the name of your application.
55 - app_name: a string containing the name of your application.
56
56
57 - contact_name: a string with the name of the person to contact.
57 - contact_name: a string with the name of the person to contact.
58
58
59 - contact_email: a string with the email address of the contact.
59 - contact_email: a string with the email address of the contact.
60
60
61 - bug_tracker: a string with the URL for your project's bug tracker.
61 - bug_tracker: a string with the URL for your project's bug tracker.
62
62
63 - crash_report_fname: a string with the filename for the crash report
63 - crash_report_fname: a string with the filename for the crash report
64 to be saved in. These reports are left in the ipython user directory
64 to be saved in. These reports are left in the ipython user directory
65 as determined by the running IPython instance.
65 as determined by the running IPython instance.
66
66
67 Optional inputs:
67 Optional inputs:
68
68
69 - show_crash_traceback(True): if false, don't print the crash
69 - show_crash_traceback(True): if false, don't print the crash
70 traceback on stderr, only generate the on-disk report
70 traceback on stderr, only generate the on-disk report
71
71
72
72
73 Non-argument instance attributes:
73 Non-argument instance attributes:
74
74
75 These instances contain some non-argument attributes which allow for
75 These instances contain some non-argument attributes which allow for
76 further customization of the crash handler's behavior. Please see the
76 further customization of the crash handler's behavior. Please see the
77 source for further details.
77 source for further details.
78 """
78 """
79
79
80 # apply args into instance
80 # apply args into instance
81 self.app = app
81 self.app = app
82 self.app_name = app_name
82 self.app_name = app_name
83 self.contact_name = contact_name
83 self.contact_name = contact_name
84 self.contact_email = contact_email
84 self.contact_email = contact_email
85 self.bug_tracker = bug_tracker
85 self.bug_tracker = bug_tracker
86 self.crash_report_fname = crash_report_fname
86 self.crash_report_fname = crash_report_fname
87 self.show_crash_traceback = show_crash_traceback
87 self.show_crash_traceback = show_crash_traceback
88 self.section_sep = '\n\n'+'*'*75+'\n\n'
88 self.section_sep = '\n\n'+'*'*75+'\n\n'
89 self.call_pdb = call_pdb
89 self.call_pdb = call_pdb
90 #self.call_pdb = True # dbg
90 #self.call_pdb = True # dbg
91
91
92 # Hardcoded defaults, which can be overridden either by subclasses or
92 # Hardcoded defaults, which can be overridden either by subclasses or
93 # at runtime for the instance.
93 # at runtime for the instance.
94
94
95 # Template for the user message. Subclasses which completely override
95 # Template for the user message. Subclasses which completely override
96 # this, or user apps, can modify it to suit their tastes. It gets
96 # this, or user apps, can modify it to suit their tastes. It gets
97 # expanded using itpl, so calls of the kind $self.foo are valid.
97 # expanded using itpl, so calls of the kind $self.foo are valid.
98 self.user_message_template = """
98 self.user_message_template = """
99 Oops, $self.app_name crashed. We do our best to make it stable, but...
99 Oops, $self.app_name crashed. We do our best to make it stable, but...
100
100
101 A crash report was automatically generated with the following information:
101 A crash report was automatically generated with the following information:
102 - A verbatim copy of the crash traceback.
102 - A verbatim copy of the crash traceback.
103 - A copy of your input history during this session.
103 - A copy of your input history during this session.
104 - Data on your current $self.app_name configuration.
104 - Data on your current $self.app_name configuration.
105
105
106 It was left in the file named:
106 It was left in the file named:
107 \t'$self.crash_report_fname'
107 \t'$self.crash_report_fname'
108 If you can email this file to the developers, the information in it will help
108 If you can email this file to the developers, the information in it will help
109 them in understanding and correcting the problem.
109 them in understanding and correcting the problem.
110
110
111 You can mail it to: $self.contact_name at $self.contact_email
111 You can mail it to: $self.contact_name at $self.contact_email
112 with the subject '$self.app_name Crash Report'.
112 with the subject '$self.app_name Crash Report'.
113
113
114 If you want to do it now, the following command will work (under Unix):
114 If you want to do it now, the following command will work (under Unix):
115 mail -s '$self.app_name Crash Report' $self.contact_email < $self.crash_report_fname
115 mail -s '$self.app_name Crash Report' $self.contact_email < $self.crash_report_fname
116
116
117 To ensure accurate tracking of this issue, please file a report about it at:
117 To ensure accurate tracking of this issue, please file a report about it at:
118 $self.bug_tracker
118 $self.bug_tracker
119 """
119 """
120
120
121 def __call__(self,etype, evalue, etb):
121 def __call__(self,etype, evalue, etb):
122 """Handle an exception, call for compatible with sys.excepthook"""
122 """Handle an exception, call for compatible with sys.excepthook"""
123
123
124 # Report tracebacks shouldn't use color in general (safer for users)
124 # Report tracebacks shouldn't use color in general (safer for users)
125 color_scheme = 'NoColor'
125 color_scheme = 'NoColor'
126
126
127 # Use this ONLY for developer debugging (keep commented out for release)
127 # Use this ONLY for developer debugging (keep commented out for release)
128 #color_scheme = 'Linux' # dbg
128 #color_scheme = 'Linux' # dbg
129
129
130 try:
130 try:
131 rptdir = self.app.ipython_dir
131 rptdir = self.app.ipython_dir
132 except:
132 except:
133 rptdir = os.getcwd()
133 rptdir = os.getcwd()
134 if not os.path.isdir(rptdir):
134 if not os.path.isdir(rptdir):
135 rptdir = os.getcwd()
135 rptdir = os.getcwd()
136 report_name = os.path.join(rptdir,self.crash_report_fname)
136 report_name = os.path.join(rptdir,self.crash_report_fname)
137 # write the report filename into the instance dict so it can get
137 # write the report filename into the instance dict so it can get
138 # properly expanded out in the user message template
138 # properly expanded out in the user message template
139 self.crash_report_fname = report_name
139 self.crash_report_fname = report_name
140 TBhandler = ultratb.VerboseTB(color_scheme=color_scheme,
140 TBhandler = ultratb.VerboseTB(color_scheme=color_scheme,
141 long_header=1,
141 long_header=1,
142 call_pdb=self.call_pdb,
142 call_pdb=self.call_pdb,
143 )
143 )
144 if self.call_pdb:
144 if self.call_pdb:
145 TBhandler(etype,evalue,etb)
145 TBhandler(etype,evalue,etb)
146 return
146 return
147 else:
147 else:
148 traceback = TBhandler.text(etype,evalue,etb,context=31)
148 traceback = TBhandler.text(etype,evalue,etb,context=31)
149
149
150 # print traceback to screen
150 # print traceback to screen
151 if self.show_crash_traceback:
151 if self.show_crash_traceback:
152 print >> sys.stderr, traceback
152 print >> sys.stderr, traceback
153
153
154 # and generate a complete report on disk
154 # and generate a complete report on disk
155 try:
155 try:
156 report = open(report_name,'w')
156 report = open(report_name,'w')
157 except:
157 except:
158 print >> sys.stderr, 'Could not create crash report on disk.'
158 print >> sys.stderr, 'Could not create crash report on disk.'
159 return
159 return
160
160
161 # Inform user on stderr of what happened
161 # Inform user on stderr of what happened
162 msg = itpl('\n'+'*'*70+'\n'+self.user_message_template)
162 msg = itpl('\n'+'*'*70+'\n'+self.user_message_template)
163 print >> sys.stderr, msg
163 print >> sys.stderr, msg
164
164
165 # Construct report on disk
165 # Construct report on disk
166 report.write(self.make_report(traceback))
166 report.write(self.make_report(traceback))
167 report.close()
167 report.close()
168 raw_input("Hit <Enter> to quit this message (your terminal may close):")
168 raw_input("Hit <Enter> to quit this message (your terminal may close):")
169
169
170 def make_report(self,traceback):
170 def make_report(self,traceback):
171 """Return a string containing a crash report."""
171 """Return a string containing a crash report."""
172
172
173 sec_sep = self.section_sep
173 sec_sep = self.section_sep
174
174
175 report = ['*'*75+'\n\n'+'IPython post-mortem report\n\n']
175 report = ['*'*75+'\n\n'+'IPython post-mortem report\n\n']
176 rpt_add = report.append
176 rpt_add = report.append
177 rpt_add(sys_info())
177 rpt_add(sys_info())
178
178
179 try:
179 try:
180 config = pformat(self.app.config)
180 config = pformat(self.app.config)
181 rpt_add(sec_sep+'Current user configuration structure:\n\n')
181 rpt_add(sec_sep+'Current user configuration structure:\n\n')
182 rpt_add(config)
182 rpt_add(config)
183 except:
183 except:
184 pass
184 pass
185 rpt_add(sec_sep+'Crash traceback:\n\n' + traceback)
185 rpt_add(sec_sep+'Crash traceback:\n\n' + traceback)
186
186
187 return ''.join(report)
187 return ''.join(report)
188
188
189
189
190 class IPythonCrashHandler(CrashHandler):
190 class IPythonCrashHandler(CrashHandler):
191 """sys.excepthook for IPython itself, leaves a detailed report on disk."""
191 """sys.excepthook for IPython itself, leaves a detailed report on disk."""
192
192
193 def __init__(self, app, app_name='IPython'):
193 def __init__(self, app, app_name='IPython'):
194
194
195 # Set here which of the IPython authors should be listed as contact
195 # Set here which of the IPython authors should be listed as contact
196 AUTHOR_CONTACT = 'Fernando'
196 AUTHOR_CONTACT = 'Fernando'
197
197
198 # Set argument defaults
198 # Set argument defaults
199 bug_tracker = 'https://bugs.launchpad.net/ipython/+filebug'
199 bug_tracker = 'https://bugs.launchpad.net/ipython/+filebug'
200 contact_name,contact_email = release.authors[AUTHOR_CONTACT][:2]
200 contact_name,contact_email = release.authors[AUTHOR_CONTACT][:2]
201 crash_report_fname = 'IPython_crash_report.txt'
201 crash_report_fname = 'IPython_crash_report.txt'
202 # Call parent constructor
202 # Call parent constructor
203 CrashHandler.__init__(self,app,app_name,contact_name,contact_email,
203 CrashHandler.__init__(self,app,app_name,contact_name,contact_email,
204 bug_tracker,crash_report_fname)
204 bug_tracker,crash_report_fname)
205
205
206 def make_report(self,traceback):
206 def make_report(self,traceback):
207 """Return a string containing a crash report."""
207 """Return a string containing a crash report."""
208
208
209 sec_sep = self.section_sep
209 sec_sep = self.section_sep
210 # Start with parent report
210 # Start with parent report
211 report = [super(IPythonCrashHandler, self).make_report(traceback)]
211 report = [super(IPythonCrashHandler, self).make_report(traceback)]
212 # Add interactive-specific info we may have
212 # Add interactive-specific info we may have
213 rpt_add = report.append
213 rpt_add = report.append
214 try:
214 try:
215 rpt_add(sec_sep+"History of session input:")
215 rpt_add(sec_sep+"History of session input:")
216 for line in self.app.shell.user_ns['_ih']:
216 for line in self.app.shell.user_ns['_ih']:
217 rpt_add(line)
217 rpt_add(line)
218 rpt_add('\n*** Last line of input (may not be in above history):\n')
218 rpt_add('\n*** Last line of input (may not be in above history):\n')
219 rpt_add(self.app.shell._last_input_line+'\n')
219 rpt_add(self.app.shell._last_input_line+'\n')
220 except:
220 except:
221 pass
221 pass
222
222
223 return ''.join(report)
223 return ''.join(report)
224
This diff has been collapsed as it changes many lines, (608 lines changed) Show them Hide them
@@ -1,648 +1,590 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # encoding: utf-8
2 # encoding: utf-8
3 """
3 """
4 The :class:`~IPython.core.application.Application` object for the command
4 The :class:`~IPython.core.application.Application` object for the command
5 line :command:`ipython` program.
5 line :command:`ipython` program.
6
6
7 Authors
7 Authors
8 -------
8 -------
9
9
10 * Brian Granger
10 * Brian Granger
11 * Fernando Perez
11 * Fernando Perez
12 """
12 """
13
13
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 # Copyright (C) 2008-2010 The IPython Development Team
15 # Copyright (C) 2008-2010 The IPython Development Team
16 #
16 #
17 # Distributed under the terms of the BSD License. The full license is in
17 # Distributed under the terms of the BSD License. The full license is in
18 # the file COPYING, distributed as part of this software.
18 # the file COPYING, distributed as part of this software.
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20
20
21 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
22 # Imports
22 # Imports
23 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
24 from __future__ import absolute_import
24 from __future__ import absolute_import
25
25
26 import logging
26 import logging
27 import os
27 import os
28 import sys
28 import sys
29
29
30 from IPython.core import crashhandler
30 from IPython.core import crashhandler
31 from IPython.core.application import Application
31 from IPython.core.application import Application, BaseAppConfigLoader
32 from IPython.core.iplib import InteractiveShell
32 from IPython.core.iplib import InteractiveShell
33 from IPython.config.loader import (
33 from IPython.config.loader import (
34 Config,
34 Config,
35 PyFileConfigLoader
35 PyFileConfigLoader
36 )
36 )
37 from IPython.lib import inputhook
37 from IPython.lib import inputhook
38 from IPython.utils.path import filefind, get_ipython_dir
38 from IPython.utils.path import filefind, get_ipython_dir
39 from . import usage
39 from . import usage
40
40
41 #-----------------------------------------------------------------------------
41 #-----------------------------------------------------------------------------
42 # Globals, utilities and helpers
42 # Globals, utilities and helpers
43 #-----------------------------------------------------------------------------
43 #-----------------------------------------------------------------------------
44
44
45 #: The default config file name for this application.
45 default_config_file_name = u'ipython_config.py'
46 default_config_file_name = u'ipython_config.py'
46
47
47 cl_args = (
48
48 (('--autocall',), dict(
49 class IPAppConfigLoader(BaseAppConfigLoader):
49 type=int, dest='InteractiveShell.autocall',
50
50 help=
51 def _add_arguments(self):
51 """Make IPython automatically call any callable object even if you
52 super(IPAppConfigLoader, self)._add_arguments()
52 didn't type explicit parentheses. For example, 'str 43' becomes
53 paa = self.parser.add_argument
53 'str(43)' automatically. The value can be '0' to disable the feature,
54 paa('-p',
54 '1' for 'smart' autocall, where it is not applied if there are no more
55 '--profile', dest='Global.profile', type=unicode,
55 arguments on the line, and '2' for 'full' autocall, where all callable
56 help=
56 objects are automatically called (even if no arguments are present).
57 """The string name of the ipython profile to be used. Assume that your
57 The default is '1'.""",
58 config file is ipython_config-<name>.py (looks in current dir first,
58 metavar='InteractiveShell.autocall')
59 then in IPYTHON_DIR). This is a quick way to keep and load multiple
59 ),
60 config files for different tasks, especially if include your basic one
60 (('--autoindent',), dict(
61 in your more specialized ones. You can keep a basic
61 action='store_true', dest='InteractiveShell.autoindent',
62 IPYTHON_DIR/ipython_config.py file and then have other 'profiles' which
62 help='Turn on autoindenting.')
63 include this one and load extra things for particular tasks.""",
63 ),
64 metavar='Global.profile')
64 (('--no-autoindent',), dict(
65 paa('--config-file',
65 action='store_false', dest='InteractiveShell.autoindent',
66 dest='Global.config_file', type=unicode,
66 help='Turn off autoindenting.')
67 help=
67 ),
68 """Set the config file name to override default. Normally IPython
68 (('--automagic',), dict(
69 loads ipython_config.py (from current directory) or
69 action='store_true', dest='InteractiveShell.automagic',
70 IPYTHON_DIR/ipython_config.py. If the loading of your config file
70 help='Turn on the auto calling of magic commands.'
71 fails, IPython starts with a bare bones configuration (no modules
71 'Type %%magic at the IPython prompt for more information.')
72 loaded at all).""",
72 ),
73 metavar='Global.config_file')
73 (('--no-automagic',), dict(
74 paa('--autocall',
74 action='store_false', dest='InteractiveShell.automagic',
75 dest='InteractiveShell.autocall', type=int,
75 help='Turn off the auto calling of magic commands.')
76 help=
76 ),
77 """Make IPython automatically call any callable object even if you
77 (('--autoedit-syntax',), dict(
78 didn't type explicit parentheses. For example, 'str 43' becomes
78 action='store_true', dest='InteractiveShell.autoedit_syntax',
79 'str(43)' automatically. The value can be '0' to disable the feature,
79 help='Turn on auto editing of files with syntax errors.')
80 '1' for 'smart' autocall, where it is not applied if there are no more
80 ),
81 arguments on the line, and '2' for 'full' autocall, where all callable
81 (('--no-autoedit-syntax',), dict(
82 objects are automatically called (even if no arguments are present).
82 action='store_false', dest='InteractiveShell.autoedit_syntax',
83 The default is '1'.""",
83 help='Turn off auto editing of files with syntax errors.')
84 metavar='InteractiveShell.autocall')
84 ),
85 paa('--autoindent',
85 (('--banner',), dict(
86 action='store_true', dest='InteractiveShell.autoindent',
86 action='store_true', dest='Global.display_banner',
87 help='Turn on autoindenting.')
87 help='Display a banner upon starting IPython.')
88 paa('--no-autoindent',
88 ),
89 action='store_false', dest='InteractiveShell.autoindent',
89 (('--no-banner',), dict(
90 help='Turn off autoindenting.')
90 action='store_false', dest='Global.display_banner',
91 paa('--automagic',
91 help="Don't display a banner upon starting IPython.")
92 action='store_true', dest='InteractiveShell.automagic',
92 ),
93 help=
93 (('--cache-size',), dict(
94 """Turn on the auto calling of magic commands. Type %%magic at the
94 type=int, dest='InteractiveShell.cache_size',
95 IPython prompt for more information.""")
95 help=
96 paa('--no-automagic',
96 """Set the size of the output cache. The default is 1000, you can
97 action='store_false', dest='InteractiveShell.automagic',
97 change it permanently in your config file. Setting it to 0 completely
98 help='Turn off the auto calling of magic commands.')
98 disables the caching system, and the minimum value accepted is 20 (if
99 paa('--autoedit-syntax',
99 you provide a value less than 20, it is reset to 0 and a warning is
100 action='store_true', dest='InteractiveShell.autoedit_syntax',
100 issued). This limit is defined because otherwise you'll spend more
101 help='Turn on auto editing of files with syntax errors.')
101 time re-flushing a too small cache than working.
102 paa('--no-autoedit-syntax',
102 """,
103 action='store_false', dest='InteractiveShell.autoedit_syntax',
103 metavar='InteractiveShell.cache_size')
104 help='Turn off auto editing of files with syntax errors.')
104 ),
105 paa('--banner',
105 (('--classic',), dict(
106 action='store_true', dest='Global.display_banner',
106 action='store_true', dest='Global.classic',
107 help='Display a banner upon starting IPython.')
107 help="Gives IPython a similar feel to the classic Python prompt.")
108 paa('--no-banner',
108 ),
109 action='store_false', dest='Global.display_banner',
109 (('--colors',), dict(
110 help="Don't display a banner upon starting IPython.")
110 type=str, dest='InteractiveShell.colors',
111 paa('--cache-size',
111 help="Set the color scheme (NoColor, Linux, and LightBG).",
112 type=int, dest='InteractiveShell.cache_size',
112 metavar='InteractiveShell.colors')
113 help=
113 ),
114 """Set the size of the output cache. The default is 1000, you can
114 (('--color-info',), dict(
115 change it permanently in your config file. Setting it to 0 completely
115 action='store_true', dest='InteractiveShell.color_info',
116 disables the caching system, and the minimum value accepted is 20 (if
116 help=
117 you provide a value less than 20, it is reset to 0 and a warning is
117 """IPython can display information about objects via a set of func-
118 issued). This limit is defined because otherwise you'll spend more
118 tions, and optionally can use colors for this, syntax highlighting
119 time re-flushing a too small cache than working""",
119 source code and various other elements. However, because this
120 metavar='InteractiveShell.cache_size')
120 information is passed through a pager (like 'less') and many pagers get
121 paa('--classic',
121 confused with color codes, this option is off by default. You can test
122 action='store_true', dest='Global.classic',
122 it and turn it on permanently in your ipython_config.py file if it
123 help="Gives IPython a similar feel to the classic Python prompt.")
123 works for you. Test it and turn it on permanently if it works with
124 paa('--colors',
124 your system. The magic function %%color_info allows you to toggle this
125 type=str, dest='InteractiveShell.colors',
125 inter- actively for testing."""
126 help="Set the color scheme (NoColor, Linux, and LightBG).",
126 )
127 metavar='InteractiveShell.colors')
127 ),
128 paa('--color-info',
128 (('--no-color-info',), dict(
129 action='store_true', dest='InteractiveShell.color_info',
129 action='store_false', dest='InteractiveShell.color_info',
130 help=
130 help="Disable using colors for info related things.")
131 """IPython can display information about objects via a set of func-
131 ),
132 tions, and optionally can use colors for this, syntax highlighting
132 (('--confirm-exit',), dict(
133 source code and various other elements. However, because this
133 action='store_true', dest='InteractiveShell.confirm_exit',
134 information is passed through a pager (like 'less') and many pagers get
134 help=
135 confused with color codes, this option is off by default. You can test
135 """Set to confirm when you try to exit IPython with an EOF (Control-D
136 it and turn it on permanently in your ipython_config.py file if it
136 in Unix, Control-Z/Enter in Windows). By typing 'exit', 'quit' or
137 works for you. Test it and turn it on permanently if it works with
137 '%%Exit', you can force a direct exit without any confirmation.
138 your system. The magic function %%color_info allows you to toggle this
138 """
139 inter- actively for testing.""")
139 )
140 paa('--no-color-info',
140 ),
141 action='store_false', dest='InteractiveShell.color_info',
141 (('--no-confirm-exit',), dict(
142 help="Disable using colors for info related things.")
142 action='store_false', dest='InteractiveShell.confirm_exit',
143 paa('--confirm-exit',
143 help="Don't prompt the user when exiting.")
144 action='store_true', dest='InteractiveShell.confirm_exit',
144 ),
145 help=
145 (('--deep-reload',), dict(
146 """Set to confirm when you try to exit IPython with an EOF (Control-D
146 action='store_true', dest='InteractiveShell.deep_reload',
147 in Unix, Control-Z/Enter in Windows). By typing 'exit', 'quit' or
147 help=
148 '%%Exit', you can force a direct exit without any confirmation.""")
148 """Enable deep (recursive) reloading by default. IPython can use the
149 paa('--no-confirm-exit',
149 deep_reload module which reloads changes in modules recursively (it
150 action='store_false', dest='InteractiveShell.confirm_exit',
150 replaces the reload() function, so you don't need to change anything to
151 help="Don't prompt the user when exiting.")
151 use it). deep_reload() forces a full reload of modules whose code may
152 paa('--deep-reload',
152 have changed, which the default reload() function does not. When
153 action='store_true', dest='InteractiveShell.deep_reload',
153 deep_reload is off, IPython will use the normal reload(), but
154 help=
154 deep_reload will still be available as dreload(). This fea- ture is off
155 """Enable deep (recursive) reloading by default. IPython can use the
155 by default [which means that you have both normal reload() and
156 deep_reload module which reloads changes in modules recursively (it
156 dreload()].""")
157 replaces the reload() function, so you don't need to change anything to
157 ),
158 use it). deep_reload() forces a full reload of modules whose code may
158 (('--no-deep-reload',), dict(
159 have changed, which the default reload() function does not. When
159 action='store_false', dest='InteractiveShell.deep_reload',
160 deep_reload is off, IPython will use the normal reload(), but
160 help="Disable deep (recursive) reloading by default.")
161 deep_reload will still be available as dreload(). This fea- ture is off
161 ),
162 by default [which means that you have both normal reload() and
162 (('--editor',), dict(
163 dreload()].""")
163 type=str, dest='InteractiveShell.editor',
164 paa('--no-deep-reload',
164 help="Set the editor used by IPython (default to $EDITOR/vi/notepad).",
165 action='store_false', dest='InteractiveShell.deep_reload',
165 metavar='InteractiveShell.editor')
166 help="Disable deep (recursive) reloading by default.")
166 ),
167 paa('--editor',
167 (('--log','-l'), dict(
168 type=str, dest='InteractiveShell.editor',
168 action='store_true', dest='InteractiveShell.logstart',
169 help="Set the editor used by IPython (default to $EDITOR/vi/notepad).",
169 help="Start logging to the default log file (./ipython_log.py).")
170 metavar='InteractiveShell.editor')
170 ),
171 paa('--log','-l',
171 (('--logfile','-lf'), dict(
172 action='store_true', dest='InteractiveShell.logstart',
172 type=unicode, dest='InteractiveShell.logfile',
173 help="Start logging to the default log file (./ipython_log.py).")
173 help="Start logging to logfile with this name.",
174 paa('--logfile','-lf',
174 metavar='InteractiveShell.logfile')
175 type=unicode, dest='InteractiveShell.logfile',
175 ),
176 help="Start logging to logfile with this name.",
176 (('--log-append','-la'), dict(
177 metavar='InteractiveShell.logfile')
177 type=unicode, dest='InteractiveShell.logappend',
178 paa('--log-append','-la',
178 help="Start logging to the given file in append mode.",
179 type=unicode, dest='InteractiveShell.logappend',
179 metavar='InteractiveShell.logfile')
180 help="Start logging to the given file in append mode.",
180 ),
181 metavar='InteractiveShell.logfile')
181 (('--pdb',), dict(
182 paa('--pdb',
182 action='store_true', dest='InteractiveShell.pdb',
183 action='store_true', dest='InteractiveShell.pdb',
183 help="Enable auto calling the pdb debugger after every exception.")
184 help="Enable auto calling the pdb debugger after every exception.")
184 ),
185 paa('--no-pdb',
185 (('--no-pdb',), dict(
186 action='store_false', dest='InteractiveShell.pdb',
186 action='store_false', dest='InteractiveShell.pdb',
187 help="Disable auto calling the pdb debugger after every exception.")
187 help="Disable auto calling the pdb debugger after every exception.")
188 paa('--pprint',
188 ),
189 action='store_true', dest='InteractiveShell.pprint',
189 (('--pprint',), dict(
190 help="Enable auto pretty printing of results.")
190 action='store_true', dest='InteractiveShell.pprint',
191 paa('--no-pprint',
191 help="Enable auto pretty printing of results.")
192 action='store_false', dest='InteractiveShell.pprint',
192 ),
193 help="Disable auto auto pretty printing of results.")
193 (('--no-pprint',), dict(
194 paa('--prompt-in1','-pi1',
194 action='store_false', dest='InteractiveShell.pprint',
195 type=str, dest='InteractiveShell.prompt_in1',
195 help="Disable auto auto pretty printing of results.")
196 help=
196 ),
197 """Set the main input prompt ('In [\#]: '). Note that if you are using
197 (('--prompt-in1','-pi1'), dict(
198 numbered prompts, the number is represented with a '\#' in the string.
198 type=str, dest='InteractiveShell.prompt_in1',
199 Don't forget to quote strings with spaces embedded in them. Most
199 help=
200 bash-like escapes can be used to customize IPython's prompts, as well
200 """Set the main input prompt ('In [\#]: '). Note that if you are using
201 as a few additional ones which are IPython-spe- cific. All valid
201 numbered prompts, the number is represented with a '\#' in the string.
202 prompt escapes are described in detail in the Customization section of
202 Don't forget to quote strings with spaces embedded in them. Most
203 the IPython manual.""",
203 bash-like escapes can be used to customize IPython's prompts, as well
204 metavar='InteractiveShell.prompt_in1')
204 as a few additional ones which are IPython-spe- cific. All valid
205 paa('--prompt-in2','-pi2',
205 prompt escapes are described in detail in the Customization section of
206 type=str, dest='InteractiveShell.prompt_in2',
206 the IPython manual.""",
207 help=
207 metavar='InteractiveShell.prompt_in1')
208 """Set the secondary input prompt (' .\D.: '). Similar to the previous
208 ),
209 option, but used for the continuation prompts. The special sequence
209 (('--prompt-in2','-pi2'), dict(
210 '\D' is similar to '\#', but with all digits replaced by dots (so you
210 type=str, dest='InteractiveShell.prompt_in2',
211 can have your continuation prompt aligned with your input prompt).
211 help=
212 Default: ' .\D.: ' (note three spaces at the start for alignment with
212 """Set the secondary input prompt (' .\D.: '). Similar to the previous
213 'In [\#]')""",
213 option, but used for the continuation prompts. The special sequence
214 metavar='InteractiveShell.prompt_in2')
214 '\D' is similar to '\#', but with all digits replaced by dots (so you
215 paa('--prompt-out','-po',
215 can have your continuation prompt aligned with your input prompt).
216 type=str, dest='InteractiveShell.prompt_out',
216 Default: ' .\D.: ' (note three spaces at the start for alignment with
217 help="Set the output prompt ('Out[\#]:')",
217 'In [\#]')""",
218 metavar='InteractiveShell.prompt_out')
218 metavar='InteractiveShell.prompt_in2')
219 paa('--quick',
219 ),
220 action='store_true', dest='Global.quick',
220 (('--prompt-out','-po'), dict(
221 help="Enable quick startup with no config files.")
221 type=str, dest='InteractiveShell.prompt_out',
222 paa('--readline',
222 help="Set the output prompt ('Out[\#]:')",
223 action='store_true', dest='InteractiveShell.readline_use',
223 metavar='InteractiveShell.prompt_out')
224 help="Enable readline for command line usage.")
224 ),
225 paa('--no-readline',
225 (('--quick',), dict(
226 action='store_false', dest='InteractiveShell.readline_use',
226 action='store_true', dest='Global.quick',
227 help="Disable readline for command line usage.")
227 help="Enable quick startup with no config files.")
228 paa('--screen-length','-sl',
228 ),
229 type=int, dest='InteractiveShell.screen_length',
229 (('--readline',), dict(
230 help=
230 action='store_true', dest='InteractiveShell.readline_use',
231 """Number of lines of your screen, used to control printing of very
231 help="Enable readline for command line usage.")
232 long strings. Strings longer than this number of lines will be sent
232 ),
233 through a pager instead of directly printed. The default value for
233 (('--no-readline',), dict(
234 this is 0, which means IPython will auto-detect your screen size every
234 action='store_false', dest='InteractiveShell.readline_use',
235 time it needs to print certain potentially long strings (this doesn't
235 help="Disable readline for command line usage.")
236 change the behavior of the 'print' keyword, it's only triggered
236 ),
237 internally). If for some reason this isn't working well (it needs
237 (('--screen-length','-sl'), dict(
238 curses support), specify it yourself. Otherwise don't change the
238 type=int, dest='InteractiveShell.screen_length',
239 default.""",
239 help=
240 metavar='InteractiveShell.screen_length')
240 """Number of lines of your screen, used to control printing of very
241 paa('--separate-in','-si',
241 long strings. Strings longer than this number of lines will be sent
242 type=str, dest='InteractiveShell.separate_in',
242 through a pager instead of directly printed. The default value for
243 help="Separator before input prompts. Default '\\n'.",
243 this is 0, which means IPython will auto-detect your screen size every
244 metavar='InteractiveShell.separate_in')
244 time it needs to print certain potentially long strings (this doesn't
245 paa('--separate-out','-so',
245 change the behavior of the 'print' keyword, it's only triggered
246 type=str, dest='InteractiveShell.separate_out',
246 internally). If for some reason this isn't working well (it needs
247 help="Separator before output prompts. Default 0 (nothing).",
247 curses support), specify it yourself. Otherwise don't change the
248 metavar='InteractiveShell.separate_out')
248 default.""",
249 paa('--separate-out2','-so2',
249 metavar='InteractiveShell.screen_length')
250 type=str, dest='InteractiveShell.separate_out2',
250 ),
251 help="Separator after output prompts. Default 0 (nonight).",
251 (('--separate-in','-si'), dict(
252 metavar='InteractiveShell.separate_out2')
252 type=str, dest='InteractiveShell.separate_in',
253 paa('-no-sep',
253 help="Separator before input prompts. Default '\\n'.",
254 action='store_true', dest='Global.nosep',
254 metavar='InteractiveShell.separate_in')
255 help="Eliminate all spacing between prompts.")
255 ),
256 paa('--term-title',
256 (('--separate-out','-so'), dict(
257 action='store_true', dest='InteractiveShell.term_title',
257 type=str, dest='InteractiveShell.separate_out',
258 help="Enable auto setting the terminal title.")
258 help="Separator before output prompts. Default 0 (nothing).",
259 paa('--no-term-title',
259 metavar='InteractiveShell.separate_out')
260 action='store_false', dest='InteractiveShell.term_title',
260 ),
261 help="Disable auto setting the terminal title.")
261 (('--separate-out2','-so2'), dict(
262 paa('--xmode',
262 type=str, dest='InteractiveShell.separate_out2',
263 type=str, dest='InteractiveShell.xmode',
263 help="Separator after output prompts. Default 0 (nonight).",
264 help=
264 metavar='InteractiveShell.separate_out2')
265 """Exception reporting mode ('Plain','Context','Verbose'). Plain:
265 ),
266 similar to python's normal traceback printing. Context: prints 5 lines
266 (('-no-sep',), dict(
267 of context source code around each line in the traceback. Verbose:
267 action='store_true', dest='Global.nosep',
268 similar to Context, but additionally prints the variables currently
268 help="Eliminate all spacing between prompts.")
269 visible where the exception happened (shortening their strings if too
269 ),
270 long). This can potentially be very slow, if you happen to have a huge
270 (('--term-title',), dict(
271 data structure whose string representation is complex to compute.
271 action='store_true', dest='InteractiveShell.term_title',
272 Your computer may appear to freeze for a while with cpu usage at 100%%.
272 help="Enable auto setting the terminal title.")
273 If this occurs, you can cancel the traceback with Ctrl-C (maybe hitting
273 ),
274 it more than once).
274 (('--no-term-title',), dict(
275 """,
275 action='store_false', dest='InteractiveShell.term_title',
276 metavar='InteractiveShell.xmode')
276 help="Disable auto setting the terminal title.")
277 paa('--ext',
277 ),
278 type=str, dest='Global.extra_extension',
278 (('--xmode',), dict(
279 help="The dotted module name of an IPython extension to load.",
279 type=str, dest='InteractiveShell.xmode',
280 metavar='Global.extra_extension')
280 help=
281 paa('-c',
281 """Exception reporting mode ('Plain','Context','Verbose'). Plain:
282 type=str, dest='Global.code_to_run',
282 similar to python's normal traceback printing. Context: prints 5 lines
283 help="Execute the given command string.",
283 of context source code around each line in the traceback. Verbose:
284 metavar='Global.code_to_run')
284 similar to Context, but additionally prints the variables currently
285 paa('-i',
285 visible where the exception happened (shortening their strings if too
286 action='store_true', dest='Global.force_interact',
286 long). This can potentially be very slow, if you happen to have a huge
287 help=
287 data structure whose string representation is complex to compute.
288 "If running code from the command line, become interactive afterwards.")
288 Your computer may appear to freeze for a while with cpu usage at 100%%.
289
289 If this occurs, you can cancel the traceback with Ctrl-C (maybe hitting
290 # Options to start with GUI control enabled from the beginning
290 it more than once).
291 paa('--gui',
291 """,
292 type=str, dest='Global.gui',
292 metavar='InteractiveShell.xmode')
293 help="Enable GUI event loop integration ('qt', 'wx', 'gtk').",
293 ),
294 metavar='gui-mode')
294 (('--ext',), dict(
295 paa('--pylab','-pylab',
295 type=str, dest='Global.extra_extension',
296 type=str, dest='Global.pylab',
296 help="The dotted module name of an IPython extension to load.",
297 nargs='?', const='auto', metavar='gui-mode',
297 metavar='Global.extra_extension')
298 help="Pre-load matplotlib and numpy for interactive use. "+
298 ),
299 "If no value is given, the gui backend is matplotlib's, else use "+
299 (('-c',), dict(
300 "one of: ['tk', 'qt', 'wx', 'gtk'].")
300 type=str, dest='Global.code_to_run',
301
301 help="Execute the given command string.",
302 # Legacy GUI options. Leave them in for backwards compatibility, but the
302 metavar='Global.code_to_run')
303 # 'thread' names are really a misnomer now.
303 ),
304 paa('--wthread', '-wthread',
304 (('-i',), dict(
305 action='store_true', dest='Global.wthread',
305 action='store_true', dest='Global.force_interact',
306 help=
306 help=
307 """Enable wxPython event loop integration. (DEPRECATED, use --gui wx)""")
307 "If running code from the command line, become interactive afterwards."
308 paa('--q4thread', '--qthread', '-q4thread', '-qthread',
308 )
309 action='store_true', dest='Global.q4thread',
309 ),
310 help=
310
311 """Enable Qt4 event loop integration. Qt3 is no longer supported.
311 # Options to start with GUI control enabled from the beginning
312 (DEPRECATED, use --gui qt)""")
312 (('--gui',), dict(
313 paa('--gthread', '-gthread',
313 type=str, dest='Global.gui',
314 action='store_true', dest='Global.gthread',
314 help="Enable GUI event loop integration ('qt', 'wx', 'gtk').",
315 help=
315 metavar='gui-mode')
316 """Enable GTK event loop integration. (DEPRECATED, use --gui gtk)""")
316 ),
317
317
318 (('--pylab','-pylab'), dict(
319 type=str, dest='Global.pylab',
320 nargs='?', const='auto', metavar='gui-mode',
321 help="Pre-load matplotlib and numpy for interactive use. "+
322 "If no value is given, the gui backend is matplotlib's, else use "+
323 "one of: ['tk', 'qt', 'wx', 'gtk'].")
324 ),
325
326 # Legacy GUI options. Leave them in for backwards compatibility, but the
327 # 'thread' names are really a misnomer now.
328 (('--wthread','-wthread'), dict(
329 action='store_true', dest='Global.wthread',
330 help="Enable wxPython event loop integration "+
331 "(DEPRECATED, use --gui wx)")
332 ),
333 (('--q4thread','--qthread','-q4thread','-qthread'), dict(
334 action='store_true', dest='Global.q4thread',
335 help="Enable Qt4 event loop integration. Qt3 is no longer supported. "+
336 "(DEPRECATED, use --gui qt)")
337 ),
338 (('--gthread','-gthread'), dict(
339 action='store_true', dest='Global.gthread',
340 help="Enable GTK event loop integration. "+
341 "(DEPRECATED, use --gui gtk)")
342 ),
343 )
344
318
345 #-----------------------------------------------------------------------------
319 #-----------------------------------------------------------------------------
346 # Main classes and functions
320 # Main classes and functions
347 #-----------------------------------------------------------------------------
321 #-----------------------------------------------------------------------------
348
322
349 class IPythonApp(Application):
323 class IPythonApp(Application):
350 name = u'ipython'
324 name = u'ipython'
351 #: argparse formats better the 'usage' than the 'description' field
325 #: argparse formats better the 'usage' than the 'description' field
352 description = None
326 description = None
353 #: usage message printed by argparse. If None, auto-generate
354 usage = usage.cl_usage
327 usage = usage.cl_usage
355
328 command_line_loader = IPAppConfigLoader
356 config_file_name = default_config_file_name
329 config_file_name = default_config_file_name
357
330
358 cl_arguments = Application.cl_arguments + cl_args
359
360 # Private and configuration attributes
331 # Private and configuration attributes
361 _CrashHandler = crashhandler.IPythonCrashHandler
332 _CrashHandler = crashhandler.IPythonCrashHandler
362
363 def __init__(self, argv=None,
364 constructor_config=None, override_config=None,
365 **shell_params):
366 """Create a new IPythonApp.
367
368 See the parent class for details on how configuration is handled.
369
370 Parameters
371 ----------
372 argv : optional, list
373 If given, used as the command-line argv environment to read arguments
374 from.
375
376 constructor_config : optional, Config
377 If given, additional config that is merged last, after internal
378 defaults, command-line and file-based configs.
379
380 override_config : optional, Config
381 If given, config that overrides all others unconditionally (except
382 for internal defaults, which ensure that all parameters exist).
383
384 shell_params : optional, dict
385 All other keywords are passed to the :class:`iplib.InteractiveShell`
386 constructor.
387 """
388 super(IPythonApp, self).__init__(argv, constructor_config,
389 override_config)
390 self.shell_params = shell_params
391
333
392 def create_default_config(self):
334 def create_default_config(self):
393 super(IPythonApp, self).create_default_config()
335 super(IPythonApp, self).create_default_config()
394 # Eliminate multiple lookups
336 # Eliminate multiple lookups
395 Global = self.default_config.Global
337 Global = self.default_config.Global
396
338
397 # Set all default values
339 # Set all default values
398 Global.display_banner = True
340 Global.display_banner = True
399
341
400 # If the -c flag is given or a file is given to run at the cmd line
342 # If the -c flag is given or a file is given to run at the cmd line
401 # like "ipython foo.py", normally we exit without starting the main
343 # like "ipython foo.py", normally we exit without starting the main
402 # loop. The force_interact config variable allows a user to override
344 # loop. The force_interact config variable allows a user to override
403 # this and interact. It is also set by the -i cmd line flag, just
345 # this and interact. It is also set by the -i cmd line flag, just
404 # like Python.
346 # like Python.
405 Global.force_interact = False
347 Global.force_interact = False
406
348
407 # By default always interact by starting the IPython mainloop.
349 # By default always interact by starting the IPython mainloop.
408 Global.interact = True
350 Global.interact = True
409
351
410 # No GUI integration by default
352 # No GUI integration by default
411 Global.gui = False
353 Global.gui = False
412 # Pylab off by default
354 # Pylab off by default
413 Global.pylab = False
355 Global.pylab = False
414
356
415 # Deprecated versions of gui support that used threading, we support
357 # Deprecated versions of gui support that used threading, we support
416 # them just for bacwards compatibility as an alternate spelling for
358 # them just for bacwards compatibility as an alternate spelling for
417 # '--gui X'
359 # '--gui X'
418 Global.qthread = False
360 Global.qthread = False
419 Global.q4thread = False
361 Global.q4thread = False
420 Global.wthread = False
362 Global.wthread = False
421 Global.gthread = False
363 Global.gthread = False
422
364
423 def load_file_config(self):
365 def load_file_config(self):
424 if hasattr(self.command_line_config.Global, 'quick'):
366 if hasattr(self.command_line_config.Global, 'quick'):
425 if self.command_line_config.Global.quick:
367 if self.command_line_config.Global.quick:
426 self.file_config = Config()
368 self.file_config = Config()
427 return
369 return
428 super(IPythonApp, self).load_file_config()
370 super(IPythonApp, self).load_file_config()
429
371
430 def post_load_file_config(self):
372 def post_load_file_config(self):
431 if hasattr(self.command_line_config.Global, 'extra_extension'):
373 if hasattr(self.command_line_config.Global, 'extra_extension'):
432 if not hasattr(self.file_config.Global, 'extensions'):
374 if not hasattr(self.file_config.Global, 'extensions'):
433 self.file_config.Global.extensions = []
375 self.file_config.Global.extensions = []
434 self.file_config.Global.extensions.append(
376 self.file_config.Global.extensions.append(
435 self.command_line_config.Global.extra_extension)
377 self.command_line_config.Global.extra_extension)
436 del self.command_line_config.Global.extra_extension
378 del self.command_line_config.Global.extra_extension
437
379
438 def pre_construct(self):
380 def pre_construct(self):
439 config = self.master_config
381 config = self.master_config
440
382
441 if hasattr(config.Global, 'classic'):
383 if hasattr(config.Global, 'classic'):
442 if config.Global.classic:
384 if config.Global.classic:
443 config.InteractiveShell.cache_size = 0
385 config.InteractiveShell.cache_size = 0
444 config.InteractiveShell.pprint = 0
386 config.InteractiveShell.pprint = 0
445 config.InteractiveShell.prompt_in1 = '>>> '
387 config.InteractiveShell.prompt_in1 = '>>> '
446 config.InteractiveShell.prompt_in2 = '... '
388 config.InteractiveShell.prompt_in2 = '... '
447 config.InteractiveShell.prompt_out = ''
389 config.InteractiveShell.prompt_out = ''
448 config.InteractiveShell.separate_in = \
390 config.InteractiveShell.separate_in = \
449 config.InteractiveShell.separate_out = \
391 config.InteractiveShell.separate_out = \
450 config.InteractiveShell.separate_out2 = ''
392 config.InteractiveShell.separate_out2 = ''
451 config.InteractiveShell.colors = 'NoColor'
393 config.InteractiveShell.colors = 'NoColor'
452 config.InteractiveShell.xmode = 'Plain'
394 config.InteractiveShell.xmode = 'Plain'
453
395
454 if hasattr(config.Global, 'nosep'):
396 if hasattr(config.Global, 'nosep'):
455 if config.Global.nosep:
397 if config.Global.nosep:
456 config.InteractiveShell.separate_in = \
398 config.InteractiveShell.separate_in = \
457 config.InteractiveShell.separate_out = \
399 config.InteractiveShell.separate_out = \
458 config.InteractiveShell.separate_out2 = ''
400 config.InteractiveShell.separate_out2 = ''
459
401
460 # if there is code of files to run from the cmd line, don't interact
402 # if there is code of files to run from the cmd line, don't interact
461 # unless the -i flag (Global.force_interact) is true.
403 # unless the -i flag (Global.force_interact) is true.
462 code_to_run = config.Global.get('code_to_run','')
404 code_to_run = config.Global.get('code_to_run','')
463 file_to_run = False
405 file_to_run = False
464 if self.extra_args and self.extra_args[0]:
406 if self.extra_args and self.extra_args[0]:
465 file_to_run = True
407 file_to_run = True
466 if file_to_run or code_to_run:
408 if file_to_run or code_to_run:
467 if not config.Global.force_interact:
409 if not config.Global.force_interact:
468 config.Global.interact = False
410 config.Global.interact = False
469
411
470 def construct(self):
412 def construct(self):
471 # I am a little hesitant to put these into InteractiveShell itself.
413 # I am a little hesitant to put these into InteractiveShell itself.
472 # But that might be the place for them
414 # But that might be the place for them
473 sys.path.insert(0, '')
415 sys.path.insert(0, '')
474
416
475 # Create an InteractiveShell instance
417 # Create an InteractiveShell instance
476 self.shell = InteractiveShell(None, self.master_config,
418 self.shell = InteractiveShell(None, self.master_config)
477 **self.shell_params )
478
419
479 def post_construct(self):
420 def post_construct(self):
480 """Do actions after construct, but before starting the app."""
421 """Do actions after construct, but before starting the app."""
481 config = self.master_config
422 config = self.master_config
482
423
483 # shell.display_banner should always be False for the terminal
424 # shell.display_banner should always be False for the terminal
484 # based app, because we call shell.show_banner() by hand below
425 # based app, because we call shell.show_banner() by hand below
485 # so the banner shows *before* all extension loading stuff.
426 # so the banner shows *before* all extension loading stuff.
486 self.shell.display_banner = False
427 self.shell.display_banner = False
487
428
488 if config.Global.display_banner and \
429 if config.Global.display_banner and \
489 config.Global.interact:
430 config.Global.interact:
490 self.shell.show_banner()
431 self.shell.show_banner()
491
432
492 # Make sure there is a space below the banner.
433 # Make sure there is a space below the banner.
493 if self.log_level <= logging.INFO: print
434 if self.log_level <= logging.INFO: print
494
435
495 # Now a variety of things that happen after the banner is printed.
436 # Now a variety of things that happen after the banner is printed.
496 self._enable_gui_pylab()
437 self._enable_gui_pylab()
497 self._load_extensions()
438 self._load_extensions()
498 self._run_exec_lines()
439 self._run_exec_lines()
499 self._run_exec_files()
440 self._run_exec_files()
500 self._run_cmd_line_code()
441 self._run_cmd_line_code()
501
442
502 def _enable_gui_pylab(self):
443 def _enable_gui_pylab(self):
503 """Enable GUI event loop integration, taking pylab into account."""
444 """Enable GUI event loop integration, taking pylab into account."""
504 Global = self.master_config.Global
445 Global = self.master_config.Global
505
446
506 # Select which gui to use
447 # Select which gui to use
507 if Global.gui:
448 if Global.gui:
508 gui = Global.gui
449 gui = Global.gui
509 # The following are deprecated, but there's likely to be a lot of use
450 # The following are deprecated, but there's likely to be a lot of use
510 # of this form out there, so we might as well support it for now. But
451 # of this form out there, so we might as well support it for now. But
511 # the --gui option above takes precedence.
452 # the --gui option above takes precedence.
512 elif Global.wthread:
453 elif Global.wthread:
513 gui = inputhook.GUI_WX
454 gui = inputhook.GUI_WX
514 elif Global.qthread:
455 elif Global.qthread:
515 gui = inputhook.GUI_QT
456 gui = inputhook.GUI_QT
516 elif Global.gthread:
457 elif Global.gthread:
517 gui = inputhook.GUI_GTK
458 gui = inputhook.GUI_GTK
518 else:
459 else:
519 gui = None
460 gui = None
520
461
521 # Using --pylab will also require gui activation, though which toolkit
462 # Using --pylab will also require gui activation, though which toolkit
522 # to use may be chosen automatically based on mpl configuration.
463 # to use may be chosen automatically based on mpl configuration.
523 if Global.pylab:
464 if Global.pylab:
524 activate = self.shell.enable_pylab
465 activate = self.shell.enable_pylab
525 if Global.pylab == 'auto':
466 if Global.pylab == 'auto':
526 gui = None
467 gui = None
527 else:
468 else:
528 gui = Global.pylab
469 gui = Global.pylab
529 else:
470 else:
530 # Enable only GUI integration, no pylab
471 # Enable only GUI integration, no pylab
531 activate = inputhook.enable_gui
472 activate = inputhook.enable_gui
532
473
533 if gui or Global.pylab:
474 if gui or Global.pylab:
534 try:
475 try:
535 self.log.info("Enabling GUI event loop integration, "
476 self.log.info("Enabling GUI event loop integration, "
536 "toolkit=%s, pylab=%s" % (gui, Global.pylab) )
477 "toolkit=%s, pylab=%s" % (gui, Global.pylab) )
537 activate(gui)
478 activate(gui)
538 except:
479 except:
539 self.log.warn("Error in enabling GUI event loop integration:")
480 self.log.warn("Error in enabling GUI event loop integration:")
540 self.shell.showtraceback()
481 self.shell.showtraceback()
541
482
542 def _load_extensions(self):
483 def _load_extensions(self):
543 """Load all IPython extensions in Global.extensions.
484 """Load all IPython extensions in Global.extensions.
544
485
545 This uses the :meth:`InteractiveShell.load_extensions` to load all
486 This uses the :meth:`InteractiveShell.load_extensions` to load all
546 the extensions listed in ``self.master_config.Global.extensions``.
487 the extensions listed in ``self.master_config.Global.extensions``.
547 """
488 """
548 try:
489 try:
549 if hasattr(self.master_config.Global, 'extensions'):
490 if hasattr(self.master_config.Global, 'extensions'):
550 self.log.debug("Loading IPython extensions...")
491 self.log.debug("Loading IPython extensions...")
551 extensions = self.master_config.Global.extensions
492 extensions = self.master_config.Global.extensions
552 for ext in extensions:
493 for ext in extensions:
553 try:
494 try:
554 self.log.info("Loading IPython extension: %s" % ext)
495 self.log.info("Loading IPython extension: %s" % ext)
555 self.shell.load_extension(ext)
496 self.shell.load_extension(ext)
556 except:
497 except:
557 self.log.warn("Error in loading extension: %s" % ext)
498 self.log.warn("Error in loading extension: %s" % ext)
558 self.shell.showtraceback()
499 self.shell.showtraceback()
559 except:
500 except:
560 self.log.warn("Unknown error in loading extensions:")
501 self.log.warn("Unknown error in loading extensions:")
561 self.shell.showtraceback()
502 self.shell.showtraceback()
562
503
563 def _run_exec_lines(self):
504 def _run_exec_lines(self):
564 """Run lines of code in Global.exec_lines in the user's namespace."""
505 """Run lines of code in Global.exec_lines in the user's namespace."""
565 try:
506 try:
566 if hasattr(self.master_config.Global, 'exec_lines'):
507 if hasattr(self.master_config.Global, 'exec_lines'):
567 self.log.debug("Running code from Global.exec_lines...")
508 self.log.debug("Running code from Global.exec_lines...")
568 exec_lines = self.master_config.Global.exec_lines
509 exec_lines = self.master_config.Global.exec_lines
569 for line in exec_lines:
510 for line in exec_lines:
570 try:
511 try:
571 self.log.info("Running code in user namespace: %s" % line)
512 self.log.info("Running code in user namespace: %s" % line)
572 self.shell.runlines(line)
513 self.shell.runlines(line)
573 except:
514 except:
574 self.log.warn("Error in executing line in user namespace: %s" % line)
515 self.log.warn("Error in executing line in user namespace: %s" % line)
575 self.shell.showtraceback()
516 self.shell.showtraceback()
576 except:
517 except:
577 self.log.warn("Unknown error in handling Global.exec_lines:")
518 self.log.warn("Unknown error in handling Global.exec_lines:")
578 self.shell.showtraceback()
519 self.shell.showtraceback()
579
520
580 def _exec_file(self, fname):
521 def _exec_file(self, fname):
581 full_filename = filefind(fname, [u'.', self.ipython_dir])
522 full_filename = filefind(fname, [u'.', self.ipython_dir])
582 if os.path.isfile(full_filename):
523 if os.path.isfile(full_filename):
583 if full_filename.endswith(u'.py'):
524 if full_filename.endswith(u'.py'):
584 self.log.info("Running file in user namespace: %s" % full_filename)
525 self.log.info("Running file in user namespace: %s" % full_filename)
585 self.shell.safe_execfile(full_filename, self.shell.user_ns)
526 self.shell.safe_execfile(full_filename, self.shell.user_ns)
586 elif full_filename.endswith('.ipy'):
527 elif full_filename.endswith('.ipy'):
587 self.log.info("Running file in user namespace: %s" % full_filename)
528 self.log.info("Running file in user namespace: %s" % full_filename)
588 self.shell.safe_execfile_ipy(full_filename)
529 self.shell.safe_execfile_ipy(full_filename)
589 else:
530 else:
590 self.log.warn("File does not have a .py or .ipy extension: <%s>" % full_filename)
531 self.log.warn("File does not have a .py or .ipy extension: <%s>" % full_filename)
591
532
592 def _run_exec_files(self):
533 def _run_exec_files(self):
593 try:
534 try:
594 if hasattr(self.master_config.Global, 'exec_files'):
535 if hasattr(self.master_config.Global, 'exec_files'):
595 self.log.debug("Running files in Global.exec_files...")
536 self.log.debug("Running files in Global.exec_files...")
596 exec_files = self.master_config.Global.exec_files
537 exec_files = self.master_config.Global.exec_files
597 for fname in exec_files:
538 for fname in exec_files:
598 self._exec_file(fname)
539 self._exec_file(fname)
599 except:
540 except:
600 self.log.warn("Unknown error in handling Global.exec_files:")
541 self.log.warn("Unknown error in handling Global.exec_files:")
601 self.shell.showtraceback()
542 self.shell.showtraceback()
602
543
603 def _run_cmd_line_code(self):
544 def _run_cmd_line_code(self):
604 if hasattr(self.master_config.Global, 'code_to_run'):
545 if hasattr(self.master_config.Global, 'code_to_run'):
605 line = self.master_config.Global.code_to_run
546 line = self.master_config.Global.code_to_run
606 try:
547 try:
607 self.log.info("Running code given at command line (-c): %s" % line)
548 self.log.info("Running code given at command line (-c): %s" % line)
608 self.shell.runlines(line)
549 self.shell.runlines(line)
609 except:
550 except:
610 self.log.warn("Error in executing line in user namespace: %s" % line)
551 self.log.warn("Error in executing line in user namespace: %s" % line)
611 self.shell.showtraceback()
552 self.shell.showtraceback()
612 return
553 return
613 # Like Python itself, ignore the second if the first of these is present
554 # Like Python itself, ignore the second if the first of these is present
614 try:
555 try:
615 fname = self.extra_args[0]
556 fname = self.extra_args[0]
616 except:
557 except:
617 pass
558 pass
618 else:
559 else:
619 try:
560 try:
620 self._exec_file(fname)
561 self._exec_file(fname)
621 except:
562 except:
622 self.log.warn("Error in executing file in user namespace: %s" % fname)
563 self.log.warn("Error in executing file in user namespace: %s" % fname)
623 self.shell.showtraceback()
564 self.shell.showtraceback()
624
565
625 def start_app(self):
566 def start_app(self):
626 if self.master_config.Global.interact:
567 if self.master_config.Global.interact:
627 self.log.debug("Starting IPython's mainloop...")
568 self.log.debug("Starting IPython's mainloop...")
628 self.shell.mainloop()
569 self.shell.mainloop()
629 else:
570 else:
630 self.log.debug("IPython not interactive, start_app is no-op...")
571 self.log.debug("IPython not interactive, start_app is no-op...")
631
572
632
573
633 def load_default_config(ipython_dir=None):
574 def load_default_config(ipython_dir=None):
634 """Load the default config file from the default ipython_dir.
575 """Load the default config file from the default ipython_dir.
635
576
636 This is useful for embedded shells.
577 This is useful for embedded shells.
637 """
578 """
638 if ipython_dir is None:
579 if ipython_dir is None:
639 ipython_dir = get_ipython_dir()
580 ipython_dir = get_ipython_dir()
640 cl = PyFileConfigLoader(default_config_file_name, ipython_dir)
581 cl = PyFileConfigLoader(default_config_file_name, ipython_dir)
641 config = cl.load_config()
582 config = cl.load_config()
642 return config
583 return config
643
584
644
585
645 def launch_new_instance():
586 def launch_new_instance():
646 """Create and run a full blown IPython instance"""
587 """Create and run a full blown IPython instance"""
647 app = IPythonApp()
588 app = IPythonApp()
648 app.start()
589 app.start()
590
@@ -1,464 +1,493 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # encoding: utf-8
2 # encoding: utf-8
3 """
3 """
4 The IPython cluster directory
4 The IPython cluster directory
5 """
5 """
6
6
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8 # Copyright (C) 2008-2009 The IPython Development Team
8 # Copyright (C) 2008-2009 The IPython Development Team
9 #
9 #
10 # Distributed under the terms of the BSD License. The full license is in
10 # Distributed under the terms of the BSD License. The full license is in
11 # the file COPYING, distributed as part of this software.
11 # the file COPYING, distributed as part of this software.
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13
13
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 # Imports
15 # Imports
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17
17
18 from __future__ import with_statement
18 from __future__ import with_statement
19
19
20 import os
20 import os
21 import shutil
21 import shutil
22 import sys
22 import sys
23 import warnings
23 import warnings
24
24
25 from twisted.python import log
25 from twisted.python import log
26
26
27 from IPython.config.loader import PyFileConfigLoader
27 from IPython.config.loader import PyFileConfigLoader
28 from IPython.core.application import Application
28 from IPython.core.application import Application, BaseAppConfigLoader
29 from IPython.core.component import Component
29 from IPython.core.component import Component
30 from IPython.utils.path import (
30 from IPython.utils.path import (
31 get_ipython_package_dir,
31 get_ipython_package_dir,
32 expand_path
32 expand_path
33 )
33 )
34 from IPython.utils.traitlets import Unicode
34 from IPython.utils.traitlets import Unicode
35
35
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37 # Warnings control
37 # Warnings control
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39 # Twisted generates annoying warnings with Python 2.6, as will do other code
39 # Twisted generates annoying warnings with Python 2.6, as will do other code
40 # that imports 'sets' as of today
40 # that imports 'sets' as of today
41 warnings.filterwarnings('ignore', 'the sets module is deprecated',
41 warnings.filterwarnings('ignore', 'the sets module is deprecated',
42 DeprecationWarning )
42 DeprecationWarning )
43
43
44 # This one also comes from Twisted
44 # This one also comes from Twisted
45 warnings.filterwarnings('ignore', 'the sha module is deprecated',
45 warnings.filterwarnings('ignore', 'the sha module is deprecated',
46 DeprecationWarning)
46 DeprecationWarning)
47
47
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49 # Classes and functions
49 # Module errors
50 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
51
51
52 class ClusterDirError(Exception):
52 class ClusterDirError(Exception):
53 pass
53 pass
54
54
55
55
56 class PIDFileError(Exception):
56 class PIDFileError(Exception):
57 pass
57 pass
58
58
59
59
60 #-----------------------------------------------------------------------------
61 # Class for managing cluster directories
62 #-----------------------------------------------------------------------------
63
60 class ClusterDir(Component):
64 class ClusterDir(Component):
61 """An object to manage the cluster directory and its resources.
65 """An object to manage the cluster directory and its resources.
62
66
63 The cluster directory is used by :command:`ipcontroller`,
67 The cluster directory is used by :command:`ipcontroller`,
64 :command:`ipcontroller` and :command:`ipcontroller` to manage the
68 :command:`ipcontroller` and :command:`ipcontroller` to manage the
65 configuration, logging and security of these applications.
69 configuration, logging and security of these applications.
66
70
67 This object knows how to find, create and manage these directories. This
71 This object knows how to find, create and manage these directories. This
68 should be used by any code that want's to handle cluster directories.
72 should be used by any code that want's to handle cluster directories.
69 """
73 """
70
74
71 security_dir_name = Unicode('security')
75 security_dir_name = Unicode('security')
72 log_dir_name = Unicode('log')
76 log_dir_name = Unicode('log')
73 pid_dir_name = Unicode('pid')
77 pid_dir_name = Unicode('pid')
74 security_dir = Unicode(u'')
78 security_dir = Unicode(u'')
75 log_dir = Unicode(u'')
79 log_dir = Unicode(u'')
76 pid_dir = Unicode(u'')
80 pid_dir = Unicode(u'')
77 location = Unicode(u'')
81 location = Unicode(u'')
78
82
79 def __init__(self, location):
83 def __init__(self, location):
80 super(ClusterDir, self).__init__(None)
84 super(ClusterDir, self).__init__(None)
81 self.location = location
85 self.location = location
82
86
83 def _location_changed(self, name, old, new):
87 def _location_changed(self, name, old, new):
84 if not os.path.isdir(new):
88 if not os.path.isdir(new):
85 os.makedirs(new)
89 os.makedirs(new)
86 self.security_dir = os.path.join(new, self.security_dir_name)
90 self.security_dir = os.path.join(new, self.security_dir_name)
87 self.log_dir = os.path.join(new, self.log_dir_name)
91 self.log_dir = os.path.join(new, self.log_dir_name)
88 self.pid_dir = os.path.join(new, self.pid_dir_name)
92 self.pid_dir = os.path.join(new, self.pid_dir_name)
89 self.check_dirs()
93 self.check_dirs()
90
94
91 def _log_dir_changed(self, name, old, new):
95 def _log_dir_changed(self, name, old, new):
92 self.check_log_dir()
96 self.check_log_dir()
93
97
94 def check_log_dir(self):
98 def check_log_dir(self):
95 if not os.path.isdir(self.log_dir):
99 if not os.path.isdir(self.log_dir):
96 os.mkdir(self.log_dir)
100 os.mkdir(self.log_dir)
97
101
98 def _security_dir_changed(self, name, old, new):
102 def _security_dir_changed(self, name, old, new):
99 self.check_security_dir()
103 self.check_security_dir()
100
104
101 def check_security_dir(self):
105 def check_security_dir(self):
102 if not os.path.isdir(self.security_dir):
106 if not os.path.isdir(self.security_dir):
103 os.mkdir(self.security_dir, 0700)
107 os.mkdir(self.security_dir, 0700)
104 os.chmod(self.security_dir, 0700)
108 os.chmod(self.security_dir, 0700)
105
109
106 def _pid_dir_changed(self, name, old, new):
110 def _pid_dir_changed(self, name, old, new):
107 self.check_pid_dir()
111 self.check_pid_dir()
108
112
109 def check_pid_dir(self):
113 def check_pid_dir(self):
110 if not os.path.isdir(self.pid_dir):
114 if not os.path.isdir(self.pid_dir):
111 os.mkdir(self.pid_dir, 0700)
115 os.mkdir(self.pid_dir, 0700)
112 os.chmod(self.pid_dir, 0700)
116 os.chmod(self.pid_dir, 0700)
113
117
114 def check_dirs(self):
118 def check_dirs(self):
115 self.check_security_dir()
119 self.check_security_dir()
116 self.check_log_dir()
120 self.check_log_dir()
117 self.check_pid_dir()
121 self.check_pid_dir()
118
122
119 def load_config_file(self, filename):
123 def load_config_file(self, filename):
120 """Load a config file from the top level of the cluster dir.
124 """Load a config file from the top level of the cluster dir.
121
125
122 Parameters
126 Parameters
123 ----------
127 ----------
124 filename : unicode or str
128 filename : unicode or str
125 The filename only of the config file that must be located in
129 The filename only of the config file that must be located in
126 the top-level of the cluster directory.
130 the top-level of the cluster directory.
127 """
131 """
128 loader = PyFileConfigLoader(filename, self.location)
132 loader = PyFileConfigLoader(filename, self.location)
129 return loader.load_config()
133 return loader.load_config()
130
134
131 def copy_config_file(self, config_file, path=None, overwrite=False):
135 def copy_config_file(self, config_file, path=None, overwrite=False):
132 """Copy a default config file into the active cluster directory.
136 """Copy a default config file into the active cluster directory.
133
137
134 Default configuration files are kept in :mod:`IPython.config.default`.
138 Default configuration files are kept in :mod:`IPython.config.default`.
135 This function moves these from that location to the working cluster
139 This function moves these from that location to the working cluster
136 directory.
140 directory.
137 """
141 """
138 if path is None:
142 if path is None:
139 import IPython.config.default
143 import IPython.config.default
140 path = IPython.config.default.__file__.split(os.path.sep)[:-1]
144 path = IPython.config.default.__file__.split(os.path.sep)[:-1]
141 path = os.path.sep.join(path)
145 path = os.path.sep.join(path)
142 src = os.path.join(path, config_file)
146 src = os.path.join(path, config_file)
143 dst = os.path.join(self.location, config_file)
147 dst = os.path.join(self.location, config_file)
144 if not os.path.isfile(dst) or overwrite:
148 if not os.path.isfile(dst) or overwrite:
145 shutil.copy(src, dst)
149 shutil.copy(src, dst)
146
150
147 def copy_all_config_files(self, path=None, overwrite=False):
151 def copy_all_config_files(self, path=None, overwrite=False):
148 """Copy all config files into the active cluster directory."""
152 """Copy all config files into the active cluster directory."""
149 for f in [u'ipcontroller_config.py', u'ipengine_config.py',
153 for f in [u'ipcontroller_config.py', u'ipengine_config.py',
150 u'ipcluster_config.py']:
154 u'ipcluster_config.py']:
151 self.copy_config_file(f, path=path, overwrite=overwrite)
155 self.copy_config_file(f, path=path, overwrite=overwrite)
152
156
153 @classmethod
157 @classmethod
154 def create_cluster_dir(csl, cluster_dir):
158 def create_cluster_dir(csl, cluster_dir):
155 """Create a new cluster directory given a full path.
159 """Create a new cluster directory given a full path.
156
160
157 Parameters
161 Parameters
158 ----------
162 ----------
159 cluster_dir : str
163 cluster_dir : str
160 The full path to the cluster directory. If it does exist, it will
164 The full path to the cluster directory. If it does exist, it will
161 be used. If not, it will be created.
165 be used. If not, it will be created.
162 """
166 """
163 return ClusterDir(cluster_dir)
167 return ClusterDir(cluster_dir)
164
168
165 @classmethod
169 @classmethod
166 def create_cluster_dir_by_profile(cls, path, profile=u'default'):
170 def create_cluster_dir_by_profile(cls, path, profile=u'default'):
167 """Create a cluster dir by profile name and path.
171 """Create a cluster dir by profile name and path.
168
172
169 Parameters
173 Parameters
170 ----------
174 ----------
171 path : str
175 path : str
172 The path (directory) to put the cluster directory in.
176 The path (directory) to put the cluster directory in.
173 profile : str
177 profile : str
174 The name of the profile. The name of the cluster directory will
178 The name of the profile. The name of the cluster directory will
175 be "cluster_<profile>".
179 be "cluster_<profile>".
176 """
180 """
177 if not os.path.isdir(path):
181 if not os.path.isdir(path):
178 raise ClusterDirError('Directory not found: %s' % path)
182 raise ClusterDirError('Directory not found: %s' % path)
179 cluster_dir = os.path.join(path, u'cluster_' + profile)
183 cluster_dir = os.path.join(path, u'cluster_' + profile)
180 return ClusterDir(cluster_dir)
184 return ClusterDir(cluster_dir)
181
185
182 @classmethod
186 @classmethod
183 def find_cluster_dir_by_profile(cls, ipython_dir, profile=u'default'):
187 def find_cluster_dir_by_profile(cls, ipython_dir, profile=u'default'):
184 """Find an existing cluster dir by profile name, return its ClusterDir.
188 """Find an existing cluster dir by profile name, return its ClusterDir.
185
189
186 This searches through a sequence of paths for a cluster dir. If it
190 This searches through a sequence of paths for a cluster dir. If it
187 is not found, a :class:`ClusterDirError` exception will be raised.
191 is not found, a :class:`ClusterDirError` exception will be raised.
188
192
189 The search path algorithm is:
193 The search path algorithm is:
190 1. ``os.getcwd()``
194 1. ``os.getcwd()``
191 2. ``ipython_dir``
195 2. ``ipython_dir``
192 3. The directories found in the ":" separated
196 3. The directories found in the ":" separated
193 :env:`IPCLUSTER_DIR_PATH` environment variable.
197 :env:`IPCLUSTER_DIR_PATH` environment variable.
194
198
195 Parameters
199 Parameters
196 ----------
200 ----------
197 ipython_dir : unicode or str
201 ipython_dir : unicode or str
198 The IPython directory to use.
202 The IPython directory to use.
199 profile : unicode or str
203 profile : unicode or str
200 The name of the profile. The name of the cluster directory
204 The name of the profile. The name of the cluster directory
201 will be "cluster_<profile>".
205 will be "cluster_<profile>".
202 """
206 """
203 dirname = u'cluster_' + profile
207 dirname = u'cluster_' + profile
204 cluster_dir_paths = os.environ.get('IPCLUSTER_DIR_PATH','')
208 cluster_dir_paths = os.environ.get('IPCLUSTER_DIR_PATH','')
205 if cluster_dir_paths:
209 if cluster_dir_paths:
206 cluster_dir_paths = cluster_dir_paths.split(':')
210 cluster_dir_paths = cluster_dir_paths.split(':')
207 else:
211 else:
208 cluster_dir_paths = []
212 cluster_dir_paths = []
209 paths = [os.getcwd(), ipython_dir] + cluster_dir_paths
213 paths = [os.getcwd(), ipython_dir] + cluster_dir_paths
210 for p in paths:
214 for p in paths:
211 cluster_dir = os.path.join(p, dirname)
215 cluster_dir = os.path.join(p, dirname)
212 if os.path.isdir(cluster_dir):
216 if os.path.isdir(cluster_dir):
213 return ClusterDir(cluster_dir)
217 return ClusterDir(cluster_dir)
214 else:
218 else:
215 raise ClusterDirError('Cluster directory not found in paths: %s' % dirname)
219 raise ClusterDirError('Cluster directory not found in paths: %s' % dirname)
216
220
217 @classmethod
221 @classmethod
218 def find_cluster_dir(cls, cluster_dir):
222 def find_cluster_dir(cls, cluster_dir):
219 """Find/create a cluster dir and return its ClusterDir.
223 """Find/create a cluster dir and return its ClusterDir.
220
224
221 This will create the cluster directory if it doesn't exist.
225 This will create the cluster directory if it doesn't exist.
222
226
223 Parameters
227 Parameters
224 ----------
228 ----------
225 cluster_dir : unicode or str
229 cluster_dir : unicode or str
226 The path of the cluster directory. This is expanded using
230 The path of the cluster directory. This is expanded using
227 :func:`IPython.utils.genutils.expand_path`.
231 :func:`IPython.utils.genutils.expand_path`.
228 """
232 """
229 cluster_dir = expand_path(cluster_dir)
233 cluster_dir = expand_path(cluster_dir)
230 if not os.path.isdir(cluster_dir):
234 if not os.path.isdir(cluster_dir):
231 raise ClusterDirError('Cluster directory not found: %s' % cluster_dir)
235 raise ClusterDirError('Cluster directory not found: %s' % cluster_dir)
232 return ClusterDir(cluster_dir)
236 return ClusterDir(cluster_dir)
233
237
234
238
235 # Default command line options for IPython cluster applications.
239 #-----------------------------------------------------------------------------
236 cl_args = (
240 # Command line options
237 (('--ipython-dir',), dict(
241 #-----------------------------------------------------------------------------
238 dest='Global.ipython_dir',type=unicode,
242
239 help='Set to override default location of Global.ipython_dir.',
243 class ClusterDirConfigLoader(BaseAppConfigLoader):
240 metavar='Global.ipython_dir') ),
244
241 (('-p', '--profile',), dict(
245 def _add_cluster_profile(self, parser):
242 dest='Global.profile',type=unicode,
246 paa = parser.add_argument
243 help=
247 paa('-p', '--profile',
244 """The string name of the profile to be used. This determines the name
248 dest='Global.profile',type=unicode,
245 of the cluster dir as: cluster_<profile>. The default profile is named
249 help=
246 'default'. The cluster directory is resolve this way if the
250 """The string name of the profile to be used. This determines the name
247 --cluster-dir option is not used.""",
251 of the cluster dir as: cluster_<profile>. The default profile is named
248 metavar='Global.profile') ),
252 'default'. The cluster directory is resolve this way if the
249 (('--cluster-dir',), dict(
253 --cluster-dir option is not used.""",
250 dest='Global.cluster_dir',type=unicode,
254 metavar='Global.profile')
251 help="""Set the cluster dir. This overrides the logic used by the
255
252 --profile option.""",
256 def _add_cluster_dir(self, parser):
253 metavar='Global.cluster_dir') ),
257 paa = parser.add_argument
254 (('--work-dir',), dict(
258 paa('--cluster-dir',
255 dest='Global.work_dir',type=unicode,
259 dest='Global.cluster_dir',type=unicode,
256 help='Set the working dir for the process.',
260 help="""Set the cluster dir. This overrides the logic used by the
257 metavar='Global.work_dir') ),
261 --profile option.""",
258 (('--clean-logs',), dict(
262 metavar='Global.cluster_dir')
259 dest='Global.clean_logs', action='store_true',
263
260 help='Delete old log flies before starting.') ),
264 def _add_work_dir(self, parser):
261 (('--no-clean-logs',), dict(
265 paa = parser.add_argument
262 dest='Global.clean_logs', action='store_false',
266 paa('--work-dir',
263 help="Don't Delete old log flies before starting.") ),
267 dest='Global.work_dir',type=unicode,
264 )
268 help='Set the working dir for the process.',
269 metavar='Global.work_dir')
270
271 def _add_clean_logs(self, parser):
272 paa = parser.add_argument
273 paa('--clean-logs',
274 dest='Global.clean_logs', action='store_true',
275 help='Delete old log flies before starting.')
276
277 def _add_no_clean_logs(self, parser):
278 paa = parser.add_argument
279 paa('--no-clean-logs',
280 dest='Global.clean_logs', action='store_false',
281 help="Don't Delete old log flies before starting.")
282
283 def _add_arguments(self):
284 super(ClusterDirConfigLoader, self)._add_arguments()
285 self._add_cluster_profile(self.parser)
286 self._add_cluster_dir(self.parser)
287 self._add_work_dir(self.parser)
288 self._add_clean_logs(self.parser)
289 self._add_no_clean_logs(self.parser)
265
290
266
291
292 #-----------------------------------------------------------------------------
293 # Main application
294 #-----------------------------------------------------------------------------
295
267 class ApplicationWithClusterDir(Application):
296 class ApplicationWithClusterDir(Application):
268 """An application that puts everything into a cluster directory.
297 """An application that puts everything into a cluster directory.
269
298
270 Instead of looking for things in the ipython_dir, this type of application
299 Instead of looking for things in the ipython_dir, this type of application
271 will use its own private directory called the "cluster directory"
300 will use its own private directory called the "cluster directory"
272 for things like config files, log files, etc.
301 for things like config files, log files, etc.
273
302
274 The cluster directory is resolved as follows:
303 The cluster directory is resolved as follows:
275
304
276 * If the ``--cluster-dir`` option is given, it is used.
305 * If the ``--cluster-dir`` option is given, it is used.
277 * If ``--cluster-dir`` is not given, the application directory is
306 * If ``--cluster-dir`` is not given, the application directory is
278 resolve using the profile name as ``cluster_<profile>``. The search
307 resolve using the profile name as ``cluster_<profile>``. The search
279 path for this directory is then i) cwd if it is found there
308 path for this directory is then i) cwd if it is found there
280 and ii) in ipython_dir otherwise.
309 and ii) in ipython_dir otherwise.
281
310
282 The config file for the application is to be put in the cluster
311 The config file for the application is to be put in the cluster
283 dir and named the value of the ``config_file_name`` class attribute.
312 dir and named the value of the ``config_file_name`` class attribute.
284 """
313 """
285
314
315 command_line_loader = ClusterDirConfigLoader
286 auto_create_cluster_dir = True
316 auto_create_cluster_dir = True
287
317
288 cl_arguments = Application.cl_arguments + cl_args
289
290 def create_default_config(self):
318 def create_default_config(self):
291 super(ApplicationWithClusterDir, self).create_default_config()
319 super(ApplicationWithClusterDir, self).create_default_config()
292 self.default_config.Global.profile = u'default'
320 self.default_config.Global.profile = u'default'
293 self.default_config.Global.cluster_dir = u''
321 self.default_config.Global.cluster_dir = u''
294 self.default_config.Global.work_dir = os.getcwd()
322 self.default_config.Global.work_dir = os.getcwd()
295 self.default_config.Global.log_to_file = False
323 self.default_config.Global.log_to_file = False
296 self.default_config.Global.clean_logs = False
324 self.default_config.Global.clean_logs = False
297
325
298 def find_resources(self):
326 def find_resources(self):
299 """This resolves the cluster directory.
327 """This resolves the cluster directory.
300
328
301 This tries to find the cluster directory and if successful, it will
329 This tries to find the cluster directory and if successful, it will
302 have done:
330 have done:
303 * Sets ``self.cluster_dir_obj`` to the :class:`ClusterDir` object for
331 * Sets ``self.cluster_dir_obj`` to the :class:`ClusterDir` object for
304 the application.
332 the application.
305 * Sets ``self.cluster_dir`` attribute of the application and config
333 * Sets ``self.cluster_dir`` attribute of the application and config
306 objects.
334 objects.
307
335
308 The algorithm used for this is as follows:
336 The algorithm used for this is as follows:
309 1. Try ``Global.cluster_dir``.
337 1. Try ``Global.cluster_dir``.
310 2. Try using ``Global.profile``.
338 2. Try using ``Global.profile``.
311 3. If both of these fail and ``self.auto_create_cluster_dir`` is
339 3. If both of these fail and ``self.auto_create_cluster_dir`` is
312 ``True``, then create the new cluster dir in the IPython directory.
340 ``True``, then create the new cluster dir in the IPython directory.
313 4. If all fails, then raise :class:`ClusterDirError`.
341 4. If all fails, then raise :class:`ClusterDirError`.
314 """
342 """
315
343
316 try:
344 try:
317 cluster_dir = self.command_line_config.Global.cluster_dir
345 cluster_dir = self.command_line_config.Global.cluster_dir
318 except AttributeError:
346 except AttributeError:
319 cluster_dir = self.default_config.Global.cluster_dir
347 cluster_dir = self.default_config.Global.cluster_dir
320 cluster_dir = expand_path(cluster_dir)
348 cluster_dir = expand_path(cluster_dir)
321 try:
349 try:
322 self.cluster_dir_obj = ClusterDir.find_cluster_dir(cluster_dir)
350 self.cluster_dir_obj = ClusterDir.find_cluster_dir(cluster_dir)
323 except ClusterDirError:
351 except ClusterDirError:
324 pass
352 pass
325 else:
353 else:
326 self.log.info('Using existing cluster dir: %s' % \
354 self.log.info('Using existing cluster dir: %s' % \
327 self.cluster_dir_obj.location
355 self.cluster_dir_obj.location
328 )
356 )
329 self.finish_cluster_dir()
357 self.finish_cluster_dir()
330 return
358 return
331
359
332 try:
360 try:
333 self.profile = self.command_line_config.Global.profile
361 self.profile = self.command_line_config.Global.profile
334 except AttributeError:
362 except AttributeError:
335 self.profile = self.default_config.Global.profile
363 self.profile = self.default_config.Global.profile
336 try:
364 try:
337 self.cluster_dir_obj = ClusterDir.find_cluster_dir_by_profile(
365 self.cluster_dir_obj = ClusterDir.find_cluster_dir_by_profile(
338 self.ipython_dir, self.profile)
366 self.ipython_dir, self.profile)
339 except ClusterDirError:
367 except ClusterDirError:
340 pass
368 pass
341 else:
369 else:
342 self.log.info('Using existing cluster dir: %s' % \
370 self.log.info('Using existing cluster dir: %s' % \
343 self.cluster_dir_obj.location
371 self.cluster_dir_obj.location
344 )
372 )
345 self.finish_cluster_dir()
373 self.finish_cluster_dir()
346 return
374 return
347
375
348 if self.auto_create_cluster_dir:
376 if self.auto_create_cluster_dir:
349 self.cluster_dir_obj = ClusterDir.create_cluster_dir_by_profile(
377 self.cluster_dir_obj = ClusterDir.create_cluster_dir_by_profile(
350 self.ipython_dir, self.profile
378 self.ipython_dir, self.profile
351 )
379 )
352 self.log.info('Creating new cluster dir: %s' % \
380 self.log.info('Creating new cluster dir: %s' % \
353 self.cluster_dir_obj.location
381 self.cluster_dir_obj.location
354 )
382 )
355 self.finish_cluster_dir()
383 self.finish_cluster_dir()
356 else:
384 else:
357 raise ClusterDirError('Could not find a valid cluster directory.')
385 raise ClusterDirError('Could not find a valid cluster directory.')
358
386
359 def finish_cluster_dir(self):
387 def finish_cluster_dir(self):
360 # Set the cluster directory
388 # Set the cluster directory
361 self.cluster_dir = self.cluster_dir_obj.location
389 self.cluster_dir = self.cluster_dir_obj.location
362
390
363 # These have to be set because they could be different from the one
391 # These have to be set because they could be different from the one
364 # that we just computed. Because command line has the highest
392 # that we just computed. Because command line has the highest
365 # priority, this will always end up in the master_config.
393 # priority, this will always end up in the master_config.
366 self.default_config.Global.cluster_dir = self.cluster_dir
394 self.default_config.Global.cluster_dir = self.cluster_dir
367 self.command_line_config.Global.cluster_dir = self.cluster_dir
395 self.command_line_config.Global.cluster_dir = self.cluster_dir
368
396
369 def find_config_file_name(self):
397 def find_config_file_name(self):
370 """Find the config file name for this application."""
398 """Find the config file name for this application."""
371 # For this type of Application it should be set as a class attribute.
399 # For this type of Application it should be set as a class attribute.
372 if not hasattr(self, 'config_file_name'):
400 if not hasattr(self, 'config_file_name'):
373 self.log.critical("No config filename found")
401 self.log.critical("No config filename found")
374
402
375 def find_config_file_paths(self):
403 def find_config_file_paths(self):
376 # Include our own config directory last, so that users can still find
404 # Include our own config directory last, so that users can still find
377 # our shipped copies of builtin config files even if they don't have
405 # our shipped copies of builtin config files even if they don't have
378 # them in their ipython cluster directory.
406 # them in their ipython cluster directory.
379 conf_dir = os.path.join(get_ipython_package_dir(), 'config', 'default')
407 conf_dir = os.path.join(get_ipython_package_dir(), 'config', 'default')
380 self.config_file_paths = (self.cluster_dir, conf_dir)
408 self.config_file_paths = (self.cluster_dir, conf_dir)
381
409
382 def pre_construct(self):
410 def pre_construct(self):
383 # The log and security dirs were set earlier, but here we put them
411 # The log and security dirs were set earlier, but here we put them
384 # into the config and log them.
412 # into the config and log them.
385 config = self.master_config
413 config = self.master_config
386 sdir = self.cluster_dir_obj.security_dir
414 sdir = self.cluster_dir_obj.security_dir
387 self.security_dir = config.Global.security_dir = sdir
415 self.security_dir = config.Global.security_dir = sdir
388 ldir = self.cluster_dir_obj.log_dir
416 ldir = self.cluster_dir_obj.log_dir
389 self.log_dir = config.Global.log_dir = ldir
417 self.log_dir = config.Global.log_dir = ldir
390 pdir = self.cluster_dir_obj.pid_dir
418 pdir = self.cluster_dir_obj.pid_dir
391 self.pid_dir = config.Global.pid_dir = pdir
419 self.pid_dir = config.Global.pid_dir = pdir
392 self.log.info("Cluster directory set to: %s" % self.cluster_dir)
420 self.log.info("Cluster directory set to: %s" % self.cluster_dir)
393 config.Global.work_dir = unicode(expand_path(config.Global.work_dir))
421 config.Global.work_dir = unicode(expand_path(config.Global.work_dir))
394 # Change to the working directory. We do this just before construct
422 # Change to the working directory. We do this just before construct
395 # is called so all the components there have the right working dir.
423 # is called so all the components there have the right working dir.
396 self.to_work_dir()
424 self.to_work_dir()
397
425
398 def to_work_dir(self):
426 def to_work_dir(self):
399 wd = self.master_config.Global.work_dir
427 wd = self.master_config.Global.work_dir
400 if unicode(wd) != unicode(os.getcwd()):
428 if unicode(wd) != unicode(os.getcwd()):
401 os.chdir(wd)
429 os.chdir(wd)
402 self.log.info("Changing to working dir: %s" % wd)
430 self.log.info("Changing to working dir: %s" % wd)
403
431
404 def start_logging(self):
432 def start_logging(self):
405 # Remove old log files
433 # Remove old log files
406 if self.master_config.Global.clean_logs:
434 if self.master_config.Global.clean_logs:
407 log_dir = self.master_config.Global.log_dir
435 log_dir = self.master_config.Global.log_dir
408 for f in os.listdir(log_dir):
436 for f in os.listdir(log_dir):
409 if f.startswith(self.name + u'-') and f.endswith('.log'):
437 if f.startswith(self.name + u'-') and f.endswith('.log'):
410 os.remove(os.path.join(log_dir, f))
438 os.remove(os.path.join(log_dir, f))
411 # Start logging to the new log file
439 # Start logging to the new log file
412 if self.master_config.Global.log_to_file:
440 if self.master_config.Global.log_to_file:
413 log_filename = self.name + u'-' + str(os.getpid()) + u'.log'
441 log_filename = self.name + u'-' + str(os.getpid()) + u'.log'
414 logfile = os.path.join(self.log_dir, log_filename)
442 logfile = os.path.join(self.log_dir, log_filename)
415 open_log_file = open(logfile, 'w')
443 open_log_file = open(logfile, 'w')
416 else:
444 else:
417 open_log_file = sys.stdout
445 open_log_file = sys.stdout
418 log.startLogging(open_log_file)
446 log.startLogging(open_log_file)
419
447
420 def write_pid_file(self, overwrite=False):
448 def write_pid_file(self, overwrite=False):
421 """Create a .pid file in the pid_dir with my pid.
449 """Create a .pid file in the pid_dir with my pid.
422
450
423 This must be called after pre_construct, which sets `self.pid_dir`.
451 This must be called after pre_construct, which sets `self.pid_dir`.
424 This raises :exc:`PIDFileError` if the pid file exists already.
452 This raises :exc:`PIDFileError` if the pid file exists already.
425 """
453 """
426 pid_file = os.path.join(self.pid_dir, self.name + u'.pid')
454 pid_file = os.path.join(self.pid_dir, self.name + u'.pid')
427 if os.path.isfile(pid_file):
455 if os.path.isfile(pid_file):
428 pid = self.get_pid_from_file()
456 pid = self.get_pid_from_file()
429 if not overwrite:
457 if not overwrite:
430 raise PIDFileError(
458 raise PIDFileError(
431 'The pid file [%s] already exists. \nThis could mean that this '
459 'The pid file [%s] already exists. \nThis could mean that this '
432 'server is already running with [pid=%s].' % (pid_file, pid)
460 'server is already running with [pid=%s].' % (pid_file, pid)
433 )
461 )
434 with open(pid_file, 'w') as f:
462 with open(pid_file, 'w') as f:
435 self.log.info("Creating pid file: %s" % pid_file)
463 self.log.info("Creating pid file: %s" % pid_file)
436 f.write(repr(os.getpid())+'\n')
464 f.write(repr(os.getpid())+'\n')
437
465
438 def remove_pid_file(self):
466 def remove_pid_file(self):
439 """Remove the pid file.
467 """Remove the pid file.
440
468
441 This should be called at shutdown by registering a callback with
469 This should be called at shutdown by registering a callback with
442 :func:`reactor.addSystemEventTrigger`. This needs to return
470 :func:`reactor.addSystemEventTrigger`. This needs to return
443 ``None``.
471 ``None``.
444 """
472 """
445 pid_file = os.path.join(self.pid_dir, self.name + u'.pid')
473 pid_file = os.path.join(self.pid_dir, self.name + u'.pid')
446 if os.path.isfile(pid_file):
474 if os.path.isfile(pid_file):
447 try:
475 try:
448 self.log.info("Removing pid file: %s" % pid_file)
476 self.log.info("Removing pid file: %s" % pid_file)
449 os.remove(pid_file)
477 os.remove(pid_file)
450 except:
478 except:
451 self.log.warn("Error removing the pid file: %s" % pid_file)
479 self.log.warn("Error removing the pid file: %s" % pid_file)
452
480
453 def get_pid_from_file(self):
481 def get_pid_from_file(self):
454 """Get the pid from the pid file.
482 """Get the pid from the pid file.
455
483
456 If the pid file doesn't exist a :exc:`PIDFileError` is raised.
484 If the pid file doesn't exist a :exc:`PIDFileError` is raised.
457 """
485 """
458 pid_file = os.path.join(self.pid_dir, self.name + u'.pid')
486 pid_file = os.path.join(self.pid_dir, self.name + u'.pid')
459 if os.path.isfile(pid_file):
487 if os.path.isfile(pid_file):
460 with open(pid_file, 'r') as f:
488 with open(pid_file, 'r') as f:
461 pid = int(f.read().strip())
489 pid = int(f.read().strip())
462 return pid
490 return pid
463 else:
491 else:
464 raise PIDFileError('pid file not found: %s' % pid_file)
492 raise PIDFileError('pid file not found: %s' % pid_file)
493
@@ -1,462 +1,459 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # encoding: utf-8
2 # encoding: utf-8
3 """
3 """
4 The ipcluster application.
4 The ipcluster application.
5 """
5 """
6
6
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8 # Copyright (C) 2008-2009 The IPython Development Team
8 # Copyright (C) 2008-2009 The IPython Development Team
9 #
9 #
10 # Distributed under the terms of the BSD License. The full license is in
10 # Distributed under the terms of the BSD License. The full license is in
11 # the file COPYING, distributed as part of this software.
11 # the file COPYING, distributed as part of this software.
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13
13
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 # Imports
15 # Imports
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17
17
18 import logging
18 import logging
19 import os
19 import os
20 import signal
20 import signal
21
21
22 if os.name=='posix':
22 if os.name=='posix':
23 from twisted.scripts._twistd_unix import daemonize
23 from twisted.scripts._twistd_unix import daemonize
24
24
25 from IPython.core import release
25 from twisted.internet import reactor, defer
26 from twisted.python import log, failure
27
28
26 from IPython.external.argparse import ArgumentParser, SUPPRESS
29 from IPython.external.argparse import ArgumentParser, SUPPRESS
27 from IPython.config.loader import ArgParseConfigLoader
28 from IPython.utils.importstring import import_item
30 from IPython.utils.importstring import import_item
29
30 from IPython.kernel.clusterdir import (
31 from IPython.kernel.clusterdir import (
31 ApplicationWithClusterDir, ClusterDirError, PIDFileError
32 ApplicationWithClusterDir, ClusterDirConfigLoader,
33 ClusterDirError, PIDFileError
32 )
34 )
33
35
34 from twisted.internet import reactor, defer
35 from twisted.python import log, failure
36
37
36
38 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
39 # The ipcluster application
38 # Module level variables
40 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
41
40
42
41
42 default_config_file_name = u'ipcluster_config.py'
43
44
45 _description = """\
46 Start an IPython cluster for parallel computing.\n\n
47
48 An IPython cluster consists of 1 controller and 1 or more engines.
49 This command automates the startup of these processes using a wide
50 range of startup methods (SSH, local processes, PBS, mpiexec,
51 Windows HPC Server 2008). To start a cluster with 4 engines on your
52 local host simply do "ipcluster start -n 4". For more complex usage
53 you will typically do "ipcluster create -p mycluster", then edit
54 configuration files, followed by "ipcluster start -p mycluster -n 4".
55 """
56
57
43 # Exit codes for ipcluster
58 # Exit codes for ipcluster
44
59
45 # This will be the exit code if the ipcluster appears to be running because
60 # This will be the exit code if the ipcluster appears to be running because
46 # a .pid file exists
61 # a .pid file exists
47 ALREADY_STARTED = 10
62 ALREADY_STARTED = 10
48
63
64
49 # This will be the exit code if ipcluster stop is run, but there is not .pid
65 # This will be the exit code if ipcluster stop is run, but there is not .pid
50 # file to be found.
66 # file to be found.
51 ALREADY_STOPPED = 11
67 ALREADY_STOPPED = 11
52
68
53
69
54 class IPClusterCLLoader(ArgParseConfigLoader):
70 #-----------------------------------------------------------------------------
71 # Command line options
72 #-----------------------------------------------------------------------------
73
55
74
56 def _add_other_arguments(self):
75 class IPClusterAppConfigLoader(ClusterDirConfigLoader):
76
77 def _add_arguments(self):
78 # Don't call ClusterDirConfigLoader._add_arguments as we don't want
79 # its defaults on self.parser. Instead, we will put those on
80 # default options on our subparsers.
81
57 # This has all the common options that all subcommands use
82 # This has all the common options that all subcommands use
58 parent_parser1 = ArgumentParser(add_help=False,
83 parent_parser1 = ArgumentParser(
59 argument_default=SUPPRESS)
84 add_help=False,
60 parent_parser1.add_argument('--ipython-dir',
85 argument_default=SUPPRESS
61 dest='Global.ipython_dir',type=unicode,
86 )
62 help='Set to override default location of Global.ipython_dir.',
87 self._add_ipython_dir(parent_parser1)
63 metavar='Global.ipython_dir')
88 self._add_log_level(parent_parser1)
64 parent_parser1.add_argument('--log-level',
65 dest="Global.log_level",type=int,
66 help='Set the log level (0,10,20,30,40,50). Default is 30.',
67 metavar='Global.log_level')
68
89
69 # This has all the common options that other subcommands use
90 # This has all the common options that other subcommands use
70 parent_parser2 = ArgumentParser(add_help=False,
91 parent_parser2 = ArgumentParser(
71 argument_default=SUPPRESS)
92 add_help=False,
72 parent_parser2.add_argument('-p','--profile',
93 argument_default=SUPPRESS
73 dest='Global.profile',type=unicode,
74 help='The string name of the profile to be used. This determines '
75 'the name of the cluster dir as: cluster_<profile>. The default profile '
76 'is named "default". The cluster directory is resolve this way '
77 'if the --cluster-dir option is not used.',
78 metavar='Global.profile')
79 parent_parser2.add_argument('--cluster-dir',
80 dest='Global.cluster_dir',type=unicode,
81 help='Set the cluster dir. This overrides the logic used by the '
82 '--profile option.',
83 metavar='Global.cluster_dir'),
84 parent_parser2.add_argument('--work-dir',
85 dest='Global.work_dir',type=unicode,
86 help='Set the working dir for the process.',
87 metavar='Global.work_dir')
88 parent_parser2.add_argument('--log-to-file',
89 action='store_true', dest='Global.log_to_file',
90 help='Log to a file in the log directory (default is stdout)'
91 )
94 )
95 self._add_cluster_profile(parent_parser2)
96 self._add_cluster_dir(parent_parser2)
97 self._add_work_dir(parent_parser2)
98 paa = parent_parser2.add_argument
99 paa('--log-to-file',
100 action='store_true', dest='Global.log_to_file',
101 help='Log to a file in the log directory (default is stdout)')
92
102
103 # Create the object used to create the subparsers.
93 subparsers = self.parser.add_subparsers(
104 subparsers = self.parser.add_subparsers(
94 dest='Global.subcommand',
105 dest='Global.subcommand',
95 title='ipcluster subcommands',
106 title='ipcluster subcommands',
96 description='ipcluster has a variety of subcommands. '
107 description=
97 'The general way of running ipcluster is "ipcluster <cmd> '
108 """ipcluster has a variety of subcommands. The general way of
98 ' [options]""',
109 running ipcluster is 'ipcluster <cmd> [options]'""",
99 help='For more help, type "ipcluster <cmd> -h"')
110 help="For more help, type 'ipcluster <cmd> -h'"
111 )
100
112
113 # The "list" subcommand parser
101 parser_list = subparsers.add_parser(
114 parser_list = subparsers.add_parser(
102 'list',
115 'list',
103 help='List all clusters in cwd and ipython_dir.',
116 help='List all clusters in cwd and ipython_dir.',
104 parents=[parent_parser1]
117 parents=[parent_parser1]
105 )
118 )
106
119
120 # The "create" subcommand parser
107 parser_create = subparsers.add_parser(
121 parser_create = subparsers.add_parser(
108 'create',
122 'create',
109 help='Create a new cluster directory.',
123 help='Create a new cluster directory.',
110 parents=[parent_parser1, parent_parser2]
124 parents=[parent_parser1, parent_parser2]
111 )
125 )
112 parser_create.add_argument(
126 paa = parser_create.add_argument
113 '--reset-config',
127 paa('--reset-config',
114 dest='Global.reset_config', action='store_true',
128 dest='Global.reset_config', action='store_true',
115 help='Recopy the default config files to the cluster directory. '
129 help=
116 'You will loose any modifications you have made to these files.'
130 """Recopy the default config files to the cluster directory.
117 )
131 You will loose any modifications you have made to these files.""")
118
132
133 # The "start" subcommand parser
119 parser_start = subparsers.add_parser(
134 parser_start = subparsers.add_parser(
120 'start',
135 'start',
121 help='Start a cluster.',
136 help='Start a cluster.',
122 parents=[parent_parser1, parent_parser2]
137 parents=[parent_parser1, parent_parser2]
123 )
138 )
124 parser_start.add_argument(
139 paa = parser_start.add_argument
125 '-n', '--number',
140 paa('-n', '--number',
126 type=int, dest='Global.n',
141 type=int, dest='Global.n',
127 help='The number of engines to start.',
142 help='The number of engines to start.',
128 metavar='Global.n'
143 metavar='Global.n')
129 )
144 paa('--clean-logs',
130 parser_start.add_argument('--clean-logs',
131 dest='Global.clean_logs', action='store_true',
145 dest='Global.clean_logs', action='store_true',
132 help='Delete old log flies before starting.',
146 help='Delete old log flies before starting.')
133 )
147 paa('--no-clean-logs',
134 parser_start.add_argument('--no-clean-logs',
135 dest='Global.clean_logs', action='store_false',
148 dest='Global.clean_logs', action='store_false',
136 help="Don't delete old log flies before starting.",
149 help="Don't delete old log flies before starting.")
137 )
150 paa('--daemon',
138 parser_start.add_argument('--daemon',
139 dest='Global.daemonize', action='store_true',
151 dest='Global.daemonize', action='store_true',
140 help='Daemonize the ipcluster program. This implies --log-to-file',
152 help='Daemonize the ipcluster program. This implies --log-to-file')
141 )
153 paa('--no-daemon',
142 parser_start.add_argument('--no-daemon',
143 dest='Global.daemonize', action='store_false',
154 dest='Global.daemonize', action='store_false',
144 help="Dont't daemonize the ipcluster program.",
155 help="Dont't daemonize the ipcluster program.")
145 )
146
156
147 parser_start = subparsers.add_parser(
157 # The "stop" subcommand parser
158 parser_stop = subparsers.add_parser(
148 'stop',
159 'stop',
149 help='Stop a cluster.',
160 help='Stop a cluster.',
150 parents=[parent_parser1, parent_parser2]
161 parents=[parent_parser1, parent_parser2]
151 )
162 )
152 parser_start.add_argument('--signal',
163 paa = parser_stop.add_argument
164 paa('--signal',
153 dest='Global.signal', type=int,
165 dest='Global.signal', type=int,
154 help="The signal number to use in stopping the cluster (default=2).",
166 help="The signal number to use in stopping the cluster (default=2).",
155 metavar="Global.signal",
167 metavar="Global.signal")
156 )
157
158
159 default_config_file_name = u'ipcluster_config.py'
160
168
161
169
162 _description = """Start an IPython cluster for parallel computing.\n\n
170 #-----------------------------------------------------------------------------
163
171 # Main application
164 An IPython cluster consists of 1 controller and 1 or more engines.
172 #-----------------------------------------------------------------------------
165 This command automates the startup of these processes using a wide
166 range of startup methods (SSH, local processes, PBS, mpiexec,
167 Windows HPC Server 2008). To start a cluster with 4 engines on your
168 local host simply do "ipcluster start -n 4". For more complex usage
169 you will typically do "ipcluster create -p mycluster", then edit
170 configuration files, followed by "ipcluster start -p mycluster -n 4".
171 """
172
173
173
174
174 class IPClusterApp(ApplicationWithClusterDir):
175 class IPClusterApp(ApplicationWithClusterDir):
175
176
176 name = u'ipcluster'
177 name = u'ipcluster'
177 description = _description
178 description = _description
179 usage = None
180 command_line_loader = IPClusterAppConfigLoader
178 config_file_name = default_config_file_name
181 config_file_name = default_config_file_name
179 default_log_level = logging.INFO
182 default_log_level = logging.INFO
180 auto_create_cluster_dir = False
183 auto_create_cluster_dir = False
181
184
182 def create_default_config(self):
185 def create_default_config(self):
183 super(IPClusterApp, self).create_default_config()
186 super(IPClusterApp, self).create_default_config()
184 self.default_config.Global.controller_launcher = \
187 self.default_config.Global.controller_launcher = \
185 'IPython.kernel.launcher.LocalControllerLauncher'
188 'IPython.kernel.launcher.LocalControllerLauncher'
186 self.default_config.Global.engine_launcher = \
189 self.default_config.Global.engine_launcher = \
187 'IPython.kernel.launcher.LocalEngineSetLauncher'
190 'IPython.kernel.launcher.LocalEngineSetLauncher'
188 self.default_config.Global.n = 2
191 self.default_config.Global.n = 2
189 self.default_config.Global.reset_config = False
192 self.default_config.Global.reset_config = False
190 self.default_config.Global.clean_logs = True
193 self.default_config.Global.clean_logs = True
191 self.default_config.Global.signal = 2
194 self.default_config.Global.signal = 2
192 self.default_config.Global.daemonize = False
195 self.default_config.Global.daemonize = False
193
196
194 def create_command_line_config(self):
195 """Create and return a command line config loader."""
196 return IPClusterCLLoader(
197 description=self.description,
198 version=release.version
199 )
200
201 def find_resources(self):
197 def find_resources(self):
202 subcommand = self.command_line_config.Global.subcommand
198 subcommand = self.command_line_config.Global.subcommand
203 if subcommand=='list':
199 if subcommand=='list':
204 self.list_cluster_dirs()
200 self.list_cluster_dirs()
205 # Exit immediately because there is nothing left to do.
201 # Exit immediately because there is nothing left to do.
206 self.exit()
202 self.exit()
207 elif subcommand=='create':
203 elif subcommand=='create':
208 self.auto_create_cluster_dir = True
204 self.auto_create_cluster_dir = True
209 super(IPClusterApp, self).find_resources()
205 super(IPClusterApp, self).find_resources()
210 elif subcommand=='start' or subcommand=='stop':
206 elif subcommand=='start' or subcommand=='stop':
211 self.auto_create_cluster_dir = True
207 self.auto_create_cluster_dir = True
212 try:
208 try:
213 super(IPClusterApp, self).find_resources()
209 super(IPClusterApp, self).find_resources()
214 except ClusterDirError:
210 except ClusterDirError:
215 raise ClusterDirError(
211 raise ClusterDirError(
216 "Could not find a cluster directory. A cluster dir must "
212 "Could not find a cluster directory. A cluster dir must "
217 "be created before running 'ipcluster start'. Do "
213 "be created before running 'ipcluster start'. Do "
218 "'ipcluster create -h' or 'ipcluster list -h' for more "
214 "'ipcluster create -h' or 'ipcluster list -h' for more "
219 "information about creating and listing cluster dirs."
215 "information about creating and listing cluster dirs."
220 )
216 )
221
217
222 def list_cluster_dirs(self):
218 def list_cluster_dirs(self):
223 # Find the search paths
219 # Find the search paths
224 cluster_dir_paths = os.environ.get('IPCLUSTER_DIR_PATH','')
220 cluster_dir_paths = os.environ.get('IPCLUSTER_DIR_PATH','')
225 if cluster_dir_paths:
221 if cluster_dir_paths:
226 cluster_dir_paths = cluster_dir_paths.split(':')
222 cluster_dir_paths = cluster_dir_paths.split(':')
227 else:
223 else:
228 cluster_dir_paths = []
224 cluster_dir_paths = []
229 try:
225 try:
230 ipython_dir = self.command_line_config.Global.ipython_dir
226 ipython_dir = self.command_line_config.Global.ipython_dir
231 except AttributeError:
227 except AttributeError:
232 ipython_dir = self.default_config.Global.ipython_dir
228 ipython_dir = self.default_config.Global.ipython_dir
233 paths = [os.getcwd(), ipython_dir] + \
229 paths = [os.getcwd(), ipython_dir] + \
234 cluster_dir_paths
230 cluster_dir_paths
235 paths = list(set(paths))
231 paths = list(set(paths))
236
232
237 self.log.info('Searching for cluster dirs in paths: %r' % paths)
233 self.log.info('Searching for cluster dirs in paths: %r' % paths)
238 for path in paths:
234 for path in paths:
239 files = os.listdir(path)
235 files = os.listdir(path)
240 for f in files:
236 for f in files:
241 full_path = os.path.join(path, f)
237 full_path = os.path.join(path, f)
242 if os.path.isdir(full_path) and f.startswith('cluster_'):
238 if os.path.isdir(full_path) and f.startswith('cluster_'):
243 profile = full_path.split('_')[-1]
239 profile = full_path.split('_')[-1]
244 start_cmd = 'ipcluster start -p %s -n 4' % profile
240 start_cmd = 'ipcluster start -p %s -n 4' % profile
245 print start_cmd + " ==> " + full_path
241 print start_cmd + " ==> " + full_path
246
242
247 def pre_construct(self):
243 def pre_construct(self):
248 # IPClusterApp.pre_construct() is where we cd to the working directory.
244 # IPClusterApp.pre_construct() is where we cd to the working directory.
249 super(IPClusterApp, self).pre_construct()
245 super(IPClusterApp, self).pre_construct()
250 config = self.master_config
246 config = self.master_config
251 try:
247 try:
252 daemon = config.Global.daemonize
248 daemon = config.Global.daemonize
253 if daemon:
249 if daemon:
254 config.Global.log_to_file = True
250 config.Global.log_to_file = True
255 except AttributeError:
251 except AttributeError:
256 pass
252 pass
257
253
258 def construct(self):
254 def construct(self):
259 config = self.master_config
255 config = self.master_config
260 subcmd = config.Global.subcommand
256 subcmd = config.Global.subcommand
261 reset = config.Global.reset_config
257 reset = config.Global.reset_config
262 if subcmd == 'list':
258 if subcmd == 'list':
263 return
259 return
264 if subcmd == 'create':
260 if subcmd == 'create':
265 self.log.info('Copying default config files to cluster directory '
261 self.log.info('Copying default config files to cluster directory '
266 '[overwrite=%r]' % (reset,))
262 '[overwrite=%r]' % (reset,))
267 self.cluster_dir_obj.copy_all_config_files(overwrite=reset)
263 self.cluster_dir_obj.copy_all_config_files(overwrite=reset)
268 if subcmd =='start':
264 if subcmd =='start':
269 self.cluster_dir_obj.copy_all_config_files(overwrite=False)
265 self.cluster_dir_obj.copy_all_config_files(overwrite=False)
270 self.start_logging()
266 self.start_logging()
271 reactor.callWhenRunning(self.start_launchers)
267 reactor.callWhenRunning(self.start_launchers)
272
268
273 def start_launchers(self):
269 def start_launchers(self):
274 config = self.master_config
270 config = self.master_config
275
271
276 # Create the launchers. In both bases, we set the work_dir of
272 # Create the launchers. In both bases, we set the work_dir of
277 # the launcher to the cluster_dir. This is where the launcher's
273 # the launcher to the cluster_dir. This is where the launcher's
278 # subprocesses will be launched. It is not where the controller
274 # subprocesses will be launched. It is not where the controller
279 # and engine will be launched.
275 # and engine will be launched.
280 el_class = import_item(config.Global.engine_launcher)
276 el_class = import_item(config.Global.engine_launcher)
281 self.engine_launcher = el_class(
277 self.engine_launcher = el_class(
282 work_dir=self.cluster_dir, config=config
278 work_dir=self.cluster_dir, config=config
283 )
279 )
284 cl_class = import_item(config.Global.controller_launcher)
280 cl_class = import_item(config.Global.controller_launcher)
285 self.controller_launcher = cl_class(
281 self.controller_launcher = cl_class(
286 work_dir=self.cluster_dir, config=config
282 work_dir=self.cluster_dir, config=config
287 )
283 )
288
284
289 # Setup signals
285 # Setup signals
290 signal.signal(signal.SIGINT, self.sigint_handler)
286 signal.signal(signal.SIGINT, self.sigint_handler)
291
287
292 # Setup the observing of stopping. If the controller dies, shut
288 # Setup the observing of stopping. If the controller dies, shut
293 # everything down as that will be completely fatal for the engines.
289 # everything down as that will be completely fatal for the engines.
294 d1 = self.controller_launcher.observe_stop()
290 d1 = self.controller_launcher.observe_stop()
295 d1.addCallback(self.stop_launchers)
291 d1.addCallback(self.stop_launchers)
296 # But, we don't monitor the stopping of engines. An engine dying
292 # But, we don't monitor the stopping of engines. An engine dying
297 # is just fine and in principle a user could start a new engine.
293 # is just fine and in principle a user could start a new engine.
298 # Also, if we did monitor engine stopping, it is difficult to
294 # Also, if we did monitor engine stopping, it is difficult to
299 # know what to do when only some engines die. Currently, the
295 # know what to do when only some engines die. Currently, the
300 # observing of engine stopping is inconsistent. Some launchers
296 # observing of engine stopping is inconsistent. Some launchers
301 # might trigger on a single engine stopping, other wait until
297 # might trigger on a single engine stopping, other wait until
302 # all stop. TODO: think more about how to handle this.
298 # all stop. TODO: think more about how to handle this.
303
299
304 # Start the controller and engines
300 # Start the controller and engines
305 self._stopping = False # Make sure stop_launchers is not called 2x.
301 self._stopping = False # Make sure stop_launchers is not called 2x.
306 d = self.start_controller()
302 d = self.start_controller()
307 d.addCallback(self.start_engines)
303 d.addCallback(self.start_engines)
308 d.addCallback(self.startup_message)
304 d.addCallback(self.startup_message)
309 # If the controller or engines fail to start, stop everything
305 # If the controller or engines fail to start, stop everything
310 d.addErrback(self.stop_launchers)
306 d.addErrback(self.stop_launchers)
311 return d
307 return d
312
308
313 def startup_message(self, r=None):
309 def startup_message(self, r=None):
314 log.msg("IPython cluster: started")
310 log.msg("IPython cluster: started")
315 return r
311 return r
316
312
317 def start_controller(self, r=None):
313 def start_controller(self, r=None):
318 # log.msg("In start_controller")
314 # log.msg("In start_controller")
319 config = self.master_config
315 config = self.master_config
320 d = self.controller_launcher.start(
316 d = self.controller_launcher.start(
321 cluster_dir=config.Global.cluster_dir
317 cluster_dir=config.Global.cluster_dir
322 )
318 )
323 return d
319 return d
324
320
325 def start_engines(self, r=None):
321 def start_engines(self, r=None):
326 # log.msg("In start_engines")
322 # log.msg("In start_engines")
327 config = self.master_config
323 config = self.master_config
328 d = self.engine_launcher.start(
324 d = self.engine_launcher.start(
329 config.Global.n,
325 config.Global.n,
330 cluster_dir=config.Global.cluster_dir
326 cluster_dir=config.Global.cluster_dir
331 )
327 )
332 return d
328 return d
333
329
334 def stop_controller(self, r=None):
330 def stop_controller(self, r=None):
335 # log.msg("In stop_controller")
331 # log.msg("In stop_controller")
336 if self.controller_launcher.running:
332 if self.controller_launcher.running:
337 d = self.controller_launcher.stop()
333 d = self.controller_launcher.stop()
338 d.addErrback(self.log_err)
334 d.addErrback(self.log_err)
339 return d
335 return d
340 else:
336 else:
341 return defer.succeed(None)
337 return defer.succeed(None)
342
338
343 def stop_engines(self, r=None):
339 def stop_engines(self, r=None):
344 # log.msg("In stop_engines")
340 # log.msg("In stop_engines")
345 if self.engine_launcher.running:
341 if self.engine_launcher.running:
346 d = self.engine_launcher.stop()
342 d = self.engine_launcher.stop()
347 d.addErrback(self.log_err)
343 d.addErrback(self.log_err)
348 return d
344 return d
349 else:
345 else:
350 return defer.succeed(None)
346 return defer.succeed(None)
351
347
352 def log_err(self, f):
348 def log_err(self, f):
353 log.msg(f.getTraceback())
349 log.msg(f.getTraceback())
354 return None
350 return None
355
351
356 def stop_launchers(self, r=None):
352 def stop_launchers(self, r=None):
357 if not self._stopping:
353 if not self._stopping:
358 self._stopping = True
354 self._stopping = True
359 if isinstance(r, failure.Failure):
355 if isinstance(r, failure.Failure):
360 log.msg('Unexpected error in ipcluster:')
356 log.msg('Unexpected error in ipcluster:')
361 log.msg(r.getTraceback())
357 log.msg(r.getTraceback())
362 log.msg("IPython cluster: stopping")
358 log.msg("IPython cluster: stopping")
363 # These return deferreds. We are not doing anything with them
359 # These return deferreds. We are not doing anything with them
364 # but we are holding refs to them as a reminder that they
360 # but we are holding refs to them as a reminder that they
365 # do return deferreds.
361 # do return deferreds.
366 d1 = self.stop_engines()
362 d1 = self.stop_engines()
367 d2 = self.stop_controller()
363 d2 = self.stop_controller()
368 # Wait a few seconds to let things shut down.
364 # Wait a few seconds to let things shut down.
369 reactor.callLater(4.0, reactor.stop)
365 reactor.callLater(4.0, reactor.stop)
370
366
371 def sigint_handler(self, signum, frame):
367 def sigint_handler(self, signum, frame):
372 self.stop_launchers()
368 self.stop_launchers()
373
369
374 def start_logging(self):
370 def start_logging(self):
375 # Remove old log files of the controller and engine
371 # Remove old log files of the controller and engine
376 if self.master_config.Global.clean_logs:
372 if self.master_config.Global.clean_logs:
377 log_dir = self.master_config.Global.log_dir
373 log_dir = self.master_config.Global.log_dir
378 for f in os.listdir(log_dir):
374 for f in os.listdir(log_dir):
379 if f.startswith('ipengine' + '-'):
375 if f.startswith('ipengine' + '-'):
380 if f.endswith('.log') or f.endswith('.out') or f.endswith('.err'):
376 if f.endswith('.log') or f.endswith('.out') or f.endswith('.err'):
381 os.remove(os.path.join(log_dir, f))
377 os.remove(os.path.join(log_dir, f))
382 if f.startswith('ipcontroller' + '-'):
378 if f.startswith('ipcontroller' + '-'):
383 if f.endswith('.log') or f.endswith('.out') or f.endswith('.err'):
379 if f.endswith('.log') or f.endswith('.out') or f.endswith('.err'):
384 os.remove(os.path.join(log_dir, f))
380 os.remove(os.path.join(log_dir, f))
385 # This will remote old log files for ipcluster itself
381 # This will remote old log files for ipcluster itself
386 super(IPClusterApp, self).start_logging()
382 super(IPClusterApp, self).start_logging()
387
383
388 def start_app(self):
384 def start_app(self):
389 """Start the application, depending on what subcommand is used."""
385 """Start the application, depending on what subcommand is used."""
390 subcmd = self.master_config.Global.subcommand
386 subcmd = self.master_config.Global.subcommand
391 if subcmd=='create' or subcmd=='list':
387 if subcmd=='create' or subcmd=='list':
392 return
388 return
393 elif subcmd=='start':
389 elif subcmd=='start':
394 self.start_app_start()
390 self.start_app_start()
395 elif subcmd=='stop':
391 elif subcmd=='stop':
396 self.start_app_stop()
392 self.start_app_stop()
397
393
398 def start_app_start(self):
394 def start_app_start(self):
399 """Start the app for the start subcommand."""
395 """Start the app for the start subcommand."""
400 config = self.master_config
396 config = self.master_config
401 # First see if the cluster is already running
397 # First see if the cluster is already running
402 try:
398 try:
403 pid = self.get_pid_from_file()
399 pid = self.get_pid_from_file()
404 except PIDFileError:
400 except PIDFileError:
405 pass
401 pass
406 else:
402 else:
407 self.log.critical(
403 self.log.critical(
408 'Cluster is already running with [pid=%s]. '
404 'Cluster is already running with [pid=%s]. '
409 'use "ipcluster stop" to stop the cluster.' % pid
405 'use "ipcluster stop" to stop the cluster.' % pid
410 )
406 )
411 # Here I exit with a unusual exit status that other processes
407 # Here I exit with a unusual exit status that other processes
412 # can watch for to learn how I existed.
408 # can watch for to learn how I existed.
413 self.exit(ALREADY_STARTED)
409 self.exit(ALREADY_STARTED)
414
410
415 # Now log and daemonize
411 # Now log and daemonize
416 self.log.info(
412 self.log.info(
417 'Starting ipcluster with [daemon=%r]' % config.Global.daemonize
413 'Starting ipcluster with [daemon=%r]' % config.Global.daemonize
418 )
414 )
419 # TODO: Get daemonize working on Windows or as a Windows Server.
415 # TODO: Get daemonize working on Windows or as a Windows Server.
420 if config.Global.daemonize:
416 if config.Global.daemonize:
421 if os.name=='posix':
417 if os.name=='posix':
422 daemonize()
418 daemonize()
423
419
424 # Now write the new pid file AFTER our new forked pid is active.
420 # Now write the new pid file AFTER our new forked pid is active.
425 self.write_pid_file()
421 self.write_pid_file()
426 reactor.addSystemEventTrigger('during','shutdown', self.remove_pid_file)
422 reactor.addSystemEventTrigger('during','shutdown', self.remove_pid_file)
427 reactor.run()
423 reactor.run()
428
424
429 def start_app_stop(self):
425 def start_app_stop(self):
430 """Start the app for the stop subcommand."""
426 """Start the app for the stop subcommand."""
431 config = self.master_config
427 config = self.master_config
432 try:
428 try:
433 pid = self.get_pid_from_file()
429 pid = self.get_pid_from_file()
434 except PIDFileError:
430 except PIDFileError:
435 self.log.critical(
431 self.log.critical(
436 'Problem reading pid file, cluster is probably not running.'
432 'Problem reading pid file, cluster is probably not running.'
437 )
433 )
438 # Here I exit with a unusual exit status that other processes
434 # Here I exit with a unusual exit status that other processes
439 # can watch for to learn how I existed.
435 # can watch for to learn how I existed.
440 self.exit(ALREADY_STOPPED)
436 self.exit(ALREADY_STOPPED)
441 else:
437 else:
442 if os.name=='posix':
438 if os.name=='posix':
443 sig = config.Global.signal
439 sig = config.Global.signal
444 self.log.info(
440 self.log.info(
445 "Stopping cluster [pid=%r] with [signal=%r]" % (pid, sig)
441 "Stopping cluster [pid=%r] with [signal=%r]" % (pid, sig)
446 )
442 )
447 os.kill(pid, sig)
443 os.kill(pid, sig)
448 elif os.name=='nt':
444 elif os.name=='nt':
449 # As of right now, we don't support daemonize on Windows, so
445 # As of right now, we don't support daemonize on Windows, so
450 # stop will not do anything. Minimally, it should clean up the
446 # stop will not do anything. Minimally, it should clean up the
451 # old .pid files.
447 # old .pid files.
452 self.remove_pid_file()
448 self.remove_pid_file()
453
449
450
454 def launch_new_instance():
451 def launch_new_instance():
455 """Create and run the IPython cluster."""
452 """Create and run the IPython cluster."""
456 app = IPClusterApp()
453 app = IPClusterApp()
457 app.start()
454 app.start()
458
455
459
456
460 if __name__ == '__main__':
457 if __name__ == '__main__':
461 launch_new_instance()
458 launch_new_instance()
462
459
@@ -1,253 +1,260 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # encoding: utf-8
2 # encoding: utf-8
3 """
3 """
4 The IPython controller application.
4 The IPython controller application.
5 """
5 """
6
6
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8 # Copyright (C) 2008-2009 The IPython Development Team
8 # Copyright (C) 2008-2009 The IPython Development Team
9 #
9 #
10 # Distributed under the terms of the BSD License. The full license is in
10 # Distributed under the terms of the BSD License. The full license is in
11 # the file COPYING, distributed as part of this software.
11 # the file COPYING, distributed as part of this software.
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13
13
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 # Imports
15 # Imports
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17
17
18 from __future__ import with_statement
18 from __future__ import with_statement
19
19
20 import copy
20 import copy
21 import sys
21 import sys
22
22
23 from twisted.application import service
23 from twisted.application import service
24 from twisted.internet import reactor
24 from twisted.internet import reactor
25 from twisted.python import log
25 from twisted.python import log
26
26
27 from IPython.config.loader import Config
27 from IPython.config.loader import Config
28 from IPython.core.application import Application
29 from IPython.kernel import controllerservice
28 from IPython.kernel import controllerservice
30 from IPython.kernel.clusterdir import ApplicationWithClusterDir
29 from IPython.kernel.clusterdir import (
30 ApplicationWithClusterDir,
31 ClusterDirConfigLoader
32 )
31 from IPython.kernel.fcutil import FCServiceFactory
33 from IPython.kernel.fcutil import FCServiceFactory
32 from IPython.utils.traitlets import Instance, Unicode
34 from IPython.utils.traitlets import Instance, Unicode
33
35
36
37 #-----------------------------------------------------------------------------
38 # Module level variables
39 #-----------------------------------------------------------------------------
40
41
42 #: The default config file name for this application
43 default_config_file_name = u'ipcontroller_config.py'
44
45
46 _description = """Start the IPython controller for parallel computing.
47
48 The IPython controller provides a gateway between the IPython engines and
49 clients. The controller needs to be started before the engines and can be
50 configured using command line options or using a cluster directory. Cluster
51 directories contain config, log and security files and are usually located in
52 your .ipython directory and named as "cluster_<profile>". See the --profile
53 and --cluster-dir options for details.
54 """
55
34 #-----------------------------------------------------------------------------
56 #-----------------------------------------------------------------------------
35 # Default interfaces
57 # Default interfaces
36 #-----------------------------------------------------------------------------
58 #-----------------------------------------------------------------------------
37
59
38 # The default client interfaces for FCClientServiceFactory.interfaces
60 # The default client interfaces for FCClientServiceFactory.interfaces
39 default_client_interfaces = Config()
61 default_client_interfaces = Config()
40 default_client_interfaces.Task.interface_chain = [
62 default_client_interfaces.Task.interface_chain = [
41 'IPython.kernel.task.ITaskController',
63 'IPython.kernel.task.ITaskController',
42 'IPython.kernel.taskfc.IFCTaskController'
64 'IPython.kernel.taskfc.IFCTaskController'
43 ]
65 ]
44
66
45 default_client_interfaces.Task.furl_file = 'ipcontroller-tc.furl'
67 default_client_interfaces.Task.furl_file = 'ipcontroller-tc.furl'
46
68
47 default_client_interfaces.MultiEngine.interface_chain = [
69 default_client_interfaces.MultiEngine.interface_chain = [
48 'IPython.kernel.multiengine.IMultiEngine',
70 'IPython.kernel.multiengine.IMultiEngine',
49 'IPython.kernel.multienginefc.IFCSynchronousMultiEngine'
71 'IPython.kernel.multienginefc.IFCSynchronousMultiEngine'
50 ]
72 ]
51
73
52 default_client_interfaces.MultiEngine.furl_file = u'ipcontroller-mec.furl'
74 default_client_interfaces.MultiEngine.furl_file = u'ipcontroller-mec.furl'
53
75
54 # Make this a dict we can pass to Config.__init__ for the default
76 # Make this a dict we can pass to Config.__init__ for the default
55 default_client_interfaces = dict(copy.deepcopy(default_client_interfaces.items()))
77 default_client_interfaces = dict(copy.deepcopy(default_client_interfaces.items()))
56
78
57
79
58
80
59 # The default engine interfaces for FCEngineServiceFactory.interfaces
81 # The default engine interfaces for FCEngineServiceFactory.interfaces
60 default_engine_interfaces = Config()
82 default_engine_interfaces = Config()
61 default_engine_interfaces.Default.interface_chain = [
83 default_engine_interfaces.Default.interface_chain = [
62 'IPython.kernel.enginefc.IFCControllerBase'
84 'IPython.kernel.enginefc.IFCControllerBase'
63 ]
85 ]
64
86
65 default_engine_interfaces.Default.furl_file = u'ipcontroller-engine.furl'
87 default_engine_interfaces.Default.furl_file = u'ipcontroller-engine.furl'
66
88
67 # Make this a dict we can pass to Config.__init__ for the default
89 # Make this a dict we can pass to Config.__init__ for the default
68 default_engine_interfaces = dict(copy.deepcopy(default_engine_interfaces.items()))
90 default_engine_interfaces = dict(copy.deepcopy(default_engine_interfaces.items()))
69
91
70
92
71 #-----------------------------------------------------------------------------
93 #-----------------------------------------------------------------------------
72 # Service factories
94 # Service factories
73 #-----------------------------------------------------------------------------
95 #-----------------------------------------------------------------------------
74
96
75
97
76 class FCClientServiceFactory(FCServiceFactory):
98 class FCClientServiceFactory(FCServiceFactory):
77 """A Foolscap implementation of the client services."""
99 """A Foolscap implementation of the client services."""
78
100
79 cert_file = Unicode(u'ipcontroller-client.pem', config=True)
101 cert_file = Unicode(u'ipcontroller-client.pem', config=True)
80 interfaces = Instance(klass=Config, kw=default_client_interfaces,
102 interfaces = Instance(klass=Config, kw=default_client_interfaces,
81 allow_none=False, config=True)
103 allow_none=False, config=True)
82
104
83
105
84 class FCEngineServiceFactory(FCServiceFactory):
106 class FCEngineServiceFactory(FCServiceFactory):
85 """A Foolscap implementation of the engine services."""
107 """A Foolscap implementation of the engine services."""
86
108
87 cert_file = Unicode(u'ipcontroller-engine.pem', config=True)
109 cert_file = Unicode(u'ipcontroller-engine.pem', config=True)
88 interfaces = Instance(klass=dict, kw=default_engine_interfaces,
110 interfaces = Instance(klass=dict, kw=default_engine_interfaces,
89 allow_none=False, config=True)
111 allow_none=False, config=True)
90
112
91
113
92 #-----------------------------------------------------------------------------
114 #-----------------------------------------------------------------------------
93 # The main application
115 # Command line options
94 #-----------------------------------------------------------------------------
116 #-----------------------------------------------------------------------------
95
117
96
118
97 cl_args = (
119 class IPControllerAppConfigLoader(ClusterDirConfigLoader):
98 # Client config
120
99 (('--client-ip',), dict(
121 def _add_arguments(self):
100 type=str, dest='FCClientServiceFactory.ip',
122 super(IPControllerAppConfigLoader, self)._add_arguments()
101 help='The IP address or hostname the controller will listen on for '
123 paa = self.parser.add_argument
102 'client connections.',
124 # Client config
103 metavar='FCClientServiceFactory.ip')
125 paa('--client-ip',
104 ),
126 type=str, dest='FCClientServiceFactory.ip',
105 (('--client-port',), dict(
127 help='The IP address or hostname the controller will listen on for '
106 type=int, dest='FCClientServiceFactory.port',
128 'client connections.',
107 help='The port the controller will listen on for client connections. '
129 metavar='FCClientServiceFactory.ip')
108 'The default is to use 0, which will autoselect an open port.',
130 paa('--client-port',
109 metavar='FCClientServiceFactory.port')
131 type=int, dest='FCClientServiceFactory.port',
110 ),
132 help='The port the controller will listen on for client connections. '
111 (('--client-location',), dict(
133 'The default is to use 0, which will autoselect an open port.',
112 type=str, dest='FCClientServiceFactory.location',
134 metavar='FCClientServiceFactory.port')
113 help='The hostname or IP that clients should connect to. This does '
135 paa('--client-location',), dict(
114 'not control which interface the controller listens on. Instead, this '
136 type=str, dest='FCClientServiceFactory.location',
115 'determines the hostname/IP that is listed in the FURL, which is how '
137 help='The hostname or IP that clients should connect to. This does '
116 'clients know where to connect. Useful if the controller is listening '
138 'not control which interface the controller listens on. Instead, this '
117 'on multiple interfaces.',
139 'determines the hostname/IP that is listed in the FURL, which is how '
118 metavar='FCClientServiceFactory.location')
140 'clients know where to connect. Useful if the controller is listening '
119 ),
141 'on multiple interfaces.',
120 # Engine config
142 metavar='FCClientServiceFactory.location')
121 (('--engine-ip',), dict(
143 # Engine config
122 type=str, dest='FCEngineServiceFactory.ip',
144 paa('--engine-ip',
123 help='The IP address or hostname the controller will listen on for '
145 type=str, dest='FCEngineServiceFactory.ip',
124 'engine connections.',
146 help='The IP address or hostname the controller will listen on for '
125 metavar='FCEngineServiceFactory.ip')
147 'engine connections.',
126 ),
148 metavar='FCEngineServiceFactory.ip')
127 (('--engine-port',), dict(
149 paa('--engine-port',
128 type=int, dest='FCEngineServiceFactory.port',
150 type=int, dest='FCEngineServiceFactory.port',
129 help='The port the controller will listen on for engine connections. '
151 help='The port the controller will listen on for engine connections. '
130 'The default is to use 0, which will autoselect an open port.',
152 'The default is to use 0, which will autoselect an open port.',
131 metavar='FCEngineServiceFactory.port')
153 metavar='FCEngineServiceFactory.port')
132 ),
154 paa('--engine-location',
133 (('--engine-location',), dict(
155 type=str, dest='FCEngineServiceFactory.location',
134 type=str, dest='FCEngineServiceFactory.location',
156 help='The hostname or IP that engines should connect to. This does '
135 help='The hostname or IP that engines should connect to. This does '
157 'not control which interface the controller listens on. Instead, this '
136 'not control which interface the controller listens on. Instead, this '
158 'determines the hostname/IP that is listed in the FURL, which is how '
137 'determines the hostname/IP that is listed in the FURL, which is how '
159 'engines know where to connect. Useful if the controller is listening '
138 'engines know where to connect. Useful if the controller is listening '
160 'on multiple interfaces.',
139 'on multiple interfaces.',
161 metavar='FCEngineServiceFactory.location')
140 metavar='FCEngineServiceFactory.location')
162 # Global config
141 ),
163 paa('--log-to-file',
142 # Global config
164 action='store_true', dest='Global.log_to_file',
143 (('--log-to-file',), dict(
165 help='Log to a file in the log directory (default is stdout)')
144 action='store_true', dest='Global.log_to_file',
166 paa('-r','--reuse-furls',
145 help='Log to a file in the log directory (default is stdout)')
167 action='store_true', dest='Global.reuse_furls',
146 ),
168 help='Try to reuse all FURL files. If this is not set all FURL files '
147 (('-r','--reuse-furls'), dict(
169 'are deleted before the controller starts. This must be set if '
148 action='store_true', dest='Global.reuse_furls',
170 'specific ports are specified by --engine-port or --client-port.')
149 help='Try to reuse all FURL files. If this is not set all FURL files '
171 paa('--no-secure',
150 'are deleted before the controller starts. This must be set if '
172 action='store_false', dest='Global.secure',
151 'specific ports are specified by --engine-port or --client-port.')
173 help='Turn off SSL encryption for all connections.')
152 ),
174 paa('--secure',
153 (('--no-secure',), dict(
175 action='store_true', dest='Global.secure',
154 action='store_false', dest='Global.secure',
176 help='Turn off SSL encryption for all connections.')
155 help='Turn off SSL encryption for all connections.')
156 ),
157 (('--secure',), dict(
158 action='store_true', dest='Global.secure',
159 help='Turn off SSL encryption for all connections.')
160 )
161 )
162
177
163
178
164 _description = """Start the IPython controller for parallel computing.
179 #-----------------------------------------------------------------------------
165
180 # The main application
166 The IPython controller provides a gateway between the IPython engines and
181 #-----------------------------------------------------------------------------
167 clients. The controller needs to be started before the engines and can be
168 configured using command line options or using a cluster directory. Cluster
169 directories contain config, log and security files and are usually located in
170 your .ipython directory and named as "cluster_<profile>". See the --profile
171 and --cluster-dir options for details.
172 """
173
174 default_config_file_name = u'ipcontroller_config.py'
175
182
176
183
177 class IPControllerApp(ApplicationWithClusterDir):
184 class IPControllerApp(ApplicationWithClusterDir):
178
185
179 name = u'ipcontroller'
186 name = u'ipcontroller'
180 description = _description
187 description = _description
188 command_line_loader = IPControllerAppConfigLoader
181 config_file_name = default_config_file_name
189 config_file_name = default_config_file_name
182 auto_create_cluster_dir = True
190 auto_create_cluster_dir = True
183 cl_arguments = Application.cl_arguments + cl_args
184
191
185 def create_default_config(self):
192 def create_default_config(self):
186 super(IPControllerApp, self).create_default_config()
193 super(IPControllerApp, self).create_default_config()
187 self.default_config.Global.reuse_furls = False
194 self.default_config.Global.reuse_furls = False
188 self.default_config.Global.secure = True
195 self.default_config.Global.secure = True
189 self.default_config.Global.import_statements = []
196 self.default_config.Global.import_statements = []
190 self.default_config.Global.clean_logs = True
197 self.default_config.Global.clean_logs = True
191
198
192 def post_load_command_line_config(self):
199 def post_load_command_line_config(self):
193 # Now setup reuse_furls
200 # Now setup reuse_furls
194 c = self.command_line_config
201 c = self.command_line_config
195 if hasattr(c.Global, 'reuse_furls'):
202 if hasattr(c.Global, 'reuse_furls'):
196 c.FCClientServiceFactory.reuse_furls = c.Global.reuse_furls
203 c.FCClientServiceFactory.reuse_furls = c.Global.reuse_furls
197 c.FCEngineServiceFactory.reuse_furls = c.Global.reuse_furls
204 c.FCEngineServiceFactory.reuse_furls = c.Global.reuse_furls
198 del c.Global.reuse_furls
205 del c.Global.reuse_furls
199 if hasattr(c.Global, 'secure'):
206 if hasattr(c.Global, 'secure'):
200 c.FCClientServiceFactory.secure = c.Global.secure
207 c.FCClientServiceFactory.secure = c.Global.secure
201 c.FCEngineServiceFactory.secure = c.Global.secure
208 c.FCEngineServiceFactory.secure = c.Global.secure
202 del c.Global.secure
209 del c.Global.secure
203
210
204 def construct(self):
211 def construct(self):
205 # This is the working dir by now.
212 # This is the working dir by now.
206 sys.path.insert(0, '')
213 sys.path.insert(0, '')
207
214
208 self.start_logging()
215 self.start_logging()
209 self.import_statements()
216 self.import_statements()
210
217
211 # Create the service hierarchy
218 # Create the service hierarchy
212 self.main_service = service.MultiService()
219 self.main_service = service.MultiService()
213 # The controller service
220 # The controller service
214 controller_service = controllerservice.ControllerService()
221 controller_service = controllerservice.ControllerService()
215 controller_service.setServiceParent(self.main_service)
222 controller_service.setServiceParent(self.main_service)
216 # The client tub and all its refereceables
223 # The client tub and all its refereceables
217 csfactory = FCClientServiceFactory(self.master_config, controller_service)
224 csfactory = FCClientServiceFactory(self.master_config, controller_service)
218 client_service = csfactory.create()
225 client_service = csfactory.create()
219 client_service.setServiceParent(self.main_service)
226 client_service.setServiceParent(self.main_service)
220 # The engine tub
227 # The engine tub
221 esfactory = FCEngineServiceFactory(self.master_config, controller_service)
228 esfactory = FCEngineServiceFactory(self.master_config, controller_service)
222 engine_service = esfactory.create()
229 engine_service = esfactory.create()
223 engine_service.setServiceParent(self.main_service)
230 engine_service.setServiceParent(self.main_service)
224
231
225 def import_statements(self):
232 def import_statements(self):
226 statements = self.master_config.Global.import_statements
233 statements = self.master_config.Global.import_statements
227 for s in statements:
234 for s in statements:
228 try:
235 try:
229 log.msg("Executing statement: '%s'" % s)
236 log.msg("Executing statement: '%s'" % s)
230 exec s in globals(), locals()
237 exec s in globals(), locals()
231 except:
238 except:
232 log.msg("Error running statement: %s" % s)
239 log.msg("Error running statement: %s" % s)
233
240
234 def start_app(self):
241 def start_app(self):
235 # Start the controller service.
242 # Start the controller service.
236 self.main_service.startService()
243 self.main_service.startService()
237 # Write the .pid file overwriting old ones. This allow multiple
244 # Write the .pid file overwriting old ones. This allow multiple
238 # controllers to clober each other. But Windows is not cleaning
245 # controllers to clober each other. But Windows is not cleaning
239 # these up properly.
246 # these up properly.
240 self.write_pid_file(overwrite=True)
247 self.write_pid_file(overwrite=True)
241 # Add a trigger to delete the .pid file upon shutting down.
248 # Add a trigger to delete the .pid file upon shutting down.
242 reactor.addSystemEventTrigger('during','shutdown', self.remove_pid_file)
249 reactor.addSystemEventTrigger('during','shutdown', self.remove_pid_file)
243 reactor.run()
250 reactor.run()
244
251
245
252
246 def launch_new_instance():
253 def launch_new_instance():
247 """Create and run the IPython controller"""
254 """Create and run the IPython controller"""
248 app = IPControllerApp()
255 app = IPControllerApp()
249 app.start()
256 app.start()
250
257
251
258
252 if __name__ == '__main__':
259 if __name__ == '__main__':
253 launch_new_instance()
260 launch_new_instance()
@@ -1,229 +1,242 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # encoding: utf-8
2 # encoding: utf-8
3 """
3 """
4 The IPython controller application
4 The IPython controller application
5 """
5 """
6
6
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8 # Copyright (C) 2008-2009 The IPython Development Team
8 # Copyright (C) 2008-2009 The IPython Development Team
9 #
9 #
10 # Distributed under the terms of the BSD License. The full license is in
10 # Distributed under the terms of the BSD License. The full license is in
11 # the file COPYING, distributed as part of this software.
11 # the file COPYING, distributed as part of this software.
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13
13
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 # Imports
15 # Imports
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17
17
18 import os
18 import os
19 import sys
19 import sys
20
20
21 from twisted.application import service
21 from twisted.application import service
22 from twisted.internet import reactor
22 from twisted.internet import reactor
23 from twisted.python import log
23 from twisted.python import log
24
24
25 from IPython.core.application import Application
25 from IPython.kernel.clusterdir import (
26 from IPython.kernel.clusterdir import ApplicationWithClusterDir
26 ApplicationWithClusterDir,
27 ClusterDirConfigLoader
28 )
27 from IPython.kernel.engineconnector import EngineConnector
29 from IPython.kernel.engineconnector import EngineConnector
28 from IPython.kernel.engineservice import EngineService
30 from IPython.kernel.engineservice import EngineService
29 from IPython.kernel.fcutil import Tub
31 from IPython.kernel.fcutil import Tub
30 from IPython.utils.importstring import import_item
32 from IPython.utils.importstring import import_item
31
33
32 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
33 # The main application
35 # Module level variables
34 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
35
37
36 cl_args = (
38 #: The default config file name for this application
37 # Controller config
39 default_config_file_name = u'ipengine_config.py'
38 (('--furl-file',), dict(
39 type=unicode, dest='Global.furl_file',
40 help='The full location of the file containing the FURL of the '
41 'controller. If this is not given, the FURL file must be in the '
42 'security directory of the cluster directory. This location is '
43 'resolved using the --profile and --app-dir options.',
44 metavar='Global.furl_file')
45 ),
46 # MPI
47 (('--mpi',), dict(
48 type=str, dest='MPI.use',
49 help='How to enable MPI (mpi4py, pytrilinos, or empty string to disable).',
50 metavar='MPI.use')
51 ),
52 # Global config
53 (('--log-to-file',), dict(
54 action='store_true', dest='Global.log_to_file',
55 help='Log to a file in the log directory (default is stdout)')
56 )
57 )
58
40
59
41
60 mpi4py_init = """from mpi4py import MPI as mpi
42 mpi4py_init = """from mpi4py import MPI as mpi
61 mpi.size = mpi.COMM_WORLD.Get_size()
43 mpi.size = mpi.COMM_WORLD.Get_size()
62 mpi.rank = mpi.COMM_WORLD.Get_rank()
44 mpi.rank = mpi.COMM_WORLD.Get_rank()
63 """
45 """
64
46
47
65 pytrilinos_init = """from PyTrilinos import Epetra
48 pytrilinos_init = """from PyTrilinos import Epetra
66 class SimpleStruct:
49 class SimpleStruct:
67 pass
50 pass
68 mpi = SimpleStruct()
51 mpi = SimpleStruct()
69 mpi.rank = 0
52 mpi.rank = 0
70 mpi.size = 0
53 mpi.size = 0
71 """
54 """
72
55
73
56
74 default_config_file_name = u'ipengine_config.py'
75
76
77 _description = """Start an IPython engine for parallel computing.\n\n
57 _description = """Start an IPython engine for parallel computing.\n\n
78
58
79 IPython engines run in parallel and perform computations on behalf of a client
59 IPython engines run in parallel and perform computations on behalf of a client
80 and controller. A controller needs to be started before the engines. The
60 and controller. A controller needs to be started before the engines. The
81 engine can be configured using command line options or using a cluster
61 engine can be configured using command line options or using a cluster
82 directory. Cluster directories contain config, log and security files and are
62 directory. Cluster directories contain config, log and security files and are
83 usually located in your .ipython directory and named as "cluster_<profile>".
63 usually located in your .ipython directory and named as "cluster_<profile>".
84 See the --profile and --cluster-dir options for details.
64 See the --profile and --cluster-dir options for details.
85 """
65 """
86
66
67 #-----------------------------------------------------------------------------
68 # Command line options
69 #-----------------------------------------------------------------------------
70
71
72 class IPEngineAppConfigLoader(ClusterDirConfigLoader):
73
74 def _add_arguments(self):
75 super(IPEngineAppConfigLoader, self)._add_arguments()
76 paa = self.parser.add_argument
77 # Controller config
78 paa('--furl-file',
79 type=unicode, dest='Global.furl_file',
80 help='The full location of the file containing the FURL of the '
81 'controller. If this is not given, the FURL file must be in the '
82 'security directory of the cluster directory. This location is '
83 'resolved using the --profile and --app-dir options.',
84 metavar='Global.furl_file')
85 # MPI
86 paa('--mpi',
87 type=str, dest='MPI.use',
88 help='How to enable MPI (mpi4py, pytrilinos, or empty string to disable).',
89 metavar='MPI.use')
90 # Global config
91 paa('--log-to-file',
92 action='store_true', dest='Global.log_to_file',
93 help='Log to a file in the log directory (default is stdout)')
94
95
96 #-----------------------------------------------------------------------------
97 # Main application
98 #-----------------------------------------------------------------------------
99
87
100
88 class IPEngineApp(ApplicationWithClusterDir):
101 class IPEngineApp(ApplicationWithClusterDir):
89
102
90 name = u'ipengine'
103 name = u'ipengine'
91 description = _description
104 description = _description
105 command_line_loader = IPEngineAppConfigLoader
92 config_file_name = default_config_file_name
106 config_file_name = default_config_file_name
93 auto_create_cluster_dir = True
107 auto_create_cluster_dir = True
94 cl_arguments = Application.cl_arguments + cl_args
95
108
96 def create_default_config(self):
109 def create_default_config(self):
97 super(IPEngineApp, self).create_default_config()
110 super(IPEngineApp, self).create_default_config()
98
111
99 # The engine should not clean logs as we don't want to remove the
112 # The engine should not clean logs as we don't want to remove the
100 # active log files of other running engines.
113 # active log files of other running engines.
101 self.default_config.Global.clean_logs = False
114 self.default_config.Global.clean_logs = False
102
115
103 # Global config attributes
116 # Global config attributes
104 self.default_config.Global.exec_lines = []
117 self.default_config.Global.exec_lines = []
105 self.default_config.Global.shell_class = 'IPython.kernel.core.interpreter.Interpreter'
118 self.default_config.Global.shell_class = 'IPython.kernel.core.interpreter.Interpreter'
106
119
107 # Configuration related to the controller
120 # Configuration related to the controller
108 # This must match the filename (path not included) that the controller
121 # This must match the filename (path not included) that the controller
109 # used for the FURL file.
122 # used for the FURL file.
110 self.default_config.Global.furl_file_name = u'ipcontroller-engine.furl'
123 self.default_config.Global.furl_file_name = u'ipcontroller-engine.furl'
111 # If given, this is the actual location of the controller's FURL file.
124 # If given, this is the actual location of the controller's FURL file.
112 # If not, this is computed using the profile, app_dir and furl_file_name
125 # If not, this is computed using the profile, app_dir and furl_file_name
113 self.default_config.Global.furl_file = u''
126 self.default_config.Global.furl_file = u''
114
127
115 # The max number of connection attemps and the initial delay between
128 # The max number of connection attemps and the initial delay between
116 # those attemps.
129 # those attemps.
117 self.default_config.Global.connect_delay = 0.1
130 self.default_config.Global.connect_delay = 0.1
118 self.default_config.Global.connect_max_tries = 15
131 self.default_config.Global.connect_max_tries = 15
119
132
120 # MPI related config attributes
133 # MPI related config attributes
121 self.default_config.MPI.use = ''
134 self.default_config.MPI.use = ''
122 self.default_config.MPI.mpi4py = mpi4py_init
135 self.default_config.MPI.mpi4py = mpi4py_init
123 self.default_config.MPI.pytrilinos = pytrilinos_init
136 self.default_config.MPI.pytrilinos = pytrilinos_init
124
137
125 def post_load_command_line_config(self):
138 def post_load_command_line_config(self):
126 pass
139 pass
127
140
128 def pre_construct(self):
141 def pre_construct(self):
129 super(IPEngineApp, self).pre_construct()
142 super(IPEngineApp, self).pre_construct()
130 self.find_cont_furl_file()
143 self.find_cont_furl_file()
131
144
132 def find_cont_furl_file(self):
145 def find_cont_furl_file(self):
133 """Set the furl file.
146 """Set the furl file.
134
147
135 Here we don't try to actually see if it exists for is valid as that
148 Here we don't try to actually see if it exists for is valid as that
136 is hadled by the connection logic.
149 is hadled by the connection logic.
137 """
150 """
138 config = self.master_config
151 config = self.master_config
139 # Find the actual controller FURL file
152 # Find the actual controller FURL file
140 if not config.Global.furl_file:
153 if not config.Global.furl_file:
141 try_this = os.path.join(
154 try_this = os.path.join(
142 config.Global.cluster_dir,
155 config.Global.cluster_dir,
143 config.Global.security_dir,
156 config.Global.security_dir,
144 config.Global.furl_file_name
157 config.Global.furl_file_name
145 )
158 )
146 config.Global.furl_file = try_this
159 config.Global.furl_file = try_this
147
160
148 def construct(self):
161 def construct(self):
149 # This is the working dir by now.
162 # This is the working dir by now.
150 sys.path.insert(0, '')
163 sys.path.insert(0, '')
151
164
152 self.start_mpi()
165 self.start_mpi()
153 self.start_logging()
166 self.start_logging()
154
167
155 # Create the underlying shell class and EngineService
168 # Create the underlying shell class and EngineService
156 shell_class = import_item(self.master_config.Global.shell_class)
169 shell_class = import_item(self.master_config.Global.shell_class)
157 self.engine_service = EngineService(shell_class, mpi=mpi)
170 self.engine_service = EngineService(shell_class, mpi=mpi)
158
171
159 self.exec_lines()
172 self.exec_lines()
160
173
161 # Create the service hierarchy
174 # Create the service hierarchy
162 self.main_service = service.MultiService()
175 self.main_service = service.MultiService()
163 self.engine_service.setServiceParent(self.main_service)
176 self.engine_service.setServiceParent(self.main_service)
164 self.tub_service = Tub()
177 self.tub_service = Tub()
165 self.tub_service.setServiceParent(self.main_service)
178 self.tub_service.setServiceParent(self.main_service)
166 # This needs to be called before the connection is initiated
179 # This needs to be called before the connection is initiated
167 self.main_service.startService()
180 self.main_service.startService()
168
181
169 # This initiates the connection to the controller and calls
182 # This initiates the connection to the controller and calls
170 # register_engine to tell the controller we are ready to do work
183 # register_engine to tell the controller we are ready to do work
171 self.engine_connector = EngineConnector(self.tub_service)
184 self.engine_connector = EngineConnector(self.tub_service)
172
185
173 log.msg("Using furl file: %s" % self.master_config.Global.furl_file)
186 log.msg("Using furl file: %s" % self.master_config.Global.furl_file)
174
187
175 reactor.callWhenRunning(self.call_connect)
188 reactor.callWhenRunning(self.call_connect)
176
189
177 def call_connect(self):
190 def call_connect(self):
178 d = self.engine_connector.connect_to_controller(
191 d = self.engine_connector.connect_to_controller(
179 self.engine_service,
192 self.engine_service,
180 self.master_config.Global.furl_file,
193 self.master_config.Global.furl_file,
181 self.master_config.Global.connect_delay,
194 self.master_config.Global.connect_delay,
182 self.master_config.Global.connect_max_tries
195 self.master_config.Global.connect_max_tries
183 )
196 )
184
197
185 def handle_error(f):
198 def handle_error(f):
186 log.msg('Error connecting to controller. This usually means that '
199 log.msg('Error connecting to controller. This usually means that '
187 'i) the controller was not started, ii) a firewall was blocking '
200 'i) the controller was not started, ii) a firewall was blocking '
188 'the engine from connecting to the controller or iii) the engine '
201 'the engine from connecting to the controller or iii) the engine '
189 ' was not pointed at the right FURL file:')
202 ' was not pointed at the right FURL file:')
190 log.msg(f.getErrorMessage())
203 log.msg(f.getErrorMessage())
191 reactor.callLater(0.1, reactor.stop)
204 reactor.callLater(0.1, reactor.stop)
192
205
193 d.addErrback(handle_error)
206 d.addErrback(handle_error)
194
207
195 def start_mpi(self):
208 def start_mpi(self):
196 global mpi
209 global mpi
197 mpikey = self.master_config.MPI.use
210 mpikey = self.master_config.MPI.use
198 mpi_import_statement = self.master_config.MPI.get(mpikey, None)
211 mpi_import_statement = self.master_config.MPI.get(mpikey, None)
199 if mpi_import_statement is not None:
212 if mpi_import_statement is not None:
200 try:
213 try:
201 self.log.info("Initializing MPI:")
214 self.log.info("Initializing MPI:")
202 self.log.info(mpi_import_statement)
215 self.log.info(mpi_import_statement)
203 exec mpi_import_statement in globals()
216 exec mpi_import_statement in globals()
204 except:
217 except:
205 mpi = None
218 mpi = None
206 else:
219 else:
207 mpi = None
220 mpi = None
208
221
209 def exec_lines(self):
222 def exec_lines(self):
210 for line in self.master_config.Global.exec_lines:
223 for line in self.master_config.Global.exec_lines:
211 try:
224 try:
212 log.msg("Executing statement: '%s'" % line)
225 log.msg("Executing statement: '%s'" % line)
213 self.engine_service.execute(line)
226 self.engine_service.execute(line)
214 except:
227 except:
215 log.msg("Error executing statement: %s" % line)
228 log.msg("Error executing statement: %s" % line)
216
229
217 def start_app(self):
230 def start_app(self):
218 reactor.run()
231 reactor.run()
219
232
220
233
221 def launch_new_instance():
234 def launch_new_instance():
222 """Create and run the IPython controller"""
235 """Create and run the IPython controller"""
223 app = IPEngineApp()
236 app = IPEngineApp()
224 app.start()
237 app.start()
225
238
226
239
227 if __name__ == '__main__':
240 if __name__ == '__main__':
228 launch_new_instance()
241 launch_new_instance()
229
242
General Comments 0
You need to be logged in to leave comments. Login now