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