Skip to content

Conversation

@Rahul-IIT-B
Copy link

@Rahul-IIT-B Rahul-IIT-B commented Jan 14, 2026

Pull Request Check List

Resolves: #10610

When virtualenvs.in-project = true, Poetry should prioritize the local .venv even if VIRTUAL_ENV is set (e.g. when running via pipx). This PR modifies EnvManager to check for the in-project virtual environment before checking environment variables.

Changes

  • Modified EnvManager.get() to check in_project_venv_exists() earlier.
  • Added regression tests in tests/utils/env/test_env_manager_priority.py.

Verification

  • Added regression tests pass interactions with VIRTUAL_ENV and .

  • Existing tests pass.

  • Added tests for changed code.

  • Updated documentation for changed code.

Summary by Sourcery

Prioritize in-project virtual environments over externally provided VIRTUAL_ENV when resolving the active environment.

Bug Fixes:

  • Ensure EnvManager.get() selects an existing in-project .venv even when VIRTUAL_ENV is set, such as when Poetry is run via pipx or with VIRTUAL_ENV='.'.

Tests:

  • Add regression tests to verify EnvManager prefers in-project .venv over VIRTUAL_ENV, including cases with VIRTUAL_ENV set to a separate path or to '.' and after poetry env use.

Copilot AI review requested due to automatic review settings January 14, 2026 17:48
@sourcery-ai
Copy link

sourcery-ai bot commented Jan 14, 2026

Reviewer's Guide

Adjusts EnvManager environment selection to always prioritize an in-project .venv over any VIRTUAL_ENV setting and adds regression tests to lock in this precedence behavior, particularly for pipx-like scenarios and edge cases where VIRTUAL_ENV is set to '.'

Updated class diagram for EnvManager prioritizing in-project venv

classDiagram
    class EnvManager {
        - poetry
        - _env_cache
        + get(reload: bool) Env
        + in_project_venv_exists() bool
        + in_project_venv str
    }

    class Env {
        <<interface>>
        + path str
    }

    class VirtualEnv {
        + VirtualEnv(path: str)
        + path str
    }

    EnvManager --> Env : returns
    EnvManager ..> VirtualEnv : constructs
Loading

File-Level Changes

Change Details Files
Change EnvManager.get() precedence so in-project .venv is returned before considering VIRTUAL_ENV or other environment-detection logic.
  • Move in_project_venv_exists() check to the top of EnvManager.get() after reading existing env metadata.
  • Return a VirtualEnv constructed from the in-project .venv path immediately when it exists, regardless of VIRTUAL_ENV or conda variables.
  • Remove the previous in-project .venv check from the later branch that only ran when not already inside a virtual env or when env metadata was present.
src/poetry/utils/env/env_manager.py
Add regression tests ensuring in-project .venv takes priority over VIRTUAL_ENV and that env.path is never reported as '.' in info output–style flows.
  • Introduce a test class covering EnvManager.get() priority behavior when VIRTUAL_ENV points to another venv, to '.', or when reload=True after poetry env use.
  • Patch VirtualEnv in tests to use a MagicMock that preserves the venv path while avoiding real system calls.
  • Use pytest fixtures, tmp_path, and patch.dict of os.environ to simulate pipx-like parent environments and verify that the resolved env.path is the in-project .venv and not the VIRTUAL_ENV value or '.'.
  • Add a dedicated test ensuring env.path string representation is never just '.' when an in-project .venv is used.
tests/utils/env/test_env_manager_priority.py

Assessment against linked issues

Issue Objective Addressed Explanation
#10610 Ensure that when virtualenvs.in-project = true and an in-project .venv exists, Poetry prioritizes that .venv over any VIRTUAL_ENV environment variable (including from pipx), so that commands like poetry env info --path and poetry install use the .venv environment.
#10610 Prevent Poetry from resolving the active virtual environment path to just "." (the current directory) in cases where VIRTUAL_ENV is set (including to ".") and an in-project .venv exists.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `tests/utils/env/test_env_manager_priority.py:36-45` </location>
<code_context>
+    def test_get_returns_in_project_venv_when_virtual_env_is_set(
</code_context>

<issue_to_address>
**suggestion (testing):** Add regression tests for cases where no in-project .venv exists to ensure VIRTUAL_ENV behavior is unchanged

Right now the tests only cover the new priority when `virtualenvs.in-project = True` and `.venv` exists. Please also add coverage for:

1. `virtualenvs.in-project = False` with `VIRTUAL_ENV` set → `EnvManager.get()` should continue to respect `VIRTUAL_ENV`.
2. `virtualenvs.in-project = True` with no `.venv` present and `VIRTUAL_ENV` set (including `"."`) → `EnvManager.get()` should fall back to the `VIRTUAL_ENV` environment.

These will confirm the early `in_project_venv_exists()` check only affects behavior when a real in-project venv is present.

Suggested implementation:

```python
    def test_get_returns_in_project_venv_when_virtual_env_is_set(
        self,
        poetry: Poetry,
        config: Config,
        mocker: MockerFixture,
        tmp_path: Path,
        mock_virtual_env: MagicMock,
    ) -> None:
        """
        When VIRTUAL_ENV is set (e.g., running via pipx) but an in-project
        .venv exists, EnvManager.get() should return the in-project venv.
        """
        from poetry.utils.env.env_manager import EnvManager

        # Enable in-project virtualenvs and create an in-project .venv
        config.set("virtualenvs.in-project", True)
        project_venv = tmp_path / ".venv"
        project_venv.mkdir()

        env_manager = EnvManager(poetry, config, mocker.Mock())

        external_venv = tmp_path / "external-venv"
        external_venv.mkdir()

        # Even with VIRTUAL_ENV set, the in-project .venv should be preferred
        with mocker.patch.dict(
            "os.environ",
            {"VIRTUAL_ENV": str(external_venv)},
            clear=False,
        ):
            env = env_manager.get()

        assert env.path == project_venv

    def test_get_respects_virtual_env_when_in_project_disabled(
        self,
        poetry: Poetry,
        config: Config,
        mocker: MockerFixture,
        tmp_path: Path,
        mock_virtual_env: MagicMock,
    ) -> None:
        """
        When virtualenvs.in-project = False, EnvManager.get() should continue
        to respect VIRTUAL_ENV and use that environment.
        """
        from poetry.utils.env.env_manager import EnvManager

        config.set("virtualenvs.in-project", False)
        env_manager = EnvManager(poetry, config, mocker.Mock())

        external_venv = tmp_path / "external-venv"
        external_venv.mkdir()

        with mocker.patch.dict(
            "os.environ",
            {"VIRTUAL_ENV": str(external_venv)},
            clear=False,
        ):
            env = env_manager.get()

        assert env.path == external_venv

    def test_get_falls_back_to_virtual_env_when_in_project_enabled_but_no_venv(
        self,
        poetry: Poetry,
        config: Config,
        mocker: MockerFixture,
        tmp_path: Path,
        mock_virtual_env: MagicMock,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """
        When virtualenvs.in-project = True but no in-project .venv exists,
        EnvManager.get() should fall back to the environment given by
        VIRTUAL_ENV (including the special case VIRTUAL_ENV=".")
        """
        from poetry.utils.env.env_manager import EnvManager

        config.set("virtualenvs.in-project", True)
        env_manager = EnvManager(poetry, config, mocker.Mock())

        # No .venv created in the project directory

        external_venv = tmp_path / "external-venv"
        external_venv.mkdir()

        # 1. VIRTUAL_ENV points to an external venv path
        with mocker.patch.dict(
            "os.environ",
            {"VIRTUAL_ENV": str(external_venv)},
            clear=False,
        ):
            env = env_manager.get()
        assert env.path == external_venv

        # 2. VIRTUAL_ENV is ".", which should resolve to the current working directory
        monkeypatch.chdir(tmp_path)
        with mocker.patch.dict(
            "os.environ",
            {"VIRTUAL_ENV": "."},
            clear=False,
        ):
            env = env_manager.get()
        assert env.path == tmp_path

```

1. Ensure `os` and `pytest` are imported at the top of `tests/utils/env/test_env_manager_priority.py` if they are not already:
   - `import os`
   - `import pytest`
2. If `EnvManager` is already imported at module level, you can remove the local `from poetry.utils.env.env_manager import EnvManager` imports inside the tests and use the existing import instead.
3. The constructor signature for `EnvManager` may differ in your codebase. Adjust `EnvManager(poetry, config, mocker.Mock())` to match the existing tests (for example, passing an `io` instance or using a shared `env_manager` fixture) to stay consistent with current patterns.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +36 to +45
def test_get_returns_in_project_venv_when_virtual_env_is_set(
self,
poetry: Poetry,
config: Config,
mocker: MockerFixture,
tmp_path: Path,
mock_virtual_env: MagicMock,
) -> None:
"""
When VIRTUAL_ENV is set (e.g., running via pipx) but an in-project
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add regression tests for cases where no in-project .venv exists to ensure VIRTUAL_ENV behavior is unchanged

Right now the tests only cover the new priority when virtualenvs.in-project = True and .venv exists. Please also add coverage for:

  1. virtualenvs.in-project = False with VIRTUAL_ENV set → EnvManager.get() should continue to respect VIRTUAL_ENV.
  2. virtualenvs.in-project = True with no .venv present and VIRTUAL_ENV set (including ".") → EnvManager.get() should fall back to the VIRTUAL_ENV environment.

These will confirm the early in_project_venv_exists() check only affects behavior when a real in-project venv is present.

Suggested implementation:

    def test_get_returns_in_project_venv_when_virtual_env_is_set(
        self,
        poetry: Poetry,
        config: Config,
        mocker: MockerFixture,
        tmp_path: Path,
        mock_virtual_env: MagicMock,
    ) -> None:
        """
        When VIRTUAL_ENV is set (e.g., running via pipx) but an in-project
        .venv exists, EnvManager.get() should return the in-project venv.
        """
        from poetry.utils.env.env_manager import EnvManager

        # Enable in-project virtualenvs and create an in-project .venv
        config.set("virtualenvs.in-project", True)
        project_venv = tmp_path / ".venv"
        project_venv.mkdir()

        env_manager = EnvManager(poetry, config, mocker.Mock())

        external_venv = tmp_path / "external-venv"
        external_venv.mkdir()

        # Even with VIRTUAL_ENV set, the in-project .venv should be preferred
        with mocker.patch.dict(
            "os.environ",
            {"VIRTUAL_ENV": str(external_venv)},
            clear=False,
        ):
            env = env_manager.get()

        assert env.path == project_venv

    def test_get_respects_virtual_env_when_in_project_disabled(
        self,
        poetry: Poetry,
        config: Config,
        mocker: MockerFixture,
        tmp_path: Path,
        mock_virtual_env: MagicMock,
    ) -> None:
        """
        When virtualenvs.in-project = False, EnvManager.get() should continue
        to respect VIRTUAL_ENV and use that environment.
        """
        from poetry.utils.env.env_manager import EnvManager

        config.set("virtualenvs.in-project", False)
        env_manager = EnvManager(poetry, config, mocker.Mock())

        external_venv = tmp_path / "external-venv"
        external_venv.mkdir()

        with mocker.patch.dict(
            "os.environ",
            {"VIRTUAL_ENV": str(external_venv)},
            clear=False,
        ):
            env = env_manager.get()

        assert env.path == external_venv

    def test_get_falls_back_to_virtual_env_when_in_project_enabled_but_no_venv(
        self,
        poetry: Poetry,
        config: Config,
        mocker: MockerFixture,
        tmp_path: Path,
        mock_virtual_env: MagicMock,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """
        When virtualenvs.in-project = True but no in-project .venv exists,
        EnvManager.get() should fall back to the environment given by
        VIRTUAL_ENV (including the special case VIRTUAL_ENV=".")
        """
        from poetry.utils.env.env_manager import EnvManager

        config.set("virtualenvs.in-project", True)
        env_manager = EnvManager(poetry, config, mocker.Mock())

        # No .venv created in the project directory

        external_venv = tmp_path / "external-venv"
        external_venv.mkdir()

        # 1. VIRTUAL_ENV points to an external venv path
        with mocker.patch.dict(
            "os.environ",
            {"VIRTUAL_ENV": str(external_venv)},
            clear=False,
        ):
            env = env_manager.get()
        assert env.path == external_venv

        # 2. VIRTUAL_ENV is ".", which should resolve to the current working directory
        monkeypatch.chdir(tmp_path)
        with mocker.patch.dict(
            "os.environ",
            {"VIRTUAL_ENV": "."},
            clear=False,
        ):
            env = env_manager.get()
        assert env.path == tmp_path
  1. Ensure os and pytest are imported at the top of tests/utils/env/test_env_manager_priority.py if they are not already:
    • import os
    • import pytest
  2. If EnvManager is already imported at module level, you can remove the local from poetry.utils.env.env_manager import EnvManager imports inside the tests and use the existing import instead.
  3. The constructor signature for EnvManager may differ in your codebase. Adjust EnvManager(poetry, config, mocker.Mock()) to match the existing tests (for example, passing an io instance or using a shared env_manager fixture) to stay consistent with current patterns.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes an issue where Poetry was not properly prioritizing in-project virtual environments (.venv) when virtualenvs.in-project = true, even when the VIRTUAL_ENV environment variable was set (e.g., when running Poetry via pipx).

Changes:

  • Modified EnvManager.get() to check for in-project venv existence before checking the VIRTUAL_ENV environment variable
  • Added comprehensive regression tests covering various scenarios with VIRTUAL_ENV and in-project venv priority

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/poetry/utils/env/env_manager.py Moved in-project venv check to execute before VIRTUAL_ENV environment variable check
tests/utils/env/test_env_manager_priority.py Added new test suite with 4 regression tests for in-project venv prioritization

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

env = manager.get(reload=True)

assert env.path == venv_path
assert "." != str(env.path)
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion order is unconventional (literal on the left side). For consistency with other assertions in this file and standard Python practice, swap the operands to assert str(env.path) != '.'

Suggested change
assert "." != str(env.path)
assert str(env.path) != "."

Copilot uses AI. Check for mistakes.
@Rahul-IIT-B
Copy link
Author

Hi maintainers,

I see that the CI check Tests / Windows (Python 3.11) / pytest (pull_request) is failing, but the failure appears to be unrelated to my changes:

Failing test: test_call_does_not_block_on_full_pipe[sys.stderr] in [tests/utils/env/test_env.py]

My changes:

  • Modified [src/poetry/utils/env/env_manager.py] (EnvManager priority logic)
  • Added [tests/utils/env/test_env_manager_priority.py] (new regression tests)

The failing test is in a completely different file and tests subprocess pipe handling, which is unrelated to the EnvManager environment selection logic I modified.

I've fixed the pre-commit formatting issues (ruff and trailing whitespace), and my new regression tests all pass successfully.

Could you please re-run the CI when you have a chance? I believe this is a flaky test failure.

Thank you!

@dimbleby
Copy link
Contributor

what makes you believe that this is related to #10610?

I don't see any particular reason to prefer either the environment variable or the directory, in case of conflict - but changing it is going to break anyone who has been relying on the existing behaviour

@radoering
Copy link
Member

I do not think this is a desired change. VIRTUAL_ENV is an activated virtual environment which has priority by design. In other words, this is not a bug.

Poetry should prioritize the local .venv even if VIRTUAL_ENV is set (e.g. when running via pipx).

This does not make sense to me. As far as I know, VIRTUAL_ENV is not set when running a tool via pipx. At least, .venv is used when running poetry via pipx (and no venv has been activated before).

@Rahul-IIT-B
Copy link
Author

Thank you for the feedback! After re-reading issue #10610 more carefully, I realize I misunderstood the root cause.

You're absolutely right that VIRTUAL_ENV should have priority when a user has explicitly activated an environment. The actual bug reported in #10610 is that poetry env info --path returns . instead of the correct .venv path when no environment is activated.

I apologize for the confusion. I'll close this PR and take another look at the issue to understand the real problem better.

Thanks for taking the time to review and explain this!

@Rahul-IIT-B Rahul-IIT-B deleted the fix/issue-10610-env-priority branch January 15, 2026 17:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Poetry doesn't recognize the virtualenv it created; insists the virtualenv path is "."

3 participants