feat: add more init variables

This commit is contained in:
DataHearth 2022-12-07 18:49:39 +00:00
parent 60fe830b58
commit 4c4d386cca
No known key found for this signature in database
GPG Key ID: E88FD356ACC5F3C4
4 changed files with 464 additions and 179 deletions

171
src/gitignore_python.txt Normal file
View File

@ -0,0 +1,171 @@
# Created by https://www.toptal.com/developers/gitignore/api/python
# Edit at https://www.toptal.com/developers/gitignore?templates=python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# End of https://www.toptal.com/developers/gitignore/api/python

View File

@ -1,171 +0,0 @@
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<String>,
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<Contributor>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub maintainers: Vec<Contributor>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub keywords: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub classifiers: Vec<String>,
#[serde(flatten)]
pub additional_fields: HashMap<String, Value>,
}
pub fn ask_user_inputs(minimum: bool) -> Result<Pyproject> {
let mut project = Project::default();
let mut build_system = BuildSystem::default();
let theme = ColorfulTheme::default();
build_system.requires = Input::<String>::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::<String>::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::<String>::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::<String>::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::<String>::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())
}
}

View File

@ -1,22 +1,45 @@
use std::{fs::write, path::PathBuf};
use std::{
env,
fs::{self, File},
path::PathBuf,
process::Command,
};
use anyhow::Result;
use clap::Parser;
use init::ask_user_inputs;
use toml::toml;
use pyproject::Pyproject;
mod init;
mod pyproject;
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)]
enum Layout {
Src,
Flat,
}
#[derive(clap::Parser)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Output will be more verbose
#[arg(short, long, default_value_t = false)]
verbose: bool,
/// Initialize a new python directory with minimal fields for "pyproject.toml"
/// Initialize with minimal fields for "pyproject.toml"
#[arg(short, long, default_value_t = true)]
minimum: bool,
/// Initialize folder with a git repository
#[arg(long, default_value_t = true)]
git: bool,
/// Initialize a new virtual environment with given name in initialized directory
#[arg(long)]
venv: Option<String>,
/// Define a layout for your project (https://setuptools.pypa.io/en/latest/userguide/package_discovery.html)
#[arg(long, value_enum)]
layout: Option<Layout>,
#[command(subcommand)]
subcommands: Subcommands,
}
@ -31,12 +54,71 @@ fn main() -> Result<()> {
let args = Args::parse();
match args.subcommands {
Subcommands::New { folder } => todo!(),
Subcommands::New { folder } => {
fs::create_dir(&folder)?;
let folder = folder.canonicalize()?;
initialize_folder(folder, args.minimum, args.layout, args.venv, args.git)?;
}
Subcommands::Init {} => {
let pyproject = ask_user_inputs(args.minimum)?;
write("pyproject.toml", toml::to_vec(&pyproject)?)?;
let folder = env::current_dir()?;
initialize_folder(folder, args.minimum, args.layout, args.venv, args.git)?;
}
};
Ok(())
}
fn initialize_folder(
folder: PathBuf,
minimum: bool,
layout: Option<Layout>,
venv: Option<String>,
git: bool,
) -> Result<()> {
// todo: avoid clone and maybe find a better way
let mut pypro = Pyproject::new(folder.clone(), minimum);
pypro.ask_inputs()?;
let project_name = pypro.get_project_name();
pypro.create_file()?;
if let Some(layout) = layout {
let layout_inner_path = match layout {
Layout::Src => {
let inner_path = folder.join(format!("src/{project_name}"));
fs::create_dir_all(&inner_path)?;
inner_path
}
Layout::Flat => {
let inner_path = folder.join(project_name);
fs::create_dir(&inner_path)?;
inner_path
}
};
File::create(layout_inner_path.join("__init__.py"))?;
}
if let Some(venv) = venv {
Command::new("python3")
.args(&["-m", "venv", &venv])
.current_dir(&folder)
.status()?;
}
if git {
Command::new("git")
.args(&["init"])
.current_dir(&folder)
.status()?;
fs::write(
folder.join(".gitignore"),
include_bytes!("gitignore_python.txt"),
)?;
}
Ok(())
}

203
src/pyproject.rs Normal file
View File

@ -0,0 +1,203 @@
use std::{fs, path::PathBuf};
use anyhow::{anyhow, Result};
use dialoguer::{theme::ColorfulTheme, Input};
use regex::Regex;
use serde::Serialize;
const PYPROJECT: &str = "pyproject.toml";
#[derive(Debug, Serialize)]
#[serde(rename_all(serialize = "kebab-case"))]
pub struct Pyproject {
#[serde(skip_serializing)]
folder: PathBuf,
#[serde(skip_serializing)]
minimum: bool,
pub build_system: BuildSystem,
pub project: Project,
}
impl Pyproject {
pub fn new(folder: PathBuf, minimum: bool) -> Self {
Pyproject {
folder,
minimum,
build_system: BuildSystem::default(),
project: Project::default(),
}
}
/// Ask user inputs to create a basic (or not) pyproject.toml file.
/// `minimum` will only ask for the `build-system` fields and the `project.name`
/// and `project.version` fields
pub fn ask_inputs(&mut self) -> Result<()> {
let theme = ColorfulTheme::default();
self.build_system.requires = Input::<String>::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();
self.build_system.build_backend = Input::with_theme(&theme)
.with_prompt("build back-end")
.default("setuptools.build_meta".to_string())
.interact_text()?;
// ? might want to switch to OsString instead, if the Serialize macro supports it
let folder = match self
.folder
.file_name()
.ok_or(anyhow!("project can't terminate by \"..\""))?
.to_str()
{
Some(v) => Some(v.to_string()),
None => None,
};
let mut input: Input<String> = Input::with_theme(&theme);
if let Some(folder) = folder {
self.project.name = input
.with_prompt("project name")
.default(folder)
.interact_text()?;
} else {
self.project.name = input.with_prompt("project name").interact_text()?;
}
self.project.version = Input::with_theme(&theme)
.with_prompt("version")
.default("0.1.0".to_string())
.interact_text()?;
if !self.minimum {
self.project.description = Input::with_theme(&theme)
.with_prompt("description")
.allow_empty(true)
.interact_text()?;
self.project.readme = Input::with_theme(&theme)
.with_prompt("readme")
.allow_empty(true)
.interact_text()?;
self.project.requires_python = Input::with_theme(&theme)
.with_prompt("minimum python version")
.allow_empty(true)
.interact_text()?;
self.project.license = Input::with_theme(&theme)
.with_prompt("license")
.allow_empty(true)
.interact_text()?;
self.project.authors = Input::<String>::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| self.parse_contributor(v))
.collect();
self.project.maintainers = Input::<String>::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| self.parse_contributor(v))
.collect();
self.project.keywords = Input::<String>::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();
self.project.classifiers = Input::<String>::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(())
}
fn parse_contributor(&self, 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())
}
}
pub fn get_project_name(&self) -> String {
// ? clone or maybe something else ?
self.project.name.clone()
}
/// Consume self and write everything to a `self.folder/pyproject.toml`
pub fn create_file(self) -> Result<()> {
fs::write(self.folder.join(PYPROJECT), toml::to_vec(&self)?)?;
Ok(())
}
}
#[derive(Default, Debug, Serialize)]
#[serde(rename_all(serialize = "kebab-case"))]
pub struct BuildSystem {
pub requires: Vec<String>,
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<Contributor>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub maintainers: Vec<Contributor>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub keywords: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub classifiers: Vec<String>,
}