def test_install_self_creates_venv_if_not_one_already(mocker: MockerFixture, silent: bool, stdout, stderr): mock = mocker.patch( "pytoil.environments.virtualenv.subprocess.run", autospec=True, ) # Make it think there isn't a venv mocker.patch( "pytoil.environments.virtualenv.Venv.exists", autospec=True, return_value=False, ) # Mock out the venv.create method mock_create = mocker.patch("pytoil.environments.virtualenv.Venv.create", autospec=True) venv = Venv(root=Path("somewhere")) venv.install_self(silent=silent) mock_create.assert_called_once() mock.assert_called_once_with( [f"{venv.executable}", "-m", "pip", "install", "-e", ".[dev]"], cwd=venv.project_path, stdout=stdout, stderr=stderr, )
def test_install_calls_pip_correctly(mocker: MockerFixture, silent: bool, stdout, stderr): mock = mocker.patch( "pytoil.environments.virtualenv.subprocess.run", autospec=True, ) venv = Venv(root=Path("somewhere")) venv.install(["black", "mypy", "isort", "flake8"], silent=silent) mock.assert_called_once_with( [ f"{venv.executable}", "-m", "pip", "install", "black", "mypy", "isort", "flake8", ], cwd=venv.project_path, stdout=stdout, stderr=stderr, )
def test_venv_create(): with tempfile.TemporaryDirectory("w") as tmp: tmp_path = Path(tmp).resolve() venv = Venv(tmp_path) venv.create(silent=True) assert tmp_path.joinpath(".venv").exists() assert tmp_path.joinpath(".venv/pyvenv.cfg").exists()
def test_exists_returns_correct_value(mocker: MockerFixture, exists_return, exists: bool): # Ensure Path.exists returns what we want it to mocker.patch( "pytoil.environments.virtualenv.Path.exists", autospec=True, return_value=exists_return, ) venv = Venv(root=Path("somewhere")) assert venv.exists() is exists
def test_virtualenv(): venv = Venv(root=Path("somewhere")) assert venv.project_path == Path("somewhere").resolve() assert venv.executable == Path("somewhere").resolve().joinpath( ".venv/bin/python") assert venv.name == "venv"
def test_install_self_calls_pip_correctly(mocker: MockerFixture, silent: bool, stdout, stderr): mock = mocker.patch( "pytoil.environments.virtualenv.subprocess.run", autospec=True, ) # Make it think there's already a venv mocker.patch( "pytoil.environments.virtualenv.Venv.exists", autospec=True, return_value=True, ) venv = Venv(root=Path("somewhere")) venv.install_self(silent=silent) mock.assert_called_once_with( [f"{venv.executable}", "-m", "pip", "install", "-e", ".[dev]"], cwd=venv.project_path, stdout=stdout, stderr=stderr, )
def dispatch_env(self, config: Config) -> Environment | None: """ Returns the correct environment object for the calling `Repo`, or `None` if it cannot detect the environment. Therefore all usage should first check for `None`. Returns: Optional[Environment]: The correct environment object if it was able to detect, or `None`. """ # This is where the magic happens for automatic environment detection # and installation # Each of the environment objects below implements the `Environment` Protocol # and has an `install_self` method that does the correct thing for it's environment exists = ( self.is_conda(), self.is_requirements(), self.is_setuptools(), self.is_poetry(), self.is_flit(), ) conda, requirements, setuptools, poetry, flit = exists if conda: return Conda( root=self.local_path, environment_name=self.name, conda=config.conda_bin ) elif requirements: return Requirements(root=self.local_path) elif setuptools: return Venv(root=self.local_path) elif poetry: return Poetry(root=self.local_path) elif flit: return Flit(root=self.local_path) else: # Could not autodetect, this is handled by the CLI return None
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)
def test_virtualenv_repr(): venv = Venv(root=Path("somewhere")) assert repr(venv) == f"Venv(root={Path('somewhere')!r})"