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

Fix issues on windows and restore changes from 0.16.1 #106

Merged
merged 3 commits into from Nov 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 0 additions & 4 deletions .github/workflows/ci.yml
Expand Up @@ -39,10 +39,6 @@ jobs:
matrix:
os: [Ubuntu, MacOS, Windows]
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
# TODO: remove this when apparent issue with GitHub actions is fixed
exclude:
- os: Windows
python-version: '3.7'
runs-on: ${{ matrix.os }}-latest
steps:
- uses: actions/checkout@v3
Expand Down
66 changes: 48 additions & 18 deletions poethepoet/executor/poetry.py
@@ -1,6 +1,5 @@
import os
import shutil
import sys
from os import environ
from pathlib import Path
from subprocess import PIPE, Popen
from typing import Dict, Optional, Sequence, Type
Expand All @@ -25,17 +24,8 @@ def execute(
Execute the given cmd as a subprocess inside the poetry managed dev environment
"""

# If this run involves multiple executions then it's worth trying to
# avoid repetative (and expensive) calls to `poetry run` if we can
poetry_env = self._get_poetry_virtualenv(force=self.context.multistage)

if (
bool(os.environ.get("POETRY_ACTIVE"))
or self.context.poe_active == PoetryExecutor.__key__
or sys.prefix == poetry_env
):
# The target venv is already active so we can execute the command unaltered
return self._execute_cmd(cmd, input=input, use_exec=use_exec)
poetry_env = self._get_poetry_virtualenv()
project_dir = self.context.config.project_dir

if poetry_env:
# Execute the task in the virtualenv from poetry
Expand All @@ -47,6 +37,10 @@ def execute(
use_exec=use_exec,
)

if self._virtualenv_creation_disabled():
# There's no poetry env, and there isn't going to be
return self._execute_cmd(cmd, input=input, use_exec=use_exec)

# Run this task with `poetry run`
return self._execute_cmd(
(self._poetry_cmd(), "run", *cmd),
Expand All @@ -57,16 +51,52 @@ def execute(
def _get_poetry_virtualenv(self, force: bool = True):
"""
Ask poetry where it put the virtualenv for this project.
This is a relatively expensive operation so uses the context.exec_cache
Invoking poetry is relatively expensive so cache the result
"""
if force and "poetry_virtualenv" not in self.context.exec_cache:
self.context.exec_cache["poetry_virtualenv"] = (
Popen((self._poetry_cmd(), "env", "info", "-p"), stdout=PIPE)

# TODO: see if there's a more efficient way to do this that doesn't involve
# invoking the poetry cli or relying on undocumented APIs

exec_cache = self.context.exec_cache

if force and "poetry_virtualenv" not in exec_cache:
# Need to make sure poetry isn't influenced by whatever virtualenv is
# currently active
clean_env = dict(environ)
clean_env.pop("VIRTUAL_ENV", None)

exec_cache["poetry_virtualenv"] = (
Popen(
(self._poetry_cmd(), "env", "info", "-p"),
stdout=PIPE,
cwd=self.context.config.project_dir,
env=clean_env,
)
.communicate()[0]
.decode()
.strip()
)
return self.context.exec_cache.get("poetry_virtualenv")

return exec_cache.get("poetry_virtualenv")

def _poetry_cmd(self):
return shutil.which("poetry") or "poetry"

def _virtualenv_creation_disabled(self):
exec_cache = self.context.exec_cache

while "poetry_virtualenvs_create_disabled" not in exec_cache:
# Check env override
env_override = environ.get("POETRY_VIRTUALENVS_CREATE")
if env_override is not None:
exec_cache["poetry_virtualenvs_create_disabled"] = (
env_override == "false"
)
break

# A complete implementation would also check for a local poetry config file
# and a global poetry config file (location for this is platform dependent)
# in that order but just checking the env will do for now.
break

return exec_cache.get("poetry_virtualenvs_create_disabled", False)
19 changes: 14 additions & 5 deletions poethepoet/task/shell.py
Expand Up @@ -109,12 +109,21 @@ def _locate_interpreter(self, interpreter: str) -> Optional[str]:
elif interpreter == "sh":
result = which("sh") or which("/bin/sh")

elif interpreter == "bash":
result = which("bash") or which("/bin/bash")

# Specifically look for git bash on windows
# Specifically look for git sh on windows
if result is None and self._is_windows:
result = which(f"{prog_files}\\Git\\bin\\bash.exe")
result = which(f"{prog_files}\\Git\\bin\\sh.exe")

elif interpreter == "bash":
if self._is_windows:
# Specifically look for git bash on windows as the preferred option
# Don't trust bash from the path becuase it might be a useless decoy
result = (
which(f"{prog_files}\\Git\\bin\\bash.exe")
or which("/bin/bash")
or which("bash")
)
else:
result = which("bash") or which("/bin/bash")

elif interpreter == "zsh":
result = which("zsh") or which("/bin/zsh")
Expand Down
16 changes: 10 additions & 6 deletions poethepoet/virtualenv.py
Expand Up @@ -76,12 +76,16 @@ def valid(self) -> bool:
)

def get_env_vars(self, base_env: Mapping[str, str]) -> Dict[str, str]:
path_delim = ";" if self._is_windows else ":"
result = dict(
base_env,
VIRTUAL_ENV=str(self.path),
PATH=f"{self.bin_dir()}{path_delim}{os.environ.get('PATH', '')}",
)
bin_dir = str(self.bin_dir())
path_var = os.environ.get("PATH", "")

if not path_var.startswith(bin_dir):
path_delim = ";" if self._is_windows else ":"
path_var = bin_dir + path_delim + path_var

result = dict(base_env, VIRTUAL_ENV=str(self.path), PATH=path_var)

if "PYTHONHOME" in result:
result.pop("PYTHONHOME")

return result
12 changes: 9 additions & 3 deletions tests/conftest.py
Expand Up @@ -139,11 +139,17 @@ def run_poe_subproc(
output=rf"open(r\"{temp_file}\", \"w\")",
)

env = dict(os.environ, **(env or {}))
subproc_env = dict(os.environ)
subproc_env.pop("VIRTUAL_ENV", None)
if env:
subproc_env.update(env)

if coverage:
env["COVERAGE_PROCESS_START"] = str(PROJECT_TOML)
subproc_env["COVERAGE_PROCESS_START"] = str(PROJECT_TOML)

poeproc = Popen(shell_cmd, shell=True, stdout=PIPE, stderr=PIPE, env=env)
poeproc = Popen(
shell_cmd, shell=True, stdout=PIPE, stderr=PIPE, env=subproc_env
)
task_out, task_err = poeproc.communicate()

with temp_file.open("rb") as output_file:
Expand Down
8 changes: 8 additions & 0 deletions tests/fixtures/example_project/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions tests/fixtures/shells_project/pyproject.toml
Expand Up @@ -35,6 +35,12 @@ poe_test_echo "bam bam baaam bam"
env = { test_var = "roflcopter"}


[tool.poe.tasks.echo_powershell]
interpreter = "powershell"
shell = "poe_test_echo $ENV:test_var"
env = { test_var = "roflcopter"}


[tool.poe.tasks.echo_python]
interpreter = "python"
shell = """
Expand Down
16 changes: 12 additions & 4 deletions tests/test_poe_config.py
@@ -1,7 +1,14 @@
import os
import shutil
import tempfile
from pathlib import Path

import pytest

# Setting POETRY_VIRTUALENVS_CREATE stops poetry from creating the virtualenv and
# spamming about it in stdout
no_venv = {"POETRY_VIRTUALENVS_CREATE": "false"}


def test_setting_default_task_type(run_poe_subproc, projects, esc_prefix):
# Also tests passing of extra_args to sys.argv
Expand All @@ -10,14 +17,15 @@ def test_setting_default_task_type(run_poe_subproc, projects, esc_prefix):
"nat,",
r"welcome to " + esc_prefix + "${POE_ROOT}",
project="scripts",
env=no_venv,
)
assert result.capture == f"Poe => echo-args nat, welcome to {projects['scripts']}\n"
assert result.stdout == f"hello nat, welcome to {projects['scripts']}\n"
assert result.stderr == ""


def test_setting_default_array_item_task_type(run_poe_subproc):
result = run_poe_subproc("composite_task", project="scripts")
result = run_poe_subproc("composite_task", project="scripts", env=no_venv)
assert (
result.capture == f"Poe => poe_test_echo Hello\nPoe => poe_test_echo World!\n"
)
Expand All @@ -26,7 +34,7 @@ def test_setting_default_array_item_task_type(run_poe_subproc):


def test_setting_global_env_vars(run_poe_subproc, is_windows):
result = run_poe_subproc("travel")
result = run_poe_subproc("travel", env=no_venv)
if is_windows:
assert (
result.capture
Expand Down Expand Up @@ -74,11 +82,11 @@ def test_partially_decrease_verbosity(run_poe_subproc, high_verbosity_project_pa
assert result.stderr == ""


def test_decrease_verbosity(run_poe_subproc, projects, is_windows):
def test_decrease_verbosity(run_poe_subproc, is_windows):
result = run_poe_subproc(
"-q",
"part1",
cwd=projects["example"],
env=no_venv,
)
assert result.capture == ""
assert result.stderr == ""
Expand Down
14 changes: 7 additions & 7 deletions tests/test_poetry_plugin.py
Expand Up @@ -15,7 +15,7 @@ def setup_poetry_project_empty_prefix(run_poetry, projects):


@pytest.mark.slow
@pytest.mark.skipif(version_info < (3, 7), reason="dependencies require python>=3.7")
@pytest.mark.skipif(version_info < (3, 8), reason="dependencies require python>=3.8")
def test_poetry_help(run_poetry, projects):
result = run_poetry([], cwd=projects["poetry_plugin"])
assert result.stdout.startswith("Poetry (version ")
Expand All @@ -25,7 +25,7 @@ def test_poetry_help(run_poetry, projects):


@pytest.mark.slow
@pytest.mark.skipif(version_info < (3, 7), reason="dependencies require python>=3.7")
@pytest.mark.skipif(version_info < (3, 8), reason="dependencies require python>=3.8")
def test_task_with_cli_dependency(
run_poetry, projects, setup_poetry_project, is_windows
):
Expand All @@ -47,7 +47,7 @@ def test_task_with_cli_dependency(


@pytest.mark.slow
@pytest.mark.skipif(version_info < (3, 7), reason="dependencies require python>=3.7")
@pytest.mark.skipif(version_info < (3, 8), reason="dependencies require python>=3.8")
def test_task_with_lib_dependency(
run_poetry, projects, setup_poetry_project, is_windows
):
Expand All @@ -59,7 +59,7 @@ def test_task_with_lib_dependency(


@pytest.mark.slow
@pytest.mark.skipif(version_info < (3, 7), reason="dependencies require python>=3.7")
@pytest.mark.skipif(version_info < (3, 8), reason="dependencies require python>=3.8")
def test_task_accepts_any_args(run_poetry, projects, setup_poetry_project):
result = run_poetry(
["poe", "echo", "--lol=:D", "--version", "--help"],
Expand All @@ -72,7 +72,7 @@ def test_task_accepts_any_args(run_poetry, projects, setup_poetry_project):


@pytest.mark.slow
@pytest.mark.skipif(version_info < (3, 7), reason="dependencies require python>=3.7")
@pytest.mark.skipif(version_info < (3, 8), reason="dependencies require python>=3.8")
def test_poetry_help_without_poe_command_prefix(
run_poetry, projects, setup_poetry_project_empty_prefix
):
Expand All @@ -84,7 +84,7 @@ def test_poetry_help_without_poe_command_prefix(


@pytest.mark.slow
@pytest.mark.skipif(version_info < (3, 7), reason="dependencies require python>=3.7")
@pytest.mark.skipif(version_info < (3, 8), reason="dependencies require python>=3.8")
def test_running_tasks_without_poe_command_prefix(
run_poetry, projects, setup_poetry_project_empty_prefix
):
Expand All @@ -99,7 +99,7 @@ def test_running_tasks_without_poe_command_prefix(


@pytest.mark.slow
@pytest.mark.skipif(version_info < (3, 7), reason="dependencies require python>=3.7")
@pytest.mark.skipif(version_info < (3, 8), reason="dependencies require python>=3.8")
def test_running_poetry_command_with_hooks(run_poetry, projects, setup_poetry_project):
result = run_poetry(["env", "info"], cwd=projects["poetry_plugin"])
assert "THIS IS YOUR ENV!" in result.stdout
Expand Down