Skip to content

Commit

Permalink
Added config manipulation command
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko committed Jun 20, 2023
1 parent 544811a commit 405f9e2
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 13 deletions.
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_

- Added `rye config` to read and manipulate the `config.toml` file. #339

- Added support for the new `behavior.global-python` flag which turns on global
Python shimming. When enabled then the `python` shim works even outside of
Rye managed projects. Additionally the shim (when run outside of Rye managed
Expand Down
14 changes: 14 additions & 0 deletions docs/guide/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ name = "default"
url = "http://pypi.org/simple/"
```

## Manipulating Config

+++ 0.9.0

The configuration can be read and modified with `rye config`. The
keys are in dotted notation. `--get` reads a key, `--set`, `--set-int`,
`--set-bool`, or `--unset` modify one.

```bash
rye config --set proxy.http=http://127.0.0.1:4000
rye config --set-bool behavior.rye-force-managed=true
rye config --get default.requires-python
```

## Per Project Config

For the project specific `pyproject.toml` config see [pyproject.toml](pyproject.md).
5 changes: 2 additions & 3 deletions docs/guide/shims.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ System python installation.
To enable global shims, you need to enable the `global-python` flag in
the [`config.toml`](config.md) file:

```toml
[behavior]
global-python = true
```bash
rye config --set-bool behavior.global-python=true
```

Afterwards if you run `python` outside of a Rye managed project it will
Expand Down
235 changes: 235 additions & 0 deletions rye/src/cli/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
use std::collections::BTreeMap;
use std::sync::Arc;

use anyhow::bail;
use anyhow::Context;
use anyhow::Error;
use clap::Parser;
use clap::ValueEnum;
use serde::Serialize;
use toml_edit::value;
use toml_edit::Item;
use toml_edit::Table;
use toml_edit::Value;

use crate::config::Config;

#[derive(ValueEnum, Copy, Clone, Serialize, Debug, PartialEq)]
#[value(rename_all = "snake_case")]
#[serde(rename_all = "snake_case")]
enum Format {
Json,
}

/// Reads or modifies the global `config.toml` file.
///
/// The config file can be read via `--get` and it can be set with one
/// of the set options (`--set`, `--set-int`, `--set-bool`, or `--unset`).
/// Each of the set operations takes a key=value pair. All of these can
/// be supplied multiple times.
#[derive(Parser, Debug)]
pub struct Args {
/// Reads a config key
#[arg(long)]
get: Vec<String>,
/// Sets a config key to a string.
#[arg(long)]
set: Vec<String>,
/// Sets a config key to an integer.
#[arg(long)]
set_int: Vec<String>,
/// Sets a config key to a bool.
#[arg(long)]
set_bool: Vec<String>,
/// Remove a config key.
#[arg(long)]
unset: Vec<String>,
/// Print the path to the config.
#[arg(long, conflicts_with = "format")]
show_path: bool,
/// Request parseable output format rather than lines.
#[arg(long)]
format: Option<Format>,
}

pub fn execute(cmd: Args) -> Result<(), Error> {
let mut config = Config::current();
let doc = Arc::make_mut(&mut config).doc_mut();

if cmd.show_path {
println!("{}", config.path().display());
return Ok(());
}

let mut read_as_json = BTreeMap::new();
let mut read_as_string = Vec::new();
let reads = !cmd.get.is_empty();

for item in cmd.get {
let mut ptr = Some(doc.as_item());
for piece in item.split('.') {
ptr = ptr.as_ref().and_then(|x| x.get(piece));
}

let val = ptr.and_then(|x| x.as_value());
match cmd.format {
None => {
read_as_string.push(value_to_string(val));
}
Some(Format::Json) => {
read_as_json.insert(item, value_to_json(val));
}
}
}

let mut updates: Vec<(&str, Value)> = Vec::new();

for item in &cmd.set {
if let Some((key, value)) = item.split_once('=') {
updates.push((key, Value::from(value)));
} else {
bail!("Invalid value for --set ({})", item);
}
}

for item in &cmd.set_int {
if let Some((key, value)) = item.split_once('=') {
updates.push((
key,
Value::from(
value
.parse::<i64>()
.with_context(|| format!("Invalid value for --set-int ({})", item))?,
),
));
} else {
bail!("Invalid value for --set-int ({})", item);
}
}

for item in &cmd.set_bool {
if let Some((key, value)) = item.split_once('=') {
updates.push((
key,
Value::from(
value
.parse::<bool>()
.with_context(|| format!("Invalid value for --set-bool ({})", item))?,
),
));
} else {
bail!("Invalid value for --set-bool ({})", item);
}
}

let modifies = !updates.is_empty() || !cmd.unset.is_empty();
if modifies && reads {
bail!("cannot mix get and set operations");
}

for (key, new_value) in updates {
let mut ptr = doc.as_item_mut();
for piece in key.split('.') {
if ptr.is_none() {
let mut tbl = Table::new();
tbl.set_implicit(true);
*ptr = Item::Table(tbl);
}
ptr = &mut ptr[piece];
}
*ptr = value(new_value);
}

for key in cmd.unset {
let mut ptr = doc.as_item_mut();
if let Some((parent, key)) = key.rsplit_once('.') {
for piece in parent.split('.') {
ptr = &mut ptr[piece];
}
if let Some(tbl) = ptr.as_table_like_mut() {
tbl.remove(key);
}
if let Item::Table(ref mut tbl) = ptr {
if tbl.is_empty() {
tbl.set_implicit(true);
}
}
} else {
doc.remove(&key);
}
}

if modifies {
config.save()?;
}

match cmd.format {
None => {
for line in read_as_string {
println!("{}", line);
}
}
Some(Format::Json) => {
println!("{}", serde_json::to_string_pretty(&read_as_json)?);
}
}

Ok(())
}

fn value_to_json(val: Option<&Value>) -> serde_json::Value {
match val {
Some(Value::String(s)) => serde_json::Value::String(s.value().into()),
Some(Value::Integer(i)) => serde_json::Value::Number((*i.value()).into()),
Some(Value::Float(f)) => match serde_json::Number::from_f64(*f.value()) {
Some(num) => serde_json::Value::Number(num),
None => serde_json::Value::Null,
},
Some(Value::Boolean(b)) => serde_json::Value::Bool(*b.value()),
Some(Value::Datetime(d)) => serde_json::Value::String(d.to_string()),
Some(Value::Array(a)) => {
serde_json::Value::Array(a.iter().map(|x| value_to_json(Some(x))).collect())
}
Some(Value::InlineTable(t)) => serde_json::Value::Object(
t.iter()
.map(|(k, v)| (k.to_string(), value_to_json(Some(v))))
.collect(),
),
None => serde_json::Value::Null,
}
}

fn value_to_string(val: Option<&Value>) -> String {
match val {
Some(Value::String(s)) => s.value().to_string(),
Some(Value::Integer(i)) => i.value().to_string(),
Some(Value::Float(f)) => f.value().to_string(),
Some(Value::Boolean(b)) => b.value().to_string(),
Some(Value::Datetime(d)) => d.value().to_string(),
Some(Value::Array(a)) => {
let mut rv = String::from('[');
for (idx, item) in a.iter().enumerate() {
if idx > 0 {
rv.push_str(", ");
}
rv.push_str(&value_to_string(Some(item)));
}
rv.push(']');
rv
}
Some(Value::InlineTable(t)) => {
let mut rv = String::from('{');
for (idx, (key, value)) in t.iter().enumerate() {
if idx > 0 {
rv.push_str(", ");
}
rv.push_str(key);
rv.push_str(" = ");
rv.push_str(&value_to_string(Some(value)));
}
rv.push('}');
rv
}
None => "?".into(),
}
}
3 changes: 3 additions & 0 deletions rye/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use clap::Parser;

mod add;
mod build;
mod config;
mod fetch;
mod init;
mod install;
Expand Down Expand Up @@ -46,6 +47,7 @@ struct Args {
enum Command {
Add(add::Args),
Build(build::Args),
Config(config::Args),
Fetch(fetch::Args),
Init(init::Args),
Install(install::Args),
Expand Down Expand Up @@ -93,6 +95,7 @@ pub fn execute() -> Result<(), Error> {
match cmd {
Command::Add(cmd) => add::execute(cmd),
Command::Build(cmd) => build::execute(cmd),
Command::Config(cmd) => config::execute(cmd),
Command::Fetch(cmd) => fetch::execute(cmd),
Command::Init(cmd) => init::execute(cmd),
Command::Install(cmd) => install::execute(cmd),
Expand Down
34 changes: 24 additions & 10 deletions rye/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::fs;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};

use anyhow::{Context, Error};
Expand All @@ -17,22 +17,19 @@ pub fn load() -> Result<(), Error> {
let cfg = if cfg_path.is_file() {
Config::from_path(&cfg_path)?
} else {
Config::default()
Config {
doc: Document::new(),
path: cfg_path,
}
};
*CONFIG.lock().unwrap() = Some(Arc::new(cfg));
Ok(())
}

#[derive(Clone)]
pub struct Config {
doc: Document,
}

impl Default for Config {
fn default() -> Self {
Config {
doc: Document::new(),
}
}
path: PathBuf,
}

impl Config {
Expand All @@ -46,6 +43,22 @@ impl Config {
.clone()
}

/// Returns a clone of the internal doc.
pub fn doc_mut(&mut self) -> &mut Document {
&mut self.doc
}

/// Saves changes back.
pub fn save(&self) -> Result<(), Error> {
fs::write(&self.path, self.doc.to_string())?;
Ok(())
}

/// Returns the path.
pub fn path(&self) -> &Path {
&self.path
}

/// Loads a config from a path.
pub fn from_path(path: &Path) -> Result<Config, Error> {
let contents = fs::read_to_string(path)
Expand All @@ -54,6 +67,7 @@ impl Config {
doc: contents
.parse::<Document>()
.with_context(|| format!("failed to parse config from '{}'", path.display()))?,
path: path.to_path_buf(),
})
}

Expand Down

0 comments on commit 405f9e2

Please sign in to comment.