Repository: astral-sh/uv (Rust)

Issue:
Print installation path when running `uv pip install`
Hey :wave: 

Small feature request: It would be great if `uv` printed the path where it is installing the packages when running `uv pip install`. I am currently debugging a weird issue in GHA while using `uv pip install --system` and it is not clear how to check where uv ends up installing stuff. 


Discussion:
Have you tried running with `--verbose`?
Yeah, indeed running with verbose prints this:

```
 uv_interpreter::python_query::find_default_python 
      0.004003s   0ms DEBUG uv_interpreter::python_query Starting interpreter discovery for default Python
      0.004109s   0ms DEBUG uv_interpreter::interpreter Probing interpreter info for: /opt/hostedtoolcache/Python/3.10.13/x64/bin/python3
      0.039094s  35ms DEBUG uv_interpreter::interpreter Found Python 3.10.13 for: /opt/hostedtoolcache/Python/3.10.13/x64/bin/python3
    0.039395s DEBUG uv::commands::pip_install Using Python 3.10.13 environment at /opt/hostedtoolcache/Python/3.10.13/x64/bin/python3
```

To me, this is not very easy to parse, and even find among the rest of the huge debug output. At least for the `--system` flag seems like a good info to print by default, especially given the number of issues / corner cases that have been reported (and fixed!) for this particular option. But of course opinions might vary. :-) 
```
it is not clear how to check where uv ends up installing stuff.
```

I've already been bitten multiple times by this, installing stuff in the wrong venv.
Because I have 100 venvs on my system,  
and I really dislike activating a venv  (because it's an anti-patteren creating state in that specific shell.)

with actual pip, it's not really an issue, because you are always using the pip from inside the virtualenv


```
> uv pip install pytest
Resolved 4 packages in 286ms
Downloaded 1 package in 64ms
Installed 4 packages in 21ms
 + iniconfig==2.0.0
 + pytest==8.0.2
  ...
```


A much more userfriendly output would be to include the path somewhere
```
> uv pip install pytest
Resolved 4 packages.                                <- I don't care about how many millisecs it takes,  it's visual pollution
Downloaded 1 package.                           <- which one?  what if I don't expect to download anything new?
Installed 4 packages in /tmp/foo/.venv/   <- include path
  ...
  + iniconfig==2.0.0
 + pytest==8.0.2  (dowloaded)
```

So, I love the new more concise verbose output (-v), however, I will still lobby for `uv` to print the install location by default. :blush: 

As previous commenter pointed out, it's too easy to make a mistake and not notice it (e.g. forgetting to deactivate the environment, switch to different directory with its own .venv, and then running `uv pip install`)
Considering starting by showing the path if it's not `.venv` (i.e., a virtualenv in the current working directory).
> Considering starting by showing the path if it's not `.venv` (i.e., a virtualenv in the current working directory).

I think this would be really useful. I was just caught out by `uv` unexpectedly installing things into my `~dev/.venv` environment, rather than my `~dev/project/env` environment, because I didn't have the latter activated, and `uv` searched up through the directory structure until it found a virtual environment called `.venv`.
another separate, but related nicety would be printing out the `Location` column that `pip list --verbose` prints:
```
$ pip list --verbose
Package            Version     Location                                                            Installer
------------------ ----------- ------------------------------------------------------------------- ---------
asttokens          2.4.1       /Users/chankang/.pyenv/versions/3.12.2/lib/python3.12/site-packages uv
build              1.2.1       /Users/chankang/.pyenv/versions/3.12.2/lib/python3.12/site-packages uv
```
 
I think it'd be reasonable to start with this: https://github.com/astral-sh/uv/issues/2155#issuecomment-2019302112
Hi! I wanted to look into this - would it be a matter of updating the line in `uv/src/commands/pip/operations.rs` to something like:

```rust
        writeln!(
            printer.stderr(),
            "{}",
            format!(
                "Installed {} in {} to {}",
                format!("{} package{}", wheels.len(), s).bold(),
                elapsed(start.elapsed()),
                venv.python_executable().display()
            )
            .dimmed()
        )?;
```

Based on the test results it looks correct (although it looks like we would need to update the snapshots)

```
Installed 3 packages in [TIME] to /private/var/folders/7s/q0mx5qbs78bfgpclqx_8tknm0000gn/T/.tmpRu7Vx5/.venv/bin/python3
```
We'd want to only display the path if it's not in the working directory which would require some sort of `if/else`. As a minor note, we use `.user_display()` for paths. Otherwise that seems like a reasonable place to start. Maybe we should print

> Installing to environment at {path}

before this message instead of adding to the end of it?
Sure! I have something like this:

```rust
        let python_executable = venv.python_executable();
        let venv_dir_canonical = fs::canonicalize(".venv").unwrap_or(PathBuf::from(".venv"));
        let is_outside_working_directory = !(python_executable.starts_with(&venv_dir_canonical));
        if is_outside_working_directory {
            writeln!(
                printer.stderr(),
                "{}",
                format!(
                    "Installing to environment at {}",
                    python_executable.user_display()
                )
            )?;
        }
```

I wasn't sure the best way to check for being in `.venv` so definitely open to suggestions there
There are 
1.) deleting packages
2.) updating packages
3.) resolving packages
4.) auditing packages 
5.) installing packages

Should I print the path in every place or just at installing packages?
Which will be good practice?
I am trying to print the path at every place where some changes in packaging is occuring.



Patch:
diff --git a/crates/uv/src/commands/pip/check.rs b/crates/uv/src/commands/pip/check.rs
index 9ba2b297e7764..d53e6a4691a10 100644
--- a/crates/uv/src/commands/pip/check.rs
+++ b/crates/uv/src/commands/pip/check.rs
@@ -3,14 +3,13 @@ use std::time::Instant;
 
 use anyhow::Result;
 use owo_colors::OwoColorize;
-use tracing::debug;
 
 use uv_cache::Cache;
 use uv_distribution_types::{Diagnostic, InstalledDist};
-use uv_fs::Simplified;
 use uv_installer::{SitePackages, SitePackagesDiagnostic};
 use uv_python::{EnvironmentPreference, PythonEnvironment, PythonRequest};
 
+use crate::commands::pip::operations::report_target_environment;
 use crate::commands::{elapsed, ExitStatus};
 use crate::printer::Printer;
 
@@ -30,11 +29,7 @@ pub(crate) fn pip_check(
         cache,
     )?;
 
-    debug!(
-        "Using Python {} environment at {}",
-        environment.interpreter().python_version(),
-        environment.python_executable().user_display().cyan()
-    );
+    report_target_environment(&environment, cache, printer)?;
 
     // Build the installed index.
     let site_packages = SitePackages::from_environment(&environment)?;
diff --git a/crates/uv/src/commands/pip/freeze.rs b/crates/uv/src/commands/pip/freeze.rs
index ea2dd6277ab15..32405b80906a1 100644
--- a/crates/uv/src/commands/pip/freeze.rs
+++ b/crates/uv/src/commands/pip/freeze.rs
@@ -3,14 +3,13 @@ use std::fmt::Write;
 use anyhow::Result;
 use itertools::Itertools;
 use owo_colors::OwoColorize;
-use tracing::debug;
 
 use uv_cache::Cache;
 use uv_distribution_types::{Diagnostic, InstalledDist, Name};
-use uv_fs::Simplified;
 use uv_installer::SitePackages;
 use uv_python::{EnvironmentPreference, PythonEnvironment, PythonRequest};
 
+use crate::commands::pip::operations::report_target_environment;
 use crate::commands::ExitStatus;
 use crate::printer::Printer;
 
@@ -30,11 +29,7 @@ pub(crate) fn pip_freeze(
         cache,
     )?;
 
-    debug!(
-        "Using Python {} environment at {}",
-        environment.interpreter().python_version(),
-        environment.python_executable().user_display().cyan()
-    );
+    report_target_environment(&environment, cache, printer)?;
 
     // Build the installed index.
     let site_packages = SitePackages::from_environment(&environment)?;
diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs
index b939df6d82a62..bd31e1b734b3d 100644
--- a/crates/uv/src/commands/pip/install.rs
+++ b/crates/uv/src/commands/pip/install.rs
@@ -33,6 +33,7 @@ use uv_resolver::{
 use uv_types::{BuildIsolation, HashStrategy};
 
 use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger};
+use crate::commands::pip::operations::report_target_environment;
 use crate::commands::pip::operations::Modifications;
 use crate::commands::pip::{operations, resolution_markers, resolution_tags};
 use crate::commands::{diagnostics, ExitStatus, SharedState};
@@ -147,11 +148,7 @@ pub(crate) async fn pip_install(
         &cache,
     )?;
 
-    debug!(
-        "Using Python {} environment at {}",
-        environment.interpreter().python_version(),
-        environment.python_executable().user_display().cyan()
-    );
+    report_target_environment(&environment, &cache, printer)?;
 
     // Apply any `--target` or `--prefix` directories.
     let environment = if let Some(target) = target {
diff --git a/crates/uv/src/commands/pip/list.rs b/crates/uv/src/commands/pip/list.rs
index 3fd915d9925d7..7401ed0a7268c 100644
--- a/crates/uv/src/commands/pip/list.rs
+++ b/crates/uv/src/commands/pip/list.rs
@@ -5,7 +5,6 @@ use anyhow::Result;
 use itertools::Itertools;
 use owo_colors::OwoColorize;
 use serde::Serialize;
-use tracing::debug;
 use unicode_width::UnicodeWidthStr;
 
 use uv_cache::Cache;
@@ -17,6 +16,7 @@ use uv_normalize::PackageName;
 use uv_python::PythonRequest;
 use uv_python::{EnvironmentPreference, PythonEnvironment};
 
+use crate::commands::pip::operations::report_target_environment;
 use crate::commands::ExitStatus;
 use crate::printer::Printer;
 
@@ -39,11 +39,7 @@ pub(crate) fn pip_list(
         cache,
     )?;
 
-    debug!(
-        "Using Python {} environment at {}",
-        environment.interpreter().python_version(),
-        environment.python_executable().user_display().cyan()
-    );
+    report_target_environment(&environment, cache, printer)?;
 
     // Build the installed index.
     let site_packages = SitePackages::from_environment(&environment)?;
diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs
index 8ff32ad0d51bf..406c3f9a1265e 100644
--- a/crates/uv/src/commands/pip/operations.rs
+++ b/crates/uv/src/commands/pip/operations.rs
@@ -7,6 +7,7 @@ use std::collections::{BTreeSet, HashSet};
 use std::fmt::Write;
 use std::path::PathBuf;
 use tracing::debug;
+use uv_tool::InstalledTools;
 
 use uv_cache::Cache;
 use uv_client::{BaseClientBuilder, RegistryClient};
@@ -538,6 +539,48 @@ pub(crate) async fn install(
     Ok(changelog)
 }
 
+/// Display a message about the target environment for the operation.
+pub(crate) fn report_target_environment(
+    env: &PythonEnvironment,
+    cache: &Cache,
+    printer: Printer,
+) -> Result<(), Error> {
+    let message = format!(
+        "Using Python {} environment at {}",
+        env.interpreter().python_version(),
+        env.root().user_display()
+    );
+
+    let Ok(target) = std::path::absolute(env.root()) else {
+        debug!("{}", message);
+        return Ok(());
+    };
+
+    // Do not report environments in the cache
+    if target.starts_with(cache.root()) {
+        debug!("{}", message);
+        return Ok(());
+    }
+
+    // Do not report tool environments
+    if let Ok(tools) = InstalledTools::from_settings() {
+        if target.starts_with(tools.root()) {
+            debug!("{}", message);
+            return Ok(());
+        }
+    }
+
+    // Do not report a default environment path
+    if let Ok(default) = std::path::absolute(PathBuf::from(".venv")) {
+        if target == default {
+            debug!("{}", message);
+            return Ok(());
+        }
+    }
+
+    Ok(writeln!(printer.stderr(), "{}", message.dimmed())?)
+}
+
 /// Report on the results of a dry-run installation.
 fn report_dry_run(
     resolution: &Resolution,
diff --git a/crates/uv/src/commands/pip/show.rs b/crates/uv/src/commands/pip/show.rs
index e519619a1ba77..7c1f249c19624 100644
--- a/crates/uv/src/commands/pip/show.rs
+++ b/crates/uv/src/commands/pip/show.rs
@@ -4,7 +4,6 @@ use anyhow::Result;
 use itertools::{Either, Itertools};
 use owo_colors::OwoColorize;
 use rustc_hash::FxHashMap;
-use tracing::debug;
 
 use uv_cache::Cache;
 use uv_distribution_types::{Diagnostic, Name};
@@ -13,6 +12,7 @@ use uv_installer::SitePackages;
 use uv_normalize::PackageName;
 use uv_python::{EnvironmentPreference, PythonEnvironment, PythonRequest};
 
+use crate::commands::pip::operations::report_target_environment;
 use crate::commands::ExitStatus;
 use crate::printer::Printer;
 
@@ -45,11 +45,7 @@ pub(crate) fn pip_show(
         cache,
     )?;
 
-    debug!(
-        "Using Python {} environment at {}",
-        environment.interpreter().python_version(),
-        environment.python_executable().user_display().cyan()
-    );
+    report_target_environment(&environment, cache, printer)?;
 
     // Build the installed index.
     let site_packages = SitePackages::from_environment(&environment)?;
diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs
index a5ea9c02fa3fc..3d49dbf7f053e 100644
--- a/crates/uv/src/commands/pip/sync.rs
+++ b/crates/uv/src/commands/pip/sync.rs
@@ -29,6 +29,7 @@ use uv_resolver::{
 use uv_types::{BuildIsolation, HashStrategy};
 
 use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
+use crate::commands::pip::operations::report_target_environment;
 use crate::commands::pip::operations::Modifications;
 use crate::commands::pip::{operations, resolution_markers, resolution_tags};
 use crate::commands::{diagnostics, ExitStatus, SharedState};
@@ -131,11 +132,7 @@ pub(crate) async fn pip_sync(
         &cache,
     )?;
 
-    debug!(
-        "Using Python {} environment at {}",
-        environment.interpreter().python_version(),
-        environment.python_executable().user_display().cyan()
-    );
+    report_target_environment(&environment, &cache, printer)?;
 
     // Apply any `--target` or `--prefix` directories.
     let environment = if let Some(target) = target {
diff --git a/crates/uv/src/commands/pip/tree.rs b/crates/uv/src/commands/pip/tree.rs
index 4f64860ea35e4..a1d5e8146ce03 100644
--- a/crates/uv/src/commands/pip/tree.rs
+++ b/crates/uv/src/commands/pip/tree.rs
@@ -4,19 +4,16 @@ use anyhow::Result;
 use indexmap::IndexMap;
 use owo_colors::OwoColorize;
 use rustc_hash::{FxHashMap, FxHashSet};
-use tracing::debug;
 
 use uv_cache::Cache;
 use uv_distribution::Metadata;
 use uv_distribution_types::{Diagnostic, Name};
-use uv_fs::Simplified;
 use uv_installer::SitePackages;
 use uv_normalize::PackageName;
 use uv_pypi_types::{RequirementSource, ResolverMarkerEnvironment};
-use uv_python::EnvironmentPreference;
-use uv_python::PythonEnvironment;
-use uv_python::PythonRequest;
+use uv_python::{EnvironmentPreference, PythonEnvironment, PythonRequest};
 
+use crate::commands::pip::operations::report_target_environment;
 use crate::commands::ExitStatus;
 use crate::printer::Printer;
 
@@ -42,11 +39,7 @@ pub(crate) fn pip_tree(
         cache,
     )?;
 
-    debug!(
-        "Using Python {} environment at {}",
-        environment.interpreter().python_version(),
-        environment.python_executable().user_display().cyan()
-    );
+    report_target_environment(&environment, cache, printer)?;
 
     // Read packages from the virtual environment.
     let site_packages = SitePackages::from_environment(&environment)?;
diff --git a/crates/uv/src/commands/pip/uninstall.rs b/crates/uv/src/commands/pip/uninstall.rs
index f39bcdc49920d..d831eeb4a1329 100644
--- a/crates/uv/src/commands/pip/uninstall.rs
+++ b/crates/uv/src/commands/pip/uninstall.rs
@@ -18,6 +18,7 @@ use uv_python::PythonRequest;
 use uv_python::{Prefix, PythonEnvironment, Target};
 use uv_requirements::{RequirementsSource, RequirementsSpecification};
 
+use crate::commands::pip::operations::report_target_environment;
 use crate::commands::{elapsed, ExitStatus};
 use crate::printer::Printer;
 
@@ -57,11 +58,7 @@ pub(crate) async fn pip_uninstall(
         &cache,
     )?;
 
-    debug!(
-        "Using Python {} environment at {}",
-        environment.interpreter().python_version(),
-        environment.python_executable().user_display().cyan(),
-    );
+    report_target_environment(&environment, &cache, printer)?;
 
     // Apply any `--target` or `--prefix` directories.
     let environment = if let Some(target) = target {


Test patch:
diff --git a/crates/uv/tests/export.rs b/crates/uv/tests/export.rs
index d482b33f31ad5..f132773e45656 100644
--- a/crates/uv/tests/export.rs
+++ b/crates/uv/tests/export.rs
@@ -537,6 +537,7 @@ fn relative_path() -> Result<()> {
     ----- stdout -----
 
     ----- stderr -----
+    Using Python 3.12.[X] environment at [VENV]/
     Resolved 3 packages in [TIME]
     Prepared 3 packages in [TIME]
     Installed 3 packages in [TIME]
diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs
index e8a8895125991..0dd4459af3eaa 100644
--- a/crates/uv/tests/pip_install.rs
+++ b/crates/uv/tests/pip_install.rs
@@ -1124,6 +1124,7 @@ fn install_editable_bare_cli() {
     ----- stdout -----
 
     ----- stderr -----
+    Using Python 3.12.[X] environment at [VENV]/
     Resolved 1 package in [TIME]
     Prepared 1 package in [TIME]
     Installed 1 package in [TIME]
@@ -1150,6 +1151,7 @@ fn install_editable_bare_requirements_txt() -> Result<()> {
     ----- stdout -----
 
     ----- stderr -----
+    Using Python 3.12.[X] environment at [VENV]/
     Resolved 1 package in [TIME]
     Prepared 1 package in [TIME]
     Installed 1 package in [TIME]
@@ -3265,6 +3267,7 @@ requires-python = ">=3.8"
     ----- stdout -----
 
     ----- stderr -----
+    Using Python 3.12.[X] environment at [VENV]/
     Resolved 4 packages in [TIME]
     Prepared 4 packages in [TIME]
     Installed 4 packages in [TIME]
@@ -3284,6 +3287,7 @@ requires-python = ">=3.8"
     ----- stdout -----
 
     ----- stderr -----
+    Using Python 3.12.[X] environment at [VENV]/
     Audited 1 package in [TIME]
     "###
     );
@@ -3309,6 +3313,7 @@ requires-python = ">=3.8"
     ----- stdout -----
 
     ----- stderr -----
+    Using Python 3.12.[X] environment at [VENV]/
     Resolved 4 packages in [TIME]
     Prepared 2 packages in [TIME]
     Uninstalled 2 packages in [TIME]
@@ -3356,6 +3361,7 @@ fn invalidate_path_on_cache_key() -> Result<()> {
     ----- stdout -----
 
     ----- stderr -----
+    Using Python 3.12.[X] environment at [VENV]/
     Resolved 4 packages in [TIME]
     Prepared 4 packages in [TIME]
     Installed 4 packages in [TIME]
@@ -3375,6 +3381,7 @@ fn invalidate_path_on_cache_key() -> Result<()> {
     ----- stdout -----
 
     ----- stderr -----
+    Using Python 3.12.[X] environment at [VENV]/
     Audited 1 package in [TIME]
     "###
     );
@@ -3391,6 +3398,7 @@ fn invalidate_path_on_cache_key() -> Result<()> {
     ----- stdout -----
 
     ----- stderr -----
+    Using Python 3.12.[X] environment at [VENV]/
     Resolved 4 packages in [TIME]
     Prepared 1 package in [TIME]
     Uninstalled 1 package in [TIME]
@@ -3411,6 +3419,7 @@ fn invalidate_path_on_cache_key() -> Result<()> {
     ----- stdout -----
 
     ----- stderr -----
+    Using Python 3.12.[X] environment at [VENV]/
     Resolved 4 packages in [TIME]
     Prepared 1 package in [TIME]
     Uninstalled 1 package in [TIME]
@@ -3441,6 +3450,7 @@ fn invalidate_path_on_cache_key() -> Result<()> {
     ----- stdout -----
 
     ----- stderr -----
+    Using Python 3.12.[X] environment at [VENV]/
     Audited 1 package in [TIME]
     "###
     );
@@ -3473,6 +3483,7 @@ fn invalidate_path_on_cache_key() -> Result<()> {
     ----- stdout -----
 
     ----- stderr -----
+    Using Python 3.12.[X] environment at [VENV]/
     Resolved 4 packages in [TIME]
     Prepared 1 package in [TIME]
     Uninstalled 1 package in [TIME]
@@ -3493,6 +3504,7 @@ fn invalidate_path_on_cache_key() -> Result<()> {
     ----- stdout -----
 
     ----- stderr -----
+    Using Python 3.12.[X] environment at [VENV]/
     Resolved 4 packages in [TIME]
     Prepared 1 package in [TIME]
     Uninstalled 1 package in [TIME]
@@ -3548,6 +3560,7 @@ fn invalidate_path_on_commit() -> Result<()> {
     ----- stdout -----
 
     ----- stderr -----
+    Using Python 3.12.[X] environment at [VENV]/
     Resolved 4 packages in [TIME]
     Prepared 4 packages in [TIME]
     Installed 4 packages in [TIME]
@@ -3567,6 +3580,7 @@ fn invalidate_path_on_commit() -> Result<()> {
     ----- stdout -----
 
     ----- stderr -----
+    Using Python 3.12.[X] environment at [VENV]/
     Audited 1 package in [TIME]
     "###
     );
@@ -3589,6 +3603,7 @@ fn invalidate_path_on_commit() -> Result<()> {
     ----- stdout -----
 
     ----- stderr -----
+    Using Python 3.12.[X] environment at [VENV]/
     Resolved 4 packages in [TIME]
     Prepared 1 package in [TIME]
     Uninstalled 1 package in [TIME]
@@ -4688,6 +4703,7 @@ fn deptry_gitignore() {
     ----- stdout -----
 
     ----- stderr -----
+    Using Python 3.12.[X] environment at [VENV]/
     Resolved 3 packages in [TIME]
     Prepared 3 packages in [TIME]
     Installed 3 packages in [TIME]