diff --git a/crates/distribution-types/src/resolution.rs b/crates/distribution-types/src/resolution.rs index 4171b4dde..8501d4a30 100644 --- a/crates/distribution-types/src/resolution.rs +++ b/crates/distribution-types/src/resolution.rs @@ -76,15 +76,34 @@ impl Resolution { pub fn filter(self, predicate: impl Fn(&ResolvedDist) -> bool) -> Self { let packages = self .packages - .iter() + .into_iter() .filter(|(_, dist)| predicate(dist)) - .map(|(name, dist)| (name.clone(), dist.clone())) .collect::>(); let hashes = self .hashes - .iter() + .into_iter() + .filter(|(name, _)| packages.contains_key(name)) + .collect(); + let diagnostics = self.diagnostics.clone(); + Self { + packages, + hashes, + diagnostics, + } + } + + /// Map over the resolved distributions in this resolution. + #[must_use] + pub fn map(self, predicate: impl Fn(ResolvedDist) -> ResolvedDist) -> Self { + let packages = self + .packages + .into_iter() + .map(|(name, dist)| (name, predicate(dist))) + .collect::>(); + let hashes = self + .hashes + .into_iter() .filter(|(name, _)| packages.contains_key(name)) - .map(|(name, hashes)| (name.clone(), hashes.clone())) .collect(); let diagnostics = self.diagnostics.clone(); Self { diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index d10d16370..c7706a451 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2408,6 +2408,11 @@ pub struct RunArgs { #[arg(long, conflicts_with("no_dev"))] pub only_dev: bool, + /// Install any editable dependencies, including the project and any workspace members, as + /// non-editable. + #[arg(long)] + pub no_editable: bool, + /// The command to run. /// /// If the path to a Python script (i.e., ending in `.py`), it will be @@ -2560,6 +2565,11 @@ pub struct SyncArgs { #[arg(long, conflicts_with("no_dev"))] pub only_dev: bool, + /// Install any editable dependencies, including the project and any workspace members, as + /// non-editable. + #[arg(long)] + pub no_editable: bool, + /// Do not remove extraneous packages present in the environment. /// /// When enabled, uv will make the minimum necessary changes to satisfy the requirements. @@ -3002,6 +3012,11 @@ pub struct ExportArgs { #[arg(long, conflicts_with("no_dev"))] pub only_dev: bool, + /// Install any editable dependencies, including the project and any workspace members, as + /// non-editable. + #[arg(long)] + pub no_editable: bool, + /// Include hashes for all dependencies. #[arg(long, overrides_with("no_hashes"), hide = true)] pub hashes: bool, diff --git a/crates/uv-configuration/src/editable.rs b/crates/uv-configuration/src/editable.rs new file mode 100644 index 000000000..d2474ffb0 --- /dev/null +++ b/crates/uv-configuration/src/editable.rs @@ -0,0 +1,17 @@ +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum EditableMode { + #[default] + Editable, + NonEditable, +} + +impl EditableMode { + /// Determine the editable mode based on the command-line arguments. + pub fn from_args(no_editable: bool) -> Self { + if no_editable { + Self::NonEditable + } else { + Self::Editable + } + } +} diff --git a/crates/uv-configuration/src/lib.rs b/crates/uv-configuration/src/lib.rs index 6ad8cdc68..fd2b6e1c4 100644 --- a/crates/uv-configuration/src/lib.rs +++ b/crates/uv-configuration/src/lib.rs @@ -4,6 +4,7 @@ pub use concurrency::*; pub use config_settings::*; pub use constraints::*; pub use dev::*; +pub use editable::*; pub use export_format::*; pub use extras::*; pub use hash::*; @@ -22,6 +23,7 @@ mod concurrency; mod config_settings; mod constraints; mod dev; +mod editable; mod export_format; mod extras; mod hash; diff --git a/crates/uv-resolver/src/lock/requirements_txt.rs b/crates/uv-resolver/src/lock/requirements_txt.rs index 8f4f82d09..88892292d 100644 --- a/crates/uv-resolver/src/lock/requirements_txt.rs +++ b/crates/uv-resolver/src/lock/requirements_txt.rs @@ -13,7 +13,7 @@ use url::Url; use distribution_filename::{DistExtension, SourceDistExtension}; use pep508_rs::MarkerTree; use pypi_types::{ParsedArchiveUrl, ParsedGitUrl}; -use uv_configuration::{DevSpecification, ExtrasSpecification, InstallOptions}; +use uv_configuration::{DevSpecification, EditableMode, ExtrasSpecification, InstallOptions}; use uv_fs::Simplified; use uv_git::GitReference; use uv_normalize::{ExtraName, PackageName}; @@ -35,6 +35,7 @@ struct Node<'lock> { pub struct RequirementsTxtExport<'lock> { nodes: Vec>, hashes: bool, + editable: EditableMode, } impl<'lock> RequirementsTxtExport<'lock> { @@ -43,6 +44,7 @@ impl<'lock> RequirementsTxtExport<'lock> { root_name: &PackageName, extras: &ExtrasSpecification, dev: DevSpecification<'_>, + editable: EditableMode, hashes: bool, install_options: &'lock InstallOptions, ) -> Result { @@ -166,7 +168,11 @@ impl<'lock> RequirementsTxtExport<'lock> { NodeComparator::from(a.package).cmp(&NodeComparator::from(b.package)) }); - Ok(Self { nodes, hashes }) + Ok(Self { + nodes, + hashes, + editable, + }) } } @@ -216,9 +222,18 @@ impl std::fmt::Display for RequirementsTxtExport<'_> { write!(f, "{}", anchor(path).portable_display())?; } } - Source::Editable(path) => { - write!(f, "-e {}", anchor(path).portable_display())?; - } + Source::Editable(path) => match self.editable { + EditableMode::Editable => { + write!(f, "-e {}", anchor(path).portable_display())?; + } + EditableMode::NonEditable => { + if path.is_absolute() { + write!(f, "{}", Url::from_file_path(path).unwrap())?; + } else { + write!(f, "{}", anchor(path).portable_display())?; + } + } + }, Source::Virtual(_) => { continue; } diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 306ced48b..48d352715 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -16,7 +16,8 @@ use uv_auth::{store_credentials_from_url, Credentials}; use uv_cache::Cache; use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - Concurrency, Constraints, DevMode, ExtrasSpecification, InstallOptions, SourceStrategy, + Concurrency, Constraints, DevMode, EditableMode, ExtrasSpecification, InstallOptions, + SourceStrategy, }; use uv_dispatch::BuildDispatch; use uv_distribution::DistributionDatabase; @@ -755,6 +756,7 @@ async fn lock_and_sync( &lock, &extras, dev, + EditableMode::Editable, InstallOptions::default(), Modifications::Sufficient, settings.into(), diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index bcb32bdf1..4fd595489 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -8,7 +8,8 @@ use std::path::PathBuf; use uv_cache::Cache; use uv_client::Connectivity; use uv_configuration::{ - Concurrency, DevMode, DevSpecification, ExportFormat, ExtrasSpecification, InstallOptions, + Concurrency, DevMode, DevSpecification, EditableMode, ExportFormat, ExtrasSpecification, + InstallOptions, }; use uv_fs::CWD; use uv_normalize::{PackageName, DEV_DEPENDENCIES}; @@ -33,6 +34,7 @@ pub(crate) async fn export( output_file: Option, extras: ExtrasSpecification, dev: DevMode, + editable: EditableMode, locked: bool, frozen: bool, python: Option, @@ -130,6 +132,7 @@ pub(crate) async fn export( project.project_name(), &extras, dev, + editable, hashes, &install_options, )?; diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 5dee4c70c..f018d8c54 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -6,7 +6,7 @@ use owo_colors::OwoColorize; use pep508_rs::PackageName; use uv_cache::Cache; use uv_client::Connectivity; -use uv_configuration::{Concurrency, DevMode, ExtrasSpecification, InstallOptions}; +use uv_configuration::{Concurrency, DevMode, EditableMode, ExtrasSpecification, InstallOptions}; use uv_fs::{Simplified, CWD}; use uv_python::{PythonDownloads, PythonPreference, PythonRequest}; use uv_scripts::Pep723Script; @@ -188,8 +188,8 @@ pub(crate) async fn remove( // Perform a full sync, because we don't know what exactly is affected by the removal. // TODO(ibraheem): Should we accept CLI overrides for this? Should we even sync here? - let extras = ExtrasSpecification::All; let dev = DevMode::Include; + let extras = ExtrasSpecification::All; let install_options = InstallOptions::default(); // Initialize any shared state. @@ -201,6 +201,7 @@ pub(crate) async fn remove( &lock, &extras, dev, + EditableMode::Editable, install_options, Modifications::Exact, settings.as_ref().into(), diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 6f783f0aa..e30732cf7 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -14,7 +14,9 @@ use tracing::{debug, warn}; use uv_cache::Cache; use uv_cli::ExternalCommand; use uv_client::{BaseClientBuilder, Connectivity}; -use uv_configuration::{Concurrency, DevMode, ExtrasSpecification, InstallOptions, SourceStrategy}; +use uv_configuration::{ + Concurrency, DevMode, EditableMode, ExtrasSpecification, InstallOptions, SourceStrategy, +}; use uv_distribution::LoweredRequirement; use uv_fs::{PythonExt, Simplified, CWD}; use uv_installer::{SatisfiesResult, SitePackages}; @@ -58,6 +60,7 @@ pub(crate) async fn run( no_config: bool, extras: ExtrasSpecification, dev: DevMode, + editable: EditableMode, python: Option, settings: ResolverInstallerSettings, python_preference: PythonPreference, @@ -501,6 +504,7 @@ pub(crate) async fn run( result.lock(), &extras, dev, + editable, install_options, Modifications::Sufficient, settings.as_ref().into(), diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 4a533915e..980e0eab1 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -1,14 +1,14 @@ use anyhow::{Context, Result}; use itertools::Itertools; -use distribution_types::{Dist, ResolvedDist, SourceDist}; +use distribution_types::{DirectorySourceDist, Dist, ResolvedDist, SourceDist}; use pep508_rs::MarkerTree; use uv_auth::store_credentials_from_url; use uv_cache::Cache; use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - Concurrency, Constraints, DevMode, DevSpecification, ExtrasSpecification, HashCheckingMode, - InstallOptions, + Concurrency, Constraints, DevMode, DevSpecification, EditableMode, ExtrasSpecification, + HashCheckingMode, InstallOptions, }; use uv_dispatch::BuildDispatch; use uv_fs::CWD; @@ -36,6 +36,7 @@ pub(crate) async fn sync( package: Option, extras: ExtrasSpecification, dev: DevMode, + editable: EditableMode, install_options: InstallOptions, modifications: Modifications, python: Option, @@ -133,6 +134,7 @@ pub(crate) async fn sync( &lock, &extras, dev, + editable, install_options, modifications, settings.as_ref().into(), @@ -157,6 +159,7 @@ pub(super) async fn do_sync( lock: &Lock, extras: &ExtrasSpecification, dev: DevMode, + editable: EditableMode, install_options: InstallOptions, modifications: Modifications, settings: InstallerSettingsRef<'_>, @@ -242,6 +245,9 @@ pub(super) async fn do_sync( // Always skip virtual projects, which shouldn't be built or installed. let resolution = apply_no_virtual_project(resolution); + // If necessary, convert editable to non-editable distributions. + let resolution = apply_editable_mode(resolution, editable); + // Add all authenticated sources to the cache. for url in index_locations.urls() { store_credentials_from_url(url); @@ -358,3 +364,38 @@ fn apply_no_virtual_project( !dist.r#virtual }) } + +/// If necessary, convert any editable requirements to non-editable. +fn apply_editable_mode( + resolution: distribution_types::Resolution, + editable: EditableMode, +) -> distribution_types::Resolution { + match editable { + // No modifications are necessary for editable mode; retain any editable distributions. + EditableMode::Editable => resolution, + + // Filter out any editable distributions. + EditableMode::NonEditable => resolution.map(|dist| { + let ResolvedDist::Installable(Dist::Source(SourceDist::Directory( + DirectorySourceDist { + name, + install_path, + editable: true, + r#virtual: false, + url, + }, + ))) = dist + else { + return dist; + }; + + ResolvedDist::Installable(Dist::Source(SourceDist::Directory(DirectorySourceDist { + name, + install_path, + editable: false, + r#virtual: false, + url, + }))) + }), + } +} diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index f9305e878..8709865b4 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1157,6 +1157,7 @@ async fn run_project( no_config, args.extras, args.dev, + args.editable, args.python, args.settings, globals.python_preference, @@ -1187,6 +1188,7 @@ async fn run_project( args.package, args.extras, args.dev, + args.editable, args.install_options, args.modifications, args.python, @@ -1355,6 +1357,7 @@ async fn run_project( args.output_file, args.extras, args.dev, + args.editable, args.locked, args.frozen, args.python, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index a2b7ca856..25898e21d 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -22,9 +22,9 @@ use uv_cli::{ }; use uv_client::Connectivity; use uv_configuration::{ - BuildOptions, Concurrency, ConfigSettings, DevMode, ExportFormat, ExtrasSpecification, - HashCheckingMode, IndexStrategy, InstallOptions, KeyringProviderType, NoBinary, NoBuild, - PreviewMode, Reinstall, SourceStrategy, TargetTriple, TrustedHost, Upgrade, + BuildOptions, Concurrency, ConfigSettings, DevMode, EditableMode, ExportFormat, + ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions, KeyringProviderType, + NoBinary, NoBuild, PreviewMode, Reinstall, SourceStrategy, TargetTriple, TrustedHost, Upgrade, }; use uv_normalize::PackageName; use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target}; @@ -210,6 +210,7 @@ pub(crate) struct RunSettings { pub(crate) frozen: bool, pub(crate) extras: ExtrasSpecification, pub(crate) dev: DevMode, + pub(crate) editable: EditableMode, pub(crate) with: Vec, pub(crate) with_editable: Vec, pub(crate) with_requirements: Vec, @@ -234,6 +235,7 @@ impl RunSettings { dev, no_dev, only_dev, + no_editable, command: _, with, with_editable, @@ -259,6 +261,7 @@ impl RunSettings { extra.unwrap_or_default(), ), dev: DevMode::from_args(dev, no_dev, only_dev), + editable: EditableMode::from_args(no_editable), with, with_editable, with_requirements: with_requirements @@ -661,6 +664,7 @@ pub(crate) struct SyncSettings { pub(crate) frozen: bool, pub(crate) extras: ExtrasSpecification, pub(crate) dev: DevMode, + pub(crate) editable: EditableMode, pub(crate) install_options: InstallOptions, pub(crate) modifications: Modifications, pub(crate) package: Option, @@ -680,6 +684,7 @@ impl SyncSettings { dev, no_dev, only_dev, + no_editable, inexact, exact, no_install_project, @@ -707,6 +712,7 @@ impl SyncSettings { extra.unwrap_or_default(), ), dev: DevMode::from_args(dev, no_dev, only_dev), + editable: EditableMode::from_args(no_editable), install_options: InstallOptions::new( no_install_project, no_install_workspace, @@ -961,6 +967,7 @@ pub(crate) struct ExportSettings { pub(crate) package: Option, pub(crate) extras: ExtrasSpecification, pub(crate) dev: DevMode, + pub(crate) editable: EditableMode, pub(crate) hashes: bool, pub(crate) install_options: InstallOptions, pub(crate) output_file: Option, @@ -984,6 +991,7 @@ impl ExportSettings { dev, no_dev, only_dev, + no_editable, hashes, no_hashes, output_file, @@ -1006,6 +1014,7 @@ impl ExportSettings { extra.unwrap_or_default(), ), dev: DevMode::from_args(dev, no_dev, only_dev), + editable: EditableMode::from_args(no_editable), hashes: flag(hashes, no_hashes).unwrap_or(true), install_options: InstallOptions::new( no_emit_project, diff --git a/crates/uv/tests/export.rs b/crates/uv/tests/export.rs index fb24520cd..babaf5021 100644 --- a/crates/uv/tests/export.rs +++ b/crates/uv/tests/export.rs @@ -905,3 +905,73 @@ fn no_emit() -> Result<()> { Ok(()) } + +#[test] +fn no_editable() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0", "child"] + + [tool.uv.workspace] + members = ["child"] + + [tool.uv.sources] + child = { workspace = true } + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + let child = context.temp_dir.child("child"); + child.child("pyproject.toml").write_str( + r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig>=2"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export().arg("--no-editable"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --no-editable + . + ./child + anyio==3.7.0 \ + --hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \ + --hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0 + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + sniffio==1.3.1 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + Ok(()) +} diff --git a/crates/uv/tests/sync.rs b/crates/uv/tests/sync.rs index 2c26b5487..7fcc80c3d 100644 --- a/crates/uv/tests/sync.rs +++ b/crates/uv/tests/sync.rs @@ -2392,6 +2392,89 @@ fn transitive_dev() -> Result<()> { Ok(()) } +/// Avoid installing dev dependencies of transitive dependencies. +#[test] +fn sync_no_editable() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "root" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["child"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [tool.uv.sources] + child = { workspace = true } + + [tool.uv.workspace] + members = ["child"] + "#, + )?; + + let src = context.temp_dir.child("src").child("albatross"); + src.create_dir_all()?; + + let init = src.child("__init__.py"); + init.touch()?; + + let child = context.temp_dir.child("child"); + fs_err::create_dir_all(&child)?; + + let pyproject_toml = child.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.12" + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + let src = child.child("src").child("child"); + src.create_dir_all()?; + + let init = src.child("__init__.py"); + init.touch()?; + + uv_snapshot!(context.filters(), context.sync().arg("--no-editable"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + child==0.1.0 (from file://[TEMP_DIR]/child) + + root==0.1.0 (from file://[TEMP_DIR]/) + "###); + + // Remove the project. + fs_err::remove_dir_all(&child)?; + + // Ensure that we can still import it. + uv_snapshot!(context.filters(), context.run().arg("--no-sync").arg("python").arg("-c").arg("import child"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "###); + + Ok(()) +} + #[test] /// Check warning message for /// if no `build-system` section is defined. diff --git a/docs/concepts/projects.md b/docs/concepts/projects.md index 51664838c..cb98cf2d0 100644 --- a/docs/concepts/projects.md +++ b/docs/concepts/projects.md @@ -67,9 +67,9 @@ project should be packaged and installed. uv uses the presence of a build system to determine if a project contains a package that should be installed in the project virtual environment. If a build system is not defined, uv will not attempt to build or install the project itself, just its dependencies. If a build system is defined, uv will -build and install the project into the project environment. Projects are installed in +build and install the project into the project environment. By default, projects are installed in [editable mode](https://setuptools.pypa.io/en/latest/userguide/development_mode.html) so changes to -the source code are reflected immediately, without reinstallation. +the source code are reflected immediately, without re-installation. ### Configuring project packaging @@ -297,6 +297,12 @@ use [`uvx`](../guides/tools.md) or managed = false ``` +By default, the project will be installed in editable mode, such that changes to the source code are +immediately reflected in the environment. `uv sync` and `uv run` both accept a `--no-editable` flag, +which instructs uv to install the project in non-editable mode. `--no-editable` is intended for +deployment use-cases, such as building a Docker container, in which the project should be included +in the deployed environment without a dependency on the originating source code. + ### Configuring the project environment path The `UV_PROJECT_ENVIRONMENT` environment variable can be used to configure the project virtual diff --git a/docs/guides/integration/docker.md b/docs/guides/integration/docker.md index a7316b753..90267b0cc 100644 --- a/docs/guides/integration/docker.md +++ b/docs/guides/integration/docker.md @@ -360,6 +360,50 @@ _contents_ are not copied into the image until the final `uv sync` command. If you want to remove specific packages from the sync, use `--no-install-package `. +### Non-editable installs + +By default, uv installs projects and workspace members in editable mode, such that changes to the +source code are immediately reflected in the environment. + +`uv sync` and `uv run` both accept a `--no-editable` flag, which instructs uv to install the project +in non-editable mode, removing any dependency on the source code. + +In the context of a multi-stage Docker image, `--no-editable` can be used to include the project in +the synced virtual environment from one stage, then copy the virtual environment alone (and not the +source code) into the final image. + +For example: + +```dockerfile title="Dockerfile" +# Install uv +FROM python:3.12-slim AS builder +COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv + +# Change the working directory to the `app` directory +WORKDIR /app + +# Install dependencies +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --frozen --no-install-project --no-editable + +# Copy the project into the intermediate image +ADD . /app + +# Sync the project +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen --no-editable + +FROM python:3.12-slim + +# Copy the environment, but not the source code +COPY --from=builder --chown=app:app /app/.venv /app/.venv + +# Run the application +CMD ["/app/.venv/bin/hello"] +``` + ### Using uv temporarily If uv isn't needed in the final image, the binary can be mounted in each invocation: diff --git a/docs/reference/cli.md b/docs/reference/cli.md index dde79738b..909d0cad5 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -249,6 +249,8 @@ uv run [OPTIONS]

This option is only available when running in a project.

+
--no-editable

Install any editable dependencies, including the project and any workspace members, as non-editable

+
--no-index

Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via --find-links

--no-progress

Hide all progress outputs.

@@ -1313,6 +1315,8 @@ uv sync [OPTIONS]

May also be set with the UV_NO_CONFIG environment variable.

--no-dev

Omit development dependencies

+
--no-editable

Install any editable dependencies, including the project and any workspace members, as non-editable

+
--no-index

Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via --find-links

--no-install-package no-install-package

Do not install the given package(s).

@@ -1869,6 +1873,8 @@ uv export [OPTIONS]

May also be set with the UV_NO_CONFIG environment variable.

--no-dev

Omit development dependencies

+
--no-editable

Install any editable dependencies, including the project and any workspace members, as non-editable

+
--no-emit-package no-emit-package

Do not emit the given package(s).

By default, all of the project’s dependencies are included in the exported requirements file. The --no-install-package option allows exclusion of specific packages.