Skip to content

Commit

Permalink
Add requires-python to uv init
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Jul 23, 2024
1 parent bea8bc6 commit e284896
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 17 deletions.
14 changes: 14 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1797,6 +1797,20 @@ pub struct InitArgs {
/// Do not create a readme file.
#[arg(long)]
pub no_readme: bool,

/// The Python interpreter to use to determine the minimum supported Python version.
///
/// By default, uv uses the virtual environment in the current working directory or any parent
/// directory, falling back to searching for a Python executable in `PATH`. The `--python`
/// option allows you to specify a different interpreter.
///
/// Supported formats:
/// - `3.10` looks for an installed Python 3.10 using `py --list-paths` on Windows, or
/// `python3.10` on Linux and macOS.
/// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`.
/// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.
#[arg(long, short, env = "UV_PYTHON", verbatim_doc_comment)]
pub python: Option<String>,
}

#[derive(Args)]
Expand Down
15 changes: 15 additions & 0 deletions crates/uv-resolver/src/requires_python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ impl RequiresPython {
}
}

/// Returns a [`RequiresPython`] from a version specifier.
pub fn from_specifiers(specifiers: &VersionSpecifiers) -> Result<Self, RequiresPythonError> {
let bound = RequiresPythonBound(
crate::pubgrub::PubGrubSpecifier::from_release_specifiers(specifiers)?
.iter()
.next()
.map(|(lower, _)| lower.clone())
.unwrap_or(Bound::Unbounded),
);
Ok(Self {
specifiers: specifiers.clone(),
bound,
})
}

/// Returns a [`RequiresPython`] to express the union of the given version specifiers.
///
/// For example, given `>=3.8` and `>=3.9`, this would return `>=3.8`.
Expand Down
112 changes: 97 additions & 15 deletions crates/uv/src/commands/project/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,23 @@ use std::path::PathBuf;

use anyhow::{Context, Result};
use owo_colors::OwoColorize;

use pep440_rs::Version;
use pep508_rs::PackageName;
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity};
use uv_configuration::PreviewMode;
use uv_fs::{absolutize_path, Simplified};
use uv_python::{
EnvironmentPreference, PythonFetch, PythonInstallation, PythonPreference, PythonRequest,
VersionRequest,
};
use uv_resolver::RequiresPython;
use uv_warnings::warn_user_once;
use uv_workspace::pyproject_mut::PyProjectTomlMut;
use uv_workspace::{Workspace, WorkspaceError};

use crate::commands::project::find_requires_python;
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::ExitStatus;
use crate::printer::Printer;

Expand All @@ -20,8 +29,14 @@ pub(crate) async fn init(
explicit_path: Option<String>,
name: Option<PackageName>,
no_readme: bool,
python: Option<String>,
isolated: bool,
preview: PreviewMode,
python_preference: PythonPreference,
python_fetch: PythonFetch,
connectivity: Connectivity,
native_tls: bool,
cache: &Cache,
printer: Printer,
) -> Result<ExitStatus> {
if preview.is_disabled() {
Expand Down Expand Up @@ -62,36 +77,103 @@ pub(crate) async fn init(
}
};

// Discover the current workspace, if it exists.
let workspace = if isolated {
None
} else {
// Attempt to find a workspace root.
let parent = path.parent().expect("Project path has no parent");
match Workspace::discover(parent, None).await {
Ok(workspace) => Some(workspace),
Err(WorkspaceError::MissingPyprojectToml) => None,
Err(err) => return Err(err.into()),
}
};

// Add a `requires-python` field to the `pyproject.toml`.
let requires_python = if let Some(request) = python.as_deref() {
// (1) Explicit request from user
match PythonRequest::parse(request) {
PythonRequest::Version(VersionRequest::MajorMinor(major, minor)) => {
RequiresPython::greater_than_equal_version(&Version::new([
u64::from(major),
u64::from(minor),
]))
}
PythonRequest::Version(VersionRequest::MajorMinorPatch(major, minor, patch)) => {
RequiresPython::greater_than_equal_version(&Version::new([
u64::from(major),
u64::from(minor),
u64::from(patch),
]))
}
PythonRequest::Version(VersionRequest::Range(specifiers)) => {
RequiresPython::from_specifiers(&specifiers)?
}
request => {
let reporter = PythonDownloadReporter::single(printer);
let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls);
let interpreter = PythonInstallation::find_or_fetch(
Some(request),
EnvironmentPreference::Any,
python_preference,
python_fetch,
&client_builder,
cache,
Some(&reporter),
)
.await?
.into_interpreter();
RequiresPython::greater_than_equal_version(&interpreter.python_minor_version())
}
}
} else if let Some(requires_python) = workspace
.as_ref()
.and_then(|workspace| find_requires_python(workspace).ok().flatten())
{
// (2) `Requires-Python` from the workspace
requires_python
} else {
// (3) Default to the system Python
let request = PythonRequest::Any;
let reporter = PythonDownloadReporter::single(printer);
let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls);
let interpreter = PythonInstallation::find_or_fetch(
Some(request),
EnvironmentPreference::Any,
python_preference,
python_fetch,
&client_builder,
cache,
Some(&reporter),
)
.await?
.into_interpreter();
RequiresPython::greater_than_equal_version(&interpreter.python_minor_version())
};

// Create the `pyproject.toml`.
let pyproject = indoc::formatdoc! {r#"
[project]
name = "{name}"
version = "0.1.0"
description = "Add your description here"{readme}
requires-python = "{requires_python}"
dependencies = []
[tool.uv]
dev-dependencies = []
"#,
readme = if no_readme { "" } else { "\nreadme = \"README.md\"" },
requires_python = requires_python.specifiers(),
};

fs_err::create_dir_all(&path)?;
fs_err::write(path.join("pyproject.toml"), pyproject)?;

// Discover the current workspace, if it exists.
let workspace = if isolated {
None
} else {
// Attempt to find a workspace root.
let parent = path.parent().expect("Project path has no parent");
match Workspace::discover(parent, None).await {
Ok(workspace) => Some(workspace),
Err(WorkspaceError::MissingPyprojectToml) => None,
Err(err) => return Err(err.into()),
}
};

// Create `src/{name}/__init__.py` if it does not already exist.
let src_dir = path.join("src").join(&*name.as_dist_info_name());
let init_py = src_dir.join("__init__.py");
Expand Down
6 changes: 6 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -834,8 +834,14 @@ async fn run_project(
args.path,
args.name,
args.no_readme,
args.python,
globals.isolated,
globals.preview,
globals.python_preference,
globals.python_fetch,
globals.connectivity,
globals.native_tls,
&cache,
printer,
)
.await
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ pub(crate) struct InitSettings {
pub(crate) path: Option<String>,
pub(crate) name: Option<PackageName>,
pub(crate) no_readme: bool,
pub(crate) python: Option<String>,
}

impl InitSettings {
Expand All @@ -164,12 +165,14 @@ impl InitSettings {
path,
name,
no_readme,
python,
} = args;

Self {
path,
name,
no_readme,
python,
}
}
}
Expand Down
Loading

0 comments on commit e284896

Please sign in to comment.