Repository: spyder-ide/spyder

Issue:
Disambiguate dedicated IPython consoles
@dalthviz, please use a similar approach to the one you used in PR #3854 in the IPython console.

*Notes*:
* You only need to apply disambiguation to dedicated consoles.
* Dedicated consoles are created with the method called `create_client_for_file`.

----

For this we need to merge first PR #4691.


Discussion:

Resolving patch:
diff --git a/spyder/plugins/ipythonconsole.py b/spyder/plugins/ipythonconsole.py
--- a/spyder/plugins/ipythonconsole.py
+++ b/spyder/plugins/ipythonconsole.py
@@ -52,7 +52,7 @@
 from spyder.utils.ipython.style import create_qss_style
 from spyder.utils.qthelpers import create_action, MENU_SEPARATOR
 from spyder.utils import icon_manager as ima
-from spyder.utils import encoding, programs
+from spyder.utils import encoding, programs, sourcecode
 from spyder.utils.misc import get_error_match, remove_backslashes
 from spyder.widgets.findreplace import FindReplace
 from spyder.widgets.ipythonconsole import ClientWidget
@@ -607,6 +607,7 @@ def __init__(self, parent, testing=False):
 
         self.master_clients = 0
         self.clients = []
+        self.filenames = []
         self.mainwindow_close = False
         self.create_new_client_if_empty = True
         self.testing = testing
@@ -740,6 +741,7 @@ def refresh_plugin(self):
             sw = client.shellwidget
             self.variableexplorer.set_shellwidget_from_id(id(sw))
             self.help.set_shell(sw)
+        self.update_tabs_text()
         self.update_plugin_title.emit()
 
     def get_plugin_actions(self):
@@ -907,7 +909,9 @@ def write_to_stdin(self, line):
 
     @Slot()
     @Slot(bool)
-    def create_new_client(self, give_focus=True):
+    @Slot(str)
+    @Slot(bool, str)
+    def create_new_client(self, give_focus=True, filename=''):
         """Create a new client"""
         self.master_clients += 1
         client_id = dict(int_id=to_text_string(self.master_clients),
@@ -920,7 +924,7 @@ def create_new_client(self, give_focus=True):
                               interpreter_versions=self.interpreter_versions(),
                               connection_file=cf,
                               menu_actions=self.menu_actions)
-        self.add_tab(client, name=client.get_name())
+        self.add_tab(client, name=client.get_name(), filename=filename)
 
         if cf is None:
             error_msg = _("The directory {} is not writable and it is "
@@ -1192,8 +1196,10 @@ def close_client(self, index=None, client=None, force=False):
         # Note: client index may have changed after closing related widgets
         self.tabwidget.removeTab(self.tabwidget.indexOf(client))
         self.clients.remove(client)
+        self.filenames.pop(index)
         if not self.tabwidget.count() and self.create_new_client_if_empty:
             self.create_new_client()
+        self.update_tabs_text()
         self.update_plugin_title.emit()
 
     def get_client_index_from_id(self, client_id):
@@ -1265,7 +1271,7 @@ def create_client_from_path(self, path):
     def create_client_for_file(self, filename):
         """Create a client to execute code related to a file."""
         # Create client
-        self.create_new_client()
+        self.create_new_client(filename=filename)
 
         # Don't increase the count of master clients
         self.master_clients -= 1
@@ -1273,7 +1279,8 @@ def create_client_for_file(self, filename):
         # Rename client tab with filename
         client = self.get_current_client()
         client.allow_rename = False
-        self.rename_client_tab(client, filename)
+        tab_text = self.disambiguate_fname(filename)
+        self.rename_client_tab(client, tab_text)
 
     def get_client_for_file(self, filename):
         """Get client associated with a given file."""
@@ -1355,25 +1362,42 @@ def restart_kernel(self):
             client.restart_kernel()
 
     #------ Public API (for tabs) ---------------------------------------------
-    def add_tab(self, widget, name):
+    def add_tab(self, widget, name, filename=''):
         """Add tab"""
         self.clients.append(widget)
         index = self.tabwidget.addTab(widget, name)
+        self.filenames.insert(index, filename)
         self.tabwidget.setCurrentIndex(index)
         if self.dockwidget and not self.ismaximized:
             self.dockwidget.setVisible(True)
             self.dockwidget.raise_()
         self.activateWindow()
         widget.get_control().setFocus()
+        self.update_tabs_text()
         
     def move_tab(self, index_from, index_to):
         """
         Move tab (tabs themselves have already been moved by the tabwidget)
         """
+        filename = self.filenames.pop(index_from)
         client = self.clients.pop(index_from)
+        self.filenames.insert(index_to, filename)
         self.clients.insert(index_to, client)
         self.update_plugin_title.emit()
 
+    def disambiguate_fname(self, fname):
+        """Generate a file name without ambiguation."""
+        files_path_list = [filename for filename in self.filenames
+                           if filename]
+        return sourcecode.disambiguate_fname(files_path_list, fname)
+
+    def update_tabs_text(self):
+        """Update the text from the tabs."""
+        for index, fname in enumerate(self.filenames):
+            if fname:
+                client = self.clients[index]
+                self.rename_client_tab(client, self.disambiguate_fname(fname))
+
     def rename_client_tab(self, client, given_name):
         """Rename client's tab"""
         index = self.get_client_index_from_id(id(client))
diff --git a/spyder/utils/sourcecode.py b/spyder/utils/sourcecode.py
--- a/spyder/utils/sourcecode.py
+++ b/spyder/utils/sourcecode.py
@@ -183,7 +183,7 @@ def differentiate_prefix(path_components0, path_components1):
             path_0 = '/'
     return path_0
 
-def get_file_title(files_path_list, filename):
+def disambiguate_fname(files_path_list, filename):
     """Get tab title without ambiguation."""
     fname = os.path.basename(filename)
     same_name_files = get_same_name_files(files_path_list, fname)
diff --git a/spyder/widgets/editor.py b/spyder/widgets/editor.py
--- a/spyder/widgets/editor.py
+++ b/spyder/widgets/editor.py
@@ -1135,7 +1135,7 @@ def get_tab_text(self, index, is_modified=None, is_readonly=None):
         """Return tab title."""
         files_path_list = [finfo.filename for finfo in self.data]
         fname = self.data[index].filename
-        fname = sourcecode.get_file_title(files_path_list, fname)
+        fname = sourcecode.disambiguate_fname(files_path_list, fname)
         return self.__modified_readonly_title(fname,
                                               is_modified, is_readonly)
 


Test patch:
diff --git a/spyder/plugins/tests/test_ipythonconsole.py b/spyder/plugins/tests/test_ipythonconsole.py
--- a/spyder/plugins/tests/test_ipythonconsole.py
+++ b/spyder/plugins/tests/test_ipythonconsole.py
@@ -35,6 +35,7 @@
 #==============================================================================
 SHELL_TIMEOUT = 20000
 PYQT_WHEEL = PYQT_VERSION > '5.6'
+TEMP_DIRECTORY = tempfile.gettempdir()
 
 
 #==============================================================================
@@ -76,6 +77,40 @@ def close_console():
 #==============================================================================
 # Tests
 #==============================================================================
+@flaky(max_runs=3)
+def test_console_disambiguation(ipyconsole, qtbot):
+    """Test the disambiguation of dedicated consoles."""
+    # Create directories and file for TEMP_DIRECTORY/a/b/c.py
+    # and TEMP_DIRECTORY/a/d/c.py
+    dir_b = osp.join(TEMP_DIRECTORY, 'a', 'b')
+    filename_b =  osp.join(dir_b, 'c.py')
+    if not osp.isdir(dir_b):
+        os.makedirs(dir_b)
+    if not osp.isfile(filename_b):
+        file_c = open(filename_b, 'w+')
+        file_c.close()
+    dir_d = osp.join(TEMP_DIRECTORY, 'a', 'd')
+    filename_d =  osp.join(dir_d, 'c.py')
+    if not osp.isdir(dir_d):
+        os.makedirs(dir_d)
+    if not osp.isfile(filename_d):
+        file_e = open(filename_d, 'w+')
+        file_e.close()
+
+    # Create new client and assert name without disambiguation
+    ipyconsole.create_client_for_file(filename_b)
+    client = ipyconsole.get_current_client()
+    assert client.get_name() == 'c.py/A'
+
+    # Create new client and assert name with disambiguation
+    ipyconsole.create_client_for_file(filename_d)
+    client = ipyconsole.get_current_client()
+    assert client.get_name() == 'c.py - d/A'
+    ipyconsole.tabwidget.setCurrentIndex(1)
+    client = ipyconsole.get_current_client()
+    assert client.get_name() == 'c.py - b/A'
+
+
 @flaky(max_runs=3)
 def test_console_coloring(ipyconsole, qtbot):
 
@@ -99,6 +134,7 @@ def test_console_coloring(ipyconsole, qtbot):
     assert console_background_color.strip() == editor_background_color.strip()
     assert console_font_color.strip() == editor_font_color.strip()
 
+
 @flaky(max_runs=3)
 @pytest.mark.skipif(os.name == 'nt', reason="It doesn't work on Windows")
 def test_get_env(ipyconsole, qtbot):
diff --git a/spyder/utils/tests/test_sourcecode.py b/spyder/utils/tests/test_sourcecode.py
--- a/spyder/utils/tests/test_sourcecode.py
+++ b/spyder/utils/tests/test_sourcecode.py
@@ -84,7 +84,7 @@ def test_shortest_path():
         shortest_path = os.path.join(*['c:','','documents','test','test.py'])
     assert sourcecode.shortest_path(files_path_list) == shortest_path
 
-def test_get_file_title():
+def test_disambiguate_fname():
     files_path_list = []
     if sys.platform.startswith('linux'):
         fname0 = os.path.join(*['','','documents','test','test.py'])
@@ -98,8 +98,8 @@ def test_get_file_title():
         files_path_list.append(fname1)
     title0 = 'test.py - ' + os.path.join(*['test'])
     title1 = 'test.py - ' + os.path.join(*['projects','test'])
-    assert sourcecode.get_file_title(files_path_list, fname0) == title0
-    assert sourcecode.get_file_title(files_path_list, fname1) == title1
+    assert sourcecode.disambiguate_fname(files_path_list, fname0) == title0
+    assert sourcecode.disambiguate_fname(files_path_list, fname1) == title1
 
 if __name__ == '__main__':
     pytest.main()