dmenv 0.20.1

Simple and practical virtualenv manager for Python
Documentation
use crate::error::*;
use crate::ui::*;
use std::path::Path;

#[derive(Debug)]
pub struct InitOptions {
    name: String,
    version: String,
    author: Option<String>,
    setup_cfg: bool,
}

impl InitOptions {
    pub fn new(name: String, version: String) -> Self {
        InitOptions {
            name,
            version,
            author: None,
            setup_cfg: true,
        }
    }

    pub fn author(&mut self, author: &str) -> &mut Self {
        self.author = Some(author.to_string());
        self
    }

    pub fn no_setup_cfg(&mut self) -> &mut Self {
        self.setup_cfg = false;
        self
    }
}

fn ensure_path_does_not_exist(path: &Path) -> Result<(), Error> {
    if path.exists() {
        return Err(Error::FileExists {
            path: path.to_path_buf(),
        });
    }
    Ok(())
}

fn write_to_path(path: &Path, contents: &str) -> Result<(), Error> {
    std::fs::write(path, contents).map_err(|e| new_write_error(e, path))
}

pub fn init(project_path: &Path, options: &InitOptions) -> Result<(), Error> {
    let setup_cfg_path = project_path.join("setup.cfg");
    let setup_py_path = project_path.join("setup.py");

    // A setup.py is written in both cases, so check we're not
    // overwriting it first
    ensure_path_does_not_exist(&setup_py_path)?;

    if options.setup_cfg {
        ensure_path_does_not_exist(&setup_cfg_path)?;
        write_from_template(include_str!("init/setup.in.cfg"), &setup_cfg_path, options)?;
    } else {
        write_from_template(include_str!("init/setup.in.py"), &setup_py_path, options)?;
    }

    if options.setup_cfg {
        // We still need an almost-empty setup.py even when all the configuration
        // is in setup.cfg :/
        write_to_path(&setup_py_path, "from setuptools import setup\nsetup()\n")?;
        print_info_1("Project initialized with setup.py and setup.cfg files");
    } else {
        print_info_1("Project initialized with a setup.py file");
    }
    Ok(())
}

fn write_from_template(
    template: &str,
    dest_path: &Path,
    options: &InitOptions,
) -> Result<(), Error> {
    // Warning: make sure the template files in `src/operations/` contain all those
    // placeholders
    let with_name = template.replace("<NAME>", &options.name);
    let with_version = with_name.replace("<VERSION>", &options.version);
    let to_write = if let Some(ref author) = options.author {
        with_version.replace("<AUTHOR>", author)
    } else {
        with_version
    };
    write_to_path(dest_path, &to_write)
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempdir;

    #[test]
    fn creates_two_files_by_default() {
        let tmp_dir = tempdir::TempDir::new("test-dmenv-init").unwrap();
        let tmp_path = tmp_dir.path();

        run_init(&tmp_path.to_path_buf()).unwrap();

        let setup_py = std::fs::read_to_string(&tmp_path.join("setup.py")).unwrap();
        assert_contains(&setup_py, "setup()");
        assert_not_contains(&setup_py, "foo");

        let setup_cfg = std::fs::read_to_string(&tmp_path.join("setup.cfg")).unwrap();
        assert_contains(&setup_cfg, "name = foo");
        assert_contains(&setup_cfg, "version = 0.42");
    }

    #[test]
    fn no_setup_cfg() {
        let temp_dir = tempdir::TempDir::new("test-dmenv-init").unwrap();
        let tmp_path = temp_dir.path();

        run_init_no_setup_cfg(&tmp_path.to_path_buf()).unwrap();

        let setup_cfg_path = tmp_path.join("setup.cfg");
        assert!(!setup_cfg_path.exists());

        let setup_py = std::fs::read_to_string(tmp_path.join("setup.py")).unwrap();
        assert_contains(&setup_py, "\"foo\"");
        assert_contains(&setup_py, "\"0.42\"");
    }

    #[test]
    fn does_not_overwrite_setup_cfg() {
        let temp_dir = tempdir::TempDir::new("test-dmenv-init").unwrap();
        let tmp_path = temp_dir.path();
        let setup_cfg_path = tmp_path.join("setup.cfg");
        touch(&setup_cfg_path);

        let err = run_init(&tmp_path.to_path_buf()).unwrap_err();
        assert_file_exists_error(err, &setup_cfg_path);
    }

    #[test]
    fn does_not_overwrite_setup_py() {
        let temp_dir = tempdir::TempDir::new("test-dmenv-init").unwrap();
        let tmp_path = temp_dir.path();
        let setup_py_path = tmp_path.join("setup.py");
        touch(&setup_py_path);

        let err = run_init_no_setup_cfg(&tmp_path.to_path_buf()).unwrap_err();
        assert_file_exists_error(err, &setup_py_path);
    }

    fn assert_contains(text: &str, sub_string: &str) {
        if !text.contains(sub_string) {
            panic!("\n{}should contain {}", text, sub_string);
        }
    }

    fn assert_not_contains(text: &str, sub_string: &str) {
        if text.contains(sub_string) {
            panic!("\n{}should not contain {}", text, sub_string);
        }
    }

    fn assert_file_exists_error(err: Error, expected_path: &Path) {
        match err {
            Error::FileExists { path } => assert_eq!(&path, expected_path),
            _ => panic!("Expecting FileExists, got: {}", err),
        }
    }

    fn touch(path: &Path) {
        std::fs::write(&path, "# don't overwrite me").unwrap()
    }

    fn run_init(tmp_path: &Path) -> Result<(), Error> {
        let init_options = InitOptions::new("foo".to_string(), "0.42".to_string());
        init(tmp_path, &init_options)
    }

    fn run_init_no_setup_cfg(tmp_path: &Path) -> Result<(), Error> {
        let mut init_options = InitOptions::new("foo".to_string(), "0.42".to_string());
        init_options.no_setup_cfg();
        init(tmp_path, &init_options)
    }
}