diff --git a/crates/ty_python_semantic/resources/mdtest/import/site_packages_discovery.md b/crates/ty_python_semantic/resources/mdtest/import/site_packages_discovery.md index 8a60a61afb..a1c256c375 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/site_packages_discovery.md +++ b/crates/ty_python_semantic/resources/mdtest/import/site_packages_discovery.md @@ -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//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//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//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 +. + +```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//foo.py`: + +```py +X: int = 42 +``` + +`/src/main.py`: + +```py +from foo import X + +reveal_type(X) # revealed: int +``` diff --git a/crates/ty_python_semantic/src/site_packages.rs b/crates/ty_python_semantic/src/site_packages.rs index c01db5483e..7da9e9ca7c 100644 --- a/crates/ty_python_semantic/src/site_packages.rs +++ b/crates/ty_python_semantic/src/site_packages.rs @@ -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 - #[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();