[ty] Improve tests for site-packages discovery (#18374)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

## 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
https://github.com/astral-sh/ruff/pull/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`
This commit is contained in:
Alex Waygood 2025-05-30 07:32:21 +01:00 committed by GitHub
parent 363f061f09
commit ad2f667ee4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 175 additions and 65 deletions

View file

@ -1,5 +1,115 @@
# Tests for `site-packages` discovery
## Malformed or absent `version` fields
The `version`/`version_info` key in a `pyvenv.cfg` file is provided by most virtual-environment
creation tools to indicate the Python version the virtual environment is for. They key is useful for
our purposes, so we try to parse it when possible. However, the key is not read by the CPython
standard library, and is provided under different keys depending on which virtual-environment
creation tool created the `pyvenv.cfg` file (the stdlib `venv` module calls the key `version`,
whereas uv and virtualenv both call it `version_info`). We therefore do not return an error when
discovering a virtual environment's `site-packages` directory if the virtula environment contains a
`pyvenv.cfg` file which doesn't have this key, or if the associated value of the key doesn't parse
according to our expectations. The file isn't really *invalid* in this situation.
### No `version` field
```toml
[environment]
python = "/.venv"
```
`/.venv/pyvenv.cfg`:
```cfg
home = /doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin
```
`/doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin/python`:
```text
```
`/.venv/<path-to-site-packages>/foo.py`:
```py
X: int = 42
```
`/src/main.py`:
```py
from foo import X
reveal_type(X) # revealed: int
```
### Malformed stdlib-style version field
```toml
[environment]
python = "/.venv"
```
`/.venv/pyvenv.cfg`:
```cfg
home = /doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin
version = wut
```
`/doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin/python`:
```text
```
`/.venv/<path-to-site-packages>/foo.py`:
```py
X: int = 42
```
`/src/main.py`:
```py
from foo import X
reveal_type(X) # revealed: int
```
### Malformed uv-style version field
```toml
[environment]
python = "/.venv"
```
`/.venv/pyvenv.cfg`:
```cfg
home = /doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin
version_info = no-really-wut
```
`/doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin/python`:
```text
```
`/.venv/<path-to-site-packages>/foo.py`:
```py
X: int = 42
```
`/src/main.py`:
```py
from foo import X
reveal_type(X) # revealed: int
```
## Ephemeral uv environments
If you use the `--with` flag when invoking `uv run`, uv will create an "ephemeral" virtual
@ -57,3 +167,41 @@ from bar import Y
reveal_type(X) # revealed: int
reveal_type(Y) # revealed: str
```
## `pyvenv.cfg` files with unusual values
`pyvenv.cfg` files can have unusual values in them, which can contain arbitrary characters. This
includes `=` characters. The following is a regression test for
<https://github.com/astral-sh/ty/issues/430>.
```toml
[environment]
python = "/.venv"
```
`/.venv/pyvenv.cfg`:
```cfg
home = /doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin
version_info = 3.13
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
```
`/doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin/python`:
```text
```
`/.venv/<path-to-site-packages>/foo.py`:
```py
X: int = 42
```
`/src/main.py`:
```py
from foo import X
reveal_type(X) # revealed: int
```

View file

@ -1001,22 +1001,7 @@ mod tests {
))
};
let expected_system_site_packages = if cfg!(target_os = "windows") {
SystemPathBuf::from(&*format!(
r"\Python3.{}\Lib\site-packages",
self.minor_version
))
} else if self.free_threaded {
SystemPathBuf::from(&*format!(
"/Python3.{minor_version}/lib/python3.{minor_version}t/site-packages",
minor_version = self.minor_version
))
} else {
SystemPathBuf::from(&*format!(
"/Python3.{minor_version}/lib/python3.{minor_version}/site-packages",
minor_version = self.minor_version
))
};
let expected_system_site_packages = self.expected_system_site_packages();
if self_venv.system_site_packages {
assert_eq!(
@ -1051,33 +1036,33 @@ mod tests {
);
let site_packages_directories = env.site_packages_directories(&self.system).unwrap();
let expected_site_packages = self.expected_system_site_packages();
assert_eq!(
site_packages_directories,
std::slice::from_ref(&expected_site_packages)
);
}
let expected_site_packages = if cfg!(target_os = "windows") {
SystemPathBuf::from(&*format!(
r"\Python3.{}\Lib\site-packages",
self.minor_version
))
fn expected_system_site_packages(&self) -> SystemPathBuf {
let minor_version = self.minor_version;
if cfg!(target_os = "windows") {
SystemPathBuf::from(&*format!(r"\Python3.{minor_version}\Lib\site-packages"))
} else if self.free_threaded {
SystemPathBuf::from(&*format!(
"/Python3.{minor_version}/lib/python3.{minor_version}t/site-packages",
minor_version = self.minor_version
"/Python3.{minor_version}/lib/python3.{minor_version}t/site-packages"
))
} else {
SystemPathBuf::from(&*format!(
"/Python3.{minor_version}/lib/python3.{minor_version}/site-packages",
minor_version = self.minor_version
"/Python3.{minor_version}/lib/python3.{minor_version}/site-packages"
))
};
assert_eq!(
site_packages_directories,
[expected_site_packages].as_slice()
);
}
}
}
#[test]
fn can_find_site_packages_directory_no_virtual_env() {
// Shouldn't be converted to an mdtest because mdtest automatically creates a
// pyvenv.cfg file for you if it sees you creating a `site-packages` directory.
let test = PythonEnvironmentTestCase {
system: TestSystem::default(),
minor_version: 12,
@ -1090,6 +1075,8 @@ mod tests {
#[test]
fn can_find_site_packages_directory_no_virtual_env_freethreaded() {
// Shouldn't be converted to an mdtest because mdtest automatically creates a
// pyvenv.cfg file for you if it sees you creating a `site-packages` directory.
let test = PythonEnvironmentTestCase {
system: TestSystem::default(),
minor_version: 13,
@ -1132,23 +1119,10 @@ mod tests {
);
}
#[test]
fn can_find_site_packages_directory_no_version_field_in_pyvenv_cfg() {
let test = PythonEnvironmentTestCase {
system: TestSystem::default(),
minor_version: 12,
free_threaded: false,
origin: SysPrefixPathOrigin::VirtualEnvVar,
virtual_env: Some(VirtualEnvironmentTestCase {
pyvenv_cfg_version_field: None,
..VirtualEnvironmentTestCase::default()
}),
};
test.run();
}
#[test]
fn can_find_site_packages_directory_venv_style_version_field_in_pyvenv_cfg() {
// Shouldn't be converted to an mdtest because we want to assert
// that we parsed the `version` field correctly in `test.run()`.
let test = PythonEnvironmentTestCase {
system: TestSystem::default(),
minor_version: 12,
@ -1164,6 +1138,8 @@ mod tests {
#[test]
fn can_find_site_packages_directory_uv_style_version_field_in_pyvenv_cfg() {
// Shouldn't be converted to an mdtest because we want to assert
// that we parsed the `version` field correctly in `test.run()`.
let test = PythonEnvironmentTestCase {
system: TestSystem::default(),
minor_version: 12,
@ -1179,6 +1155,8 @@ mod tests {
#[test]
fn can_find_site_packages_directory_virtualenv_style_version_field_in_pyvenv_cfg() {
// Shouldn't be converted to an mdtest because we want to assert
// that we parsed the `version` field correctly in `test.run()`.
let test = PythonEnvironmentTestCase {
system: TestSystem::default(),
minor_version: 12,
@ -1209,6 +1187,9 @@ mod tests {
#[test]
fn finds_system_site_packages() {
// Can't be converted to an mdtest because the system installation's `sys.prefix`
// path is at a different location relative to the `pyvenv.cfg` file's `home` value
// on Windows.
let test = PythonEnvironmentTestCase {
system: TestSystem::default(),
minor_version: 13,
@ -1366,25 +1347,6 @@ mod tests {
);
}
/// See <https://github.com/astral-sh/ty/issues/430>
#[test]
fn parsing_pyvenv_cfg_with_equals_in_value() {
let test = PythonEnvironmentTestCase {
system: TestSystem::default(),
minor_version: 13,
free_threaded: true,
origin: SysPrefixPathOrigin::VirtualEnvVar,
virtual_env: Some(VirtualEnvironmentTestCase {
pyvenv_cfg_version_field: Some("version_info = 3.13"),
command_field: Some(
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"#,
),
..VirtualEnvironmentTestCase::default()
}),
};
test.run();
}
#[test]
fn parsing_pyvenv_cfg_with_key_but_no_value_fails() {
let system = TestSystem::default();