Repository: spyder-ide/spyder

Issue:
Breakpoint doesn't appear in gui (red circle) but is listed in Debug > Breakpoints > List breakpoints
_From [Chana...@gmail.com](https://code.google.com/u/103791044127940926669/) on 2015-02-17T07:04:02Z_

Spyder Version:  2.3.2
Python Version:  3.4.1
Qt Version    :  4.8.6, PyQt4 (API v2) 4.10.4 on Windows
pyflakes >=0.6.0:  0.8.1 (OK)
pep8 >=0.6      :  None (NOK)
IPython >=0.13  :  2.3.1 (OK)
zmq >=2.1.11    :  14.5.0 (OK)
pygments >=1.6  :  2.0.1 (OK)
pandas >=0.13.1 :  0.14.1 (OK)
sphinx >=0.6.6  :  1.2.3 (OK)
rope >=0.9.2    :  0.9.4-1 (OK)
matplotlib >=1.0:  1.4.2 (OK)
sympy >=0.7.0   :  0.7.5 (OK)
pylint >=0.25   :  None (NOK) 

What steps will reproduce the problem?
1. Create a breakpoint by pressing a line number, a red circle appears
2. Remove it with the gui, the red circle disappear
3. look at menu Debug\Breakpoints\ListBreakpoints
4. breakpoint is still active (code stops) 

What is the expected output? What do you see instead?

 breakpoint should be removed from breakdown list if cleared in gui. 

Please provide any additional information below

. This does not happened all the time, I cannot detect when does it happen exactly.

_Original issue: http://code.google.com/p/spyderlib/issues/detail?id=2179_



Discussion:
I don't see this bug, but what I do see, is when calling with `Shift` to get the conditional breakpoint dialog and hitting on cancel, then the red circle stays in the line number... but nothing appears on the pane

![image](https://cloud.githubusercontent.com/assets/3627835/11614828/3fc279d0-9c1c-11e5-86ef-80ad00fd1730.png)


Resolving patch:
diff --git a/spyder/widgets/sourcecode/codeeditor.py b/spyder/widgets/sourcecode/codeeditor.py
--- a/spyder/widgets/sourcecode/codeeditor.py
+++ b/spyder/widgets/sourcecode/codeeditor.py
@@ -1313,37 +1313,27 @@ def add_remove_breakpoint(self, line_number=None, condition=None,
         else:
             block = self.document().findBlockByNumber(line_number-1)
         data = block.userData()
-        if data:
-            data.breakpoint = not data.breakpoint
-            old_breakpoint_condition = data.breakpoint_condition
-            data.breakpoint_condition = None
-        else:
+        if not data:
             data = BlockUserData(self)
             data.breakpoint = True
-            old_breakpoint_condition = None
+        elif not edit_condition:
+            data.breakpoint = not data.breakpoint
+            data.breakpoint_condition = None
         if condition is not None:
             data.breakpoint_condition = condition
         if edit_condition:
-            data.breakpoint = True
             condition = data.breakpoint_condition
-            if old_breakpoint_condition is not None:
-                condition = old_breakpoint_condition
             condition, valid = QInputDialog.getText(self,
                                         _('Breakpoint'),
                                         _("Condition:"),
                                         QLineEdit.Normal, condition)
-            if valid:
-                condition = str(condition)
-                if not condition:
-                    condition = None
-                data.breakpoint_condition = condition
-            else:
-                data.breakpoint_condition = old_breakpoint_condition
+            if not valid:
                 return
+            data.breakpoint = True
+            data.breakpoint_condition = str(condition) if condition else None
         if data.breakpoint:
             text = to_text_string(block.text()).strip()
-            if len(text) == 0 or text.startswith('#') or text.startswith('"') \
-               or text.startswith("'"):
+            if len(text) == 0 or text.startswith(('#', '"', "'")):
                 data.breakpoint = False
         block.setUserData(data)
         self.linenumberarea.update()
@@ -1368,6 +1358,9 @@ def clear_breakpoints(self):
             data.breakpoint = False
             # data.breakpoint_condition = None  # not necessary, but logical
             if data.is_empty():
+                # This is not calling the __del__ in BlockUserData.  Not
+                # sure if it's supposed to or not, but that seems to be the
+                # intent.
                 del data
 
     def set_breakpoints(self, breakpoints):


Test patch:
diff --git a/spyder/widgets/sourcecode/tests/test_breakpoints.py b/spyder/widgets/sourcecode/tests/test_breakpoints.py
new file mode 100644
--- /dev/null
+++ b/spyder/widgets/sourcecode/tests/test_breakpoints.py
@@ -0,0 +1,271 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+#
+
+"""
+Tests for breakpoints.
+"""
+
+try:
+    from unittest.mock import Mock
+except ImportError:
+    from mock import Mock  # Python 2
+
+# Third party imports
+import pytest
+from qtpy.QtGui import QTextCursor
+
+# Local imports
+from spyder import version_info
+from spyder.py3compat import to_text_string
+import spyder.widgets.sourcecode.codeeditor as codeeditor
+
+
+# --- Helper methods
+# -----------------------------------------------------------------------------
+def reset_emits(editor):
+    "Reset signal mocks."
+    editor.linenumberarea.reset_mock()
+    if version_info > (4, ):
+        editor.sig_flags_changed.reset_mock()
+    editor.breakpoints_changed.reset_mock()
+
+
+def editor_assert_helper(editor, block=None, bp=False, bpc=None, emits=True):
+    """Run the tests for call to add_remove_breakpoint.
+
+    Args:
+        editor: CodeEditor instance.
+        block: Block of text.
+        bp: Is breakpoint active?
+        bpc: Condition set for breakpoint.
+        emits: Boolean to test if signals were emitted?
+    """
+    data = block.userData()
+    assert data.breakpoint == bp
+    assert data.breakpoint_condition == bpc
+    if emits:
+        editor.linenumberarea.update.assert_called_with()
+        if version_info > (4, ):
+            editor.sig_flags_changed.emit.assert_called_with()
+        editor.breakpoints_changed.emit.assert_called_with()
+    else:
+        editor.linenumberarea.update.assert_not_called()
+        if version_info > (4, ):
+            editor.sig_flags_changed.emit.assert_not_called()
+        editor.breakpoints_changed.emit.assert_not_called()
+
+
+# --- Fixtures
+# -----------------------------------------------------------------------------
+@pytest.fixture
+def code_editor_bot(qtbot):
+    """Create code editor with default Python code."""
+    editor = codeeditor.CodeEditor(parent=None)
+    indent_chars = ' ' * 4
+    tab_stop_width_spaces = 4
+    editor.setup_editor(language='Python', indent_chars=indent_chars,
+                        tab_stop_width_spaces=tab_stop_width_spaces)
+    # Mock the screen updates and signal emits to test when they've been
+    # called.
+    editor.linenumberarea = Mock()
+    if version_info > (4, ):
+        editor.sig_flags_changed = Mock()
+    else:
+        editor.get_linenumberarea_width = Mock(return_value=1)
+    editor.breakpoints_changed = Mock()
+    text = ('def f1(a, b):\n'
+            '"Double quote string."\n'
+            '\n'  # Blank line.
+            '    c = a * b\n'
+            '    return c\n'
+            )
+    editor.set_text(text)
+    return editor, qtbot
+
+
+# --- Tests
+# -----------------------------------------------------------------------------
+def test_add_remove_breakpoint(code_editor_bot, mocker):
+    """Test CodeEditor.add_remove_breakpoint()."""
+    editor, qtbot = code_editor_bot
+    arb = editor.add_remove_breakpoint
+
+    mocker.patch.object(codeeditor.QInputDialog, 'getText')
+
+    editor.go_to_line(1)
+    block = editor.textCursor().block()
+
+    # Breakpoints are only for Python-like files.
+    editor.set_language(None)
+    reset_emits(editor)
+    arb()
+    assert block  # Block exists.
+    assert not block.userData()  # But user data not added to it.
+    editor.linenumberarea.update.assert_not_called()
+    if version_info > (4, ):
+        editor.sig_flags_changed.emit.assert_not_called()
+    editor.breakpoints_changed.emit.assert_not_called()
+
+    # Reset language.
+    editor.set_language('Python')
+
+    # Test with default call on text line containing code.
+    reset_emits(editor)
+    arb()
+    editor_assert_helper(editor, block, bp=True, bpc=None, emits=True)
+
+    # Calling again removes breakpoint.
+    reset_emits(editor)
+    arb()
+    editor_assert_helper(editor, block, bp=False, bpc=None, emits=True)
+
+    # Test on blank line.
+    reset_emits(editor)
+    editor.go_to_line(3)
+    block = editor.textCursor().block()
+    arb()
+    editor_assert_helper(editor, block, bp=False, bpc=None, emits=True)
+
+    # Test adding condition on line containing code.
+    reset_emits(editor)
+    block = editor.document().findBlockByLineNumber(3)  # Block is one less.
+    arb(line_number=4, condition='a > 50')
+    editor_assert_helper(editor, block, bp=True, bpc='a > 50', emits=True)
+
+    # Call already set breakpoint with edit condition.
+    reset_emits(editor)
+    codeeditor.QInputDialog.getText.return_value = ('a == 42', False)
+    arb(line_number=4, edit_condition=True)
+    # Condition not changed because edit was cancelled.
+    editor_assert_helper(editor, block, bp=True, bpc='a > 50', emits=False)
+
+    # Condition changed.
+    codeeditor.QInputDialog.getText.return_value = ('a == 42', True)  # OK.
+    reset_emits(editor)
+    arb(line_number=4, edit_condition=True)
+    editor_assert_helper(editor, block, bp=True, bpc='a == 42', emits=True)
+
+
+def test_add_remove_breakpoint_with_edit_condition(code_editor_bot, mocker):
+    """Test add/remove breakpoint with edit_condition."""
+    # For issue 2179.
+
+    editor, qtbot = code_editor_bot
+    arb = editor.add_remove_breakpoint
+    mocker.patch.object(codeeditor.QInputDialog, 'getText')
+
+    linenumber = 5
+    block = editor.document().findBlockByLineNumber(linenumber - 1)
+
+    # Call with edit_breakpoint on line that has never had a breakpoint set.
+    # Once a line has a breakpoint set, it remains in userData(), which results
+    # in a different behavior when calling the dialog box (tested below).
+    reset_emits(editor)
+    codeeditor.QInputDialog.getText.return_value = ('b == 1', False)
+    arb(line_number=linenumber, edit_condition=True)
+    data = block.userData()
+    assert not data  # Data isn't saved in this case.
+    # Confirm line number area, scrollflag, and breakpoints not called.
+    editor.linenumberarea.update.assert_not_called()
+    if version_info > (4, ):
+        editor.sig_flags_changed.emit.assert_not_called()
+    editor.breakpoints_changed.emit.assert_not_called()
+
+    # Call as if 'OK' button pressed.
+    reset_emits(editor)
+    codeeditor.QInputDialog.getText.return_value = ('b == 1', True)
+    arb(line_number=linenumber, edit_condition=True)
+    editor_assert_helper(editor, block, bp=True, bpc='b == 1', emits=True)
+
+    # Call again with dialog cancelled - breakpoint is already active.
+    reset_emits(editor)
+    codeeditor.QInputDialog.getText.return_value = ('b == 9', False)
+    arb(line_number=linenumber, edit_condition=True)
+    # Breakpoint stays active, but signals aren't emitted.
+    editor_assert_helper(editor, block, bp=True, bpc='b == 1', emits=False)
+
+    # Remove breakpoint and condition.
+    reset_emits(editor)
+    arb(line_number=linenumber)
+    editor_assert_helper(editor, block, bp=False, bpc=None, emits=True)
+
+    # Call again with dialog cancelled.
+    reset_emits(editor)
+    codeeditor.QInputDialog.getText.return_value = ('b == 9', False)
+    arb(line_number=linenumber, edit_condition=True)
+    editor_assert_helper(editor, block, bp=False, bpc=None, emits=False)
+
+
+def test_get_breakpoints(code_editor_bot):
+    """Test CodeEditor.get_breakpoints."""
+    editor, qtbot = code_editor_bot
+    arb = editor.add_remove_breakpoint
+    gb = editor.get_breakpoints
+
+    assert(gb() == [])
+
+    # Add breakpoints.
+    bp = [(1, None), (3, None), (4, 'a > 1'), (5, 'c == 10')]
+    editor.set_breakpoints(bp)
+    assert(gb() == [(1, None), (4, 'a > 1'), (5, 'c == 10')])
+
+    # Only includes active breakpoints.  Calling add_remove turns the
+    # status to inactive, even with a change to condition.
+    arb(line_number=1, condition='a < b')
+    arb(line_number=4)
+    assert(gb() == [(5, 'c == 10')])
+
+
+def test_clear_breakpoints(code_editor_bot):
+    """Test CodeEditor.clear_breakpoints."""
+    editor, qtbot = code_editor_bot
+
+    assert len(editor.blockuserdata_list) == 0
+
+    bp = [(1, None), (4, None)]
+    editor.set_breakpoints(bp)
+    assert editor.get_breakpoints() == bp
+    assert len(editor.blockuserdata_list) == 2
+
+    editor.clear_breakpoints()
+    assert editor.get_breakpoints() == []
+    # Even though there is a 'del data' that would pop the item from the
+    # list, the __del__ funcion isn't called.
+    assert len(editor.blockuserdata_list) == 2
+    for data in editor.blockuserdata_list:
+        assert not data.breakpoint
+
+
+def test_set_breakpoints(code_editor_bot):
+    """Test CodeEditor.set_breakpoints."""
+    editor, qtbot = code_editor_bot
+
+    editor.set_breakpoints([])
+    assert editor.get_breakpoints() == []
+
+    bp = [(1, 'a > b'), (4, None)]
+    editor.set_breakpoints(bp)
+    assert editor.get_breakpoints() == bp
+    assert editor.blockuserdata_list[0].breakpoint
+
+    bp = [(1, None), (5, 'c == 50')]
+    editor.set_breakpoints(bp)
+    assert editor.get_breakpoints() == bp
+    assert editor.blockuserdata_list[0].breakpoint
+
+
+def test_update_breakpoints(code_editor_bot):
+    """Test CodeEditor.update_breakpoints."""
+    editor, qtbot = code_editor_bot
+    reset_emits(editor)
+    editor.breakpoints_changed.emit.assert_not_called()
+    # update_breakpoints is the slot for the blockCountChanged signal.
+    editor.textCursor().insertBlock()
+    editor.breakpoints_changed.emit.assert_called_with()
+
+
+if __name__ == "__main__":
+    pytest.main()