diff --git a/docs/guide/commands/run.md b/docs/guide/commands/run.md index eab03d8d47..560dbff87a 100644 --- a/docs/guide/commands/run.md +++ b/docs/guide/commands/run.md @@ -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). diff --git a/docs/guide/pyproject.md b/docs/guide/pyproject.md index 34db616b37..0df200d10e 100644 --- a/docs/guide/pyproject.md +++ b/docs/guide/pyproject.md @@ -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 diff --git a/rye/src/cli/run.rs b/rye/src/cli/run.rs index 7db917d72b..394c025827 100644 --- a/rye/src/cli/run.rs +++ b/rye/src/cli/run.rs @@ -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)] @@ -26,6 +26,12 @@ pub struct Args { /// Use this pyproject.toml file #[arg(long, value_name = "PYPROJECT_TOML")] pyproject: Option, + /// Enables verbose diagnostics. + #[arg(short, long)] + verbose: bool, + /// Turns off all output. + #[arg(short, long, conflicts_with = "verbose")] + quiet: bool, } #[derive(Parser, Debug)] @@ -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 { diff --git a/rye/src/sync.rs b/rye/src/sync.rs index 2acf751a15..41f031cbd5 100644 --- a/rye/src/sync.rs +++ b/rye/src/sync.rs @@ -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. diff --git a/rye/tests/common/mod.rs b/rye/tests/common/mod.rs index a1827c4fdc..ebca7bb85b 100644 --- a/rye/tests/common/mod.rs +++ b/rye/tests/common/mod.rs @@ -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 { diff --git a/rye/tests/test_run.rs b/rye/tests/test_run.rs new file mode 100644 index 0000000000..7fd5fabc38 --- /dev/null +++ b/rye/tests/test_run.rs @@ -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 ----- + "###); +} diff --git a/typos.toml b/typos.toml new file mode 100644 index 0000000000..04ef337911 --- /dev/null +++ b/typos.toml @@ -0,0 +1,2 @@ +[default.extend-identifiers] +ws = "ws"