diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 0fbc46d58..745a4cea6 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -48,6 +48,9 @@ # The cached list of all known modules modules = set() +# List of stored paths to compare against so that real paths are not repeated +# handles symlinks not mount points +paths = set() fully_loaded = False @@ -190,9 +193,12 @@ def find_modules(path): continue else: if is_package: - for subname in find_modules(pathname): - if subname != "__init__": - yield "%s.%s" % (name, subname) + path_real = os.path.realpath(pathname) + if path_real not in paths: + paths.add(path_real) + for subname in find_modules(pathname): + if subname != "__init__": + yield "%s.%s" % (name, subname) yield name diff --git a/bpython/test/test_import_not_cyclical.py b/bpython/test/test_import_not_cyclical.py new file mode 100644 index 000000000..4e2d99c35 --- /dev/null +++ b/bpython/test/test_import_not_cyclical.py @@ -0,0 +1,92 @@ +from bpython.test import unittest +from bpython.importcompletion import find_modules +import os, sys, tempfile + + +@unittest.skipIf(sys.version_info[0] <= 2, "Test doesn't work in python 2.") +class TestAvoidSymbolicLinks(unittest.TestCase): + def setUp(self): + with tempfile.TemporaryDirectory() as import_test_folder: + os.mkdir(os.path.join(import_test_folder, "Level0")) + os.mkdir(os.path.join(import_test_folder, "Right")) + os.mkdir(os.path.join(import_test_folder, "Left")) + + current_path = os.path.join(import_test_folder, "Level0") + with open( + os.path.join(current_path, "__init__.py"), "x" + ) as init_file: + pass + + current_path = os.path.join(current_path, "Level1") + os.mkdir(current_path) + with open( + os.path.join(current_path, "__init__.py"), "x" + ) as init_file: + pass + + current_path = os.path.join(current_path, "Level2") + os.mkdir(current_path) + with open( + os.path.join(current_path, "__init__.py"), "x" + ) as init_file: + pass + + os.symlink( + os.path.join(import_test_folder, "Level0/Level1"), + os.path.join(current_path, "Level3"), + True, + ) + + current_path = os.path.join(import_test_folder, "Right") + with open( + os.path.join(current_path, "__init__.py"), "x" + ) as init_file: + pass + + os.symlink( + os.path.join(import_test_folder, "Left"), + os.path.join(current_path, "toLeft"), + True, + ) + + current_path = os.path.join(import_test_folder, "Left") + with open( + os.path.join(current_path, "__init__.py"), "x" + ) as init_file: + pass + + os.symlink( + os.path.join(import_test_folder, "Right"), + os.path.join(current_path, "toRight"), + True, + ) + + self.foo = list(find_modules(os.path.abspath(import_test_folder))) + self.filepaths = [ + "Left.toRight.toLeft", + "Left.toRight", + "Left", + "Level0.Level1.Level2.Level3", + "Level0.Level1.Level2", + "Level0.Level1", + "Level0", + "Right", + "Right.toLeft", + "Right.toLeft.toRight", + ] + + def test_simple_symbolic_link_loop(self): + for thing in self.foo: + self.assertTrue(thing in self.filepaths) + if thing == "Left.toRight.toLeft": + self.filepaths.remove("Right.toLeft") + self.filepaths.remove("Right.toLeft.toRight") + if thing == "Right.toLeft.toRight": + self.filepaths.remove("Left.toRight.toLeft") + self.filepaths.remove("Left.toRight") + self.filepaths.remove(thing) + self.assertFalse(self.filepaths) + + +if __name__ == "__main__": + unittest.main()