diff --git a/spyder_kernels/console/tests/test_console_kernel.py b/spyder_kernels/console/tests/test_console_kernel.py index 6cb81741..b7bbbc6b 100644 --- a/spyder_kernels/console/tests/test_console_kernel.py +++ b/spyder_kernels/console/tests/test_console_kernel.py @@ -1391,5 +1391,26 @@ def test_django_settings(kernel): assert "'settings':" in nsview +def test_hard_link_pdb(tmpdir): + """ + Test that breakpoints on a file are recognised even when the path is + different. + """ + # Create a file and a hard link + d = tmpdir.join("file.py") + d.write('def func():\n bb = "hello"\n') + folder = tmpdir.join("folder") + os.mkdir(folder) + hard_link = folder.join("file.py") + os.link(d, hard_link) + + # Make sure both paths point to the same file + assert os.path.samefile(d, hard_link) + + # Make sure canonic returns the same path for a single file + pdb_obj = SpyderPdb() + assert pdb_obj.canonic(str(d)) == pdb_obj.canonic(str(hard_link)) + + if __name__ == "__main__": pytest.main() diff --git a/spyder_kernels/customize/spyderpdb.py b/spyder_kernels/customize/spyderpdb.py index 944bd1e5..8fd4e975 100755 --- a/spyder_kernels/customize/spyderpdb.py +++ b/spyder_kernels/customize/spyderpdb.py @@ -118,6 +118,10 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, # Fixes spyder-ide/spyder#20639. self._predicates["debuggerskip"] = False + # Save seen files inodes + self._canonic_inode_to_filename = {} + self._canonic_filename_to_inode = {} + # --- Methods overriden for code execution def print_exclamation_warning(self): """Print pdb warning for exclamation mark.""" @@ -621,8 +625,42 @@ def _cmdloop(self): @lru_cache def canonic(self, filename): - """Return canonical form of filename.""" - return super().canonic(filename) + """ + Return canonical form of filename. + + In some case two path can point to the same file. For this reason + os.path.samefile uses os.stat. Here we normalise the path with os.stat + so a single path is returned for the same file. + + see: https://docs.python.org/3/library/os.path.html#os.path.samefile + note: os.stat can be slow on windows so call it once per file. + """ + if filename == "<" + filename[1:-1] + ">": + return filename + + filename = super().canonic(filename) + + if filename in self._canonic_filename_to_inode: + inode = self._canonic_filename_to_inode[filename] + else: + try: + stat = os.stat(filename) + except OSError: + self._canonic_filename_to_inode[filename] = None + return filename + + inode = (stat.st_dev, stat.st_ino) + if stat.st_ino == 0: + inode = None + self._canonic_filename_to_inode[filename] = inode + if inode is not None and inode not in self._canonic_inode_to_filename: + # First time this inode is seen + self._canonic_inode_to_filename[inode] = filename + + if inode is None: + return filename + return self._canonic_inode_to_filename[inode] + def do_exitdb(self, arg): """Exit the debugger"""