[ty] Support ephemeral uv virtual environments (#18335)

This commit is contained in:
Alex Waygood 2025-05-28 15:54:59 +01:00 committed by GitHub
parent 9925910a29
commit a5ebb3f3a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 283 additions and 97 deletions

View file

@ -596,6 +596,13 @@ impl AsRef<SystemPath> for Utf8PathBuf {
} }
} }
impl AsRef<SystemPath> for camino::Utf8Component<'_> {
#[inline]
fn as_ref(&self) -> &SystemPath {
SystemPath::new(self.as_str())
}
}
impl AsRef<SystemPath> for str { impl AsRef<SystemPath> for str {
#[inline] #[inline]
fn as_ref(&self) -> &SystemPath { fn as_ref(&self) -> &SystemPath {
@ -626,6 +633,22 @@ impl Deref for SystemPathBuf {
} }
} }
impl<P: AsRef<SystemPath>> FromIterator<P> for SystemPathBuf {
fn from_iter<I: IntoIterator<Item = P>>(iter: I) -> Self {
let mut buf = SystemPathBuf::new();
buf.extend(iter);
buf
}
}
impl<P: AsRef<SystemPath>> Extend<P> for SystemPathBuf {
fn extend<I: IntoIterator<Item = P>>(&mut self, iter: I) {
for path in iter {
self.push(path);
}
}
}
impl std::fmt::Debug for SystemPath { impl std::fmt::Debug for SystemPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f) self.0.fmt(f)

View file

@ -0,0 +1,59 @@
# Tests for `site-packages` discovery
## Ephemeral uv environments
If you use the `--with` flag when invoking `uv run`, uv will create an "ephemeral" virtual
environment that is layered on top of the pre-existing environment. `site-packages` directories from
the pre-existing environment will be added as an import search path at runtime as well as the
`site-packages` directory from the ephemeral environment. The `VIRTUAL_ENV` environment variable
will only point to the ephemeral virtual environment, but, following uv commit
`7bba3d00d4ad1fb3daba86b98eb25d8d9e9836ae`, uv writes the `sys.prefix` path of the parent
environment to an `extends-environment` key in the ephemeral environment's `pyvenv.cfg` file.
This test ensures that we are able to resolve imports that point to packages in either
`site-packages` directory (the one of the ephemeral environment or the one of the parent
environment) if we detect that an ephemeral uv environment has been activated.
```toml
[environment]
python = "/.venv"
```
`/.venv/pyvenv.cfg`:
```cfg
home = /doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin
implementation = CPython
uv = 0.7.6
version_info = 3.13.2
include-system-site-packages = false
prompt = ruff
extends-environment = /.other-environment
```
`/doo/doo/wop/cpython-3.13.2-macos-aarch64-none/bin/python`:
```text
```
`/.venv/<path-to-site-packages>/foo.py`:
```py
X: int = 42
```
`/.other-environment/<path-to-site-packages>/bar.py`:
```py
Y: "str" = "Y"
```
`/src/main.py`:
```py
from foo import X
from bar import Y
reveal_type(X) # revealed: int
reveal_type(Y) # revealed: str
```

View file

@ -14,7 +14,9 @@ use ruff_python_ast::PythonVersion;
use crate::db::Db; use crate::db::Db;
use crate::module_name::ModuleName; use crate::module_name::ModuleName;
use crate::module_resolver::typeshed::{TypeshedVersions, vendored_typeshed_versions}; use crate::module_resolver::typeshed::{TypeshedVersions, vendored_typeshed_versions};
use crate::site_packages::{PythonEnvironment, SitePackagesDiscoveryError, SysPrefixPathOrigin}; use crate::site_packages::{
PythonEnvironment, SitePackagesDiscoveryError, SitePackagesPaths, SysPrefixPathOrigin,
};
use crate::{Program, PythonPath, SearchPathSettings}; use crate::{Program, PythonPath, SearchPathSettings};
use super::module::{Module, ModuleKind}; use super::module::{Module, ModuleKind};
@ -289,7 +291,7 @@ impl SearchPaths {
virtual_env_path, virtual_env_path,
error error
); );
vec![] SitePackagesPaths::default()
}; };
match PythonEnvironment::new( match PythonEnvironment::new(
@ -304,7 +306,7 @@ impl SearchPaths {
} }
} else { } else {
tracing::debug!("No virtual environment found"); tracing::debug!("No virtual environment found");
vec![] SitePackagesPaths::default()
} }
} }

View file

@ -14,11 +14,71 @@ use std::io;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::ops::Deref; use std::ops::Deref;
use indexmap::IndexSet;
use ruff_db::system::{System, SystemPath, SystemPathBuf}; use ruff_db::system::{System, SystemPath, SystemPathBuf};
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;
type SitePackagesDiscoveryResult<T> = Result<T, SitePackagesDiscoveryError>; type SitePackagesDiscoveryResult<T> = Result<T, SitePackagesDiscoveryError>;
/// An ordered, deduplicated set of `site-packages` search paths.
///
/// Most environments will only have one `site-packages` directory.
/// Some virtual environments created with `--system-site-packages`
/// will also have the system installation's `site-packages` packages
/// available, however. Ephemeral environments created with `uv` in
/// `uv run --with` invocations, meanwhile, "extend" a parent environment
/// (which could be another virtual environment or a system installation,
/// and which could itself have multiple `site-packages` directories).
///
/// We use an `IndexSet` here to guard against the (very remote)
/// possibility that an environment might somehow be marked as being
/// both a `--system-site-packages` virtual environment *and* an
/// ephemeral environment that extends the system environment. If this
/// were the case, the system environment's `site-packages` directory
/// *might* be added to the `SitePackagesPaths` twice, but we wouldn't
/// want duplicates to appear in this set.
#[derive(Debug, PartialEq, Eq, Default)]
pub(crate) struct SitePackagesPaths(IndexSet<SystemPathBuf>);
impl SitePackagesPaths {
pub(crate) fn len(&self) -> usize {
self.0.len()
}
fn single(path: SystemPathBuf) -> Self {
Self(IndexSet::from([path]))
}
fn insert(&mut self, path: SystemPathBuf) {
self.0.insert(path);
}
fn extend(&mut self, other: Self) {
self.0.extend(other.0);
}
}
impl FromIterator<SystemPathBuf> for SitePackagesPaths {
fn from_iter<T: IntoIterator<Item = SystemPathBuf>>(iter: T) -> Self {
Self(IndexSet::from_iter(iter))
}
}
impl IntoIterator for SitePackagesPaths {
type Item = SystemPathBuf;
type IntoIter = indexmap::set::IntoIter<SystemPathBuf>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl PartialEq<&[SystemPathBuf]> for SitePackagesPaths {
fn eq(&self, other: &&[SystemPathBuf]) -> bool {
self.0.as_slice() == *other
}
}
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum PythonEnvironment { pub(crate) enum PythonEnvironment {
Virtual(VirtualEnvironment), Virtual(VirtualEnvironment),
@ -35,7 +95,7 @@ impl PythonEnvironment {
// Attempt to inspect as a virtual environment first // Attempt to inspect as a virtual environment first
// TODO(zanieb): Consider avoiding the clone here by checking for `pyvenv.cfg` ahead-of-time // TODO(zanieb): Consider avoiding the clone here by checking for `pyvenv.cfg` ahead-of-time
match VirtualEnvironment::new(path.clone(), origin, system) { match VirtualEnvironment::new(path.clone(), system) {
Ok(venv) => Ok(Self::Virtual(venv)), Ok(venv) => Ok(Self::Virtual(venv)),
// If there's not a `pyvenv.cfg` marker, attempt to inspect as a system environment // If there's not a `pyvenv.cfg` marker, attempt to inspect as a system environment
// //
@ -54,7 +114,7 @@ impl PythonEnvironment {
pub(crate) fn site_packages_directories( pub(crate) fn site_packages_directories(
&self, &self,
system: &dyn System, system: &dyn System,
) -> SitePackagesDiscoveryResult<Vec<SystemPathBuf>> { ) -> SitePackagesDiscoveryResult<SitePackagesPaths> {
match self { match self {
Self::Virtual(env) => env.site_packages_directories(system), Self::Virtual(env) => env.site_packages_directories(system),
Self::System(env) => env.site_packages_directories(system), Self::System(env) => env.site_packages_directories(system),
@ -111,12 +171,19 @@ pub(crate) struct VirtualEnvironment {
/// This field will be `None` if so. /// This field will be `None` if so.
version: Option<PythonVersion>, version: Option<PythonVersion>,
implementation: PythonImplementation, implementation: PythonImplementation,
/// If this virtual environment was created using uv,
/// it may be an "ephemeral" virtual environment that dynamically adds the `site-packages`
/// directories of its parent environment to `sys.path` at runtime.
/// Newer versions of uv record the parent environment in the `pyvenv.cfg` file;
/// we'll want to add the `site-packages` directories of the parent environment
/// as search paths as well as the `site-packages` directories of this virtual environment.
parent_environment: Option<Box<PythonEnvironment>>,
} }
impl VirtualEnvironment { impl VirtualEnvironment {
pub(crate) fn new( pub(crate) fn new(
path: SysPrefixPath, path: SysPrefixPath,
origin: SysPrefixPathOrigin,
system: &dyn System, system: &dyn System,
) -> SitePackagesDiscoveryResult<Self> { ) -> SitePackagesDiscoveryResult<Self> {
fn pyvenv_cfg_line_number(index: usize) -> NonZeroUsize { fn pyvenv_cfg_line_number(index: usize) -> NonZeroUsize {
@ -128,12 +195,14 @@ impl VirtualEnvironment {
let pyvenv_cfg = system let pyvenv_cfg = system
.read_to_string(&pyvenv_cfg_path) .read_to_string(&pyvenv_cfg_path)
.map_err(|io_err| SitePackagesDiscoveryError::NoPyvenvCfgFile(origin, io_err))?; .map_err(|io_err| SitePackagesDiscoveryError::NoPyvenvCfgFile(path.origin, io_err))?;
let mut include_system_site_packages = false; let mut include_system_site_packages = false;
let mut base_executable_home_path = None; let mut base_executable_home_path = None;
let mut version_info_string = None; let mut version_info_string = None;
let mut implementation = PythonImplementation::Unknown; let mut implementation = PythonImplementation::Unknown;
let mut created_with_uv = false;
let mut parent_environment = None;
// A `pyvenv.cfg` file *looks* like a `.ini` file, but actually isn't valid `.ini` syntax! // A `pyvenv.cfg` file *looks* like a `.ini` file, but actually isn't valid `.ini` syntax!
// The Python standard-library's `site` module parses these files by splitting each line on // The Python standard-library's `site` module parses these files by splitting each line on
@ -178,6 +247,8 @@ impl VirtualEnvironment {
_ => PythonImplementation::Unknown, _ => PythonImplementation::Unknown,
}; };
} }
"uv" => created_with_uv = true,
"extends-environment" => parent_environment = Some(value),
_ => continue, _ => continue,
} }
} }
@ -196,11 +267,32 @@ impl VirtualEnvironment {
let base_executable_home_path = PythonHomePath::new(base_executable_home_path, system) let base_executable_home_path = PythonHomePath::new(base_executable_home_path, system)
.map_err(|io_err| { .map_err(|io_err| {
SitePackagesDiscoveryError::PyvenvCfgParseError( SitePackagesDiscoveryError::PyvenvCfgParseError(
pyvenv_cfg_path, pyvenv_cfg_path.clone(),
PyvenvCfgParseErrorKind::InvalidHomeValue(io_err), PyvenvCfgParseErrorKind::InvalidHomeValue(io_err),
) )
})?; })?;
// Since the `extends-environment` key is nonstandard,
// for now we only trust it if the virtual environment was created with `uv`.
let parent_environment = if created_with_uv {
parent_environment
.and_then(|sys_prefix| {
PythonEnvironment::new(sys_prefix, SysPrefixPathOrigin::DerivedFromPyvenvCfg, system)
.inspect_err(|err| {
tracing::warn!(
"Failed to resolve the parent environment of this ephemeral uv virtual environment \
from the `extends-environment` value specified in the `pyvenv.cfg` file at {pyvenv_cfg_path}. \
Imports will not be resolved correctly if they refer to packages installed into the parent \
environment. Underlying error: {err}",
);
})
.ok()
})
.map(Box::new)
} else {
None
};
// but the `version`/`version_info` key is not read by the standard library, // but the `version`/`version_info` key is not read by the standard library,
// and is provided under different keys depending on which virtual-environment creation tool // and is provided under different keys depending on which virtual-environment creation tool
// created the `pyvenv.cfg` file. Lenient parsing is appropriate here: // created the `pyvenv.cfg` file. Lenient parsing is appropriate here:
@ -218,6 +310,7 @@ impl VirtualEnvironment {
include_system_site_packages, include_system_site_packages,
version, version,
implementation, implementation,
parent_environment,
}; };
tracing::trace!("Resolved metadata for virtual environment: {metadata:?}"); tracing::trace!("Resolved metadata for virtual environment: {metadata:?}");
@ -230,21 +323,34 @@ impl VirtualEnvironment {
pub(crate) fn site_packages_directories( pub(crate) fn site_packages_directories(
&self, &self,
system: &dyn System, system: &dyn System,
) -> SitePackagesDiscoveryResult<Vec<SystemPathBuf>> { ) -> SitePackagesDiscoveryResult<SitePackagesPaths> {
let VirtualEnvironment { let VirtualEnvironment {
root_path, root_path,
base_executable_home_path, base_executable_home_path,
include_system_site_packages, include_system_site_packages,
implementation, implementation,
version, version,
parent_environment,
} = self; } = self;
let mut site_packages_directories = vec![site_packages_directory_from_sys_prefix( let mut site_packages_directories = SitePackagesPaths::single(
root_path, site_packages_directory_from_sys_prefix(root_path, *version, *implementation, system)?,
*version, );
*implementation,
system, if let Some(parent_env_site_packages) = parent_environment.as_deref() {
)?]; match parent_env_site_packages.site_packages_directories(system) {
Ok(parent_environment_site_packages) => {
site_packages_directories.extend(parent_environment_site_packages);
}
Err(err) => {
tracing::warn!(
"Failed to resolve the site-packages directories of this ephemeral uv virtual environment's \
parent environment. Imports will not be resolved correctly if they refer to packages installed \
into the parent environment. Underlying error: {err}"
);
}
}
}
if *include_system_site_packages { if *include_system_site_packages {
let system_sys_prefix = let system_sys_prefix =
@ -261,7 +367,7 @@ impl VirtualEnvironment {
system, system,
) { ) {
Ok(site_packages_directory) => { Ok(site_packages_directory) => {
site_packages_directories.push(site_packages_directory); site_packages_directories.insert(site_packages_directory);
} }
Err(error) => tracing::warn!( Err(error) => tracing::warn!(
"{error}. System site-packages will not be used for module resolution." "{error}. System site-packages will not be used for module resolution."
@ -309,15 +415,16 @@ impl SystemEnvironment {
pub(crate) fn site_packages_directories( pub(crate) fn site_packages_directories(
&self, &self,
system: &dyn System, system: &dyn System,
) -> SitePackagesDiscoveryResult<Vec<SystemPathBuf>> { ) -> SitePackagesDiscoveryResult<SitePackagesPaths> {
let SystemEnvironment { root_path } = self; let SystemEnvironment { root_path } = self;
let site_packages_directories = vec![site_packages_directory_from_sys_prefix( let site_packages_directories =
root_path, SitePackagesPaths::single(site_packages_directory_from_sys_prefix(
None, root_path,
PythonImplementation::Unknown, None,
system, PythonImplementation::Unknown,
)?]; system,
)?);
tracing::debug!( tracing::debug!(
"Resolved site-packages directories for this environment are: {site_packages_directories:?}" "Resolved site-packages directories for this environment are: {site_packages_directories:?}"
@ -550,12 +657,12 @@ impl SysPrefixPath {
if cfg!(target_os = "windows") { if cfg!(target_os = "windows") {
Some(Self { Some(Self {
inner: path.to_path_buf(), inner: path.to_path_buf(),
origin: SysPrefixPathOrigin::Derived, origin: SysPrefixPathOrigin::DerivedFromPyvenvCfg,
}) })
} else { } else {
path.parent().map(|path| Self { path.parent().map(|path| Self {
inner: path.to_path_buf(), inner: path.to_path_buf(),
origin: SysPrefixPathOrigin::Derived, origin: SysPrefixPathOrigin::DerivedFromPyvenvCfg,
}) })
} }
} }
@ -575,13 +682,22 @@ impl fmt::Display for SysPrefixPath {
} }
} }
/// Enumeration of sources a `sys.prefix` path can come from.
#[derive(Debug, PartialEq, Eq, Copy, Clone)] #[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SysPrefixPathOrigin { pub enum SysPrefixPathOrigin {
/// The `sys.prefix` path came from a `--python` CLI flag
PythonCliFlag, PythonCliFlag,
/// The `sys.prefix` path came from the `VIRTUAL_ENV` environment variable
VirtualEnvVar, VirtualEnvVar,
/// The `sys.prefix` path came from the `CONDA_PREFIX` environment variable
CondaPrefixVar, CondaPrefixVar,
Derived, /// The `sys.prefix` path was derived from a value in a `pyvenv.cfg` file:
/// either the value associated with the `home` key
/// or the value associated with the `extends-environment` key
DerivedFromPyvenvCfg,
/// A `.venv` directory was found in the current working directory,
/// and the `sys.prefix` path is the path to that virtual environment.
LocalVenv, LocalVenv,
} }
@ -591,7 +707,7 @@ impl SysPrefixPathOrigin {
pub(crate) fn must_be_virtual_env(self) -> bool { pub(crate) fn must_be_virtual_env(self) -> bool {
match self { match self {
Self::LocalVenv | Self::VirtualEnvVar => true, Self::LocalVenv | Self::VirtualEnvVar => true,
Self::PythonCliFlag | Self::Derived | Self::CondaPrefixVar => false, Self::PythonCliFlag | Self::DerivedFromPyvenvCfg | Self::CondaPrefixVar => false,
} }
} }
} }
@ -602,7 +718,7 @@ impl Display for SysPrefixPathOrigin {
Self::PythonCliFlag => f.write_str("`--python` argument"), Self::PythonCliFlag => f.write_str("`--python` argument"),
Self::VirtualEnvVar => f.write_str("`VIRTUAL_ENV` environment variable"), Self::VirtualEnvVar => f.write_str("`VIRTUAL_ENV` environment variable"),
Self::CondaPrefixVar => f.write_str("`CONDA_PREFIX` environment variable"), Self::CondaPrefixVar => f.write_str("`CONDA_PREFIX` environment variable"),
Self::Derived => f.write_str("derived `sys.prefix` path"), Self::DerivedFromPyvenvCfg => f.write_str("derived `sys.prefix` path"),
Self::LocalVenv => f.write_str("local virtual environment"), Self::LocalVenv => f.write_str("local virtual environment"),
} }
} }
@ -692,6 +808,7 @@ mod tests {
} }
} }
#[derive(Default)]
struct VirtualEnvironmentTestCase { struct VirtualEnvironmentTestCase {
system_site_packages: bool, system_site_packages: bool,
pyvenv_cfg_version_field: Option<&'static str>, pyvenv_cfg_version_field: Option<&'static str>,
@ -784,6 +901,7 @@ mod tests {
pyvenv_cfg_contents.push_str(implementation_field); pyvenv_cfg_contents.push_str(implementation_field);
pyvenv_cfg_contents.push('\n'); pyvenv_cfg_contents.push('\n');
} }
// Deliberately using weird casing here to test that our pyvenv.cfg parsing is case-insensitive: // Deliberately using weird casing here to test that our pyvenv.cfg parsing is case-insensitive:
if *system_site_packages { if *system_site_packages {
pyvenv_cfg_contents.push_str("include-system-site-packages = TRuE\n"); pyvenv_cfg_contents.push_str("include-system-site-packages = TRuE\n");
@ -827,6 +945,7 @@ mod tests {
env env
} }
#[track_caller]
fn assert_virtual_environment( fn assert_virtual_environment(
&self, &self,
venv: &VirtualEnvironment, venv: &VirtualEnvironment,
@ -901,14 +1020,18 @@ mod tests {
if self_venv.system_site_packages { if self_venv.system_site_packages {
assert_eq!( assert_eq!(
&site_packages_directories, site_packages_directories,
&[expected_venv_site_packages, expected_system_site_packages] [expected_venv_site_packages, expected_system_site_packages].as_slice()
); );
} else { } else {
assert_eq!(&site_packages_directories, &[expected_venv_site_packages]); assert_eq!(
&site_packages_directories.into_iter().next().unwrap(),
&expected_venv_site_packages
);
} }
} }
#[track_caller]
fn assert_system_environment( fn assert_system_environment(
&self, &self,
env: &SystemEnvironment, env: &SystemEnvironment,
@ -946,7 +1069,10 @@ mod tests {
)) ))
}; };
assert_eq!(&site_packages_directories, &[expected_site_packages]); assert_eq!(
site_packages_directories,
[expected_site_packages].as_slice()
);
} }
} }
@ -1014,10 +1140,8 @@ mod tests {
free_threaded: false, free_threaded: false,
origin: SysPrefixPathOrigin::VirtualEnvVar, origin: SysPrefixPathOrigin::VirtualEnvVar,
virtual_env: Some(VirtualEnvironmentTestCase { virtual_env: Some(VirtualEnvironmentTestCase {
system_site_packages: false,
pyvenv_cfg_version_field: None, pyvenv_cfg_version_field: None,
command_field: None, ..VirtualEnvironmentTestCase::default()
implementation_field: None,
}), }),
}; };
test.run(); test.run();
@ -1031,10 +1155,8 @@ mod tests {
free_threaded: false, free_threaded: false,
origin: SysPrefixPathOrigin::VirtualEnvVar, origin: SysPrefixPathOrigin::VirtualEnvVar,
virtual_env: Some(VirtualEnvironmentTestCase { virtual_env: Some(VirtualEnvironmentTestCase {
system_site_packages: false,
pyvenv_cfg_version_field: Some("version = 3.12"), pyvenv_cfg_version_field: Some("version = 3.12"),
command_field: None, ..VirtualEnvironmentTestCase::default()
implementation_field: None,
}), }),
}; };
test.run(); test.run();
@ -1048,10 +1170,8 @@ mod tests {
free_threaded: false, free_threaded: false,
origin: SysPrefixPathOrigin::VirtualEnvVar, origin: SysPrefixPathOrigin::VirtualEnvVar,
virtual_env: Some(VirtualEnvironmentTestCase { virtual_env: Some(VirtualEnvironmentTestCase {
system_site_packages: false,
pyvenv_cfg_version_field: Some("version_info = 3.12"), pyvenv_cfg_version_field: Some("version_info = 3.12"),
command_field: None, ..VirtualEnvironmentTestCase::default()
implementation_field: None,
}), }),
}; };
test.run(); test.run();
@ -1065,10 +1185,8 @@ mod tests {
free_threaded: false, free_threaded: false,
origin: SysPrefixPathOrigin::VirtualEnvVar, origin: SysPrefixPathOrigin::VirtualEnvVar,
virtual_env: Some(VirtualEnvironmentTestCase { virtual_env: Some(VirtualEnvironmentTestCase {
system_site_packages: false,
pyvenv_cfg_version_field: Some("version_info = 3.12.0rc2"), pyvenv_cfg_version_field: Some("version_info = 3.12.0rc2"),
command_field: None, ..VirtualEnvironmentTestCase::default()
implementation_field: None,
}), }),
}; };
test.run(); test.run();
@ -1082,10 +1200,8 @@ mod tests {
free_threaded: true, free_threaded: true,
origin: SysPrefixPathOrigin::VirtualEnvVar, origin: SysPrefixPathOrigin::VirtualEnvVar,
virtual_env: Some(VirtualEnvironmentTestCase { virtual_env: Some(VirtualEnvironmentTestCase {
system_site_packages: false,
pyvenv_cfg_version_field: Some("version_info = 3.13"), pyvenv_cfg_version_field: Some("version_info = 3.13"),
command_field: None, ..VirtualEnvironmentTestCase::default()
implementation_field: None,
}), }),
}; };
test.run(); test.run();
@ -1101,8 +1217,7 @@ mod tests {
virtual_env: Some(VirtualEnvironmentTestCase { virtual_env: Some(VirtualEnvironmentTestCase {
system_site_packages: true, system_site_packages: true,
pyvenv_cfg_version_field: Some("version_info = 3.13"), pyvenv_cfg_version_field: Some("version_info = 3.13"),
command_field: None, ..VirtualEnvironmentTestCase::default()
implementation_field: None,
}), }),
}; };
test.run(); test.run();
@ -1116,10 +1231,8 @@ mod tests {
free_threaded: true, free_threaded: true,
origin: SysPrefixPathOrigin::VirtualEnvVar, origin: SysPrefixPathOrigin::VirtualEnvVar,
virtual_env: Some(VirtualEnvironmentTestCase { virtual_env: Some(VirtualEnvironmentTestCase {
system_site_packages: true,
pyvenv_cfg_version_field: None,
command_field: None,
implementation_field: Some("implementation = PyPy"), implementation_field: Some("implementation = PyPy"),
..VirtualEnvironmentTestCase::default()
}), }),
}; };
let venv = test.run().expect_venv(); let venv = test.run().expect_venv();
@ -1134,10 +1247,8 @@ mod tests {
free_threaded: true, free_threaded: true,
origin: SysPrefixPathOrigin::VirtualEnvVar, origin: SysPrefixPathOrigin::VirtualEnvVar,
virtual_env: Some(VirtualEnvironmentTestCase { virtual_env: Some(VirtualEnvironmentTestCase {
system_site_packages: true,
pyvenv_cfg_version_field: None,
command_field: None,
implementation_field: Some("implementation = CPython"), implementation_field: Some("implementation = CPython"),
..VirtualEnvironmentTestCase::default()
}), }),
}; };
let venv = test.run().expect_venv(); let venv = test.run().expect_venv();
@ -1152,10 +1263,8 @@ mod tests {
free_threaded: true, free_threaded: true,
origin: SysPrefixPathOrigin::VirtualEnvVar, origin: SysPrefixPathOrigin::VirtualEnvVar,
virtual_env: Some(VirtualEnvironmentTestCase { virtual_env: Some(VirtualEnvironmentTestCase {
system_site_packages: true,
pyvenv_cfg_version_field: None,
command_field: None,
implementation_field: Some("implementation = GraalVM"), implementation_field: Some("implementation = GraalVM"),
..VirtualEnvironmentTestCase::default()
}), }),
}; };
let venv = test.run().expect_venv(); let venv = test.run().expect_venv();
@ -1169,12 +1278,7 @@ mod tests {
minor_version: 13, minor_version: 13,
free_threaded: true, free_threaded: true,
origin: SysPrefixPathOrigin::VirtualEnvVar, origin: SysPrefixPathOrigin::VirtualEnvVar,
virtual_env: Some(VirtualEnvironmentTestCase { virtual_env: Some(VirtualEnvironmentTestCase::default()),
system_site_packages: true,
pyvenv_cfg_version_field: None,
command_field: None,
implementation_field: None,
}),
}; };
let venv = test.run().expect_venv(); let venv = test.run().expect_venv();
assert_eq!(venv.implementation, PythonImplementation::Unknown); assert_eq!(venv.implementation, PythonImplementation::Unknown);
@ -1271,12 +1375,11 @@ mod tests {
free_threaded: true, free_threaded: true,
origin: SysPrefixPathOrigin::VirtualEnvVar, origin: SysPrefixPathOrigin::VirtualEnvVar,
virtual_env: Some(VirtualEnvironmentTestCase { virtual_env: Some(VirtualEnvironmentTestCase {
system_site_packages: true,
pyvenv_cfg_version_field: Some("version_info = 3.13"), pyvenv_cfg_version_field: Some("version_info = 3.13"),
command_field: Some( 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"#, 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"#,
), ),
implementation_field: None, ..VirtualEnvironmentTestCase::default()
}), }),
}; };
test.run(); test.run();

View file

@ -298,7 +298,9 @@ python-version = "3.10"
This configuration will apply to all tests in the same section, and all nested sections within that This configuration will apply to all tests in the same section, and all nested sections within that
section. Nested sections can override configurations from their parent sections. section. Nested sections can override configurations from their parent sections.
See [`MarkdownTestConfig`](https://github.com/astral-sh/ruff/blob/main/crates/ty_test/src/config.rs) for the full list of supported configuration options. To enable logging in an mdtest, set `log = true` at the top level of the TOML block.
See [`MarkdownTestConfig`](https://github.com/astral-sh/ruff/blob/main/crates/ty_test/src/config.rs)
for the full list of supported configuration options.
### Specifying a custom typeshed ### Specifying a custom typeshed

View file

@ -169,7 +169,6 @@ fn run_test(
let src_path = project_root.clone(); let src_path = project_root.clone();
let custom_typeshed_path = test.configuration().typeshed(); let custom_typeshed_path = test.configuration().typeshed();
let python_path = test.configuration().python();
let python_version = test.configuration().python_version().unwrap_or_default(); let python_version = test.configuration().python_version().unwrap_or_default();
let mut typeshed_files = vec![]; let mut typeshed_files = vec![];
@ -189,37 +188,35 @@ fn run_test(
let mut full_path = embedded.full_path(&project_root); let mut full_path = embedded.full_path(&project_root);
if let Some(typeshed_path) = custom_typeshed_path { if let Some(relative_path_to_custom_typeshed) = custom_typeshed_path
if let Ok(relative_path) = full_path.strip_prefix(typeshed_path.join("stdlib")) { .and_then(|typeshed| full_path.strip_prefix(typeshed.join("stdlib")).ok())
if relative_path.as_str() == "VERSIONS" { {
has_custom_versions_file = true; if relative_path_to_custom_typeshed.as_str() == "VERSIONS" {
} else if relative_path.extension().is_some_and(|ext| ext == "pyi") { has_custom_versions_file = true;
typeshed_files.push(relative_path.to_path_buf()); } else if relative_path_to_custom_typeshed
} .extension()
.is_some_and(|ext| ext == "pyi")
{
typeshed_files.push(relative_path_to_custom_typeshed.to_path_buf());
} }
} else if let Some(python_path) = python_path { } else if let Some(component_index) = full_path
if let Ok(relative_path) = full_path.strip_prefix(python_path) { .components()
// Construct the path to the site-packages directory .position(|c| c.as_str() == "<path-to-site-packages>")
if relative_path.as_str() != "pyvenv.cfg" { {
let mut new_path = SystemPathBuf::new(); // If the path contains `<path-to-site-packages>`, we need to replace it with the
for component in full_path.components() { // actual site-packages directory based on the Python platform and version.
let component = component.as_str(); let mut components = full_path.components();
if component == "<path-to-site-packages>" { let mut new_path: SystemPathBuf =
if cfg!(target_os = "windows") { components.by_ref().take(component_index).collect();
new_path.push("Lib"); if cfg!(target_os = "windows") {
new_path.push("site-packages"); new_path.extend(["Lib", "site-packages"]);
} else { } else {
new_path.push("lib"); new_path.push("lib");
new_path.push(format!("python{python_version}")); new_path.push(format!("python{python_version}"));
new_path.push("site-packages"); new_path.push("site-packages");
}
} else {
new_path.push(component);
}
}
full_path = new_path;
}
} }
new_path.extend(components.skip(1));
full_path = new_path;
} }
db.write_file(&full_path, &embedded.code).unwrap(); db.write_file(&full_path, &embedded.code).unwrap();