Skip to content

Commit

Permalink
Methods moved from Workspace to VirtualProject to allow private locks…
Browse files Browse the repository at this point in the history
… support (astral-sh#4574)
  • Loading branch information
idlsoft committed Oct 15, 2024
1 parent 34be3af commit 3501a0a
Show file tree
Hide file tree
Showing 10 changed files with 286 additions and 267 deletions.
389 changes: 217 additions & 172 deletions crates/uv-workspace/src/workspace.rs

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ pub(crate) async fn add(

// Discover or create the virtual environment.
let venv = project::get_or_init_environment(
project.workspace(),
&project,
python.as_deref().map(PythonRequest::parse),
python_preference,
python_downloads,
Expand Down Expand Up @@ -634,7 +634,7 @@ async fn lock_and_sync(
let mut lock = project::lock::do_safe_lock(
locked,
frozen,
project.workspace(),
&project,
venv.interpreter(),
settings.into(),
&state,
Expand Down Expand Up @@ -742,7 +742,7 @@ async fn lock_and_sync(
lock = project::lock::do_safe_lock(
locked,
frozen,
project.workspace(),
&project,
venv.interpreter(),
settings.into(),
&state,
Expand Down
8 changes: 4 additions & 4 deletions crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ pub(crate) async fn export(
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
};

let VirtualProject::Project(project) = project else {
let VirtualProject::Project(_) = project else {
return Err(anyhow::anyhow!("Legacy non-project roots are not supported in `uv export`; add a `[project]` table to your `pyproject.toml` to enable exports"));
};

// Find an interpreter for the project
let interpreter = ProjectInterpreter::discover(
project.workspace(),
&project,
python.as_deref().map(PythonRequest::parse),
python_preference,
python_downloads,
Expand All @@ -95,7 +95,7 @@ pub(crate) async fn export(
let lock = match do_safe_lock(
locked,
frozen,
project.workspace(),
&project,
&interpreter,
settings.as_ref(),
&state,
Expand Down Expand Up @@ -145,7 +145,7 @@ pub(crate) async fn export(
ExportFormat::RequirementsTxt => {
let export = RequirementsTxtExport::from_lock(
&lock,
project.project_name(),
project.project_name().unwrap(),
&extras,
dev,
editable,
Expand Down
88 changes: 40 additions & 48 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use uv_resolver::{
};
use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::{DiscoveryOptions, Workspace};
use uv_workspace::{DiscoveryOptions, VirtualProject};

use crate::commands::pip::loggers::{DefaultResolveLogger, ResolveLogger, SummaryResolveLogger};
use crate::commands::project::{
Expand Down Expand Up @@ -85,11 +85,11 @@ pub(crate) async fn lock(
printer: Printer,
) -> anyhow::Result<ExitStatus> {
// Find the project requirements.
let workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?;
let project = VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?;

// Find an interpreter for the project
let interpreter = ProjectInterpreter::discover(
&workspace,
&project,
python.as_deref().map(PythonRequest::parse),
python_preference,
python_downloads,
Expand All @@ -108,7 +108,7 @@ pub(crate) async fn lock(
match do_safe_lock(
locked,
frozen,
&workspace,
&project,
&interpreter,
settings.as_ref(),
&state,
Expand Down Expand Up @@ -154,7 +154,7 @@ pub(crate) async fn lock(
pub(super) async fn do_safe_lock(
locked: bool,
frozen: bool,
workspace: &Workspace,
project: &VirtualProject,
interpreter: &Interpreter,
settings: ResolverSettingsRef<'_>,
state: &SharedState,
Expand All @@ -167,19 +167,19 @@ pub(super) async fn do_safe_lock(
) -> Result<LockResult, ProjectError> {
if frozen {
// Read the existing lockfile, but don't attempt to lock the project.
let existing = read(workspace)
let existing = read(project)
.await?
.ok_or_else(|| ProjectError::MissingLockfile)?;
Ok(LockResult::Unchanged(existing))
} else if locked {
// Read the existing lockfile.
let existing = read(workspace)
let existing = read(project)
.await?
.ok_or_else(|| ProjectError::MissingLockfile)?;

// Perform the lock operation, but don't write the lockfile to disk.
let result = do_lock(
workspace,
project,
interpreter,
Some(existing),
settings,
Expand All @@ -201,11 +201,11 @@ pub(super) async fn do_safe_lock(
Ok(result)
} else {
// Read the existing lockfile.
let existing = read(workspace).await?;
let existing = read(project).await?;

// Perform the lock operation.
let result = do_lock(
workspace,
project,
interpreter,
existing,
settings,
Expand All @@ -221,7 +221,7 @@ pub(super) async fn do_safe_lock(

// If the lockfile changed, write it to disk.
if let LockResult::Changed(_, lock) = &result {
commit(lock, workspace).await?;
commit(lock, project).await?;
}

Ok(result)
Expand All @@ -230,7 +230,7 @@ pub(super) async fn do_safe_lock(

/// Lock the project requirements into a lockfile.
async fn do_lock(
workspace: &Workspace,
project: &VirtualProject,
interpreter: &Interpreter,
existing_lock: Option<Lock>,
settings: ResolverSettingsRef<'_>,
Expand Down Expand Up @@ -264,30 +264,21 @@ async fn do_lock(
} = settings;

// Collect the requirements, etc.
let requirements = workspace.non_project_requirements().collect::<Vec<_>>();
let overrides = workspace.overrides().into_iter().collect::<Vec<_>>();
let constraints = workspace.constraints();
let requirements = project
.workspace()
.non_project_requirements()
.collect::<Vec<_>>();
let overrides = project.overrides().into_iter().collect::<Vec<_>>();
let constraints = project.constraints();
let dev = vec![DEV_DEPENDENCIES.clone()];
let source_trees = vec![];

// Collect the list of members.
let members = {
let mut members = workspace.packages().keys().cloned().collect::<Vec<_>>();
members.sort();

// If this is a non-virtual project with a single member, we can omit it from the lockfile.
// If any members are added or removed, it will inherently mismatch. If the member is
// renamed, it will also mismatch.
if members.len() == 1 && !workspace.is_non_project() {
members.clear();
}

members
};
let members = project.packages_to_lock();

// Collect the list of supported environments.
let environments = {
let environments = workspace.environments();
let environments = project.environments();

// Ensure that the environments are disjoint.
if let Some(environments) = &environments {
Expand Down Expand Up @@ -323,7 +314,7 @@ async fn do_lock(

// Determine the supported Python range. If no range is defined, and warn and default to the
// current minor version.
let requires_python = find_requires_python(workspace)?;
let requires_python = find_requires_python(project.workspace())?;

let requires_python = if let Some(requires_python) = requires_python {
if requires_python.is_unbounded() {
Expand Down Expand Up @@ -443,7 +434,7 @@ async fn do_lock(
let existing_lock = if let Some(existing_lock) = existing_lock {
match ValidatedLock::validate(
existing_lock,
workspace,
project,
&members,
&requirements,
&constraints,
Expand Down Expand Up @@ -537,7 +528,7 @@ async fn do_lock(
let resolution = pip::operations::resolve(
ExtrasResolver::new(&hasher, &state.index, database)
.with_reporter(ResolverReporter::from(printer))
.resolve(workspace.members_requirements())
.resolve(project.members_requirements())
.await?
.into_iter()
.chain(requirements.iter().cloned())
Expand All @@ -557,7 +548,7 @@ async fn do_lock(
source_trees,
// The root is always null in workspaces, it "depends on" the projects
None,
Some(workspace.packages().keys().cloned().collect()),
Some(project.packages_to_resolve().cloned().collect()),
&extras,
preferences,
EmptyInstalledPackages,
Expand Down Expand Up @@ -591,17 +582,18 @@ async fn do_lock(
overrides,
dependency_metadata.values().cloned(),
)
.relative_to(workspace)?;
.relative_to(project.workspace())?;

let previous = existing_lock.map(ValidatedLock::into_lock);
let lock = Lock::from_resolution_graph(&resolution, workspace.install_path())?
.with_manifest(manifest)
.with_supported_environments(
environments
.cloned()
.map(SupportedEnvironments::into_markers)
.unwrap_or_default(),
);
let lock =
Lock::from_resolution_graph(&resolution, project.workspace().install_path())?
.with_manifest(manifest)
.with_supported_environments(
environments
.cloned()
.map(SupportedEnvironments::into_markers)
.unwrap_or_default(),
);

Ok(LockResult::Changed(previous, lock))
}
Expand All @@ -626,7 +618,7 @@ impl ValidatedLock {
/// Validate a [`Lock`] against the workspace requirements.
async fn validate<Context: BuildContext>(
lock: Lock,
workspace: &Workspace,
project: &VirtualProject,
members: &[PackageName],
requirements: &[Requirement],
constraints: &[Requirement],
Expand Down Expand Up @@ -756,7 +748,7 @@ impl ValidatedLock {
// Determine whether the lockfile satisfies the workspace requirements.
match lock
.satisfies(
workspace,
project.workspace(),
members,
requirements,
constraints,
Expand Down Expand Up @@ -880,17 +872,17 @@ impl ValidatedLock {
}

/// Write the lockfile to disk.
async fn commit(lock: &Lock, workspace: &Workspace) -> Result<(), ProjectError> {
async fn commit(lock: &Lock, project: &VirtualProject) -> Result<(), ProjectError> {
let encoded = lock.to_toml()?;
fs_err::tokio::write(workspace.install_path().join("uv.lock"), encoded).await?;
fs_err::tokio::write(project.lockfile(), encoded).await?;
Ok(())
}

/// Read the lockfile from the workspace.
///
/// Returns `Ok(None)` if the lockfile does not exist.
pub(crate) async fn read(workspace: &Workspace) -> Result<Option<Lock>, ProjectError> {
match fs_err::tokio::read_to_string(&workspace.install_path().join("uv.lock")).await {
pub(crate) async fn read(project: &VirtualProject) -> Result<Option<Lock>, ProjectError> {
match fs_err::tokio::read_to_string(&project.lockfile()).await {
Ok(encoded) => match toml::from_str(&encoded) {
Ok(lock) => Ok(Some(lock)),
Err(err) => {
Expand Down
34 changes: 10 additions & 24 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use uv_resolver::{
};
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::Workspace;
use uv_workspace::{VirtualProject, Workspace};

use crate::commands::pip::loggers::{InstallLogger, ResolveLogger};
use crate::commands::pip::operations::{Changelog, Modifications};
Expand Down Expand Up @@ -359,7 +359,7 @@ impl WorkspacePython {
impl ProjectInterpreter {
/// Discover the interpreter to use in the current [`Workspace`].
pub(crate) async fn discover(
workspace: &Workspace,
project: &VirtualProject,
python_request: Option<PythonRequest>,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
Expand All @@ -373,10 +373,10 @@ impl ProjectInterpreter {
source,
python_request,
requires_python,
} = WorkspacePython::from_request(python_request, workspace).await?;
} = WorkspacePython::from_request(python_request, project.workspace()).await?;

// Read from the virtual environment first.
let venv = workspace.venv();
let venv = project.venv();
match PythonEnvironment::from_root(&venv, cache) {
Ok(venv) => {
if python_request.as_ref().map_or(true, |request| {
Expand Down Expand Up @@ -477,7 +477,7 @@ impl ProjectInterpreter {
}

if let Some(requires_python) = requires_python.as_ref() {
validate_requires_python(&interpreter, workspace, requires_python, &source)?;
validate_requires_python(&interpreter, project.workspace(), requires_python, &source)?;
}

Ok(Self::Interpreter(interpreter))
Expand All @@ -494,7 +494,7 @@ impl ProjectInterpreter {

/// Initialize a virtual environment for the current project.
pub(crate) async fn get_or_init_environment(
workspace: &Workspace,
project: &VirtualProject,
python: Option<PythonRequest>,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
Expand All @@ -504,7 +504,7 @@ pub(crate) async fn get_or_init_environment(
printer: Printer,
) -> Result<PythonEnvironment, ProjectError> {
match ProjectInterpreter::discover(
workspace,
project,
python,
python_preference,
python_downloads,
Expand All @@ -520,7 +520,7 @@ pub(crate) async fn get_or_init_environment(

// Otherwise, create a virtual environment with the discovered interpreter.
ProjectInterpreter::Interpreter(interpreter) => {
let venv = workspace.venv();
let venv = project.venv();

// Avoid removing things that are not virtual environments
let should_remove = match (venv.try_exists(), venv.join("pyvenv.cfg").try_exists()) {
Expand Down Expand Up @@ -565,22 +565,8 @@ pub(crate) async fn get_or_init_environment(
venv.user_display().cyan()
)?;

// Determine a prompt for the environment, in order of preference:
//
// 1) The name of the project
// 2) The name of the directory at the root of the workspace
// 3) No prompt
let prompt = workspace
.pyproject_toml()
.project
.as_ref()
.map(|p| p.name.to_string())
.or_else(|| {
workspace
.install_path()
.file_name()
.map(|f| f.to_string_lossy().to_string())
})
let prompt = project
.venv_name()
.map(uv_virtualenv::Prompt::Static)
.unwrap_or(uv_virtualenv::Prompt::None);

Expand Down
Loading

0 comments on commit 3501a0a

Please sign in to comment.