Skip to content

Commit ad2f667

Browse files
authored
[ty] Improve tests for site-packages discovery (#18374)
## Summary - Convert tests demonstrating our resilience to malformed/absent `version` fields in `pyvenf.cfg` files to mdtests. Also make them more expansive. - Convert the regression test I added in #18157 to an mdtest - Add comments next to unit tests that cannot be converted to mdtests (but where it's not obvious why they can't) so I don't have to do this exercise again 😄 - In `site_packages.rs`, factor out the logic for figuring out where we expect the system-installation `site-packages` to be. Currently we have the same logic twice. ## Test Plan `cargo test -p ty_python_semantic`
1 parent 363f061 commit ad2f667

File tree

2 files changed

+175
-65
lines changed

2 files changed

+175
-65
lines changed

crates/ty_python_semantic/resources/mdtest/import/site_packages_discovery.md

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,115 @@
11
# Tests for `site-packages` discovery
22

3+
## Malformed or absent `version` fields
4+
5+
The `version`/`version_info` key in a `pyvenv.cfg` file is provided by most virtual-environment
6+
creation tools to indicate the Python version the virtual environment is for. They key is useful for
7+
our purposes, so we try to parse it when possible. However, the key is not read by the CPython
8+
standard library, and is provided under different keys depending on which virtual-environment
9+
creation tool created the `pyvenv.cfg` file (the stdlib `venv` module calls the key `version`,
10+
whereas uv and virtualenv both call it `version_info`). We therefore do not return an error when
11+
discovering a virtual environment's `site-packages` directory if the virtula environment contains a
12+
`pyvenv.cfg` file which doesn't have this key, or if the associated value of the key doesn't parse
13+
according to our expectations. The file isn't really *invalid* in this situation.
14+
15+
### No `version` field
16+
17+
```toml
18+
[environment]
19+
python = "/.venv"
20+
```
21+
22+
`/.venv/pyvenv.cfg`:
23+
24+
```cfg
25+
home = /doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin
26+
```
27+
28+
`/doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin/python`:
29+
30+
```text
31+
```
32+
33+
`/.venv/<path-to-site-packages>/foo.py`:
34+
35+
```py
36+
X: int = 42
37+
```
38+
39+
`/src/main.py`:
40+
41+
```py
42+
from foo import X
43+
44+
reveal_type(X) # revealed: int
45+
```
46+
47+
### Malformed stdlib-style version field
48+
49+
```toml
50+
[environment]
51+
python = "/.venv"
52+
```
53+
54+
`/.venv/pyvenv.cfg`:
55+
56+
```cfg
57+
home = /doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin
58+
version = wut
59+
```
60+
61+
`/doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin/python`:
62+
63+
```text
64+
```
65+
66+
`/.venv/<path-to-site-packages>/foo.py`:
67+
68+
```py
69+
X: int = 42
70+
```
71+
72+
`/src/main.py`:
73+
74+
```py
75+
from foo import X
76+
77+
reveal_type(X) # revealed: int
78+
```
79+
80+
### Malformed uv-style version field
81+
82+
```toml
83+
[environment]
84+
python = "/.venv"
85+
```
86+
87+
`/.venv/pyvenv.cfg`:
88+
89+
```cfg
90+
home = /doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin
91+
version_info = no-really-wut
92+
```
93+
94+
`/doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin/python`:
95+
96+
```text
97+
```
98+
99+
`/.venv/<path-to-site-packages>/foo.py`:
100+
101+
```py
102+
X: int = 42
103+
```
104+
105+
`/src/main.py`:
106+
107+
```py
108+
from foo import X
109+
110+
reveal_type(X) # revealed: int
111+
```
112+
3113
## Ephemeral uv environments
4114

5115
If you use the `--with` flag when invoking `uv run`, uv will create an "ephemeral" virtual
@@ -57,3 +167,41 @@ from bar import Y
57167
reveal_type(X) # revealed: int
58168
reveal_type(Y) # revealed: str
59169
```
170+
171+
## `pyvenv.cfg` files with unusual values
172+
173+
`pyvenv.cfg` files can have unusual values in them, which can contain arbitrary characters. This
174+
includes `=` characters. The following is a regression test for
175+
<https://github.com/astral-sh/ty/issues/430>.
176+
177+
```toml
178+
[environment]
179+
python = "/.venv"
180+
```
181+
182+
`/.venv/pyvenv.cfg`:
183+
184+
```cfg
185+
home = /doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin
186+
version_info = 3.13
187+
command = /.pyenv/versions/3.13.3/bin/python3.13 -m venv --without-pip --prompt="python-default/3.13.3" /somewhere-else/python/virtualenvs/python-default/3.13.3
188+
```
189+
190+
`/doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin/python`:
191+
192+
```text
193+
```
194+
195+
`/.venv/<path-to-site-packages>/foo.py`:
196+
197+
```py
198+
X: int = 42
199+
```
200+
201+
`/src/main.py`:
202+
203+
```py
204+
from foo import X
205+
206+
reveal_type(X) # revealed: int
207+
```

crates/ty_python_semantic/src/site_packages.rs

Lines changed: 27 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,22 +1001,7 @@ mod tests {
10011001
))
10021002
};
10031003

1004-
let expected_system_site_packages = if cfg!(target_os = "windows") {
1005-
SystemPathBuf::from(&*format!(
1006-
r"\Python3.{}\Lib\site-packages",
1007-
self.minor_version
1008-
))
1009-
} else if self.free_threaded {
1010-
SystemPathBuf::from(&*format!(
1011-
"/Python3.{minor_version}/lib/python3.{minor_version}t/site-packages",
1012-
minor_version = self.minor_version
1013-
))
1014-
} else {
1015-
SystemPathBuf::from(&*format!(
1016-
"/Python3.{minor_version}/lib/python3.{minor_version}/site-packages",
1017-
minor_version = self.minor_version
1018-
))
1019-
};
1004+
let expected_system_site_packages = self.expected_system_site_packages();
10201005

10211006
if self_venv.system_site_packages {
10221007
assert_eq!(
@@ -1051,33 +1036,33 @@ mod tests {
10511036
);
10521037

10531038
let site_packages_directories = env.site_packages_directories(&self.system).unwrap();
1039+
let expected_site_packages = self.expected_system_site_packages();
1040+
assert_eq!(
1041+
site_packages_directories,
1042+
std::slice::from_ref(&expected_site_packages)
1043+
);
1044+
}
10541045

1055-
let expected_site_packages = if cfg!(target_os = "windows") {
1056-
SystemPathBuf::from(&*format!(
1057-
r"\Python3.{}\Lib\site-packages",
1058-
self.minor_version
1059-
))
1046+
fn expected_system_site_packages(&self) -> SystemPathBuf {
1047+
let minor_version = self.minor_version;
1048+
if cfg!(target_os = "windows") {
1049+
SystemPathBuf::from(&*format!(r"\Python3.{minor_version}\Lib\site-packages"))
10601050
} else if self.free_threaded {
10611051
SystemPathBuf::from(&*format!(
1062-
"/Python3.{minor_version}/lib/python3.{minor_version}t/site-packages",
1063-
minor_version = self.minor_version
1052+
"/Python3.{minor_version}/lib/python3.{minor_version}t/site-packages"
10641053
))
10651054
} else {
10661055
SystemPathBuf::from(&*format!(
1067-
"/Python3.{minor_version}/lib/python3.{minor_version}/site-packages",
1068-
minor_version = self.minor_version
1056+
"/Python3.{minor_version}/lib/python3.{minor_version}/site-packages"
10691057
))
1070-
};
1071-
1072-
assert_eq!(
1073-
site_packages_directories,
1074-
[expected_site_packages].as_slice()
1075-
);
1058+
}
10761059
}
10771060
}
10781061

10791062
#[test]
10801063
fn can_find_site_packages_directory_no_virtual_env() {
1064+
// Shouldn't be converted to an mdtest because mdtest automatically creates a
1065+
// pyvenv.cfg file for you if it sees you creating a `site-packages` directory.
10811066
let test = PythonEnvironmentTestCase {
10821067
system: TestSystem::default(),
10831068
minor_version: 12,
@@ -1090,6 +1075,8 @@ mod tests {
10901075

10911076
#[test]
10921077
fn can_find_site_packages_directory_no_virtual_env_freethreaded() {
1078+
// Shouldn't be converted to an mdtest because mdtest automatically creates a
1079+
// pyvenv.cfg file for you if it sees you creating a `site-packages` directory.
10931080
let test = PythonEnvironmentTestCase {
10941081
system: TestSystem::default(),
10951082
minor_version: 13,
@@ -1132,23 +1119,10 @@ mod tests {
11321119
);
11331120
}
11341121

1135-
#[test]
1136-
fn can_find_site_packages_directory_no_version_field_in_pyvenv_cfg() {
1137-
let test = PythonEnvironmentTestCase {
1138-
system: TestSystem::default(),
1139-
minor_version: 12,
1140-
free_threaded: false,
1141-
origin: SysPrefixPathOrigin::VirtualEnvVar,
1142-
virtual_env: Some(VirtualEnvironmentTestCase {
1143-
pyvenv_cfg_version_field: None,
1144-
..VirtualEnvironmentTestCase::default()
1145-
}),
1146-
};
1147-
test.run();
1148-
}
1149-
11501122
#[test]
11511123
fn can_find_site_packages_directory_venv_style_version_field_in_pyvenv_cfg() {
1124+
// Shouldn't be converted to an mdtest because we want to assert
1125+
// that we parsed the `version` field correctly in `test.run()`.
11521126
let test = PythonEnvironmentTestCase {
11531127
system: TestSystem::default(),
11541128
minor_version: 12,
@@ -1164,6 +1138,8 @@ mod tests {
11641138

11651139
#[test]
11661140
fn can_find_site_packages_directory_uv_style_version_field_in_pyvenv_cfg() {
1141+
// Shouldn't be converted to an mdtest because we want to assert
1142+
// that we parsed the `version` field correctly in `test.run()`.
11671143
let test = PythonEnvironmentTestCase {
11681144
system: TestSystem::default(),
11691145
minor_version: 12,
@@ -1179,6 +1155,8 @@ mod tests {
11791155

11801156
#[test]
11811157
fn can_find_site_packages_directory_virtualenv_style_version_field_in_pyvenv_cfg() {
1158+
// Shouldn't be converted to an mdtest because we want to assert
1159+
// that we parsed the `version` field correctly in `test.run()`.
11821160
let test = PythonEnvironmentTestCase {
11831161
system: TestSystem::default(),
11841162
minor_version: 12,
@@ -1209,6 +1187,9 @@ mod tests {
12091187

12101188
#[test]
12111189
fn finds_system_site_packages() {
1190+
// Can't be converted to an mdtest because the system installation's `sys.prefix`
1191+
// path is at a different location relative to the `pyvenv.cfg` file's `home` value
1192+
// on Windows.
12121193
let test = PythonEnvironmentTestCase {
12131194
system: TestSystem::default(),
12141195
minor_version: 13,
@@ -1366,25 +1347,6 @@ mod tests {
13661347
);
13671348
}
13681349

1369-
/// See <https://github.com/astral-sh/ty/issues/430>
1370-
#[test]
1371-
fn parsing_pyvenv_cfg_with_equals_in_value() {
1372-
let test = PythonEnvironmentTestCase {
1373-
system: TestSystem::default(),
1374-
minor_version: 13,
1375-
free_threaded: true,
1376-
origin: SysPrefixPathOrigin::VirtualEnvVar,
1377-
virtual_env: Some(VirtualEnvironmentTestCase {
1378-
pyvenv_cfg_version_field: Some("version_info = 3.13"),
1379-
command_field: Some(
1380-
r#"command = /.pyenv/versions/3.13.3/bin/python3.13 -m venv --without-pip --prompt="python-default/3.13.3" /somewhere-else/python/virtualenvs/python-default/3.13.3"#,
1381-
),
1382-
..VirtualEnvironmentTestCase::default()
1383-
}),
1384-
};
1385-
test.run();
1386-
}
1387-
13881350
#[test]
13891351
fn parsing_pyvenv_cfg_with_key_but_no_value_fails() {
13901352
let system = TestSystem::default();

0 commit comments

Comments
 (0)