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