From 60fe830b58d4e4a26c0d5fef2e893d8ed6029d9c Mon Sep 17 00:00:00 2001 From: DataHearth Date: Wed, 7 Dec 2022 00:18:33 +0100 Subject: [PATCH] feat: add init command --- .gitignore | 1 + Cargo.lock | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 7 +- src/init.rs | 171 +++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 43 +++++++++++- 5 files changed, 406 insertions(+), 3 deletions(-) create mode 100644 src/init.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..19a9d4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +pyproject.toml \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index b50b389..7869e18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" + [[package]] name = "bitflags" version = "1.3.2" @@ -14,6 +29,12 @@ version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.0.29" @@ -51,6 +72,37 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "console" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "terminal_size", + "unicode-width", + "winapi", +] + +[[package]] +name = "dialoguer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92e7e37ecef6857fdc0c0c5d42fd5b0938e46590c2183cc92dd310a6d078eb1" +dependencies = [ + "console", + "tempfile", + "zeroize", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "errno" version = "0.2.8" @@ -72,6 +124,15 @@ dependencies = [ "libc", ] +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + [[package]] name = "heck" version = "0.4.0" @@ -87,6 +148,15 @@ dependencies = [ "libc", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "io-lifetimes" version = "1.0.3" @@ -109,6 +179,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.137" @@ -121,6 +197,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + [[package]] name = "once_cell" version = "1.16.0" @@ -170,7 +252,12 @@ dependencies = [ name = "pynit" version = "0.1.0" dependencies = [ + "anyhow", "clap", + "dialoguer", + "regex", + "serde", + "toml", ] [[package]] @@ -182,6 +269,41 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "rustix" version = "0.36.4" @@ -196,6 +318,26 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "serde" +version = "1.0.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "strsim" version = "0.10.0" @@ -213,6 +355,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -222,12 +378,37 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + [[package]] name = "unicode-ident" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + [[package]] name = "version_check" version = "0.9.4" @@ -321,3 +502,9 @@ name = "windows_x86_64_msvc" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + +[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" diff --git a/Cargo.toml b/Cargo.toml index caddc02..84313ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,9 +4,14 @@ version = "0.1.0" edition = "2021" authors = ["Antoine Langlois "] license = "GPL-3.0-or-later" -description = "small CLI tool to initialize a python project" +description = "Small CLI tool to initialize a python project" homepage = "https://gitea.antoine-langlois.net/DataHeart/pynit" repository = "https://gitea.antoine-langlois.net/DataHeart/pynit" [dependencies] +anyhow = "1.0" clap = { version = "4.0", features = ["derive"] } +dialoguer = "0.10" +regex = "1.7" +serde = { version = "1.0", features = ["derive"] } +toml = "0.5" diff --git a/src/init.rs b/src/init.rs new file mode 100644 index 0000000..6e24fd0 --- /dev/null +++ b/src/init.rs @@ -0,0 +1,171 @@ +use std::{collections::HashMap, env}; + +use anyhow::Result; +use dialoguer::{theme::ColorfulTheme, Input}; +use regex::Regex; +use serde::Serialize; +use toml::Value; + +#[derive(Debug, Serialize)] +#[serde(rename_all(serialize = "kebab-case"))] +pub struct Pyproject { + pub build_system: BuildSystem, + pub project: Project, +} + +#[derive(Default, Debug, Serialize)] +#[serde(rename_all(serialize = "kebab-case"))] +pub struct BuildSystem { + pub requires: Vec, + pub build_backend: String, +} + +#[derive(Debug, Serialize)] +pub enum Contributor { + Flat(String), + Complex { name: String, email: String }, +} + +#[derive(Default, Debug, Serialize)] +#[serde(rename_all(serialize = "kebab-case"))] +pub struct Project { + pub name: String, + pub version: String, + + #[serde(skip_serializing_if = "String::is_empty")] + pub description: String, + #[serde(skip_serializing_if = "String::is_empty")] + pub readme: String, + #[serde(skip_serializing_if = "String::is_empty")] + pub requires_python: String, + #[serde(skip_serializing_if = "String::is_empty")] + pub license: String, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub authors: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub maintainers: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub keywords: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub classifiers: Vec, + + #[serde(flatten)] + pub additional_fields: HashMap, +} + +pub fn ask_user_inputs(minimum: bool) -> Result { + let mut project = Project::default(); + let mut build_system = BuildSystem::default(); + let theme = ColorfulTheme::default(); + + build_system.requires = Input::::with_theme(&theme) + .with_prompt("build dependencies (comma separated)") + .default("setuptools,wheel".to_string()) + .interact_text()? + .split(',') + .filter(|v| !v.is_empty()) + .map(|v| v.to_string()) + .collect(); + build_system.build_backend = Input::with_theme(&theme) + .with_prompt("build back-end") + .default("setuptools.build_meta".to_string()) + .interact_text()?; + + project.name = Input::with_theme(&theme) + .with_prompt("project name") + .default( + env::current_dir()? + .file_name() + .expect("invalid current directory") + .to_str() + .expect("invalid UTF-8 cwd name") + .to_string(), + ) + .interact_text()?; + project.version = Input::with_theme(&theme) + .with_prompt("version") + .default("0.1.0".to_string()) + .interact_text()?; + + if !minimum { + project.description = Input::with_theme(&theme) + .with_prompt("description") + .allow_empty(true) + .interact_text()?; + project.readme = Input::with_theme(&theme) + .with_prompt("readme") + .allow_empty(true) + .interact_text()?; + project.requires_python = Input::with_theme(&theme) + .with_prompt("minimum python version") + .allow_empty(true) + .interact_text()?; + project.license = Input::with_theme(&theme) + .with_prompt("license") + .allow_empty(true) + .interact_text()?; + project.authors = Input::::with_theme(&theme) + .with_prompt( + r#"authors (e.g: "Antoine Langlois";"name="Antoine L",email="email@domain.net"")"#, + ) + .allow_empty(true) + .interact_text()? + .split(';') + .filter(|v| !v.is_empty()) + .map(|v| parse_contributor(v)) + .collect(); + project.maintainers = Input::::with_theme(&theme) + .with_prompt( + r#"maintainers (e.g: "Antoine Langlois";"name="Antoine L",email="email@domain.net"")"#, + ) + .allow_empty(true) + .interact_text()? + .split(';') + .filter(|v| !v.is_empty()) + .map(|v| parse_contributor(v)) + .collect(); + project.keywords = Input::::with_theme(&theme) + .with_prompt("keywords (e.g: KEYW1;KEYW2)") + .allow_empty(true) + .interact_text()? + .split(';') + .filter(|v| !v.is_empty()) + .map(|v| v.to_string()) + .collect(); + project.classifiers = Input::::with_theme(&theme) + .with_prompt("classifiers (e.g: CLASS1;CLASS2)") + .allow_empty(true) + .interact_text()? + .split(';') + .filter(|v| !v.is_empty()) + .map(|v| v.to_string()) + .collect(); + } + + Ok(Pyproject { + build_system, + project, + }) +} + +fn parse_contributor(contributor: &str) -> Contributor { + let name_regex = Regex::new(r#"name="([\w\s\-\.]*)""#).expect("invalid name regex expression"); + let email_regex = + Regex::new(r#"email="([\w\s\-\.@]*)""#).expect("invalid email regex expression"); + + let name = name_regex.captures(contributor); + let email = email_regex.captures(contributor); + + if name.is_some() && email.is_some() { + Contributor::Complex { + name: name.unwrap().get(1).unwrap().as_str().to_string(), + email: email.unwrap().get(1).unwrap().as_str().to_string(), + } + } else if let Some(name_match) = name { + Contributor::Flat(name_match.get(1).unwrap().as_str().to_string()) + } else if let Some(email_match) = email { + Contributor::Flat(email_match.get(1).unwrap().as_str().to_string()) + } else { + Contributor::Flat(contributor.to_string()) + } +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..2152a50 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,42 @@ -fn main() { - println!("Hello, world!"); +use std::{fs::write, path::PathBuf}; + +use anyhow::Result; +use clap::Parser; +use init::ask_user_inputs; +use toml::toml; + +mod init; + +#[derive(clap::Parser)] +#[command(author, version, about, long_about = None)] +struct Args { + #[arg(short, long, default_value_t = false)] + verbose: bool, + + /// Initialize a new python directory with minimal fields for "pyproject.toml" + #[arg(short, long, default_value_t = true)] + minimum: bool, + + #[command(subcommand)] + subcommands: Subcommands, +} + +#[derive(clap::Subcommand)] +enum Subcommands { + New { folder: PathBuf }, + Init {}, +} + +fn main() -> Result<()> { + let args = Args::parse(); + + match args.subcommands { + Subcommands::New { folder } => todo!(), + Subcommands::Init {} => { + let pyproject = ask_user_inputs(args.minimum)?; + write("pyproject.toml", toml::to_vec(&pyproject)?)?; + } + }; + + Ok(()) }