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
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-03 20:19 +0000
1from pathlib import Path 1abcdefghi
3from library_skills.python_env import ( 1abcdefghi
4 find_node_modules,
5 find_project_root,
6 find_venv,
7 get_site_packages_dir,
8)
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
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
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#$%'()*+,
33 assert find_project_root(nested) == subproject 1#$%'()*+,
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-./:;=?@[
42 assert find_project_root(nested) == project 1-./:;=?@[
44 (project / "package.json").unlink() 1-./:;=?@[
45 (project / "node_modules").mkdir() 1-./:;=?@[
47 assert find_project_root(nested) == project 1-./:;=?@[
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
62 assert find_venv(project) == uv_env 1pqrstuvwx
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
88 assert find_venv(member) == custom_env 1mZ0n12o34
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{|
112 assert find_venv(member) is None 1y]^B_`E{|
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
123 assert find_project_root(nested) == project 2z } ~ C abbbF cbdb
124 assert find_venv(nested) == venv 2z } ~ C abbbF cbdb
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
134 assert find_venv(project) == venv 2R IbJbT KbLbV MbNb
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!
144 assert find_project_root(nested) is None 1N56O78P9!
145 assert find_venv(nested) is None 1N56O78P9!
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!
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
159 assert find_project_root(nested) is None 1jHIkJKlLM
160 assert find_venv(nested) is None 1jHIkJKlLM
162 (project / ".venv").rmdir() 1jHIkJKlLM
163 (project / ".venv").write_text("env\n", encoding="utf-8") 1jHIkJKlLM
165 def read_text(self: Path, *args, **kwargs) -> str: 1jHIkJKlLM
166 raise OSError 1jHIkJKlLM
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
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
180 assert find_venv(project) is None 2Q ObPbS QbRbU SbTb
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
194 assert find_venv(nested) == virtual_env 2A ebfbD gbhbG ibjb
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
205 assert find_venv(project) == conda 2W tbubX vbwbY xbyb
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
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
217 assert get_site_packages_dir(windows_env) == windows_site 2kblbmbnbobpbqbrbsb
218 assert get_site_packages_dir(linux_env) == linux_site 2kblbmbnbobpbqbrbsb
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
225 assert get_site_packages_dir(venv) is None 2UbVbWbXbYbZb0b1b2b
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
235 assert find_node_modules(nested) == node_modules 2zbAbBbCbDbEbFbGbHb
236 assert find_node_modules(tmp_path / "missing") is None 2zbAbBbCbDbEbFbGbHb