Skip to content

Commit

Permalink
uses python-finder during env activate
Browse files Browse the repository at this point in the history
  • Loading branch information
bmarroquin committed Jun 5, 2022
1 parent 80b03ec commit 17b56d2
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 71 deletions.
143 changes: 97 additions & 46 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ tomlkit = ">=0.7.0,<1.0.0"
virtualenv = "(>=20.4.3,<20.4.5 || >=20.4.7)"
urllib3 = "^1.26.0"
dulwich = "^0.20.35"
pythonfinder = "^1.2.10"

[tool.poetry.dev-dependencies]
tox = "^3.18"
Expand Down
7 changes: 6 additions & 1 deletion src/poetry/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,11 @@ class Config:
},
"prefer-active-python": False,
},
"experimental": {"new-installer": True, "system-git-client": False},
"experimental": {
"new-installer": True,
"system-git-client": False,
"python-finder": True,
},
"installer": {"parallel": True, "max-workers": None, "no-binary": None},
}

Expand Down Expand Up @@ -249,6 +253,7 @@ def _get_normalizer(name: str) -> Callable[[str], Any]:
"virtualenvs.options.system-site-packages",
"virtualenvs.options.prefer-active-python",
"experimental.new-installer",
"experimental.python-finder",
"experimental.system-git-client",
"installer.parallel",
}:
Expand Down
81 changes: 59 additions & 22 deletions src/poetry/utils/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from poetry.core.semver.version import Version
from poetry.core.toml.file import TOMLFile
from poetry.core.utils.helpers import temporary_directory
from pythonfinder import Finder
from virtualenv.seed.wheels.embed import get_embed_wheel

from poetry.locations import CACHE_DIR
Expand Down Expand Up @@ -489,6 +490,13 @@ def __init__(self, expected: str, given: str) -> None:
super().__init__(message)


class InterpreterNotFound(EnvError):
def __init__(self, python: str) -> None:
message = f"Python interpreter not found for input ({python})"

super().__init__(message)


class EnvManager:
"""
Environments manager
Expand Down Expand Up @@ -535,28 +543,7 @@ def _detect_active_python(self, io: IO) -> str:
)
return executable

def activate(self, python: str, io: IO) -> Env:
venv_path = self._poetry.config.get("virtualenvs.path")
if venv_path is None:
venv_path = CACHE_DIR / "virtualenvs"
else:
venv_path = Path(venv_path)

cwd = self._poetry.file.parent

envs_file = TOMLFile(venv_path / self.ENVS_FILE)

try:
python_version = Version.parse(python)
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
pass

python = self._full_python_path(python)

def _get_python_versions(self, python: str) -> tuple[str, str]:
try:
python_version = decode(
subprocess.check_output(
Expand All @@ -570,6 +557,56 @@ def activate(self, python: str, io: IO) -> Env:
python_version = Version.parse(python_version.strip())
minor = f"{python_version.major}.{python_version.minor}"
patch = python_version.text
return minor, patch

def _find_python(self, python: str) -> tuple[str, str, str]:
# Handle full path
path = Path(python)
if path.is_absolute():
if path.exists():
minor, patch = self._get_python_versions(python)
return python, minor, patch
else:
raise InterpreterNotFound(python)

# python is either <major>[.<minor>] or python[<major>[.<minor>]]
# both are supported as first parameter to Finder.find_python_version
finder = Finder(sort_by_path=True)
try:
path = finder.find_python_version(python)
if path:
minor = f"{path.py_version.major}.{path.py_version.minor}"
patch = f"{minor}.{path.py_version.patch}"
return str(path.path), minor, patch
except ValueError:
pass

raise InterpreterNotFound(python)

def activate(self, python: str, io: IO) -> Env:
venv_path = self._poetry.config.get("virtualenvs.path")
if venv_path is None:
venv_path = CACHE_DIR / "virtualenvs"
else:
venv_path = Path(venv_path)

cwd = self._poetry.file.parent

envs_file = TOMLFile(venv_path / self.ENVS_FILE)
experimental_finder = self._poetry.config.get("experimental.python-finder")
if experimental_finder:
python, minor, patch = self._find_python(python)
else:
try:
python_version = Version.parse(python)
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
pass
python = self._full_python_path(python)
minor, patch = self._get_python_versions(python)

create = False
is_root_venv = self._poetry.config.get("virtualenvs.in-project")
Expand Down
14 changes: 14 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@

from _pytest.config import Config as PyTestConfig
from _pytest.config.argparsing import Parser
from _pytest.fixtures import FixtureRequest
from pytest_mock import MockerFixture

from poetry.poetry import Poetry
Expand Down Expand Up @@ -441,3 +442,16 @@ def set_simple_log_formatter() -> None:
for handler in logging.getLogger(name).handlers:
# replace formatter with simple formatter for testing
handler.setFormatter(logging.Formatter(fmt="%(message)s"))


@pytest.fixture(params=[True, False])
def find_python_mode(config: Config, request: FixtureRequest) -> None:
config.merge(
{
"experimental": {
# param is an optional attribute present
# when fixtures are parameterized
"python-finder": request.param # type: ignore
}
}
)
10 changes: 10 additions & 0 deletions tests/console/commands/env/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,13 @@ def check_output(cmd: str, *_: Any, **__: Any) -> str:
return str(Path("/prefix"))

return check_output


def find_python_wrapper(
version: PEP440Version = VERSION_3_7_1,
) -> Callable[[str], tuple[str, str, str]]:
def find_python_output(python: str) -> tuple[str, str, str]:
path = f"/usr/bin/python{version.major}.{version.minor}"
return path, f"{version.major}.{version.minor}", version.text

return find_python_output
15 changes: 13 additions & 2 deletions tests/console/commands/env/test_use.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from poetry.utils.env import MockEnv
from tests.console.commands.env.helpers import build_venv
from tests.console.commands.env.helpers import check_output_wrapper
from tests.console.commands.env.helpers import find_python_wrapper


if TYPE_CHECKING:
Expand All @@ -34,18 +35,25 @@ def setup(mocker: MockerFixture) -> None:
def mock_subprocess_calls(
setup: None, current_python: tuple[int, int, int], mocker: MockerFixture
) -> None:
version = Version.from_parts(*current_python)
mocker.patch(
"subprocess.check_output",
side_effect=check_output_wrapper(Version.from_parts(*current_python)),
side_effect=check_output_wrapper(version),
)
mocker.patch(
"subprocess.Popen.communicate",
side_effect=[("/prefix", None), ("/prefix", None), ("/prefix", None)],
)
mocker.patch(
"poetry.utils.env.EnvManager._find_python",
side_effect=find_python_wrapper(version),
)


@pytest.fixture
def tester(command_tester_factory: CommandTesterFactory) -> CommandTester:
def tester(
command_tester_factory: CommandTesterFactory, find_python_mode: None
) -> CommandTester:
return command_tester_factory("env use")


Expand All @@ -60,6 +68,9 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file(
"subprocess.check_output",
side_effect=check_output_wrapper(),
)
mocker.patch(
"poetry.utils.env.EnvManager._find_python", side_effect=find_python_wrapper()
)

mock_build_env = mocker.patch(
"poetry.utils.env.EnvManager.build_venv", side_effect=build_venv
Expand Down
3 changes: 3 additions & 0 deletions tests/console/commands/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def test_list_displays_default_value_if_not_set(
venv_path = json.dumps(os.path.join("{cache-dir}", "virtualenvs"))
expected = f"""cache-dir = {cache_dir}
experimental.new-installer = true
experimental.python-finder = true
experimental.system-git-client = false
installer.max-workers = null
installer.no-binary = null
Expand Down Expand Up @@ -80,6 +81,7 @@ def test_list_displays_set_get_setting(
venv_path = json.dumps(os.path.join("{cache-dir}", "virtualenvs"))
expected = f"""cache-dir = {cache_dir}
experimental.new-installer = true
experimental.python-finder = true
experimental.system-git-client = false
installer.max-workers = null
installer.no-binary = null
Expand Down Expand Up @@ -132,6 +134,7 @@ def test_list_displays_set_get_local_setting(
venv_path = json.dumps(os.path.join("{cache-dir}", "virtualenvs"))
expected = f"""cache-dir = {cache_dir}
experimental.new-installer = true
experimental.python-finder = true
experimental.system-git-client = false
installer.max-workers = null
installer.no-binary = null
Expand Down
43 changes: 43 additions & 0 deletions tests/utils/test_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,23 @@ def check_output(cmd: str, *args: Any, **kwargs: Any) -> str:
return check_output


def find_python_wrapper(
version: Version = VERSION_3_7_1,
) -> Callable[[str], tuple[str, str, str]]:
def find_python_output(python: str) -> tuple[str, str, str]:
path = f"/usr/bin/python{version.major}.{version.minor}"
return path, f"{version.major}.{version.minor}", version.text

return find_python_output


def test_activate_activates_non_existing_virtualenv_no_envs_file(
tmp_dir: str,
manager: EnvManager,
poetry: Poetry,
config: Config,
mocker: MockerFixture,
find_python_mode: None,
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
Expand All @@ -204,6 +215,11 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file(
"subprocess.Popen.communicate",
side_effect=[("/prefix", None), ("/prefix", None)],
)

mocker.patch(
"poetry.utils.env.EnvManager._find_python", side_effect=find_python_wrapper()
)

m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv)

env = manager.activate("python3.7", NullIO())
Expand Down Expand Up @@ -236,6 +252,7 @@ def test_activate_activates_existing_virtualenv_no_envs_file(
poetry: Poetry,
config: Config,
mocker: MockerFixture,
find_python_mode: None,
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
Expand All @@ -254,6 +271,9 @@ def test_activate_activates_existing_virtualenv_no_envs_file(
"subprocess.Popen.communicate",
side_effect=[("/prefix", None)],
)
mocker.patch(
"poetry.utils.env.EnvManager._find_python", side_effect=find_python_wrapper()
)
m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv)

env = manager.activate("python3.7", NullIO())
Expand All @@ -276,6 +296,7 @@ def test_activate_activates_same_virtualenv_with_envs_file(
poetry: Poetry,
config: Config,
mocker: MockerFixture,
find_python_mode: None,
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
Expand All @@ -299,6 +320,9 @@ def test_activate_activates_same_virtualenv_with_envs_file(
"subprocess.Popen.communicate",
side_effect=[("/prefix", None)],
)
mocker.patch(
"poetry.utils.env.EnvManager._find_python", side_effect=find_python_wrapper()
)
m = mocker.patch("poetry.utils.env.EnvManager.create_venv")

env = manager.activate("python3.7", NullIO())
Expand All @@ -320,6 +344,7 @@ def test_activate_activates_different_virtualenv_with_envs_file(
poetry: Poetry,
config: Config,
mocker: MockerFixture,
find_python_mode: None,
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
Expand All @@ -342,6 +367,10 @@ def test_activate_activates_different_virtualenv_with_envs_file(
"subprocess.Popen.communicate",
side_effect=[("/prefix", None), ("/prefix", None), ("/prefix", None)],
)
mocker.patch(
"poetry.utils.env.EnvManager._find_python",
side_effect=find_python_wrapper(Version.parse("3.6.6")),
)
m = mocker.patch("poetry.utils.env.EnvManager.build_venv", side_effect=build_venv)

env = manager.activate("python3.6", NullIO())
Expand Down Expand Up @@ -372,6 +401,7 @@ def test_activate_activates_recreates_for_different_patch(
poetry: Poetry,
config: Config,
mocker: MockerFixture,
find_python_mode: None,
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
Expand Down Expand Up @@ -407,6 +437,9 @@ def test_activate_activates_recreates_for_different_patch(
"poetry.utils.env.EnvManager.remove_venv", side_effect=EnvManager.remove_venv
)

mocker.patch(
"poetry.utils.env.EnvManager._find_python", side_effect=find_python_wrapper()
)
env = manager.activate("python3.7", NullIO())

build_venv_m.assert_called_with(
Expand Down Expand Up @@ -437,6 +470,7 @@ def test_activate_does_not_recreate_when_switching_minor(
poetry: Poetry,
config: Config,
mocker: MockerFixture,
find_python_mode: None,
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
Expand All @@ -460,6 +494,10 @@ def test_activate_does_not_recreate_when_switching_minor(
"subprocess.Popen.communicate",
side_effect=[("/prefix", None), ("/prefix", None), ("/prefix", None)],
)
mocker.patch(
"poetry.utils.env.EnvManager._find_python",
side_effect=find_python_wrapper(Version.parse("3.6.6")),
)
build_venv_m = mocker.patch(
"poetry.utils.env.EnvManager.build_venv", side_effect=build_venv
)
Expand Down Expand Up @@ -1057,6 +1095,7 @@ def test_activate_with_in_project_setting_does_not_fail_if_no_venvs_dir(
config: Config,
tmp_dir: str,
mocker: MockerFixture,
find_python_mode: None,
):
if "VIRTUAL_ENV" in os.environ:
del os.environ["VIRTUAL_ENV"]
Expand All @@ -1078,6 +1117,10 @@ def test_activate_with_in_project_setting_does_not_fail_if_no_venvs_dir(
"subprocess.Popen.communicate",
side_effect=[("/prefix", None), ("/prefix", None)],
)

mocker.patch(
"poetry.utils.env.EnvManager._find_python", side_effect=find_python_wrapper()
)
m = mocker.patch("poetry.utils.env.EnvManager.build_venv")

manager.activate("python3.7", NullIO())
Expand Down

0 comments on commit 17b56d2

Please sign in to comment.