uv-python 0.0.40

This is an internal component crate of uv
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
use std::borrow::Cow;
use std::str::FromStr;
use std::{
    env, io,
    path::{Path, PathBuf},
};

use fs_err as fs;
use thiserror::Error;

use uv_preview::{Preview, PreviewFeature};
use uv_pypi_types::Scheme;
use uv_static::EnvVars;

use crate::PythonVersion;

/// The layout of a virtual environment.
#[derive(Debug)]
pub struct VirtualEnvironment {
    /// The absolute path to the root of the virtualenv, e.g., `/path/to/.venv`.
    pub root: PathBuf,

    /// The path to the Python interpreter inside the virtualenv, e.g., `.venv/bin/python`
    /// (Unix, Python 3.11).
    pub executable: PathBuf,

    /// The path to the base executable for the environment, within the `home` directory.
    pub base_executable: PathBuf,

    /// The [`Scheme`] paths for the virtualenv, as returned by (e.g.) `sysconfig.get_paths()`.
    pub scheme: Scheme,
}

/// A parsed `pyvenv.cfg`
#[derive(Debug, Clone)]
pub struct PyVenvConfiguration {
    /// The `PYTHONHOME` directory containing the base Python executable.
    pub(crate) home: Option<PathBuf>,
    /// Was the virtual environment created with the `virtualenv` package?
    pub(crate) virtualenv: bool,
    /// Was the virtual environment created with the `uv` package?
    pub(crate) uv: bool,
    /// Is the virtual environment relocatable?
    pub(crate) relocatable: bool,
    /// Was the virtual environment populated with seed packages?
    pub(crate) seed: bool,
    /// Should the virtual environment include system site packages?
    pub(crate) include_system_site_packages: bool,
    /// The Python version the virtual environment was created with
    pub(crate) version: Option<PythonVersion>,
}

#[derive(Debug, Error)]
pub enum Error {
    #[error(transparent)]
    Io(#[from] io::Error),
    #[error("Broken virtual environment `{0}`: `pyvenv.cfg` is missing")]
    MissingPyVenvCfg(PathBuf),
    #[error("Broken virtual environment `{0}`: `pyvenv.cfg` could not be parsed")]
    ParsePyVenvCfg(PathBuf, #[source] io::Error),
}

/// Locate an active virtual environment by inspecting environment variables.
///
/// Supports `VIRTUAL_ENV`.
pub(crate) fn virtualenv_from_env() -> Option<PathBuf> {
    if let Some(dir) = env::var_os(EnvVars::VIRTUAL_ENV).filter(|value| !value.is_empty()) {
        return Some(PathBuf::from(dir));
    }

    None
}

#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub(crate) enum CondaEnvironmentKind {
    /// The base Conda environment; treated like a system Python environment.
    Base,
    /// Any other Conda environment; treated like a virtual environment.
    Child,
}

impl CondaEnvironmentKind {
    /// Whether the given `CONDA_PREFIX` path is the base Conda environment.
    ///
    /// The base environment is typically stored in a location matching the `_CONDA_ROOT` path.
    ///
    /// Additionally, when the base environment is active, `CONDA_DEFAULT_ENV` will be set to a
    /// name, e.g., `base`, which does not match the `CONDA_PREFIX`, e.g., `/usr/local` instead of
    /// `/usr/local/conda/envs/<name>`. Note the name `CONDA_DEFAULT_ENV` is misleading, it's the
    /// active environment name, not a constant base environment name.
    fn from_prefix_path(path: &Path, preview: Preview) -> Self {
        // Pixi never creates true "base" envs and names project envs "default", confusing our
        // heuristics, so treat Pixi prefixes as child envs outright.
        if is_pixi_environment(path) {
            return Self::Child;
        }

        // If `_CONDA_ROOT` is set and matches `CONDA_PREFIX`, it's the base environment.
        if let Ok(conda_root) = env::var(EnvVars::CONDA_ROOT) {
            if path == Path::new(&conda_root) {
                return Self::Base;
            }
        }

        // Next, we'll use a heuristic based on `CONDA_DEFAULT_ENV`
        let Ok(current_env) = env::var(EnvVars::CONDA_DEFAULT_ENV) else {
            return Self::Child;
        };

        // If the `CONDA_PREFIX` equals the `CONDA_DEFAULT_ENV`, we're in an unnamed environment
        // which is typical for environments created with `conda create -p /path/to/env`.
        if path == Path::new(&current_env) {
            return Self::Child;
        }

        // If the environment name is "base" or "root", treat it as a base environment
        //
        // These are the expected names for the base environment; and is retained for backwards
        // compatibility, but can be removed with the `special-conda-env-names` preview feature.
        if !preview.is_enabled(PreviewFeature::SpecialCondaEnvNames)
            && (current_env == "base" || current_env == "root")
        {
            return Self::Base;
        }

        // For other environment names, use the path-based logic
        let Some(name) = path.file_name() else {
            return Self::Child;
        };

        // If the environment is in a directory matching the name of the environment, it's not
        // usually a base environment.
        if name.to_str().is_some_and(|name| name == current_env) {
            Self::Child
        } else {
            Self::Base
        }
    }
}

/// Detect whether the current `CONDA_PREFIX` belongs to a Pixi-managed environment.
fn is_pixi_environment(path: &Path) -> bool {
    path.join("conda-meta").join("pixi").is_file()
}

/// Locate an active conda environment by inspecting environment variables.
///
/// If `base` is true, the active environment must be the base environment or `None` is returned,
/// and vice-versa.
pub(crate) fn conda_environment_from_env(
    kind: CondaEnvironmentKind,
    preview: Preview,
) -> Option<PathBuf> {
    let dir = env::var_os(EnvVars::CONDA_PREFIX).filter(|value| !value.is_empty())?;
    let path = PathBuf::from(dir);

    if kind != CondaEnvironmentKind::from_prefix_path(&path, preview) {
        return None;
    }

    Some(path)
}

/// Locate a virtual environment by searching the file system.
///
/// Searches for a `.venv` directory in the current or any parent directory. If the current
/// directory is itself a virtual environment (or a subdirectory of a virtual environment), the
/// containing virtual environment is returned.
pub(crate) fn virtualenv_from_working_dir() -> Result<Option<PathBuf>, Error> {
    let current_dir = crate::current_dir()?;

    for dir in current_dir.ancestors() {
        // If we're _within_ a virtualenv, return it.
        if uv_fs::is_virtualenv_base(dir) {
            return Ok(Some(dir.to_path_buf()));
        }

        // Otherwise, search for a `.venv` directory.
        let dot_venv = dir.join(".venv");
        if dot_venv.is_dir() {
            if !uv_fs::is_virtualenv_base(&dot_venv) {
                return Err(Error::MissingPyVenvCfg(dot_venv));
            }
            return Ok(Some(dot_venv));
        }
    }

    Ok(None)
}

/// Returns the path to the `python` executable inside a virtual environment.
pub(crate) fn virtualenv_python_executable(venv: impl AsRef<Path>) -> PathBuf {
    let venv = venv.as_ref();
    if cfg!(windows) {
        // Search for `python.exe` in the `Scripts` directory.
        let default_executable = venv.join("Scripts").join("python.exe");
        if default_executable.exists() {
            return default_executable;
        }

        // Apparently, Python installed via msys2 on Windows _might_ produce a POSIX-like layout.
        // See: https://github.com/PyO3/maturin/issues/1108
        let executable = venv.join("bin").join("python.exe");
        if executable.exists() {
            return executable;
        }

        // Fallback for Conda environments.
        let executable = venv.join("python.exe");
        if executable.exists() {
            return executable;
        }

        // If none of these exist, return the standard location
        default_executable
    } else {
        // Check for both `python3` over `python`, preferring the more specific one
        let default_executable = venv.join("bin").join("python3");
        if default_executable.exists() {
            return default_executable;
        }

        let executable = venv.join("bin").join("python");
        if executable.exists() {
            return executable;
        }

        // If none of these exist, return the standard location
        default_executable
    }
}

impl PyVenvConfiguration {
    /// Parse a `pyvenv.cfg` file into a [`PyVenvConfiguration`].
    pub fn parse(cfg: impl AsRef<Path>) -> Result<Self, Error> {
        let mut home = None;
        let mut virtualenv = false;
        let mut uv = false;
        let mut relocatable = false;
        let mut seed = false;
        let mut include_system_site_packages = true;
        let mut version = None;

        // Per https://snarky.ca/how-virtual-environments-work/, the `pyvenv.cfg` file is not a
        // valid INI file, and is instead expected to be parsed by partitioning each line on the
        // first equals sign.
        let content = fs::read_to_string(&cfg)
            .map_err(|err| Error::ParsePyVenvCfg(cfg.as_ref().to_path_buf(), err))?;
        for line in content.lines() {
            let Some((key, value)) = line.split_once('=') else {
                continue;
            };
            match key.trim() {
                "home" => {
                    home = Some(PathBuf::from(value.trim()));
                }
                "virtualenv" => {
                    virtualenv = true;
                }
                "uv" => {
                    uv = true;
                }
                "relocatable" => {
                    relocatable = value.trim().to_lowercase() == "true";
                }
                "seed" => {
                    seed = value.trim().to_lowercase() == "true";
                }
                "include-system-site-packages" => {
                    include_system_site_packages = value.trim().to_lowercase() == "true";
                }
                "version" | "version_info" => {
                    version = Some(
                        PythonVersion::from_str(value.trim())
                            .map_err(|e| io::Error::new(std::io::ErrorKind::InvalidData, e))?,
                    );
                }
                _ => {}
            }
        }

        Ok(Self {
            home,
            virtualenv,
            uv,
            relocatable,
            seed,
            include_system_site_packages,
            version,
        })
    }

    /// Returns true if the virtual environment was created with the `virtualenv` package.
    pub fn is_virtualenv(&self) -> bool {
        self.virtualenv
    }

    /// Returns true if the virtual environment was created with the uv package.
    pub fn is_uv(&self) -> bool {
        self.uv
    }

    /// Returns true if the virtual environment is relocatable.
    pub fn is_relocatable(&self) -> bool {
        self.relocatable
    }

    /// Returns true if the virtual environment was populated with seed packages.
    pub fn is_seed(&self) -> bool {
        self.seed
    }

    /// Returns true if the virtual environment should include system site packages.
    pub fn include_system_site_packages(&self) -> bool {
        self.include_system_site_packages
    }

    /// Set the key-value pair in the `pyvenv.cfg` file.
    pub fn set(content: &str, key: &str, value: &str) -> String {
        let mut lines = content.lines().map(Cow::Borrowed).collect::<Vec<_>>();
        let mut found = false;
        for line in &mut lines {
            if let Some((lhs, _)) = line.split_once('=') {
                if lhs.trim() == key {
                    *line = Cow::Owned(format!("{key} = {value}"));
                    found = true;
                    break;
                }
            }
        }
        if !found {
            lines.push(Cow::Owned(format!("{key} = {value}")));
        }
        if lines.is_empty() {
            String::new()
        } else {
            format!("{}\n", lines.join("\n"))
        }
    }
}

#[cfg(test)]
mod tests {
    use std::ffi::OsStr;

    use indoc::indoc;
    use temp_env::with_vars;
    use tempfile::tempdir;

    use super::*;

    #[test]
    fn pixi_environment_is_treated_as_child() {
        let tempdir = tempdir().unwrap();
        let prefix = tempdir.path();
        let conda_meta = prefix.join("conda-meta");

        fs::create_dir_all(&conda_meta).unwrap();
        fs::write(conda_meta.join("pixi"), []).unwrap();

        let vars = [
            (EnvVars::CONDA_ROOT, None),
            (EnvVars::CONDA_PREFIX, Some(prefix.as_os_str())),
            (EnvVars::CONDA_DEFAULT_ENV, Some(OsStr::new("example"))),
        ];

        with_vars(vars, || {
            assert_eq!(
                CondaEnvironmentKind::from_prefix_path(prefix, Preview::default()),
                CondaEnvironmentKind::Child
            );
        });
    }

    #[test]
    fn test_set_existing_key() {
        let content = indoc! {"
            home = /path/to/python
            version = 3.8.0
            include-system-site-packages = false
        "};
        let result = PyVenvConfiguration::set(content, "version", "3.9.0");
        assert_eq!(
            result,
            indoc! {"
                home = /path/to/python
                version = 3.9.0
                include-system-site-packages = false
            "}
        );
    }

    #[test]
    fn test_set_new_key() {
        let content = indoc! {"
            home = /path/to/python
            version = 3.8.0
        "};
        let result = PyVenvConfiguration::set(content, "include-system-site-packages", "false");
        assert_eq!(
            result,
            indoc! {"
                home = /path/to/python
                version = 3.8.0
                include-system-site-packages = false
            "}
        );
    }

    #[test]
    fn test_set_key_no_spaces() {
        let content = indoc! {"
            home=/path/to/python
            version=3.8.0
        "};
        let result = PyVenvConfiguration::set(content, "include-system-site-packages", "false");
        assert_eq!(
            result,
            indoc! {"
                home=/path/to/python
                version=3.8.0
                include-system-site-packages = false
            "}
        );
    }

    #[test]
    fn test_set_key_prefix() {
        let content = indoc! {"
            home = /path/to/python
            home_dir = /other/path
        "};
        let result = PyVenvConfiguration::set(content, "home", "new/path");
        assert_eq!(
            result,
            indoc! {"
                home = new/path
                home_dir = /other/path
            "}
        );
    }

    #[test]
    fn test_set_empty_content() {
        let content = "";
        let result = PyVenvConfiguration::set(content, "version", "3.9.0");
        assert_eq!(
            result,
            indoc! {"
                version = 3.9.0
            "}
        );
    }
}