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