mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-16 17:55:01 +00:00
Refactor .python-version
discovery (#6359)
In preparation for more comprehensive discovery
This commit is contained in:
parent
1377c6807d
commit
2fbe12ee1b
9 changed files with 186 additions and 133 deletions
|
@ -1,6 +1,7 @@
|
|||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use fs_err as fs;
|
||||
use itertools::Itertools;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::PythonRequest;
|
||||
|
@ -11,63 +12,51 @@ pub static PYTHON_VERSION_FILENAME: &str = ".python-version";
|
|||
/// The file name for multiple Python version declarations.
|
||||
pub static PYTHON_VERSIONS_FILENAME: &str = ".python-versions";
|
||||
|
||||
/// Read [`PythonRequest`]s from a version file, if present.
|
||||
///
|
||||
/// Prefers `.python-versions` then `.python-version`.
|
||||
/// If only one Python version is desired, use [`request_from_version_files`] which prefers the `.python-version` file.
|
||||
pub async fn requests_from_version_file(
|
||||
directory: &Path,
|
||||
) -> Result<Option<Vec<PythonRequest>>, std::io::Error> {
|
||||
if let Some(versions) = read_versions_file(directory).await? {
|
||||
Ok(Some(
|
||||
versions
|
||||
.into_iter()
|
||||
.map(|version| PythonRequest::parse(&version))
|
||||
.collect(),
|
||||
))
|
||||
} else if let Some(version) = read_version_file(directory).await? {
|
||||
Ok(Some(vec![PythonRequest::parse(&version)]))
|
||||
} else {
|
||||
/// A `.python-version` or `.python-versions` file.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PythonVersionFile {
|
||||
/// The path to the version file.
|
||||
path: PathBuf,
|
||||
/// The Python version requests declared in the file.
|
||||
versions: Vec<PythonRequest>,
|
||||
}
|
||||
|
||||
impl PythonVersionFile {
|
||||
/// Find a Python version file in the given directory.
|
||||
pub async fn discover(
|
||||
working_directory: impl AsRef<Path>,
|
||||
no_config: bool,
|
||||
) -> Result<Option<Self>, std::io::Error> {
|
||||
let versions_path = working_directory.as_ref().join(PYTHON_VERSIONS_FILENAME);
|
||||
let version_path = working_directory.as_ref().join(PYTHON_VERSION_FILENAME);
|
||||
|
||||
if no_config {
|
||||
if version_path.exists() {
|
||||
debug!("Ignoring `.python-version` file due to `--no-config`");
|
||||
} else if versions_path.exists() {
|
||||
debug!("Ignoring `.python-versions` file due to `--no-config`");
|
||||
};
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if let Some(result) = Self::try_from_path(version_path).await? {
|
||||
return Ok(Some(result));
|
||||
};
|
||||
if let Some(result) = Self::try_from_path(versions_path).await? {
|
||||
return Ok(Some(result));
|
||||
};
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a [`PythonRequest`] from a version file, if present.
|
||||
///
|
||||
/// Find the version file inside directory, or the current directory
|
||||
/// if None.
|
||||
///
|
||||
/// Prefers `.python-version` then the first entry of `.python-versions`.
|
||||
/// If multiple Python versions are desired, use [`requests_from_version_files`] instead.
|
||||
pub async fn request_from_version_file(
|
||||
directory: &Path,
|
||||
) -> Result<Option<PythonRequest>, std::io::Error> {
|
||||
if let Some(version) = read_version_file(directory).await? {
|
||||
Ok(Some(PythonRequest::parse(&version)))
|
||||
} else if let Some(versions) = read_versions_file(directory).await? {
|
||||
Ok(versions
|
||||
.into_iter()
|
||||
.next()
|
||||
.inspect(|_| debug!("Using the first version from `{PYTHON_VERSIONS_FILENAME}`"))
|
||||
.map(|version| PythonRequest::parse(&version)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a version to a .`python-version` file.
|
||||
pub async fn write_version_file(version: &str) -> Result<(), std::io::Error> {
|
||||
debug!("Writing Python version `{version}` to `{PYTHON_VERSION_FILENAME}`");
|
||||
fs::tokio::write(PYTHON_VERSION_FILENAME, format!("{version}\n")).await
|
||||
}
|
||||
|
||||
async fn read_versions_file(directory: &Path) -> Result<Option<Vec<String>>, std::io::Error> {
|
||||
let path = directory.join(PYTHON_VERSIONS_FILENAME);
|
||||
match fs::tokio::read_to_string(&path).await {
|
||||
Ok(content) => {
|
||||
debug!("Reading requests from `{}`", path.display());
|
||||
Ok(Some(
|
||||
content
|
||||
/// Try to read a Python version file at the given path.
|
||||
///
|
||||
/// If the file does not exist, `Ok(None)` is returned.
|
||||
pub async fn try_from_path(path: PathBuf) -> Result<Option<Self>, std::io::Error> {
|
||||
match fs::tokio::read_to_string(&path).await {
|
||||
Ok(content) => {
|
||||
debug!("Reading requests from `{}`", path.display());
|
||||
let versions = content
|
||||
.lines()
|
||||
.filter(|line| {
|
||||
// Skip comments and empty lines.
|
||||
|
@ -75,29 +64,84 @@ async fn read_versions_file(directory: &Path) -> Result<Option<Vec<String>>, std
|
|||
!(trimmed.is_empty() || trimmed.starts_with('#'))
|
||||
})
|
||||
.map(ToString::to_string)
|
||||
.collect(),
|
||||
))
|
||||
.map(|version| PythonRequest::parse(&version))
|
||||
.collect();
|
||||
Ok(Some(Self { path, versions }))
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_version_file(directory: &Path) -> Result<Option<String>, std::io::Error> {
|
||||
let path = directory.join(PYTHON_VERSION_FILENAME);
|
||||
match fs::tokio::read_to_string(&path).await {
|
||||
Ok(content) => {
|
||||
debug!("Reading requests from `{}`", path.display());
|
||||
Ok(content
|
||||
.lines()
|
||||
.find(|line| {
|
||||
// Skip comments and empty lines.
|
||||
let trimmed = line.trim();
|
||||
!(trimmed.is_empty() || trimmed.starts_with('#'))
|
||||
})
|
||||
.map(ToString::to_string))
|
||||
/// Read a Python version file at the given path.
|
||||
///
|
||||
/// If the file does not exist, an error is returned.
|
||||
pub async fn from_path(path: PathBuf) -> Result<Self, std::io::Error> {
|
||||
let Some(result) = Self::try_from_path(path).await? else {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
"Version file not found".to_string(),
|
||||
));
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Create a new representation of a version file at the given path.
|
||||
///
|
||||
/// The file will not any versions; see [`PythonVersionFile::with_versions`].
|
||||
/// The file will not be created; see [`PythonVersionFile::write`].
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
Self {
|
||||
path,
|
||||
versions: vec![],
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
|
||||
/// Return the first version declared in the file, if any.
|
||||
pub fn version(&self) -> Option<&PythonRequest> {
|
||||
self.versions.first()
|
||||
}
|
||||
|
||||
/// Iterate of all versions declared in the file.
|
||||
pub fn versions(&self) -> impl Iterator<Item = &PythonRequest> {
|
||||
self.versions.iter()
|
||||
}
|
||||
|
||||
/// Cast to a list of all versions declared in the file.
|
||||
pub fn into_versions(self) -> Vec<PythonRequest> {
|
||||
self.versions
|
||||
}
|
||||
|
||||
/// Cast to the first version declared in the file, if any.
|
||||
pub fn into_version(self) -> Option<PythonRequest> {
|
||||
self.versions.into_iter().next()
|
||||
}
|
||||
|
||||
/// Return the path to the version file.
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// Set the versions for the file.
|
||||
#[must_use]
|
||||
pub fn with_versions(self, versions: Vec<PythonRequest>) -> Self {
|
||||
Self {
|
||||
path: self.path,
|
||||
versions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the version file on the file system.
|
||||
pub async fn write(&self) -> Result<(), std::io::Error> {
|
||||
debug!("Writing Python versions to `{}`", self.path.display());
|
||||
fs::tokio::write(
|
||||
&self.path,
|
||||
self.versions
|
||||
.iter()
|
||||
.map(PythonRequest::to_canonical_string)
|
||||
.join("\n")
|
||||
.as_bytes(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue