Skip to content

Commit b2111bd

Browse files
Prevent symbolic link loops in import completion
Use os.path.realpath while crawling the filesystem for import completion make sure that we are not in a loop. Resolves #806
1 parent 012ea90 commit b2111bd

File tree

2 files changed

+101
-3
lines changed

2 files changed

+101
-3
lines changed

bpython/importcompletion.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@
4848

4949
# The cached list of all known modules
5050
modules = set()
51+
# List of stored paths to compare against so that real paths are not repeated
52+
# handles symlinks not mount points
53+
paths = set()
5154
fully_loaded = False
5255

5356

@@ -190,9 +193,12 @@ def find_modules(path):
190193
continue
191194
else:
192195
if is_package:
193-
for subname in find_modules(pathname):
194-
if subname != "__init__":
195-
yield "%s.%s" % (name, subname)
196+
path_real = os.path.realpath(pathname)
197+
if path_real not in paths:
198+
paths.add(path_real)
199+
for subname in find_modules(pathname):
200+
if subname != "__init__":
201+
yield "%s.%s" % (name, subname)
196202
yield name
197203

198204

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from bpython.test import unittest
2+
from bpython.importcompletion import find_modules
3+
import os, sys, tempfile
4+
5+
6+
@unittest.skipIf(sys.version_info[0] <= 2, "Test doesn't work in python 2.")
7+
class TestAvoidSymbolicLinks(unittest.TestCase):
8+
def setUp(self):
9+
with tempfile.TemporaryDirectory() as import_test_folder:
10+
os.mkdir(os.path.join(import_test_folder, "Level0"))
11+
os.mkdir(os.path.join(import_test_folder, "Right"))
12+
os.mkdir(os.path.join(import_test_folder, "Left"))
13+
14+
current_path = os.path.join(import_test_folder, "Level0")
15+
with open(
16+
os.path.join(current_path, "__init__.py"), "x"
17+
) as init_file:
18+
pass
19+
20+
current_path = os.path.join(current_path, "Level1")
21+
os.mkdir(current_path)
22+
with open(
23+
os.path.join(current_path, "__init__.py"), "x"
24+
) as init_file:
25+
pass
26+
27+
current_path = os.path.join(current_path, "Level2")
28+
os.mkdir(current_path)
29+
with open(
30+
os.path.join(current_path, "__init__.py"), "x"
31+
) as init_file:
32+
pass
33+
34+
os.symlink(
35+
os.path.join(import_test_folder, "Level0/Level1"),
36+
os.path.join(current_path, "Level3"),
37+
True,
38+
)
39+
40+
current_path = os.path.join(import_test_folder, "Right")
41+
with open(
42+
os.path.join(current_path, "__init__.py"), "x"
43+
) as init_file:
44+
pass
45+
46+
os.symlink(
47+
os.path.join(import_test_folder, "Left"),
48+
os.path.join(current_path, "toLeft"),
49+
True,
50+
)
51+
52+
current_path = os.path.join(import_test_folder, "Left")
53+
with open(
54+
os.path.join(current_path, "__init__.py"), "x"
55+
) as init_file:
56+
pass
57+
58+
os.symlink(
59+
os.path.join(import_test_folder, "Right"),
60+
os.path.join(current_path, "toRight"),
61+
True,
62+
)
63+
64+
self.foo = list(find_modules(os.path.abspath(import_test_folder)))
65+
self.filepaths = [
66+
"Left.toRight.toLeft",
67+
"Left.toRight",
68+
"Left",
69+
"Level0.Level1.Level2.Level3",
70+
"Level0.Level1.Level2",
71+
"Level0.Level1",
72+
"Level0",
73+
"Right",
74+
"Right.toLeft",
75+
"Right.toLeft.toRight",
76+
]
77+
78+
def test_simple_symbolic_link_loop(self):
79+
for thing in self.foo:
80+
self.assertTrue(thing in self.filepaths)
81+
if thing == "Left.toRight.toLeft":
82+
self.filepaths.remove("Right.toLeft")
83+
self.filepaths.remove("Right.toLeft.toRight")
84+
if thing == "Right.toLeft.toRight":
85+
self.filepaths.remove("Left.toRight.toLeft")
86+
self.filepaths.remove("Left.toRight")
87+
self.filepaths.remove(thing)
88+
self.assertFalse(self.filepaths)
89+
90+
91+
if __name__ == "__main__":
92+
unittest.main()

0 commit comments

Comments
 (0)