Repository: spyder-ide/spyder

Issue:
Pressing "Clear comparison" in Profiler while its running stops it and raises a TypeError
## Description
when profiler is running somecode(any code) click "clear comparison". 
the code stop running and spyder pump up an error as following:

>   File "c:\users\53282\appdata\local\programs\python\python36\lib\site-packages\spyder_profiler\widgets\profilergui.py", line 264, in <lambda>
>     self.finished(ec, es))
>   File "c:\users\53282\appdata\local\programs\python\python36\lib\site-packages\spyder_profiler\widgets\profilergui.py", line 326, in finished
>     self.output = self.error_output + self.output
> TypeError: must be str, not NoneType

i found this maybe caused by this:
```python
    def show_data(self, justanalyzed=False):
        if not justanalyzed:
            self.output = None
        self.log_button.setEnabled(self.output is not None \
                                   and len(self.output) > 0)
        self.kill_if_running()
```
so i edit it:
```python
    def show_data(self, justanalyzed=False):
        if not justanalyzed:
            self.output = ''
        self.log_button.setEnabled(self.output is not None \
                                   and len(self.output) > 0)
        #self.kill_if_running()
```
and now "clear comparison" wont stop the code running and no error will pump up.

## Version and main components

* Spyder Version: 3.2.6 
* Python Version: 3.6.4
* Qt Versions: 5.10.0, PyQt5 5.10 on Windows

## Dependencies
```
pyflakes >=0.6.0 :  1.6.0 (OK)
pycodestyle >=2.3:  2.3.1 (OK)
pygments >=2.0   :  2.2.0 (OK)
pandas >=0.13.1  :  0.22.0 (OK)
numpy >=1.7      :  1.14.0 (OK)
sphinx >=0.6.6   :  1.6.7 (OK)
rope >=0.9.4     :  0.10.7 (OK)
jedi >=0.9.0     :  0.11.1 (OK)
nbconvert >=4.0  :  5.3.1 (OK)
sympy >=0.7.3    :  None (NOK)
cython >=0.21    :  None (NOK)
qtconsole >=4.2.0:  4.3.1 (OK)
IPython >=4.0    :  6.2.1 (OK)
pylint >=0.25    :  1.8.2 (OK)

```



Discussion:
Thanks for reporting; I was able to reproduce this. If it works, it works,, though perhaps there's a cleaner fix, perhaps one step up the chain in the function called by the "clear comparison" button, as this seems a bit kludgy to the untrained eye. In any case, we'd be glad to review a PR with this fix; you could probably mock a test but probably better to just run a "real world" one with qtbot, running on a test file and clicking the appropriate buttons.  
yes, i am one with untrained eyes(at least for now) and know little about qtbot/qt. so maybe i wont be capable to fix this "in a cleaner way".

i dont really understand what u mean by "real world".
do u mean profile another piece of code and try "clear comparison" button to see if it is all ok? i have done that, and noticed some side-effects mocking it this way.


> i am one with untrained eyes(at least for now) and know little about qtbot/qt

No worries! You can just go ahead and submit a PR, and if there's a cleaner way I'm sure @ccordoba12 or someone will point it out during a review. I didn't know anything about qt/qtbot either going in, but the basics were easier than they seemed; you can look at the other tests to get a sense of how to do it. I could see two approaches: Either way, you'd presumably want to create two test files in the ``spyder/plugins/tests`` directory, one to test the behavior itself, with something like
```
import time
time.sleep(3)
```
and the other with arbitrary content to load as comparison data,
```
import time
print("Testing profiler")
time.sleep(0.1)
```
and create a new profiler object/window, as presumably the other tests do. 

Then, you would need to do four actions:

1. Run the profiler on the second, comparison file. 
2. Load those results 
3. Run the profiler on the first, test file and 
4. Trigger the method linked to the "Clear results" button. 

These can be accomplished either by calling the relevant methods for each one directly, or with doing ``qtbot.mouseClick()`` on the relevant buttons and/or ``qtbot.keyClick()`` on the hotkeys. But again, if you can't get it working just submit the PR and we can help you further.

> i dont really understand what u mean by "real world". do u mean profile another piece of code and try "clear comparison" button to see if it is all ok?

Yes, exactly, and just automating it with method calls or qtbot.

> i have done that, and noticed some side-effects mocking it this way.

Oh; could you explain further?



@bcolsen, @jnsebgosselin what are your thoughts on how this should flow?

<img width="761" alt="Screen Shot 2019-11-28 at 20 55 32" src="https://user-images.githubusercontent.com/3627835/69838411-74bd3880-1221-11ea-9e80-d8b0ba2cb80a.png">

We could just disable the "clear comparison" while it is running? or should we not stop the process?
I didn't even realize that you could make comparisons. I thought that you could just save and load data to see it again later. Maybe `Load Comparison Data` or something. Also being able to just compare to the previous result without saving would be great.

Sorry side tracked. As for this issue I don't see why I shouldn't be able to clear a comparison, load data or save data while the profiler is running. From a usability stand point it makes sense.

If we have technical limitations for doing these things while the profiler is running then disabling buttons is the way to go. I also don't think that a user needs to clear a comparison, load data or save data while the profiler is running.


Thanks for the input and the suggestion! We need to work a bit on the profiler to update with other panes. This one is a bit orphaned. :-) 
> Also being able to just compare to the previous result without saving would be great.

I think it is already done if you keep running the profiler

Resolving patch:
diff --git a/spyder/plugins/profiler/widgets/main_widget.py b/spyder/plugins/profiler/widgets/main_widget.py
--- a/spyder/plugins/profiler/widgets/main_widget.py
+++ b/spyder/plugins/profiler/widgets/main_widget.py
@@ -252,6 +252,7 @@ def setup(self):
             triggered=self.clear,
         )
         self.clear_action.setEnabled(False)
+        self.save_action.setEnabled(False)
 
         # Main Toolbar
         toolbar = self.get_main_toolbar()
@@ -299,6 +300,8 @@ def update_actions(self):
             icon = self.create_icon('run')
         self.start_action.setIcon(icon)
 
+        self.load_action.setEnabled(not self.running)
+        self.clear_action.setEnabled(not self.running)
         self.start_action.setEnabled(bool(self.filecombo.currentText()))
 
     # --- Private API
@@ -328,6 +331,7 @@ def _finished(self, exit_code, exit_status):
         self.output = self.error_output + self.output
         self.datelabel.setText('')
         self.show_data(justanalyzed=True)
+        self.save_action.setEnabled(True)
         self.update_actions()
 
     def _read_output(self, error=False):
@@ -368,6 +372,11 @@ def save_data(self):
             getcwd_or_home(),
             _("Profiler result") + " (*.Result)",
         )
+        extension = osp.splitext(filename)[1].lower()
+        if not extension:
+            # Needed to prevent trying to save a data file without extension
+            # See spyder-ide/spyder#19633
+            filename = filename + '.Result'
 
         if filename:
             self.datatree.save_data(filename)
@@ -384,6 +393,7 @@ def compare(self):
         if filename:
             self.datatree.compare(filename)
             self.show_data()
+            self.save_action.setEnabled(True)
             self.clear_action.setEnabled(True)
 
     def clear(self):
@@ -673,6 +683,7 @@ def __init__(self, parent=None):
         }
         self.profdata = None   # To be filled by self.load_data()
         self.stats = None      # To be filled by self.load_data()
+        self.stats1 = []       # To be filled by self.load_data()
         self.item_depth = None
         self.item_list = None
         self.items_to_be_shown = None
@@ -738,7 +749,8 @@ def hide_diff_cols(self, hide):
 
     def save_data(self, filename):
         """Save profiler data."""
-        self.stats1[0].dump_stats(filename)
+        if len(self.stats1) > 0:
+            self.stats1[0].dump_stats(filename)
 
     def find_root(self):
         """Find a function without a caller"""


Test patch: