Skip to content
3 changes: 2 additions & 1 deletion docs/guide/pyproject.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,14 +204,15 @@ hello-world = { call = "builtins:print('Hello World!')" }
## `tool.rye.workspace`

When a table with that key is stored, then a project is declared to be a
[workspace](../workspaces/) root. By default all Python projects discovered in
[workspace](../workspaces/) root. By default, all Python projects discovered in
sub folders will then become members of this workspace and share a virtualenv.
Optionally the `members` key (an array) can be used to restrict these members.
In that list globs can be used. The root project itself is always a member.

```toml
[tool.rye.workspace]
members = ["mylib-*"]
per_member_lock = false
```

For more information consult the [Workspaces Guide](../workspaces/).
16 changes: 16 additions & 0 deletions docs/guide/workspaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,19 @@ of the `myname-bar` package you would need to do this:
```
rye sync --features=myname-bar/foo
```

## Per member lockfile

+++ 0.36.0

Rye will always merge the requirements of all members in a workspace and generate a top level
lockfile to keep the virtual environment consistent and up to date. In cases where you need to
still keep a different lockfile per member to split installation (for example, in a docker context,
where members live in different containers), you can enable `per_member_lock`, which will generate
a lockfile in the root of each member.

```toml
[tool.rye.workspace]
members = ["myname-*"]
per_member_lock = true
```
22 changes: 16 additions & 6 deletions rye/src/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,19 @@ pub fn update_workspace_lockfile(
DependencyKind::Dev,
)?;
}

if workspace.per_member_lock && !pyproject.is_workspace_root() {
update_single_project_lockfile(
py_ver,
pyproject,
lock_mode,
&pyproject.root_path().join(lockfile.file_name().unwrap()),
output,
sources,
&lock_options,
keyring_provider,
)?;
}
}

req_file.flush()?;
Expand Down Expand Up @@ -347,12 +360,9 @@ pub fn update_single_project_lockfile(
if !pyproject.is_virtual() {
let features_by_project = collect_workspace_features(&lock_options);
let applicable_extras = format_project_extras(features_by_project.as_ref(), pyproject)?;
writeln!(
req_file,
"-e {}{}",
make_relative_url(&pyproject.root_path(), &pyproject.workspace_path())?,
applicable_extras
)?;
// We can always write `file:.` here as this will only ever be called when updating
// a lockfile of a project de-attached from the workspace
writeln!(req_file, "-e file:.{}", applicable_extras)?;
}

for dep in pyproject.iter_dependencies(DependencyKind::Normal) {
Expand Down
10 changes: 10 additions & 0 deletions rye/src/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ pub struct Workspace {
root: PathBuf,
doc: DocumentMut,
members: Option<Vec<String>>,
pub per_member_lock: bool,
}

impl Workspace {
Expand All @@ -393,6 +394,10 @@ impl Workspace {
.filter_map(|item| item.as_str().map(|x| x.to_string()))
.collect::<Vec<_>>()
}),
per_member_lock: workspace
.get("per_member_lock")
.and_then(|x| x.as_bool())
.unwrap_or(false),
})
}

Expand Down Expand Up @@ -469,6 +474,11 @@ impl Workspace {
self: &'a Arc<Self>,
) -> impl Iterator<Item = Result<PyProject, Error>> + 'a {
walkdir::WalkDir::new(&self.root)
.sort_by(
// Perform proper sorting to avoid platform dependency to ensure
// output reproducibility. This is important for tests
|x, y| x.file_name().cmp(y.file_name()),
)
.into_iter()
.filter_entry(|entry| {
!(entry.file_type().is_dir() && skip_recurse_into(entry.file_name()))
Expand Down
54 changes: 54 additions & 0 deletions rye/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,60 @@ impl Space {
assert!(status.success());
}

#[allow(unused)]
pub fn init_virtual(&self, name: &str) {
let status = self
.cmd(get_bin())
.arg("init")
.arg("--name")
.arg(name)
.arg("--virtual")
.arg("-q")
.current_dir(self.project_path())
.status()
.unwrap();
assert!(status.success());
}

#[allow(unused)]
pub fn init_workspace_member(&self, name: &str) {
// First we need to create the directory where it will be placed
let p = self.project_path().join(name);
fs::create_dir(p.clone()).ok();

// Create the workspace member
let status = self
.cmd(get_bin())
.arg("init")
.arg("--name")
.arg(name)
.arg("-q")
.current_dir(p)
.status()
.unwrap();
assert!(status.success());
}

#[allow(unused)]
pub fn init_virtual_workspace_member(&self, name: &str) {
// First we need to create the directory where it will be placed
let p = self.project_path().join(name);
fs::create_dir(p.clone()).ok();

// Create the workspace member
let status = self
.cmd(get_bin())
.arg("init")
.arg("--name")
.arg(name)
.arg("-q")
.arg("--virtual")
.current_dir(p)
.status()
.unwrap();
assert!(status.success());
}

pub fn rye_home(&self) -> &Path {
&self.rye_home
}
Expand Down
Loading