##// END OF EJS Templates
Don't rely upon traitlets copying self.config...
Min RK -
Show More
@@ -1,455 +1,458 b''
1 1 # encoding: utf-8
2 2 """
3 3 An application for IPython.
4 4
5 5 All top-level applications should use the classes in this module for
6 6 handling configuration and creating configurables.
7 7
8 8 The job of an :class:`Application` is to create the master configuration
9 9 object and then create the configurable objects, passing the config to them.
10 10 """
11 11
12 12 # Copyright (c) IPython Development Team.
13 13 # Distributed under the terms of the Modified BSD License.
14 14
15 15 import atexit
16 from copy import deepcopy
16 17 import glob
17 18 import logging
18 19 import os
19 20 import shutil
20 21 import sys
21 22
22 23 from traitlets.config.application import Application, catch_config_error
23 24 from traitlets.config.loader import ConfigFileNotFound, PyFileConfigLoader
24 25 from IPython.core import release, crashhandler
25 26 from IPython.core.profiledir import ProfileDir, ProfileDirError
26 27 from IPython.paths import get_ipython_dir, get_ipython_package_dir
27 28 from IPython.utils.path import ensure_dir_exists
28 29 from IPython.utils import py3compat
29 30 from traitlets import (
30 31 List, Unicode, Type, Bool, Dict, Set, Instance, Undefined,
31 32 default, observe,
32 33 )
33 34
34 35 if os.name == 'nt':
35 36 programdata = os.environ.get('PROGRAMDATA', None)
36 37 if programdata:
37 38 SYSTEM_CONFIG_DIRS = [os.path.join(programdata, 'ipython')]
38 39 else: # PROGRAMDATA is not defined by default on XP.
39 40 SYSTEM_CONFIG_DIRS = []
40 41 else:
41 42 SYSTEM_CONFIG_DIRS = [
42 43 "/usr/local/etc/ipython",
43 44 "/etc/ipython",
44 45 ]
45 46
46 47 _envvar = os.environ.get('IPYTHON_SUPPRESS_CONFIG_ERRORS')
47 48 if _envvar in {None, ''}:
48 49 IPYTHON_SUPPRESS_CONFIG_ERRORS = None
49 50 else:
50 51 if _envvar.lower() in {'1','true'}:
51 52 IPYTHON_SUPPRESS_CONFIG_ERRORS = True
52 53 elif _envvar.lower() in {'0','false'} :
53 54 IPYTHON_SUPPRESS_CONFIG_ERRORS = False
54 55 else:
55 56 sys.exit("Unsupported value for environment variable: 'IPYTHON_SUPPRESS_CONFIG_ERRORS' is set to '%s' which is none of {'0', '1', 'false', 'true', ''}."% _envvar )
56 57
57 58 # aliases and flags
58 59
59 60 base_aliases = {
60 61 'profile-dir' : 'ProfileDir.location',
61 62 'profile' : 'BaseIPythonApplication.profile',
62 63 'ipython-dir' : 'BaseIPythonApplication.ipython_dir',
63 64 'log-level' : 'Application.log_level',
64 65 'config' : 'BaseIPythonApplication.extra_config_file',
65 66 }
66 67
67 68 base_flags = dict(
68 69 debug = ({'Application' : {'log_level' : logging.DEBUG}},
69 70 "set log level to logging.DEBUG (maximize logging output)"),
70 71 quiet = ({'Application' : {'log_level' : logging.CRITICAL}},
71 72 "set log level to logging.CRITICAL (minimize logging output)"),
72 73 init = ({'BaseIPythonApplication' : {
73 74 'copy_config_files' : True,
74 75 'auto_create' : True}
75 76 }, """Initialize profile with default config files. This is equivalent
76 77 to running `ipython profile create <profile>` prior to startup.
77 78 """)
78 79 )
79 80
80 81 class ProfileAwareConfigLoader(PyFileConfigLoader):
81 82 """A Python file config loader that is aware of IPython profiles."""
82 83 def load_subconfig(self, fname, path=None, profile=None):
83 84 if profile is not None:
84 85 try:
85 86 profile_dir = ProfileDir.find_profile_dir_by_name(
86 87 get_ipython_dir(),
87 88 profile,
88 89 )
89 90 except ProfileDirError:
90 91 return
91 92 path = profile_dir.location
92 93 return super(ProfileAwareConfigLoader, self).load_subconfig(fname, path=path)
93 94
94 95 class BaseIPythonApplication(Application):
95 96
96 97 name = Unicode(u'ipython')
97 98 description = Unicode(u'IPython: an enhanced interactive Python shell.')
98 99 version = Unicode(release.version)
99 100
100 101 aliases = Dict(base_aliases)
101 102 flags = Dict(base_flags)
102 103 classes = List([ProfileDir])
103 104
104 105 # enable `load_subconfig('cfg.py', profile='name')`
105 106 python_config_loader_class = ProfileAwareConfigLoader
106 107
107 108 # Track whether the config_file has changed,
108 109 # because some logic happens only if we aren't using the default.
109 110 config_file_specified = Set()
110 111
111 112 config_file_name = Unicode()
112 113 @default('config_file_name')
113 114 def _config_file_name_default(self):
114 115 return self.name.replace('-','_') + u'_config.py'
115 116 @observe('config_file_name')
116 117 def _config_file_name_changed(self, change):
117 118 if change['new'] != change['old']:
118 119 self.config_file_specified.add(change['new'])
119 120
120 121 # The directory that contains IPython's builtin profiles.
121 122 builtin_profile_dir = Unicode(
122 123 os.path.join(get_ipython_package_dir(), u'config', u'profile', u'default')
123 124 )
124 125
125 126 config_file_paths = List(Unicode())
126 127 @default('config_file_paths')
127 128 def _config_file_paths_default(self):
128 129 return [py3compat.getcwd()]
129 130
130 131 extra_config_file = Unicode(
131 132 help="""Path to an extra config file to load.
132 133
133 134 If specified, load this config file in addition to any other IPython config.
134 135 """).tag(config=True)
135 136 @observe('extra_config_file')
136 137 def _extra_config_file_changed(self, change):
137 138 old = change['old']
138 139 new = change['new']
139 140 try:
140 141 self.config_files.remove(old)
141 142 except ValueError:
142 143 pass
143 144 self.config_file_specified.add(new)
144 145 self.config_files.append(new)
145 146
146 147 profile = Unicode(u'default',
147 148 help="""The IPython profile to use."""
148 149 ).tag(config=True)
149 150
150 151 @observe('profile')
151 152 def _profile_changed(self, change):
152 153 self.builtin_profile_dir = os.path.join(
153 154 get_ipython_package_dir(), u'config', u'profile', change['new']
154 155 )
155 156
156 157 ipython_dir = Unicode(
157 158 help="""
158 159 The name of the IPython directory. This directory is used for logging
159 160 configuration (through profiles), history storage, etc. The default
160 161 is usually $HOME/.ipython. This option can also be specified through
161 162 the environment variable IPYTHONDIR.
162 163 """
163 164 ).tag(config=True)
164 165 @default('ipython_dir')
165 166 def _ipython_dir_default(self):
166 167 d = get_ipython_dir()
167 168 self._ipython_dir_changed({
168 169 'name': 'ipython_dir',
169 170 'old': d,
170 171 'new': d,
171 172 })
172 173 return d
173 174
174 175 _in_init_profile_dir = False
175 176 profile_dir = Instance(ProfileDir, allow_none=True)
176 177 @default('profile_dir')
177 178 def _profile_dir_default(self):
178 179 # avoid recursion
179 180 if self._in_init_profile_dir:
180 181 return
181 182 # profile_dir requested early, force initialization
182 183 self.init_profile_dir()
183 184 return self.profile_dir
184 185
185 186 overwrite = Bool(False,
186 187 help="""Whether to overwrite existing config files when copying"""
187 188 ).tag(config=True)
188 189 auto_create = Bool(False,
189 190 help="""Whether to create profile dir if it doesn't exist"""
190 191 ).tag(config=True)
191 192
192 193 config_files = List(Unicode())
193 194 @default('config_files')
194 195 def _config_files_default(self):
195 196 return [self.config_file_name]
196 197
197 198 copy_config_files = Bool(False,
198 199 help="""Whether to install the default config files into the profile dir.
199 200 If a new profile is being created, and IPython contains config files for that
200 201 profile, then they will be staged into the new directory. Otherwise,
201 202 default config files will be automatically generated.
202 203 """).tag(config=True)
203 204
204 205 verbose_crash = Bool(False,
205 206 help="""Create a massive crash report when IPython encounters what may be an
206 207 internal error. The default is to append a short message to the
207 208 usual traceback""").tag(config=True)
208 209
209 210 # The class to use as the crash handler.
210 211 crash_handler_class = Type(crashhandler.CrashHandler)
211 212
212 213 @catch_config_error
213 214 def __init__(self, **kwargs):
214 215 super(BaseIPythonApplication, self).__init__(**kwargs)
215 216 # ensure current working directory exists
216 217 try:
217 218 py3compat.getcwd()
218 219 except:
219 220 # exit if cwd doesn't exist
220 221 self.log.error("Current working directory doesn't exist.")
221 222 self.exit(1)
222 223
223 224 #-------------------------------------------------------------------------
224 225 # Various stages of Application creation
225 226 #-------------------------------------------------------------------------
226 227
227 228 deprecated_subcommands = {}
228 229
229 230 def initialize_subcommand(self, subc, argv=None):
230 231 if subc in self.deprecated_subcommands:
231 232 self.log.warning("Subcommand `ipython {sub}` is deprecated and will be removed "
232 233 "in future versions.".format(sub=subc))
233 234 self.log.warning("You likely want to use `jupyter {sub}` in the "
234 235 "future".format(sub=subc))
235 236 return super(BaseIPythonApplication, self).initialize_subcommand(subc, argv)
236 237
237 238 def init_crash_handler(self):
238 239 """Create a crash handler, typically setting sys.excepthook to it."""
239 240 self.crash_handler = self.crash_handler_class(self)
240 241 sys.excepthook = self.excepthook
241 242 def unset_crashhandler():
242 243 sys.excepthook = sys.__excepthook__
243 244 atexit.register(unset_crashhandler)
244 245
245 246 def excepthook(self, etype, evalue, tb):
246 247 """this is sys.excepthook after init_crashhandler
247 248
248 249 set self.verbose_crash=True to use our full crashhandler, instead of
249 250 a regular traceback with a short message (crash_handler_lite)
250 251 """
251 252
252 253 if self.verbose_crash:
253 254 return self.crash_handler(etype, evalue, tb)
254 255 else:
255 256 return crashhandler.crash_handler_lite(etype, evalue, tb)
256 257
257 258 @observe('ipython_dir')
258 259 def _ipython_dir_changed(self, change):
259 260 old = change['old']
260 261 new = change['new']
261 262 if old is not Undefined:
262 263 str_old = py3compat.cast_bytes_py2(os.path.abspath(old),
263 264 sys.getfilesystemencoding()
264 265 )
265 266 if str_old in sys.path:
266 267 sys.path.remove(str_old)
267 268 str_path = py3compat.cast_bytes_py2(os.path.abspath(new),
268 269 sys.getfilesystemencoding()
269 270 )
270 271 sys.path.append(str_path)
271 272 ensure_dir_exists(new)
272 273 readme = os.path.join(new, 'README')
273 274 readme_src = os.path.join(get_ipython_package_dir(), u'config', u'profile', 'README')
274 275 if not os.path.exists(readme) and os.path.exists(readme_src):
275 276 shutil.copy(readme_src, readme)
276 277 for d in ('extensions', 'nbextensions'):
277 278 path = os.path.join(new, d)
278 279 try:
279 280 ensure_dir_exists(path)
280 281 except OSError as e:
281 282 # this will not be EEXIST
282 283 self.log.error("couldn't create path %s: %s", path, e)
283 284 self.log.debug("IPYTHONDIR set to: %s" % new)
284 285
285 286 def load_config_file(self, suppress_errors=IPYTHON_SUPPRESS_CONFIG_ERRORS):
286 287 """Load the config file.
287 288
288 289 By default, errors in loading config are handled, and a warning
289 290 printed on screen. For testing, the suppress_errors option is set
290 291 to False, so errors will make tests fail.
291 292
292 293 `supress_errors` default value is to be `None` in which case the
293 294 behavior default to the one of `traitlets.Application`.
294 295
295 296 The default value can be set :
296 297 - to `False` by setting 'IPYTHON_SUPPRESS_CONFIG_ERRORS' environment variable to '0', or 'false' (case insensitive).
297 298 - to `True` by setting 'IPYTHON_SUPPRESS_CONFIG_ERRORS' environment variable to '1' or 'true' (case insensitive).
298 299 - to `None` by setting 'IPYTHON_SUPPRESS_CONFIG_ERRORS' environment variable to '' (empty string) or leaving it unset.
299 300
300 301 Any other value are invalid, and will make IPython exit with a non-zero return code.
301 302 """
302 303
303 304
304 305 self.log.debug("Searching path %s for config files", self.config_file_paths)
305 306 base_config = 'ipython_config.py'
306 307 self.log.debug("Attempting to load config file: %s" %
307 308 base_config)
308 309 try:
309 310 if suppress_errors is not None:
310 311 old_value = Application.raise_config_file_errors
311 312 Application.raise_config_file_errors = not suppress_errors;
312 313 Application.load_config_file(
313 314 self,
314 315 base_config,
315 316 path=self.config_file_paths
316 317 )
317 318 except ConfigFileNotFound:
318 319 # ignore errors loading parent
319 320 self.log.debug("Config file %s not found", base_config)
320 321 pass
321 322 if suppress_errors is not None:
322 323 Application.raise_config_file_errors = old_value
323 324
324 325 for config_file_name in self.config_files:
325 326 if not config_file_name or config_file_name == base_config:
326 327 continue
327 328 self.log.debug("Attempting to load config file: %s" %
328 329 self.config_file_name)
329 330 try:
330 331 Application.load_config_file(
331 332 self,
332 333 config_file_name,
333 334 path=self.config_file_paths
334 335 )
335 336 except ConfigFileNotFound:
336 337 # Only warn if the default config file was NOT being used.
337 338 if config_file_name in self.config_file_specified:
338 339 msg = self.log.warning
339 340 else:
340 341 msg = self.log.debug
341 342 msg("Config file not found, skipping: %s", config_file_name)
342 343 except Exception:
343 344 # For testing purposes.
344 345 if not suppress_errors:
345 346 raise
346 347 self.log.warning("Error loading config file: %s" %
347 348 self.config_file_name, exc_info=True)
348 349
349 350 def init_profile_dir(self):
350 351 """initialize the profile dir"""
351 352 self._in_init_profile_dir = True
352 353 if self.profile_dir is not None:
353 354 # already ran
354 355 return
355 356 if 'ProfileDir.location' not in self.config:
356 357 # location not specified, find by profile name
357 358 try:
358 359 p = ProfileDir.find_profile_dir_by_name(self.ipython_dir, self.profile, self.config)
359 360 except ProfileDirError:
360 361 # not found, maybe create it (always create default profile)
361 362 if self.auto_create or self.profile == 'default':
362 363 try:
363 364 p = ProfileDir.create_profile_dir_by_name(self.ipython_dir, self.profile, self.config)
364 365 except ProfileDirError:
365 366 self.log.fatal("Could not create profile: %r"%self.profile)
366 367 self.exit(1)
367 368 else:
368 369 self.log.info("Created profile dir: %r"%p.location)
369 370 else:
370 371 self.log.fatal("Profile %r not found."%self.profile)
371 372 self.exit(1)
372 373 else:
373 374 self.log.debug("Using existing profile dir: %r"%p.location)
374 375 else:
375 376 location = self.config.ProfileDir.location
376 377 # location is fully specified
377 378 try:
378 379 p = ProfileDir.find_profile_dir(location, self.config)
379 380 except ProfileDirError:
380 381 # not found, maybe create it
381 382 if self.auto_create:
382 383 try:
383 384 p = ProfileDir.create_profile_dir(location, self.config)
384 385 except ProfileDirError:
385 386 self.log.fatal("Could not create profile directory: %r"%location)
386 387 self.exit(1)
387 388 else:
388 389 self.log.debug("Creating new profile dir: %r"%location)
389 390 else:
390 391 self.log.fatal("Profile directory %r not found."%location)
391 392 self.exit(1)
392 393 else:
393 394 self.log.info("Using existing profile dir: %r"%location)
394 395 # if profile_dir is specified explicitly, set profile name
395 396 dir_name = os.path.basename(p.location)
396 397 if dir_name.startswith('profile_'):
397 398 self.profile = dir_name[8:]
398 399
399 400 self.profile_dir = p
400 401 self.config_file_paths.append(p.location)
401 402 self._in_init_profile_dir = False
402 403
403 404 def init_config_files(self):
404 405 """[optionally] copy default config files into profile dir."""
405 406 self.config_file_paths.extend(SYSTEM_CONFIG_DIRS)
406 407 # copy config files
407 408 path = self.builtin_profile_dir
408 409 if self.copy_config_files:
409 410 src = self.profile
410 411
411 412 cfg = self.config_file_name
412 413 if path and os.path.exists(os.path.join(path, cfg)):
413 414 self.log.warning("Staging %r from %s into %r [overwrite=%s]"%(
414 415 cfg, src, self.profile_dir.location, self.overwrite)
415 416 )
416 417 self.profile_dir.copy_config_file(cfg, path=path, overwrite=self.overwrite)
417 418 else:
418 419 self.stage_default_config_file()
419 420 else:
420 421 # Still stage *bundled* config files, but not generated ones
421 422 # This is necessary for `ipython profile=sympy` to load the profile
422 423 # on the first go
423 424 files = glob.glob(os.path.join(path, '*.py'))
424 425 for fullpath in files:
425 426 cfg = os.path.basename(fullpath)
426 427 if self.profile_dir.copy_config_file(cfg, path=path, overwrite=False):
427 428 # file was copied
428 429 self.log.warning("Staging bundled %s from %s into %r"%(
429 430 cfg, self.profile, self.profile_dir.location)
430 431 )
431 432
432 433
433 434 def stage_default_config_file(self):
434 435 """auto generate default config file, and stage it into the profile."""
435 436 s = self.generate_config_file()
436 437 fname = os.path.join(self.profile_dir.location, self.config_file_name)
437 438 if self.overwrite or not os.path.exists(fname):
438 439 self.log.warning("Generating default config file: %r"%(fname))
439 440 with open(fname, 'w') as f:
440 441 f.write(s)
441 442
442 443 @catch_config_error
443 444 def initialize(self, argv=None):
444 445 # don't hook up crash handler before parsing command-line
445 446 self.parse_command_line(argv)
446 447 self.init_crash_handler()
447 448 if self.subapp is not None:
448 449 # stop here if subapp is taking over
449 450 return
450 cl_config = self.config
451 # save a copy of CLI config to re-load after config files
452 # so that it has highest priority
453 cl_config = deepcopy(self.config)
451 454 self.init_profile_dir()
452 455 self.init_config_files()
453 456 self.load_config_file()
454 457 # enforce cl-opts override configfile opts:
455 458 self.update_config(cl_config)
@@ -1,50 +1,74 b''
1 1 # coding: utf-8
2 2 """Tests for IPython.core.application"""
3 3
4 4 import os
5 5 import tempfile
6 6
7 import nose.tools as nt
8
9 from traitlets import Unicode
10
7 11 from IPython.core.application import BaseIPythonApplication
8 12 from IPython.testing import decorators as dec
9 13 from IPython.utils import py3compat
14 from IPython.utils.tempdir import TemporaryDirectory
15
10 16
11 17 @dec.onlyif_unicode_paths
12 18 def test_unicode_cwd():
13 19 """Check that IPython starts with non-ascii characters in the path."""
14 20 wd = tempfile.mkdtemp(suffix=u"€")
15 21
16 22 old_wd = py3compat.getcwd()
17 23 os.chdir(wd)
18 24 #raise Exception(repr(py3compat.getcwd()))
19 25 try:
20 26 app = BaseIPythonApplication()
21 27 # The lines below are copied from Application.initialize()
22 28 app.init_profile_dir()
23 29 app.init_config_files()
24 30 app.load_config_file(suppress_errors=False)
25 31 finally:
26 32 os.chdir(old_wd)
27 33
28 34 @dec.onlyif_unicode_paths
29 35 def test_unicode_ipdir():
30 36 """Check that IPython starts with non-ascii characters in the IP dir."""
31 37 ipdir = tempfile.mkdtemp(suffix=u"€")
32 38
33 39 # Create the config file, so it tries to load it.
34 40 with open(os.path.join(ipdir, 'ipython_config.py'), "w") as f:
35 41 pass
36 42
37 43 old_ipdir1 = os.environ.pop("IPYTHONDIR", None)
38 44 old_ipdir2 = os.environ.pop("IPYTHON_DIR", None)
39 45 os.environ["IPYTHONDIR"] = py3compat.unicode_to_str(ipdir, "utf-8")
40 46 try:
41 47 app = BaseIPythonApplication()
42 48 # The lines below are copied from Application.initialize()
43 49 app.init_profile_dir()
44 50 app.init_config_files()
45 51 app.load_config_file(suppress_errors=False)
46 52 finally:
47 53 if old_ipdir1:
48 54 os.environ["IPYTHONDIR"] = old_ipdir1
49 55 if old_ipdir2:
50 56 os.environ["IPYTHONDIR"] = old_ipdir2
57
58 def test_cli_priority():
59 with TemporaryDirectory() as td:
60
61 class TestApp(BaseIPythonApplication):
62 test = Unicode().tag(config=True)
63
64 # Create the config file, so it tries to load it.
65 with open(os.path.join(td, 'ipython_config.py'), "w") as f:
66 f.write("c.TestApp.test = 'config file'")
67
68 app = TestApp()
69 app.initialize(['--profile-dir', td])
70 nt.assert_equal(app.test, 'config file')
71 app = TestApp()
72 app.initialize(['--profile-dir', td, '--TestApp.test=cli'])
73 nt.assert_equal(app.test, 'cli')
74
General Comments 0
You need to be logged in to leave comments. Login now