Format: text/x-python
⬇ Download File (18.5 KB)
   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