diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 7d7d0fe..b78654e 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -911,7 +911,8 @@ class InteractiveShell(SingletonConfigurable): try: main_mod = self._main_mod_cache[filename] except KeyError: - main_mod = self._main_mod_cache[filename] = types.ModuleType(modname, + main_mod = self._main_mod_cache[filename] = types.ModuleType( + py3compat.cast_bytes_py2(modname), doc="Module created for script run in IPython") else: main_mod.__dict__.clear() diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py index 56211a8..de8d8d7 100644 --- a/IPython/core/tests/test_interactiveshell.py +++ b/IPython/core/tests/test_interactiveshell.py @@ -472,6 +472,12 @@ class InteractiveShellTestCase(unittest.TestCase): with open(filename, 'r') as f: self.assertEqual(f.read(), 'blah') + def test_new_main_mod(self): + # Smoketest to check that this accepts a unicode module name + name = u'jiefmw' + mod = ip.new_main_mod(u'%s.py' % name, name) + self.assertEqual(mod.__name__, name) + class TestSafeExecfileNonAsciiPath(unittest.TestCase): @onlyif_unicode_paths diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py index 8d02e8a..5fa0113 100644 --- a/IPython/core/ultratb.py +++ b/IPython/core/ultratb.py @@ -805,8 +805,10 @@ class VerboseTB(TBTools): elif token_type == tokenize.NEWLINE: break - except (IndexError, UnicodeDecodeError): + except (IndexError, UnicodeDecodeError, SyntaxError): # signals exit of tokenizer + # SyntaxError can occur if the file is not actually Python + # - see gh-6300 pass except tokenize.TokenError as msg: _m = ("An unexpected error occurred while tokenizing input\n" diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py index 3ca611e..07bc059 100644 --- a/IPython/html/base/handlers.py +++ b/IPython/html/base/handlers.py @@ -235,12 +235,13 @@ class IPythonHandler(AuthenticatedHandler): raise web.HTTPError(400, u'Invalid JSON in body of request') return model - def get_error_html(self, status_code, **kwargs): + def write_error(self, status_code, **kwargs): """render custom error pages""" - exception = kwargs.get('exception') + exc_info = kwargs.get('exc_info') message = '' status_message = responses.get(status_code, 'Unknown HTTP Error') - if exception: + if exc_info: + exception = exc_info[1] # get the custom message, if defined try: message = exception.log_message % exception.args @@ -260,13 +261,16 @@ class IPythonHandler(AuthenticatedHandler): exception=exception, ) + self.set_header('Content-Type', 'text/html') # render the template try: html = self.render_template('%s.html' % status_code, **ns) except TemplateNotFound: self.log.debug("No template for %d", status_code) html = self.render_template('error.html', **ns) - return html + + self.write(html) + class Template404(IPythonHandler): diff --git a/IPython/html/static/base/js/utils.js b/IPython/html/static/base/js/utils.js index 5923256..bb903ad 100644 --- a/IPython/html/static/base/js/utils.js +++ b/IPython/html/static/base/js/utils.js @@ -514,15 +514,20 @@ define([ } }; + var ajax_error_msg = function (jqXHR) { + // Return a JSON error message if there is one, + // otherwise the basic HTTP status text. + if (jqXHR.responseJSON && jqXHR.responseJSON.message) { + return jqXHR.responseJSON.message; + } else { + return jqXHR.statusText; + } + } var log_ajax_error = function (jqXHR, status, error) { // log ajax failures with informative messages var msg = "API request failed (" + jqXHR.status + "): "; console.log(jqXHR); - if (jqXHR.responseJSON && jqXHR.responseJSON.message) { - msg += jqXHR.responseJSON.message; - } else { - msg += jqXHR.statusText; - } + msg += ajax_error_msg(jqXHR); console.log(msg); }; @@ -547,6 +552,7 @@ define([ platform: platform, is_or_has : is_or_has, is_focused : is_focused, + ajax_error_msg : ajax_error_msg, log_ajax_error : log_ajax_error, }; diff --git a/IPython/html/static/notebook/js/notebook.js b/IPython/html/static/notebook/js/notebook.js index 02a4439..e22fd51 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -2286,13 +2286,14 @@ define([ */ Notebook.prototype.load_notebook_error = function (xhr, status, error) { this.events.trigger('notebook_load_failed.Notebook', [xhr, status, error]); + utils.log_ajax_error(xhr, status, error); var msg; if (xhr.status === 400) { - msg = error; + msg = escape(utils.ajax_error_msg(xhr)); } else if (xhr.status === 500) { msg = "An unknown error occurred while loading this notebook. " + "This version can load notebook formats " + - "v" + this.nbformat + " or earlier."; + "v" + this.nbformat + " or earlier. See the server log for details."; } dialog.modal({ notebook: this, @@ -2567,10 +2568,10 @@ define([ * @method delete_checkpoint_error * @param {jqXHR} xhr jQuery Ajax object * @param {String} status Description of response status - * @param {String} error_msg HTTP error message + * @param {String} error HTTP error message */ - Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) { - this.events.trigger('checkpoint_delete_failed.Notebook'); + Notebook.prototype.delete_checkpoint_error = function (xhr, status, error) { + this.events.trigger('checkpoint_delete_failed.Notebook', [xhr, status, error]); }; diff --git a/IPython/html/static/widgets/js/widget_int.js b/IPython/html/static/widgets/js/widget_int.js index 09353a4..a92b1d9 100644 --- a/IPython/html/static/widgets/js/widget_int.js +++ b/IPython/html/static/widgets/js/widget_int.js @@ -62,22 +62,35 @@ define([ // handle in the vertical slider is always // consistent. var orientation = this.model.get('orientation'); - var value = this.model.get('min'); + var min = this.model.get('min'); + var max = this.model.get('max'); if (this.model.get('range')) { - this.$slider.slider('option', 'values', [value, value]); + this.$slider.slider('option', 'values', [min, min]); } else { - this.$slider.slider('option', 'value', value); + this.$slider.slider('option', 'value', min); } this.$slider.slider('option', 'orientation', orientation); - value = this.model.get('value'); + var value = this.model.get('value'); if (this.model.get('range')) { + // values for the range case are validated python-side in + // _Bounded{Int,Float}RangeWidget._validate this.$slider.slider('option', 'values', value); this.$readout.text(value.join("-")); } else { + if(value > max) { + value = max; + } + else if(value < min){ + value = min; + } this.$slider.slider('option', 'value', value); this.$readout.text(value); } + if(this.model.get('value')!=value) { + this.model.set('value', value, {updated_view: this}); + this.touch(); + } // Use the right CSS classes for vertical & horizontal sliders if (orientation=='vertical') { diff --git a/IPython/html/tree/handlers.py b/IPython/html/tree/handlers.py index 61d1417..376575d 100644 --- a/IPython/html/tree/handlers.py +++ b/IPython/html/tree/handlers.py @@ -80,5 +80,5 @@ default_handlers = [ (r"/tree%s" % notebook_path_regex, TreeHandler), (r"/tree%s" % path_regex, TreeHandler), (r"/tree", TreeHandler), - (r"", TreeRedirectHandler), + (r"/?", TreeRedirectHandler), ] diff --git a/IPython/html/widgets/widget.py b/IPython/html/widgets/widget.py index be945ca..7ad0ffc 100644 --- a/IPython/html/widgets/widget.py +++ b/IPython/html/widgets/widget.py @@ -13,11 +13,12 @@ in the IPython notebook front-end. # Imports #----------------------------------------------------------------------------- from contextlib import contextmanager +import collections from IPython.core.getipython import get_ipython from IPython.kernel.comm import Comm from IPython.config import LoggingConfigurable -from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple, Int +from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple, Int, Set from IPython.utils.py3compat import string_types #----------------------------------------------------------------------------- @@ -98,9 +99,9 @@ class Widget(LoggingConfigurable): #------------------------------------------------------------------------- _model_name = Unicode('WidgetModel', help="""Name of the backbone model registered in the front-end to create and sync this widget with.""") - _view_name = Unicode(help="""Default view registered in the front-end + _view_name = Unicode('WidgetView', help="""Default view registered in the front-end to use to represent the widget.""", sync=True) - _comm = Instance('IPython.kernel.comm.Comm') + comm = Instance('IPython.kernel.comm.Comm') msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the front-end can send before receiving an idle msg from the back-end.""") @@ -110,7 +111,8 @@ class Widget(LoggingConfigurable): return [name for name in self.traits(sync=True)] _property_lock = Tuple((None, None)) - + _send_state_lock = Int(0) + _states_to_send = Set(allow_none=False) _display_callbacks = Instance(CallbackDispatcher, ()) _msg_callbacks = Instance(CallbackDispatcher, ()) @@ -119,10 +121,12 @@ class Widget(LoggingConfigurable): #------------------------------------------------------------------------- def __init__(self, **kwargs): """Public constructor""" + self._model_id = kwargs.pop('model_id', None) super(Widget, self).__init__(**kwargs) self.on_trait_change(self._handle_property_changed, self.keys) Widget._call_widget_constructed(self) + self.open() def __del__(self): """Object disposal""" @@ -132,21 +136,20 @@ class Widget(LoggingConfigurable): # Properties #------------------------------------------------------------------------- - @property - def comm(self): - """Gets the Comm associated with this widget. - - If a Comm doesn't exist yet, a Comm will be created automagically.""" - if self._comm is None: - # Create a comm. - self._comm = Comm(target_name=self._model_name) - self._comm.on_msg(self._handle_msg) + def open(self): + """Open a comm to the frontend if one isn't already open.""" + if self.comm is None: + if self._model_id is None: + self.comm = Comm(target_name=self._model_name) + self._model_id = self.model_id + else: + self.comm = Comm(target_name=self._model_name, comm_id=self._model_id) + self.comm.on_msg(self._handle_msg) Widget.widgets[self.model_id] = self # first update self.send_state() - return self._comm - + @property def model_id(self): """Gets the model id of this widget. @@ -164,22 +167,22 @@ class Widget(LoggingConfigurable): Closes the underlying comm. When the comm is closed, all of the widget views are automatically removed from the front-end.""" - if self._comm is not None: + if self.comm is not None: Widget.widgets.pop(self.model_id, None) - self._comm.close() - self._comm = None + self.comm.close() + self.comm = None def send_state(self, key=None): """Sends the widget state, or a piece of it, to the front-end. Parameters ---------- - key : unicode (optional) - A single property's name to sync with the front-end. + key : unicode, or iterable (optional) + A single property's name or iterable of property names to sync with the front-end. """ self._send({ "method" : "update", - "state" : self.get_state() + "state" : self.get_state(key=key) }) def get_state(self, key=None): @@ -187,10 +190,17 @@ class Widget(LoggingConfigurable): Parameters ---------- - key : unicode (optional) - A single property's name to get. + key : unicode or iterable (optional) + A single property's name or iterable of property names to get. """ - keys = self.keys if key is None else [key] + if key is None: + keys = self.keys + elif isinstance(key, string_types): + keys = [key] + elif isinstance(key, collections.Iterable): + keys = key + else: + raise ValueError("key must be a string, an iterable of keys, or None") state = {} for k in keys: f = self.trait_metadata(k, 'to_json') @@ -255,10 +265,29 @@ class Widget(LoggingConfigurable): finally: self._property_lock = (None, None) + @contextmanager + def hold_sync(self): + """Hold syncing any state until the context manager is released""" + # We increment a value so that this can be nested. Syncing will happen when + # all levels have been released. + self._send_state_lock += 1 + try: + yield + finally: + self._send_state_lock -=1 + if self._send_state_lock == 0: + self.send_state(self._states_to_send) + self._states_to_send.clear() + def _should_send_property(self, key, value): """Check the property lock (property_lock)""" - return key != self._property_lock[0] or \ - value != self._property_lock[1] + if (key == self._property_lock[0] and value == self._property_lock[1]): + return False + elif self._send_state_lock > 0: + self._states_to_send.add(key) + return False + else: + return True # Event handlers @_show_traceback @@ -388,7 +417,10 @@ class DOMWidget(Widget): selector: unicode (optional, kwarg only) JQuery selector to use to apply the CSS key/value. If no selector is provided, an empty selector is used. An empty selector makes the - front-end try to apply the css to the top-level element. + front-end try to apply the css to a default element. The default + element is an attribute unique to each view, which is a DOM element + of the view that should be styled with common CSS (see + `$el_to_style` in the Javascript code). """ if value is None: css_dict = dict_or_key diff --git a/IPython/kernel/comm/comm.py b/IPython/kernel/comm/comm.py index 502210b..59bee03 100644 --- a/IPython/kernel/comm/comm.py +++ b/IPython/kernel/comm/comm.py @@ -23,7 +23,7 @@ class Comm(LoggingConfigurable): return self.shell.kernel.iopub_socket session = Instance('IPython.kernel.zmq.session.Session') def _session_default(self): - if self.shell is None: + if self.shell is None or not hasattr(self.shell, 'kernel'): return return self.shell.kernel.session @@ -56,15 +56,16 @@ class Comm(LoggingConfigurable): def _publish_msg(self, msg_type, data=None, metadata=None, **keys): """Helper for sending a comm message on IOPub""" - data = {} if data is None else data - metadata = {} if metadata is None else metadata - content = json_clean(dict(data=data, comm_id=self.comm_id, **keys)) - self.session.send(self.iopub_socket, msg_type, - content, - metadata=json_clean(metadata), - parent=self.shell.get_parent(), - ident=self.topic, - ) + if self.session is not None: + data = {} if data is None else data + metadata = {} if metadata is None else metadata + content = json_clean(dict(data=data, comm_id=self.comm_id, **keys)) + self.session.send(self.iopub_socket, msg_type, + content, + metadata=json_clean(metadata), + parent=self.shell.get_parent(), + ident=self.topic, + ) def __del__(self): """trigger close on gc""" @@ -77,7 +78,9 @@ class Comm(LoggingConfigurable): if data is None: data = self._open_data self._closed = False - get_ipython().comm_manager.register_comm(self) + ip = get_ipython() + if hasattr(ip, 'comm_manager'): + ip.comm_manager.register_comm(self) self._publish_msg('comm_open', data, metadata, target_name=self.target_name) def close(self, data=None, metadata=None): @@ -88,7 +91,9 @@ class Comm(LoggingConfigurable): if data is None: data = self._close_data self._publish_msg('comm_close', data, metadata) - get_ipython().comm_manager.unregister_comm(self) + ip = get_ipython() + if hasattr(ip, 'comm_manager'): + ip.comm_manager.unregister_comm(self) self._closed = True def send(self, data=None, metadata=None): diff --git a/IPython/lib/inputhook.py b/IPython/lib/inputhook.py index a29cec0..13425e1 100644 --- a/IPython/lib/inputhook.py +++ b/IPython/lib/inputhook.py @@ -211,7 +211,9 @@ class InputHookManager(object): raise ValueError("requires wxPython >= 2.8, but you have %s" % wx.__version__) from IPython.lib.inputhookwx import inputhook_wx + from IPython.external.appnope import nope self.set_inputhook(inputhook_wx) + nope() self._current_gui = GUI_WX import wx if app is None: @@ -227,9 +229,11 @@ class InputHookManager(object): This merely sets PyOS_InputHook to NULL. """ + from IPython.external.appnope import nap if GUI_WX in self._apps: self._apps[GUI_WX]._in_event_loop = False self.clear_inputhook() + nap() def enable_qt4(self, app=None): """Enable event loop integration with PyQt4. @@ -254,8 +258,10 @@ class InputHookManager(object): app = QtGui.QApplication(sys.argv) """ from IPython.lib.inputhookqt4 import create_inputhook_qt4 + from IPython.external.appnope import nope app, inputhook_qt4 = create_inputhook_qt4(self, app) self.set_inputhook(inputhook_qt4) + nope() self._current_gui = GUI_QT4 app._in_event_loop = True @@ -267,9 +273,11 @@ class InputHookManager(object): This merely sets PyOS_InputHook to NULL. """ + from IPython.external.appnope import nap if GUI_QT4 in self._apps: self._apps[GUI_QT4]._in_event_loop = False self.clear_inputhook() + nap() def enable_gtk(self, app=None): """Enable event loop integration with PyGTK. diff --git a/IPython/nbformat/reader.py b/IPython/nbformat/reader.py index 78e8a1d..bb94ee4 100644 --- a/IPython/nbformat/reader.py +++ b/IPython/nbformat/reader.py @@ -80,6 +80,8 @@ def reads(s, **kwargs): nb : NotebookNode The notebook that was read. """ + from .current import NBFormatError + nb_dict = parse_json(s, **kwargs) (major, minor) = get_version(nb_dict) if major in versions: diff --git a/IPython/utils/eventful.py b/IPython/utils/eventful.py new file mode 100644 index 0000000..d02be7b --- /dev/null +++ b/IPython/utils/eventful.py @@ -0,0 +1,299 @@ +"""Contains eventful dict and list implementations.""" + +# void function used as a callback placeholder. +def _void(*p, **k): return None + +class EventfulDict(dict): + """Eventful dictionary. + + This class inherits from the Python intrinsic dictionary class, dict. It + adds events to the get, set, and del actions and optionally allows you to + intercept and cancel these actions. The eventfulness isn't recursive. In + other words, if you add a dict as a child, the events of that dict won't be + listened to. If you find you need something recursive, listen to the `add` + and `set` methods, and then cancel `dict` values from being set, and instead + set `EventfulDict`s that wrap those `dict`s. Then you can wire the events + to the same handlers if necessary. + + See the on_events, on_add, on_set, and on_del methods for registering + event handlers.""" + + def __init__(self, *args, **kwargs): + """Public constructor""" + self._add_callback = _void + self._del_callback = _void + self._set_callback = _void + dict.__init__(self, *args, **kwargs) + + def on_events(self, add_callback=None, set_callback=None, del_callback=None): + """Register callbacks for add, set, and del actions. + + See the doctstrings for on_(add/set/del) for details about each + callback. + + add_callback: [callback = None] + set_callback: [callback = None] + del_callback: [callback = None]""" + self.on_add(add_callback) + self.on_set(set_callback) + self.on_del(del_callback) + + def on_add(self, callback): + """Register a callback for when an item is added to the dict. + + Allows the listener to detect when items are added to the dictionary and + optionally cancel the addition. + + callback: callable or None + If you want to ignore the addition event, pass None as the callback. + The callback should have a signature of callback(key, value). The + callback should return a boolean True if the additon should be + canceled, False or None otherwise.""" + self._add_callback = callback if callable(callback) else _void + + def on_del(self, callback): + """Register a callback for when an item is deleted from the dict. + + Allows the listener to detect when items are deleted from the dictionary + and optionally cancel the deletion. + + callback: callable or None + If you want to ignore the deletion event, pass None as the callback. + The callback should have a signature of callback(key). The + callback should return a boolean True if the deletion should be + canceled, False or None otherwise.""" + self._del_callback = callback if callable(callback) else _void + + def on_set(self, callback): + """Register a callback for when an item is changed in the dict. + + Allows the listener to detect when items are changed in the dictionary + and optionally cancel the change. + + callback: callable or None + If you want to ignore the change event, pass None as the callback. + The callback should have a signature of callback(key, value). The + callback should return a boolean True if the change should be + canceled, False or None otherwise.""" + self._set_callback = callback if callable(callback) else _void + + def pop(self, key): + """Returns the value of an item in the dictionary and then deletes the + item from the dictionary.""" + if self._can_del(key): + return dict.pop(self, key) + else: + raise Exception('Cannot `pop`, deletion of key "{}" failed.'.format(key)) + + def popitem(self): + """Pop the next key/value pair from the dictionary.""" + key = next(iter(self)) + return key, self.pop(key) + + def update(self, other_dict): + """Copy the key/value pairs from another dictionary into this dictionary, + overwriting any conflicting keys in this dictionary.""" + for (key, value) in other_dict.items(): + self[key] = value + + def clear(self): + """Clear the dictionary.""" + for key in list(self.keys()): + del self[key] + + def __setitem__(self, key, value): + if (key in self and self._can_set(key, value)) or \ + (key not in self and self._can_add(key, value)): + return dict.__setitem__(self, key, value) + + def __delitem__(self, key): + if self._can_del(key): + return dict.__delitem__(self, key) + + def _can_add(self, key, value): + """Check if the item can be added to the dict.""" + return not bool(self._add_callback(key, value)) + + def _can_del(self, key): + """Check if the item can be deleted from the dict.""" + return not bool(self._del_callback(key)) + + def _can_set(self, key, value): + """Check if the item can be changed in the dict.""" + return not bool(self._set_callback(key, value)) + + +class EventfulList(list): + """Eventful list. + + This class inherits from the Python intrinsic `list` class. It adds events + that allow you to listen for actions that modify the list. You can + optionally cancel the actions. + + See the on_del, on_set, on_insert, on_sort, and on_reverse methods for + registering an event handler. + + Some of the method docstrings were taken from the Python documentation at + https://docs.python.org/2/tutorial/datastructures.html""" + + def __init__(self, *pargs, **kwargs): + """Public constructor""" + self._insert_callback = _void + self._set_callback = _void + self._del_callback = _void + self._sort_callback = _void + self._reverse_callback = _void + list.__init__(self, *pargs, **kwargs) + + def on_events(self, insert_callback=None, set_callback=None, + del_callback=None, reverse_callback=None, sort_callback=None): + """Register callbacks for add, set, and del actions. + + See the doctstrings for on_(insert/set/del/reverse/sort) for details + about each callback. + + insert_callback: [callback = None] + set_callback: [callback = None] + del_callback: [callback = None] + reverse_callback: [callback = None] + sort_callback: [callback = None]""" + self.on_insert(insert_callback) + self.on_set(set_callback) + self.on_del(del_callback) + self.on_reverse(reverse_callback) + self.on_sort(sort_callback) + + def on_insert(self, callback): + """Register a callback for when an item is inserted into the list. + + Allows the listener to detect when items are inserted into the list and + optionally cancel the insertion. + + callback: callable or None + If you want to ignore the insertion event, pass None as the callback. + The callback should have a signature of callback(index, value). The + callback should return a boolean True if the insertion should be + canceled, False or None otherwise.""" + self._insert_callback = callback if callable(callback) else _void + + def on_del(self, callback): + """Register a callback for item deletion. + + Allows the listener to detect when items are deleted from the list and + optionally cancel the deletion. + + callback: callable or None + If you want to ignore the deletion event, pass None as the callback. + The callback should have a signature of callback(index). The + callback should return a boolean True if the deletion should be + canceled, False or None otherwise.""" + self._del_callback = callback if callable(callback) else _void + + def on_set(self, callback): + """Register a callback for items are set. + + Allows the listener to detect when items are set and optionally cancel + the setting. Note, `set` is also called when one or more items are + added to the end of the list. + + callback: callable or None + If you want to ignore the set event, pass None as the callback. + The callback should have a signature of callback(index, value). The + callback should return a boolean True if the set should be + canceled, False or None otherwise.""" + self._set_callback = callback if callable(callback) else _void + + def on_reverse(self, callback): + """Register a callback for list reversal. + + callback: callable or None + If you want to ignore the reverse event, pass None as the callback. + The callback should have a signature of callback(). The + callback should return a boolean True if the reverse should be + canceled, False or None otherwise.""" + self._reverse_callback = callback if callable(callback) else _void + + def on_sort(self, callback): + """Register a callback for sortting of the list. + + callback: callable or None + If you want to ignore the sort event, pass None as the callback. + The callback signature should match that of Python list's `.sort` + method or `callback(*pargs, **kwargs)` as a catch all. The callback + should return a boolean True if the reverse should be canceled, + False or None otherwise.""" + self._sort_callback = callback if callable(callback) else _void + + def append(self, x): + """Add an item to the end of the list.""" + self[len(self):] = [x] + + def extend(self, L): + """Extend the list by appending all the items in the given list.""" + self[len(self):] = L + + def remove(self, x): + """Remove the first item from the list whose value is x. It is an error + if there is no such item.""" + del self[self.index(x)] + + def pop(self, i=None): + """Remove the item at the given position in the list, and return it. If + no index is specified, a.pop() removes and returns the last item in the + list.""" + if i is None: + i = len(self) - 1 + val = self[i] + del self[i] + return val + + def reverse(self): + """Reverse the elements of the list, in place.""" + if self._can_reverse(): + list.reverse(self) + + def insert(self, index, value): + """Insert an item at a given position. The first argument is the index + of the element before which to insert, so a.insert(0, x) inserts at the + front of the list, and a.insert(len(a), x) is equivalent to + a.append(x).""" + if self._can_insert(index, value): + list.insert(self, index, value) + + def sort(self, *pargs, **kwargs): + """Sort the items of the list in place (the arguments can be used for + sort customization, see Python's sorted() for their explanation).""" + if self._can_sort(*pargs, **kwargs): + list.sort(self, *pargs, **kwargs) + + def __delitem__(self, index): + if self._can_del(index): + list.__delitem__(self, index) + + def __setitem__(self, index, value): + if self._can_set(index, value): + list.__setitem__(self, index, value) + + def __setslice__(self, start, end, value): + if self._can_set(slice(start, end), value): + list.__setslice__(self, start, end, value) + + def _can_insert(self, index, value): + """Check if the item can be inserted.""" + return not bool(self._insert_callback(index, value)) + + def _can_del(self, index): + """Check if the item can be deleted.""" + return not bool(self._del_callback(index)) + + def _can_set(self, index, value): + """Check if the item can be set.""" + return not bool(self._set_callback(index, value)) + + def _can_reverse(self): + """Check if the list can be reversed.""" + return not bool(self._reverse_callback()) + + def _can_sort(self, *pargs, **kwargs): + """Check if the list can be sorted.""" + return not bool(self._sort_callback(*pargs, **kwargs)) diff --git a/IPython/utils/tests/test_traitlets.py b/IPython/utils/tests/test_traitlets.py index 8992345..fcb3ca2 100644 --- a/IPython/utils/tests/test_traitlets.py +++ b/IPython/utils/tests/test_traitlets.py @@ -19,7 +19,8 @@ from IPython.utils.traitlets import ( HasTraits, MetaHasTraits, TraitType, Any, CBytes, Dict, Int, Long, Integer, Float, Complex, Bytes, Unicode, TraitError, Undefined, Type, This, Instance, TCPAddress, List, Tuple, - ObjectName, DottedObjectName, CRegExp, link + ObjectName, DottedObjectName, CRegExp, link, directional_link, + EventfulList, EventfulDict ) from IPython.utils import py3compat from IPython.testing.decorators import skipif @@ -1034,7 +1035,7 @@ class TestCRegExp(TraitTestBase): _default_value = re.compile(r'') _good_values = [r'\d+', re.compile(r'\d+')] - _bad_values = [r'(', None, ()] + _bad_values = ['(', None, ()] class DictTrait(HasTraits): value = Dict() @@ -1147,6 +1148,71 @@ class TestLink(TestCase): self.assertEqual(''.join(callback_count), 'ab') del callback_count[:] +class TestDirectionalLink(TestCase): + def test_connect_same(self): + """Verify two traitlets of the same type can be linked together using directional_link.""" + + # Create two simple classes with Int traitlets. + class A(HasTraits): + value = Int() + a = A(value=9) + b = A(value=8) + + # Conenct the two classes. + c = directional_link((a, 'value'), (b, 'value')) + + # Make sure the values are the same at the point of linking. + self.assertEqual(a.value, b.value) + + # Change one the value of the source and check that it synchronizes the target. + a.value = 5 + self.assertEqual(b.value, 5) + # Change one the value of the target and check that it has no impact on the source + b.value = 6 + self.assertEqual(a.value, 5) + + def test_link_different(self): + """Verify two traitlets of different types can be linked together using link.""" + + # Create two simple classes with Int traitlets. + class A(HasTraits): + value = Int() + class B(HasTraits): + count = Int() + a = A(value=9) + b = B(count=8) + + # Conenct the two classes. + c = directional_link((a, 'value'), (b, 'count')) + + # Make sure the values are the same at the point of linking. + self.assertEqual(a.value, b.count) + + # Change one the value of the source and check that it synchronizes the target. + a.value = 5 + self.assertEqual(b.count, 5) + # Change one the value of the target and check that it has no impact on the source + b.value = 6 + self.assertEqual(a.value, 5) + + def test_unlink(self): + """Verify two linked traitlets can be unlinked.""" + + # Create two simple classes with Int traitlets. + class A(HasTraits): + value = Int() + a = A(value=9) + b = A(value=8) + + # Connect the two classes. + c = directional_link((a, 'value'), (b, 'value')) + a.value = 4 + c.unlink() + + # Change one of the values to make sure they don't stay in sync. + a.value = 5 + self.assertNotEqual(a.value, b.value) + class Pickleable(HasTraits): i = Int() j = Int() @@ -1171,4 +1237,65 @@ def test_pickle_hastraits(): c2 = pickle.loads(p) nt.assert_equal(c2.i, c.i) nt.assert_equal(c2.j, c.j) - + +class TestEventful(TestCase): + + def test_list(self): + """Does the EventfulList work?""" + event_cache = [] + + class A(HasTraits): + x = EventfulList([c for c in 'abc']) + a = A() + a.x.on_events(lambda i, x: event_cache.append('insert'), \ + lambda i, x: event_cache.append('set'), \ + lambda i: event_cache.append('del'), \ + lambda: event_cache.append('reverse'), \ + lambda *p, **k: event_cache.append('sort')) + + a.x.remove('c') + # ab + a.x.insert(0, 'z') + # zab + del a.x[1] + # zb + a.x.reverse() + # bz + a.x[1] = 'o' + # bo + a.x.append('a') + # boa + a.x.sort() + # abo + + # Were the correct events captured? + self.assertEqual(event_cache, ['del', 'insert', 'del', 'reverse', 'set', 'set', 'sort']) + + # Is the output correct? + self.assertEqual(a.x, [c for c in 'abo']) + + def test_dict(self): + """Does the EventfulDict work?""" + event_cache = [] + + class A(HasTraits): + x = EventfulDict({c: c for c in 'abc'}) + a = A() + a.x.on_events(lambda k, v: event_cache.append('add'), \ + lambda k, v: event_cache.append('set'), \ + lambda k: event_cache.append('del')) + + del a.x['c'] + # ab + a.x['z'] = 1 + # abz + a.x['z'] = 'z' + # abz + a.x.pop('a') + # bz + + # Were the correct events captured? + self.assertEqual(event_cache, ['del', 'add', 'set', 'del']) + + # Is the output correct? + self.assertEqual(a.x, {c: c for c in 'bz'}) diff --git a/IPython/utils/traitlets.py b/IPython/utils/traitlets.py index 1a68b04..7b65196 100644 --- a/IPython/utils/traitlets.py +++ b/IPython/utils/traitlets.py @@ -54,6 +54,7 @@ except: from .importstring import import_item from IPython.utils import py3compat +from IPython.utils import eventful from IPython.utils.py3compat import iteritems from IPython.testing.skipdoctest import skip_doctest @@ -226,6 +227,61 @@ class link(object): (obj,attr) = key obj.on_trait_change(callback, attr, remove=True) +@skip_doctest +class directional_link(object): + """Link the trait of a source object with traits of target objects. + + Parameters + ---------- + source : pair of object, name + targets : pairs of objects/attributes + + Examples + -------- + + >>> c = directional_link((src, 'value'), (tgt1, 'value'), (tgt2, 'value')) + >>> src.value = 5 # updates target objects + >>> tgt1.value = 6 # does not update other objects + """ + updating = False + + def __init__(self, source, *targets): + self.source = source + self.targets = targets + + # Update current value + src_attr_value = getattr(source[0], source[1]) + for obj, attr in targets: + if getattr(obj, attr) != src_attr_value: + setattr(obj, attr, src_attr_value) + + # Wire + self.source[0].on_trait_change(self._update, self.source[1]) + + @contextlib.contextmanager + def _busy_updating(self): + self.updating = True + try: + yield + finally: + self.updating = False + + def _update(self, name, old, new): + if self.updating: + return + with self._busy_updating(): + for obj, attr in self.targets: + setattr(obj, attr, new) + + def unlink(self): + self.source[0].on_trait_change(self._update, self.source[1], remove=True) + self.source = None + self.targets = [] + +def dlink(source, *targets): + """Shorter helper function returning a directional_link object""" + return directional_link(source, *targets) + #----------------------------------------------------------------------------- # Base TraitType for all traits #----------------------------------------------------------------------------- @@ -1490,6 +1546,49 @@ class Dict(Instance): super(Dict,self).__init__(klass=dict, args=args, allow_none=allow_none, **metadata) + +class EventfulDict(Instance): + """An instance of an EventfulDict.""" + + def __init__(self, default_value=None, allow_none=True, **metadata): + """Create a EventfulDict trait type from a dict. + + The default value is created by doing + ``eventful.EvenfulDict(default_value)``, which creates a copy of the + ``default_value``. + """ + if default_value is None: + args = ((),) + elif isinstance(default_value, dict): + args = (default_value,) + elif isinstance(default_value, SequenceTypes): + args = (default_value,) + else: + raise TypeError('default value of EventfulDict was %s' % default_value) + + super(EventfulDict, self).__init__(klass=eventful.EventfulDict, args=args, + allow_none=allow_none, **metadata) + + +class EventfulList(Instance): + """An instance of an EventfulList.""" + + def __init__(self, default_value=None, allow_none=True, **metadata): + """Create a EventfulList trait type from a dict. + + The default value is created by doing + ``eventful.EvenfulList(default_value)``, which creates a copy of the + ``default_value``. + """ + if default_value is None: + args = ((),) + else: + args = (default_value,) + + super(EventfulList, self).__init__(klass=eventful.EventfulList, args=args, + allow_none=allow_none, **metadata) + + class TCPAddress(TraitType): """A trait for an (ip, port) tuple. diff --git a/docs/source/config/extensions/rmagic.rst b/docs/source/config/extensions/rmagic.rst index 77104f2..cc16592 100644 --- a/docs/source/config/extensions/rmagic.rst +++ b/docs/source/config/extensions/rmagic.rst @@ -7,6 +7,6 @@ rmagic .. note:: The rmagic extension has been moved to `rpy2 `_ - as :mod:`rpy2.interactive.ipython`. + as :mod:`rpy2.ipython`. .. automodule:: IPython.extensions.rmagic diff --git a/docs/source/development/wrapperkernels.rst b/docs/source/development/wrapperkernels.rst index e345333..a8817a0 100644 --- a/docs/source/development/wrapperkernels.rst +++ b/docs/source/development/wrapperkernels.rst @@ -73,7 +73,7 @@ Example language_version = '0.1' banner = "Echo kernel - as useful as a parrot" - def do_execute(self, code, silent, store_history=True, user_experssions=None, + def do_execute(self, code, silent, store_history=True, user_expressions=None, allow_stdin=False): if not silent: stream_content = {'name': 'stdout', 'data':code} diff --git a/git-hooks/post-checkout b/git-hooks/post-checkout index 5bb02c7..a8b1206 100755 --- a/git-hooks/post-checkout +++ b/git-hooks/post-checkout @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash git submodule init git submodule update diff --git a/tools/build_release b/tools/build_release index 3f2cfe6..c19870e 100755 --- a/tools/build_release +++ b/tools/build_release @@ -3,6 +3,7 @@ """ import os +from shutil import rmtree from toollib import * @@ -20,7 +21,7 @@ compile_tree() for d in ['build', 'dist', pjoin('docs', 'build'), pjoin('docs', 'dist'), pjoin('docs', 'source', 'api', 'generated')]: if os.path.isdir(d): - remove_tree(d) + rmtree(d) # Build source and binary distros sh(sdists) diff --git a/tools/release b/tools/release index 6b25012..56a82a8 100755 --- a/tools/release +++ b/tools/release @@ -56,7 +56,7 @@ sh('./setup.py register') # Upload all files sh(sdists + ' upload') for py in ('2.7', '3.4'): - sh('python%s setupegg.py bdist_wheel' % py) + sh('python%s setupegg.py bdist_wheel upload' % py) cd(distdir) print( 'Uploading distribution files...') diff --git a/tools/toollib.py b/tools/toollib.py index 6804674..a448d57 100644 --- a/tools/toollib.py +++ b/tools/toollib.py @@ -42,7 +42,7 @@ def get_ipdir(): try: ipdir = sys.argv[1] except IndexError: - ipdir = '..' + ipdir = pjoin(os.path.dirname(__file__), os.pardir) ipdir = os.path.abspath(ipdir)