Repository: spyder-ide/spyder

Issue:
%debug magic does not work repeatably for untitled files
### What steps reproduce the problem?

1.  Create a simple file with a single cell with like this:
    ```
    #%% define foo
    def foo(bar):
        x = ['a']
        for i in range(100):
            x.append(i + 3)
        return x
    ```

2. Execute the file using the green arrow:
    ```
    runfile('.../test.py', wdir='...')
    ```

3. Place a breakpoint in line 4:
    ![image](https://user-images.githubusercontent.com/4617010/133154988-7add913e-f8e1-4330-85ff-65aee56b301b.png)

4. Run a `%debug` magic on `foo` with any argument:
    ![image](https://user-images.githubusercontent.com/4617010/133155073-55f32d81-2b73-4a7f-bcc5-c06c59964e87.png)

    Enters debugging normally:
    ![image](https://user-images.githubusercontent.com/4617010/133155246-31447d69-de4d-45ba-a88b-cf220b5b1841.png)

5. Quit debugging (or step through) and try again

    ![image](https://user-images.githubusercontent.com/4617010/133155442-5bd7f462-e959-483c-8249-e7f3b74bf52d.png)
    ![image](https://user-images.githubusercontent.com/4617010/133155499-561c85b8-4a88-4f25-9458-5591cdaa1768.png)

    Only "s" appears to work, and pressing "c" ignores the breakpoint entirely:

    ![image](https://user-images.githubusercontent.com/4617010/133155629-14788800-717b-4eae-b876-4aa213f26b04.png)

## Expected Behavior

I expect `%debug` to work twice, especially when I don't modify the file.

## Attempted solutions:

1. Rerun the file:
    ![image](https://user-images.githubusercontent.com/4617010/133160634-4295270d-5c20-48b0-81ff-79ae01d49252.png)
    Does not work
2. Modify the file, save it, then rerurn and attempt to debug. Output shows that the modification propagated to the `foo` in the console:
    ![image](https://user-images.githubusercontent.com/4617010/133160974-1ba63e2b-e9f0-41a4-8595-085a63c1ddbf.png)
    ![image](https://user-images.githubusercontent.com/4617010/133160923-d7247644-8527-4ab6-bc07-54a42ff69875.png)
    Does not work.
3. Clear all variables. Does not work.
4. Restart kernel (or IDE). That works as expected every time. But doesn't seem like that's a very good solution.

## Comment

The reason that I mention cells in the example is that ideally, I would like to reload my code by running the cell rather than the entire file. The files I have at the moment are sort of like notebooks that I am using the cell functionality to run. I would love not to have to reload the entire file (mush less the kernel), every time I made a change just to the function I'm trying to debug.

## Versions

* Spyder version: 4.1.5
* Python version: 3.8.8 (default, Apr 13 2021, 15:08:03) [MSC v.1916 64 bit (AMD64)]
* Qt version: 5.9.7
* PyQt version: 5.9.2
* Operating System name/version: Windows 10

### Dependencies

```
# Mandatory:
atomicwrites >=1.2.0           :  1.4.0 (OK)
chardet >=2.0.0                :  4.0.0 (OK)
cloudpickle >=0.5.0            :  1.6.0 (OK)
diff_match_patch >=20181111    :  20200713 (OK)
intervaltree                   :  None (OK)
IPython >=4.0                  :  7.22.0 (OK)
jedi =0.17.1                   :  0.17.1 (OK)
nbconvert >=4.0                :  6.0.7 (OK)
numpydoc >=0.6.0               :  1.1.0 (OK)
paramiko >=2.4.0               :  2.7.2 (OK)
parso =0.7.0                   :  0.7.0 (OK)
pexpect >=4.4.0                :  4.8.0 (OK)
pickleshare >=0.4              :  0.7.5 (OK)
psutil >=5.3                   :  5.8.0 (OK)
pygments >=2.0                 :  2.8.1 (OK)
pylint >=1.0                   :  2.7.4 (OK)
pyls >=0.34.0;<1.0.0           :  0.34.1 (OK)
qdarkstyle >=2.8               :  3.0.2 (OK)
qtawesome >=0.5.7              :  1.0.2 (OK)
qtconsole >=4.6.0              :  5.0.3 (OK)
qtpy >=1.5.0                   :  1.9.0 (OK)
rtree >=0.8.3                  :  0.9.7 (OK)
sphinx >=0.6.6                 :  3.1.2 (OK)
spyder_kernels >=1.9.4;<1.10.0 :  1.9.4 (OK)
watchdog                       :  None (OK)
zmq >=17                       :  20.0.0 (OK)

# Optional:
cython >=0.21                  :  0.29.23 (OK)
matplotlib >=2.0.0             :  3.3.4 (OK)
numpy >=1.7                    :  1.20.1 (OK)
pandas >=0.13.1                :  1.2.4 (OK)
scipy >=0.17.0                 :  1.6.2 (OK)
sympy >=0.7.3                  :  1.8 (OK)
```



Discussion:
Hi @madphysicist,

Thanks for reporting, I was able to reproduce this issue. @impact27, do you know why this is happening? 
@steff456 Thanks for confirming. I always have a lingering doubt that I missed something stupid in the docs or configuration when I post bug reports.
It looks like the breakpoints are broken for unsaved files. Can you reproduced for a saved file?
@impact27 the file was saved in this example. I used the name `untitled0.py` out of laziness, but it was on disk in my documents folder as indicated in the output.

I initially started posting the issue because the file wouldn't debug after I modified the function, saved, and reran the cell. But it quickly became apparent that there was no need to do all that. The debugging inline magic breaks even with a completely intact source.
I can't reproduce with the latest 5.x
To solve this for unsaved files, we could remove the last two lines of https://github.com/spyder-ide/spyder-kernels/blob/master/spyder_kernels/customize/namespace_manager.py , but that would mean the files stay cached forever

Resolving patch:
diff --git a/external-deps/spyder-kernels/spyder_kernels/customize/namespace_manager.py b/external-deps/spyder-kernels/spyder_kernels/customize/namespace_manager.py
--- a/external-deps/spyder-kernels/spyder_kernels/customize/namespace_manager.py
+++ b/external-deps/spyder-kernels/spyder_kernels/customize/namespace_manager.py
@@ -4,8 +4,9 @@
 # Licensed under the terms of the MIT License
 # (see spyder_kernels/__init__.py for details)
 
-import sys
 import linecache
+import os.path
+import sys
 
 from IPython.core.getipython import get_ipython
 
@@ -103,5 +104,5 @@ def __exit__(self, exc_type, exc_val, exc_tb):
             sys.modules['__main__'] = self._previous_main
         elif '__main__' in sys.modules and self._reset_main:
             del sys.modules['__main__']
-        if self.filename in linecache.cache:
+        if self.filename in linecache.cache and os.path.exists(self.filename):
             linecache.cache.pop(self.filename)
diff --git a/external-deps/spyder-kernels/spyder_kernels/customize/spydercustomize.py b/external-deps/spyder-kernels/spyder_kernels/customize/spydercustomize.py
--- a/external-deps/spyder-kernels/spyder_kernels/customize/spydercustomize.py
+++ b/external-deps/spyder-kernels/spyder_kernels/customize/spydercustomize.py
@@ -543,6 +543,10 @@ def runfile(filename=None, args=None, wdir=None, namespace=None,
         _print("Could not get code from editor.\n")
         return
 
+    # Normalise the filename
+    filename = os.path.abspath(filename)
+    filename = os.path.normcase(filename)
+
     with NamespaceManager(filename, namespace, current_namespace,
                           file_code=file_code) as (ns_globals, ns_locals):
         sys.argv = [filename]
@@ -676,6 +680,11 @@ def runcell(cellname, filename=None, post_mortem=False):
         file_code = get_file_code(filename, save_all=False)
     except Exception:
         file_code = None
+
+    # Normalise the filename
+    filename = os.path.abspath(filename)
+    filename = os.path.normcase(filename)
+
     with NamespaceManager(filename, current_namespace=True,
                           file_code=file_code) as (ns_globals, ns_locals):
         exec_code(cell_code, filename, ns_globals, ns_locals,


Test patch:
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -4409,6 +4409,7 @@ def crash_func():
 @flaky(max_runs=3)
 @pytest.mark.skipif(os.name == 'nt', reason="Tour messes up focus on Windows")
 @pytest.mark.parametrize("focus_to_editor", [True, False])
+@pytest.mark.skipif(os.name == 'nt', reason="Fails on Windows")
 def test_focus_to_editor(main_window, qtbot, tmpdir, focus_to_editor):
     """Test that the focus_to_editor option works as expected."""
     # Write code with cells to a file
@@ -4593,5 +4594,41 @@ def test_history_from_ipyconsole(main_window, qtbot):
     assert text.splitlines()[-1] == code
 
 
+@pytest.mark.slow
+def test_debug_unsaved_function(main_window, qtbot):
+    """
+    Test that a breakpoint in an unsaved file is reached.
+    """
+    # Main variables
+    shell = main_window.ipyconsole.get_current_shellwidget()
+    control = shell._control
+    run_action = main_window.run_toolbar_actions[0]
+    run_button = main_window.run_toolbar.widgetForAction(run_action)
+
+    # Clear all breakpoints
+    main_window.editor.clear_all_breakpoints()
+
+    # create new file
+    main_window.editor.new()
+    code_editor = main_window.editor.get_focus_widget()
+    code_editor.set_text('def foo():\n    print(1)')
+
+    # Set breakpoint
+    code_editor.debugger.toogle_breakpoint(line_number=2)
+
+    # run file
+    with qtbot.waitSignal(shell.executed):
+        qtbot.mouseClick(run_button, Qt.LeftButton)
+
+    # debug foo
+    with qtbot.waitSignal(shell.executed):
+        shell.execute('%debug foo()')
+
+    with qtbot.waitSignal(shell.executed):
+        shell.execute('continue')
+
+    assert "1---> 2     print(1)" in control.toPlainText()
+
+
 if __name__ == "__main__":
     pytest.main()