##// END OF EJS Templates
Fix DeprecationWarning with system-wide argparse.
Thomas Kluyver -
Show More
@@ -1,373 +1,374 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # coding: utf-8
2 # coding: utf-8
3 """A simple configuration system.
3 """A simple configuration system.
4
4
5 Authors
5 Authors
6 -------
6 -------
7 * Brian Granger
7 * Brian Granger
8 * Fernando Perez
8 * Fernando Perez
9 """
9 """
10
10
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Copyright (C) 2008-2009 The IPython Development Team
12 # Copyright (C) 2008-2009 The IPython Development Team
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 # Imports
19 # Imports
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22 import __builtin__
22 import __builtin__
23 import os
23 import os
24 import sys
24 import sys
25
25
26 from IPython.external import argparse
26 from IPython.external import argparse
27 from IPython.utils.path import filefind
27 from IPython.utils.path import filefind
28
28
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30 # Exceptions
30 # Exceptions
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32
32
33
33
34 class ConfigError(Exception):
34 class ConfigError(Exception):
35 pass
35 pass
36
36
37
37
38 class ConfigLoaderError(ConfigError):
38 class ConfigLoaderError(ConfigError):
39 pass
39 pass
40
40
41 #-----------------------------------------------------------------------------
41 #-----------------------------------------------------------------------------
42 # Argparse fix
42 # Argparse fix
43 #-----------------------------------------------------------------------------
43 #-----------------------------------------------------------------------------
44
44
45 # Unfortunately argparse by default prints help messages to stderr instead of
45 # Unfortunately argparse by default prints help messages to stderr instead of
46 # stdout. This makes it annoying to capture long help screens at the command
46 # stdout. This makes it annoying to capture long help screens at the command
47 # line, since one must know how to pipe stderr, which many users don't know how
47 # line, since one must know how to pipe stderr, which many users don't know how
48 # to do. So we override the print_help method with one that defaults to
48 # to do. So we override the print_help method with one that defaults to
49 # stdout and use our class instead.
49 # stdout and use our class instead.
50
50
51 class ArgumentParser(argparse.ArgumentParser):
51 class ArgumentParser(argparse.ArgumentParser):
52 """Simple argparse subclass that prints help to stdout by default."""
52 """Simple argparse subclass that prints help to stdout by default."""
53
53
54 def print_help(self, file=None):
54 def print_help(self, file=None):
55 if file is None:
55 if file is None:
56 file = sys.stdout
56 file = sys.stdout
57 return super(ArgumentParser, self).print_help(file)
57 return super(ArgumentParser, self).print_help(file)
58
58
59 print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__
59 print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__
60
60
61 #-----------------------------------------------------------------------------
61 #-----------------------------------------------------------------------------
62 # Config class for holding config information
62 # Config class for holding config information
63 #-----------------------------------------------------------------------------
63 #-----------------------------------------------------------------------------
64
64
65
65
66 class Config(dict):
66 class Config(dict):
67 """An attribute based dict that can do smart merges."""
67 """An attribute based dict that can do smart merges."""
68
68
69 def __init__(self, *args, **kwds):
69 def __init__(self, *args, **kwds):
70 dict.__init__(self, *args, **kwds)
70 dict.__init__(self, *args, **kwds)
71 # This sets self.__dict__ = self, but it has to be done this way
71 # This sets self.__dict__ = self, but it has to be done this way
72 # because we are also overriding __setattr__.
72 # because we are also overriding __setattr__.
73 dict.__setattr__(self, '__dict__', self)
73 dict.__setattr__(self, '__dict__', self)
74
74
75 def _merge(self, other):
75 def _merge(self, other):
76 to_update = {}
76 to_update = {}
77 for k, v in other.iteritems():
77 for k, v in other.iteritems():
78 if not self.has_key(k):
78 if not self.has_key(k):
79 to_update[k] = v
79 to_update[k] = v
80 else: # I have this key
80 else: # I have this key
81 if isinstance(v, Config):
81 if isinstance(v, Config):
82 # Recursively merge common sub Configs
82 # Recursively merge common sub Configs
83 self[k]._merge(v)
83 self[k]._merge(v)
84 else:
84 else:
85 # Plain updates for non-Configs
85 # Plain updates for non-Configs
86 to_update[k] = v
86 to_update[k] = v
87
87
88 self.update(to_update)
88 self.update(to_update)
89
89
90 def _is_section_key(self, key):
90 def _is_section_key(self, key):
91 if key[0].upper()==key[0] and not key.startswith('_'):
91 if key[0].upper()==key[0] and not key.startswith('_'):
92 return True
92 return True
93 else:
93 else:
94 return False
94 return False
95
95
96 def __contains__(self, key):
96 def __contains__(self, key):
97 if self._is_section_key(key):
97 if self._is_section_key(key):
98 return True
98 return True
99 else:
99 else:
100 return super(Config, self).__contains__(key)
100 return super(Config, self).__contains__(key)
101 # .has_key is deprecated for dictionaries.
101 # .has_key is deprecated for dictionaries.
102 has_key = __contains__
102 has_key = __contains__
103
103
104 def _has_section(self, key):
104 def _has_section(self, key):
105 if self._is_section_key(key):
105 if self._is_section_key(key):
106 if super(Config, self).__contains__(key):
106 if super(Config, self).__contains__(key):
107 return True
107 return True
108 return False
108 return False
109
109
110 def copy(self):
110 def copy(self):
111 return type(self)(dict.copy(self))
111 return type(self)(dict.copy(self))
112
112
113 def __copy__(self):
113 def __copy__(self):
114 return self.copy()
114 return self.copy()
115
115
116 def __deepcopy__(self, memo):
116 def __deepcopy__(self, memo):
117 import copy
117 import copy
118 return type(self)(copy.deepcopy(self.items()))
118 return type(self)(copy.deepcopy(self.items()))
119
119
120 def __getitem__(self, key):
120 def __getitem__(self, key):
121 # Because we use this for an exec namespace, we need to delegate
121 # Because we use this for an exec namespace, we need to delegate
122 # the lookup of names in __builtin__ to itself. This means
122 # the lookup of names in __builtin__ to itself. This means
123 # that you can't have section or attribute names that are
123 # that you can't have section or attribute names that are
124 # builtins.
124 # builtins.
125 try:
125 try:
126 return getattr(__builtin__, key)
126 return getattr(__builtin__, key)
127 except AttributeError:
127 except AttributeError:
128 pass
128 pass
129 if self._is_section_key(key):
129 if self._is_section_key(key):
130 try:
130 try:
131 return dict.__getitem__(self, key)
131 return dict.__getitem__(self, key)
132 except KeyError:
132 except KeyError:
133 c = Config()
133 c = Config()
134 dict.__setitem__(self, key, c)
134 dict.__setitem__(self, key, c)
135 return c
135 return c
136 else:
136 else:
137 return dict.__getitem__(self, key)
137 return dict.__getitem__(self, key)
138
138
139 def __setitem__(self, key, value):
139 def __setitem__(self, key, value):
140 # Don't allow names in __builtin__ to be modified.
140 # Don't allow names in __builtin__ to be modified.
141 if hasattr(__builtin__, key):
141 if hasattr(__builtin__, key):
142 raise ConfigError('Config variable names cannot have the same name '
142 raise ConfigError('Config variable names cannot have the same name '
143 'as a Python builtin: %s' % key)
143 'as a Python builtin: %s' % key)
144 if self._is_section_key(key):
144 if self._is_section_key(key):
145 if not isinstance(value, Config):
145 if not isinstance(value, Config):
146 raise ValueError('values whose keys begin with an uppercase '
146 raise ValueError('values whose keys begin with an uppercase '
147 'char must be Config instances: %r, %r' % (key, value))
147 'char must be Config instances: %r, %r' % (key, value))
148 else:
148 else:
149 dict.__setitem__(self, key, value)
149 dict.__setitem__(self, key, value)
150
150
151 def __getattr__(self, key):
151 def __getattr__(self, key):
152 try:
152 try:
153 return self.__getitem__(key)
153 return self.__getitem__(key)
154 except KeyError, e:
154 except KeyError, e:
155 raise AttributeError(e)
155 raise AttributeError(e)
156
156
157 def __setattr__(self, key, value):
157 def __setattr__(self, key, value):
158 try:
158 try:
159 self.__setitem__(key, value)
159 self.__setitem__(key, value)
160 except KeyError, e:
160 except KeyError, e:
161 raise AttributeError(e)
161 raise AttributeError(e)
162
162
163 def __delattr__(self, key):
163 def __delattr__(self, key):
164 try:
164 try:
165 dict.__delitem__(self, key)
165 dict.__delitem__(self, key)
166 except KeyError, e:
166 except KeyError, e:
167 raise AttributeError(e)
167 raise AttributeError(e)
168
168
169
169
170 #-----------------------------------------------------------------------------
170 #-----------------------------------------------------------------------------
171 # Config loading classes
171 # Config loading classes
172 #-----------------------------------------------------------------------------
172 #-----------------------------------------------------------------------------
173
173
174
174
175 class ConfigLoader(object):
175 class ConfigLoader(object):
176 """A object for loading configurations from just about anywhere.
176 """A object for loading configurations from just about anywhere.
177
177
178 The resulting configuration is packaged as a :class:`Struct`.
178 The resulting configuration is packaged as a :class:`Struct`.
179
179
180 Notes
180 Notes
181 -----
181 -----
182 A :class:`ConfigLoader` does one thing: load a config from a source
182 A :class:`ConfigLoader` does one thing: load a config from a source
183 (file, command line arguments) and returns the data as a :class:`Struct`.
183 (file, command line arguments) and returns the data as a :class:`Struct`.
184 There are lots of things that :class:`ConfigLoader` does not do. It does
184 There are lots of things that :class:`ConfigLoader` does not do. It does
185 not implement complex logic for finding config files. It does not handle
185 not implement complex logic for finding config files. It does not handle
186 default values or merge multiple configs. These things need to be
186 default values or merge multiple configs. These things need to be
187 handled elsewhere.
187 handled elsewhere.
188 """
188 """
189
189
190 def __init__(self):
190 def __init__(self):
191 """A base class for config loaders.
191 """A base class for config loaders.
192
192
193 Examples
193 Examples
194 --------
194 --------
195
195
196 >>> cl = ConfigLoader()
196 >>> cl = ConfigLoader()
197 >>> config = cl.load_config()
197 >>> config = cl.load_config()
198 >>> config
198 >>> config
199 {}
199 {}
200 """
200 """
201 self.clear()
201 self.clear()
202
202
203 def clear(self):
203 def clear(self):
204 self.config = Config()
204 self.config = Config()
205
205
206 def load_config(self):
206 def load_config(self):
207 """Load a config from somewhere, return a :class:`Config` instance.
207 """Load a config from somewhere, return a :class:`Config` instance.
208
208
209 Usually, this will cause self.config to be set and then returned.
209 Usually, this will cause self.config to be set and then returned.
210 However, in most cases, :meth:`ConfigLoader.clear` should be called
210 However, in most cases, :meth:`ConfigLoader.clear` should be called
211 to erase any previous state.
211 to erase any previous state.
212 """
212 """
213 self.clear()
213 self.clear()
214 return self.config
214 return self.config
215
215
216
216
217 class FileConfigLoader(ConfigLoader):
217 class FileConfigLoader(ConfigLoader):
218 """A base class for file based configurations.
218 """A base class for file based configurations.
219
219
220 As we add more file based config loaders, the common logic should go
220 As we add more file based config loaders, the common logic should go
221 here.
221 here.
222 """
222 """
223 pass
223 pass
224
224
225
225
226 class PyFileConfigLoader(FileConfigLoader):
226 class PyFileConfigLoader(FileConfigLoader):
227 """A config loader for pure python files.
227 """A config loader for pure python files.
228
228
229 This calls execfile on a plain python file and looks for attributes
229 This calls execfile on a plain python file and looks for attributes
230 that are all caps. These attribute are added to the config Struct.
230 that are all caps. These attribute are added to the config Struct.
231 """
231 """
232
232
233 def __init__(self, filename, path=None):
233 def __init__(self, filename, path=None):
234 """Build a config loader for a filename and path.
234 """Build a config loader for a filename and path.
235
235
236 Parameters
236 Parameters
237 ----------
237 ----------
238 filename : str
238 filename : str
239 The file name of the config file.
239 The file name of the config file.
240 path : str, list, tuple
240 path : str, list, tuple
241 The path to search for the config file on, or a sequence of
241 The path to search for the config file on, or a sequence of
242 paths to try in order.
242 paths to try in order.
243 """
243 """
244 super(PyFileConfigLoader, self).__init__()
244 super(PyFileConfigLoader, self).__init__()
245 self.filename = filename
245 self.filename = filename
246 self.path = path
246 self.path = path
247 self.full_filename = ''
247 self.full_filename = ''
248 self.data = None
248 self.data = None
249
249
250 def load_config(self):
250 def load_config(self):
251 """Load the config from a file and return it as a Struct."""
251 """Load the config from a file and return it as a Struct."""
252 self.clear()
252 self.clear()
253 self._find_file()
253 self._find_file()
254 self._read_file_as_dict()
254 self._read_file_as_dict()
255 self._convert_to_config()
255 self._convert_to_config()
256 return self.config
256 return self.config
257
257
258 def _find_file(self):
258 def _find_file(self):
259 """Try to find the file by searching the paths."""
259 """Try to find the file by searching the paths."""
260 self.full_filename = filefind(self.filename, self.path)
260 self.full_filename = filefind(self.filename, self.path)
261
261
262 def _read_file_as_dict(self):
262 def _read_file_as_dict(self):
263 """Load the config file into self.config, with recursive loading."""
263 """Load the config file into self.config, with recursive loading."""
264 # This closure is made available in the namespace that is used
264 # This closure is made available in the namespace that is used
265 # to exec the config file. This allows users to call
265 # to exec the config file. This allows users to call
266 # load_subconfig('myconfig.py') to load config files recursively.
266 # load_subconfig('myconfig.py') to load config files recursively.
267 # It needs to be a closure because it has references to self.path
267 # It needs to be a closure because it has references to self.path
268 # and self.config. The sub-config is loaded with the same path
268 # and self.config. The sub-config is loaded with the same path
269 # as the parent, but it uses an empty config which is then merged
269 # as the parent, but it uses an empty config which is then merged
270 # with the parents.
270 # with the parents.
271 def load_subconfig(fname):
271 def load_subconfig(fname):
272 loader = PyFileConfigLoader(fname, self.path)
272 loader = PyFileConfigLoader(fname, self.path)
273 try:
273 try:
274 sub_config = loader.load_config()
274 sub_config = loader.load_config()
275 except IOError:
275 except IOError:
276 # Pass silently if the sub config is not there. This happens
276 # Pass silently if the sub config is not there. This happens
277 # when a user us using a profile, but not the default config.
277 # when a user us using a profile, but not the default config.
278 pass
278 pass
279 else:
279 else:
280 self.config._merge(sub_config)
280 self.config._merge(sub_config)
281
281
282 # Again, this needs to be a closure and should be used in config
282 # Again, this needs to be a closure and should be used in config
283 # files to get the config being loaded.
283 # files to get the config being loaded.
284 def get_config():
284 def get_config():
285 return self.config
285 return self.config
286
286
287 namespace = dict(load_subconfig=load_subconfig, get_config=get_config)
287 namespace = dict(load_subconfig=load_subconfig, get_config=get_config)
288 execfile(self.full_filename, namespace)
288 execfile(self.full_filename, namespace)
289
289
290 def _convert_to_config(self):
290 def _convert_to_config(self):
291 if self.data is None:
291 if self.data is None:
292 ConfigLoaderError('self.data does not exist')
292 ConfigLoaderError('self.data does not exist')
293
293
294
294
295 class CommandLineConfigLoader(ConfigLoader):
295 class CommandLineConfigLoader(ConfigLoader):
296 """A config loader for command line arguments.
296 """A config loader for command line arguments.
297
297
298 As we add more command line based loaders, the common logic should go
298 As we add more command line based loaders, the common logic should go
299 here.
299 here.
300 """
300 """
301
301
302
302
303 class ArgParseConfigLoader(CommandLineConfigLoader):
303 class ArgParseConfigLoader(CommandLineConfigLoader):
304
304
305 def __init__(self, argv=None, *parser_args, **parser_kw):
305 def __init__(self, argv=None, *parser_args, **parser_kw):
306 """Create a config loader for use with argparse.
306 """Create a config loader for use with argparse.
307
307
308 Parameters
308 Parameters
309 ----------
309 ----------
310
310
311 argv : optional, list
311 argv : optional, list
312 If given, used to read command-line arguments from, otherwise
312 If given, used to read command-line arguments from, otherwise
313 sys.argv[1:] is used.
313 sys.argv[1:] is used.
314
314
315 parser_args : tuple
315 parser_args : tuple
316 A tuple of positional arguments that will be passed to the
316 A tuple of positional arguments that will be passed to the
317 constructor of :class:`argparse.ArgumentParser`.
317 constructor of :class:`argparse.ArgumentParser`.
318
318
319 parser_kw : dict
319 parser_kw : dict
320 A tuple of keyword arguments that will be passed to the
320 A tuple of keyword arguments that will be passed to the
321 constructor of :class:`argparse.ArgumentParser`.
321 constructor of :class:`argparse.ArgumentParser`.
322 """
322 """
323 super(CommandLineConfigLoader, self).__init__()
323 super(CommandLineConfigLoader, self).__init__()
324 if argv == None:
324 if argv == None:
325 argv = sys.argv[1:]
325 argv = sys.argv[1:]
326 self.argv = argv
326 self.argv = argv
327 self.parser_args = parser_args
327 self.parser_args = parser_args
328 self.version = parser_kw.pop("version", None)
328 kwargs = dict(argument_default=argparse.SUPPRESS)
329 kwargs = dict(argument_default=argparse.SUPPRESS)
329 kwargs.update(parser_kw)
330 kwargs.update(parser_kw)
330 self.parser_kw = kwargs
331 self.parser_kw = kwargs
331
332
332 def load_config(self, args=None):
333 def load_config(self, args=None):
333 """Parse command line arguments and return as a Struct.
334 """Parse command line arguments and return as a Struct.
334
335
335 Parameters
336 Parameters
336 ----------
337 ----------
337
338
338 args : optional, list
339 args : optional, list
339 If given, a list with the structure of sys.argv[1:] to parse
340 If given, a list with the structure of sys.argv[1:] to parse
340 arguments from. If not given, the instance's self.argv attribute
341 arguments from. If not given, the instance's self.argv attribute
341 (given at construction time) is used."""
342 (given at construction time) is used."""
342 self.clear()
343 self.clear()
343 if args is None:
344 if args is None:
344 args = self.argv
345 args = self.argv
345 self._create_parser()
346 self._create_parser()
346 self._parse_args(args)
347 self._parse_args(args)
347 self._convert_to_config()
348 self._convert_to_config()
348 return self.config
349 return self.config
349
350
350 def get_extra_args(self):
351 def get_extra_args(self):
351 if hasattr(self, 'extra_args'):
352 if hasattr(self, 'extra_args'):
352 return self.extra_args
353 return self.extra_args
353 else:
354 else:
354 return []
355 return []
355
356
356 def _create_parser(self):
357 def _create_parser(self):
357 self.parser = ArgumentParser(*self.parser_args, **self.parser_kw)
358 self.parser = ArgumentParser(*self.parser_args, **self.parser_kw)
358 self._add_arguments()
359 self._add_arguments()
359
360
360 def _add_arguments(self):
361 def _add_arguments(self):
361 raise NotImplementedError("subclasses must implement _add_arguments")
362 raise NotImplementedError("subclasses must implement _add_arguments")
362
363
363 def _parse_args(self, args):
364 def _parse_args(self, args):
364 """self.parser->self.parsed_data"""
365 """self.parser->self.parsed_data"""
365 self.parsed_data, self.extra_args = self.parser.parse_known_args(args)
366 self.parsed_data, self.extra_args = self.parser.parse_known_args(args)
366
367
367 def _convert_to_config(self):
368 def _convert_to_config(self):
368 """self.parsed_data->self.config"""
369 """self.parsed_data->self.config"""
369 for k, v in vars(self.parsed_data).iteritems():
370 for k, v in vars(self.parsed_data).iteritems():
370 exec_str = 'self.config.' + k + '= v'
371 exec_str = 'self.config.' + k + '= v'
371 exec exec_str in locals(), globals()
372 exec exec_str in locals(), globals()
372
373
373
374
@@ -1,453 +1,459 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 configurable objects, passing the config to them.
9 object and then create the configurable objects, 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 class BaseAppConfigLoader(ArgParseConfigLoader):
51 class BaseAppConfigLoader(ArgParseConfigLoader):
52 """Default command line options for IPython based applications."""
52 """Default command line options for IPython based applications."""
53
53
54 def _add_ipython_dir(self, parser):
54 def _add_ipython_dir(self, parser):
55 """Add the --ipython-dir option to the parser."""
55 """Add the --ipython-dir option to the parser."""
56 paa = parser.add_argument
56 paa = parser.add_argument
57 paa('--ipython-dir',
57 paa('--ipython-dir',
58 dest='Global.ipython_dir',type=unicode,
58 dest='Global.ipython_dir',type=unicode,
59 help=
59 help=
60 """Set to override default location of the IPython directory
60 """Set to override default location of the IPython directory
61 IPYTHON_DIR, stored as Global.ipython_dir. This can also be
61 IPYTHON_DIR, stored as Global.ipython_dir. This can also be
62 specified through the environment variable IPYTHON_DIR.""",
62 specified through the environment variable IPYTHON_DIR.""",
63 metavar='Global.ipython_dir')
63 metavar='Global.ipython_dir')
64
64
65 def _add_log_level(self, parser):
65 def _add_log_level(self, parser):
66 """Add the --log-level option to the parser."""
66 """Add the --log-level option to the parser."""
67 paa = parser.add_argument
67 paa = parser.add_argument
68 paa('--log-level',
68 paa('--log-level',
69 dest="Global.log_level",type=int,
69 dest="Global.log_level",type=int,
70 help='Set the log level (0,10,20,30,40,50). Default is 30.',
70 help='Set the log level (0,10,20,30,40,50). Default is 30.',
71 metavar='Global.log_level')
71 metavar='Global.log_level')
72
72
73 def _add_version(self, parser):
74 """Add the --version option to the parser."""
75 parser.add_argument('--version', action="version",
76 version=self.version)
77
73 def _add_arguments(self):
78 def _add_arguments(self):
74 self._add_ipython_dir(self.parser)
79 self._add_ipython_dir(self.parser)
75 self._add_log_level(self.parser)
80 self._add_log_level(self.parser)
81 self._add_version(self.parser)
76
82
77
83
78 class Application(object):
84 class Application(object):
79 """Load a config, construct configurables and set them running.
85 """Load a config, construct configurables and set them running.
80
86
81 The configuration of an application can be done via three different Config
87 The configuration of an application can be done via three different Config
82 objects, which are loaded and ultimately merged into a single one used
88 objects, which are loaded and ultimately merged into a single one used
83 from that point on by the app. These are:
89 from that point on by the app. These are:
84
90
85 1. default_config: internal defaults, implemented in code.
91 1. default_config: internal defaults, implemented in code.
86 2. file_config: read from the filesystem.
92 2. file_config: read from the filesystem.
87 3. command_line_config: read from the system's command line flags.
93 3. command_line_config: read from the system's command line flags.
88
94
89 During initialization, 3 is actually read before 2, since at the
95 During initialization, 3 is actually read before 2, since at the
90 command-line one may override the location of the file to be read. But the
96 command-line one may override the location of the file to be read. But the
91 above is the order in which the merge is made.
97 above is the order in which the merge is made.
92 """
98 """
93
99
94 name = u'ipython'
100 name = u'ipython'
95 description = 'IPython: an enhanced interactive Python shell.'
101 description = 'IPython: an enhanced interactive Python shell.'
96 #: Usage message printed by argparse. If None, auto-generate
102 #: Usage message printed by argparse. If None, auto-generate
97 usage = None
103 usage = None
98 #: The command line config loader. Subclass of ArgParseConfigLoader.
104 #: The command line config loader. Subclass of ArgParseConfigLoader.
99 command_line_loader = BaseAppConfigLoader
105 command_line_loader = BaseAppConfigLoader
100 #: The name of the config file to load, determined at runtime
106 #: The name of the config file to load, determined at runtime
101 config_file_name = None
107 config_file_name = None
102 #: The name of the default config file. Track separately from the actual
108 #: The name of the default config file. Track separately from the actual
103 #: name because some logic happens only if we aren't using the default.
109 #: name because some logic happens only if we aren't using the default.
104 default_config_file_name = u'ipython_config.py'
110 default_config_file_name = u'ipython_config.py'
105 default_log_level = logging.WARN
111 default_log_level = logging.WARN
106 #: Set by --profile option
112 #: Set by --profile option
107 profile_name = None
113 profile_name = None
108 #: User's ipython directory, typically ~/.ipython or ~/.config/ipython/
114 #: User's ipython directory, typically ~/.ipython or ~/.config/ipython/
109 ipython_dir = None
115 ipython_dir = None
110 #: Internal defaults, implemented in code.
116 #: Internal defaults, implemented in code.
111 default_config = None
117 default_config = None
112 #: Read from the filesystem.
118 #: Read from the filesystem.
113 file_config = None
119 file_config = None
114 #: Read from the system's command line flags.
120 #: Read from the system's command line flags.
115 command_line_config = None
121 command_line_config = None
116 #: The final config that will be passed to the main object.
122 #: The final config that will be passed to the main object.
117 master_config = None
123 master_config = None
118 #: A reference to the argv to be used (typically ends up being sys.argv[1:])
124 #: A reference to the argv to be used (typically ends up being sys.argv[1:])
119 argv = None
125 argv = None
120 #: extra arguments computed by the command-line loader
126 #: extra arguments computed by the command-line loader
121 extra_args = None
127 extra_args = None
122 #: The class to use as the crash handler.
128 #: The class to use as the crash handler.
123 crash_handler_class = crashhandler.CrashHandler
129 crash_handler_class = crashhandler.CrashHandler
124
130
125 # Private attributes
131 # Private attributes
126 _exiting = False
132 _exiting = False
127 _initialized = False
133 _initialized = False
128
134
129 def __init__(self, argv=None):
135 def __init__(self, argv=None):
130 self.argv = sys.argv[1:] if argv is None else argv
136 self.argv = sys.argv[1:] if argv is None else argv
131 self.init_logger()
137 self.init_logger()
132
138
133 def init_logger(self):
139 def init_logger(self):
134 self.log = logging.getLogger(self.__class__.__name__)
140 self.log = logging.getLogger(self.__class__.__name__)
135 # This is used as the default until the command line arguments are read.
141 # This is used as the default until the command line arguments are read.
136 self.log.setLevel(self.default_log_level)
142 self.log.setLevel(self.default_log_level)
137 self._log_handler = logging.StreamHandler()
143 self._log_handler = logging.StreamHandler()
138 self._log_formatter = logging.Formatter("[%(name)s] %(message)s")
144 self._log_formatter = logging.Formatter("[%(name)s] %(message)s")
139 self._log_handler.setFormatter(self._log_formatter)
145 self._log_handler.setFormatter(self._log_formatter)
140 self.log.addHandler(self._log_handler)
146 self.log.addHandler(self._log_handler)
141
147
142 def _set_log_level(self, level):
148 def _set_log_level(self, level):
143 self.log.setLevel(level)
149 self.log.setLevel(level)
144
150
145 def _get_log_level(self):
151 def _get_log_level(self):
146 return self.log.level
152 return self.log.level
147
153
148 log_level = property(_get_log_level, _set_log_level)
154 log_level = property(_get_log_level, _set_log_level)
149
155
150 def initialize(self):
156 def initialize(self):
151 """Initialize the application.
157 """Initialize the application.
152
158
153 Loads all configuration information and sets all application state, but
159 Loads all configuration information and sets all application state, but
154 does not start any relevant processing (typically some kind of event
160 does not start any relevant processing (typically some kind of event
155 loop).
161 loop).
156
162
157 Once this method has been called, the application is flagged as
163 Once this method has been called, the application is flagged as
158 initialized and the method becomes a no-op."""
164 initialized and the method becomes a no-op."""
159
165
160 if self._initialized:
166 if self._initialized:
161 return
167 return
162
168
163 # The first part is protected with an 'attempt' wrapper, that will log
169 # The first part is protected with an 'attempt' wrapper, that will log
164 # failures with the basic system traceback machinery. Once our crash
170 # failures with the basic system traceback machinery. Once our crash
165 # handler is in place, we can let any subsequent exception propagate,
171 # handler is in place, we can let any subsequent exception propagate,
166 # as our handler will log it with much better detail than the default.
172 # as our handler will log it with much better detail than the default.
167 self.attempt(self.create_crash_handler)
173 self.attempt(self.create_crash_handler)
168
174
169 # Configuration phase
175 # Configuration phase
170 # Default config (internally hardwired in application code)
176 # Default config (internally hardwired in application code)
171 self.create_default_config()
177 self.create_default_config()
172 self.log_default_config()
178 self.log_default_config()
173 self.set_default_config_log_level()
179 self.set_default_config_log_level()
174
180
175 # Command-line config
181 # Command-line config
176 self.pre_load_command_line_config()
182 self.pre_load_command_line_config()
177 self.load_command_line_config()
183 self.load_command_line_config()
178 self.set_command_line_config_log_level()
184 self.set_command_line_config_log_level()
179 self.post_load_command_line_config()
185 self.post_load_command_line_config()
180 self.log_command_line_config()
186 self.log_command_line_config()
181
187
182 # Find resources needed for filesystem access, using information from
188 # Find resources needed for filesystem access, using information from
183 # the above two
189 # the above two
184 self.find_ipython_dir()
190 self.find_ipython_dir()
185 self.find_resources()
191 self.find_resources()
186 self.find_config_file_name()
192 self.find_config_file_name()
187 self.find_config_file_paths()
193 self.find_config_file_paths()
188
194
189 # File-based config
195 # File-based config
190 self.pre_load_file_config()
196 self.pre_load_file_config()
191 self.load_file_config()
197 self.load_file_config()
192 self.set_file_config_log_level()
198 self.set_file_config_log_level()
193 self.post_load_file_config()
199 self.post_load_file_config()
194 self.log_file_config()
200 self.log_file_config()
195
201
196 # Merge all config objects into a single one the app can then use
202 # Merge all config objects into a single one the app can then use
197 self.merge_configs()
203 self.merge_configs()
198 self.log_master_config()
204 self.log_master_config()
199
205
200 # Construction phase
206 # Construction phase
201 self.pre_construct()
207 self.pre_construct()
202 self.construct()
208 self.construct()
203 self.post_construct()
209 self.post_construct()
204
210
205 # Done, flag as such and
211 # Done, flag as such and
206 self._initialized = True
212 self._initialized = True
207
213
208 def start(self):
214 def start(self):
209 """Start the application."""
215 """Start the application."""
210 self.initialize()
216 self.initialize()
211 self.start_app()
217 self.start_app()
212
218
213 #-------------------------------------------------------------------------
219 #-------------------------------------------------------------------------
214 # Various stages of Application creation
220 # Various stages of Application creation
215 #-------------------------------------------------------------------------
221 #-------------------------------------------------------------------------
216
222
217 def create_crash_handler(self):
223 def create_crash_handler(self):
218 """Create a crash handler, typically setting sys.excepthook to it."""
224 """Create a crash handler, typically setting sys.excepthook to it."""
219 self.crash_handler = self.crash_handler_class(self)
225 self.crash_handler = self.crash_handler_class(self)
220 sys.excepthook = self.crash_handler
226 sys.excepthook = self.crash_handler
221
227
222 def create_default_config(self):
228 def create_default_config(self):
223 """Create defaults that can't be set elsewhere.
229 """Create defaults that can't be set elsewhere.
224
230
225 For the most part, we try to set default in the class attributes
231 For the most part, we try to set default in the class attributes
226 of Configurables. But, defaults the top-level Application (which is
232 of Configurables. But, defaults the top-level Application (which is
227 not a HasTraits or Configurables) are not set in this way. Instead
233 not a HasTraits or Configurables) are not set in this way. Instead
228 we set them here. The Global section is for variables like this that
234 we set them here. The Global section is for variables like this that
229 don't belong to a particular configurable.
235 don't belong to a particular configurable.
230 """
236 """
231 c = Config()
237 c = Config()
232 c.Global.ipython_dir = get_ipython_dir()
238 c.Global.ipython_dir = get_ipython_dir()
233 c.Global.log_level = self.log_level
239 c.Global.log_level = self.log_level
234 self.default_config = c
240 self.default_config = c
235
241
236 def log_default_config(self):
242 def log_default_config(self):
237 self.log.debug('Default config loaded:')
243 self.log.debug('Default config loaded:')
238 self.log.debug(repr(self.default_config))
244 self.log.debug(repr(self.default_config))
239
245
240 def set_default_config_log_level(self):
246 def set_default_config_log_level(self):
241 try:
247 try:
242 self.log_level = self.default_config.Global.log_level
248 self.log_level = self.default_config.Global.log_level
243 except AttributeError:
249 except AttributeError:
244 # Fallback to the default_log_level class attribute
250 # Fallback to the default_log_level class attribute
245 pass
251 pass
246
252
247 def create_command_line_config(self):
253 def create_command_line_config(self):
248 """Create and return a command line config loader."""
254 """Create and return a command line config loader."""
249 return self.command_line_loader(
255 return self.command_line_loader(
250 self.argv,
256 self.argv,
251 description=self.description,
257 description=self.description,
252 version=release.version,
258 version=release.version,
253 usage=self.usage
259 usage=self.usage
254 )
260 )
255
261
256 def pre_load_command_line_config(self):
262 def pre_load_command_line_config(self):
257 """Do actions just before loading the command line config."""
263 """Do actions just before loading the command line config."""
258 pass
264 pass
259
265
260 def load_command_line_config(self):
266 def load_command_line_config(self):
261 """Load the command line config."""
267 """Load the command line config."""
262 loader = self.create_command_line_config()
268 loader = self.create_command_line_config()
263 self.command_line_config = loader.load_config()
269 self.command_line_config = loader.load_config()
264 self.extra_args = loader.get_extra_args()
270 self.extra_args = loader.get_extra_args()
265
271
266 def set_command_line_config_log_level(self):
272 def set_command_line_config_log_level(self):
267 try:
273 try:
268 self.log_level = self.command_line_config.Global.log_level
274 self.log_level = self.command_line_config.Global.log_level
269 except AttributeError:
275 except AttributeError:
270 pass
276 pass
271
277
272 def post_load_command_line_config(self):
278 def post_load_command_line_config(self):
273 """Do actions just after loading the command line config."""
279 """Do actions just after loading the command line config."""
274 pass
280 pass
275
281
276 def log_command_line_config(self):
282 def log_command_line_config(self):
277 self.log.debug("Command line config loaded:")
283 self.log.debug("Command line config loaded:")
278 self.log.debug(repr(self.command_line_config))
284 self.log.debug(repr(self.command_line_config))
279
285
280 def find_ipython_dir(self):
286 def find_ipython_dir(self):
281 """Set the IPython directory.
287 """Set the IPython directory.
282
288
283 This sets ``self.ipython_dir``, but the actual value that is passed to
289 This sets ``self.ipython_dir``, but the actual value that is passed to
284 the application is kept in either ``self.default_config`` or
290 the application is kept in either ``self.default_config`` or
285 ``self.command_line_config``. This also adds ``self.ipython_dir`` to
291 ``self.command_line_config``. This also adds ``self.ipython_dir`` to
286 ``sys.path`` so config files there can be referenced by other config
292 ``sys.path`` so config files there can be referenced by other config
287 files.
293 files.
288 """
294 """
289
295
290 try:
296 try:
291 self.ipython_dir = self.command_line_config.Global.ipython_dir
297 self.ipython_dir = self.command_line_config.Global.ipython_dir
292 except AttributeError:
298 except AttributeError:
293 self.ipython_dir = self.default_config.Global.ipython_dir
299 self.ipython_dir = self.default_config.Global.ipython_dir
294 sys.path.append(os.path.abspath(self.ipython_dir))
300 sys.path.append(os.path.abspath(self.ipython_dir))
295 if not os.path.isdir(self.ipython_dir):
301 if not os.path.isdir(self.ipython_dir):
296 os.makedirs(self.ipython_dir, mode=0777)
302 os.makedirs(self.ipython_dir, mode=0777)
297 self.log.debug("IPYTHON_DIR set to: %s" % self.ipython_dir)
303 self.log.debug("IPYTHON_DIR set to: %s" % self.ipython_dir)
298
304
299 def find_resources(self):
305 def find_resources(self):
300 """Find other resources that need to be in place.
306 """Find other resources that need to be in place.
301
307
302 Things like cluster directories need to be in place to find the
308 Things like cluster directories need to be in place to find the
303 config file. These happen right after the IPython directory has
309 config file. These happen right after the IPython directory has
304 been set.
310 been set.
305 """
311 """
306 pass
312 pass
307
313
308 def find_config_file_name(self):
314 def find_config_file_name(self):
309 """Find the config file name for this application.
315 """Find the config file name for this application.
310
316
311 This must set ``self.config_file_name`` to the filename of the
317 This must set ``self.config_file_name`` to the filename of the
312 config file to use (just the filename). The search paths for the
318 config file to use (just the filename). The search paths for the
313 config file are set in :meth:`find_config_file_paths` and then passed
319 config file are set in :meth:`find_config_file_paths` and then passed
314 to the config file loader where they are resolved to an absolute path.
320 to the config file loader where they are resolved to an absolute path.
315
321
316 If a profile has been set at the command line, this will resolve it.
322 If a profile has been set at the command line, this will resolve it.
317 """
323 """
318 try:
324 try:
319 self.config_file_name = self.command_line_config.Global.config_file
325 self.config_file_name = self.command_line_config.Global.config_file
320 except AttributeError:
326 except AttributeError:
321 pass
327 pass
322 else:
328 else:
323 return
329 return
324
330
325 try:
331 try:
326 self.profile_name = self.command_line_config.Global.profile
332 self.profile_name = self.command_line_config.Global.profile
327 except AttributeError:
333 except AttributeError:
328 # Just use the default as there is no profile
334 # Just use the default as there is no profile
329 self.config_file_name = self.default_config_file_name
335 self.config_file_name = self.default_config_file_name
330 else:
336 else:
331 # Use the default config file name and profile name if set
337 # Use the default config file name and profile name if set
332 # to determine the used config file name.
338 # to determine the used config file name.
333 name_parts = self.default_config_file_name.split('.')
339 name_parts = self.default_config_file_name.split('.')
334 name_parts.insert(1, u'_' + self.profile_name + u'.')
340 name_parts.insert(1, u'_' + self.profile_name + u'.')
335 self.config_file_name = ''.join(name_parts)
341 self.config_file_name = ''.join(name_parts)
336
342
337 def find_config_file_paths(self):
343 def find_config_file_paths(self):
338 """Set the search paths for resolving the config file.
344 """Set the search paths for resolving the config file.
339
345
340 This must set ``self.config_file_paths`` to a sequence of search
346 This must set ``self.config_file_paths`` to a sequence of search
341 paths to pass to the config file loader.
347 paths to pass to the config file loader.
342 """
348 """
343 # Include our own profiles directory last, so that users can still find
349 # Include our own profiles directory last, so that users can still find
344 # our shipped copies of builtin profiles even if they don't have them
350 # our shipped copies of builtin profiles even if they don't have them
345 # in their local ipython directory.
351 # in their local ipython directory.
346 prof_dir = os.path.join(get_ipython_package_dir(), 'config', 'profile')
352 prof_dir = os.path.join(get_ipython_package_dir(), 'config', 'profile')
347 self.config_file_paths = (os.getcwd(), self.ipython_dir, prof_dir)
353 self.config_file_paths = (os.getcwd(), self.ipython_dir, prof_dir)
348
354
349 def pre_load_file_config(self):
355 def pre_load_file_config(self):
350 """Do actions before the config file is loaded."""
356 """Do actions before the config file is loaded."""
351 pass
357 pass
352
358
353 def load_file_config(self):
359 def load_file_config(self):
354 """Load the config file.
360 """Load the config file.
355
361
356 This tries to load the config file from disk. If successful, the
362 This tries to load the config file from disk. If successful, the
357 ``CONFIG_FILE`` config variable is set to the resolved config file
363 ``CONFIG_FILE`` config variable is set to the resolved config file
358 location. If not successful, an empty config is used.
364 location. If not successful, an empty config is used.
359 """
365 """
360 self.log.debug("Attempting to load config file: %s" %
366 self.log.debug("Attempting to load config file: %s" %
361 self.config_file_name)
367 self.config_file_name)
362 loader = PyFileConfigLoader(self.config_file_name,
368 loader = PyFileConfigLoader(self.config_file_name,
363 path=self.config_file_paths)
369 path=self.config_file_paths)
364 try:
370 try:
365 self.file_config = loader.load_config()
371 self.file_config = loader.load_config()
366 self.file_config.Global.config_file = loader.full_filename
372 self.file_config.Global.config_file = loader.full_filename
367 except IOError:
373 except IOError:
368 # Only warn if the default config file was NOT being used.
374 # Only warn if the default config file was NOT being used.
369 if not self.config_file_name==self.default_config_file_name:
375 if not self.config_file_name==self.default_config_file_name:
370 self.log.warn("Config file not found, skipping: %s" %
376 self.log.warn("Config file not found, skipping: %s" %
371 self.config_file_name, exc_info=True)
377 self.config_file_name, exc_info=True)
372 self.file_config = Config()
378 self.file_config = Config()
373 except:
379 except:
374 self.log.warn("Error loading config file: %s" %
380 self.log.warn("Error loading config file: %s" %
375 self.config_file_name, exc_info=True)
381 self.config_file_name, exc_info=True)
376 self.file_config = Config()
382 self.file_config = Config()
377
383
378 def set_file_config_log_level(self):
384 def set_file_config_log_level(self):
379 # We need to keeep self.log_level updated. But we only use the value
385 # We need to keeep self.log_level updated. But we only use the value
380 # of the file_config if a value was not specified at the command
386 # of the file_config if a value was not specified at the command
381 # line, because the command line overrides everything.
387 # line, because the command line overrides everything.
382 if not hasattr(self.command_line_config.Global, 'log_level'):
388 if not hasattr(self.command_line_config.Global, 'log_level'):
383 try:
389 try:
384 self.log_level = self.file_config.Global.log_level
390 self.log_level = self.file_config.Global.log_level
385 except AttributeError:
391 except AttributeError:
386 pass # Use existing value
392 pass # Use existing value
387
393
388 def post_load_file_config(self):
394 def post_load_file_config(self):
389 """Do actions after the config file is loaded."""
395 """Do actions after the config file is loaded."""
390 pass
396 pass
391
397
392 def log_file_config(self):
398 def log_file_config(self):
393 if hasattr(self.file_config.Global, 'config_file'):
399 if hasattr(self.file_config.Global, 'config_file'):
394 self.log.debug("Config file loaded: %s" %
400 self.log.debug("Config file loaded: %s" %
395 self.file_config.Global.config_file)
401 self.file_config.Global.config_file)
396 self.log.debug(repr(self.file_config))
402 self.log.debug(repr(self.file_config))
397
403
398 def merge_configs(self):
404 def merge_configs(self):
399 """Merge the default, command line and file config objects."""
405 """Merge the default, command line and file config objects."""
400 config = Config()
406 config = Config()
401 config._merge(self.default_config)
407 config._merge(self.default_config)
402 config._merge(self.file_config)
408 config._merge(self.file_config)
403 config._merge(self.command_line_config)
409 config._merge(self.command_line_config)
404
410
405 # XXX fperez - propose to Brian we rename master_config to simply
411 # XXX fperez - propose to Brian we rename master_config to simply
406 # config, I think this is going to be heavily used in examples and
412 # config, I think this is going to be heavily used in examples and
407 # application code and the name is shorter/easier to find/remember.
413 # application code and the name is shorter/easier to find/remember.
408 # For now, just alias it...
414 # For now, just alias it...
409 self.master_config = config
415 self.master_config = config
410 self.config = config
416 self.config = config
411
417
412 def log_master_config(self):
418 def log_master_config(self):
413 self.log.debug("Master config created:")
419 self.log.debug("Master config created:")
414 self.log.debug(repr(self.master_config))
420 self.log.debug(repr(self.master_config))
415
421
416 def pre_construct(self):
422 def pre_construct(self):
417 """Do actions after the config has been built, but before construct."""
423 """Do actions after the config has been built, but before construct."""
418 pass
424 pass
419
425
420 def construct(self):
426 def construct(self):
421 """Construct the main objects that make up this app."""
427 """Construct the main objects that make up this app."""
422 self.log.debug("Constructing main objects for application")
428 self.log.debug("Constructing main objects for application")
423
429
424 def post_construct(self):
430 def post_construct(self):
425 """Do actions after construct, but before starting the app."""
431 """Do actions after construct, but before starting the app."""
426 pass
432 pass
427
433
428 def start_app(self):
434 def start_app(self):
429 """Actually start the app."""
435 """Actually start the app."""
430 self.log.debug("Starting application")
436 self.log.debug("Starting application")
431
437
432 #-------------------------------------------------------------------------
438 #-------------------------------------------------------------------------
433 # Utility methods
439 # Utility methods
434 #-------------------------------------------------------------------------
440 #-------------------------------------------------------------------------
435
441
436 def exit(self, exit_status=0):
442 def exit(self, exit_status=0):
437 if self._exiting:
443 if self._exiting:
438 pass
444 pass
439 else:
445 else:
440 self.log.debug("Exiting application: %s" % self.name)
446 self.log.debug("Exiting application: %s" % self.name)
441 self._exiting = True
447 self._exiting = True
442 sys.exit(exit_status)
448 sys.exit(exit_status)
443
449
444 def attempt(self, func):
450 def attempt(self, func):
445 try:
451 try:
446 func()
452 func()
447 except SystemExit:
453 except SystemExit:
448 raise
454 raise
449 except:
455 except:
450 self.log.critical("Aborting application: %s" % self.name,
456 self.log.critical("Aborting application: %s" % self.name,
451 exc_info=True)
457 exc_info=True)
452 self.exit(0)
458 self.exit(0)
453
459
General Comments 0
You need to be logged in to leave comments. Login now