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