1"""The IPython kernel implementation"""
2
3import asyncio
4import builtins
5from contextlib import contextmanager
6from functools import partial
7import getpass
8import signal
9import sys
10
11from IPython.core import release
12from ipython_genutils.py3compat import safe_unicode
13from IPython.utils.tokenutil import token_at_cursor, line_at_cursor
14from tornado import gen
15from traitlets import Instance, Type, Any, List, Bool, observe, observe_compat
16
17from .comm import CommManager
18from .kernelbase import Kernel as KernelBase
19from .zmqshell import ZMQInteractiveShell
20from .eventloops import _use_appnope
21
22try:
23    from IPython.core.interactiveshell import _asyncio_runner
24except ImportError:
25    _asyncio_runner = None
26
27try:
28    from IPython.core.completer import rectify_completions as _rectify_completions, provisionalcompleter as _provisionalcompleter
29    _use_experimental_60_completion = True
30except ImportError:
31    _use_experimental_60_completion = False
32
33_EXPERIMENTAL_KEY_NAME = '_jupyter_types_experimental'
34
35
36class IPythonKernel(KernelBase):
37    shell = Instance('IPython.core.interactiveshell.InteractiveShellABC',
38                     allow_none=True)
39    shell_class = Type(ZMQInteractiveShell)
40
41    use_experimental_completions = Bool(True,
42        help="Set this flag to False to deactivate the use of experimental IPython completion APIs.",
43    ).tag(config=True)
44
45    user_module = Any()
46    @observe('user_module')
47    @observe_compat
48    def _user_module_changed(self, change):
49        if self.shell is not None:
50            self.shell.user_module = change['new']
51
52    user_ns = Instance(dict, args=None, allow_none=True)
53    @observe('user_ns')
54    @observe_compat
55    def _user_ns_changed(self, change):
56        if self.shell is not None:
57            self.shell.user_ns = change['new']
58            self.shell.init_user_ns()
59
60    # A reference to the Python builtin 'raw_input' function.
61    # (i.e., __builtin__.raw_input for Python 2.7, builtins.input for Python 3)
62    _sys_raw_input = Any()
63    _sys_eval_input = Any()
64
65    def __init__(self, **kwargs):
66        super(IPythonKernel, self).__init__(**kwargs)
67
68        # Initialize the InteractiveShell subclass
69        self.shell = self.shell_class.instance(parent=self,
70            profile_dir = self.profile_dir,
71            user_module = self.user_module,
72            user_ns     = self.user_ns,
73            kernel      = self,
74        )
75        self.shell.displayhook.session = self.session
76        self.shell.displayhook.pub_socket = self.iopub_socket
77        self.shell.displayhook.topic = self._topic('execute_result')
78        self.shell.display_pub.session = self.session
79        self.shell.display_pub.pub_socket = self.iopub_socket
80
81        self.comm_manager = CommManager(parent=self, kernel=self)
82
83        self.shell.configurables.append(self.comm_manager)
84        comm_msg_types = [ 'comm_open', 'comm_msg', 'comm_close' ]
85        for msg_type in comm_msg_types:
86            self.shell_handlers[msg_type] = getattr(self.comm_manager, msg_type)
87
88        if _use_appnope() and self._darwin_app_nap:
89            # Disable app-nap as the kernel is not a gui but can have guis
90            import appnope
91            appnope.nope()
92
93    help_links = List([
94        {
95            'text': "Python Reference",
96            'url': "https://docs.python.org/%i.%i" % sys.version_info[:2],
97        },
98        {
99            'text': "IPython Reference",
100            'url': "https://ipython.org/documentation.html",
101        },
102        {
103            'text': "NumPy Reference",
104            'url': "https://docs.scipy.org/doc/numpy/reference/",
105        },
106        {
107            'text': "SciPy Reference",
108            'url': "https://docs.scipy.org/doc/scipy/reference/",
109        },
110        {
111            'text': "Matplotlib Reference",
112            'url': "https://matplotlib.org/contents.html",
113        },
114        {
115            'text': "SymPy Reference",
116            'url': "http://docs.sympy.org/latest/index.html",
117        },
118        {
119            'text': "pandas Reference",
120            'url': "https://pandas.pydata.org/pandas-docs/stable/",
121        },
122    ]).tag(config=True)
123
124    # Kernel info fields
125    implementation = 'ipython'
126    implementation_version = release.version
127    language_info = {
128        'name': 'python',
129        'version': sys.version.split()[0],
130        'mimetype': 'text/x-python',
131        'codemirror_mode': {
132            'name': 'ipython',
133            'version': sys.version_info[0]
134        },
135        'pygments_lexer': 'ipython%d' % 3,
136        'nbconvert_exporter': 'python',
137        'file_extension': '.py'
138    }
139
140    @property
141    def banner(self):
142        return self.shell.banner
143
144    def start(self):
145        self.shell.exit_now = False
146        super(IPythonKernel, self).start()
147
148    def set_parent(self, ident, parent):
149        """Overridden from parent to tell the display hook and output streams
150        about the parent message.
151        """
152        super(IPythonKernel, self).set_parent(ident, parent)
153        self.shell.set_parent(parent)
154
155    def init_metadata(self, parent):
156        """Initialize metadata.
157
158        Run at the beginning of each execution request.
159        """
160        md = super(IPythonKernel, self).init_metadata(parent)
161        # FIXME: remove deprecated ipyparallel-specific code
162        # This is required for ipyparallel < 5.0
163        md.update({
164            'dependencies_met' : True,
165            'engine' : self.ident,
166        })
167        return md
168
169    def finish_metadata(self, parent, metadata, reply_content):
170        """Finish populating metadata.
171
172        Run after completing an execution request.
173        """
174        # FIXME: remove deprecated ipyparallel-specific code
175        # This is required by ipyparallel < 5.0
176        metadata['status'] = reply_content['status']
177        if reply_content['status'] == 'error' and reply_content['ename'] == 'UnmetDependency':
178                metadata['dependencies_met'] = False
179
180        return metadata
181
182    def _forward_input(self, allow_stdin=False):
183        """Forward raw_input and getpass to the current frontend.
184
185        via input_request
186        """
187        self._allow_stdin = allow_stdin
188
189        self._sys_raw_input = builtins.input
190        builtins.input = self.raw_input
191
192        self._save_getpass = getpass.getpass
193        getpass.getpass = self.getpass
194
195    def _restore_input(self):
196        """Restore raw_input, getpass"""
197        builtins.input = self._sys_raw_input
198
199        getpass.getpass = self._save_getpass
200
201    @property
202    def execution_count(self):
203        return self.shell.execution_count
204
205    @execution_count.setter
206    def execution_count(self, value):
207        # Ignore the incrementing done by KernelBase, in favour of our shell's
208        # execution counter.
209        pass
210
211    @contextmanager
212    def _cancel_on_sigint(self, future):
213        """ContextManager for capturing SIGINT and cancelling a future
214
215        SIGINT raises in the event loop when running async code,
216        but we want it to halt a coroutine.
217
218        Ideally, it would raise KeyboardInterrupt,
219        but this turns it into a CancelledError.
220        At least it gets a decent traceback to the user.
221        """
222        sigint_future = asyncio.Future()
223
224        # whichever future finishes first,
225        # cancel the other one
226        def cancel_unless_done(f, _ignored):
227            if f.cancelled() or f.done():
228                return
229            f.cancel()
230
231        # when sigint finishes,
232        # abort the coroutine with CancelledError
233        sigint_future.add_done_callback(
234            partial(cancel_unless_done, future)
235        )
236        # when the main future finishes,
237        # stop watching for SIGINT events
238        future.add_done_callback(
239            partial(cancel_unless_done, sigint_future)
240        )
241
242        def handle_sigint(*args):
243            def set_sigint_result():
244                if sigint_future.cancelled() or sigint_future.done():
245                    return
246                sigint_future.set_result(1)
247            # use add_callback for thread safety
248            self.io_loop.add_callback(set_sigint_result)
249
250        # set the custom sigint hander during this context
251        save_sigint = signal.signal(signal.SIGINT, handle_sigint)
252        try:
253            yield
254        finally:
255            # restore the previous sigint handler
256            signal.signal(signal.SIGINT, save_sigint)
257
258    @gen.coroutine
259    def do_execute(self, code, silent, store_history=True,
260                   user_expressions=None, allow_stdin=False):
261        shell = self.shell # we'll need this a lot here
262
263        self._forward_input(allow_stdin)
264
265        reply_content = {}
266        if hasattr(shell, 'run_cell_async') and hasattr(shell, 'should_run_async'):
267            run_cell = shell.run_cell_async
268            should_run_async = shell.should_run_async
269        else:
270            should_run_async = lambda cell: False
271            # older IPython,
272            # use blocking run_cell and wrap it in coroutine
273            @gen.coroutine
274            def run_cell(*args, **kwargs):
275                return shell.run_cell(*args, **kwargs)
276        try:
277
278            # default case: runner is asyncio and asyncio is already running
279            # TODO: this should check every case for "are we inside the runner",
280            # not just asyncio
281            if (
282                _asyncio_runner
283                and should_run_async(code)
284                and shell.loop_runner is _asyncio_runner
285                and asyncio.get_event_loop().is_running()
286            ):
287                coro = run_cell(code, store_history=store_history, silent=silent)
288                coro_future = asyncio.ensure_future(coro)
289
290                with self._cancel_on_sigint(coro_future):
291                    res = None
292                    try:
293                        res = yield coro_future
294                    finally:
295                        shell.events.trigger('post_execute')
296                        if not silent:
297                            shell.events.trigger('post_run_cell', res)
298            else:
299                # runner isn't already running,
300                # make synchronous call,
301                # letting shell dispatch to loop runners
302                res = shell.run_cell(code, store_history=store_history, silent=silent)
303        finally:
304            self._restore_input()
305
306        if res.error_before_exec is not None:
307            err = res.error_before_exec
308        else:
309            err = res.error_in_exec
310
311        if res.success:
312            reply_content['status'] = 'ok'
313        else:
314            reply_content['status'] = 'error'
315
316            reply_content.update({
317                'traceback': shell._last_traceback or [],
318                'ename': str(type(err).__name__),
319                'evalue': safe_unicode(err),
320            })
321
322            # FIXME: deprecated piece for ipyparallel (remove in 5.0):
323            e_info = dict(engine_uuid=self.ident, engine_id=self.int_id,
324                          method='execute')
325            reply_content['engine_info'] = e_info
326
327
328        # Return the execution counter so clients can display prompts
329        reply_content['execution_count'] = shell.execution_count - 1
330
331        if 'traceback' in reply_content:
332            self.log.info("Exception in execute request:\n%s", '\n'.join(reply_content['traceback']))
333
334
335        # At this point, we can tell whether the main code execution succeeded
336        # or not.  If it did, we proceed to evaluate user_expressions
337        if reply_content['status'] == 'ok':
338            reply_content['user_expressions'] = \
339                         shell.user_expressions(user_expressions or {})
340        else:
341            # If there was an error, don't even try to compute expressions
342            reply_content['user_expressions'] = {}
343
344        # Payloads should be retrieved regardless of outcome, so we can both
345        # recover partial output (that could have been generated early in a
346        # block, before an error) and always clear the payload system.
347        reply_content['payload'] = shell.payload_manager.read_payload()
348        # Be aggressive about clearing the payload because we don't want
349        # it to sit in memory until the next execute_request comes in.
350        shell.payload_manager.clear_payload()
351
352        return reply_content
353
354    def do_complete(self, code, cursor_pos):
355        if _use_experimental_60_completion and self.use_experimental_completions:
356            return self._experimental_do_complete(code, cursor_pos)
357
358        # FIXME: IPython completers currently assume single line,
359        # but completion messages give multi-line context
360        # For now, extract line from cell, based on cursor_pos:
361        if cursor_pos is None:
362            cursor_pos = len(code)
363        line, offset = line_at_cursor(code, cursor_pos)
364        line_cursor = cursor_pos - offset
365
366        txt, matches = self.shell.complete('', line, line_cursor)
367        return {'matches' : matches,
368                'cursor_end' : cursor_pos,
369                'cursor_start' : cursor_pos - len(txt),
370                'metadata' : {},
371                'status' : 'ok'}
372
373    def _experimental_do_complete(self, code, cursor_pos):
374        """
375        Experimental completions from IPython, using Jedi.
376        """
377        if cursor_pos is None:
378            cursor_pos = len(code)
379        with _provisionalcompleter():
380            raw_completions = self.shell.Completer.completions(code, cursor_pos)
381            completions = list(_rectify_completions(code, raw_completions))
382
383            comps = []
384            for comp in completions:
385                comps.append(dict(
386                            start=comp.start,
387                            end=comp.end,
388                            text=comp.text,
389                            type=comp.type,
390                ))
391
392        if completions:
393            s = completions[0].start
394            e = completions[0].end
395            matches = [c.text for c in completions]
396        else:
397            s = cursor_pos
398            e = cursor_pos
399            matches = []
400
401        return {'matches': matches,
402                'cursor_end': e,
403                'cursor_start': s,
404                'metadata': {_EXPERIMENTAL_KEY_NAME: comps},
405                'status': 'ok'}
406
407
408
409    def do_inspect(self, code, cursor_pos, detail_level=0):
410        name = token_at_cursor(code, cursor_pos)
411
412        reply_content = {'status' : 'ok'}
413        reply_content['data'] = {}
414        reply_content['metadata'] = {}
415        try:
416            reply_content['data'].update(
417                self.shell.object_inspect_mime(
418                    name,
419                    detail_level=detail_level
420                )
421            )
422            if not self.shell.enable_html_pager:
423                reply_content['data'].pop('text/html')
424            reply_content['found'] = True
425        except KeyError:
426            reply_content['found'] = False
427
428        return reply_content
429
430    def do_history(self, hist_access_type, output, raw, session=0, start=0,
431                   stop=None, n=None, pattern=None, unique=False):
432        if hist_access_type == 'tail':
433            hist = self.shell.history_manager.get_tail(n, raw=raw, output=output,
434                                                            include_latest=True)
435
436        elif hist_access_type == 'range':
437            hist = self.shell.history_manager.get_range(session, start, stop,
438                                                        raw=raw, output=output)
439
440        elif hist_access_type == 'search':
441            hist = self.shell.history_manager.search(
442                pattern, raw=raw, output=output, n=n, unique=unique)
443        else:
444            hist = []
445
446        return {
447            'status': 'ok',
448            'history' : list(hist),
449        }
450
451    def do_shutdown(self, restart):
452        self.shell.exit_now = True
453        return dict(status='ok', restart=restart)
454
455    def do_is_complete(self, code):
456        transformer_manager = getattr(self.shell, 'input_transformer_manager', None)
457        if transformer_manager is None:
458            # input_splitter attribute is deprecated
459            transformer_manager = self.shell.input_splitter
460        status, indent_spaces = transformer_manager.check_complete(code)
461        r = {'status': status}
462        if status == 'incomplete':
463            r['indent'] = ' ' * indent_spaces
464        return r
465
466    def do_apply(self, content, bufs, msg_id, reply_metadata):
467        from .serialize import serialize_object, unpack_apply_message
468        shell = self.shell
469        try:
470            working = shell.user_ns
471
472            prefix = "_"+str(msg_id).replace("-","")+"_"
473
474            f,args,kwargs = unpack_apply_message(bufs, working, copy=False)
475
476            fname = getattr(f, '__name__', 'f')
477
478            fname = prefix+"f"
479            argname = prefix+"args"
480            kwargname = prefix+"kwargs"
481            resultname = prefix+"result"
482
483            ns = { fname : f, argname : args, kwargname : kwargs , resultname : None }
484            # print ns
485            working.update(ns)
486            code = "%s = %s(*%s,**%s)" % (resultname, fname, argname, kwargname)
487            try:
488                exec(code, shell.user_global_ns, shell.user_ns)
489                result = working.get(resultname)
490            finally:
491                for key in ns:
492                    working.pop(key)
493
494            result_buf = serialize_object(result,
495                buffer_threshold=self.session.buffer_threshold,
496                item_threshold=self.session.item_threshold,
497            )
498
499        except BaseException as e:
500            # invoke IPython traceback formatting
501            shell.showtraceback()
502            reply_content = {
503                'traceback': shell._last_traceback or [],
504                'ename': str(type(e).__name__),
505                'evalue': safe_unicode(e),
506            }
507            # FIXME: deprecated piece for ipyparallel (remove in 5.0):
508            e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='apply')
509            reply_content['engine_info'] = e_info
510
511            self.send_response(self.iopub_socket, 'error', reply_content,
512                               ident=self._topic('error'))
513            self.log.info("Exception in apply request:\n%s", '\n'.join(reply_content['traceback']))
514            result_buf = []
515            reply_content['status'] = 'error'
516        else:
517            reply_content = {'status' : 'ok'}
518
519        return reply_content, result_buf
520
521    def do_clear(self):
522        self.shell.reset(False)
523        return dict(status='ok')
524
525
526# This exists only for backwards compatibility - use IPythonKernel instead
527
528class Kernel(IPythonKernel):
529    def __init__(self, *args, **kwargs):
530        import warnings
531        warnings.warn('Kernel is a deprecated alias of ipykernel.ipkernel.IPythonKernel',
532                      DeprecationWarning)
533        super(Kernel, self).__init__(*args, **kwargs)
534