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