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 @@ ...@@ -5,6 +5,10 @@
### Added ### Added
- Added an `export` command to export the lock file to other formats (only `requirements.txt` is currently supported). - 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 ### Changed
...@@ -13,6 +17,7 @@ ...@@ -13,6 +17,7 @@
- The `debug:info` command has been renamed to `debug info`. - The `debug:info` command has been renamed to `debug info`.
- The `debug:resolve` command has been renamed to `debug resolve`. - The `debug:resolve` command has been renamed to `debug resolve`.
- The `self:update` command has been renamed to `self update`. - 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 ### Fixed
......
...@@ -155,19 +155,99 @@ When you execute the `install` command (or any other "install" commands like `ad ...@@ -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 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. 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 ```bash
the currently activated Python version. poetry env list
```
To easily switch between Python versions, it is recommended to will output something like the following:
use [pyenv](https://github.com/pyenv/pyenv) or similar tools.
For instance, if your project is Python 2.7 only, a standard workflow ```text
would be: 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 If your remove the currently activated virtualenv, it will be automatically deactivated.
pyenv install 2.7.15
pyenv local 2.7.15 # Activate Python 2.7 for the current project
poetry install
```
...@@ -375,3 +375,112 @@ poetry export -f requirements.txt ...@@ -375,3 +375,112 @@ poetry export -f requirements.txt
!!!note !!!note
Only the `requirements.txt` format is currently supported. 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 ...@@ -33,6 +33,8 @@ from .commands.self import SelfCommand
from .config import ApplicationConfig from .config import ApplicationConfig
from .commands.env import EnvCommand
class Application(BaseApplication): class Application(BaseApplication):
def __init__(self): def __init__(self):
...@@ -89,6 +91,9 @@ class Application(BaseApplication): ...@@ -89,6 +91,9 @@ class Application(BaseApplication):
# Debug command # Debug command
commands += [DebugCommand()] commands += [DebugCommand()]
# Env command
commands += [EnvCommand()]
# Self commands # Self commands
commands += [SelfCommand()] commands += [SelfCommand()]
......
import os import os
import sys import sys
from clikit.args import StringArgs
from ..command import Command from ..command import Command
...@@ -12,54 +14,21 @@ class DebugInfoCommand(Command): ...@@ -12,54 +14,21 @@ class DebugInfoCommand(Command):
""" """
def handle(self): 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]) poetry_python_version = ".".join(str(s) for s in sys.version_info[:3])
self.line("") self.line("")
self.line("<b>Poetry</b>") self.line("<b>Poetry</b>")
self.line("") self.line(
self.line("<info>Version</info>: <comment>{}</>".format(poetry.VERSION)) "\n".join(
self.line("<info>Python</info>: <comment>{}</>".format(poetry_python_version)) [
"<info>Version</info>: <comment>{}</>".format(self.poetry.VERSION),
self.line("") "<info>Python</info>: <comment>{}</>".format(
poetry_python_version
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"
), ),
] ]
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: return command.run(args, self._io)
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("")
...@@ -24,7 +24,7 @@ class DebugResolveCommand(Command): ...@@ -24,7 +24,7 @@ class DebugResolveCommand(Command):
from poetry.puzzle import Solver from poetry.puzzle import Solver
from poetry.repositories.repository import Repository from poetry.repositories.repository import Repository
from poetry.semver import parse_constraint from poetry.semver import parse_constraint
from poetry.utils.env import Env from poetry.utils.env import EnvManager
packages = self.argument("package") packages = self.argument("package")
...@@ -78,7 +78,7 @@ class DebugResolveCommand(Command): ...@@ -78,7 +78,7 @@ class DebugResolveCommand(Command):
return 0 return 0
env = Env.get(self.poetry.file.parent) env = EnvManager(self.poetry.config).get(self.poetry.file.parent)
current_python_version = parse_constraint( current_python_version = parse_constraint(
".".join(str(v) for v in env.version_info) ".".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 ...@@ -37,7 +37,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
def handle(self): def handle(self):
from poetry.layouts import layout from poetry.layouts import layout
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.env import Env from poetry.utils.env import EnvManager
from poetry.vcs.git import GitConfig from poetry.vcs.git import GitConfig
if (Path.cwd() / "pyproject.toml").exists(): if (Path.cwd() / "pyproject.toml").exists():
...@@ -100,7 +100,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in ...@@ -100,7 +100,7 @@ The <info>init</info> command creates a basic <comment>pyproject.toml</> file in
question.set_validator(self._validate_license) question.set_validator(self._validate_license)
license = self.ask(question) license = self.ask(question)
current_env = Env.get(Path.cwd()) current_env = EnvManager().get(Path.cwd())
default_python = "^{}".format( default_python = "^{}".format(
".".join(str(v) for v in current_env.version_info[:2]) ".".join(str(v) for v in current_env.version_info[:2])
) )
......
...@@ -14,7 +14,7 @@ class NewCommand(Command): ...@@ -14,7 +14,7 @@ class NewCommand(Command):
def handle(self): def handle(self):
from poetry.layouts import layout from poetry.layouts import layout
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.env import Env from poetry.utils.env import EnvManager
from poetry.vcs.git import GitConfig from poetry.vcs.git import GitConfig
if self.option("src"): if self.option("src"):
...@@ -45,7 +45,7 @@ class NewCommand(Command): ...@@ -45,7 +45,7 @@ class NewCommand(Command):
if author_email: if author_email:
author += " <{}>".format(author_email) author += " <{}>".format(author_email)
current_env = Env.get(Path.cwd()) current_env = EnvManager().get(Path.cwd())
default_python = "^{}".format( default_python = "^{}".format(
".".join(str(v) for v in current_env.version_info[:2]) ".".join(str(v) for v in current_env.version_info[:2])
) )
......
...@@ -57,18 +57,20 @@ class ApplicationConfig(BaseApplicationConfig): ...@@ -57,18 +57,20 @@ class ApplicationConfig(BaseApplicationConfig):
def set_env(self, event, event_name, _): # type: (PreHandleEvent, str, ...) -> None def set_env(self, event, event_name, _): # type: (PreHandleEvent, str, ...) -> None
from poetry.semver import parse_constraint 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): if not isinstance(command, EnvCommand):
return return
io = event.io io = event.io
poetry = command.poetry poetry = command.poetry
env_manager = EnvManager(poetry.config)
# Checking compatibility of the current environment with # Checking compatibility of the current environment with
# the python dependency specified in pyproject.toml # 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 supported_python = poetry.package.python_constraint
current_python = parse_constraint( current_python = parse_constraint(
".".join(str(v) for v in current_env.version_info[:3]) ".".join(str(v) for v in current_env.version_info[:3])
...@@ -82,7 +84,7 @@ class ApplicationConfig(BaseApplicationConfig): ...@@ -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(): if env.is_venv() and io.is_verbose():
io.write_line("Using virtualenv: <comment>{}</>".format(env.path)) io.write_line("Using virtualenv: <comment>{}</>".format(env.path))
......
...@@ -30,7 +30,7 @@ from poetry.utils._compat import PY35 ...@@ -30,7 +30,7 @@ from poetry.utils._compat import PY35
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.helpers import parse_requires from poetry.utils.helpers import parse_requires
from poetry.utils.helpers import safe_rmtree 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.env import EnvCommandError
from poetry.utils.setup_reader import SetupReader from poetry.utils.setup_reader import SetupReader
...@@ -260,7 +260,7 @@ class Provider: ...@@ -260,7 +260,7 @@ class Provider:
try: try:
cwd = dependency.full_path cwd = dependency.full_path
venv = Env.get(cwd) venv = EnvManager().get(cwd)
venv.run("python", "setup.py", "egg_info") venv.run("python", "setup.py", "egg_info")
except EnvCommandError: except EnvCommandError:
result = SetupReader.read_from_directory(dependency.full_path) result = SetupReader.read_from_directory(dependency.full_path)
......
import base64
import hashlib
import json import json
import os import os
import platform import platform
import re
import shutil
import subprocess import subprocess
import sys import sys
import sysconfig import sysconfig
import warnings import warnings
import tomlkit
from contextlib import contextmanager from contextlib import contextmanager
from subprocess import CalledProcessError from subprocess import CalledProcessError
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import List
from typing import Optional from typing import Optional
from typing import Tuple from typing import Tuple
...@@ -17,10 +24,12 @@ from clikit.api.io import IO ...@@ -17,10 +24,12 @@ from clikit.api.io import IO
from poetry.config import Config from poetry.config import Config
from poetry.locations import CACHE_DIR from poetry.locations import CACHE_DIR
from poetry.semver import Version
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils._compat import decode from poetry.utils._compat import decode
from poetry.utils._compat import encode from poetry.utils._compat import encode
from poetry.utils._compat import list_to_shell_command from poetry.utils._compat import list_to_shell_command
from poetry.utils.toml_file import TomlFile
from poetry.version.markers import BaseMarker from poetry.version.markers import BaseMarker
...@@ -84,6 +93,22 @@ import sys ...@@ -84,6 +93,22 @@ import sys
print('.'.join([str(s) for s in sys.version_info[:3]])) 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): class EnvError(Exception):
...@@ -99,92 +124,165 @@ class EnvCommandError(EnvError): ...@@ -99,92 +124,165 @@ class EnvCommandError(EnvError):
super(EnvCommandError, self).__init__(message) super(EnvCommandError, self).__init__(message)
class Env(object): class EnvManager(object):
""" """
An abstract Python environment. Environments manager
""" """
_env = None _env = None
def __init__(self, path, base=None): # type: (Path, Optional[Path]) -> None ENVS_FILE = "envs.toml"
self._is_windows = sys.platform == "win32"
self._path = path def __init__(self, config=None): # type: (Config) -> None
bin_dir = "bin" if not self._is_windows else "Scripts" if config is None:
self._bin_dir = self._path / bin_dir 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 envs_file = TomlFile(venv_path / self.ENVS_FILE)
def path(self): # type: () -> Path
return self._path
@property try:
def base(self): # type: () -> Path python_version = Version.parse(python)
return self._base 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 try:
def version_info(self): # type: () -> Tuple[int] python_version = decode(
return tuple(self.marker_env["version_info"]) 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 python_version = Version.parse(python_version.strip())
def python_implementation(self): # type: () -> str minor = "{}.{}".format(python_version.major, python_version.minor)
return self.marker_env["platform_python_implementation"] 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 # Create if needed
def python(self): # type: () -> str if not venv.exists() or venv.exists() and create:
""" in_venv = os.environ.get("VIRTUAL_ENV") is not None
Path to current python executable if in_venv or not venv.exists():
""" create = True
return self._bin("python")
@property if venv.exists():
def marker_env(self): # We need to check if the patch version is correct
if self._marker_env is None: _venv = VirtualEnv(venv)
self._marker_env = self.get_marker_env() current_patch = ".".join(str(v) for v in _venv.version_info[:3])
return self._marker_env if patch != current_patch:
create = True
@property self.create_venv(cwd, io, executable=python, force=create)
def pip(self): # type: () -> str
"""
Path to current pip executable
"""
return self._bin("pip")
@classmethod # Activate
def get(cls, cwd, reload=False): # type: (Path, bool) -> Env envs[base_env_name] = {"minor": minor, "patch": patch}
if cls._env is not None and not reload: envs_file.write(envs)
return cls._env
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 # Check if we are inside a virtualenv or not
in_venv = os.environ.get("VIRTUAL_ENV") is not None 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 # Checking if a local virtualenv exists
if (cwd / ".venv").exists(): if (cwd / ".venv").exists():
venv = cwd / ".venv" venv = cwd / ".venv"
return VirtualEnv(venv) return VirtualEnv(venv)
config = Config.create("config.toml") create_venv = self._config.setting("settings.virtualenvs.create", True)
create_venv = config.setting("settings.virtualenvs.create", True)
if not create_venv: if not create_venv:
return SystemEnv(Path(sys.prefix)) 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: if venv_path is None:
venv_path = Path(CACHE_DIR) / "virtualenvs" venv_path = Path(CACHE_DIR) / "virtualenvs"
else: else:
venv_path = Path(venv_path) venv_path = Path(venv_path)
name = cwd.name name = "{}-py{}".format(base_env_name, python_minor.strip())
name = "{}-py{}".format(
name, ".".join([str(v) for v in sys.version_info[:2]])
)
venv = venv_path / name venv = venv_path / name
...@@ -198,26 +296,134 @@ class Env(object): ...@@ -198,26 +296,134 @@ class Env(object):
base_prefix = None base_prefix = None
else: else:
prefix = Path(sys.prefix) prefix = Path(sys.prefix)
base_prefix = cls.get_base_prefix() base_prefix = self.get_base_prefix()
return VirtualEnv(prefix, base_prefix) return VirtualEnv(prefix, base_prefix)
@classmethod def list(self, cwd, name=None): # type: (Path, Optional[str]) -> List[VirtualEnv]
def create_venv(cls, cwd, io, name=None): # type: (Path, IO, bool) -> Env if name is None:
if cls._env is not None: name = cwd.name
return cls._env
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) raise ValueError(
if env.is_venv(): '<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. # Already inside a virtualenv.
return env return env
config = Config.create("config.toml") create_venv = self._config.setting("settings.virtualenvs.create")
root_venv = self._config.setting("settings.virtualenvs.in-project")
create_venv = config.setting("settings.virtualenvs.create")
root_venv = config.setting("settings.virtualenvs.in-project")
venv_path = config.setting("settings.virtualenvs.path") venv_path = self._config.setting("settings.virtualenvs.path")
if root_venv: if root_venv:
venv_path = cwd / ".venv" venv_path = cwd / ".venv"
elif venv_path is None: elif venv_path is None:
...@@ -228,11 +434,26 @@ class Env(object): ...@@ -228,11 +434,26 @@ class Env(object):
if not name: if not name:
name = cwd.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: if root_venv:
venv = venv_path venv = venv_path
else: else:
name = self.generate_env_name(name, str(cwd))
name = "{}-py{}".format(name, python_minor.strip())
venv = venv_path / name venv = venv_path / name
if not venv.exists(): if not venv.exists():
...@@ -250,9 +471,15 @@ class Env(object): ...@@ -250,9 +471,15 @@ class Env(object):
"Creating virtualenv <info>{}</> in {}".format(name, str(venv_path)) "Creating virtualenv <info>{}</> in {}".format(name, str(venv_path))
) )
cls.build_venv(str(venv)) self.build_venv(str(venv), executable=executable)
else: 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)) io.write_line("Virtualenv <info>{}</> already exists.".format(name))
# venv detection: # venv detection:
...@@ -269,12 +496,23 @@ class Env(object): ...@@ -269,12 +496,23 @@ class Env(object):
p_venv = os.path.normcase(str(venv)) p_venv = os.path.normcase(str(venv))
if any(p.startswith(p_venv) for p in paths): if any(p.startswith(p_venv) for p in paths):
# Running properly in the virtualenv, don't need to do anything # 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) return VirtualEnv(venv)
@classmethod def build_venv(self, path, executable=None):
def build_venv(cls, path): 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: try:
from venv import EnvBuilder from venv import EnvBuilder
...@@ -288,6 +526,89 @@ class Env(object): ...@@ -288,6 +526,89 @@ class Env(object):
build(path) 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 @classmethod
def get_base_prefix(cls): # type: () -> Path def get_base_prefix(cls): # type: () -> Path
if hasattr(sys, "real_prefix"): if hasattr(sys, "real_prefix"):
...@@ -375,6 +696,9 @@ class Env(object): ...@@ -375,6 +696,9 @@ class Env(object):
return str(bin_path) return str(bin_path)
def __eq__(self, other): # type: (Env) -> bool
return other.__class__ == self.__class__ and other.path == self.path
def __repr__(self): def __repr__(self):
return '{}("{}")'.format(self.__class__.__name__, self._path) return '{}("{}")'.format(self.__class__.__name__, self._path)
......
...@@ -47,12 +47,22 @@ def mock_clone(_, source, dest): ...@@ -47,12 +47,22 @@ def mock_clone(_, source, dest):
@pytest.fixture @pytest.fixture
def tmp_dir():
dir_ = tempfile.mkdtemp(prefix="poetry_")
yield dir_
shutil.rmtree(dir_)
@pytest.fixture
def environ(): 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) @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 = [ ...@@ -37,7 +37,7 @@ classifiers = [
# Requirements # Requirements
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "~2.7 || ^3.6" python = "~2.7 || ^3.4"
foo = "^1.0" foo = "^1.0"
""" """
......
import sys
from cleo.testers import CommandTester from cleo.testers import CommandTester
from poetry.utils._compat import Path from poetry.utils._compat import Path
...@@ -126,9 +127,11 @@ description = "" ...@@ -126,9 +127,11 @@ description = ""
authors = ["Your Name <you@example.com>"] authors = ["Your Name <you@example.com>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.7" python = "^{python}"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
""" """.format(
python=".".join(str(c) for c in sys.version_info[:2])
)
assert expected in tester.io.fetch_output() assert expected in tester.io.fetch_output()
from cleo.testers import CommandTester from cleo.testers import CommandTester
from poetry.utils._compat import Path
from poetry.utils.env import MockEnv from poetry.utils.env import MockEnv
def test_run_passes_all_args(app, mocker): def test_run_passes_all_args(app, mocker):
env = MockEnv(is_venv=True) env = MockEnv(path=Path("/prefix"), base=Path("/base/prefix"), is_venv=True)
mocker.patch("poetry.utils.env.Env.get", return_value=env) mocker.patch("poetry.utils.env.EnvManager.get", return_value=env)
command = app.find("run") command = app.find("run")
tester = CommandTester(command) tester = CommandTester(command)
......
...@@ -17,7 +17,6 @@ from poetry.packages import Locker as BaseLocker ...@@ -17,7 +17,6 @@ from poetry.packages import Locker as BaseLocker
from poetry.repositories import Pool from poetry.repositories import Pool
from poetry.repositories import Repository as BaseRepository from poetry.repositories import Repository as BaseRepository
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.env import MockEnv
from poetry.utils.toml_file import TomlFile from poetry.utils.toml_file import TomlFile
from poetry.repositories.exceptions import PackageNotFound from poetry.repositories.exceptions import PackageNotFound
...@@ -49,11 +48,7 @@ def installed(): ...@@ -49,11 +48,7 @@ def installed():
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup(mocker, installer, installed): def setup(mocker, installer, installed, config):
mocker.patch(
"poetry.utils.env.Env.get", return_value=MockEnv(is_venv=True, execute=True)
)
# Set Installer's installer # Set Installer's installer
p = mocker.patch("poetry.installation.installer.Installer._get_installer") p = mocker.patch("poetry.installation.installer.Installer._get_installer")
p.return_value = installer p.return_value = installer
......
...@@ -20,7 +20,7 @@ classifiers = [ ...@@ -20,7 +20,7 @@ classifiers = [
# Requirements # Requirements
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "~2.7 || ^3.6" python = "~2.7 || ^3.4"
cachy = "^0.1.0" cachy = "^0.1.0"
pendulum = "^2.0.0" pendulum = "^2.0.0"
......
...@@ -22,7 +22,7 @@ classifiers = [ ...@@ -22,7 +22,7 @@ classifiers = [
# Requirements # Requirements
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "~2.7 || ^3.6" python = "~2.7 || ^3.4"
# File dependency # File dependency
demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" } demo = { path = "../distributions/demo-0.1.0-py2.py3-none-any.whl" }
......
...@@ -22,4 +22,4 @@ classifiers = [ ...@@ -22,4 +22,4 @@ classifiers = [
# Requirements # Requirements
[tool.poetry.dependencies] [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): ...@@ -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") @pytest.mark.skipif(not PY35, reason="AST parsing does not work for Python <3.4")
def test_search_for_vcs_read_setup(provider, mocker): 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") dependency = VCSDependency("demo", "git", "https://github.com/demo/demo.git")
...@@ -98,7 +98,7 @@ def test_search_for_vcs_read_setup(provider, mocker): ...@@ -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") @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): 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 = VCSDependency("demo", "git", "https://github.com/demo/demo.git")
dependency.extras.append("foo") dependency.extras.append("foo")
...@@ -118,7 +118,7 @@ def test_search_for_vcs_read_setup_with_extras(provider, mocker): ...@@ -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): 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") 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): ...@@ -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") @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): 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( dependency = DirectoryDependency(
"demo", "demo",
...@@ -201,7 +201,7 @@ def test_search_for_directory_setup_read_setup(provider, mocker): ...@@ -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") @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): 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( dependency = DirectoryDependency(
"demo", "demo",
......
import os 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._compat import Path
from poetry.utils.env import Env from poetry.utils.env import EnvManager
from poetry.utils.env import VirtualEnv 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" venv_path = Path(tmp_dir) / "Virtual Env"
Env.build_venv(str(venv_path)) EnvManager(config).build_venv(str(venv_path))
venv = VirtualEnv(venv_path) venv = VirtualEnv(venv_path)
assert venv.run("python", "-V", shell=True).startswith("Python") assert venv.run("python", "-V", shell=True).startswith("Python")
def test_env_get_in_project_venv(tmp_dir, environ): def test_env_get_in_project_venv(tmp_dir, config):
if "VIRTUAL_ENV" in environ: if "VIRTUAL_ENV" in os.environ:
del environ["VIRTUAL_ENV"] del os.environ["VIRTUAL_ENV"]
(Path(tmp_dir) / ".venv").mkdir() (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" 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