Repository: spyder-ide/spyder

Issue:
`spyder-line-profiler` fails silently with latest 5.x due to missing import in `py3compat`
<!--- **PLEASE READ:** When submitting here, please ensure you've completed the following checklist and checked the boxes to confirm. Issue reports without it may be closed. Thanks! --->

## Problem Description

Running Spyder 5.x from `bootstrap.py` in an env with `spyder-line-profiler` 0.3.1 shows a traceback in the cmd/terminal from where Spyder was launched and the plugin doesn't load.

Maybe related with https://github.com/spyder-ide/spyder/pull/20366 and probably also affects other external plugins which rely on the `py3compat` module

### What steps reproduce the problem?

1. Install  `spyder-line-profiler` 0.3.1 
2. Run Spyder from `bootstrap.py`

### What is the expected output? What do you see instead?

The plugin to be able to load

### Paste Traceback/Error Below (if applicable)
<!--- Copy from error dialog or View > Panes > Internal Console --->

```python-traceback
spyder_line_profiler: cannot import name 'pickle' from 'spyder.py3compat' (e:\acer\documentos\spyder\spyder\spyder\py3compat.py)
Traceback (most recent call last):
  File "e:\acer\documentos\spyder\spyder\spyder\app\find_plugins.py", line 67, in find_external_plugins
    mod = importlib.import_module(entry_point.module_name)
  File "C:\Users\dalth\anaconda3\envs\spyder-env-manager\lib\importlib\__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1030, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1007, in _find_and_load
  File "<frozen importlib._bootstrap>", line 986, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 680, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 850, in exec_module
  File "<frozen importlib._bootstrap>", line 228, in _call_with_frames_removed
  File "C:\Users\dalth\anaconda3\envs\spyder-env-manager\lib\site-packages\spyder_line_profiler\spyder\plugin.py", line 29, in <module>
    from spyder_line_profiler.spyder.confpage import SpyderLineProfilerConfigPage
  File "C:\Users\dalth\anaconda3\envs\spyder-env-manager\lib\site-packages\spyder_line_profiler\spyder\confpage.py", line 19, in <module>
    from .widgets import SpyderLineProfilerWidget
  File "C:\Users\dalth\anaconda3\envs\spyder-env-manager\lib\site-packages\spyder_line_profiler\spyder\widgets.py", line 38, in <module>
    from spyder.py3compat import to_text_string, pickle
ImportError: cannot import name 'pickle' from 'spyder.py3compat' (e:\acer\documentos\spyder\spyder\spyder\py3compat.py)
```

## Versions
<!--- You can get this information from Help > About Spyder...
or (if Spyder won't launch) the "conda list" command
from the Anaconda Prompt/Terminal/command line. --->

* Spyder version: 5.5.0.dev0 4a5d86ecd (conda)
* Python version: 3.9.15 64-bit
* Qt version: 5.15.2
* PyQt5 version: 5.15.7
* Operating System: Windows 10




Discussion:
Thanks @dalthviz for reporting. I didn't know external plugins were importing directly from our `py3compat` module, which I think it's really bad because that's not public API. Fortunately, this is easy to solve: we only need to restore the missing `pickle` import in `py3compat` and remove it in `master`, which I'll do right away so that previous versions of `spyder-line-profiler` don't break.

I also quickly checked if the other external plugins we maintain (unittest, terminal, vim and notebook) import from `py3compat`, but fortunately they don't.
@dalthviz, could you also open a PR in `spyder-line-profiler` to remove all references to `py3compat`? That way that plugin will be ready when we remove that module in Spyder 6.

Resolving patch:
diff --git a/spyder/py3compat.py b/spyder/py3compat.py
--- a/spyder/py3compat.py
+++ b/spyder/py3compat.py
@@ -10,13 +10,10 @@
 
 Transitional module providing compatibility functions intended to help
 migrating from Python 2 to Python 3.
-
-This module should be fully compatible with:
-    * Python >=v2.6
-    * Python 3
 """
 
 import operator
+import pickle  # noqa. For compatibility with spyder-line-profiler
 
 
 #==============================================================================
@@ -66,8 +63,6 @@ def to_binary_string(obj, encoding='utf-8'):
 #==============================================================================
 # Misc.
 #==============================================================================
-# Python 3
-
 def qbytearray_to_str(qba):
     """Convert QByteArray object to str in a way compatible with Python 3"""
     return str(bytes(qba.toHex().data()).decode())


Test patch:
diff --git a/spyder/app/tests/conftest.py b/spyder/app/tests/conftest.py
--- a/spyder/app/tests/conftest.py
+++ b/spyder/app/tests/conftest.py
@@ -214,6 +214,28 @@ def create_namespace_project(tmpdir):
     spy_project.set_recent_files(abs_filenames)
 
 
+def preferences_dialog_helper(qtbot, main_window, section):
+    """
+    Open preferences dialog and select page with `section` (CONF_SECTION).
+    """
+    # Wait until the window is fully up
+    shell = main_window.ipyconsole.get_current_shellwidget()
+    qtbot.waitUntil(
+        lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT)
+
+    main_window.show_preferences()
+    preferences = main_window.preferences
+    container = preferences.get_container()
+
+    qtbot.waitUntil(lambda: container.dialog is not None,
+                    timeout=5000)
+    dlg = container.dialog
+    index = dlg.get_index_by_name(section)
+    page = dlg.get_page(index)
+    dlg.set_current_index(index)
+    return dlg, index, page
+
+
 # =============================================================================
 # ---- Pytest hooks
 # =============================================================================
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
@@ -47,8 +47,9 @@
 from spyder.api.plugins import Plugins
 from spyder.app.tests.conftest import (
     COMPILE_AND_EVAL_TIMEOUT, COMPLETION_TIMEOUT, EVAL_TIMEOUT,
-    find_desired_tab_in_window, LOCATION, open_file_in_editor, PY37,
-    read_asset_file, reset_run_code, SHELL_TIMEOUT, start_new_kernel)
+    find_desired_tab_in_window, LOCATION, open_file_in_editor,
+    preferences_dialog_helper, PY37, read_asset_file, reset_run_code,
+    SHELL_TIMEOUT, start_new_kernel)
 from spyder.config.base import (
     get_home_dir, get_conf_path, get_module_path, running_in_ci)
 from spyder.config.manager import CONF
@@ -1004,6 +1005,7 @@ def test_run_cython_code(main_window, qtbot):
     main_window.editor.close_file()
 
 
+@flaky(max_runs=5)
 def test_project_path(main_window, tmpdir, qtbot):
     """Test project path added to spyder_pythonpath and IPython Console."""
     projects = main_window.projects
@@ -2627,30 +2629,7 @@ def test_break_while_running(main_window, qtbot, tmpdir):
     main_window.editor.clear_all_breakpoints()
 
 
-# --- Preferences
-# ----------------------------------------------------------------------------
-def preferences_dialog_helper(qtbot, main_window, section):
-    """
-    Open preferences dialog and select page with `section` (CONF_SECTION).
-    """
-    # Wait until the window is fully up
-    shell = main_window.ipyconsole.get_current_shellwidget()
-    qtbot.waitUntil(
-        lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT)
-
-    main_window.show_preferences()
-    preferences = main_window.preferences
-    container = preferences.get_container()
-
-    qtbot.waitUntil(lambda: container.dialog is not None,
-                    timeout=5000)
-    dlg = container.dialog
-    index = dlg.get_index_by_name(section)
-    page = dlg.get_page(index)
-    dlg.set_current_index(index)
-    return dlg, index, page
-
-
+@flaky(max_runs=5)
 def test_preferences_run_section_exists(main_window, qtbot):
     """
     Test for spyder-ide/spyder#13524 regression.
@@ -5105,6 +5084,7 @@ def test_debug_unsaved_function(main_window, qtbot):
     assert "1---> 2     print(1)" in control.toPlainText()
 
 
+@flaky(max_runs=5)
 @pytest.mark.close_main_window
 def test_out_runfile_runcell(main_window, qtbot):
     """
@@ -5207,6 +5187,7 @@ def test_cwd_is_synced_when_switching_consoles(main_window, qtbot, tmpdir):
         assert shell_cwd == workdir.get_workdir() == files.get_current_folder()
 
 
+@flaky(max_runs=5)
 def test_console_initial_cwd_is_synced(main_window, qtbot, tmpdir):
     """
     Test that the initial current working directory for new consoles is synced
@@ -5334,6 +5315,7 @@ def editors_filled():
 @pytest.mark.skipif(
     sys.platform == 'darwin',
     reason="Only works on Windows and Linux")
+@pytest.mark.order(before='test_tour_message')
 def test_switch_to_plugin(main_window, qtbot):
     """
     Test that switching between the two most important plugins, the Editor and
@@ -5359,6 +5341,7 @@ def test_switch_to_plugin(main_window, qtbot):
     assert QApplication.focusWidget() is code_editor
 
 
+@flaky(max_runs=5)
 def test_PYTHONPATH_in_consoles(main_window, qtbot, tmp_path):
     """
     Test that PYTHONPATH is passed to IPython consoles under different