##// END OF EJS Templates
catch ConfigFileNotFound where appropriate...
MinRK -
Show More
@@ -1,317 +1,317 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 * Min RK
15 * Min RK
16
16
17 """
17 """
18
18
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20 # Copyright (C) 2008-2011 The IPython Development Team
20 # Copyright (C) 2008-2011 The IPython Development Team
21 #
21 #
22 # Distributed under the terms of the BSD License. The full license is in
22 # Distributed under the terms of the BSD License. The full license is in
23 # the file COPYING, distributed as part of this software.
23 # the file COPYING, distributed as part of this software.
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25
25
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 # Imports
27 # Imports
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29
29
30 import glob
30 import glob
31 import logging
31 import logging
32 import os
32 import os
33 import shutil
33 import shutil
34 import sys
34 import sys
35
35
36 from IPython.config.application import Application
36 from IPython.config.application import Application
37 from IPython.config.configurable import Configurable
37 from IPython.config.configurable import Configurable
38 from IPython.config.loader import Config
38 from IPython.config.loader import Config, ConfigFileNotFound
39 from IPython.core import release, crashhandler
39 from IPython.core import release, crashhandler
40 from IPython.core.profiledir import ProfileDir, ProfileDirError
40 from IPython.core.profiledir import ProfileDir, ProfileDirError
41 from IPython.utils.path import get_ipython_dir, get_ipython_package_dir
41 from IPython.utils.path import get_ipython_dir, get_ipython_package_dir
42 from IPython.utils.traitlets import List, Unicode, Type, Bool, Dict
42 from IPython.utils.traitlets import List, Unicode, Type, Bool, Dict
43 from IPython.utils import py3compat
43 from IPython.utils import py3compat
44
44
45 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
46 # Classes and functions
46 # Classes and functions
47 #-----------------------------------------------------------------------------
47 #-----------------------------------------------------------------------------
48
48
49
49
50 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
51 # Base Application Class
51 # Base Application Class
52 #-----------------------------------------------------------------------------
52 #-----------------------------------------------------------------------------
53
53
54 # aliases and flags
54 # aliases and flags
55
55
56 base_aliases = {
56 base_aliases = {
57 'profile' : 'BaseIPythonApplication.profile',
57 'profile' : 'BaseIPythonApplication.profile',
58 'ipython-dir' : 'BaseIPythonApplication.ipython_dir',
58 'ipython-dir' : 'BaseIPythonApplication.ipython_dir',
59 'log-level' : 'Application.log_level',
59 'log-level' : 'Application.log_level',
60 }
60 }
61
61
62 base_flags = dict(
62 base_flags = dict(
63 debug = ({'Application' : {'log_level' : logging.DEBUG}},
63 debug = ({'Application' : {'log_level' : logging.DEBUG}},
64 "set log level to logging.DEBUG (maximize logging output)"),
64 "set log level to logging.DEBUG (maximize logging output)"),
65 quiet = ({'Application' : {'log_level' : logging.CRITICAL}},
65 quiet = ({'Application' : {'log_level' : logging.CRITICAL}},
66 "set log level to logging.CRITICAL (minimize logging output)"),
66 "set log level to logging.CRITICAL (minimize logging output)"),
67 init = ({'BaseIPythonApplication' : {
67 init = ({'BaseIPythonApplication' : {
68 'copy_config_files' : True,
68 'copy_config_files' : True,
69 'auto_create' : True}
69 'auto_create' : True}
70 }, """Initialize profile with default config files. This is equivalent
70 }, """Initialize profile with default config files. This is equivalent
71 to running `ipython profile create <profile>` prior to startup.
71 to running `ipython profile create <profile>` prior to startup.
72 """)
72 """)
73 )
73 )
74
74
75
75
76 class BaseIPythonApplication(Application):
76 class BaseIPythonApplication(Application):
77
77
78 name = Unicode(u'ipython')
78 name = Unicode(u'ipython')
79 description = Unicode(u'IPython: an enhanced interactive Python shell.')
79 description = Unicode(u'IPython: an enhanced interactive Python shell.')
80 version = Unicode(release.version)
80 version = Unicode(release.version)
81
81
82 aliases = Dict(base_aliases)
82 aliases = Dict(base_aliases)
83 flags = Dict(base_flags)
83 flags = Dict(base_flags)
84 classes = List([ProfileDir])
84 classes = List([ProfileDir])
85
85
86 # Track whether the config_file has changed,
86 # Track whether the config_file has changed,
87 # because some logic happens only if we aren't using the default.
87 # because some logic happens only if we aren't using the default.
88 config_file_specified = Bool(False)
88 config_file_specified = Bool(False)
89
89
90 config_file_name = Unicode(u'ipython_config.py')
90 config_file_name = Unicode(u'ipython_config.py')
91 def _config_file_name_default(self):
91 def _config_file_name_default(self):
92 return self.name.replace('-','_') + u'_config.py'
92 return self.name.replace('-','_') + u'_config.py'
93 def _config_file_name_changed(self, name, old, new):
93 def _config_file_name_changed(self, name, old, new):
94 if new != old:
94 if new != old:
95 self.config_file_specified = True
95 self.config_file_specified = True
96
96
97 # The directory that contains IPython's builtin profiles.
97 # The directory that contains IPython's builtin profiles.
98 builtin_profile_dir = Unicode(
98 builtin_profile_dir = Unicode(
99 os.path.join(get_ipython_package_dir(), u'config', u'profile', u'default')
99 os.path.join(get_ipython_package_dir(), u'config', u'profile', u'default')
100 )
100 )
101
101
102 config_file_paths = List(Unicode)
102 config_file_paths = List(Unicode)
103 def _config_file_paths_default(self):
103 def _config_file_paths_default(self):
104 return [os.getcwdu()]
104 return [os.getcwdu()]
105
105
106 profile = Unicode(u'', config=True,
106 profile = Unicode(u'', config=True,
107 help="""The IPython profile to use."""
107 help="""The IPython profile to use."""
108 )
108 )
109 def _profile_default(self):
109 def _profile_default(self):
110 return "python3" if py3compat.PY3 else "default"
110 return "python3" if py3compat.PY3 else "default"
111
111
112 def _profile_changed(self, name, old, new):
112 def _profile_changed(self, name, old, new):
113 self.builtin_profile_dir = os.path.join(
113 self.builtin_profile_dir = os.path.join(
114 get_ipython_package_dir(), u'config', u'profile', new
114 get_ipython_package_dir(), u'config', u'profile', new
115 )
115 )
116
116
117 ipython_dir = Unicode(get_ipython_dir(), config=True,
117 ipython_dir = Unicode(get_ipython_dir(), config=True,
118 help="""
118 help="""
119 The name of the IPython directory. This directory is used for logging
119 The name of the IPython directory. This directory is used for logging
120 configuration (through profiles), history storage, etc. The default
120 configuration (through profiles), history storage, etc. The default
121 is usually $HOME/.ipython. This options can also be specified through
121 is usually $HOME/.ipython. This options can also be specified through
122 the environment variable IPYTHON_DIR.
122 the environment variable IPYTHON_DIR.
123 """
123 """
124 )
124 )
125
125
126 overwrite = Bool(False, config=True,
126 overwrite = Bool(False, config=True,
127 help="""Whether to overwrite existing config files when copying""")
127 help="""Whether to overwrite existing config files when copying""")
128 auto_create = Bool(False, config=True,
128 auto_create = Bool(False, config=True,
129 help="""Whether to create profile dir if it doesn't exist""")
129 help="""Whether to create profile dir if it doesn't exist""")
130
130
131 config_files = List(Unicode)
131 config_files = List(Unicode)
132 def _config_files_default(self):
132 def _config_files_default(self):
133 return [u'ipython_config.py']
133 return [u'ipython_config.py']
134
134
135 copy_config_files = Bool(False, config=True,
135 copy_config_files = Bool(False, config=True,
136 help="""Whether to install the default config files into the profile dir.
136 help="""Whether to install the default config files into the profile dir.
137 If a new profile is being created, and IPython contains config files for that
137 If a new profile is being created, and IPython contains config files for that
138 profile, then they will be staged into the new directory. Otherwise,
138 profile, then they will be staged into the new directory. Otherwise,
139 default config files will be automatically generated.
139 default config files will be automatically generated.
140 """)
140 """)
141
141
142 # The class to use as the crash handler.
142 # The class to use as the crash handler.
143 crash_handler_class = Type(crashhandler.CrashHandler)
143 crash_handler_class = Type(crashhandler.CrashHandler)
144
144
145 def __init__(self, **kwargs):
145 def __init__(self, **kwargs):
146 super(BaseIPythonApplication, self).__init__(**kwargs)
146 super(BaseIPythonApplication, self).__init__(**kwargs)
147 # ensure even default IPYTHON_DIR exists
147 # ensure even default IPYTHON_DIR exists
148 if not os.path.exists(self.ipython_dir):
148 if not os.path.exists(self.ipython_dir):
149 self._ipython_dir_changed('ipython_dir', self.ipython_dir, self.ipython_dir)
149 self._ipython_dir_changed('ipython_dir', self.ipython_dir, self.ipython_dir)
150
150
151 #-------------------------------------------------------------------------
151 #-------------------------------------------------------------------------
152 # Various stages of Application creation
152 # Various stages of Application creation
153 #-------------------------------------------------------------------------
153 #-------------------------------------------------------------------------
154
154
155 def init_crash_handler(self):
155 def init_crash_handler(self):
156 """Create a crash handler, typically setting sys.excepthook to it."""
156 """Create a crash handler, typically setting sys.excepthook to it."""
157 self.crash_handler = self.crash_handler_class(self)
157 self.crash_handler = self.crash_handler_class(self)
158 sys.excepthook = self.crash_handler
158 sys.excepthook = self.crash_handler
159
159
160 def _ipython_dir_changed(self, name, old, new):
160 def _ipython_dir_changed(self, name, old, new):
161 if old in sys.path:
161 if old in sys.path:
162 sys.path.remove(old)
162 sys.path.remove(old)
163 sys.path.append(os.path.abspath(new))
163 sys.path.append(os.path.abspath(new))
164 if not os.path.isdir(new):
164 if not os.path.isdir(new):
165 os.makedirs(new, mode=0777)
165 os.makedirs(new, mode=0777)
166 readme = os.path.join(new, 'README')
166 readme = os.path.join(new, 'README')
167 if not os.path.exists(readme):
167 if not os.path.exists(readme):
168 path = os.path.join(get_ipython_package_dir(), u'config', u'profile')
168 path = os.path.join(get_ipython_package_dir(), u'config', u'profile')
169 shutil.copy(os.path.join(path, 'README'), readme)
169 shutil.copy(os.path.join(path, 'README'), readme)
170 self.log.debug("IPYTHON_DIR set to: %s" % new)
170 self.log.debug("IPYTHON_DIR set to: %s" % new)
171
171
172 def load_config_file(self, suppress_errors=True):
172 def load_config_file(self, suppress_errors=True):
173 """Load the config file.
173 """Load the config file.
174
174
175 By default, errors in loading config are handled, and a warning
175 By default, errors in loading config are handled, and a warning
176 printed on screen. For testing, the suppress_errors option is set
176 printed on screen. For testing, the suppress_errors option is set
177 to False, so errors will make tests fail.
177 to False, so errors will make tests fail.
178 """
178 """
179 self.log.debug("Searching path %s for config files", self.config_file_paths)
179 self.log.debug("Searching path %s for config files", self.config_file_paths)
180 base_config = 'ipython_config.py'
180 base_config = 'ipython_config.py'
181 self.log.debug("Attempting to load config file: %s" %
181 self.log.debug("Attempting to load config file: %s" %
182 base_config)
182 base_config)
183 try:
183 try:
184 Application.load_config_file(
184 Application.load_config_file(
185 self,
185 self,
186 base_config,
186 base_config,
187 path=self.config_file_paths
187 path=self.config_file_paths
188 )
188 )
189 except IOError:
189 except ConfigFileNotFound:
190 # ignore errors loading parent
190 # ignore errors loading parent
191 self.log.debug("Config file %s not found", base_config)
191 self.log.debug("Config file %s not found", base_config)
192 pass
192 pass
193 if self.config_file_name == base_config:
193 if self.config_file_name == base_config:
194 # don't load secondary config
194 # don't load secondary config
195 return
195 return
196 self.log.debug("Attempting to load config file: %s" %
196 self.log.debug("Attempting to load config file: %s" %
197 self.config_file_name)
197 self.config_file_name)
198 try:
198 try:
199 Application.load_config_file(
199 Application.load_config_file(
200 self,
200 self,
201 self.config_file_name,
201 self.config_file_name,
202 path=self.config_file_paths
202 path=self.config_file_paths
203 )
203 )
204 except IOError:
204 except ConfigFileNotFound:
205 # Only warn if the default config file was NOT being used.
205 # Only warn if the default config file was NOT being used.
206 if self.config_file_specified:
206 if self.config_file_specified:
207 msg = self.log.warn
207 msg = self.log.warn
208 else:
208 else:
209 msg = self.log.debug
209 msg = self.log.debug
210 msg("Config file not found, skipping: %s", self.config_file_name)
210 msg("Config file not found, skipping: %s", self.config_file_name)
211 except:
211 except:
212 # For testing purposes.
212 # For testing purposes.
213 if not suppress_errors:
213 if not suppress_errors:
214 raise
214 raise
215 self.log.warn("Error loading config file: %s" %
215 self.log.warn("Error loading config file: %s" %
216 self.config_file_name, exc_info=True)
216 self.config_file_name, exc_info=True)
217
217
218 def init_profile_dir(self):
218 def init_profile_dir(self):
219 """initialize the profile dir"""
219 """initialize the profile dir"""
220 try:
220 try:
221 # location explicitly specified:
221 # location explicitly specified:
222 location = self.config.ProfileDir.location
222 location = self.config.ProfileDir.location
223 except AttributeError:
223 except AttributeError:
224 # location not specified, find by profile name
224 # location not specified, find by profile name
225 try:
225 try:
226 p = ProfileDir.find_profile_dir_by_name(self.ipython_dir, self.profile, self.config)
226 p = ProfileDir.find_profile_dir_by_name(self.ipython_dir, self.profile, self.config)
227 except ProfileDirError:
227 except ProfileDirError:
228 # not found, maybe create it (always create default profile)
228 # not found, maybe create it (always create default profile)
229 if self.auto_create or self.profile==self._profile_default():
229 if self.auto_create or self.profile==self._profile_default():
230 try:
230 try:
231 p = ProfileDir.create_profile_dir_by_name(self.ipython_dir, self.profile, self.config)
231 p = ProfileDir.create_profile_dir_by_name(self.ipython_dir, self.profile, self.config)
232 except ProfileDirError:
232 except ProfileDirError:
233 self.log.fatal("Could not create profile: %r"%self.profile)
233 self.log.fatal("Could not create profile: %r"%self.profile)
234 self.exit(1)
234 self.exit(1)
235 else:
235 else:
236 self.log.info("Created profile dir: %r"%p.location)
236 self.log.info("Created profile dir: %r"%p.location)
237 else:
237 else:
238 self.log.fatal("Profile %r not found."%self.profile)
238 self.log.fatal("Profile %r not found."%self.profile)
239 self.exit(1)
239 self.exit(1)
240 else:
240 else:
241 self.log.info("Using existing profile dir: %r"%p.location)
241 self.log.info("Using existing profile dir: %r"%p.location)
242 else:
242 else:
243 # location is fully specified
243 # location is fully specified
244 try:
244 try:
245 p = ProfileDir.find_profile_dir(location, self.config)
245 p = ProfileDir.find_profile_dir(location, self.config)
246 except ProfileDirError:
246 except ProfileDirError:
247 # not found, maybe create it
247 # not found, maybe create it
248 if self.auto_create:
248 if self.auto_create:
249 try:
249 try:
250 p = ProfileDir.create_profile_dir(location, self.config)
250 p = ProfileDir.create_profile_dir(location, self.config)
251 except ProfileDirError:
251 except ProfileDirError:
252 self.log.fatal("Could not create profile directory: %r"%location)
252 self.log.fatal("Could not create profile directory: %r"%location)
253 self.exit(1)
253 self.exit(1)
254 else:
254 else:
255 self.log.info("Creating new profile dir: %r"%location)
255 self.log.info("Creating new profile dir: %r"%location)
256 else:
256 else:
257 self.log.fatal("Profile directory %r not found."%location)
257 self.log.fatal("Profile directory %r not found."%location)
258 self.exit(1)
258 self.exit(1)
259 else:
259 else:
260 self.log.info("Using existing profile dir: %r"%location)
260 self.log.info("Using existing profile dir: %r"%location)
261
261
262 self.profile_dir = p
262 self.profile_dir = p
263 self.config_file_paths.append(p.location)
263 self.config_file_paths.append(p.location)
264
264
265 def init_config_files(self):
265 def init_config_files(self):
266 """[optionally] copy default config files into profile dir."""
266 """[optionally] copy default config files into profile dir."""
267 # copy config files
267 # copy config files
268 path = self.builtin_profile_dir
268 path = self.builtin_profile_dir
269 if self.copy_config_files:
269 if self.copy_config_files:
270 src = self.profile
270 src = self.profile
271
271
272 cfg = self.config_file_name
272 cfg = self.config_file_name
273 if path and os.path.exists(os.path.join(path, cfg)):
273 if path and os.path.exists(os.path.join(path, cfg)):
274 self.log.warn("Staging %r from %s into %r [overwrite=%s]"%(
274 self.log.warn("Staging %r from %s into %r [overwrite=%s]"%(
275 cfg, src, self.profile_dir.location, self.overwrite)
275 cfg, src, self.profile_dir.location, self.overwrite)
276 )
276 )
277 self.profile_dir.copy_config_file(cfg, path=path, overwrite=self.overwrite)
277 self.profile_dir.copy_config_file(cfg, path=path, overwrite=self.overwrite)
278 else:
278 else:
279 self.stage_default_config_file()
279 self.stage_default_config_file()
280 else:
280 else:
281 # Still stage *bundled* config files, but not generated ones
281 # Still stage *bundled* config files, but not generated ones
282 # This is necessary for `ipython profile=sympy` to load the profile
282 # This is necessary for `ipython profile=sympy` to load the profile
283 # on the first go
283 # on the first go
284 files = glob.glob(os.path.join(path, '*.py'))
284 files = glob.glob(os.path.join(path, '*.py'))
285 for fullpath in files:
285 for fullpath in files:
286 cfg = os.path.basename(fullpath)
286 cfg = os.path.basename(fullpath)
287 if self.profile_dir.copy_config_file(cfg, path=path, overwrite=False):
287 if self.profile_dir.copy_config_file(cfg, path=path, overwrite=False):
288 # file was copied
288 # file was copied
289 self.log.warn("Staging bundled %s from %s into %r"%(
289 self.log.warn("Staging bundled %s from %s into %r"%(
290 cfg, self.profile, self.profile_dir.location)
290 cfg, self.profile, self.profile_dir.location)
291 )
291 )
292
292
293
293
294 def stage_default_config_file(self):
294 def stage_default_config_file(self):
295 """auto generate default config file, and stage it into the profile."""
295 """auto generate default config file, and stage it into the profile."""
296 s = self.generate_config_file()
296 s = self.generate_config_file()
297 fname = os.path.join(self.profile_dir.location, self.config_file_name)
297 fname = os.path.join(self.profile_dir.location, self.config_file_name)
298 if self.overwrite or not os.path.exists(fname):
298 if self.overwrite or not os.path.exists(fname):
299 self.log.warn("Generating default config file: %r"%(fname))
299 self.log.warn("Generating default config file: %r"%(fname))
300 with open(fname, 'w') as f:
300 with open(fname, 'w') as f:
301 f.write(s)
301 f.write(s)
302
302
303
303
304 def initialize(self, argv=None):
304 def initialize(self, argv=None):
305 # don't hook up crash handler before parsing command-line
305 # don't hook up crash handler before parsing command-line
306 self.parse_command_line(argv)
306 self.parse_command_line(argv)
307 self.init_crash_handler()
307 self.init_crash_handler()
308 if self.subapp is not None:
308 if self.subapp is not None:
309 # stop here if subapp is taking over
309 # stop here if subapp is taking over
310 return
310 return
311 cl_config = self.config
311 cl_config = self.config
312 self.init_profile_dir()
312 self.init_profile_dir()
313 self.init_config_files()
313 self.init_config_files()
314 self.load_config_file()
314 self.load_config_file()
315 # enforce cl-opts override configfile opts:
315 # enforce cl-opts override configfile opts:
316 self.update_config(cl_config)
316 self.update_config(cl_config)
317
317
@@ -1,395 +1,395 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 * Min Ragan-Kelley
12 * Min Ragan-Kelley
13 """
13 """
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Copyright (C) 2008-2010 The IPython Development Team
16 # Copyright (C) 2008-2010 The IPython Development Team
17 #
17 #
18 # Distributed under the terms of the BSD License. The full license is in
18 # Distributed under the terms of the BSD License. The full license is in
19 # the file COPYING, distributed as part of this software.
19 # the file COPYING, distributed as part of this software.
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23 # Imports
23 # Imports
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25
25
26 from __future__ import absolute_import
26 from __future__ import absolute_import
27
27
28 import logging
28 import logging
29 import os
29 import os
30 import sys
30 import sys
31
31
32 from IPython.config.loader import (
32 from IPython.config.loader import (
33 Config, PyFileConfigLoader
33 Config, PyFileConfigLoader, ConfigFileNotFound
34 )
34 )
35 from IPython.config.application import boolean_flag
35 from IPython.config.application import boolean_flag
36 from IPython.core import release
36 from IPython.core import release
37 from IPython.core import usage
37 from IPython.core import usage
38 from IPython.core.completer import Completer
38 from IPython.core.completer import Completer
39 from IPython.core.crashhandler import CrashHandler
39 from IPython.core.crashhandler import CrashHandler
40 from IPython.core.formatters import PlainTextFormatter
40 from IPython.core.formatters import PlainTextFormatter
41 from IPython.core.application import (
41 from IPython.core.application import (
42 ProfileDir, BaseIPythonApplication, base_flags, base_aliases
42 ProfileDir, BaseIPythonApplication, base_flags, base_aliases
43 )
43 )
44 from IPython.core.shellapp import (
44 from IPython.core.shellapp import (
45 InteractiveShellApp, shell_flags, shell_aliases
45 InteractiveShellApp, shell_flags, shell_aliases
46 )
46 )
47 from IPython.frontend.terminal.interactiveshell import TerminalInteractiveShell
47 from IPython.frontend.terminal.interactiveshell import TerminalInteractiveShell
48 from IPython.lib import inputhook
48 from IPython.lib import inputhook
49 from IPython.utils import warn
49 from IPython.utils import warn
50 from IPython.utils.path import get_ipython_dir, check_for_old_config
50 from IPython.utils.path import get_ipython_dir, check_for_old_config
51 from IPython.utils.traitlets import (
51 from IPython.utils.traitlets import (
52 Bool, List, Dict, CaselessStrEnum
52 Bool, List, Dict, CaselessStrEnum
53 )
53 )
54
54
55 #-----------------------------------------------------------------------------
55 #-----------------------------------------------------------------------------
56 # Globals, utilities and helpers
56 # Globals, utilities and helpers
57 #-----------------------------------------------------------------------------
57 #-----------------------------------------------------------------------------
58
58
59 #: The default config file name for this application.
59 #: The default config file name for this application.
60 default_config_file_name = u'ipython_config.py'
60 default_config_file_name = u'ipython_config.py'
61
61
62 _examples = """
62 _examples = """
63 ipython --pylab # start in pylab mode
63 ipython --pylab # start in pylab mode
64 ipython --pylab=qt # start in pylab mode with the qt4 backend
64 ipython --pylab=qt # start in pylab mode with the qt4 backend
65 ipython --log-level=DEBUG # set logging to DEBUG
65 ipython --log-level=DEBUG # set logging to DEBUG
66 ipython --profile=foo # start with profile foo
66 ipython --profile=foo # start with profile foo
67
67
68 ipython qtconsole # start the qtconsole GUI application
68 ipython qtconsole # start the qtconsole GUI application
69 ipython qtconsole -h # show the help string for the qtconsole subcmd
69 ipython qtconsole -h # show the help string for the qtconsole subcmd
70
70
71 ipython profile create foo # create profile foo w/ default config files
71 ipython profile create foo # create profile foo w/ default config files
72 ipython profile -h # show the help string for the profile subcmd
72 ipython profile -h # show the help string for the profile subcmd
73 """
73 """
74
74
75 #-----------------------------------------------------------------------------
75 #-----------------------------------------------------------------------------
76 # Crash handler for this application
76 # Crash handler for this application
77 #-----------------------------------------------------------------------------
77 #-----------------------------------------------------------------------------
78
78
79 class IPAppCrashHandler(CrashHandler):
79 class IPAppCrashHandler(CrashHandler):
80 """sys.excepthook for IPython itself, leaves a detailed report on disk."""
80 """sys.excepthook for IPython itself, leaves a detailed report on disk."""
81
81
82 def __init__(self, app):
82 def __init__(self, app):
83 contact_name = release.authors['Fernando'][0]
83 contact_name = release.authors['Fernando'][0]
84 contact_email = release.authors['Fernando'][1]
84 contact_email = release.authors['Fernando'][1]
85 bug_tracker = 'http://github.com/ipython/ipython/issues'
85 bug_tracker = 'http://github.com/ipython/ipython/issues'
86 super(IPAppCrashHandler,self).__init__(
86 super(IPAppCrashHandler,self).__init__(
87 app, contact_name, contact_email, bug_tracker
87 app, contact_name, contact_email, bug_tracker
88 )
88 )
89
89
90 def make_report(self,traceback):
90 def make_report(self,traceback):
91 """Return a string containing a crash report."""
91 """Return a string containing a crash report."""
92
92
93 sec_sep = self.section_sep
93 sec_sep = self.section_sep
94 # Start with parent report
94 # Start with parent report
95 report = [super(IPAppCrashHandler, self).make_report(traceback)]
95 report = [super(IPAppCrashHandler, self).make_report(traceback)]
96 # Add interactive-specific info we may have
96 # Add interactive-specific info we may have
97 rpt_add = report.append
97 rpt_add = report.append
98 try:
98 try:
99 rpt_add(sec_sep+"History of session input:")
99 rpt_add(sec_sep+"History of session input:")
100 for line in self.app.shell.user_ns['_ih']:
100 for line in self.app.shell.user_ns['_ih']:
101 rpt_add(line)
101 rpt_add(line)
102 rpt_add('\n*** Last line of input (may not be in above history):\n')
102 rpt_add('\n*** Last line of input (may not be in above history):\n')
103 rpt_add(self.app.shell._last_input_line+'\n')
103 rpt_add(self.app.shell._last_input_line+'\n')
104 except:
104 except:
105 pass
105 pass
106
106
107 return ''.join(report)
107 return ''.join(report)
108
108
109 #-----------------------------------------------------------------------------
109 #-----------------------------------------------------------------------------
110 # Aliases and Flags
110 # Aliases and Flags
111 #-----------------------------------------------------------------------------
111 #-----------------------------------------------------------------------------
112 flags = dict(base_flags)
112 flags = dict(base_flags)
113 flags.update(shell_flags)
113 flags.update(shell_flags)
114 addflag = lambda *args: flags.update(boolean_flag(*args))
114 addflag = lambda *args: flags.update(boolean_flag(*args))
115 addflag('autoedit-syntax', 'TerminalInteractiveShell.autoedit_syntax',
115 addflag('autoedit-syntax', 'TerminalInteractiveShell.autoedit_syntax',
116 'Turn on auto editing of files with syntax errors.',
116 'Turn on auto editing of files with syntax errors.',
117 'Turn off auto editing of files with syntax errors.'
117 'Turn off auto editing of files with syntax errors.'
118 )
118 )
119 addflag('banner', 'TerminalIPythonApp.display_banner',
119 addflag('banner', 'TerminalIPythonApp.display_banner',
120 "Display a banner upon starting IPython.",
120 "Display a banner upon starting IPython.",
121 "Don't display a banner upon starting IPython."
121 "Don't display a banner upon starting IPython."
122 )
122 )
123 addflag('confirm-exit', 'TerminalInteractiveShell.confirm_exit',
123 addflag('confirm-exit', 'TerminalInteractiveShell.confirm_exit',
124 """Set to confirm when you try to exit IPython with an EOF (Control-D
124 """Set to confirm when you try to exit IPython with an EOF (Control-D
125 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
125 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
126 you can force a direct exit without any confirmation.""",
126 you can force a direct exit without any confirmation.""",
127 "Don't prompt the user when exiting."
127 "Don't prompt the user when exiting."
128 )
128 )
129 addflag('term-title', 'TerminalInteractiveShell.term_title',
129 addflag('term-title', 'TerminalInteractiveShell.term_title',
130 "Enable auto setting the terminal title.",
130 "Enable auto setting the terminal title.",
131 "Disable auto setting the terminal title."
131 "Disable auto setting the terminal title."
132 )
132 )
133 classic_config = Config()
133 classic_config = Config()
134 classic_config.InteractiveShell.cache_size = 0
134 classic_config.InteractiveShell.cache_size = 0
135 classic_config.PlainTextFormatter.pprint = False
135 classic_config.PlainTextFormatter.pprint = False
136 classic_config.InteractiveShell.prompt_in1 = '>>> '
136 classic_config.InteractiveShell.prompt_in1 = '>>> '
137 classic_config.InteractiveShell.prompt_in2 = '... '
137 classic_config.InteractiveShell.prompt_in2 = '... '
138 classic_config.InteractiveShell.prompt_out = ''
138 classic_config.InteractiveShell.prompt_out = ''
139 classic_config.InteractiveShell.separate_in = ''
139 classic_config.InteractiveShell.separate_in = ''
140 classic_config.InteractiveShell.separate_out = ''
140 classic_config.InteractiveShell.separate_out = ''
141 classic_config.InteractiveShell.separate_out2 = ''
141 classic_config.InteractiveShell.separate_out2 = ''
142 classic_config.InteractiveShell.colors = 'NoColor'
142 classic_config.InteractiveShell.colors = 'NoColor'
143 classic_config.InteractiveShell.xmode = 'Plain'
143 classic_config.InteractiveShell.xmode = 'Plain'
144
144
145 flags['classic']=(
145 flags['classic']=(
146 classic_config,
146 classic_config,
147 "Gives IPython a similar feel to the classic Python prompt."
147 "Gives IPython a similar feel to the classic Python prompt."
148 )
148 )
149 # # log doesn't make so much sense this way anymore
149 # # log doesn't make so much sense this way anymore
150 # paa('--log','-l',
150 # paa('--log','-l',
151 # action='store_true', dest='InteractiveShell.logstart',
151 # action='store_true', dest='InteractiveShell.logstart',
152 # help="Start logging to the default log file (./ipython_log.py).")
152 # help="Start logging to the default log file (./ipython_log.py).")
153 #
153 #
154 # # quick is harder to implement
154 # # quick is harder to implement
155 flags['quick']=(
155 flags['quick']=(
156 {'TerminalIPythonApp' : {'quick' : True}},
156 {'TerminalIPythonApp' : {'quick' : True}},
157 "Enable quick startup with no config files."
157 "Enable quick startup with no config files."
158 )
158 )
159
159
160 flags['i'] = (
160 flags['i'] = (
161 {'TerminalIPythonApp' : {'force_interact' : True}},
161 {'TerminalIPythonApp' : {'force_interact' : True}},
162 """If running code from the command line, become interactive afterwards.
162 """If running code from the command line, become interactive afterwards.
163 Note: can also be given simply as '-i.'"""
163 Note: can also be given simply as '-i.'"""
164 )
164 )
165 flags['pylab'] = (
165 flags['pylab'] = (
166 {'TerminalIPythonApp' : {'pylab' : 'auto'}},
166 {'TerminalIPythonApp' : {'pylab' : 'auto'}},
167 """Pre-load matplotlib and numpy for interactive use with
167 """Pre-load matplotlib and numpy for interactive use with
168 the default matplotlib backend."""
168 the default matplotlib backend."""
169 )
169 )
170
170
171 aliases = dict(base_aliases)
171 aliases = dict(base_aliases)
172 aliases.update(shell_aliases)
172 aliases.update(shell_aliases)
173
173
174 # it's possible we don't want short aliases for *all* of these:
174 # it's possible we don't want short aliases for *all* of these:
175 aliases.update(dict(
175 aliases.update(dict(
176 gui='TerminalIPythonApp.gui',
176 gui='TerminalIPythonApp.gui',
177 pylab='TerminalIPythonApp.pylab',
177 pylab='TerminalIPythonApp.pylab',
178 ))
178 ))
179
179
180 #-----------------------------------------------------------------------------
180 #-----------------------------------------------------------------------------
181 # Main classes and functions
181 # Main classes and functions
182 #-----------------------------------------------------------------------------
182 #-----------------------------------------------------------------------------
183
183
184 class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp):
184 class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp):
185 name = u'ipython'
185 name = u'ipython'
186 description = usage.cl_usage
186 description = usage.cl_usage
187 default_config_file_name = default_config_file_name
187 default_config_file_name = default_config_file_name
188 crash_handler_class = IPAppCrashHandler
188 crash_handler_class = IPAppCrashHandler
189 examples = _examples
189 examples = _examples
190
190
191 flags = Dict(flags)
191 flags = Dict(flags)
192 aliases = Dict(aliases)
192 aliases = Dict(aliases)
193 classes = List()
193 classes = List()
194 def _classes_default(self):
194 def _classes_default(self):
195 """This has to be in a method, for TerminalIPythonApp to be available."""
195 """This has to be in a method, for TerminalIPythonApp to be available."""
196 return [
196 return [
197 InteractiveShellApp, # ShellApp comes before TerminalApp, because
197 InteractiveShellApp, # ShellApp comes before TerminalApp, because
198 self.__class__, # it will also affect subclasses (e.g. QtConsole)
198 self.__class__, # it will also affect subclasses (e.g. QtConsole)
199 TerminalInteractiveShell,
199 TerminalInteractiveShell,
200 ProfileDir,
200 ProfileDir,
201 PlainTextFormatter,
201 PlainTextFormatter,
202 Completer,
202 Completer,
203 ]
203 ]
204
204
205 subcommands = Dict(dict(
205 subcommands = Dict(dict(
206 qtconsole=('IPython.frontend.qt.console.qtconsoleapp.IPythonQtConsoleApp',
206 qtconsole=('IPython.frontend.qt.console.qtconsoleapp.IPythonQtConsoleApp',
207 """Launch the IPython Qt Console."""
207 """Launch the IPython Qt Console."""
208 ),
208 ),
209 notebook=('IPython.frontend.html.notebook.notebookapp.IPythonNotebookApp',
209 notebook=('IPython.frontend.html.notebook.notebookapp.IPythonNotebookApp',
210 """Launch the IPython HTML Notebook Server"""
210 """Launch the IPython HTML Notebook Server"""
211 ),
211 ),
212 profile = ("IPython.core.profileapp.ProfileApp",
212 profile = ("IPython.core.profileapp.ProfileApp",
213 "Create and manage IPython profiles."
213 "Create and manage IPython profiles."
214 ),
214 ),
215 kernel = ("IPython.zmq.ipkernel.IPKernelApp",
215 kernel = ("IPython.zmq.ipkernel.IPKernelApp",
216 "Start a kernel without an attached frontend."
216 "Start a kernel without an attached frontend."
217 ),
217 ),
218 ))
218 ))
219
219
220 # *do* autocreate requested profile, but don't create the config file.
220 # *do* autocreate requested profile, but don't create the config file.
221 auto_create=Bool(True)
221 auto_create=Bool(True)
222 # configurables
222 # configurables
223 ignore_old_config=Bool(False, config=True,
223 ignore_old_config=Bool(False, config=True,
224 help="Suppress warning messages about legacy config files"
224 help="Suppress warning messages about legacy config files"
225 )
225 )
226 quick = Bool(False, config=True,
226 quick = Bool(False, config=True,
227 help="""Start IPython quickly by skipping the loading of config files."""
227 help="""Start IPython quickly by skipping the loading of config files."""
228 )
228 )
229 def _quick_changed(self, name, old, new):
229 def _quick_changed(self, name, old, new):
230 if new:
230 if new:
231 self.load_config_file = lambda *a, **kw: None
231 self.load_config_file = lambda *a, **kw: None
232 self.ignore_old_config=True
232 self.ignore_old_config=True
233
233
234 gui = CaselessStrEnum(('qt', 'wx', 'gtk', 'glut', 'pyglet'), config=True,
234 gui = CaselessStrEnum(('qt', 'wx', 'gtk', 'glut', 'pyglet'), config=True,
235 help="Enable GUI event loop integration ('qt', 'wx', 'gtk', 'glut', 'pyglet')."
235 help="Enable GUI event loop integration ('qt', 'wx', 'gtk', 'glut', 'pyglet')."
236 )
236 )
237 pylab = CaselessStrEnum(['tk', 'qt', 'wx', 'gtk', 'osx', 'auto'],
237 pylab = CaselessStrEnum(['tk', 'qt', 'wx', 'gtk', 'osx', 'auto'],
238 config=True,
238 config=True,
239 help="""Pre-load matplotlib and numpy for interactive use,
239 help="""Pre-load matplotlib and numpy for interactive use,
240 selecting a particular matplotlib backend and loop integration.
240 selecting a particular matplotlib backend and loop integration.
241 """
241 """
242 )
242 )
243 display_banner = Bool(True, config=True,
243 display_banner = Bool(True, config=True,
244 help="Whether to display a banner upon starting IPython."
244 help="Whether to display a banner upon starting IPython."
245 )
245 )
246
246
247 # if there is code of files to run from the cmd line, don't interact
247 # if there is code of files to run from the cmd line, don't interact
248 # unless the --i flag (App.force_interact) is true.
248 # unless the --i flag (App.force_interact) is true.
249 force_interact = Bool(False, config=True,
249 force_interact = Bool(False, config=True,
250 help="""If a command or file is given via the command-line,
250 help="""If a command or file is given via the command-line,
251 e.g. 'ipython foo.py"""
251 e.g. 'ipython foo.py"""
252 )
252 )
253 def _force_interact_changed(self, name, old, new):
253 def _force_interact_changed(self, name, old, new):
254 if new:
254 if new:
255 self.interact = True
255 self.interact = True
256
256
257 def _file_to_run_changed(self, name, old, new):
257 def _file_to_run_changed(self, name, old, new):
258 if new and not self.force_interact:
258 if new and not self.force_interact:
259 self.interact = False
259 self.interact = False
260 _code_to_run_changed = _file_to_run_changed
260 _code_to_run_changed = _file_to_run_changed
261
261
262 # internal, not-configurable
262 # internal, not-configurable
263 interact=Bool(True)
263 interact=Bool(True)
264
264
265
265
266 def parse_command_line(self, argv=None):
266 def parse_command_line(self, argv=None):
267 """override to allow old '-pylab' flag with deprecation warning"""
267 """override to allow old '-pylab' flag with deprecation warning"""
268
268
269 argv = sys.argv[1:] if argv is None else argv
269 argv = sys.argv[1:] if argv is None else argv
270
270
271 if '-pylab' in argv:
271 if '-pylab' in argv:
272 # deprecated `-pylab` given,
272 # deprecated `-pylab` given,
273 # warn and transform into current syntax
273 # warn and transform into current syntax
274 argv = argv[:] # copy, don't clobber
274 argv = argv[:] # copy, don't clobber
275 idx = argv.index('-pylab')
275 idx = argv.index('-pylab')
276 warn.warn("`-pylab` flag has been deprecated.\n"
276 warn.warn("`-pylab` flag has been deprecated.\n"
277 " Use `--pylab` instead, or `--pylab=foo` to specify a backend.")
277 " Use `--pylab` instead, or `--pylab=foo` to specify a backend.")
278 sub = '--pylab'
278 sub = '--pylab'
279 if len(argv) > idx+1:
279 if len(argv) > idx+1:
280 # check for gui arg, as in '-pylab qt'
280 # check for gui arg, as in '-pylab qt'
281 gui = argv[idx+1]
281 gui = argv[idx+1]
282 if gui in ('wx', 'qt', 'qt4', 'gtk', 'auto'):
282 if gui in ('wx', 'qt', 'qt4', 'gtk', 'auto'):
283 sub = '--pylab='+gui
283 sub = '--pylab='+gui
284 argv.pop(idx+1)
284 argv.pop(idx+1)
285 argv[idx] = sub
285 argv[idx] = sub
286
286
287 return super(TerminalIPythonApp, self).parse_command_line(argv)
287 return super(TerminalIPythonApp, self).parse_command_line(argv)
288
288
289 def initialize(self, argv=None):
289 def initialize(self, argv=None):
290 """Do actions after construct, but before starting the app."""
290 """Do actions after construct, but before starting the app."""
291 super(TerminalIPythonApp, self).initialize(argv)
291 super(TerminalIPythonApp, self).initialize(argv)
292 if self.subapp is not None:
292 if self.subapp is not None:
293 # don't bother initializing further, starting subapp
293 # don't bother initializing further, starting subapp
294 return
294 return
295 if not self.ignore_old_config:
295 if not self.ignore_old_config:
296 check_for_old_config(self.ipython_dir)
296 check_for_old_config(self.ipython_dir)
297 # print self.extra_args
297 # print self.extra_args
298 if self.extra_args:
298 if self.extra_args:
299 self.file_to_run = self.extra_args[0]
299 self.file_to_run = self.extra_args[0]
300 # create the shell
300 # create the shell
301 self.init_shell()
301 self.init_shell()
302 # and draw the banner
302 # and draw the banner
303 self.init_banner()
303 self.init_banner()
304 # Now a variety of things that happen after the banner is printed.
304 # Now a variety of things that happen after the banner is printed.
305 self.init_gui_pylab()
305 self.init_gui_pylab()
306 self.init_extensions()
306 self.init_extensions()
307 self.init_code()
307 self.init_code()
308
308
309 def init_shell(self):
309 def init_shell(self):
310 """initialize the InteractiveShell instance"""
310 """initialize the InteractiveShell instance"""
311 # I am a little hesitant to put these into InteractiveShell itself.
311 # I am a little hesitant to put these into InteractiveShell itself.
312 # But that might be the place for them
312 # But that might be the place for them
313 sys.path.insert(0, '')
313 sys.path.insert(0, '')
314
314
315 # Create an InteractiveShell instance.
315 # Create an InteractiveShell instance.
316 # shell.display_banner should always be False for the terminal
316 # shell.display_banner should always be False for the terminal
317 # based app, because we call shell.show_banner() by hand below
317 # based app, because we call shell.show_banner() by hand below
318 # so the banner shows *before* all extension loading stuff.
318 # so the banner shows *before* all extension loading stuff.
319 self.shell = TerminalInteractiveShell.instance(config=self.config,
319 self.shell = TerminalInteractiveShell.instance(config=self.config,
320 display_banner=False, profile_dir=self.profile_dir,
320 display_banner=False, profile_dir=self.profile_dir,
321 ipython_dir=self.ipython_dir)
321 ipython_dir=self.ipython_dir)
322
322
323 def init_banner(self):
323 def init_banner(self):
324 """optionally display the banner"""
324 """optionally display the banner"""
325 if self.display_banner and self.interact:
325 if self.display_banner and self.interact:
326 self.shell.show_banner()
326 self.shell.show_banner()
327 # Make sure there is a space below the banner.
327 # Make sure there is a space below the banner.
328 if self.log_level <= logging.INFO: print
328 if self.log_level <= logging.INFO: print
329
329
330
330
331 def init_gui_pylab(self):
331 def init_gui_pylab(self):
332 """Enable GUI event loop integration, taking pylab into account."""
332 """Enable GUI event loop integration, taking pylab into account."""
333 gui = self.gui
333 gui = self.gui
334
334
335 # Using `pylab` will also require gui activation, though which toolkit
335 # Using `pylab` will also require gui activation, though which toolkit
336 # to use may be chosen automatically based on mpl configuration.
336 # to use may be chosen automatically based on mpl configuration.
337 if self.pylab:
337 if self.pylab:
338 activate = self.shell.enable_pylab
338 activate = self.shell.enable_pylab
339 if self.pylab == 'auto':
339 if self.pylab == 'auto':
340 gui = None
340 gui = None
341 else:
341 else:
342 gui = self.pylab
342 gui = self.pylab
343 else:
343 else:
344 # Enable only GUI integration, no pylab
344 # Enable only GUI integration, no pylab
345 activate = inputhook.enable_gui
345 activate = inputhook.enable_gui
346
346
347 if gui or self.pylab:
347 if gui or self.pylab:
348 try:
348 try:
349 self.log.info("Enabling GUI event loop integration, "
349 self.log.info("Enabling GUI event loop integration, "
350 "toolkit=%s, pylab=%s" % (gui, self.pylab) )
350 "toolkit=%s, pylab=%s" % (gui, self.pylab) )
351 if self.pylab:
351 if self.pylab:
352 activate(gui, import_all=self.pylab_import_all)
352 activate(gui, import_all=self.pylab_import_all)
353 else:
353 else:
354 activate(gui)
354 activate(gui)
355 except:
355 except:
356 self.log.warn("Error in enabling GUI event loop integration:")
356 self.log.warn("Error in enabling GUI event loop integration:")
357 self.shell.showtraceback()
357 self.shell.showtraceback()
358
358
359 def start(self):
359 def start(self):
360 if self.subapp is not None:
360 if self.subapp is not None:
361 return self.subapp.start()
361 return self.subapp.start()
362 # perform any prexec steps:
362 # perform any prexec steps:
363 if self.interact:
363 if self.interact:
364 self.log.debug("Starting IPython's mainloop...")
364 self.log.debug("Starting IPython's mainloop...")
365 self.shell.mainloop()
365 self.shell.mainloop()
366 else:
366 else:
367 self.log.debug("IPython not interactive...")
367 self.log.debug("IPython not interactive...")
368
368
369
369
370 def load_default_config(ipython_dir=None):
370 def load_default_config(ipython_dir=None):
371 """Load the default config file from the default ipython_dir.
371 """Load the default config file from the default ipython_dir.
372
372
373 This is useful for embedded shells.
373 This is useful for embedded shells.
374 """
374 """
375 if ipython_dir is None:
375 if ipython_dir is None:
376 ipython_dir = get_ipython_dir()
376 ipython_dir = get_ipython_dir()
377 profile_dir = os.path.join(ipython_dir, 'profile_default')
377 profile_dir = os.path.join(ipython_dir, 'profile_default')
378 cl = PyFileConfigLoader(default_config_file_name, profile_dir)
378 cl = PyFileConfigLoader(default_config_file_name, profile_dir)
379 try:
379 try:
380 config = cl.load_config()
380 config = cl.load_config()
381 except IOError:
381 except ConfigFileNotFound:
382 # no config found
382 # no config found
383 config = Config()
383 config = Config()
384 return config
384 return config
385
385
386
386
387 def launch_new_instance():
387 def launch_new_instance():
388 """Create and run a full blown IPython instance"""
388 """Create and run a full blown IPython instance"""
389 app = TerminalIPythonApp.instance()
389 app = TerminalIPythonApp.instance()
390 app.initialize()
390 app.initialize()
391 app.start()
391 app.start()
392
392
393
393
394 if __name__ == '__main__':
394 if __name__ == '__main__':
395 launch_new_instance()
395 launch_new_instance()
General Comments 0
You need to be logged in to leave comments. Login now