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

support pdm or pyproject style scripts #345

Merged
merged 4 commits into from
Jun 22, 2023
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ that were not yet released.

_Unreleased_

- Scripts now support a PDM style `call` script type. #345

- The `init` command is now capable of importing existing projects. #265

- Fixed the global shim behavior on Windows. #344
Expand Down
18 changes: 18 additions & 0 deletions docs/guide/pyproject.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,24 @@ lint = { chain = ["lint:black", "lint:flake8" ] }
"lint:flake8" = "flake8 src"
```

### `call`

This is a special key that can be set instead of `cmd` to make a command invoke python
functions or modules. The format is one of the three following formats:

* `<module_name>`: equivalent to `python -m <module_name>`
* `<module_name>:<function_name>`: runs `<function_name>` from `<module_name>` and exits with the return value
* `<module_name>:<function_name>(<args>)`: passes specific arguments to the function

Extra arguments provided on the command line are passed in `sys.argv`.

```toml
[tool.rye.scripts]
serve = { call = "http.server" }
help = { call = "builtins:help" }
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 root. By
Expand Down
26 changes: 25 additions & 1 deletion rye/src/cli/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,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, success_status};
use crate::utils::{exec_spawn, get_venv_python_bin, success_status};

/// Runs a command installed into this package.
#[derive(Parser, Debug)]
Expand Down Expand Up @@ -62,6 +62,30 @@ fn invoke_script(
let mut env_overrides = None;

match pyproject.get_script_cmd(&args[0].to_string_lossy()) {
Some(Script::Call(entry, env_vars)) => {
let py = OsString::from(get_venv_python_bin(&pyproject.venv_path()));
env_overrides = Some(env_vars);
args = if let Some((module, func)) = entry.split_once(':') {
if module.is_empty() || func.is_empty() {
bail!("Python callable must be in the form <module_name>:<callable_name> or <module_name>")
}
let call = if !func.contains('(') {
format!("{func}()")
} else {
func.to_string()
};
[
py,
OsString::from("-c"),
OsString::from(format!("import sys, {module} as _1; sys.exit(_1.{call})")),
]
} else {
[py, OsString::from("-m"), OsString::from(entry)]
}
.into_iter()
.chain(args.into_iter().skip(1))
.collect();
}
Some(Script::Cmd(script_args, env_vars)) => {
if script_args.is_empty() {
bail!("script has no arguments");
Expand Down
61 changes: 43 additions & 18 deletions rye/src/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use pep440_rs::{Operator, Version, VersionSpecifiers};
use pep508_rs::Requirement;
use regex::Regex;
use serde::Serialize;
use toml_edit::{Array, Document, Formatted, Item, Table, Value};
use toml_edit::{Array, Document, Formatted, Item, Table, TableLike, Value};
use url::Url;

use crate::config::Config;
Expand Down Expand Up @@ -182,6 +182,8 @@ type EnvVars = HashMap<String, String>;
/// A reference to a script
#[derive(Clone, Debug)]
pub enum Script {
/// Call python module entry
Call(String, EnvVars),
/// A command alias
Cmd(Vec<String>, EnvVars),
/// A multi-script execution
Expand Down Expand Up @@ -210,29 +212,38 @@ fn toml_value_as_command_args(value: &Value) -> Option<Vec<String>> {

impl Script {
fn from_toml_item(item: &Item) -> Option<Script> {
fn get_env_vars(detailed: &dyn TableLike) -> HashMap<String, String> {
let env_vars = detailed
.get("env")
.and_then(|x| x.as_table_like())
.map(|x| {
x.iter()
.map(|x| {
(
x.0.to_string(),
x.1.as_str()
.map(|x| x.to_string())
.unwrap_or_else(|| x.1.to_string()),
)
})
.collect()
})
.unwrap_or_default();
env_vars
}

if let Some(detailed) = item.as_table_like() {
if let Some(cmds) = detailed.get("chain").and_then(|x| x.as_array()) {
if let Some(call) = detailed.get("call") {
let entry = call.as_str()?.to_string();
let env_vars = get_env_vars(detailed);
Some(Script::Call(entry, env_vars))
} else if let Some(cmds) = detailed.get("chain").and_then(|x| x.as_array()) {
Some(Script::Chain(
cmds.iter().flat_map(toml_value_as_command_args).collect(),
))
} else if let Some(cmd) = detailed.get("cmd") {
let cmd = toml_value_as_command_args(cmd.as_value()?)?;
let env_vars = detailed
.get("env")
.and_then(|x| x.as_table_like())
.map(|x| {
x.iter()
.map(|x| {
(
x.0.to_string(),
x.1.as_str()
.map(|x| x.to_string())
.unwrap_or_else(|| x.1.to_string()),
)
})
.collect()
})
.unwrap_or_default();
let env_vars = get_env_vars(detailed);
Some(Script::Cmd(cmd, env_vars))
} else {
None
Expand All @@ -247,6 +258,20 @@ impl Script {
impl fmt::Display for Script {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Script::Call(entry, env) => {
write!(f, "{}", shlex::quote(entry))?;
if !env.is_empty() {
write!(f, " (env: ")?;
for (idx, (key, value)) in env.iter().enumerate() {
if idx > 0 {
write!(f, " ")?;
}
write!(f, "{}={}", shlex::quote(key), shlex::quote(value))?;
}
write!(f, ")")?;
}
Ok(())
}
Script::Cmd(args, env) => {
let mut need_space = false;
for (key, value) in env.iter() {
Expand Down