Esempio n. 1
0
def test_dispatch_env_correctly_identifies_flit(fake_flit_project: Path):

    repo = Repo(name="test", owner="me", local_path=fake_flit_project)

    env = repo.dispatch_env(config=Config())

    assert isinstance(env, Flit)
Esempio n. 2
0
def main(ctx: click.Context) -> None:
    """
    Helpful CLI to automate the development workflow.

    - Create and manage your local and remote projects

    - Build projects from cookiecutter templates.

    - Easily create/manage virtual environments.

    - Minimal configuration required.
    """
    # Load the config once on launch of the app and pass it down to the child commands
    # through click's context
    try:
        config = Config.load()
    except FileNotFoundError:
        interactive_config()
    else:
        if not config.can_use_api():
            printer.error(
                "You must set your GitHub username and personal access token to use API"
                " features.",
                exits=1,
            )
        # We have a valid config file at the right place so load it into click's
        # context and pass it down to all subcommands
        ctx.obj = config
Esempio n. 3
0
def test_dispatch_env_correctly_identifies_requirements_dev_txt(
    requirements_dev_project: Path,
):

    repo = Repo(name="test", owner="me", local_path=requirements_dev_project)

    env = repo.dispatch_env(config=Config())

    assert isinstance(env, Requirements)
Esempio n. 4
0
def test_dispatch_env_correctly_identifies_conda(mocker: MockerFixture):

    mocker.patch("pytoil.repo.Repo.is_conda", autospec=True, return_value=True)

    repo = Repo(name="test", owner="me", local_path=Path("somewhere"))

    env = repo.dispatch_env(config=Config())

    assert isinstance(env, Conda)
Esempio n. 5
0
def test_file_write():
    with tempfile.NamedTemporaryFile(
            "w", delete=False if ON_CI and ON_WINDOWS else True) as file:
        # Make a fake config object
        config = Config(
            projects_dir=Path("some/dir"),
            token="sometoken",
            username="******",
            editor="fakeedit",
            common_packages=["black", "mypy", "flake8"],
            git=False,
        )

        # Write the config
        config.write(path=Path(file.name))

        file_config = Config.load(Path(file.name))

        assert file_config == config
Esempio n. 6
0
def test_config_helper():

    config = Config.helper()

    assert config.projects_dir == defaults.PROJECTS_DIR
    assert config.token == "Put your GitHub personal access token here"
    assert config.username == "This your GitHub username"
    assert config.editor == defaults.EDITOR
    assert config.conda_bin == defaults.CONDA_BIN
    assert config.common_packages == defaults.COMMON_PACKAGES
    assert config.git == defaults.GIT
Esempio n. 7
0
def test_config_init_defaults():

    config = Config()

    assert config.projects_dir == defaults.PROJECTS_DIR
    assert config.token == defaults.TOKEN
    assert config.username == defaults.USERNAME
    assert config.editor == defaults.EDITOR
    assert config.conda_bin == defaults.CONDA_BIN
    assert config.common_packages == defaults.COMMON_PACKAGES
    assert config.git == defaults.GIT
Esempio n. 8
0
def test_dispatch_env_returns_none_if_it_cant_detect(mocker: MockerFixture):

    mocker.patch("pytoil.repo.Repo.is_conda", autospec=True, return_value=False)

    mocker.patch("pytoil.repo.Repo.is_setuptools", autospec=True, return_value=False)

    repo = Repo(name="test", owner="me", local_path=Path("somewhere"))

    env = repo.dispatch_env(config=Config())

    assert env is None
Esempio n. 9
0
def test_config_init_passed():

    config = Config(
        projects_dir=Path("some/dir"),
        token="sometoken",
        username="******",
        editor="fakeedit",
        conda_bin="mamba",
        common_packages=["black", "mypy", "flake8"],
        git=False,
    )

    assert config.projects_dir == Path("some/dir")
    assert config.token == "sometoken"
    assert config.username == "me"
    assert config.editor == "fakeedit"
    assert config.conda_bin == "mamba"
    assert config.common_packages == ["black", "mypy", "flake8"]
    assert config.git is False
Esempio n. 10
0
def show(config: Config) -> None:
    """
    Show pytoil's config.

    The show command allows you to easily see pytoil's current config.

    The values are taken directly from the config file where specified or
    the defaults otherwise.

    Examples:

    $ pytoil config show
    """
    table = Table(box=box.SIMPLE)
    table.add_column("Key", style="cyan", justify="right")
    table.add_column("Value", justify="left")

    for key, val in config.to_dict().items():
        table.add_row(f"{key}:", str(val))

    console = Console()
    console.print(table)
Esempio n. 11
0
def new(  # noqa: C901
    config: Config,
    project: str,
    packages: tuple[str, ...],
    cookie: str | None,
    _copier: str | None,
    starter: str | None,
    venv: str | None,
    no_git: bool = False,
) -> None:
    """
    Create a new development project.

    Bare usage will simply create an empty folder in your configured projects
    directory.

    You can also create a project from a cookiecutter or copier template by passing a valid
    url to the '--cookie/-c' or '--copier/-C' flags.

    If you just want a very simple, language-specific starting template, use the
    '--starter/-s' option.

    By default, pytoil will initialise a local git repo in the folder and commit it,
    following the style of modern language build tools such as rust's cargo. You can disable
    this behaviour by setting 'git' to false in pytoil's config file
    or by passing the '--no-git/-n' flag here.

    If you want pytoil to create a new virtual environment for your project, you
    can use the '--venv/-v' flag. Standard python and conda virtual environments
    are supported.

    If the '--venv/-v' flag is used, you may also pass a list of python packages
    to install into the created virtual environment. These will be delegated to
    the appropriate tool (pip or conda) depending on what environment was created.
    If the environment is conda, the packages will be passed at environment creation
    time meaning they will have their dependencies resolved together. Normal python
    environments will first be created and then have specified packages installed.

    If 'common_packages' is specified in pytoil's config file, these will automatically
    be included in the environment.

    To specify versions of packages via the command line, you must enclose them
    in double quotes e.g. "flask>=1.0.0" not flask>=1.0.0 otherwise this will
    be interpreted by the shell as a command redirection.

    Examples:

    $ pytoil new my_project

    $ pytoil new my_project --cookie https://github.com/some/cookie.git

    $ pytoil new my_project --venv conda

    $ pytoil new my_project -c https://github.com/some/cookie.git -v conda --no-git

    $ pytoil new my_project -v venv requests "flask>=1.0.0"

    $ pytoil new my_project --starter python
    """
    api = API(username=config.username, token=config.token)
    repo = Repo(
        owner=config.username,
        name=project,
        local_path=config.projects_dir.joinpath(project),
    )
    git = Git()

    # Additional packages to include
    to_install: list[str] = [*packages] + config.common_packages

    # Can't use --cookie and --starter
    if cookie and starter:
        printer.error("--cookie and --starter are mutually exclusive", exits=1)

    # Can't use --copier and --starter
    if _copier and starter:
        printer.error("--copier and --starter are mutually exclusive", exits=1)

    # Can't use --venv with non-python starters
    if (
        starter is not None  # User specified --starter
        and starter != "python"  # Requested starter is not python
        and venv is not None  # And the user wants a virtual environment
    ):
        printer.error(f"Can't create a venv for a {starter} project", exits=1)

    # Resolve config vs flag for no-git
    # flag takes priority over config
    use_git: bool = config.git and not no_git

    # Does this project already exist?
    # Mightaswell check concurrently
    local = repo.exists_local()
    remote = repo.exists_remote(api)

    if local:
        printer.error(f"{repo.name} already exists locally.")
        printer.note(
            f"To checkout this project, use `pytoil checkout {repo.name}`.", exits=1
        )

    if remote:
        printer.error(f"{repo.name} already exists on GitHub.")
        printer.note(
            f"To checkout this project, use `pytoil checkout {repo.name}`.", exits=1
        )

    # If we get here, we're good to create a new project
    if cookie:
        printer.info(f"Creating {repo.name} from cookiecutter: {cookie}.")
        cookiecutter(template=cookie, output_dir=str(config.projects_dir))

    elif _copier:
        printer.info(f"Creating {repo.name} from copier: {_copier}.")
        copier.run_auto(src_path=_copier, dst_path=repo.local_path)

    elif starter == "go":
        printer.info(f"Creating {repo.name} from starter: {starter}.")
        go_starter = GoStarter(path=config.projects_dir, name=repo.name)

        try:
            go_starter.generate(username=config.username)
        except GoNotInstalledError:
            printer.error("Go not installed.", exits=1)
        else:
            if use_git:
                git.init(cwd=repo.local_path, silent=False)
                git.add(cwd=repo.local_path, silent=False)
                git.commit(cwd=repo.local_path, silent=False)

    elif starter == "python":
        printer.info(f"Creating {repo.name} from starter: {starter}.")
        python_starter = PythonStarter(path=config.projects_dir, name=repo.name)
        python_starter.generate()
        if use_git:
            git.init(cwd=repo.local_path, silent=False)
            git.add(cwd=repo.local_path, silent=False)
            git.commit(cwd=repo.local_path, silent=False)

    elif starter == "rust":
        printer.info(f"Creating {repo.name} from starter: {starter}.")
        rust_starter = RustStarter(path=config.projects_dir, name=repo.name)

        try:
            rust_starter.generate()
        except CargoNotInstalledError:
            printer.error("Cargo not installed.", exits=1)
        else:
            if use_git:
                git.init(cwd=repo.local_path, silent=False)
                git.add(cwd=repo.local_path, silent=False)
                git.commit(cwd=repo.local_path, silent=False)

    else:
        # Just a blank new project
        printer.info(f"Creating {repo.name} at '{repo.local_path}'.")
        repo.local_path.mkdir()
        if use_git:
            git.init(cwd=repo.local_path, silent=False)

    # Now we need to handle any requested virtual environments
    if venv == "venv":
        printer.info(f"Creating virtual environment for {repo.name}")
        if to_install:
            printer.note(f"Including {', '.join(to_install)}")

        env = Venv(root=repo.local_path)
        with printer.progress() as p:
            p.add_task("[bold white]Working")
            env.create(packages=to_install, silent=True)

    elif venv == "conda":
        # Note, conda installs take longer so by default we don't hide the output
        # like we do for normal python environments
        printer.info(f"Creating conda environment for {repo.name}")
        if to_install:
            printer.note(f"Including {', '.join(to_install)}")

        conda_env = Conda(
            root=repo.local_path, environment_name=repo.name, conda=config.conda_bin
        )
        try:
            conda_env.create(packages=to_install)
        except EnvironmentAlreadyExistsError:
            printer.error(
                f"Conda environment {conda_env.environment_name!r} already exists",
                exits=1,
            )
        else:
            # Export the environment.yml
            conda_env.export_yml()

    # Now handle opening in an editor
    if config.specifies_editor():
        printer.sub_info(f"Opening {repo.name} with {config.editor}")
        editor.launch(path=repo.local_path, bin=config.editor)
Esempio n. 12
0
def interactive_config() -> None:
    """
    Prompt the user with a series of questions
    to configure pytoil interactively.
    """
    printer.warn("No pytoil config file detected!")
    interactive: bool = questionary.confirm(
        "Interactively configure pytoil?", default=False, auto_enter=False
    ).ask()

    if not interactive:
        # User doesn't want to interactively walk through a config file
        # so just make a default and exit cleanly
        Config.helper().write()
        printer.good("I made a default file for you.")
        printer.note(
            f"It's here: {defaults.CONFIG_FILE}, you can edit it with `pytoil"
            " config edit``",
            exits=0,
        )
        return

    # If we get here, the user wants to interactively make the config
    projects_dir: str = questionary.path(
        "Where do you keep your projects?",
        default=str(defaults.PROJECTS_DIR),
        only_directories=True,
    ).ask()

    token: str = questionary.text("GitHub personal access token?").ask()

    username: str = questionary.text("What's your GitHub username?").ask()

    use_editor: bool = questionary.confirm(
        "Auto open projects in an editor?", default=False, auto_enter=False
    ).ask()

    if use_editor:
        editor: str = questionary.text("Name of the editor binary to use?").ask()
    else:
        editor = "None"

    git: bool = questionary.confirm(
        "Make git repos when creating new projects?", default=True, auto_enter=False
    ).ask()

    conda_bin: str = questionary.select(
        "Use conda or mamba for conda environments?",
        choices=("conda", "mamba"),
        default="conda",
    ).ask()

    config = Config(
        projects_dir=Path(projects_dir).resolve(),
        token=token,
        username=username,
        editor=editor,
        conda_bin=conda_bin,
        git=git,
    )

    config.write()

    printer.good("Config created")
    printer.note(f"It's available at {defaults.CONFIG_FILE}.", exits=0)
Esempio n. 13
0
def test_from_file_raises_on_missing_file():

    with pytest.raises(FileNotFoundError):
        Config.load(path=Path("not/here.toml"))
Esempio n. 14
0
def test_specifies_editor(editor: str, want: bool):

    config = Config(editor=editor)
    assert config.specifies_editor() is want
Esempio n. 15
0
def test_can_use_api(username, token, expected):

    config = Config(username=username, token=token)

    assert config.can_use_api() is expected