Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
```
92 changes: 27 additions & 65 deletions crates/ty_python_semantic/src/site_packages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down
Loading