Skip to content

Commit 163d526

Browse files
authored
Allow passing a virtual environment to ruff analyze graph (#17743)
Summary -- Fixes #16598 by adding the `--python` flag to `ruff analyze graph`, which adds a `PythonPath` to the `SearchPathSettings` for module resolution. For the [albatross-virtual-workspace] example from the uv repo, this updates the output from the initial issue: ```shell > ruff analyze graph packages/albatross { "packages/albatross/check_installed_albatross.py": [ "packages/albatross/src/albatross/__init__.py" ], "packages/albatross/src/albatross/__init__.py": [] } ``` To include both the the workspace `bird_feeder` import _and_ the third-party `tqdm` import in the output: ```shell > myruff analyze graph packages/albatross --python .venv { "packages/albatross/check_installed_albatross.py": [ "packages/albatross/src/albatross/__init__.py" ], "packages/albatross/src/albatross/__init__.py": [ ".venv/lib/python3.12/site-packages/tqdm/__init__.py", "packages/bird-feeder/src/bird_feeder/__init__.py" ] } ``` Note the hash in the uv link! I was temporarily very confused why my local tests were showing an `iniconfig` import instead of `tqdm` until I realized that the example has been updated on the uv main branch, which I had locally. Test Plan -- A new integration test with a stripped down venv based on the `albatross` example. [albatross-virtual-workspace]: https://github.com/astral-sh/uv/tree/aa629c4a54c31d6132ab1655b90dd7542c17d120/scripts/workspaces/albatross-virtual-workspace
1 parent 75effb8 commit 163d526

File tree

4 files changed

+164
-2
lines changed

4 files changed

+164
-2
lines changed

crates/ruff/src/args.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,9 @@ pub struct AnalyzeGraphCommand {
177177
/// The minimum Python version that should be supported.
178178
#[arg(long, value_enum)]
179179
target_version: Option<PythonVersion>,
180+
/// Path to a virtual environment to use for resolving additional dependencies
181+
#[arg(long)]
182+
python: Option<PathBuf>,
180183
}
181184

182185
// The `Parser` derive is for ruff_dev, for ruff `Args` would be sufficient
@@ -796,6 +799,7 @@ impl AnalyzeGraphCommand {
796799
let format_arguments = AnalyzeGraphArgs {
797800
files: self.files,
798801
direction: self.direction,
802+
python: self.python,
799803
};
800804

801805
let cli_overrides = ExplicitConfigOverrides {
@@ -1261,6 +1265,7 @@ impl LineColumnParseError {
12611265
pub struct AnalyzeGraphArgs {
12621266
pub files: Vec<PathBuf>,
12631267
pub direction: Direction,
1268+
pub python: Option<PathBuf>,
12641269
}
12651270

12661271
/// Configuration overrides provided via dedicated CLI flags:

crates/ruff/src/commands/analyze_graph.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ pub(crate) fn analyze_graph(
7575
.target_version
7676
.as_tuple()
7777
.into(),
78+
args.python
79+
.and_then(|python| SystemPathBuf::from_path_buf(python).ok()),
7880
)?;
7981

8082
let imports = {

crates/ruff/tests/analyze_graph.rs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,3 +422,153 @@ fn nested_imports() -> Result<()> {
422422

423423
Ok(())
424424
}
425+
426+
/// Test for venv resolution with the `--python` flag.
427+
///
428+
/// Based on the [albatross-virtual-workspace] example from the uv repo and the report in [#16598].
429+
///
430+
/// [albatross-virtual-workspace]: https://github.com/astral-sh/uv/tree/aa629c4a/scripts/workspaces/albatross-virtual-workspace
431+
/// [#16598]: https://github.com/astral-sh/ruff/issues/16598
432+
#[test]
433+
fn venv() -> Result<()> {
434+
let tempdir = TempDir::new()?;
435+
let root = ChildPath::new(tempdir.path());
436+
437+
// packages
438+
// ├── albatross
439+
// │ ├── check_installed_albatross.py
440+
// │ ├── pyproject.toml
441+
// │ └── src
442+
// │ └── albatross
443+
// │ └── __init__.py
444+
// └── bird-feeder
445+
// ├── check_installed_bird_feeder.py
446+
// ├── pyproject.toml
447+
// └── src
448+
// └── bird_feeder
449+
// └── __init__.py
450+
451+
let packages = root.child("packages");
452+
453+
let albatross = packages.child("albatross");
454+
albatross
455+
.child("check_installed_albatross.py")
456+
.write_str("from albatross import fly")?;
457+
albatross
458+
.child("pyproject.toml")
459+
.write_str(indoc::indoc! {r#"
460+
[project]
461+
name = "albatross"
462+
version = "0.1.0"
463+
requires-python = ">=3.12"
464+
dependencies = ["bird-feeder", "tqdm>=4,<5"]
465+
466+
[tool.uv.sources]
467+
bird-feeder = { workspace = true }
468+
"#})?;
469+
albatross
470+
.child("src")
471+
.child("albatross")
472+
.child("__init__.py")
473+
.write_str("import tqdm; from bird_feeder import use")?;
474+
475+
let bird_feeder = packages.child("bird-feeder");
476+
bird_feeder
477+
.child("check_installed_bird_feeder.py")
478+
.write_str("from bird_feeder import use; from albatross import fly")?;
479+
bird_feeder
480+
.child("pyproject.toml")
481+
.write_str(indoc::indoc! {r#"
482+
[project]
483+
name = "bird-feeder"
484+
version = "1.0.0"
485+
requires-python = ">=3.12"
486+
dependencies = ["anyio>=4.3.0,<5"]
487+
"#})?;
488+
bird_feeder
489+
.child("src")
490+
.child("bird_feeder")
491+
.child("__init__.py")
492+
.write_str("import anyio")?;
493+
494+
let venv = root.child(".venv");
495+
let bin = venv.child("bin");
496+
bin.child("python").touch()?;
497+
let home = format!("home = {}", bin.to_string_lossy());
498+
venv.child("pyvenv.cfg").write_str(&home)?;
499+
let site_packages = venv.child("lib").child("python3.12").child("site-packages");
500+
site_packages
501+
.child("_albatross.pth")
502+
.write_str(&albatross.join("src").to_string_lossy())?;
503+
site_packages
504+
.child("_bird_feeder.pth")
505+
.write_str(&bird_feeder.join("src").to_string_lossy())?;
506+
site_packages.child("tqdm").child("__init__.py").touch()?;
507+
508+
// without `--python .venv`, the result should only include dependencies within the albatross
509+
// package
510+
insta::with_settings!({
511+
filters => INSTA_FILTERS.to_vec(),
512+
}, {
513+
assert_cmd_snapshot!(
514+
command().arg("packages/albatross").current_dir(&root),
515+
@r#"
516+
success: true
517+
exit_code: 0
518+
----- stdout -----
519+
{
520+
"packages/albatross/check_installed_albatross.py": [
521+
"packages/albatross/src/albatross/__init__.py"
522+
],
523+
"packages/albatross/src/albatross/__init__.py": []
524+
}
525+
526+
----- stderr -----
527+
"#);
528+
});
529+
530+
// with `--python .venv` both workspace and third-party dependencies are included
531+
insta::with_settings!({
532+
filters => INSTA_FILTERS.to_vec(),
533+
}, {
534+
assert_cmd_snapshot!(
535+
command().args(["--python", ".venv"]).arg("packages/albatross").current_dir(&root),
536+
@r#"
537+
success: true
538+
exit_code: 0
539+
----- stdout -----
540+
{
541+
"packages/albatross/check_installed_albatross.py": [
542+
"packages/albatross/src/albatross/__init__.py"
543+
],
544+
"packages/albatross/src/albatross/__init__.py": [
545+
".venv/lib/python3.12/site-packages/tqdm/__init__.py",
546+
"packages/bird-feeder/src/bird_feeder/__init__.py"
547+
]
548+
}
549+
550+
----- stderr -----
551+
"#);
552+
});
553+
554+
// test the error message for a non-existent venv. it's important that the `ruff analyze graph`
555+
// flag matches the red-knot flag used to generate the error message (`--python`)
556+
insta::with_settings!({
557+
filters => INSTA_FILTERS.to_vec(),
558+
}, {
559+
assert_cmd_snapshot!(
560+
command().args(["--python", "none"]).arg("packages/albatross").current_dir(&root),
561+
@r"
562+
success: false
563+
exit_code: 2
564+
----- stdout -----
565+
566+
----- stderr -----
567+
ruff failed
568+
Cause: Invalid search path settings
569+
Cause: Failed to discover the site-packages directory: Invalid `--python` argument: `none` could not be canonicalized
570+
");
571+
});
572+
573+
Ok(())
574+
}

crates/ruff_graph/src/db.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ use zip::CompressionMethod;
44

55
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
66
use red_knot_python_semantic::{
7-
default_lint_registry, Db, Program, ProgramSettings, PythonPlatform, SearchPathSettings,
7+
default_lint_registry, Db, Program, ProgramSettings, PythonPath, PythonPlatform,
8+
SearchPathSettings,
89
};
910
use ruff_db::files::{File, Files};
1011
use ruff_db::system::{OsSystem, System, SystemPathBuf};
@@ -32,8 +33,12 @@ impl ModuleDb {
3233
pub fn from_src_roots(
3334
src_roots: Vec<SystemPathBuf>,
3435
python_version: PythonVersion,
36+
venv_path: Option<SystemPathBuf>,
3537
) -> Result<Self> {
36-
let search_paths = SearchPathSettings::new(src_roots);
38+
let mut search_paths = SearchPathSettings::new(src_roots);
39+
if let Some(venv_path) = venv_path {
40+
search_paths.python_path = PythonPath::from_cli_flag(venv_path);
41+
}
3742

3843
let db = Self::default();
3944
Program::from_settings(

0 commit comments

Comments
 (0)