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