Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tests and docs for rye run #982

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/guide/commands/run.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# `run`

Runs a command installed into this package. This either runs a script or application
made available in the virtualenv or a Rye specific script.
made available in the virtualenv or a Rye specific script. If no command is provided, it will list all available commands.

If there is a script in the virtualenv having the same name as a Rye script, the virtualenv script will take precedence over the Rye script.

For more information see [`rye.tool.scripts`](../pyproject.md#toolryescripts).

Expand Down
20 changes: 18 additions & 2 deletions docs/guide/pyproject.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,28 @@ devserver-alt = ["flask", "run", "--app", "./hello.py", "--debug"]
devserver-explicit = { cmd = "flask run --app ./hello.py --debug" }
```

In addition to the shell's pre-existing PATH, Rye run adds `.venv/bin` to the PATH provided to scripts. Any scripts provided by locally-installed dependencies can be used without the `.venv/bin` prefix. For example, if there is a
`dev-dependencies` on pytest in your package, you should write:

```toml
[tool.rye.scripts]
test = "pytest --lf"
```

instead of

```toml
[tool.rye.scripts]
test = ".venv/bin/pytest --lf"
```

Scripts are not run in a shell, so shell specific interpolation is unavailable.

The following keys are possible for a script:

### `cmd`

The command to execute. This is either a `string` or an `array` of arguments. In either case
shell specific interpolation is unavailable. The command will invoke one of the tools in the
The command to execute. This is either a `string` or an `array` of arguments. The command will invoke one of the tools in the
virtualenv if it's available there.

```toml
Expand Down
18 changes: 15 additions & 3 deletions rye/src/cli/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use console::style;
use crate::pyproject::{PyProject, Script};
use crate::sync::{sync, SyncOptions};
use crate::tui::redirect_to_stderr;
use crate::utils::{exec_spawn, get_venv_python_bin, success_status, IoPathContext};
use crate::utils::{exec_spawn, get_venv_python_bin, success_status, CommandOutput, IoPathContext};

/// Runs a command installed into this package.
#[derive(Parser, Debug)]
Expand All @@ -26,6 +26,12 @@ pub struct Args {
/// Use this pyproject.toml file
#[arg(long, value_name = "PYPROJECT_TOML")]
pyproject: Option<PathBuf>,
/// Enables verbose diagnostics.
#[arg(short, long)]
verbose: bool,
/// Turns off all output.
#[arg(short, long, conflicts_with = "verbose")]
quiet: bool,
}

#[derive(Parser, Debug)]
Expand All @@ -36,13 +42,19 @@ enum Cmd {

pub fn execute(cmd: Args) -> Result<(), Error> {
let _guard = redirect_to_stderr(true);
let output = CommandOutput::from_quiet_and_verbose(cmd.quiet, cmd.verbose);
let pyproject = PyProject::load_or_discover(cmd.pyproject.as_deref())?;

// make sure we have the minimal virtualenv.
sync(SyncOptions::python_only().pyproject(cmd.pyproject))
.context("failed to sync ahead of run")?;
sync(
SyncOptions::python_only()
.pyproject(cmd.pyproject)
.output(output),
)
.context("failed to sync ahead of run")?;

if cmd.list || cmd.cmd.is_none() {
drop(_guard);
return list_scripts(&pyproject);
}
let args = match cmd.cmd {
Expand Down
5 changes: 5 additions & 0 deletions rye/src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ impl SyncOptions {
self.pyproject = pyproject;
self
}

pub fn output(mut self, output: CommandOutput) -> Self {
self.output = output;
self
}
}

/// Config written into the virtualenv for sync purposes.
Expand Down
1 change: 1 addition & 0 deletions rye/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub const INSTA_FILTERS: &[(&str, &str)] = &[
(r" in (\d+\.)?\d+(ms|s)\b", " in [EXECUTION_TIME]"),
(r"\\\\?([\w\d.])", "/$1"),
(r"rye.exe", "rye"),
(r"exit (code|status)", "exit code"),
];

fn marked_tempdir() -> TempDir {
Expand Down
285 changes: 285 additions & 0 deletions rye/tests/test_run.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
use crate::common::{rye_cmd_snapshot, Space};
use std::fs;
use toml_edit::{table, value, Array};

mod common;

#[test]
fn test_run_list() {
let space = Space::new();
space.init("my-project");

let status = space
.rye_cmd()
.arg("add")
.arg("Flask==3.0.0")
.arg("--sync")
.status()
.unwrap();
assert!(status.success());

space.edit_toml("pyproject.toml", |doc| {
let mut scripts = table();
scripts["hello"] = value("echo hello");
doc["tool"]["rye"]["scripts"] = scripts;
});

#[cfg(not(windows))]
rye_cmd_snapshot!(space.rye_cmd().arg("run").arg("--list"), @r###"
success: true
exit_code: 0
----- stdout -----
flask
hello (echo hello)
python
python3
python3.12

----- stderr -----
"###);
#[cfg(windows)]
rye_cmd_snapshot!(space.rye_cmd().arg("run").arg("--list"), @r###"
success: true
exit_code: 0
----- stdout -----
flask
hello (echo hello)
pydoc
python
pythonw

----- stderr -----
"###);
}

#[test]
fn test_basic_run() {
let space = Space::new();
space.init("my-project");

// Run a virtualenv script
let status = space
.rye_cmd()
.arg("add")
.arg("Flask==3.0.0")
.arg("--sync")
.status()
.unwrap();
assert!(status.success());
rye_cmd_snapshot!(space.rye_cmd().arg("run").arg("-q").arg("flask").arg("--version"), @r###"
success: true
exit_code: 0
----- stdout -----
Python 3.12.2
Flask 3.0.0
Werkzeug 3.0.1

----- stderr -----
"###);

// Run a non-existing script
rye_cmd_snapshot!(space.rye_cmd().arg("run").arg("not_exist_script"), @r###"
success: false
exit_code: 1
----- stdout -----

----- stderr -----
error: invalid or unknown script 'not_exist_script'
"###);

let init_script = space
.project_path()
.join("src")
.join("my_project")
.join("__init__.py");
fs::write(
&init_script,
"def hello():\n print('Hello from my-project!')\n return 0",
)
.unwrap();

let env_file = space.project_path().join("env_file");
fs::write(&env_file, r#"HELLO="Hello from env_file!""#).unwrap();

// Run Rye scripts
space.edit_toml("pyproject.toml", |doc| {
let mut scripts = table();
// A simple string command
scripts["script_1"] = value(r#"python -c 'print("Hello from script_1!")'"#);
// A simple array command
let mut script_2_args = Array::new();
script_2_args.extend(["python", "-c", "print('Hello from script_2!')"]);
scripts["script_2"] = value(script_2_args);
// A simple command using `cmd` key
scripts["script_3"]["cmd"] = value(r#"python -c 'print("Hello from script_3!")'"#);
// A `call` script
scripts["script_4"]["call"] = value("my_project:hello");
// A failing script
scripts["script_5"]["cmd"] = value(r#"python -c 'import sys; sys.exit(1)'"#);
// A script with environment variables
scripts["script_6"]["cmd"] = value(r#"python -c 'import os; print(os.getenv("HELLO"))'"#);
scripts["script_6"]["env"]["HELLO"] = value("Hello from script_6!");
// A script with an env-file
scripts["script_7"]["cmd"] = value(r#"python -c 'import os; print(os.getenv("HELLO"))'"#);
scripts["script_7"]["env-file"] = value(env_file.to_string_lossy().into_owned());

doc["tool"]["rye"]["scripts"] = scripts;
});

rye_cmd_snapshot!(space.rye_cmd().arg("run").arg("script_1"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello from script_1!

----- stderr -----
"###);
rye_cmd_snapshot!(space.rye_cmd().arg("run").arg("script_2"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello from script_2!

----- stderr -----
"###);
rye_cmd_snapshot!(space.rye_cmd().arg("run").arg("script_3"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello from script_3!

----- stderr -----
"###);
rye_cmd_snapshot!(space.rye_cmd().arg("run").arg("script_4"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello from my-project!

----- stderr -----
"###);
rye_cmd_snapshot!(space.rye_cmd().arg("run").arg("script_5"), @r###"
success: false
exit_code: 1
----- stdout -----

----- stderr -----
"###);
rye_cmd_snapshot!(space.rye_cmd().arg("run").arg("script_6"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello from script_6!

----- stderr -----
"###);
rye_cmd_snapshot!(space.rye_cmd().arg("run").arg("script_7"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello from env_file!

----- stderr -----
"###);
}

#[test]
fn test_script_chain() {
let space = Space::new();
space.init("my-project");

space.edit_toml("pyproject.toml", |doc| {
let mut scripts = table();
// A simple string command
scripts["script_1"] = value(r#"python -c 'print("Hello from script_1!")'"#);
// A simple command using `cmd` key
scripts["script_2"]["cmd"] = value(r#"python -c 'print("Hello from script_2!")'"#);
// A failing script
scripts["script_3"]["cmd"] = value(r#"python -c 'import sys; sys.exit(1)'"#);
// A `chain` script
scripts["script_4"]["chain"] = value(Array::from_iter([
"script_1",
"script_2",
r#"python -c 'print("hello")'"#,
]));
// A failing `chain` script
scripts["script_5"]["chain"] = value(Array::from_iter([
"script_1", "script_2", "script_3", "script_1",
]));
// A nested `chain` script
scripts["script_6"]["chain"] =
value(Array::from_iter(["script_1", "script_4", "script_5"]));
// NEED FIX: A recursive `chain` script
scripts["script_7"]["chain"] = value(Array::from_iter(["script_7"]));

doc["tool"]["rye"]["scripts"] = scripts;
});

rye_cmd_snapshot!(space.rye_cmd().arg("run").arg("-q").arg("script_4"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello from script_1!
Hello from script_2!
hello

----- stderr -----
"###);
rye_cmd_snapshot!(space.rye_cmd().arg("run").arg("script_5"), @r###"
success: false
exit_code: 1
----- stdout -----
Hello from script_1!
Hello from script_2!

----- stderr -----
error: script failed with exit code: 1
"###);
rye_cmd_snapshot!(space.rye_cmd().arg("run").arg("script_6"), @r###"
success: false
exit_code: 1
----- stdout -----
Hello from script_1!
Hello from script_1!
Hello from script_2!
hello
Hello from script_1!
Hello from script_2!

----- stderr -----
error: script failed with exit code: 1
"###);
// rye_cmd_snapshot!(space.rye_cmd().arg("run").arg("script_7"), @r###""###);
}

#[test]
fn test_run_name_collision() {
let space = Space::new();
space.init("my-project");

let status = space
.rye_cmd()
.arg("add")
.arg("Flask==3.0.0")
.arg("--sync")
.status()
.unwrap();
assert!(status.success());

// Add a script with the same name as a virtualenv script, it should be shadowed by the virtualenv script.
space.edit_toml("pyproject.toml", |doc| {
doc["tool"]["rye"]["scripts"] = table();
doc["tool"]["rye"]["scripts"]["flask"] =
value(r#"python -c 'print("flask from rye script")'"#);
});
rye_cmd_snapshot!(space.rye_cmd().arg("run").arg("flask").arg("--version"), @r###"
success: true
exit_code: 0
----- stdout -----
Python 3.12.2
Flask 3.0.0
Werkzeug 3.0.1

----- stderr -----
"###);
}
2 changes: 2 additions & 0 deletions typos.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[default.extend-identifiers]
ws = "ws"
Loading