Format: text/x-python
⬇ Download File (18.8 KB)
   1  # encoding: utf-8
   2  """
   3  A mixin for :class:`~IPython.core.application.Application` classes that
   4  launch InteractiveShell instances, load extensions, etc.
   5  """
   6  
   7  # Copyright (c) IPython Development Team.
   8  # Distributed under the terms of the Modified BSD License.
   9  
  10  import glob
  11  from itertools import chain
  12  import os
  13  import sys
  14  import typing as t
  15  
  16  from traitlets.config.application import boolean_flag
  17  from traitlets.config.configurable import Configurable
  18  from traitlets.config.loader import Config
  19  from IPython.core.application import SYSTEM_CONFIG_DIRS, ENV_CONFIG_DIRS
  20  from IPython.utils.contexts import preserve_keys
  21  from IPython.utils.path import filefind
  22  from traitlets import (
  23      Unicode,
  24      Instance,
  25      List,
  26      Bool,
  27      CaselessStrEnum,
  28      observe,
  29      DottedObjectName,
  30      Undefined,
  31  )
  32  from IPython.terminal import pt_inputhooks
  33  
  34  # -----------------------------------------------------------------------------
  35  # Aliases and Flags
  36  # -----------------------------------------------------------------------------
  37  
  38  gui_keys = tuple(sorted(pt_inputhooks.backends) + sorted(pt_inputhooks.aliases))
  39  
  40  shell_flags = {}
  41  
  42  addflag = lambda *args: shell_flags.update(boolean_flag(*args))
  43  addflag(
  44      "autoindent",
  45      "InteractiveShell.autoindent",
  46      "Turn on autoindenting.",
  47      "Turn off autoindenting.",
  48  )
  49  addflag(
  50      "automagic",
  51      "InteractiveShell.automagic",
  52      """Turn on the auto calling of magic commands. Type %%magic at the
  53          IPython  prompt  for  more information.""",
  54          'Turn off the auto calling of magic commands.'
  55  )
  56  addflag('pdb', 'InteractiveShell.pdb',
  57      "Enable auto calling the pdb debugger after every exception.",
  58      "Disable auto calling the pdb debugger after every exception."
  59  )
  60  addflag('pprint', 'PlainTextFormatter.pprint',
  61      "Enable auto pretty printing of results.",
  62      "Disable auto pretty printing of results."
  63  )
  64  addflag('color-info', 'InteractiveShell.color_info',
  65      """IPython can display information about objects via a set of functions,
  66      and optionally can use colors for this, syntax highlighting
  67      source code and various other elements. This is on by default, but can cause
  68      problems with some pagers. If you see such problems, you can disable the
  69      colours.""",
  70      "Disable using colors for info related things."
  71  )
  72  addflag('ignore-cwd', 'InteractiveShellApp.ignore_cwd',
  73          "Exclude the current working directory from sys.path",
  74          "Include the current working directory in sys.path",
  75  )
  76  nosep_config = Config()
  77  nosep_config.InteractiveShell.separate_in = ''
  78  nosep_config.InteractiveShell.separate_out = ''
  79  nosep_config.InteractiveShell.separate_out2 = ''
  80  
  81  shell_flags['nosep']=(nosep_config, "Eliminate all spacing between prompts.")
  82  shell_flags['pylab'] = (
  83      {'InteractiveShellApp' : {'pylab' : 'auto'}},
  84      """Pre-load matplotlib and numpy for interactive use with
  85      the default matplotlib backend. The exact options available
  86      depend on what Matplotlib provides at runtime.""",
  87  )
  88  shell_flags['matplotlib'] = (
  89      {'InteractiveShellApp' : {'matplotlib' : 'auto'}},
  90      """Configure matplotlib for interactive use with
  91      the default matplotlib backend. The exact options available
  92      depend on what Matplotlib provides at runtime.""",
  93  )
  94  
  95  # it's possible we don't want short aliases for *all* of these:
  96  shell_aliases = dict(
  97      autocall='InteractiveShell.autocall',
  98      colors='InteractiveShell.colors',
  99      logfile='InteractiveShell.logfile',
 100      logappend='InteractiveShell.logappend',
 101      c='InteractiveShellApp.code_to_run',
 102      m='InteractiveShellApp.module_to_run',
 103      ext="InteractiveShellApp.extra_extensions",
 104      gui='InteractiveShellApp.gui',
 105      pylab='InteractiveShellApp.pylab',
 106      matplotlib='InteractiveShellApp.matplotlib',
 107  )
 108  shell_aliases['cache-size'] = 'InteractiveShell.cache_size'
 109  
 110  
 111  # -----------------------------------------------------------------------------
 112  # Traitlets
 113  # -----------------------------------------------------------------------------
 114  
 115  
 116  class MatplotlibBackendCaselessStrEnum(CaselessStrEnum):
 117      """An enum of Matplotlib backend strings where the case should be ignored.
 118  
 119      Prior to Matplotlib 3.9.0 the list of valid backends is hardcoded in
 120      pylabtools.backends. After that, Matplotlib manages backends.
 121  
 122      The list of valid backends is determined when it is first needed to avoid
 123      wasting unnecessary initialisation time.
 124      """
 125  
 126      def __init__(
 127          self: CaselessStrEnum[t.Any],
 128          default_value: t.Any = Undefined,
 129          **kwargs: t.Any,
 130      ) -> None:
 131          super().__init__(None, default_value=default_value, **kwargs)
 132  
 133      def __getattribute__(self, name):
 134          if name == "values" and object.__getattribute__(self, name) is None:
 135              from IPython.core.pylabtools import _list_matplotlib_backends_and_gui_loops
 136  
 137              self.values = _list_matplotlib_backends_and_gui_loops()
 138          return object.__getattribute__(self, name)
 139  
 140  
 141  #-----------------------------------------------------------------------------
 142  # Main classes and functions
 143  #-----------------------------------------------------------------------------
 144  
 145  class InteractiveShellApp(Configurable):
 146      """A Mixin for applications that start InteractiveShell instances.
 147  
 148      Provides configurables for loading extensions and executing files
 149      as part of configuring a Shell environment.
 150  
 151      The following methods should be called by the :meth:`initialize` method
 152      of the subclass:
 153  
 154        - :meth:`init_path`
 155        - :meth:`init_shell` (to be implemented by the subclass)
 156        - :meth:`init_gui_pylab`
 157        - :meth:`init_extensions`
 158        - :meth:`init_code`
 159      """
 160      extensions = List(Unicode(),
 161          help="A list of dotted module names of IPython extensions to load."
 162      ).tag(config=True)
 163  
 164      extra_extensions = List(
 165          DottedObjectName(),
 166          help="""
 167          Dotted module name(s) of one or more IPython extensions to load.
 168  
 169          For specifying extra extensions to load on the command-line.
 170  
 171          .. versionadded:: 7.10
 172          """,
 173      ).tag(config=True)
 174  
 175      reraise_ipython_extension_failures = Bool(False,
 176          help="Reraise exceptions encountered loading IPython extensions?",
 177      ).tag(config=True)
 178  
 179      # Extensions that are always loaded (not configurable)
 180      default_extensions = List(Unicode(), [u'storemagic']).tag(config=False)
 181  
 182      hide_initial_ns = Bool(True,
 183          help="""Should variables loaded at startup (by startup files, exec_lines, etc.)
 184          be hidden from tools like %who?"""
 185      ).tag(config=True)
 186  
 187      exec_files = List(Unicode(),
 188          help="""List of files to run at IPython startup."""
 189      ).tag(config=True)
 190      exec_PYTHONSTARTUP = Bool(True,
 191          help="""Run the file referenced by the PYTHONSTARTUP environment
 192          variable at IPython startup."""
 193      ).tag(config=True)
 194      file_to_run = Unicode('',
 195          help="""A file to be run""").tag(config=True)
 196  
 197      exec_lines = List(Unicode(),
 198          help="""lines of code to run at IPython startup."""
 199      ).tag(config=True)
 200      code_to_run = Unicode("", help="Execute the given command string.").tag(config=True)
 201      module_to_run = Unicode("", help="Run the module as a script.").tag(config=True)
 202      gui = CaselessStrEnum(
 203          gui_keys,
 204          allow_none=True,
 205          help="Enable GUI event loop integration with any of {0}.".format(gui_keys),
 206      ).tag(config=True)
 207      matplotlib = MatplotlibBackendCaselessStrEnum(
 208          allow_none=True,
 209          help="""Configure matplotlib for interactive use with
 210          the default matplotlib backend. The exact options available
 211          depend on what Matplotlib provides at runtime.""",
 212      ).tag(config=True)
 213      pylab = MatplotlibBackendCaselessStrEnum(
 214          allow_none=True,
 215          help="""Pre-load matplotlib and numpy for interactive use,
 216          selecting a particular matplotlib backend and loop integration.
 217          The exact options available depend on what Matplotlib provides at runtime.
 218          """,
 219      ).tag(config=True)
 220      pylab_import_all = Bool(
 221          True,
 222          help="""If true, IPython will populate the user namespace with numpy, pylab, etc.
 223          and an ``import *`` is done from numpy and pylab, when using pylab mode.
 224  
 225          When False, pylab mode should not import any names into the user namespace.
 226          """,
 227      ).tag(config=True)
 228      ignore_cwd = Bool(
 229          False,
 230          help="""If True, IPython will not add the current working directory to sys.path.
 231          When False, the current working directory is added to sys.path, allowing imports
 232          of modules defined in the current directory."""
 233      ).tag(config=True)
 234      shell = Instance('IPython.core.interactiveshell.InteractiveShellABC',
 235                       allow_none=True)
 236      # whether interact-loop should start
 237      interact = Bool(True)
 238  
 239      user_ns = Instance(dict, args=None, allow_none=True)
 240      @observe('user_ns')
 241      def _user_ns_changed(self, change):
 242          if self.shell is not None:
 243              self.shell.user_ns = change['new']
 244              self.shell.init_user_ns()
 245  
 246      def init_path(self):
 247          """Add current working directory, '', to sys.path
 248  
 249          Unlike Python's default, we insert before the first `site-packages`
 250          or `dist-packages` directory,
 251          so that it is after the standard library.
 252  
 253          .. versionchanged:: 7.2
 254              Try to insert after the standard library, instead of first.
 255          .. versionchanged:: 8.0
 256              Allow optionally not including the current directory in sys.path
 257          """
 258          if '' in sys.path or self.ignore_cwd:
 259              return
 260          for idx, path in enumerate(sys.path):
 261              parent, last_part = os.path.split(path)
 262              if last_part in {'site-packages', 'dist-packages'}:
 263                  break
 264          else:
 265              # no site-packages or dist-packages found (?!)
 266              # back to original behavior of inserting at the front
 267              idx = 0
 268          sys.path.insert(idx, '')
 269  
 270      def init_shell(self):
 271          raise NotImplementedError("Override in subclasses")
 272  
 273      def init_gui_pylab(self):
 274          """Enable GUI event loop integration, taking pylab into account."""
 275          enable = False
 276          shell = self.shell
 277          if self.pylab:
 278              enable = lambda key: shell.enable_pylab(key, import_all=self.pylab_import_all)
 279              key = self.pylab
 280          elif self.matplotlib:
 281              enable = shell.enable_matplotlib
 282              key = self.matplotlib
 283          elif self.gui:
 284              enable = shell.enable_gui
 285              key = self.gui
 286  
 287          if not enable:
 288              return
 289  
 290          try:
 291              r = enable(key)
 292          except ImportError:
 293              self.log.warning("Eventloop or matplotlib integration failed. Is matplotlib installed?")
 294              self.shell.showtraceback()
 295              return
 296          except Exception:
 297              self.log.warning("GUI event loop or pylab initialization failed")
 298              self.shell.showtraceback()
 299              return
 300  
 301          if isinstance(r, tuple):
 302              gui, backend = r[:2]
 303              self.log.info("Enabling GUI event loop integration, "
 304                        "eventloop=%s, matplotlib=%s", gui, backend)
 305              if key == "auto":
 306                  print("Using matplotlib backend: %s" % backend)
 307          else:
 308              gui = r
 309              self.log.info("Enabling GUI event loop integration, "
 310                        "eventloop=%s", gui)
 311  
 312      def init_extensions(self):
 313          """Load all IPython extensions in IPythonApp.extensions.
 314  
 315          This uses the :meth:`ExtensionManager.load_extensions` to load all
 316          the extensions listed in ``self.extensions``.
 317          """
 318          try:
 319              self.log.debug("Loading IPython extensions...")
 320              extensions = (
 321                  self.default_extensions + self.extensions + self.extra_extensions
 322              )
 323              for ext in extensions:
 324                  try:
 325                      self.log.info("Loading IPython extension: %s", ext)
 326                      self.shell.extension_manager.load_extension(ext)
 327                  except:
 328                      if self.reraise_ipython_extension_failures:
 329                          raise
 330                      msg = ("Error in loading extension: {ext}\n"
 331                             "Check your config files in {location}".format(
 332                                 ext=ext,
 333                                 location=self.profile_dir.location
 334                             ))
 335                      self.log.warning(msg, exc_info=True)
 336          except:
 337              if self.reraise_ipython_extension_failures:
 338                  raise
 339              self.log.warning("Unknown error in loading extensions:", exc_info=True)
 340  
 341      def init_code(self):
 342          """run the pre-flight code, specified via exec_lines"""
 343          self._run_startup_files()
 344          self._run_exec_lines()
 345          self._run_exec_files()
 346  
 347          # Hide variables defined here from %who etc.
 348          if self.hide_initial_ns:
 349              self.shell.user_ns_hidden.update(self.shell.user_ns)
 350  
 351          # command-line execution (ipython -i script.py, ipython -m module)
 352          # should *not* be excluded from %whos
 353          self._run_cmd_line_code()
 354          self._run_module()
 355  
 356          # flush output, so itwon't be attached to the first cell
 357          sys.stdout.flush()
 358          sys.stderr.flush()
 359          self.shell._sys_modules_keys = set(sys.modules.keys())
 360  
 361      def _run_exec_lines(self):
 362          """Run lines of code in IPythonApp.exec_lines in the user's namespace."""
 363          if not self.exec_lines:
 364              return
 365          try:
 366              self.log.debug("Running code from IPythonApp.exec_lines...")
 367              for line in self.exec_lines:
 368                  try:
 369                      self.log.info("Running code in user namespace: %s" %
 370                                    line)
 371                      self.shell.run_cell(line, store_history=False)
 372                  except:
 373                      self.log.warning("Error in executing line in user "
 374                                    "namespace: %s" % line)
 375                      self.shell.showtraceback()
 376          except:
 377              self.log.warning("Unknown error in handling IPythonApp.exec_lines:")
 378              self.shell.showtraceback()
 379  
 380      def _exec_file(self, fname, shell_futures=False):
 381          try:
 382              full_filename = filefind(fname, [u'.', self.ipython_dir])
 383          except IOError:
 384              self.log.warning("File not found: %r"%fname)
 385              return
 386          # Make sure that the running script gets a proper sys.argv as if it
 387          # were run from a system shell.
 388          save_argv = sys.argv
 389          sys.argv = [full_filename] + self.extra_args[1:]
 390          try:
 391              if os.path.isfile(full_filename):
 392                  self.log.info("Running file in user namespace: %s" %
 393                                full_filename)
 394                  # Ensure that __file__ is always defined to match Python
 395                  # behavior.
 396                  with preserve_keys(self.shell.user_ns, '__file__'):
 397                      self.shell.user_ns['__file__'] = fname
 398                      if full_filename.endswith('.ipy') or full_filename.endswith('.ipynb'):
 399                          self.shell.safe_execfile_ipy(full_filename,
 400                                                       shell_futures=shell_futures)
 401                      else:
 402                          # default to python, even without extension
 403                          self.shell.safe_execfile(full_filename,
 404                                                   self.shell.user_ns,
 405                                                   shell_futures=shell_futures,
 406                                                   raise_exceptions=True)
 407          finally:
 408              sys.argv = save_argv
 409  
 410      def _run_startup_files(self):
 411          """Run files from profile startup directory"""
 412          startup_dirs = [self.profile_dir.startup_dir] + [
 413              os.path.join(p, 'startup') for p in chain(ENV_CONFIG_DIRS, SYSTEM_CONFIG_DIRS)
 414          ]
 415          startup_files = []
 416  
 417          if self.exec_PYTHONSTARTUP and os.environ.get('PYTHONSTARTUP', False) and \
 418                  not (self.file_to_run or self.code_to_run or self.module_to_run):
 419              python_startup = os.environ['PYTHONSTARTUP']
 420              self.log.debug("Running PYTHONSTARTUP file %s...", python_startup)
 421              try:
 422                  self._exec_file(python_startup)
 423              except:
 424                  self.log.warning("Unknown error in handling PYTHONSTARTUP file %s:", python_startup)
 425                  self.shell.showtraceback()
 426          for startup_dir in startup_dirs[::-1]:
 427              startup_files += glob.glob(os.path.join(startup_dir, '*.py'))
 428              startup_files += glob.glob(os.path.join(startup_dir, '*.ipy'))
 429          if not startup_files:
 430              return
 431  
 432          self.log.debug("Running startup files from %s...", startup_dir)
 433          try:
 434              for fname in sorted(startup_files):
 435                  self._exec_file(fname)
 436          except:
 437              self.log.warning("Unknown error in handling startup files:")
 438              self.shell.showtraceback()
 439  
 440      def _run_exec_files(self):
 441          """Run files from IPythonApp.exec_files"""
 442          if not self.exec_files:
 443              return
 444  
 445          self.log.debug("Running files in IPythonApp.exec_files...")
 446          try:
 447              for fname in self.exec_files:
 448                  self._exec_file(fname)
 449          except:
 450              self.log.warning("Unknown error in handling IPythonApp.exec_files:")
 451              self.shell.showtraceback()
 452  
 453      def _run_cmd_line_code(self):
 454          """Run code or file specified at the command-line"""
 455          if self.code_to_run:
 456              line = self.code_to_run
 457              try:
 458                  self.log.info("Running code given at command line (c=): %s" %
 459                                line)
 460                  self.shell.run_cell(line, store_history=False)
 461              except:
 462                  self.log.warning("Error in executing line in user namespace: %s" %
 463                                line)
 464                  self.shell.showtraceback()
 465                  if not self.interact:
 466                      self.exit(1)
 467  
 468          # Like Python itself, ignore the second if the first of these is present
 469          elif self.file_to_run:
 470              fname = self.file_to_run
 471              if os.path.isdir(fname):
 472                  fname = os.path.join(fname, "__main__.py")
 473              if not os.path.exists(fname):
 474                  self.log.warning("File '%s' doesn't exist", fname)
 475                  if not self.interact:
 476                      self.exit(2)
 477              try:
 478                  self._exec_file(fname, shell_futures=True)
 479              except:
 480                  self.shell.showtraceback(tb_offset=4)
 481                  if not self.interact:
 482                      self.exit(1)
 483  
 484      def _run_module(self):
 485          """Run module specified at the command-line."""
 486          if self.module_to_run:
 487              # Make sure that the module gets a proper sys.argv as if it were
 488              # run using `python -m`.
 489              save_argv = sys.argv
 490              sys.argv = [sys.executable] + self.extra_args
 491              try:
 492                  self.shell.safe_run_module(self.module_to_run,
 493                                             self.shell.user_ns)
 494              finally:
 495                  sys.argv = save_argv
 496