diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 97a8d44..8e3ecc7 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -71,11 +71,10 @@ from IPython.core.error import TryNext from IPython.core.inputsplitter import ESC_MAGIC from IPython.core.latex_symbols import latex_symbols, reverse_latex_symbol from IPython.utils import generics -from IPython.utils import io from IPython.utils.decorators import undoc from IPython.utils.dir2 import dir2, get_real_method from IPython.utils.process import arg_split -from IPython.utils.py3compat import builtin_mod, string_types, PY3 +from IPython.utils.py3compat import builtin_mod, string_types, PY3, cast_unicode_py2 from traitlets import CBool, Enum try: @@ -201,6 +200,9 @@ def completions_sorting_key(word): elif word.startswith('_'): prio1 = 1 + if word.endswith('='): + prio1 = -1 + if word.startswith('%%'): # If there's another % in there, this is something else, so leave it alone if not "%" in word[2:]: @@ -358,7 +360,7 @@ class Completer(Configurable): for word in lst: if word[:n] == text and word != "__builtins__": match_append(word) - return matches + return [cast_unicode_py2(m) for m in matches] def attr_matches(self, text): """Compute matches when text contains a dot. @@ -410,8 +412,7 @@ class Completer(Configurable): pass # Build match list to return n = len(attr) - res = ["%s.%s" % (expr, w) for w in words if w[:n] == attr ] - return res + return [u"%s.%s" % (expr, w) for w in words if w[:n] == attr ] def get__all__entries(obj): @@ -421,7 +422,7 @@ def get__all__entries(obj): except: return [] - return [w for w in words if isinstance(w, string_types)] + return [cast_unicode_py2(w) for w in words if isinstance(w, string_types)] def match_dict_keys(keys, prefix, delims): @@ -695,9 +696,9 @@ class IPCompleter(Completer): # when escaped with backslash if text.startswith('!'): text = text[1:] - text_prefix = '!' + text_prefix = u'!' else: - text_prefix = '' + text_prefix = u'' text_until_cursor = self.text_until_cursor # track strings with open quotes @@ -728,7 +729,7 @@ class IPCompleter(Completer): text = os.path.expanduser(text) if text == "": - return [text_prefix + protect_filename(f) for f in self.glob("*")] + return [text_prefix + cast_unicode_py2(protect_filename(f)) for f in self.glob("*")] # Compute the matches from the filesystem m0 = self.clean_glob(text.replace('\\','')) @@ -751,8 +752,7 @@ class IPCompleter(Completer): protect_filename(f) for f in m0] # Mark directories in input list by appending '/' to their names. - matches = [x+'/' if os.path.isdir(x) else x for x in matches] - return matches + return [cast_unicode_py2(x+'/') if os.path.isdir(x) else x for x in matches] def magic_matches(self, text): """Match magics""" @@ -773,7 +773,7 @@ class IPCompleter(Completer): comp = [ pre2+m for m in cell_magics if m.startswith(bare_text)] if not text.startswith(pre2): comp += [ pre+m for m in line_magics if m.startswith(bare_text)] - return comp + return [cast_unicode_py2(c) for c in comp] def python_jedi_matches(self, text, line_buffer, cursor_pos): """Match attributes or global Python names using Jedi.""" @@ -849,7 +849,6 @@ class IPCompleter(Completer): matches = [] else: matches = self.global_matches(text) - return matches def _default_arguments_from_docstring(self, doc): @@ -978,7 +977,7 @@ class IPCompleter(Completer): for namedArg in namedArgs: if namedArg.startswith(text): - argMatches.append("%s=" %namedArg) + argMatches.append(u"%s=" %namedArg) return argMatches def dict_key_matches(self, text): @@ -1164,12 +1163,12 @@ class IPCompleter(Completer): res = c(event) if res: # first, try case sensitive match - withcase = [r for r in res if r.startswith(text)] + withcase = [cast_unicode_py2(r) for r in res if r.startswith(text)] if withcase: return withcase # if none, then case insensitive ones are ok too text_low = text.lower() - return [r for r in res if r.lower().startswith(text_low)] + return [cast_unicode_py2(r) for r in res if r.lower().startswith(text_low)] except TryNext: pass diff --git a/IPython/core/formatters.py b/IPython/core/formatters.py index 700a903..62d296f 100644 --- a/IPython/core/formatters.py +++ b/IPython/core/formatters.py @@ -944,7 +944,7 @@ def format_display_data(obj, include=None, exclude=None): """ from IPython.core.interactiveshell import InteractiveShell - InteractiveShell.instance().display_formatter.format( + return InteractiveShell.instance().display_formatter.format( obj, include, exclude diff --git a/IPython/core/history.py b/IPython/core/history.py index fc79642..be0afc2 100644 --- a/IPython/core/history.py +++ b/IPython/core/history.py @@ -1,18 +1,10 @@ """ History related magics and functionality """ -#----------------------------------------------------------------------------- -# Copyright (C) 2010-2011 The IPython Development Team. -# -# Distributed under the terms of the BSD License. -# -# The full license is in the file COPYING.txt, distributed with this software. -#----------------------------------------------------------------------------- -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + from __future__ import print_function -# Stdlib imports import atexit import datetime import os @@ -26,7 +18,6 @@ except ImportError: sqlite3 = None import threading -# Our own packages from traitlets.config.configurable import LoggingConfigurable from decorator import decorator from IPython.utils.decorators import undoc @@ -80,27 +71,52 @@ else: class OperationalError(Exception): "Dummy exception when sqlite could not be imported. Should never occur." +# use 16kB as threshold for whether a corrupt history db should be saved +# that should be at least 100 entries or so +_SAVE_DB_SIZE = 16384 + @decorator def catch_corrupt_db(f, self, *a, **kw): """A decorator which wraps HistoryAccessor method calls to catch errors from a corrupt SQLite database, move the old database out of the way, and create a new one. + + We avoid clobbering larger databases because this may be triggered due to filesystem issues, + not just a corrupt file. """ try: return f(self, *a, **kw) - except (DatabaseError, OperationalError): - if os.path.isfile(self.hist_file): - # Try to move the file out of the way - base,ext = os.path.splitext(self.hist_file) - newpath = base + '-corrupt' + ext - os.rename(self.hist_file, newpath) + except (DatabaseError, OperationalError) as e: + self._corrupt_db_counter += 1 + self.log.error("Failed to open SQLite history %s (%s).", self.hist_file, e) + if self.hist_file != ':memory:': + if self._corrupt_db_counter > self._corrupt_db_limit: + self.hist_file = ':memory:' + self.log.error("Failed to load history too many times, history will not be saved.") + elif os.path.isfile(self.hist_file): + # move the file out of the way + base, ext = os.path.splitext(self.hist_file) + size = os.stat(self.hist_file).st_size + if size >= _SAVE_DB_SIZE: + # if there's significant content, avoid clobbering + now = datetime.datetime.now().isoformat().replace(':', '.') + newpath = base + '-corrupt-' + now + ext + # don't clobber previous corrupt backups + for i in range(100): + if not os.path.isfile(newpath): + break + else: + newpath = base + '-corrupt-' + now + (u'-%i' % i) + ext + else: + # not much content, possibly empty; don't worry about clobbering + # maybe we should just delete it? + newpath = base + '-corrupt' + ext + os.rename(self.hist_file, newpath) + self.log.error("History file was moved to %s and a new file created.", newpath) self.init_db() - print("ERROR! History file wasn't a valid SQLite database.", - "It was moved to %s" % newpath, "and a new file created.") return [] - else: - # The hist_file is probably :memory: or something else. + # Failed with :memory:, something serious is wrong raise class HistoryAccessorBase(LoggingConfigurable): @@ -126,6 +142,11 @@ class HistoryAccessor(HistoryAccessorBase): This is intended for use by standalone history tools. IPython shells use HistoryManager, below, which is a subclass of this.""" + # counter for init_db retries, so we don't keep trying over and over + _corrupt_db_counter = 0 + # after two failures, fallback on :memory: + _corrupt_db_limit = 2 + # String holding the path to the history file hist_file = Unicode(config=True, help="""Path to file to use for SQLite history database. @@ -239,6 +260,8 @@ class HistoryAccessor(HistoryAccessorBase): (session integer, line integer, output text, PRIMARY KEY (session, line))""") self.db.commit() + # success! reset corrupt db count + self._corrupt_db_counter = 0 def writeout_cache(self): """Overridden by HistoryManager to dump the cache before certain diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 738f1d4..04536a9 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -57,7 +57,7 @@ from IPython.core.prefilter import PrefilterManager from IPython.core.profiledir import ProfileDir from IPython.core.prompts import PromptManager from IPython.core.usage import default_banner -from IPython.testing.skipdoctest import skip_doctest +from IPython.testing.skipdoctest import skip_doctest_py2, skip_doctest from IPython.utils import PyColorize from IPython.utils import io from IPython.utils import py3compat @@ -1939,6 +1939,7 @@ class InteractiveShell(SingletonConfigurable): self.set_hook('complete_command', reset_completer, str_key = '%reset') + @skip_doctest_py2 def complete(self, text, line=None, cursor_pos=None): """Return the completed text and a list of completions. diff --git a/IPython/core/magics/execution.py b/IPython/core/magics/execution.py index 4dd208b..4edadc9 100644 --- a/IPython/core/magics/execution.py +++ b/IPython/core/magics/execution.py @@ -61,12 +61,12 @@ class TimeitResult(object): """ Object returned by the timeit magic with info about the run. - Contain the following attributes : + Contains the following attributes : - loops: (int) number of loop done per measurement - repeat: (int) number of time the mesurement has been repeated - best: (float) best execusion time / number - all_runs: (list of float) execusion time of each run (in s) + loops: (int) number of loops done per measurement + repeat: (int) number of times the measurement has been repeated + best: (float) best execution time / number + all_runs: (list of float) execution time of each run (in s) compile_time: (float) time of statement compilation (s) """ diff --git a/IPython/core/page.py b/IPython/core/page.py index b3f06e6..ec2e077 100644 --- a/IPython/core/page.py +++ b/IPython/core/page.py @@ -298,7 +298,7 @@ def get_pager_cmd(pager_cmd=None): Makes some attempts at finding an OS-correct one. """ if os.name == 'posix': - default_pager_cmd = 'less -r' # -r for color control sequences + default_pager_cmd = 'less -R' # -R for color control sequences elif os.name in ['nt','dos']: default_pager_cmd = 'type' @@ -308,8 +308,8 @@ def get_pager_cmd(pager_cmd=None): except: pager_cmd = default_pager_cmd - if pager_cmd == 'less' and '-r' not in os.environ.get('LESS', ''): - pager_cmd += ' -r' + if pager_cmd == 'less' and '-r' not in os.environ.get('LESS', '').lower(): + pager_cmd += ' -R' return pager_cmd diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index 91f06dd..b0f90ec 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -790,6 +790,7 @@ def test_nested_import_module_completer(): _, matches = ip.complete(None, 'import IPython.co', 17) nt.assert_in('IPython.core', matches) nt.assert_not_in('import IPython.core', matches) + nt.assert_not_in('IPython.display', matches) def test_import_module_completer(): ip = get_ipython() diff --git a/IPython/core/tests/test_ultratb.py b/IPython/core/tests/test_ultratb.py index 9d1f26a..27b6fc4 100644 --- a/IPython/core/tests/test_ultratb.py +++ b/IPython/core/tests/test_ultratb.py @@ -99,6 +99,21 @@ class NonAsciiTest(unittest.TestCase): with tt.AssertPrints("ZeroDivisionError"): with tt.AssertPrints(u'дбИЖ', suppress=False): ip.run_cell('fail()') + + def test_nonascii_msg(self): + cell = u"raise Exception('é')" + expected = u"Exception('é')" + ip.run_cell("%xmode plain") + with tt.AssertPrints(expected): + ip.run_cell(cell) + + ip.run_cell("%xmode verbose") + with tt.AssertPrints(expected): + ip.run_cell(cell) + + ip.run_cell("%xmode context") + with tt.AssertPrints(expected): + ip.run_cell(cell) class NestedGenExprTestCase(unittest.TestCase): diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py index b41bf4e..4ba08ba 100644 --- a/IPython/core/ultratb.py +++ b/IPython/core/ultratb.py @@ -709,10 +709,10 @@ class ListTB(TBTools): have_filedata = False Colors = self.Colors list = [] - stype = Colors.excName + etype.__name__ + Colors.Normal + stype = py3compat.cast_unicode(Colors.excName + etype.__name__ + Colors.Normal) if value is None: # Not sure if this can still happen in Python 2.6 and above - list.append(py3compat.cast_unicode(stype) + '\n') + list.append(stype + '\n') else: if issubclass(etype, SyntaxError): have_filedata = True @@ -752,10 +752,10 @@ class ListTB(TBTools): except Exception: s = self._some_str(value) if s: - list.append('%s%s:%s %s\n' % (str(stype), Colors.excName, + list.append('%s%s:%s %s\n' % (stype, Colors.excName, Colors.Normal, s)) else: - list.append('%s\n' % str(stype)) + list.append('%s\n' % stype) # sync with user hooks if have_filedata: @@ -793,9 +793,9 @@ class ListTB(TBTools): def _some_str(self, value): # Lifted from traceback.py try: - return str(value) + return py3compat.cast_unicode(str(value)) except: - return '' % type(value).__name__ + return u'' % type(value).__name__ #---------------------------------------------------------------------------- @@ -1432,6 +1432,7 @@ class SyntaxTB(ListTB): newtext = ulinecache.getline(value.filename, value.lineno) if newtext: value.text = newtext + self.last_syntax_error = value return super(SyntaxTB, self).structured_traceback(etype, value, elist, tb_offset=tb_offset, context=context) diff --git a/IPython/lib/display.py b/IPython/lib/display.py index b0176ff..63da80f 100644 --- a/IPython/lib/display.py +++ b/IPython/lib/display.py @@ -41,7 +41,7 @@ class Audio(DisplayObject): filename : unicode Path to a local file to load the data from. embed : boolean - Should the image data be embedded using a data URI (True) or should + Should the audio data be embedded using a data URI (True) or should the original source be referenced. Set this to True if you want the audio to playable later with no internet connection in the notebook. diff --git a/IPython/terminal/embed.py b/IPython/terminal/embed.py index 85dc9d1..aa43ca4 100644 --- a/IPython/terminal/embed.py +++ b/IPython/terminal/embed.py @@ -15,7 +15,7 @@ from IPython.core import ultratb, compilerop from IPython.core.magic import Magics, magics_class, line_magic from IPython.core.interactiveshell import DummyMod from IPython.core.interactiveshell import InteractiveShell -from IPython.terminal.interactiveshell import TerminalInteractiveShell +from IPython.terminal.ptshell import TerminalInteractiveShell from IPython.terminal.ipapp import load_default_config from traitlets import Bool, CBool, Unicode @@ -136,6 +136,9 @@ class InteractiveShellEmbed(TerminalInteractiveShell): else: self.old_banner2 = '' + if self.display_banner: + self.show_banner() + # Call the embedding code with a stack depth of 1 so it can skip over # our call and get the original caller's namespaces. self.mainloop(local_ns, module, stack_depth=stack_depth, @@ -182,6 +185,9 @@ class InteractiveShellEmbed(TerminalInteractiveShell): module = DummyMod() module.__dict__ = global_ns + if (display_banner is not None): + warnings.warn("The display_banner parameter is deprecated.", DeprecationWarning) + # Get locals and globals from caller if ((local_ns is None or module is None or compile_flags is None) and self.default_user_namespaces): @@ -191,7 +197,14 @@ class InteractiveShellEmbed(TerminalInteractiveShell): local_ns = call_frame.f_locals if module is None: global_ns = call_frame.f_globals - module = sys.modules[global_ns['__name__']] + try: + module = sys.modules[global_ns['__name__']] + except KeyError: + warnings.warn("Failed to get module %s" % \ + global_ns.get('__name__', 'unknown module') + ) + module = DummyMod() + module.__dict__ = global_ns if compile_flags is None: compile_flags = (call_frame.f_code.co_flags & compilerop.PyCF_MASK) @@ -226,7 +239,7 @@ class InteractiveShellEmbed(TerminalInteractiveShell): self.set_completer_frame() with self.builtin_trap, self.display_trap: - self.interact(display_banner=display_banner) + self.interact() # now, purge out the local namespace of IPython's hidden variables. if local_ns is not None: diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 822029b..90b7a50 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -4,18 +4,22 @@ from __future__ import print_function import os import sys import signal +import unicodedata +from warnings import warn +from wcwidth import wcwidth +from IPython.core.error import TryNext from IPython.core.interactiveshell import InteractiveShell from IPython.utils.py3compat import PY3, cast_unicode_py2, input from IPython.utils.terminal import toggle_set_term_title, set_term_title from IPython.utils.process import abbrev_cwd -from traitlets import Bool, Unicode, Dict +from traitlets import Bool, CBool, Unicode, Dict, Integer from prompt_toolkit.completion import Completer, Completion -from prompt_toolkit.enums import DEFAULT_BUFFER +from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER from prompt_toolkit.filters import HasFocus, HasSelection, Condition from prompt_toolkit.history import InMemoryHistory -from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop +from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop, create_prompt_layout from prompt_toolkit.interface import CommandLineInterface from prompt_toolkit.key_binding.manager import KeyBindingManager from prompt_toolkit.key_binding.vi_state import InputMode @@ -23,9 +27,9 @@ from prompt_toolkit.key_binding.bindings.vi import ViStateFilter from prompt_toolkit.keys import Keys from prompt_toolkit.layout.lexers import Lexer from prompt_toolkit.layout.lexers import PygmentsLexer -from prompt_toolkit.styles import PygmentsStyle +from prompt_toolkit.styles import PygmentsStyle, DynamicStyle -from pygments.styles import get_style_by_name +from pygments.styles import get_style_by_name, get_all_styles from pygments.lexers import Python3Lexer, BashLexer, PythonLexer from pygments.token import Token @@ -33,7 +37,6 @@ from .pt_inputhooks import get_inputhook_func from .interactiveshell import get_default_editor, TerminalMagics - class IPythonPTCompleter(Completer): """Adaptor to provide IPython completions to prompt_toolkit""" def __init__(self, ipy_completer): @@ -49,6 +52,22 @@ class IPythonPTCompleter(Completer): ) start_pos = -len(used) for m in matches: + m = unicodedata.normalize('NFC', m) + + # When the first character of the completion has a zero length, + # then it's probably a decomposed unicode character. E.g. caused by + # the "\dot" completion. Try to compose again with the previous + # character. + if wcwidth(m[0]) == 0: + if document.cursor_position + start_pos > 0: + char_before = document.text[document.cursor_position + start_pos - 1] + m = unicodedata.normalize('NFC', char_before + m) + + # Yield the modified completion instead, if this worked. + if wcwidth(m[0:1]) == 1: + yield Completion(m, start_position=start_pos - 1) + continue + # TODO: Use Jedi to determine meta_text # (Jedi currently has a bug that results in incorrect information.) # meta_text = '' @@ -74,8 +93,23 @@ class IPythonPTLexer(Lexer): class TerminalInteractiveShell(InteractiveShell): colors_force = True + space_for_menu = Integer(6, config=True, help='Number of line at the bottom of the screen ' + 'to reserve for the completion menu') + + def _space_for_menu_changed(self, old, new): + self._update_layout() + pt_cli = None + autoedit_syntax = CBool(False, config=True, + help="auto editing of files with syntax errors.") + + confirm_exit = CBool(True, config=True, + help=""" + Set to confirm when you try to exit IPython with an EOF (Control-D + in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit', + you can force a direct exit without any confirmation.""", + ) vi_mode = Bool(False, config=True, help="Use vi style keybindings at the prompt", ) @@ -84,10 +118,13 @@ class TerminalInteractiveShell(InteractiveShell): help="Enable mouse support in the prompt" ) - highlighting_style = Unicode('', config=True, - help="The name of a Pygments style to use for syntax highlighting" + highlighting_style = Unicode('default', config=True, + help="The name of a Pygments style to use for syntax highlighting: \n %s" % ', '.join(get_all_styles()) ) + def _highlighting_style_changed(self, old, new): + self._style = self._make_style_from_name(self.highlighting_style) + highlighting_style_overrides = Dict(config=True, help="Override highlighting format for specific tokens" ) @@ -158,6 +195,13 @@ class TerminalInteractiveShell(InteractiveShell): def _(event): event.current_buffer.reset() + @kbmanager.registry.add_binding(Keys.ControlC, filter=HasFocus(SEARCH_BUFFER)) + def _(event): + if event.current_buffer.document.text: + event.current_buffer.reset() + else: + event.cli.push_focus(DEFAULT_BUFFER) + supports_suspend = Condition(lambda cli: hasattr(signal, 'SIGTSTP')) @kbmanager.registry.add_binding(Keys.ControlZ, filter=supports_suspend) @@ -189,13 +233,33 @@ class TerminalInteractiveShell(InteractiveShell): if cell and (cell != last_cell): history.append(cell) + self._style = self._make_style_from_name(self.highlighting_style) + style = DynamicStyle(lambda: self._style) + + self._app = create_prompt_application( + key_bindings_registry=kbmanager.registry, + history=history, + completer=IPythonPTCompleter(self.Completer), + enable_history_search=True, + style=style, + mouse_support=self.mouse_support, + **self._layout_options() + ) + self.pt_cli = CommandLineInterface(self._app, + eventloop=create_eventloop(self.inputhook)) + + def _make_style_from_name(self, name): + """ + Small wrapper that make an IPython compatible style from a style name + + We need that to add style for prompt ... etc. + """ + style_cls = get_style_by_name(name) style_overrides = { Token.Prompt: '#009900', Token.PromptNum: '#00ff00 bold', } - if self.highlighting_style: - style_cls = get_style_by_name(self.highlighting_style) - else: + if name is 'default': style_cls = get_style_by_name('default') # The default theme needs to be visible on both a dark background # and a light background, because we can't tell what the terminal @@ -212,21 +276,27 @@ class TerminalInteractiveShell(InteractiveShell): style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls, style_dict=style_overrides) - app = create_prompt_application(multiline=True, - lexer=IPythonPTLexer(), - get_prompt_tokens=self.get_prompt_tokens, - get_continuation_tokens=self.get_continuation_tokens, - key_bindings_registry=kbmanager.registry, - history=history, - completer=IPythonPTCompleter(self.Completer), - enable_history_search=True, - style=style, - mouse_support=self.mouse_support, - reserve_space_for_menu=6, - ) + return style + + def _layout_options(self): + """ + Return the current layout option for the current Terminal InteractiveShell + """ + return { + 'lexer':IPythonPTLexer(), + 'reserve_space_for_menu':self.space_for_menu, + 'get_prompt_tokens':self.get_prompt_tokens, + 'get_continuation_tokens':self.get_continuation_tokens, + 'multiline':True, + } - self.pt_cli = CommandLineInterface(app, - eventloop=create_eventloop(self.inputhook)) + + def _update_layout(self): + """ + Ask for a re computation of the application layout, if for example , + some configuration options have changed. + """ + self._app.layout = create_prompt_layout(**self._layout_options()) def prompt_for_code(self): document = self.pt_cli.run(pre_run=self.pre_prompt) @@ -286,12 +356,15 @@ class TerminalInteractiveShell(InteractiveShell): try: code = self.prompt_for_code() except EOFError: - if self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'): + if (not self.confirm_exit) \ + or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'): self.ask_exit() else: if code: self.run_cell(code, store_history=True) + if self.autoedit_syntax and self.SyntaxTB.last_syntax_error: + self.edit_syntax_error() def mainloop(self): # An extra layer of protection in case someone mashing Ctrl-C breaks @@ -314,5 +387,69 @@ class TerminalInteractiveShell(InteractiveShell): else: self._inputhook = None + # Methods to support auto-editing of SyntaxErrors: + + def edit_syntax_error(self): + """The bottom half of the syntax error handler called in the main loop. + + Loop until syntax error is fixed or user cancels. + """ + + while self.SyntaxTB.last_syntax_error: + # copy and clear last_syntax_error + err = self.SyntaxTB.clear_err_state() + if not self._should_recompile(err): + return + try: + # may set last_syntax_error again if a SyntaxError is raised + self.safe_execfile(err.filename, self.user_ns) + except: + self.showtraceback() + else: + try: + with open(err.filename) as f: + # This should be inside a display_trap block and I + # think it is. + sys.displayhook(f.read()) + except: + self.showtraceback() + + def _should_recompile(self, e): + """Utility routine for edit_syntax_error""" + + if e.filename in ('', '', '', + '', '', + None): + return False + try: + if (self.autoedit_syntax and + not self.ask_yes_no( + 'Return to editor to correct syntax error? ' + '[Y/n] ', 'y')): + return False + except EOFError: + return False + + def int0(x): + try: + return int(x) + except TypeError: + return 0 + + # always pass integer line and offset values to editor hook + try: + self.hooks.fix_error_editor(e.filename, + int0(e.lineno), int0(e.offset), + e.msg) + except TryNext: + warn('Could not open editor') + return False + return True + + # Run !system commands directly, not through pipes, so terminal programs + # work correctly. + system = InteractiveShell.system_raw + + if __name__ == '__main__': TerminalInteractiveShell.instance().interact() diff --git a/IPython/testing/plugin/ipdoctest.py b/IPython/testing/plugin/ipdoctest.py index baebc8b..bc750e0 100644 --- a/IPython/testing/plugin/ipdoctest.py +++ b/IPython/testing/plugin/ipdoctest.py @@ -25,8 +25,6 @@ import logging import os import re import sys -import traceback -import unittest from testpath import modified_env @@ -41,10 +39,9 @@ from doctest import (REPORTING_FLAGS, REPORT_ONLY_FIRST_FAILURE, linecache) # Third-party modules -import nose.core from nose.plugins import doctests, Plugin -from nose.util import anyp, getpackage, test_address, resolve_name, tolist +from nose.util import anyp, tolist # Our own imports from IPython.utils.py3compat import builtin_mod, PY3, getcwd @@ -143,7 +140,7 @@ class DocTestFinder(doctest.DocTestFinder): # doctests in extension modules. # Local shorthands - from inspect import isroutine, isclass, ismodule + from inspect import isroutine, isclass # Look for tests in a module's contained objects. if inspect.ismodule(obj) and self._recurse: diff --git a/IPython/testing/skipdoctest.py b/IPython/testing/skipdoctest.py index c055f43..564ca54 100644 --- a/IPython/testing/skipdoctest.py +++ b/IPython/testing/skipdoctest.py @@ -36,3 +36,8 @@ def skip_doctest_py3(f): """Decorator - skip the doctest under Python 3.""" f.skip_doctest = (sys.version_info[0] >= 3) return f + +def skip_doctest_py2(f): + """Decorator - skip the doctest under Python 3.""" + f.skip_doctest = (sys.version_info[0] < 3) + return f diff --git a/IPython/utils/terminal.py b/IPython/utils/terminal.py index 9e7be2a..a1f0f73 100644 --- a/IPython/utils/terminal.py +++ b/IPython/utils/terminal.py @@ -9,22 +9,18 @@ Authors: * Alexander Belchenko (e-mail: bialix AT ukr.net) """ -#----------------------------------------------------------------------------- -# Copyright (C) 2008-2011 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- - -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. import os import struct import sys import warnings -import backports.shutil_get_terminal_size +try: + from shutil import get_terminal_size as _get_terminal_size +except ImportError: + # use backport on Python 2 + from backports.shutil_get_terminal_size import get_terminal_size as _get_terminal_size from . import py3compat @@ -122,4 +118,4 @@ def freeze_term_title(): def get_terminal_size(defaultx=80, defaulty=25): - return backports.shutil_get_terminal_size.get_terminal_size((defaultx, defaulty)) + return _get_terminal_size((defaultx, defaulty)) diff --git a/IPython/utils/tests/test_text.py b/IPython/utils/tests/test_text.py index fd2c64f..57171a9 100644 --- a/IPython/utils/tests/test_text.py +++ b/IPython/utils/tests/test_text.py @@ -19,7 +19,11 @@ import random import sys import nose.tools as nt -import path +try: + from pathlib import Path +except ImportError: + # Python 2 backport + from pathlib2 import Path from IPython.utils import text @@ -207,7 +211,7 @@ def test_LSString(): nt.assert_equal(lss.l, ['abc', 'def']) nt.assert_equal(lss.s, 'abc def') lss = text.LSString(os.getcwd()) - nt.assert_is_instance(lss.p[0], path.path) + nt.assert_is_instance(lss.p[0], Path) def test_SList(): sl = text.SList(['a 11', 'b 1', 'a 2']) diff --git a/IPython/utils/text.py b/IPython/utils/text.py index 6766abb..5ed1a84 100644 --- a/IPython/utils/text.py +++ b/IPython/utils/text.py @@ -14,6 +14,11 @@ import re import sys import textwrap from string import Formatter +try: + from pathlib import Path +except ImportError: + # Python 2 backport + from pathlib2 import Path from IPython.testing.skipdoctest import skip_doctest_py3, skip_doctest from IPython.utils import py3compat @@ -64,11 +69,10 @@ class LSString(str): n = nlstr = property(get_nlstr) def get_paths(self): - from path import path try: return self.__paths except AttributeError: - self.__paths = [path(p) for p in self.split('\n') if os.path.exists(p)] + self.__paths = [Path(p) for p in self.split('\n') if os.path.exists(p)] return self.__paths p = paths = property(get_paths) @@ -123,11 +127,10 @@ class SList(list): n = nlstr = property(get_nlstr) def get_paths(self): - from path import path try: return self.__paths except AttributeError: - self.__paths = [path(p) for p in self if os.path.exists(p)] + self.__paths = [Path(p) for p in self if os.path.exists(p)] return self.__paths p = paths = property(get_paths) diff --git a/README.rst b/README.rst index 2ba75a4..9da53f0 100644 --- a/README.rst +++ b/README.rst @@ -28,7 +28,7 @@ these manuals. If you have Sphinx installed, you can build them by typing See the `install page `__ to install IPython. The Notebook, Qt console and a number of other pieces are now parts of *Jupyter*. -See the `Jupyter installation docs `__ +See the `Jupyter installation docs `__ if you want to use these. Officially, IPython requires Python version 2.7, or 3.3 and above. diff --git a/docs/source/conf.py b/docs/source/conf.py index cdf39f9..6b7a5fb 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -22,7 +22,7 @@ ON_RTD = os.environ.get('READTHEDOCS', None) == 'True' if ON_RTD: # Mock the presence of matplotlib, which we don't have on RTD # see - # http://read-the-docs.readthedocs.org/en/latest/faq.html + # http://read-the-docs.readthedocs.io/en/latest/faq.html tags.add('rtd') # RTD doesn't use the Makefile, so re-run autogen_{things}.py here. @@ -201,12 +201,12 @@ html_additional_pages = { # Output file base name for HTML help builder. htmlhelp_basename = 'ipythondoc' -intersphinx_mapping = {'python': ('http://docs.python.org/2/', None), +intersphinx_mapping = {'python': ('http://docs.python.org/3/', None), 'rpy2': ('http://rpy.sourceforge.net/rpy2/doc-2.4/html/', None), - 'traitlets': ('http://traitlets.readthedocs.org/en/latest/', None), - 'jupyterclient': ('http://jupyter-client.readthedocs.org/en/latest/', None), - 'ipyparallel': ('http://ipyparallel.readthedocs.org/en/latest/', None), - 'jupyter': ('http://jupyter.readthedocs.org/en/latest/', None), + 'traitlets': ('http://traitlets.readthedocs.io/en/latest/', None), + 'jupyterclient': ('http://jupyter-client.readthedocs.io/en/latest/', None), + 'ipyparallel': ('http://ipyparallel.readthedocs.io/en/latest/', None), + 'jupyter': ('http://jupyter.readthedocs.io/en/latest/', None), } # Options for LaTeX output diff --git a/docs/source/development/index.rst b/docs/source/development/index.rst index a945625..389fdea 100644 --- a/docs/source/development/index.rst +++ b/docs/source/development/index.rst @@ -19,4 +19,4 @@ Developer's guide for third party tools and libraries lexer pycompat config - inputhook_app \ No newline at end of file + inputhook_app diff --git a/docs/source/development/wrapperkernels.rst b/docs/source/development/wrapperkernels.rst index f6e308b..65f3a1e 100644 --- a/docs/source/development/wrapperkernels.rst +++ b/docs/source/development/wrapperkernels.rst @@ -7,7 +7,7 @@ You can now re-use the kernel machinery in IPython to easily make new kernels. This is useful for languages that have Python bindings, such as `Octave `_ (via `Oct2Py `_), or languages -where the REPL can be controlled in a tty using `pexpect `_, +where the REPL can be controlled in a tty using `pexpect `_, such as bash. .. seealso:: diff --git a/docs/source/index.rst b/docs/source/index.rst index fc948f1..7b4625e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -27,10 +27,10 @@ Contents .. seealso:: - `Jupyter documentation `__ + `Jupyter documentation `__ The Notebook code and many other pieces formerly in IPython are now parts of Project Jupyter. - `ipyparallel documentation `__ + `ipyparallel documentation `__ Formerly ``IPython.parallel``. diff --git a/docs/source/install/install.rst b/docs/source/install/install.rst index 249aaa5..c2895f6 100644 --- a/docs/source/install/install.rst +++ b/docs/source/install/install.rst @@ -2,7 +2,7 @@ IPython requires Python 2.7 or ≥ 3.3. .. seealso:: - `Installing Jupyter `__ + `Installing Jupyter `__ The Notebook, nbconvert, and many other former pieces of IPython are now part of Project Jupyter. diff --git a/docs/source/interactive/index.rst b/docs/source/interactive/index.rst index 3441c70..26c393d 100644 --- a/docs/source/interactive/index.rst +++ b/docs/source/interactive/index.rst @@ -15,4 +15,4 @@ Using IPython for interactive work .. seealso:: `A Qt Console for Jupyter `__ - `The Jupyter Notebook `__ + `The Jupyter Notebook `__ diff --git a/docs/source/overview.rst b/docs/source/overview.rst index 7d4b8b0..a81604d 100644 --- a/docs/source/overview.rst +++ b/docs/source/overview.rst @@ -219,7 +219,7 @@ different numbers which correspond to the Process ID of the kernel. You can read more about using `ipython qtconsole `_, and -`ipython notebook `_. There +`ipython notebook `_. There is also a :ref:`message spec ` which documents the protocol for communication between kernels and clients. diff --git a/docs/source/whatsnew/github-stats-1.0.rst b/docs/source/whatsnew/github-stats-1.0.rst index 05bdbde..d0b1ae1 100644 --- a/docs/source/whatsnew/github-stats-1.0.rst +++ b/docs/source/whatsnew/github-stats-1.0.rst @@ -1000,7 +1000,7 @@ Pull Requests (793): * :ghpull:`2274`: CLN: Use name to id mapping of notebooks instead of searching. * :ghpull:`2270`: SSHLauncher tweaks * :ghpull:`2269`: add missing location when disambiguating controller IP -* :ghpull:`2263`: Allow docs to build on http://readthedocs.org/ +* :ghpull:`2263`: Allow docs to build on http://readthedocs.io/ * :ghpull:`2256`: Adding data publication example notebook. * :ghpull:`2255`: better flush iopub with AsyncResults * :ghpull:`2261`: Fix: longest_substr([]) -> '' diff --git a/docs/source/whatsnew/github-stats-4.rst b/docs/source/whatsnew/github-stats-4.rst index 480ddc2..32d49d3 100644 --- a/docs/source/whatsnew/github-stats-4.rst +++ b/docs/source/whatsnew/github-stats-4.rst @@ -3,9 +3,33 @@ Issues closed in the 4.x development cycle ========================================== -Issues closed in 4.1 + +Issues closed in 4.2 -------------------- +GitHub stats for 2015/02/02 - 2016/04/20 (since 4.1) + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 10 issues and merged 22 pull requests. +The full list can be seen `on GitHub `__ + +The following 10 authors contributed 27 commits. + +* Benjamin Ragan-Kelley +* Carlos Cordoba +* Gökhan Karabulut +* Jonas Rauber +* Matthias Bussonnier +* Paul Ivanov +* Sebastian Bank +* Thomas A Caswell +* Thomas Kluyver +* Vincent Woo + + +Issues closed in 4.1 +-------------------- GitHub stats for 2015/08/12 - 2016/02/02 (since 4.0.0) diff --git a/docs/source/whatsnew/version4.rst b/docs/source/whatsnew/version4.rst index a7fc23c..6150a86 100644 --- a/docs/source/whatsnew/version4.rst +++ b/docs/source/whatsnew/version4.rst @@ -2,6 +2,19 @@ 4.x Series ============ +IPython 4.2 +=========== + +IPython 4.2 (April, 2016) includes various bugfixes and improvements over 4.1. + +- Fix ``ipython -i`` on errors, which was broken in 4.1. +- The delay meant to highlight deprecated commands that have moved to jupyter has been removed. +- Improve compatibility with future versions of traitlets and matplotlib. +- Use stdlib :func:`python:shutil.get_terminal_size` to measure terminal width when displaying tracebacks + (provided by ``backports.shutil_get_terminal_size`` on Python 2). + +You can see the rest `on GitHub `__. + IPython 4.1 =========== @@ -34,8 +47,8 @@ Released August, 2015 IPython 4.0 is the first major release after the Big Split. IPython no longer contains the notebook, qtconsole, etc. which have moved to -`jupyter `_. -IPython subprojects, such as `IPython.parallel `_ and `widgets `_ have moved to their own repos as well. +`jupyter `_. +IPython subprojects, such as `IPython.parallel `_ and `widgets `_ have moved to their own repos as well. The following subpackages are deprecated: diff --git a/examples/IPython Kernel/Third Party Rich Output.ipynb b/examples/IPython Kernel/Third Party Rich Output.ipynb index a0a137c..e040310 100644 --- a/examples/IPython Kernel/Third Party Rich Output.ipynb +++ b/examples/IPython Kernel/Third Party Rich Output.ipynb @@ -354,7 +354,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "[Vincent](https://vincent.readthedocs.org/en/latest/) is a visualization library that uses the [Vega](http://trifacta.github.io/vega/) visualization grammar to build [d3.js](http://d3js.org/) based visualizations in the Notebook and on http://nbviewer.ipython.org. `Visualization` objects in Vincetn have rich HTML and JavaSrcript representations." + "[Vincent](https://vincent.readthedocs.io/en/latest/) is a visualization library that uses the [Vega](http://trifacta.github.io/vega/) visualization grammar to build [d3.js](http://d3js.org/) based visualizations in the Notebook and on http://nbviewer.ipython.org. `Visualization` objects in Vincetn have rich HTML and JavaSrcript representations." ] }, { diff --git a/setup.py b/setup.py index b9293b1..e4d0c03 100755 --- a/setup.py +++ b/setup.py @@ -198,7 +198,6 @@ install_requires = [ 'traitlets', 'prompt_toolkit>=0.60', 'pygments', - 'backports.shutil_get_terminal_size', ] # Platform-specific dependencies: @@ -206,6 +205,8 @@ install_requires = [ # but requires pip >= 6. pip < 6 ignores these. extras_require.update({ + ':python_version == "2.7"': ['backports.shutil_get_terminal_size'], + ':python_version == "2.7" or python_version == "3.3"': ['pathlib2'], ':sys_platform != "win32"': ['pexpect'], ':sys_platform == "darwin"': ['appnope'], ':sys_platform == "win32"': ['colorama'], diff --git a/tools/github_stats.py b/tools/github_stats.py index fe76aff..efe34a8 100755 --- a/tools/github_stats.py +++ b/tools/github_stats.py @@ -168,7 +168,7 @@ if __name__ == "__main__": state='closed', auth=True, ) - issues, pulls = split_pulls(issues_and_pulls) + issues, pulls = split_pulls(issues_and_pulls, project=project) else: issues = issues_closed_since(since, project=project, pulls=False) pulls = issues_closed_since(since, project=project, pulls=True)