Coverage for tests / test_python_env.py: 100%

146 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-03 20:19 +0000

1from pathlib import Path 1abcdefghi

2 

3from library_skills.python_env import ( 1abcdefghi

4 find_node_modules, 

5 find_project_root, 

6 find_venv, 

7 get_site_packages_dir, 

8) 

9 

10 

11def make_venv(path: Path) -> Path: 1abcdefghi

12 path.mkdir(parents=True) 1QpmyRzAqrSsnBTCDtuUvoEVFGwx

13 (path / "pyvenv.cfg").write_text("home = /usr/bin\n", encoding="utf-8") 1QpmyRzAqrSsnBTCDtuUvoEVFGwx

14 return path 1QpmyRzAqrSsnBTCDtuUvoEVFGwx

15 

16 

17def clear_python_env_vars(monkeypatch) -> None: 1abcdefghi

18 monkeypatch.delenv("UV_PROJECT_ENVIRONMENT", raising=False) 1jNQpmyRzWAqrkOSsnBTCXDtulPUvoEVFYGwx

19 monkeypatch.delenv("VIRTUAL_ENV", raising=False) 1jNQpmyRzWAqrkOSsnBTCXDtulPUvoEVFYGwx

20 monkeypatch.delenv("CONDA_PREFIX", raising=False) 1jNQpmyRzWAqrkOSsnBTCXDtulPUvoEVFYGwx

21 

22 

23def test_find_project_root_uses_nearest_python_project_marker(tmp_path): 1abcdefghi

24 root = tmp_path / "repo" 1#$%'()*+,

25 subproject = root / "packages" / "api" 1#$%'()*+,

26 nested = subproject / "src" / "pkg" 1#$%'()*+,

27 root.mkdir(parents=True) 1#$%'()*+,

28 subproject.mkdir(parents=True) 1#$%'()*+,

29 nested.mkdir(parents=True) 1#$%'()*+,

30 (root / "pyproject.toml").write_text("[project]\nname = 'root'\n", encoding="utf-8") 1#$%'()*+,

31 (subproject / "uv.lock").write_text("", encoding="utf-8") 1#$%'()*+,

32 

33 assert find_project_root(nested) == subproject 1#$%'()*+,

34 

35 

36def test_find_project_root_uses_node_project_markers(tmp_path): 1abcdefghi

37 project = tmp_path / "project" 1-./:;=?@[

38 nested = project / "src" 1-./:;=?@[

39 nested.mkdir(parents=True) 1-./:;=?@[

40 (project / "package.json").write_text("{}", encoding="utf-8") 1-./:;=?@[

41 

42 assert find_project_root(nested) == project 1-./:;=?@[

43 

44 (project / "package.json").unlink() 1-./:;=?@[

45 (project / "node_modules").mkdir() 1-./:;=?@[

46 

47 assert find_project_root(nested) == project 1-./:;=?@[

48 

49 

50def test_find_venv_prefers_uv_project_environment(monkeypatch, tmp_path): 1abcdefghi

51 project = tmp_path / "project" 1pqrstuvwx

52 project.mkdir() 1pqrstuvwx

53 (project / "pyproject.toml").write_text( 1pqrstuvwx

54 "[project]\nname = 'demo'\n", encoding="utf-8" 

55 ) 

56 uv_env = make_venv(project / ".custom-venv") 1pqrstuvwx

57 dot_venv = make_venv(project / ".venv") 1pqrstuvwx

58 clear_python_env_vars(monkeypatch) 1pqrstuvwx

59 monkeypatch.setenv("UV_PROJECT_ENVIRONMENT", ".custom-venv") 1pqrstuvwx

60 monkeypatch.setenv("VIRTUAL_ENV", str(dot_venv)) 1pqrstuvwx

61 

62 assert find_venv(project) == uv_env 1pqrstuvwx

63 

64 

65def test_find_venv_resolves_uv_project_environment_from_workspace_root( 1abcdefghi

66 monkeypatch, 

67 tmp_path, 

68): 

69 workspace = tmp_path / "workspace" 1mZ0n12o34

70 member = workspace / "packages" / "api" 1mZ0n12o34

71 member.mkdir(parents=True) 1mZ0n12o34

72 (workspace / "pyproject.toml").write_text( 1mZ0n12o34

73 """ 

74[tool.uv.workspace] 

75members = ["packages/*"] 

76""", 

77 encoding="utf-8", 

78 ) 

79 (member / "pyproject.toml").write_text( 1mZ0n12o34

80 "[project]\nname = 'api'\nversion = '0.1.0'\n", 

81 encoding="utf-8", 

82 ) 

83 custom_env = make_venv(workspace / ".custom-venv") 1mZ0n12o34

84 make_venv(member / ".custom-venv") 1mZ0n12o34

85 clear_python_env_vars(monkeypatch) 1mZ0n12o34

86 monkeypatch.setenv("UV_PROJECT_ENVIRONMENT", ".custom-venv") 1mZ0n12o34

87 

88 assert find_venv(member) == custom_env 1mZ0n12o34

89 

90 

91def test_find_venv_returns_none_for_workspace_without_root_environment( 1abcdefghi

92 monkeypatch, 

93 tmp_path, 

94): 

95 workspace = tmp_path / "workspace" 1y]^B_`E{|

96 member = workspace / "packages" / "api" 1y]^B_`E{|

97 member.mkdir(parents=True) 1y]^B_`E{|

98 (workspace / "pyproject.toml").write_text( 1y]^B_`E{|

99 """ 

100[tool.uv.workspace] 

101members = ["packages/*"] 

102""", 

103 encoding="utf-8", 

104 ) 

105 (member / "pyproject.toml").write_text( 1y]^B_`E{|

106 "[project]\nname = 'api'\nversion = '0.1.0'\n", 

107 encoding="utf-8", 

108 ) 

109 make_venv(member / ".venv") 1y]^B_`E{|

110 clear_python_env_vars(monkeypatch) 1y]^B_`E{|

111 

112 assert find_venv(member) is None 1y]^B_`E{|

113 

114 

115def test_find_venv_supports_pep_832_redirect_file(monkeypatch, tmp_path): 1abcdefghi

116 project = tmp_path / "project" 2z } ~ C abbbF cbdb

117 nested = project / "src" / "pkg" 2z } ~ C abbbF cbdb

118 nested.mkdir(parents=True) 2z } ~ C abbbF cbdb

119 venv = make_venv(tmp_path / "envs" / "project") 2z } ~ C abbbF cbdb

120 (project / ".venv").write_text("../envs/project\nignored\n", encoding="utf-8") 2z } ~ C abbbF cbdb

121 clear_python_env_vars(monkeypatch) 2z } ~ C abbbF cbdb

122 

123 assert find_project_root(nested) == project 2z } ~ C abbbF cbdb

124 assert find_venv(nested) == venv 2z } ~ C abbbF cbdb

125 

126 

127def test_find_venv_supports_absolute_pep_832_redirect_file(monkeypatch, tmp_path): 1abcdefghi

128 project = tmp_path / "project" 2R IbJbT KbLbV MbNb

129 project.mkdir() 2R IbJbT KbLbV MbNb

130 venv = make_venv(tmp_path / "env") 2R IbJbT KbLbV MbNb

131 (project / ".venv").write_text(str(venv), encoding="utf-8") 2R IbJbT KbLbV MbNb

132 clear_python_env_vars(monkeypatch) 2R IbJbT KbLbV MbNb

133 

134 assert find_venv(project) == venv 2R IbJbT KbLbV MbNb

135 

136 

137def test_find_venv_ignores_invalid_pep_832_redirect_file(monkeypatch, tmp_path): 1abcdefghi

138 project = tmp_path / "project" 1N56O78P9!

139 nested = project / "pkg" 1N56O78P9!

140 nested.mkdir(parents=True) 1N56O78P9!

141 (project / ".venv").write_text("missing\n", encoding="utf-8") 1N56O78P9!

142 clear_python_env_vars(monkeypatch) 1N56O78P9!

143 

144 assert find_project_root(nested) is None 1N56O78P9!

145 assert find_venv(nested) is None 1N56O78P9!

146 

147 (project / ".venv").write_text("", encoding="utf-8") 1N56O78P9!

148 assert find_project_root(nested) is None 1N56O78P9!

149 assert find_venv(nested) is None 1N56O78P9!

150 

151 

152def test_find_venv_ignores_invalid_pep_832_entries(monkeypatch, tmp_path): 1abcdefghi

153 project = tmp_path / "project" 1jHIkJKlLM

154 nested = project / "pkg" 1jHIkJKlLM

155 nested.mkdir(parents=True) 1jHIkJKlLM

156 (project / ".venv").mkdir() 1jHIkJKlLM

157 clear_python_env_vars(monkeypatch) 1jHIkJKlLM

158 

159 assert find_project_root(nested) is None 1jHIkJKlLM

160 assert find_venv(nested) is None 1jHIkJKlLM

161 

162 (project / ".venv").rmdir() 1jHIkJKlLM

163 (project / ".venv").write_text("env\n", encoding="utf-8") 1jHIkJKlLM

164 

165 def read_text(self: Path, *args, **kwargs) -> str: 1jHIkJKlLM

166 raise OSError 1jHIkJKlLM

167 

168 monkeypatch.setattr(Path, "read_text", read_text) 1jHIkJKlLM

169 assert find_project_root(nested) is None 1jHIkJKlLM

170 assert find_venv(nested) is None 1jHIkJKlLM

171 

172 

173def test_find_venv_ignores_virtual_env_outside_project(monkeypatch, tmp_path): 1abcdefghi

174 project = tmp_path / "project" 2Q ObPbS QbRbU SbTb

175 project.mkdir() 2Q ObPbS QbRbU SbTb

176 outside = make_venv(tmp_path / "tool-env") 2Q ObPbS QbRbU SbTb

177 clear_python_env_vars(monkeypatch) 2Q ObPbS QbRbU SbTb

178 monkeypatch.setenv("VIRTUAL_ENV", str(outside)) 2Q ObPbS QbRbU SbTb

179 

180 assert find_venv(project) is None 2Q ObPbS QbRbU SbTb

181 

182 

183def test_find_venv_uses_virtual_env_inside_project(monkeypatch, tmp_path): 1abcdefghi

184 project = tmp_path / "project" 2A ebfbD gbhbG ibjb

185 nested = project / "src" 2A ebfbD gbhbG ibjb

186 nested.mkdir(parents=True) 2A ebfbD gbhbG ibjb

187 (project / "pyproject.toml").write_text( 2A ebfbD gbhbG ibjb

188 "[project]\nname = 'demo'\n", encoding="utf-8" 

189 ) 

190 virtual_env = make_venv(project / "env") 2A ebfbD gbhbG ibjb

191 clear_python_env_vars(monkeypatch) 2A ebfbD gbhbG ibjb

192 monkeypatch.setenv("VIRTUAL_ENV", str(virtual_env)) 2A ebfbD gbhbG ibjb

193 

194 assert find_venv(nested) == virtual_env 2A ebfbD gbhbG ibjb

195 

196 

197def test_find_venv_uses_conda_prefix_when_no_project_env(monkeypatch, tmp_path): 1abcdefghi

198 project = tmp_path / "project" 2W tbubX vbwbY xbyb

199 project.mkdir() 2W tbubX vbwbY xbyb

200 conda = tmp_path / "conda-env" 2W tbubX vbwbY xbyb

201 conda.mkdir() 2W tbubX vbwbY xbyb

202 clear_python_env_vars(monkeypatch) 2W tbubX vbwbY xbyb

203 monkeypatch.setenv("CONDA_PREFIX", str(conda)) 2W tbubX vbwbY xbyb

204 

205 assert find_venv(project) == conda 2W tbubX vbwbY xbyb

206 

207 

208def test_get_site_packages_dir_supports_windows_lib_and_lib64(tmp_path): 1abcdefghi

209 windows_env = tmp_path / "windows" 2kblbmbnbobpbqbrbsb

210 windows_site = windows_env / "Lib" / "site-packages" 2kblbmbnbobpbqbrbsb

211 windows_site.mkdir(parents=True) 2kblbmbnbobpbqbrbsb

212 

213 linux_env = tmp_path / "linux" 2kblbmbnbobpbqbrbsb

214 linux_site = linux_env / "lib64" / "python3.12" / "site-packages" 2kblbmbnbobpbqbrbsb

215 linux_site.mkdir(parents=True) 2kblbmbnbobpbqbrbsb

216 

217 assert get_site_packages_dir(windows_env) == windows_site 2kblbmbnbobpbqbrbsb

218 assert get_site_packages_dir(linux_env) == linux_site 2kblbmbnbobpbqbrbsb

219 

220 

221def test_get_site_packages_dir_returns_none_without_site_packages(tmp_path): 1abcdefghi

222 venv = tmp_path / "venv" 2UbVbWbXbYbZb0b1b2b

223 (venv / "lib" / "python3.12").mkdir(parents=True) 2UbVbWbXbYbZb0b1b2b

224 

225 assert get_site_packages_dir(venv) is None 2UbVbWbXbYbZb0b1b2b

226 

227 

228def test_find_node_modules_uses_nearest_ancestor(tmp_path): 1abcdefghi

229 project = tmp_path / "project" 2zbAbBbCbDbEbFbGbHb

230 nested = project / "src" / "pkg" 2zbAbBbCbDbEbFbGbHb

231 node_modules = project / "node_modules" 2zbAbBbCbDbEbFbGbHb

232 nested.mkdir(parents=True) 2zbAbBbCbDbEbFbGbHb

233 node_modules.mkdir() 2zbAbBbCbDbEbFbGbHb

234 

235 assert find_node_modules(nested) == node_modules 2zbAbBbCbDbEbFbGbHb

236 assert find_node_modules(tmp_path / "missing") is None 2zbAbBbCbDbEbFbGbHb