Commit 3c26c8d0 by Sébastien Eustace Committed by GitHub

New env command and sub commands (#731)

* Add a env:info command

* Add env:use command

* Fix tests on Windows

* Fix tests

* Fix tests

* Fix tests for Python 3.5

* Use a hash in virtualenv names dependent on the project’s path

* Add tests for the `env use` command

* Fix tests

* Add a `env list` command

* Fix venvs being recreated when switching minor Python versions

* Add a `env remove` command

* Update CHANGELOG

* Update documentation
parent 41c2736d
......@@ -5,6 +5,10 @@
### Added
- Added an `export` command to export the lock file to other formats (only `requirements.txt` is currently supported).
- Added a `env info` command to get basic information about the current environment.
- Added a `env use` command to control the Python version used by the project.
- Added a `env list` command to list the virtualenvs associated with the current project.
- Added a `env remove` command to delete virtualenvs associated with the current project.
### Changed
......@@ -13,6 +17,7 @@
- The `debug:info` command has been renamed to `debug info`.
- The `debug:resolve` command has been renamed to `debug resolve`.
- The `self:update` command has been renamed to `self update`.
- Changed the way virtualenvs are stored (names now depend on the project's path).
### Fixed
......
......@@ -155,19 +155,99 @@ When you execute the `install` command (or any other "install" commands like `ad
Poetry will check if it's currently inside a virtualenv and, if not, will use an existing one
or create a brand new one for you to always work isolated from your global Python installation.
!!!note
By default, Poetry will use the currently activated Python version
to create the virtualenv for the current project.
To easily switch between Python versions, it is recommended to
use [pyenv](https://github.com/pyenv/pyenv) or similar tools.
For instance, if your project is Python 2.7 only, a standard workflow
would be:
```bash
pyenv install 2.7.15
pyenv local 2.7.15 # Activate Python 2.7 for the current project
poetry install
```
However, this might not be feasible for your system, especially Windows where `pyenv`,
is not available. To circumvent that you can use the `env use` command to tell
Poetry which Python version to use for the current project.
```bash
poetry env use /full/path/to/python
```
If you have the python executable in your `PATH` you can use it:
```bash
poetry env use python3.7
```
You can even just use the minor Python version in this case:
```bash
poetry env use 3.7
```
If you want to disable the explicitly activated virtualenv, you can use the
special `system` Python version to retrieve the default behavior:
```bash
poetry env use system
```
If you want to get basic information about the currently activated virtualenv,
you can use the `env info` command:
```bash
poetry env info
```
will output something similar to this:
```text
Virtualenv
Python: 3.7.1
Implementation: CPython
Path: /path/to/poetry/cache/virtualenvs/test-O3eWbxRl-py3.7
Valid: True
System
Platform: darwin
OS: posix
Python: /path/to/main/python
```
If you only want to know the path to the virtualenv, you can pass the `--path` option
to `env info`:
```bash
poetry env info --path
```
You can also list all the virtualenvs associated with the current virtualenv
with the `env list` command:
To create the virtualenv for the current project, Poetry will use
the currently activated Python version.
```bash
poetry env list
```
To easily switch between Python versions, it is recommended to
use [pyenv](https://github.com/pyenv/pyenv) or similar tools.
will output something like the following:
For instance, if your project is Python 2.7 only, a standard workflow
would be:
```text
test-O3eWbxRl-py2.7
test-O3eWbxRl-py3.6
test-O3eWbxRl-py3.7 (Activated)
```
Finally, you can delete existing virtualenvs by using `env remove`:
```bash
poetry env remove /full/path/to/python
poetry env remove python3.7
poetry env remove 3.7
poetry env remove test-O3eWbxRl-py3.7
```
```bash
pyenv install 2.7.15
pyenv local 2.7.15 # Activate Python 2.7 for the current project
poetry install
```
If your remove the currently activated virtualenv, it will be automatically deactivated.
......@@ -375,3 +375,112 @@ poetry export -f requirements.txt
!!!note
Only the `requirements.txt` format is currently supported.
## env
The `env` command regroups sub commands to interact with the virtualenvs
associated with a specific project.
### env use
The `env use` command tells Poetry which Python version
to use for the current project.
```bash
poetry env use /full/path/to/python
```
If you have the python executable in your `PATH` you can use it:
```bash
poetry env use python3.7
```
You can even just use the minor Python version in this case:
```bash
poetry env use 3.7
```
If you want to disable the explicitly activated virtualenv, you can use the
special `system` Python version to retrieve the default behavior:
```bash
poetry env use system
```
### env info
The `env info` command displays basic information about the currently activated virtualenv:
```bash
poetry env info
```
will output something similar to this:
```text
Virtualenv
Python: 3.7.1
Implementation: CPython
Path: /path/to/poetry/cache/virtualenvs/test-O3eWbxRl-py3.7
Valid: True
System
Platform: darwin
OS: posix
Python: /path/to/main/python
```
If you only want to know the path to the virtualenv, you can pass the `--path` option
to `env info`:
```bash
poetry env info --path
```
#### Options
* `--path`: Only display the path of the virtualenv.
### env list
The `env list` command lists all the virtualenvs associated with the current virtualenv.
```bash
poetry env list
```
will output something like the following:
```text
test-O3eWbxRl-py2.7
test-O3eWbxRl-py3.6
test-O3eWbxRl-py3.7 (Activated)
```
#### Options
* `--full-path`: Display the full path of the virtualenvs.
### env remove
The `env remove` command deletes virtualenvs associated with the current project:
```bash
poetry env remove /full/path/to/python
```
Similarly to `env use`, you can either pass `python3.7`, `3.7` or the name of
the virtualenv (as returned by `env list`):
```bash
poetry env remove python3.7
poetry env remove 3.7
poetry env remove test-O3eWbxRl-py3.7
```
!!!note
If your remove the currently activated virtualenv, it will be automatically deactivated.
......@@ -33,6 +33,8 @@ from .commands.self import SelfCommand
from .config import ApplicationConfig
from .commands.env import EnvCommand
class Application(BaseApplication):
def __init__(self):
......@@ -89,6 +91,9 @@ class Application(BaseApplication):
# Debug command
commands += [DebugCommand()]
# Env command
commands += [EnvCommand()]
# Self commands
commands += [SelfCommand()]
......
import os
import sys
from clikit.args import StringArgs
from ..command import Command
......@@ -12,54 +14,21 @@ class DebugInfoCommand(Command):
"""
def handle(self):
from ....utils.env import Env
poetry = self.poetry
env = Env.get(poetry.file.parent)
poetry_python_version = ".".join(str(s) for s in sys.version_info[:3])
self.line("")
self.line("<b>Poetry</b>")
self.line("")
self.line("<info>Version</info>: <comment>{}</>".format(poetry.VERSION))
self.line("<info>Python</info>: <comment>{}</>".format(poetry_python_version))
self.line("")
env_python_version = ".".join(str(s) for s in env.version_info[:3])
self.line("<b>Virtualenv</b>")
self.line("")
listing = [
"<info>Python</info>: <comment>{}</>".format(env_python_version),
"<info>Implementation</info>: <comment>{}</>".format(
env.python_implementation
),
"<info>Path</info>: <comment>{}</>".format(
env.path if env.is_venv() else "NA"
self.line(
"\n".join(
[
"<info>Version</info>: <comment>{}</>".format(self.poetry.VERSION),
"<info>Python</info>: <comment>{}</>".format(
poetry_python_version
),
]
if env.is_venv():
listing.append(
"<info>Valid</info>: <{tag}>{is_valid}</{tag}>".format(
tag="comment" if env.is_sane() else "error", is_valid=env.is_sane()
)
)
args = StringArgs("")
command = self.application.get_command("env").get_sub_command("info")
for line in listing:
self.line(line)
self.line("")
self.line("<b>System</b>")
self.line("")
listing = [
"<info>Platform</info>: <comment>{}</>".format(sys.platform),
"<info>OS</info>: <comment>{}</>".format(os.name),
"<info>Python</info>: <comment>{}</>".format(env.base),
]
for line in listing:
self.line(line)
self.line("")
return command.run(args, self._io)
......@@ -24,7 +24,7 @@ class DebugResolveCommand(Command):
from poetry.puzzle import Solver
from poetry.repositories.repository import Repository
from poetry.semver import parse_constraint
from poetry.utils.env import Env
from poetry.utils.env import EnvManager
packages = self.argument("package")
......@@ -78,7 +78,7 @@ class DebugResolveCommand(Command):
return 0
env = Env.get(self.poetry.file.parent)
env = EnvManager(self.poetry.config).get(self.poetry.file.parent)
current_python_version = parse_constraint(
".".join(str(v) for v in env.version_info)
)
......
from ..command import Command
from .info import EnvInfoCommand
from .list import EnvListCommand
from .remove import EnvRemoveCommand
from .use import EnvUseCommand
class EnvCommand(Command):
"""
Interact with Poetry's project environments.
env
"""
commands = [EnvInfoCommand(), EnvListCommand(), EnvRemoveCommand(), EnvUseCommand()]
def handle(self): # type: () -> int
return self.call("help", self._config.name)
from ..command import Command
class EnvInfoCommand(Command):
"""
Display information about the current environment.
info
{--p|path : Only display the environment's path}
"""
def handle(self):
from poetry.utils.env import EnvManager
poetry = self.poetry
env = EnvManager(poetry.config).get(cwd=poetry.file.parent)
if self.option("path"):
if not env.is_venv():
return 1
self.write(str(env.path))
return
self._display_complete_info(env)
def _display_complete_info(self, env):
env_python_version = ".".join(str(s) for s in env.version_info[:3])
self.line("")
self.line("<b>Virtualenv</b>")
listing = [
"<info>Python</info>: <comment>{}</>".format(env_python_version),
"<info>Implementation</info>: <comment>{}</>".format(
env.python_implementation
),
"<info>Path</info>: <comment>{}</>".format(
env.path if env.is_venv() else "NA"
),
]
if env.is_venv():
listing.append(
"<info>Valid</info>: <{tag}>{is_valid}</{tag}>".format(
tag="comment" if env.is_sane() else "error", is_valid=env.is_sane()
)
)
self.line("\n".join(listing))
self.line("")
self.line("<b>System</b>")
self.line(
"\n".join(
[
"<info>Platform</info>: <comment>{}</>".format(env.platform),
"<info>OS</info>: <comment>{}</>".format(env.os),
"<info>Python</info>: <comment>{}</>".format(env.base),
]
)
)
from ..command import Command
class EnvListCommand(Command):
"""
List all virtualenvs associated with the current project.
list
{--full-path : Output the full paths of the virtualenvs}
"""
def handle(self):
from poetry.utils.env import EnvManager
poetry = self.poetry
manager = EnvManager(poetry.config)
current_env = manager.get(self.poetry.file.parent)
for venv in manager.list(self.poetry.file.parent):
name = venv.path.name
if self.option("full-path"):
name = str(venv.path)
if venv == current_env:
self.line("<info>{} (Activated)</info>".format(name))
continue
self.line(name)
from ..command import Command
class EnvRemoveCommand(Command):
"""
Remove a specific virtualenv associated with the project.
remove
{python : The python executable to remove the virtualenv for.}
"""
def handle(self):
from poetry.utils.env import EnvManager
poetry = self.poetry
manager = EnvManager(poetry.config)
venv = manager.remove(self.argument("python"), poetry.file.parent)
self.line("Deleted virtualenv: <comment>{}</comment>".format(venv.path))
from ..command import Command
class EnvUseCommand(Command):
"""
Activate or create a new virtualenv for the current project.
use
{python : The python executable to use.}
"""
def handle(self):
from poetry.utils.env import EnvManager
poetry = self.poetry
manager = EnvManager(poetry.config)
if self.argument("python") == "system":
manager.deactivate(poetry.file.parent, self._io)
return
env = manager.activate(self.argument("python"), poetry.file.parent, self._io)
self.line("Using virtualenv: <comment>{}</>".format(env.path))
......@@ -37,7 +37,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
def handle(self):
from poetry.layouts import layout
from poetry.utils._compat import Path
from poetry.utils.env import Env
from poetry.utils.env import EnvManager
from poetry.vcs.git import GitConfig
if (Path.cwd() / "pyproject.toml").exists():
......@@ -100,7 +100,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
question.set_validator(self._validate_license)
license = self.ask(question)
current_env = Env.get(Path.cwd())
current_env = EnvManager().get(Path.cwd())
default_python = "^{}".format(
".".join(str(v) for v in current_env.version_info[:2])
)
......
......@@ -14,7 +14,7 @@ class NewCommand(Command):
def handle(self):
from poetry.layouts import layout
from poetry.utils._compat import Path
from poetry.utils.env import Env
from poetry.utils.env import EnvManager
from poetry.vcs.git import GitConfig
if self.option("src"):
......@@ -45,7 +45,7 @@ class NewCommand(Command):
if author_email:
author += " <{}>".format(author_email)
current_env = Env.get(Path.cwd())
current_env = EnvManager().get(Path.cwd())
default_python = "^{}".format(
".".join(str(v) for v in current_env.version_info[:2])
)
......
......@@ -57,18 +57,20 @@ class ApplicationConfig(BaseApplicationConfig):
def set_env(self, event, event_name, _): # type: (PreHandleEvent, str, ...) -> None
from poetry.semver import parse_constraint
from poetry.utils.env import Env
from poetry.utils.env import EnvManager
command = event.command.config.handler
command = event.command.config.handler # type: EnvCommand
if not isinstance(command, EnvCommand):
return
io = event.io
poetry = command.poetry
env_manager = EnvManager(poetry.config)
# Checking compatibility of the current environment with
# the python dependency specified in pyproject.toml
current_env = Env.get(poetry.file.parent)
current_env = env_manager.get(poetry.file.parent)
supported_python = poetry.package.python_constraint
current_python = parse_constraint(
".".join(str(v) for v in current_env.version_info[:3])
......@@ -82,7 +84,7 @@ class ApplicationConfig(BaseApplicationConfig):
)
)
env = Env.create_venv(poetry.file.parent, io, poetry.package.name)
env = env_manager.create_venv(poetry.file.parent, io, poetry.package.name)
if env.is_venv() and io.is_verbose():
io.write_line("Using virtualenv: <comment>{}</>".format(env.path))
......
......@@ -30,7 +30,7 @@ from poetry.utils._compat import PY35
from poetry.utils._compat import Path
from poetry.utils.helpers import parse_requires
from poetry.utils.helpers import safe_rmtree
from poetry.utils.env import Env
from poetry.utils.env import EnvManager
from poetry.utils.env import EnvCommandError
from poetry.utils.setup_reader import SetupReader
......@@ -260,7 +260,7 @@ class Provider:
try:
cwd = dependency.full_path
venv = Env.get(cwd)
venv = EnvManager().get(cwd)
venv.run("python", "setup.py", "egg_info")
except EnvCommandError:
result = SetupReader.read_from_directory(dependency.full_path)
......
import base64
import hashlib
import json
import os
import platform
import re
import shutil
import subprocess
import sys
import sysconfig
import warnings
import tomlkit
from contextlib import contextmanager
from subprocess import CalledProcessError
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
......@@ -17,10 +24,12 @@ from clikit.api.io import IO
from poetry.config import Config
from poetry.locations import CACHE_DIR
from poetry.semver import Version
from poetry.utils._compat import Path
from poetry.utils._compat import decode
from poetry.utils._compat import encode
from poetry.utils._compat import list_to_shell_command
from poetry.utils.toml_file import TomlFile
from poetry.version.markers import BaseMarker
......@@ -84,6 +93,22 @@ import sys
print('.'.join([str(s) for s in sys.version_info[:3]]))
"""
CREATE_VENV_COMMAND = """\
path = {!r}
try:
from venv import EnvBuilder
builder = EnvBuilder(with_pip=True)
build = builder.create
except ImportError:
# We fallback on virtualenv for Python 2.7
from virtualenv import create_environment
build = create_environment
build(path)"""
class EnvError(Exception):
......@@ -99,92 +124,165 @@ class EnvCommandError(EnvError):
super(EnvCommandError, self).__init__(message)
class Env(object):
class EnvManager(object):
"""
An abstract Python environment.
Environments manager
"""
_env = None
def __init__(self, path, base=None): # type: (Path, Optional[Path]) -> None
self._is_windows = sys.platform == "win32"
ENVS_FILE = "envs.toml"
self._path = path
bin_dir = "bin" if not self._is_windows else "Scripts"
self._bin_dir = self._path / bin_dir
def __init__(self, config=None): # type: (Config) -> None
if config is None:
config = Config.create("config.toml")
self._base = base or path
self._config = config
self._marker_env = None
def activate(self, python, cwd, io): # type: (str, Optional[Path], IO) -> Env
venv_path = self._config.setting("settings.virtualenvs.path")
if venv_path is None:
venv_path = Path(CACHE_DIR) / "virtualenvs"
else:
venv_path = Path(venv_path)
@property
def path(self): # type: () -> Path
return self._path
envs_file = TomlFile(venv_path / self.ENVS_FILE)
@property
def base(self): # type: () -> Path
return self._base
try:
python_version = Version.parse(python)
python = "python{}".format(python_version.major)
if python_version.precision > 1:
python += ".{}".format(python_version.minor)
except ValueError:
# Executable in PATH or full executable path
pass
@property
def version_info(self): # type: () -> Tuple[int]
return tuple(self.marker_env["version_info"])
try:
python_version = decode(
subprocess.check_output(
" ".join(
[
python,
"-c",
"\"import sys; print('.'.join([str(s) for s in sys.version_info[:3]]))\"",
]
),
shell=True,
)
)
except CalledProcessError as e:
raise EnvCommandError(e)
@property
def python_implementation(self): # type: () -> str
return self.marker_env["platform_python_implementation"]
python_version = Version.parse(python_version.strip())
minor = "{}.{}".format(python_version.major, python_version.minor)
patch = python_version.text
create = False
envs = tomlkit.document()
base_env_name = self.generate_env_name(cwd.name, str(cwd))
if envs_file.exists():
envs = envs_file.read()
current_env = envs.get(base_env_name)
if current_env is not None:
current_minor = current_env["minor"]
current_patch = current_env["patch"]
if current_minor == minor and current_patch != patch:
# We need to recreate
create = True
name = "{}-py{}".format(base_env_name, minor)
venv = venv_path / name
@property
def python(self): # type: () -> str
"""
Path to current python executable
"""
return self._bin("python")
# Create if needed
if not venv.exists() or venv.exists() and create:
in_venv = os.environ.get("VIRTUAL_ENV") is not None
if in_venv or not venv.exists():
create = True
@property
def marker_env(self):
if self._marker_env is None:
self._marker_env = self.get_marker_env()
if venv.exists():
# We need to check if the patch version is correct
_venv = VirtualEnv(venv)
current_patch = ".".join(str(v) for v in _venv.version_info[:3])
return self._marker_env
if patch != current_patch:
create = True
@property
def pip(self): # type: () -> str
"""
Path to current pip executable
"""
return self._bin("pip")
self.create_venv(cwd, io, executable=python, force=create)
@classmethod
def get(cls, cwd, reload=False): # type: (Path, bool) -> Env
if cls._env is not None and not reload:
return cls._env
# Activate
envs[base_env_name] = {"minor": minor, "patch": patch}
envs_file.write(envs)
return self.get(cwd, reload=True)
def deactivate(self, cwd, io): # type: (Optional[Path], IO) -> None
venv_path = self._config.setting("settings.virtualenvs.path")
if venv_path is None:
venv_path = Path(CACHE_DIR) / "virtualenvs"
else:
venv_path = Path(venv_path)
name = cwd.name
name = self.generate_env_name(name, str(cwd))
envs_file = TomlFile(venv_path / self.ENVS_FILE)
if envs_file.exists():
envs = envs_file.read()
env = envs.get(name)
if env is not None:
io.write_line(
"Deactivating virtualenv: <comment>{}</comment>".format(
venv_path / (name + "-py{}".format(env["minor"]))
)
)
del envs[name]
envs_file.write(envs)
def get(self, cwd, reload=False): # type: (Path, bool) -> Env
if self._env is not None and not reload:
return self._env
python_minor = ".".join([str(v) for v in sys.version_info[:2]])
venv_path = self._config.setting("settings.virtualenvs.path")
if venv_path is None:
venv_path = Path(CACHE_DIR) / "virtualenvs"
else:
venv_path = Path(venv_path)
envs_file = TomlFile(venv_path / self.ENVS_FILE)
env = None
base_env_name = self.generate_env_name(cwd.name, str(cwd))
if envs_file.exists():
envs = envs_file.read()
env = envs.get(base_env_name)
if env:
python_minor = env["minor"]
# Check if we are inside a virtualenv or not
in_venv = os.environ.get("VIRTUAL_ENV") is not None
if not in_venv:
if not in_venv or env is not None:
# Checking if a local virtualenv exists
if (cwd / ".venv").exists():
venv = cwd / ".venv"
return VirtualEnv(venv)
config = Config.create("config.toml")
create_venv = config.setting("settings.virtualenvs.create", True)
create_venv = self._config.setting("settings.virtualenvs.create", True)
if not create_venv:
return SystemEnv(Path(sys.prefix))
venv_path = config.setting("settings.virtualenvs.path")
venv_path = self._config.setting("settings.virtualenvs.path")
if venv_path is None:
venv_path = Path(CACHE_DIR) / "virtualenvs"
else:
venv_path = Path(venv_path)
name = cwd.name
name = "{}-py{}".format(
name, ".".join([str(v) for v in sys.version_info[:2]])
)
name = "{}-py{}".format(base_env_name, python_minor.strip())
venv = venv_path / name
......@@ -198,26 +296,134 @@ class Env(object):
base_prefix = None
else:
prefix = Path(sys.prefix)
base_prefix = cls.get_base_prefix()
base_prefix = self.get_base_prefix()
return VirtualEnv(prefix, base_prefix)
@classmethod
def create_venv(cls, cwd, io, name=None): # type: (Path, IO, bool) -> Env
if cls._env is not None:
return cls._env
def list(self, cwd, name=None): # type: (Path, Optional[str]) -> List[VirtualEnv]
if name is None:
name = cwd.name
venv_name = self.generate_env_name(name, str(cwd))
venv_path = self._config.setting("settings.virtualenvs.path")
if venv_path is None:
venv_path = Path(CACHE_DIR) / "virtualenvs"
else:
venv_path = Path(venv_path)
return [
VirtualEnv(Path(p))
for p in sorted(venv_path.glob("{}-py*".format(venv_name)))
]
def remove(self, python, cwd): # type: (str, Optional[Path]) -> Env
venv_path = self._config.setting("settings.virtualenvs.path")
if venv_path is None:
venv_path = Path(CACHE_DIR) / "virtualenvs"
else:
venv_path = Path(venv_path)
envs_file = TomlFile(venv_path / self.ENVS_FILE)
base_env_name = self.generate_env_name(cwd.name, str(cwd))
if python.startswith(base_env_name):
venvs = self.list(cwd)
for venv in venvs:
if venv.path.name == python:
# Exact virtualenv name
if not envs_file.exists():
self.remove_venv(str(venv.path))
return venv
venv_minor = ".".join(str(v) for v in venv.version_info[:2])
base_env_name = self.generate_env_name(cwd.name, str(cwd))
envs = envs_file.read()
current_env = envs.get(base_env_name)
if not current_env:
self.remove_venv(str(venv.path))
return venv
if current_env["minor"] == venv_minor:
del envs[base_env_name]
envs_file.write(envs)
self.remove_venv(str(venv.path))
return venv
env = cls.get(cwd)
if env.is_venv():
raise ValueError(
'<warning>Environment "{}" does not exist.</warning>'.format(python)
)
try:
python_version = Version.parse(python)
python = "python{}".format(python_version.major)
if python_version.precision > 1:
python += ".{}".format(python_version.minor)
except ValueError:
# Executable in PATH or full executable path
pass
try:
python_version = decode(
subprocess.check_output(
" ".join(
[
python,
"-c",
"\"import sys; print('.'.join([str(s) for s in sys.version_info[:3]]))\"",
]
),
shell=True,
)
)
except CalledProcessError as e:
raise EnvCommandError(e)
python_version = Version.parse(python_version.strip())
minor = "{}.{}".format(python_version.major, python_version.minor)
name = "{}-py{}".format(base_env_name, minor)
venv = venv_path / name
if not venv.exists():
raise ValueError(
'<warning>Environment "{}" does not exist.</warning>'.format(name)
)
if envs_file.exists():
envs = envs_file.read()
current_env = envs.get(base_env_name)
if current_env is not None:
current_minor = current_env["minor"]
if current_minor == minor:
del envs[base_env_name]
envs_file.write(envs)
self.remove_venv(str(venv))
return VirtualEnv(venv)
def create_venv(
self, cwd, io, name=None, executable=None, force=False
): # type: (Path, IO, Optional[str], Optional[str], bool) -> Env
if self._env is not None and not force:
return self._env
env = self.get(cwd, reload=True)
if env.is_venv() and not force:
# Already inside a virtualenv.
return env
config = Config.create("config.toml")
create_venv = config.setting("settings.virtualenvs.create")
root_venv = config.setting("settings.virtualenvs.in-project")
create_venv = self._config.setting("settings.virtualenvs.create")
root_venv = self._config.setting("settings.virtualenvs.in-project")
venv_path = config.setting("settings.virtualenvs.path")
venv_path = self._config.setting("settings.virtualenvs.path")
if root_venv:
venv_path = cwd / ".venv"
elif venv_path is None:
......@@ -228,11 +434,26 @@ class Env(object):
if not name:
name = cwd.name
name = "{}-py{}".format(name, ".".join([str(v) for v in sys.version_info[:2]]))
python_minor = ".".join([str(v) for v in sys.version_info[:2]])
if executable:
python_minor = decode(
subprocess.check_output(
" ".join(
[
executable,
"-c",
"\"import sys; print('.'.join([str(s) for s in sys.version_info[:2]]))\"",
]
),
shell=True,
)
)
if root_venv:
venv = venv_path
else:
name = self.generate_env_name(name, str(cwd))
name = "{}-py{}".format(name, python_minor.strip())
venv = venv_path / name
if not venv.exists():
......@@ -250,9 +471,15 @@ class Env(object):
"Creating virtualenv <info>{}</> in {}".format(name, str(venv_path))
)
cls.build_venv(str(venv))
self.build_venv(str(venv), executable=executable)
else:
if io.is_very_verbose():
if force:
io.write_line(
"Recreating virtualenv <info>{}</> in {}".format(name, str(venv))
)
self.remove_venv(str(venv))
self.build_venv(str(venv), executable=executable)
elif io.is_very_verbose():
io.write_line("Virtualenv <info>{}</> already exists.".format(name))
# venv detection:
......@@ -269,12 +496,23 @@ class Env(object):
p_venv = os.path.normcase(str(venv))
if any(p.startswith(p_venv) for p in paths):
# Running properly in the virtualenv, don't need to do anything
return SystemEnv(Path(sys.prefix), cls.get_base_prefix())
return SystemEnv(Path(sys.prefix), self.get_base_prefix())
return VirtualEnv(venv)
@classmethod
def build_venv(cls, path):
def build_venv(self, path, executable=None):
if executable is not None:
# Create virtualenv by using an external executable
try:
p = subprocess.Popen(
" ".join([executable, "-"]), stdin=subprocess.PIPE, shell=True
)
p.communicate(encode(CREATE_VENV_COMMAND.format(path)))
except CalledProcessError as e:
raise EnvCommandError(e)
return
try:
from venv import EnvBuilder
......@@ -288,6 +526,89 @@ class Env(object):
build(path)
def remove_venv(self, path): # type: (str) -> None
shutil.rmtree(path)
def get_base_prefix(self): # type: () -> Path
if hasattr(sys, "real_prefix"):
return sys.real_prefix
if hasattr(sys, "base_prefix"):
return sys.base_prefix
return sys.prefix
@classmethod
def generate_env_name(cls, name, cwd): # type: (str, str) -> str
name = name.lower()
sanitized_name = re.sub(r'[ $`!*@"\\\r\n\t]', "_", name)[:42]
h = hashlib.sha256(encode(cwd)).digest()
h = base64.urlsafe_b64encode(h).decode()[:8]
return "{}-{}".format(sanitized_name, h)
class Env(object):
"""
An abstract Python environment.
"""
def __init__(self, path, base=None): # type: (Path, Optional[Path]) -> None
self._is_windows = sys.platform == "win32"
self._path = path
bin_dir = "bin" if not self._is_windows else "Scripts"
self._bin_dir = self._path / bin_dir
self._base = base or path
self._marker_env = None
@property
def path(self): # type: () -> Path
return self._path
@property
def base(self): # type: () -> Path
return self._base
@property
def version_info(self): # type: () -> Tuple[int]
return tuple(self.marker_env["version_info"])
@property
def python_implementation(self): # type: () -> str
return self.marker_env["platform_python_implementation"]
@property
def python(self): # type: () -> str
"""
Path to current python executable
"""
return self._bin("python")
@property
def marker_env(self):
if self._marker_env is None:
self._marker_env = self.get_marker_env()
return self._marker_env
@property
def pip(self): # type: () -> str
"""
Path to current pip executable
"""
return self._bin("pip")
@property
def platform(self): # type: () -> str
return sys.platform
@property
def os(self): # type: () -> str
return os.name
@classmethod
def get_base_prefix(cls): # type: () -> Path
if hasattr(sys, "real_prefix"):
......@@ -375,6 +696,9 @@ class Env(object):
return str(bin_path)
def __eq__(self, other): # type: (Env) -> bool
return other.__class__ == self.__class__ and other.path == self.path
def __repr__(self):
return '{}("{}")'.format(self.__class__.__name__, self._path)
......
......@@ -47,12 +47,22 @@ def mock_clone(_, source, dest):
@pytest.fixture
def tmp_dir():
dir_ = tempfile.mkdtemp(prefix="poetry_")
yield dir_
shutil.rmtree(dir_)
@pytest.fixture
def environ():
original_environ = os.environ
original_environ = dict(os.environ)
yield os.environ
yield
os.environ = original_environ
os.environ.clear()
os.environ.update(original_environ)
@pytest.fixture(autouse=True)
......
import pytest
from cleo.testers import CommandTester
from poetry.utils._compat import Path
from poetry.utils.env import MockEnv
@pytest.fixture(autouse=True)
def setup(mocker):
mocker.patch(
"poetry.utils.env.EnvManager.get",
return_value=MockEnv(
path=Path("/prefix"), base=Path("/base/prefix"), is_venv=True
),
)
def test_env_info_displays_complete_info(app):
command = app.find("env info")
tester = CommandTester(command)
tester.execute()
expected = """
Virtualenv
Python: 3.7.0
Implementation: CPython
Path: {prefix}
Valid: True
System
Platform: darwin
OS: posix
Python: {base_prefix}
""".format(
prefix=str(Path("/prefix")), base_prefix=str(Path("/base/prefix"))
)
assert expected == tester.io.fetch_output()
def test_env_info_displays_path_only(app):
command = app.find("env info")
tester = CommandTester(command)
tester.execute("--path")
expected = str(Path("/prefix"))
assert expected == tester.io.fetch_output()
import tomlkit
from cleo.testers import CommandTester
from poetry.utils._compat import Path
from poetry.utils.env import EnvManager
from poetry.utils.toml_file import TomlFile
def test_none_activated(app, tmp_dir, config):
app.poetry._config = config
config.add_property("settings.virtualenvs.path", str(tmp_dir))
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
)
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
command = app.find("env list")
tester = CommandTester(command)
tester.execute()
expected = """\
{}-py3.6
{}-py3.7
""".format(
venv_name, venv_name
)
assert expected == tester.io.fetch_output()
def test_activated(app, tmp_dir, config):
app.poetry._config = config
config.add_property("settings.virtualenvs.path", str(tmp_dir))
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
)
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
doc = tomlkit.document()
doc[venv_name] = {"minor": "3.7", "patch": "3.7.0"}
envs_file.write(doc)
command = app.find("env list")
tester = CommandTester(command)
tester.execute()
expected = """\
{}-py3.6
{}-py3.7 (Activated)
""".format(
venv_name, venv_name
)
assert expected == tester.io.fetch_output()
from cleo.testers import CommandTester
from poetry.utils._compat import Path
from poetry.utils.env import EnvManager
from .test_use import Version
from .test_use import check_output_wrapper
def test_remove_by_python_version(app, tmp_dir, config, mocker):
app.poetry._config = config
config.add_property("settings.virtualenvs.path", str(tmp_dir))
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
)
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
mocker.patch(
"subprocess.check_output",
side_effect=check_output_wrapper(Version.parse("3.6.6")),
)
command = app.find("env remove")
tester = CommandTester(command)
tester.execute("3.6")
assert not (Path(tmp_dir) / "{}-py3.6".format(venv_name)).exists()
expected = "Deleted virtualenv: {}\n".format(
(Path(tmp_dir) / "{}-py3.6".format(venv_name))
)
assert expected == tester.io.fetch_output()
def test_remove_by_name(app, tmp_dir, config, mocker):
app.poetry._config = config
config.add_property("settings.virtualenvs.path", str(tmp_dir))
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
)
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
mocker.patch(
"subprocess.check_output",
side_effect=check_output_wrapper(Version.parse("3.6.6")),
)
command = app.find("env remove")
tester = CommandTester(command)
tester.execute("{}-py3.6".format(venv_name))
assert not (Path(tmp_dir) / "{}-py3.6".format(venv_name)).exists()
expected = "Deleted virtualenv: {}\n".format(
(Path(tmp_dir) / "{}-py3.6".format(venv_name))
)
assert expected == tester.io.fetch_output()
import os
import shutil
import sys
import tomlkit
from cleo.testers import CommandTester
from poetry.semver import Version
from poetry.utils._compat import Path
from poetry.utils.env import EnvManager
from poetry.utils.env import MockEnv
from poetry.utils.toml_file import TomlFile
CWD = Path(__file__).parent.parent / "fixtures" / "simple_project"
def build_venv(path, executable=None):
os.mkdir(path)
def remove_venv(path):
shutil.rmtree(path)
def check_output_wrapper(version=Version.parse("3.7.1")):
def check_output(cmd, *args, **kwargs):
if "sys.version_info[:3]" in cmd:
return version.text
elif "sys.version_info[:2]" in cmd:
return "{}.{}".format(version.major, version.minor)
else:
return str(Path("/prefix"))
return check_output
def test_activate_activates_non_existing_virtualenv_no_envs_file(
app, tmp_dir, config, mocker
):
app.poetry._config = config
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
config.add_property("settings.virtualenvs.path", str(tmp_dir))
mocker.patch("subprocess.check_output", side_effect=check_output_wrapper())
mocker.patch(
"subprocess.Popen.communicate",
side_effect=[("/prefix", None), ("/prefix", None)],
)
m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv)
command = app.find("env use")
tester = CommandTester(command)
tester.execute("3.7")
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
)
m.assert_called_with(
os.path.join(tmp_dir, "{}-py3.7".format(venv_name)), executable="python3.7"
)
envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
assert envs_file.exists()
envs = envs_file.read()
assert envs[venv_name]["minor"] == "3.7"
assert envs[venv_name]["patch"] == "3.7.1"
expected = """\
Creating virtualenv {} in {}
Using virtualenv: {}
""".format(
"{}-py3.7".format(venv_name),
tmp_dir,
os.path.join(tmp_dir, "{}-py3.7".format(venv_name)),
)
assert expected == tester.io.fetch_output()
def test_get_prefers_explicitly_activated_virtualenvs_over_env_var(
app, tmp_dir, config, mocker
):
app.poetry._config = config
os.environ["VIRTUAL_ENV"] = "/environment/prefix"
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
)
current_python = sys.version_info[:3]
python_minor = ".".join(str(v) for v in current_python[:2])
python_patch = ".".join(str(v) for v in current_python)
config.add_property("settings.virtualenvs.path", str(tmp_dir))
(Path(tmp_dir) / "{}-py{}".format(venv_name, python_minor)).mkdir()
envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
doc = tomlkit.document()
doc[venv_name] = {"minor": python_minor, "patch": python_patch}
envs_file.write(doc)
mocker.patch(
"subprocess.check_output",
side_effect=check_output_wrapper(Version(*current_python)),
)
mocker.patch(
"subprocess.Popen.communicate",
side_effect=[("/prefix", None), ("/prefix", None), ("/prefix", None)],
)
command = app.find("env use")
tester = CommandTester(command)
tester.execute(python_minor)
expected = """\
Using virtualenv: {}
""".format(
os.path.join(tmp_dir, "{}-py{}".format(venv_name, python_minor))
)
assert expected == tester.io.fetch_output()
def test_get_prefers_explicitly_activated_non_existing_virtualenvs_over_env_var(
app, tmp_dir, config, mocker
):
app.poetry._config = config
os.environ["VIRTUAL_ENV"] = "/environment/prefix"
venv_name = EnvManager.generate_env_name(
"simple_project", str(app.poetry.file.parent)
)
current_python = sys.version_info[:3]
python_minor = ".".join(str(v) for v in current_python[:2])
config.add_property("settings.virtualenvs.path", str(tmp_dir))
mocker.patch(
"poetry.utils.env.EnvManager._env",
new_callable=mocker.PropertyMock,
return_value=MockEnv(
path=Path("/environment/prefix"),
base=Path("/base/prefix"),
version_info=current_python,
is_venv=True,
),
)
mocker.patch(
"subprocess.check_output",
side_effect=check_output_wrapper(Version(*current_python)),
)
mocker.patch(
"subprocess.Popen.communicate",
side_effect=[("/prefix", None), ("/prefix", None), ("/prefix", None)],
)
mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv)
command = app.find("env use")
tester = CommandTester(command)
tester.execute(python_minor)
expected = """\
Creating virtualenv {} in {}
Using virtualenv: {}
""".format(
"{}-py{}".format(venv_name, python_minor),
tmp_dir,
os.path.join(tmp_dir, "{}-py{}".format(venv_name, python_minor)),
)
assert expected == tester.io.fetch_output()
......@@ -37,7 +37,7 @@ classifiers = [
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
python = "~2.7 || ^3.4"
foo = "^1.0"
"""
......
import sys
from cleo.testers import CommandTester
from poetry.utils._compat import Path
......@@ -126,9 +127,11 @@ description = ""
authors = ["Your Name <you@example.com>"]
[tool.poetry.dependencies]
python = "^3.7"
python = "^{python}"
[tool.poetry.dev-dependencies]
"""
""".format(
python=".".join(str(c) for c in sys.version_info[:2])
)
assert expected in tester.io.fetch_output()
from cleo.testers import CommandTester
from poetry.utils._compat import Path
from poetry.utils.env import MockEnv
def test_run_passes_all_args(app, mocker):
env = MockEnv(is_venv=True)
mocker.patch("poetry.utils.env.Env.get", return_value=env)
env = MockEnv(path=Path("/prefix"), base=Path("/base/prefix"), is_venv=True)
mocker.patch("poetry.utils.env.EnvManager.get", return_value=env)
command = app.find("run")
tester = CommandTester(command)
......
......@@ -17,7 +17,6 @@ from poetry.packages import Locker as BaseLocker
from poetry.repositories import Pool
from poetry.repositories import Repository as BaseRepository
from poetry.utils._compat import Path
from poetry.utils.env import MockEnv
from poetry.utils.toml_file import TomlFile
from poetry.repositories.exceptions import PackageNotFound
......@@ -49,11 +48,7 @@ def installed():
@pytest.fixture(autouse=True)
def setup(mocker, installer, installed):
mocker.patch(
"poetry.utils.env.Env.get", return_value=MockEnv(is_venv=True, execute=True)
)
def setup(mocker, installer, installed, config):
# Set Installer's installer
p = mocker.patch("poetry.installation.installer.Installer._get_installer")
p.return_value = installer
......
......@@ -20,7 +20,7 @@ classifiers = [
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
python = "~2.7 || ^3.4"
cachy = "^0.1.0"
pendulum = "^2.0.0"
......
......@@ -22,7 +22,7 @@ classifiers = [
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
python = "~2.7 || ^3.4"
# File dependency
demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" }
......
......@@ -22,4 +22,4 @@ classifiers = [
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
python = "~2.7 || ^3.4"
......@@ -81,7 +81,7 @@ def test_search_for_vcs_setup_egg_info_with_extras(provider):
@pytest.mark.skipif(not PY35, reason="AST parsing does not work for Python <3.4")
def test_search_for_vcs_read_setup(provider, mocker):
mocker.patch("poetry.utils.env.Env.get", return_value=MockEnv())
mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv())
dependency = VCSDependency("demo", "git", "https://github.com/demo/demo.git")
......@@ -98,7 +98,7 @@ def test_search_for_vcs_read_setup(provider, mocker):
@pytest.mark.skipif(not PY35, reason="AST parsing does not work for Python <3.4")
def test_search_for_vcs_read_setup_with_extras(provider, mocker):
mocker.patch("poetry.utils.env.Env.get", return_value=MockEnv())
mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv())
dependency = VCSDependency("demo", "git", "https://github.com/demo/demo.git")
dependency.extras.append("foo")
......@@ -118,7 +118,7 @@ def test_search_for_vcs_read_setup_with_extras(provider, mocker):
def test_search_for_vcs_read_setup_raises_error_if_no_version(provider, mocker):
mocker.patch("poetry.utils.env.Env.get", return_value=MockEnv())
mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv())
dependency = VCSDependency("demo", "git", "https://github.com/demo/no-version.git")
......@@ -176,7 +176,7 @@ def test_search_for_directory_setup_egg_info_with_extras(provider):
@pytest.mark.skipif(not PY35, reason="AST parsing does not work for Python <3.4")
def test_search_for_directory_setup_read_setup(provider, mocker):
mocker.patch("poetry.utils.env.Env.get", return_value=MockEnv())
mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv())
dependency = DirectoryDependency(
"demo",
......@@ -201,7 +201,7 @@ def test_search_for_directory_setup_read_setup(provider, mocker):
@pytest.mark.skipif(not PY35, reason="AST parsing does not work for Python <3.4")
def test_search_for_directory_setup_read_setup_with_extras(provider, mocker):
mocker.patch("poetry.utils.env.Env.get", return_value=MockEnv())
mocker.patch("poetry.utils.env.EnvManager.get", return_value=MockEnv())
dependency = DirectoryDependency(
"demo",
......
import os
import shutil
import sys
import tomlkit
from clikit.io import NullIO
from poetry.semver import Version
from poetry.utils._compat import Path
from poetry.utils.env import Env
from poetry.utils.env import EnvManager
from poetry.utils.env import VirtualEnv
from poetry.utils.toml_file import TomlFile
def test_virtualenvs_with_spaces_in_their_path_work_as_expected(tmp_dir):
def test_virtualenvs_with_spaces_in_their_path_work_as_expected(tmp_dir, config):
venv_path = Path(tmp_dir) / "Virtual Env"
Env.build_venv(str(venv_path))
EnvManager(config).build_venv(str(venv_path))
venv = VirtualEnv(venv_path)
assert venv.run("python", "-V", shell=True).startswith("Python")
def test_env_get_in_project_venv(tmp_dir, environ):
if "VIRTUAL_ENV" in environ:
del environ["VIRTUAL_ENV"]
def test_env_get_in_project_venv(tmp_dir, config):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
(Path(tmp_dir) / ".venv").mkdir()
venv = Env.get(cwd=Path(tmp_dir))
venv = EnvManager(config).get(Path(tmp_dir))
assert venv.path == Path(tmp_dir) / ".venv"
CWD = Path(__file__).parent.parent / "fixtures" / "simple_project"
def build_venv(path, executable=None):
os.mkdir(path)
def remove_venv(path):
shutil.rmtree(path)
def check_output_wrapper(version=Version.parse("3.7.1")):
def check_output(cmd, *args, **kwargs):
if "sys.version_info[:3]" in cmd:
return version.text
elif "sys.version_info[:2]" in cmd:
return "{}.{}".format(version.major, version.minor)
else:
return str(Path("/prefix"))
return check_output
def test_activate_activates_non_existing_virtualenv_no_envs_file(
tmp_dir, config, mocker
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
config.add_property("settings.virtualenvs.path", str(tmp_dir))
mocker.patch("subprocess.check_output", side_effect=check_output_wrapper())
mocker.patch(
"subprocess.Popen.communicate",
side_effect=[("/prefix", None), ("/prefix", None)],
)
m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv)
env = EnvManager(config).activate("python3.7", CWD, NullIO())
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
m.assert_called_with(
os.path.join(tmp_dir, "{}-py3.7".format(venv_name)), executable="python3.7"
)
envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
assert envs_file.exists()
envs = envs_file.read()
assert envs[venv_name]["minor"] == "3.7"
assert envs[venv_name]["patch"] == "3.7.1"
assert env.path == Path(tmp_dir) / "{}-py3.7".format(venv_name)
assert env.base == Path("/prefix")
def test_activate_activates_existing_virtualenv_no_envs_file(tmp_dir, config, mocker):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name)))
config.add_property("settings.virtualenvs.path", str(tmp_dir))
mocker.patch("subprocess.check_output", side_effect=check_output_wrapper())
mocker.patch("subprocess.Popen.communicate", side_effect=[("/prefix", None)])
m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv)
env = EnvManager(config).activate("python3.7", CWD, NullIO())
m.assert_not_called()
envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
assert envs_file.exists()
envs = envs_file.read()
assert envs[venv_name]["minor"] == "3.7"
assert envs[venv_name]["patch"] == "3.7.1"
assert env.path == Path(tmp_dir) / "{}-py3.7".format(venv_name)
assert env.base == Path("/prefix")
def test_activate_activates_same_virtualenv_with_envs_file(tmp_dir, config, mocker):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
doc = tomlkit.document()
doc[venv_name] = {"minor": "3.7", "patch": "3.7.1"}
envs_file.write(doc)
os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name)))
config.add_property("settings.virtualenvs.path", str(tmp_dir))
mocker.patch("subprocess.check_output", side_effect=check_output_wrapper())
mocker.patch("subprocess.Popen.communicate", side_effect=[("/prefix", None)])
m = mocker.patch("poetry.utils.env.EnvManager.create_venv")
env = EnvManager(config).activate("python3.7", CWD, NullIO())
m.assert_not_called()
assert envs_file.exists()
envs = envs_file.read()
assert envs[venv_name]["minor"] == "3.7"
assert envs[venv_name]["patch"] == "3.7.1"
assert env.path == Path(tmp_dir) / "{}-py3.7".format(venv_name)
assert env.base == Path("/prefix")
def test_activate_activates_different_virtualenv_with_envs_file(
tmp_dir, config, mocker
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
doc = tomlkit.document()
doc[venv_name] = {"minor": "3.7", "patch": "3.7.1"}
envs_file.write(doc)
os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name)))
config.add_property("settings.virtualenvs.path", str(tmp_dir))
mocker.patch(
"subprocess.check_output",
side_effect=check_output_wrapper(Version.parse("3.6.6")),
)
mocker.patch(
"subprocess.Popen.communicate",
side_effect=[("/prefix", None), ("/prefix", None), ("/prefix", None)],
)
m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv)
env = EnvManager(config).activate("python3.6", CWD, NullIO())
m.assert_called_with(
os.path.join(tmp_dir, "{}-py3.6".format(venv_name)), executable="python3.6"
)
assert envs_file.exists()
envs = envs_file.read()
assert envs[venv_name]["minor"] == "3.6"
assert envs[venv_name]["patch"] == "3.6.6"
assert env.path == Path(tmp_dir) / "{}-py3.6".format(venv_name)
assert env.base == Path("/prefix")
def test_activate_activates_recreates_for_different_patch(tmp_dir, config, mocker):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
doc = tomlkit.document()
doc[venv_name] = {"minor": "3.7", "patch": "3.7.0"}
envs_file.write(doc)
os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name)))
config.add_property("settings.virtualenvs.path", str(tmp_dir))
mocker.patch("subprocess.check_output", side_effect=check_output_wrapper())
mocker.patch(
"subprocess.Popen.communicate",
side_effect=[
("/prefix", None),
('{"version_info": [3, 7, 0]}', None),
("/prefix", None),
("/prefix", None),
("/prefix", None),
],
)
build_venv_m = mocker.patch(
"poetry.utils.env.EnvManager.build_venv", side_effect=build_venv
)
remove_venv_m = mocker.patch(
"poetry.utils.env.EnvManager.remove_venv", side_effect=remove_venv
)
env = EnvManager(config).activate("python3.7", CWD, NullIO())
build_venv_m.assert_called_with(
os.path.join(tmp_dir, "{}-py3.7".format(venv_name)), executable="python3.7"
)
remove_venv_m.assert_called_with(
os.path.join(tmp_dir, "{}-py3.7".format(venv_name))
)
assert envs_file.exists()
envs = envs_file.read()
assert envs[venv_name]["minor"] == "3.7"
assert envs[venv_name]["patch"] == "3.7.1"
assert env.path == Path(tmp_dir) / "{}-py3.7".format(venv_name)
assert env.base == Path("/prefix")
assert (Path(tmp_dir) / "{}-py3.7".format(venv_name)).exists()
def test_activate_does_not_recreate_when_switching_minor(tmp_dir, config, mocker):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
doc = tomlkit.document()
doc[venv_name] = {"minor": "3.7", "patch": "3.7.0"}
envs_file.write(doc)
os.mkdir(os.path.join(tmp_dir, "{}-py3.7".format(venv_name)))
os.mkdir(os.path.join(tmp_dir, "{}-py3.6".format(venv_name)))
config.add_property("settings.virtualenvs.path", str(tmp_dir))
mocker.patch(
"subprocess.check_output",
side_effect=check_output_wrapper(Version.parse("3.6.6")),
)
mocker.patch(
"subprocess.Popen.communicate",
side_effect=[("/prefix", None), ("/prefix", None), ("/prefix", None)],
)
build_venv_m = mocker.patch(
"poetry.utils.env.EnvManager.build_venv", side_effect=build_venv
)
remove_venv_m = mocker.patch(
"poetry.utils.env.EnvManager.remove_venv", side_effect=remove_venv
)
env = EnvManager(config).activate("python3.6", CWD, NullIO())
build_venv_m.assert_not_called()
remove_venv_m.assert_not_called()
assert envs_file.exists()
envs = envs_file.read()
assert envs[venv_name]["minor"] == "3.6"
assert envs[venv_name]["patch"] == "3.6.6"
assert env.path == Path(tmp_dir) / "{}-py3.6".format(venv_name)
assert env.base == Path("/prefix")
assert (Path(tmp_dir) / "{}-py3.6".format(venv_name)).exists()
def test_deactivate_non_activated_but_existing(tmp_dir, config, mocker):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
(
Path(tmp_dir)
/ "{}-py{}".format(venv_name, ".".join(str(c) for c in sys.version_info[:2]))
).mkdir()
config.add_property("settings.virtualenvs.path", str(tmp_dir))
mocker.patch("subprocess.check_output", side_effect=check_output_wrapper())
EnvManager(config).deactivate(CWD, NullIO())
env = EnvManager(config).get(CWD)
assert env.path == Path(tmp_dir) / "{}-py{}".format(
venv_name, ".".join(str(c) for c in sys.version_info[:2])
)
assert Path("/prefix")
def test_deactivate_activated(tmp_dir, config, mocker):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
version = Version.parse(".".join(str(c) for c in sys.version_info[:3]))
other_version = Version.parse("3.4") if version.major == 2 else version.next_minor
(
Path(tmp_dir) / "{}-py{}.{}".format(venv_name, version.major, version.minor)
).mkdir()
(
Path(tmp_dir)
/ "{}-py{}.{}".format(venv_name, other_version.major, other_version.minor)
).mkdir()
envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
doc = tomlkit.document()
doc[venv_name] = {
"minor": "{}.{}".format(other_version.major, other_version.minor),
"patch": other_version.text,
}
envs_file.write(doc)
config.add_property("settings.virtualenvs.path", str(tmp_dir))
mocker.patch("subprocess.check_output", side_effect=check_output_wrapper())
EnvManager(config).deactivate(CWD, NullIO())
env = EnvManager(config).get(CWD)
assert env.path == Path(tmp_dir) / "{}-py{}.{}".format(
venv_name, version.major, version.minor
)
assert Path("/prefix")
envs = envs_file.read()
assert len(envs) == 0
def test_get_prefers_explicitly_activated_virtualenvs_over_env_var(
tmp_dir, config, mocker
):
os.environ["VIRTUAL_ENV"] = "/environment/prefix"
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
config.add_property("settings.virtualenvs.path", str(tmp_dir))
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
doc = tomlkit.document()
doc[venv_name] = {"minor": "3.7", "patch": "3.7.0"}
envs_file.write(doc)
mocker.patch("subprocess.check_output", side_effect=check_output_wrapper())
mocker.patch("subprocess.Popen.communicate", side_effect=[("/prefix", None)])
env = EnvManager(config).get(CWD)
assert env.path == Path(tmp_dir) / "{}-py3.7".format(venv_name)
assert env.base == Path("/prefix")
def test_list(tmp_dir, config):
config.add_property("settings.virtualenvs.path", str(tmp_dir))
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
venvs = EnvManager(config).list(CWD)
assert 2 == len(venvs)
assert (Path(tmp_dir) / "{}-py3.6".format(venv_name)) == venvs[0].path
assert (Path(tmp_dir) / "{}-py3.7".format(venv_name)) == venvs[1].path
def test_remove_by_python_version(tmp_dir, config, mocker):
config.add_property("settings.virtualenvs.path", str(tmp_dir))
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
mocker.patch(
"subprocess.check_output",
side_effect=check_output_wrapper(Version.parse("3.6.6")),
)
manager = EnvManager(config)
venv = manager.remove("3.6", CWD)
assert (Path(tmp_dir) / "{}-py3.6".format(venv_name)) == venv.path
assert not (Path(tmp_dir) / "{}-py3.6".format(venv_name)).exists()
def test_remove_by_name(tmp_dir, config, mocker):
config.add_property("settings.virtualenvs.path", str(tmp_dir))
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
mocker.patch(
"subprocess.check_output",
side_effect=check_output_wrapper(Version.parse("3.6.6")),
)
manager = EnvManager(config)
venv = manager.remove("{}-py3.6".format(venv_name), CWD)
assert (Path(tmp_dir) / "{}-py3.6".format(venv_name)) == venv.path
assert not (Path(tmp_dir) / "{}-py3.6".format(venv_name)).exists()
def test_remove_also_deactivates(tmp_dir, config, mocker):
config.add_property("settings.virtualenvs.path", str(tmp_dir))
venv_name = EnvManager.generate_env_name("simple_project", str(CWD))
(Path(tmp_dir) / "{}-py3.7".format(venv_name)).mkdir()
(Path(tmp_dir) / "{}-py3.6".format(venv_name)).mkdir()
mocker.patch(
"subprocess.check_output",
side_effect=check_output_wrapper(Version.parse("3.6.6")),
)
envs_file = TomlFile(Path(tmp_dir) / "envs.toml")
doc = tomlkit.document()
doc[venv_name] = {"minor": "3.6", "patch": "3.6.6"}
envs_file.write(doc)
manager = EnvManager(config)
venv = manager.remove("python3.6", CWD)
assert (Path(tmp_dir) / "{}-py3.6".format(venv_name)) == venv.path
assert not (Path(tmp_dir) / "{}-py3.6".format(venv_name)).exists()
envs = envs_file.read()
assert venv_name not in envs
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment