Skip to content

Commit 81e5830

Browse files
authored
Workspace discovery (#14308)
1 parent 2b58705 commit 81e5830

38 files changed

+1968
-177
lines changed

.editorconfig

+4-1
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,7 @@ indent_size = 4
1717
trim_trailing_whitespace = false
1818

1919
[*.md]
20-
max_line_length = 100
20+
max_line_length = 100
21+
22+
[*.toml]
23+
indent_size = 4

Cargo.lock

+33-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ criterion = { version = "0.5.1", default-features = false }
6666
crossbeam = { version = "0.8.4" }
6767
dashmap = { version = "6.0.1" }
6868
dir-test = { version = "0.3.0" }
69+
dunce = { version = "1.0.5" }
6970
drop_bomb = { version = "0.1.5" }
7071
env_logger = { version = "0.11.0" }
7172
etcetera = { version = "0.8.0" }
@@ -81,7 +82,7 @@ hashbrown = { version = "0.15.0", default-features = false, features = [
8182
ignore = { version = "0.4.22" }
8283
imara-diff = { version = "0.1.5" }
8384
imperative = { version = "1.0.4" }
84-
indexmap = {version = "2.6.0" }
85+
indexmap = { version = "2.6.0" }
8586
indicatif = { version = "0.17.8" }
8687
indoc = { version = "2.0.4" }
8788
insta = { version = "1.35.1" }

_typos.toml

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
[files]
22
# https://github.com/crate-ci/typos/issues/868
3-
extend-exclude = ["crates/red_knot_vendored/vendor/**/*", "**/resources/**/*", "**/snapshots/**/*"]
3+
extend-exclude = [
4+
"crates/red_knot_vendored/vendor/**/*",
5+
"**/resources/**/*",
6+
"**/snapshots/**/*",
7+
"crates/red_knot_workspace/src/workspace/pyproject/package_name.rs"
8+
]
49

510
[default.extend-words]
611
"arange" = "arange" # e.g. `numpy.arange`

crates/red_knot/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ tracing-tree = { workspace = true }
3434
[dev-dependencies]
3535
filetime = { workspace = true }
3636
tempfile = { workspace = true }
37+
ruff_db = { workspace = true, features = ["testing"] }
3738

3839
[lints]
3940
workspace = true

crates/red_knot/src/main.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,10 @@ fn run() -> anyhow::Result<ExitStatus> {
183183

184184
let system = OsSystem::new(cwd.clone());
185185
let cli_configuration = args.to_configuration(&cwd);
186-
let workspace_metadata = WorkspaceMetadata::from_path(
186+
let workspace_metadata = WorkspaceMetadata::discover(
187187
system.current_directory(),
188188
&system,
189-
Some(cli_configuration.clone()),
189+
Some(&cli_configuration),
190190
)?;
191191

192192
// TODO: Use the `program_settings` to compute the key for the database's persistent

crates/red_knot/tests/file_watching.rs

+143-7
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ use std::time::Duration;
66
use anyhow::{anyhow, Context};
77

88
use red_knot_python_semantic::{resolve_module, ModuleName, Program, PythonVersion, SitePackages};
9-
use red_knot_workspace::db::RootDatabase;
9+
use red_knot_workspace::db::{Db, RootDatabase};
1010
use red_knot_workspace::watch;
1111
use red_knot_workspace::watch::{directory_watcher, WorkspaceWatcher};
1212
use red_knot_workspace::workspace::settings::{Configuration, SearchPathConfiguration};
1313
use red_knot_workspace::workspace::WorkspaceMetadata;
1414
use ruff_db::files::{system_path_to_file, File, FileError};
1515
use ruff_db::source::source_text;
1616
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
17+
use ruff_db::testing::setup_logging;
1718
use ruff_db::Upcast;
1819

1920
struct TestCase {
@@ -69,7 +70,6 @@ impl TestCase {
6970
Some(all_events)
7071
}
7172

72-
#[cfg(unix)]
7373
fn take_watch_changes(&self) -> Vec<watch::ChangeEvent> {
7474
self.try_take_watch_changes(Duration::from_secs(10))
7575
.expect("Expected watch changes but observed none")
@@ -110,8 +110,8 @@ impl TestCase {
110110
) -> anyhow::Result<()> {
111111
let program = Program::get(self.db());
112112

113-
self.configuration.search_paths = configuration.clone();
114-
let new_settings = configuration.into_settings(self.db.workspace().root(&self.db));
113+
let new_settings = configuration.to_settings(self.db.workspace().root(&self.db));
114+
self.configuration.search_paths = configuration;
115115

116116
program.update_search_paths(&mut self.db, &new_settings)?;
117117

@@ -204,7 +204,9 @@ where
204204
.as_utf8_path()
205205
.canonicalize_utf8()
206206
.with_context(|| "Failed to canonicalize root path.")?,
207-
);
207+
)
208+
.simplified()
209+
.to_path_buf();
208210

209211
let workspace_path = root_path.join("workspace");
210212

@@ -241,8 +243,7 @@ where
241243
search_paths,
242244
};
243245

244-
let workspace =
245-
WorkspaceMetadata::from_path(&workspace_path, &system, Some(configuration.clone()))?;
246+
let workspace = WorkspaceMetadata::discover(&workspace_path, &system, Some(&configuration))?;
246247

247248
let db = RootDatabase::new(workspace, system)?;
248249

@@ -1311,3 +1312,138 @@ mod unix {
13111312
Ok(())
13121313
}
13131314
}
1315+
1316+
#[test]
1317+
fn nested_packages_delete_root() -> anyhow::Result<()> {
1318+
let mut case = setup(|root: &SystemPath, workspace_root: &SystemPath| {
1319+
std::fs::write(
1320+
workspace_root.join("pyproject.toml").as_std_path(),
1321+
r#"
1322+
[project]
1323+
name = "inner"
1324+
"#,
1325+
)?;
1326+
1327+
std::fs::write(
1328+
root.join("pyproject.toml").as_std_path(),
1329+
r#"
1330+
[project]
1331+
name = "outer"
1332+
"#,
1333+
)?;
1334+
1335+
Ok(())
1336+
})?;
1337+
1338+
assert_eq!(
1339+
case.db().workspace().root(case.db()),
1340+
&*case.workspace_path("")
1341+
);
1342+
1343+
std::fs::remove_file(case.workspace_path("pyproject.toml").as_std_path())?;
1344+
1345+
let changes = case.stop_watch();
1346+
1347+
case.apply_changes(changes);
1348+
1349+
// It should now pick up the outer workspace.
1350+
assert_eq!(case.db().workspace().root(case.db()), case.root_path());
1351+
1352+
Ok(())
1353+
}
1354+
1355+
#[test]
1356+
fn added_package() -> anyhow::Result<()> {
1357+
let _ = setup_logging();
1358+
let mut case = setup([
1359+
(
1360+
"pyproject.toml",
1361+
r#"
1362+
[project]
1363+
name = "inner"
1364+
1365+
[tool.knot.workspace]
1366+
members = ["packages/*"]
1367+
"#,
1368+
),
1369+
(
1370+
"packages/a/pyproject.toml",
1371+
r#"
1372+
[project]
1373+
name = "a"
1374+
"#,
1375+
),
1376+
])?;
1377+
1378+
assert_eq!(case.db().workspace().packages(case.db()).len(), 2);
1379+
1380+
std::fs::create_dir(case.workspace_path("packages/b").as_std_path())
1381+
.context("failed to create folder for package 'b'")?;
1382+
1383+
// It seems that the file watcher won't pick up on file changes shortly after the folder
1384+
// was created... I suspect this is because most file watchers don't support recursive
1385+
// file watching. Instead, file-watching libraries manually implement recursive file watching
1386+
// by setting a watcher for each directory. But doing this obviously "lags" behind.
1387+
case.take_watch_changes();
1388+
1389+
std::fs::write(
1390+
case.workspace_path("packages/b/pyproject.toml")
1391+
.as_std_path(),
1392+
r#"
1393+
[project]
1394+
name = "b"
1395+
"#,
1396+
)
1397+
.context("failed to write pyproject.toml for package b")?;
1398+
1399+
let changes = case.stop_watch();
1400+
1401+
case.apply_changes(changes);
1402+
1403+
assert_eq!(case.db().workspace().packages(case.db()).len(), 3);
1404+
1405+
Ok(())
1406+
}
1407+
1408+
#[test]
1409+
fn removed_package() -> anyhow::Result<()> {
1410+
let mut case = setup([
1411+
(
1412+
"pyproject.toml",
1413+
r#"
1414+
[project]
1415+
name = "inner"
1416+
1417+
[tool.knot.workspace]
1418+
members = ["packages/*"]
1419+
"#,
1420+
),
1421+
(
1422+
"packages/a/pyproject.toml",
1423+
r#"
1424+
[project]
1425+
name = "a"
1426+
"#,
1427+
),
1428+
(
1429+
"packages/b/pyproject.toml",
1430+
r#"
1431+
[project]
1432+
name = "b"
1433+
"#,
1434+
),
1435+
])?;
1436+
1437+
assert_eq!(case.db().workspace().packages(case.db()).len(), 3);
1438+
1439+
std::fs::remove_dir_all(case.workspace_path("packages/b").as_std_path())
1440+
.context("failed to remove package 'b'")?;
1441+
1442+
let changes = case.stop_watch();
1443+
1444+
case.apply_changes(changes);
1445+
1446+
assert_eq!(case.db().workspace().packages(case.db()).len(), 2);
1447+
1448+
Ok(())
1449+
}

crates/red_knot_python_semantic/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ thiserror = { workspace = true }
3333
tracing = { workspace = true }
3434
rustc-hash = { workspace = true }
3535
hashbrown = { workspace = true }
36+
serde = { workspace = true, optional = true }
3637
smallvec = { workspace = true }
3738
static_assertions = { workspace = true }
3839
test-case = { workspace = true }

crates/red_knot_python_semantic/src/program.rs

+3
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,15 @@ impl Program {
5454
}
5555

5656
#[derive(Clone, Debug, Eq, PartialEq)]
57+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
5758
pub struct ProgramSettings {
5859
pub target_version: PythonVersion,
5960
pub search_paths: SearchPathSettings,
6061
}
6162

6263
/// Configures the search paths for module resolution.
6364
#[derive(Eq, PartialEq, Debug, Clone)]
65+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
6466
pub struct SearchPathSettings {
6567
/// List of user-provided paths that should take first priority in the module resolution.
6668
/// Examples in other type checkers are mypy's MYPYPATH environment variable,
@@ -91,6 +93,7 @@ impl SearchPathSettings {
9193
}
9294

9395
#[derive(Debug, Clone, Eq, PartialEq)]
96+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
9497
pub enum SitePackages {
9598
Derived {
9699
venv_path: SystemPathBuf,

crates/red_knot_python_semantic/src/python_version.rs

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::fmt;
55
/// Unlike the `TargetVersion` enums in the CLI crates,
66
/// this does not necessarily represent a Python version that we actually support.
77
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
8+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
89
pub struct PythonVersion {
910
pub major: u8,
1011
pub minor: u8,

0 commit comments

Comments
 (0)