Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add flag --use-env to poetry commands #4179

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ then `--help` combined with any of those can give you more information.
* `--no-ansi`: Disable ANSI output.
* `--version (-V)`: Display this application version.
* `--no-interaction (-n)`: Do not ask any interactive question.
* `--no-plugins` Disables plugins.
* `--use-env=USE-ENV` The python executable to use.


## new
Expand Down
36 changes: 33 additions & 3 deletions docs/managing-environments.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ To achieve this, it will first check if it's currently running inside a virtual
If it is, it will use it directly without creating a new one. But if it's not, it will use
one that it has already created or create a brand new one for you.

By default, Poetry will try to use the currently activated Python version
to create the virtual environment for the current project.
By default, Poetry will use the Python used during installation to create the virtual environment
for the current project.

However, for various reasons, this Python version might not be compatible
with the `python` requirement of the project. In this case, Poetry will try
Expand All @@ -36,7 +36,7 @@ would be:
```bash
pyenv install 2.7.15
pyenv local 2.7.15 # Activate Python 2.7 for the current project
poetry install
poetry install --use-env python2.7
```
{{% /note %}}

Expand Down Expand Up @@ -70,6 +70,36 @@ special `system` Python version to retrieve the default behavior:
poetry env use system
```

# Multiple environments

You can use `poetry env use` to create multiple environments for your project with different interpreters. `poetry env use`
will store the information about the used currently used python version in `{cachedir}/virtualenvs/env.toml` by default.

Sometimes it might be feasible to run a poetry command like `poetry shell` or `poetry run` on an environment without
switching the default. For this purpose the global option `--use-env` exists.

Let's assume we already have an evironment for python3.7 and want to add another one for python3.8 without setting it
as the new default one:

```bash
poetry install --use-env python3.8
```

We can even open a new shell for this environment:

```bash
poetry shell --use-env python3.8
```

Or run a specific command:

```bash
poetry run --use-env python3.8 python --version
```

Just like `poetry env use` the `--use-env` parameter expects a full path to an executable, a python executable in `$PATH`
or minor Python version.

## Displaying the environment information

If you want to get basic information about the currently activated virtual environment,
Expand Down
17 changes: 16 additions & 1 deletion poetry/console/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from cleo.io.outputs.output import Output

from poetry.__version__ import __version__
from poetry.core.semver.version import Version

from .command_loader import CommandLoader
from .commands.command import Command
Expand Down Expand Up @@ -264,8 +265,17 @@ def configure_env(
io = event.io
poetry = command.poetry

try:
python_version = Version.parse(event.io.input.option("use-env"))
python = f"python{python_version.major}"
if python_version.precision > 1:
python += f".{python_version.minor}"
except ValueError:
# Executable in PATH or full executable path
python = event.io.input.option("use-env")

env_manager = EnvManager(poetry)
env = env_manager.create_venv(io)
env = env_manager.create_venv(io, executable=python)

if env.is_venv() and io.is_verbose():
io.write_line(f"Using virtualenv: <comment>{env.path}</>")
Expand Down Expand Up @@ -327,6 +337,11 @@ def _default_definition(self) -> "Definition":
definition.add_option(
Option("--no-plugins", flag=True, description="Disables plugins.")
)
definition.add_option(
Option(
"--use-env", flag=False, description="The python executable to use."
),
)

return definition

Expand Down
111 changes: 43 additions & 68 deletions poetry/utils/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from contextlib import contextmanager
from copy import deepcopy
from functools import lru_cache
from pathlib import Path
from subprocess import CalledProcessError
from typing import Any
Expand Down Expand Up @@ -427,6 +428,26 @@ class EnvManager:
def __init__(self, poetry: Poetry) -> None:
self._poetry = poetry

@lru_cache()
def _python_version(self, executable: str, precision: int) -> Version:
try:
python_version = decode(
subprocess.check_output(
list_to_shell_command(
[
executable,
"-c",
f"\"import sys; print('.'.join([str(s) for s in sys.version_info[:{precision}]]))\"",
]
),
shell=True,
)
)
except CalledProcessError as e:
raise EnvCommandError(e)

return Version.parse(python_version.strip())

def activate(self, python: str, io: IO) -> "Env":
venv_path = self._poetry.config.get("virtualenvs.path")
if venv_path is None:
Expand All @@ -447,23 +468,7 @@ def activate(self, python: str, io: IO) -> "Env":
# Executable in PATH or full executable path
pass

try:
python_version = decode(
subprocess.check_output(
list_to_shell_command(
[
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())
python_version = self._python_version(python, precision=3)
minor = f"{python_version.major}.{python_version.minor}"
patch = python_version.text

Expand Down Expand Up @@ -548,11 +553,17 @@ def deactivate(self, io: IO) -> None:

envs_file.write(envs)

def get(self, reload: bool = False) -> Union["VirtualEnv", "SystemEnv"]:
def get(
self, reload: bool = False, executable: Optional[str] = None
) -> Union["VirtualEnv", "SystemEnv"]:
if self._env is not None and not reload:
return self._env

python_minor = ".".join([str(v) for v in sys.version_info[:2]])
if executable:
python_version = self._python_version(executable, precision=3)
python_minor = f"{python_version.major}.{python_version.minor}"
else:
python_minor = ".".join([str(v) for v in sys.version_info[:2]])

venv_path = self._poetry.config.get("virtualenvs.path")
if venv_path is None:
Expand Down Expand Up @@ -693,23 +704,7 @@ def remove(self, python: str) -> "Env":
# Executable in PATH or full executable path
pass

try:
python_version = decode(
subprocess.check_output(
list_to_shell_command(
[
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())
python_version = self._python_version(python, precision=3)
minor = f"{python_version.major}.{python_version.minor}"

name = f"{base_env_name}-py{minor}"
Expand Down Expand Up @@ -743,7 +738,7 @@ def create_venv(
return self._env

cwd = self._poetry.file.parent
env = self.get(reload=True)
env = self.get(reload=True, executable=executable)

if not env.is_sane():
force = True
Expand All @@ -766,25 +761,17 @@ def create_venv(
if not name:
name = self._poetry.package.name

python_patch = ".".join([str(v) for v in sys.version_info[:3]])
python_minor = ".".join([str(v) for v in sys.version_info[:2]])
if executable:
python_patch = decode(
subprocess.check_output(
list_to_shell_command(
[
executable,
"-c",
"\"import sys; print('.'.join([str(s) for s in sys.version_info[:3]]))\"",
]
),
shell=True,
).strip()
python_patch = self._python_version(executable, precision=3)
python_minor = f"{python_patch.major}.{python_patch.minor}"
else:
python_patch = Version.parse(
".".join([str(v) for v in sys.version_info[:3]])
)
python_minor = ".".join(python_patch.split(".")[:2])
python_minor = ".".join([str(v) for v in sys.version_info[:2]])

supported_python = self._poetry.package.python_constraint
if not supported_python.allows(Version.parse(python_patch)):
if not supported_python.allows(python_patch):
# The currently activated or chosen Python version
# is not compatible with the Python constraint specified
# for the project.
Expand All @@ -793,7 +780,7 @@ def create_venv(
# Otherwise, we try to find a compatible Python version.
if executable:
raise NoCompatiblePythonVersionFound(
self._poetry.package.python_versions, python_patch
self._poetry.package.python_versions, str(python_patch)
)

io.write_line(
Expand Down Expand Up @@ -826,29 +813,17 @@ def create_venv(
io.write_line(f"<debug>Trying {python}</debug>")

try:
python_patch = decode(
subprocess.check_output(
list_to_shell_command(
[
python,
"-c",
"\"import sys; print('.'.join([str(s) for s in sys.version_info[:3]]))\"",
]
),
stderr=subprocess.STDOUT,
shell=True,
).strip()
)
python_patch = self._python_version(python, precision=3)
except CalledProcessError:
continue

if not python_patch:
continue

if supported_python.allows(Version.parse(python_patch)):
if supported_python.allows(python_patch):
io.write_line(f"Using <c1>{python}</c1> ({python_patch})")
executable = python
python_minor = ".".join(python_patch.split(".")[:2])
python_minor = f"{python_patch.major}.{python_patch.minor}"
break

if not executable:
Expand Down