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