Skip to content

fix(env): prefer project .venv over inherited VIRTUAL_ENV with -C#10780

Open
SergioChan wants to merge 2 commits intopython-poetry:mainfrom
SergioChan:fix-10773-prefer-project-venv-on-cdir
Open

fix(env): prefer project .venv over inherited VIRTUAL_ENV with -C#10780
SergioChan wants to merge 2 commits intopython-poetry:mainfrom
SergioChan:fix-10773-prefer-project-venv-on-cdir

Conversation

@SergioChan
Copy link
Copy Markdown
Contributor

@SergioChan SergioChan commented Mar 23, 2026

Pull Request Check List

Resolves: #10773

  • Added tests for changed code.
  • Updated documentation for changed code.

Summary

  • Prefer the target project's in-project virtualenv when Poetry is invoked from outside that project while VIRTUAL_ENV is already set (for example via poetry -C <project> from another environment).
  • Keep existing behavior when running inside the target project directory: an explicitly activated VIRTUAL_ENV is still respected.
  • Add regression coverage for both paths in tests/utils/env/test_env_manager.py.

Testing

  • python -m pytest tests/utils/env/test_env_manager.py -k "prefers_in_project_venv_when_running_outside_project or keeps_active_virtualenv_when_running_inside_project or prefers_explicitly_activated_virtualenvs_over_env_var" -q

Summary by Sourcery

Adjust environment selection to prefer a target project's in-project virtualenv when invoked from outside the project while already inside another virtualenv.

Bug Fixes:

  • Ensure the in-project virtualenv is used instead of an inherited VIRTUAL_ENV when operating on a different project directory (e.g. via the -C option), while keeping the existing behavior of respecting an active virtualenv when running inside the project.

Tests:

  • Add regression tests covering environment selection when running outside the project with VIRTUAL_ENV set and when running inside the project with an active virtualenv.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 23, 2026

Reviewer's Guide

Adjusts EnvManager.get() to prefer a target project’s in-project virtualenv over an inherited VIRTUAL_ENV when Poetry is invoked from outside that project (e.g., via -C), while adding regression tests to cover both the new behavior and the preserved behavior when running inside the project directory.

Class diagram for EnvManager and virtualenv preference logic

classDiagram
    class EnvManager {
        - Poetry _poetry
        - Path in_project_venv
        Env get(reload: bool)
        bool in_project_venv_exists()
    }

    class Env {
        <<abstract>>
    }

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

    class Poetry {
        File file
    }

    class File {
        Path path
    }

    class Path

    EnvManager --> Poetry : uses
    Poetry --> File : has
    File --> Path : has

    Env <|-- VirtualEnv : extends
    EnvManager --> Env : returns
    EnvManager --> VirtualEnv : constructs

    note for EnvManager "Updated get logic:<br/>- Detects in_venv via env_prefix and conda_env_name<br/>- Computes project_path from _poetry.file.path.parent<br/>- Computes cwd from Path.cwd<br/>- If in_venv && env is None && invoked_outside_project && in_project_venv_exists:<br/>  returns VirtualEnv(in_project_venv)<br/>- Otherwise retains existing behavior, including respecting<br/>  explicitly activated VIRTUAL_ENV when running inside project"
Loading

File-Level Changes

Change Details Files
Change environment selection in EnvManager.get() to detect when Poetry is invoked from outside the project and, in that case, prefer the project’s in-project virtualenv over an inherited VIRTUAL_ENV.
  • Resolve the project path from the Poetry file and the current working directory, and detect whether the current directory is outside the project tree, including subdirectories.
  • Compute an invoked_outside_project flag that is true when the current working directory is neither the project root nor within it.
  • When in_venv is true, no explicit env is selected yet, and invoked_outside_project is true, check for an in-project virtualenv and immediately return it if present, instead of using the inherited VIRTUAL_ENV.
  • Leave existing branches intact so that when running inside the project directory, an explicitly activated VIRTUAL_ENV is still respected and the prior resolution behavior is preserved.
src/poetry/utils/env/env_manager.py
Add regression tests to validate environment selection behavior both when running outside and inside the project directory with VIRTUAL_ENV set.
  • Add a test that sets VIRTUAL_ENV, changes the working directory to a temporary outside path, calls manager.get(), and asserts that the selected environment is the project’s in-project virtualenv and that its base is sys.base_prefix.
  • Add a test that sets VIRTUAL_ENV, changes the working directory to the project directory, calls manager.get(), and asserts that the explicitly activated VIRTUAL_ENV is kept as the environment path.
  • Preserve existing test coverage for explicit virtualenv preference and environment listing behavior around the new tests.
tests/utils/env/test_env_manager.py

Assessment against linked issues

Issue Objective Addressed Explanation
#10773 Modify environment selection so that when Poetry is invoked on a target project via -C/--directory while VIRTUAL_ENV is set, the target project's in-project .venv is preferred over the inherited VIRTUAL_ENV.
#10773 Preserve existing behavior when running inside the target project directory so that an explicitly activated VIRTUAL_ENV is still respected over the in-project .venv.
#10773 Add regression tests covering both running outside the project (with -C) and inside the project to ensure correct environment selection behavior.

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
Copy Markdown

@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 2 issues, and left some high level feedback:

  • In the tests, consider using monkeypatch.setenv instead of writing to os.environ directly so environment variables are automatically restored and tests remain isolated.
  • The use of Path.is_relative_to assumes a minimum Python version of 3.9; if Poetry still supports older versions, you may need to replace this with a compatibility helper (e.g., try/except or manual path comparison).
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the tests, consider using `monkeypatch.setenv` instead of writing to `os.environ` directly so environment variables are automatically restored and tests remain isolated.
- The use of `Path.is_relative_to` assumes a minimum Python version of 3.9; if Poetry still supports older versions, you may need to replace this with a compatibility helper (e.g., try/except or manual path comparison).

## Individual Comments

### Comment 1
<location path="src/poetry/utils/env/env_manager.py" line_range="221-222" />
<code_context>

+        project_path = self._poetry.file.path.parent.resolve()
+        cwd = Path.cwd().resolve()
+        invoked_outside_project = not (
+            cwd == project_path or cwd.is_relative_to(project_path)
+        )
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Using Path.is_relative_to requires Python 3.9+; ensure this aligns with the supported runtime or add a compatibility fallback.

If any supported runtime (including bootstrap/runtime tooling, not just managed envs) can be <3.9, this will raise AttributeError at runtime. If 3.9+ is guaranteed everywhere, no change needed; otherwise, add a small compatibility helper (e.g., try `cwd.is_relative_to` and fall back to a `str(cwd).startswith(str(project_path) + os.sep)` check) or centralize this logic in a utility so the version-specific handling lives in one place.
</issue_to_address>

### Comment 2
<location path="tests/utils/env/test_env_manager.py" line_range="580-589" />
<code_context>
     assert env.base == Path(sys.base_prefix)


+def test_get_prefers_in_project_venv_when_running_outside_project(
+    tmp_path: Path,
+    manager: EnvManager,
+    in_project_venv_dir: Path,
+    monkeypatch: pytest.MonkeyPatch,
+) -> None:
+    os.environ["VIRTUAL_ENV"] = "/environment/prefix"
+    outside_cwd = tmp_path / "outside"
+    outside_cwd.mkdir()
+    monkeypatch.chdir(outside_cwd)
+
+    env = manager.get()
</code_context>
<issue_to_address>
**suggestion (testing):** Use `monkeypatch.setenv` instead of mutating `os.environ` directly to avoid cross-test leakage

This test (and the one below) assigns to `os.environ["VIRTUAL_ENV"]`, which can leak into later tests if cleanup is skipped. Since `monkeypatch` is already in use, please switch to `monkeypatch.setenv("VIRTUAL_ENV", "/environment/prefix")` in both tests so the environment is automatically restored.

Suggested implementation:

```python
def test_get_prefers_in_project_venv_when_running_outside_project(
    tmp_path: Path,
    manager: EnvManager,
    in_project_venv_dir: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("VIRTUAL_ENV", "/environment/prefix")
    outside_cwd = tmp_path / "outside"

```

In the same file (`tests/utils/env/test_env_manager.py`), inside the `test_get_keeps_active_virtualenv_when_running_inside_project` function, replace any direct assignment like:
```python
os.environ["VIRTUAL_ENV"] = "/environment/prefix"
```
(or similar) with:
```python
monkeypatch.setenv("VIRTUAL_ENV", "/environment/prefix")
```
This ensures the environment variable is automatically restored after the test and prevents cross-test leakage.
</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 +221 to +222
invoked_outside_project = not (
cwd == project_path or cwd.is_relative_to(project_path)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Using Path.is_relative_to requires Python 3.9+; ensure this aligns with the supported runtime or add a compatibility fallback.

If any supported runtime (including bootstrap/runtime tooling, not just managed envs) can be <3.9, this will raise AttributeError at runtime. If 3.9+ is guaranteed everywhere, no change needed; otherwise, add a small compatibility helper (e.g., try cwd.is_relative_to and fall back to a str(cwd).startswith(str(project_path) + os.sep) check) or centralize this logic in a utility so the version-specific handling lives in one place.

Comment on lines +580 to +589
def test_get_prefers_in_project_venv_when_running_outside_project(
tmp_path: Path,
manager: EnvManager,
in_project_venv_dir: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
os.environ["VIRTUAL_ENV"] = "/environment/prefix"
outside_cwd = tmp_path / "outside"
outside_cwd.mkdir()
monkeypatch.chdir(outside_cwd)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Use monkeypatch.setenv instead of mutating os.environ directly to avoid cross-test leakage

This test (and the one below) assigns to os.environ["VIRTUAL_ENV"], which can leak into later tests if cleanup is skipped. Since monkeypatch is already in use, please switch to monkeypatch.setenv("VIRTUAL_ENV", "/environment/prefix") in both tests so the environment is automatically restored.

Suggested implementation:

def test_get_prefers_in_project_venv_when_running_outside_project(
    tmp_path: Path,
    manager: EnvManager,
    in_project_venv_dir: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setenv("VIRTUAL_ENV", "/environment/prefix")
    outside_cwd = tmp_path / "outside"

In the same file (tests/utils/env/test_env_manager.py), inside the test_get_keeps_active_virtualenv_when_running_inside_project function, replace any direct assignment like:

os.environ["VIRTUAL_ENV"] = "/environment/prefix"

(or similar) with:

monkeypatch.setenv("VIRTUAL_ENV", "/environment/prefix")

This ensures the environment variable is automatically restored after the test and prevents cross-test leakage.

@dosubot
Copy link
Copy Markdown

dosubot bot commented Mar 23, 2026

Related Documentation

3 document(s) may need updating based on files changed in this PR:

Python Poetry

basic-usage /poetry/blob/main/docs/basic-usage.md
View Suggested Changes
@@ -168,6 +168,8 @@
 
 To take advantage of this, simply activate a virtual environment using your preferred method or tooling, before running
 any Poetry commands that expect to manipulate an environment.
+
+When Poetry is invoked with the `-C` flag to operate on a project from outside its directory, it will prefer the target project's in-project virtualenv (`.venv`) over any `VIRTUAL_ENV` environment variable from your shell. This ensures commands target the correct project's environment. When running Poetry from within the project directory itself, it continues to respect explicitly activated virtual environments as described above.
 {{% /note %}}
 
 ### Using `poetry run`

[Accept] [Decline]

cli /poetry/blob/main/docs/cli.md
View Suggested Changes
@@ -45,6 +45,12 @@
 * `--no-plugins`: Disables plugins.
 * `--no-cache`: Disables Poetry source caches.
 * `--directory=DIRECTORY (-C)`: The working directory for the Poetry command (defaults to the current working directory). All command-line arguments will be resolved relative to the given directory.
+
+{{% note %}}
+When using `--directory` to operate on a project from outside its directory, Poetry will prefer the target project's in-project virtualenv (`.venv`) over any `VIRTUAL_ENV` environment variable inherited from the calling process. This ensures that operations target the correct project's environment rather than using an unrelated active virtual environment from your shell.
+
+When running from within the project directory itself, Poetry continues to respect an explicitly activated `VIRTUAL_ENV`.
+{{% /note %}}
 * `--project=PROJECT (-P)`: Specify another path as the project root. All command-line arguments will be resolved relative to the current working directory or directory specified using `--directory` option if used.
 
 ## about

[Accept] [Decline]

managing-environments /poetry/blob/main/docs/managing-environments.md
View Suggested Changes
@@ -17,6 +17,16 @@
 To achieve this, it will first check if it's currently running inside a virtual environment.
 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.
+
+{{% note %}}
+When Poetry is invoked from outside the target project directory (for example, using `poetry -C <project-path>`), 
+it will prioritize the target project's in-project virtualenv (`.venv`) over any inherited `VIRTUAL_ENV` 
+environment variable from the calling process. This ensures that operating on another project with Poetry 
+doesn't accidentally use the wrong virtual environment when you have a different environment activated in your shell.
+
+However, when Poetry is executed from within the target project directory, an explicitly activated `VIRTUAL_ENV` 
+will be respected and used directly.
+{{% /note %}}
 
 By default, Poetry will try to use the Python version used during Poetry's installation
 to create the virtual environment for the current project.

[Accept] [Decline]

Note: You must be authenticated to accept/decline updates.

How did I do? Any feedback?  Join Discord

@SergioChan
Copy link
Copy Markdown
Contributor Author

Pushed a follow-up commit to address CI/review feedback:

  • collapsed the nested if in EnvManager.get() to satisfy ruff (SIM102)
  • switched the two new tests to monkeypatch.setenv(...) so VIRTUAL_ENV is cleaned up automatically per test

Validation run locally:

  • python -m pytest tests/utils/env/test_env_manager.py -k "prefers_in_project_venv_when_running_outside_project or keeps_active_virtualenv_when_running_inside_project" -q
  • python -m pre_commit run --all-files

@dimbleby
Copy link
Copy Markdown
Contributor

dimbleby commented Mar 23, 2026

cf #10689 - I imagine this one should be rejected for the same reasons as that one.

Plus the additional reason that behaving differently depending on whether the invocation was from "inside" or "outside" the project will be confusing.

@SergioChan
Copy link
Copy Markdown
Contributor Author

Thanks for the candid feedback — that’s fair.

The intended difference vs #10689 was to narrow behavior to -C/--directory invocations from outside the target project, while still preserving today’s behavior when running inside the project directory.

That said, I agree the inside/outside split can feel surprising from a UX perspective. If maintainers prefer to keep behavior aligned with the direction in #10689, I’m happy to close this PR and rework toward a less surprising approach (for example, surfacing a clear warning/error instead of auto-switching envs).

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.

VIRTUAL_ENV env var prevents poetry -C from finding the target project's in-project .venv

2 participants