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 Unless disabled by ignore_cwd config or sys.flags.safe_path.
251
252 Unlike Python's default, we insert before the first `site-packages`
253 or `dist-packages` directory,
254 so that it is after the standard library.
255
256 .. versionchanged:: 7.2
257 Try to insert after the standard library, instead of first.
258 .. versionchanged:: 8.0
259 Allow optionally not including the current directory in sys.path
260 .. versionchanged:: 9.7
261 Respect sys.flags.safe_path (PYTHONSAFEPATH and -P flag)
262 """
263 if "" in sys.path or self.ignore_cwd or sys.flags.safe_path:
264 return
265 for idx, path in enumerate(sys.path):
266 parent, last_part = os.path.split(path)
267 if last_part in {'site-packages', 'dist-packages'}:
268 break
269 else:
270 # no site-packages or dist-packages found (?!)
271 # back to original behavior of inserting at the front
272 idx = 0
273 sys.path.insert(idx, '')
274
275 def init_shell(self):
276 raise NotImplementedError("Override in subclasses")
277
278 def init_gui_pylab(self):
279 """Enable GUI event loop integration, taking pylab into account."""
280 enable = False
281 shell = self.shell
282 if self.pylab:
283 enable = lambda key: shell.enable_pylab(key, import_all=self.pylab_import_all)
284 key = self.pylab
285 elif self.matplotlib:
286 enable = shell.enable_matplotlib
287 key = self.matplotlib
288 elif self.gui:
289 enable = shell.enable_gui
290 key = self.gui
291
292 if not enable:
293 return
294
295 try:
296 r = enable(key)
297 except ImportError:
298 self.log.warning("Eventloop or matplotlib integration failed. Is matplotlib installed?")
299 self.shell.showtraceback()
300 return
301 except Exception:
302 self.log.warning("GUI event loop or pylab initialization failed")
303 self.shell.showtraceback()
304 return
305
306 if isinstance(r, tuple):
307 gui, backend = r[:2]
308 self.log.info("Enabling GUI event loop integration, "
309 "eventloop=%s, matplotlib=%s", gui, backend)
310 if key == "auto":
311 print("Using matplotlib backend: %s" % backend)
312 else:
313 gui = r
314 self.log.info("Enabling GUI event loop integration, "
315 "eventloop=%s", gui)
316
317 def init_extensions(self):
318 """Load all IPython extensions in IPythonApp.extensions.
319
320 This uses the :meth:`ExtensionManager.load_extensions` to load all
321 the extensions listed in ``self.extensions``.
322 """
323 try:
324 self.log.debug("Loading IPython extensions...")
325 extensions = (
326 self.default_extensions + self.extensions + self.extra_extensions
327 )
328 for ext in extensions:
329 try:
330 self.log.info("Loading IPython extension: %s", ext)
331 self.shell.extension_manager.load_extension(ext)
332 except:
333 if self.reraise_ipython_extension_failures:
334 raise
335 msg = ("Error in loading extension: {ext}\n"
336 "Check your config files in {location}".format(
337 ext=ext,
338 location=self.profile_dir.location
339 ))
340 self.log.warning(msg, exc_info=True)
341 except:
342 if self.reraise_ipython_extension_failures:
343 raise
344 self.log.warning("Unknown error in loading extensions:", exc_info=True)
345
346 def init_code(self):
347 """run the pre-flight code, specified via exec_lines"""
348 self._run_startup_files()
349 self._run_exec_lines()
350 self._run_exec_files()
351
352 # Hide variables defined here from %who etc.
353 if self.hide_initial_ns:
354 self.shell.user_ns_hidden.update(self.shell.user_ns)
355
356 # command-line execution (ipython -i script.py, ipython -m module)
357 # should *not* be excluded from %whos
358 self._run_cmd_line_code()
359 self._run_module()
360
361 # flush output, so itwon't be attached to the first cell
362 sys.stdout.flush()
363 sys.stderr.flush()
364 self.shell._sys_modules_keys = set(sys.modules.keys())
365
366 def _run_exec_lines(self):
367 """Run lines of code in IPythonApp.exec_lines in the user's namespace."""
368 if not self.exec_lines:
369 return
370 try:
371 self.log.debug("Running code from IPythonApp.exec_lines...")
372 for line in self.exec_lines:
373 try:
374 self.log.info("Running code in user namespace: %s" %
375 line)
376 self.shell.run_cell(line, store_history=False)
377 except:
378 self.log.warning("Error in executing line in user "
379 "namespace: %s" % line)
380 self.shell.showtraceback()
381 except:
382 self.log.warning("Unknown error in handling IPythonApp.exec_lines:")
383 self.shell.showtraceback()
384
385 def _exec_file(self, fname, shell_futures=False):
386 try:
387 full_filename = filefind(fname, [u'.', self.ipython_dir])
388 except IOError:
389 self.log.warning("File not found: %r"%fname)
390 return
391 # Make sure that the running script gets a proper sys.argv as if it
392 # were run from a system shell.
393 save_argv = sys.argv
394 sys.argv = [full_filename] + self.extra_args[1:]
395 try:
396 if os.path.isfile(full_filename):
397 self.log.info("Running file in user namespace: %s" %
398 full_filename)
399 # Ensure that __file__ is always defined to match Python
400 # behavior.
401 with preserve_keys(self.shell.user_ns, '__file__'):
402 self.shell.user_ns['__file__'] = fname
403 if full_filename.endswith('.ipy') or full_filename.endswith('.ipynb'):
404 self.shell.safe_execfile_ipy(full_filename,
405 shell_futures=shell_futures)
406 else:
407 # default to python, even without extension
408 self.shell.safe_execfile(full_filename,
409 self.shell.user_ns,
410 shell_futures=shell_futures,
411 raise_exceptions=True)
412 finally:
413 sys.argv = save_argv
414
415 def _run_startup_files(self):
416 """Run files from profile startup directory"""
417 startup_dirs = [self.profile_dir.startup_dir] + [
418 os.path.join(p, 'startup') for p in chain(ENV_CONFIG_DIRS, SYSTEM_CONFIG_DIRS)
419 ]
420 startup_files = []
421
422 if self.exec_PYTHONSTARTUP and os.environ.get('PYTHONSTARTUP', False) and \
423 not (self.file_to_run or self.code_to_run or self.module_to_run):
424 python_startup = os.environ['PYTHONSTARTUP']
425 self.log.debug("Running PYTHONSTARTUP file %s...", python_startup)
426 try:
427 self._exec_file(python_startup)
428 except:
429 self.log.warning("Unknown error in handling PYTHONSTARTUP file %s:", python_startup)
430 self.shell.showtraceback()
431 for startup_dir in startup_dirs[::-1]:
432 startup_files += glob.glob(os.path.join(startup_dir, '*.py'))
433 startup_files += glob.glob(os.path.join(startup_dir, '*.ipy'))
434 if not startup_files:
435 return
436
437 self.log.debug("Running startup files from %s...", startup_dir)
438 try:
439 for fname in sorted(startup_files):
440 self._exec_file(fname)
441 except:
442 self.log.warning("Unknown error in handling startup files:")
443 self.shell.showtraceback()
444
445 def _run_exec_files(self):
446 """Run files from IPythonApp.exec_files"""
447 if not self.exec_files:
448 return
449
450 self.log.debug("Running files in IPythonApp.exec_files...")
451 try:
452 for fname in self.exec_files:
453 self._exec_file(fname)
454 except:
455 self.log.warning("Unknown error in handling IPythonApp.exec_files:")
456 self.shell.showtraceback()
457
458 def _run_cmd_line_code(self):
459 """Run code or file specified at the command-line"""
460 if self.code_to_run:
461 line = self.code_to_run
462 try:
463 self.log.info("Running code given at command line (c=): %s" %
464 line)
465 self.shell.run_cell(line, store_history=False)
466 except:
467 self.log.warning("Error in executing line in user namespace: %s" %
468 line)
469 self.shell.showtraceback()
470 if not self.interact:
471 self.exit(1)
472
473 # Like Python itself, ignore the second if the first of these is present
474 elif self.file_to_run:
475 fname = self.file_to_run
476 if os.path.isdir(fname):
477 fname = os.path.join(fname, "__main__.py")
478 if not os.path.exists(fname):
479 self.log.warning("File '%s' doesn't exist", fname)
480 if not self.interact:
481 self.exit(2)
482 try:
483 self._exec_file(fname, shell_futures=True)
484 except:
485 self.shell.showtraceback(tb_offset=4)
486 if not self.interact:
487 self.exit(1)
488
489 def _run_module(self):
490 """Run module specified at the command-line."""
491 if self.module_to_run:
492 # Make sure that the module gets a proper sys.argv as if it were
493 # run using `python -m`.
494 save_argv = sys.argv
495 sys.argv = [sys.executable] + self.extra_args
496 try:
497 self.shell.safe_run_module(self.module_to_run,
498 self.shell.user_ns)
499 finally:
500 sys.argv = save_argv