Skip to content

env: improve error handling if .venv is not a directory but a file#10777

Merged
radoering merged 1 commit intopython-poetry:mainfrom
radoering:venv-is-file
Mar 28, 2026
Merged

env: improve error handling if .venv is not a directory but a file#10777
radoering merged 1 commit intopython-poetry:mainfrom
radoering:venv-is-file

Conversation

@radoering
Copy link
Copy Markdown
Member

@radoering radoering commented Mar 21, 2026

Pull Request Check List

Currently, if an in-project .venv is a file instead of a directory, Poetry errors out with

[Errno 2] No such file or directory: 'python'

With the fix, a warning is printed, the file is removed and a venv is created.

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

Summary by Sourcery

Handle in-project virtual environment path correctly when it is a file instead of a directory and ensure a proper virtualenv is created with user-visible warnings.

Bug Fixes:

  • Prevent activation failures when an in-project .venv path exists as a file by treating it as invalid and recreating the virtual environment.

Enhancements:

  • Emit a clear warning and automatically remove an invalid .venv file before creating a new virtual environment, and only treat existing directories as valid virtualenvs.

Tests:

  • Add regression test covering activation behavior when an in-project .venv is a file, including verification of warning output and venv recreation.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 21, 2026

Reviewer's Guide

Improves EnvManager’s handling of in-project .venv paths by treating non-directory paths as invalid, cleaning up stray .venv files, emitting a warning, and creating a fresh virtualenv, with tests and fixtures updated to cover the new behavior and IO dependency.

Sequence diagram for EnvManager.create_venv when .venv is a file

sequenceDiagram
    actor Developer
    participant PoetryCLI
    participant EnvManager
    participant Path_venv
    participant IO
    participant VirtualEnv

    Developer->>PoetryCLI: run_command
    PoetryCLI->>EnvManager: create_venv(name, io, options)

    EnvManager->>Path_venv: is_dir()
    alt venv_is_not_directory
        EnvManager->>Path_venv: is_file()
        alt venv_is_file
            EnvManager->>IO: write_error_line(<warning>.venv is not a virtual environment but a file. Removing it.)
            EnvManager->>Path_venv: unlink()
        else venv_path_missing_or_other
            Note over EnvManager,Path_venv: No file removal is performed
        end

        EnvManager->>IO: write_error_line(Creating virtualenv ...)
        EnvManager->>VirtualEnv: create_at(venv_path, name)
        VirtualEnv-->>EnvManager: venv_instance
        EnvManager-->>PoetryCLI: VirtualEnvEnv
    else venv_is_directory
        EnvManager->>VirtualEnv: use_existing(venv_path)
        VirtualEnv-->>EnvManager: venv_instance
        EnvManager-->>PoetryCLI: VirtualEnvEnv
    end

    PoetryCLI-->>Developer: command_result
Loading

Updated class diagram for EnvManager virtualenv handling

classDiagram
    class EnvManager {
        - IO _io
        - Path in_project_venv
        + use_in_project_venv() bool
        + activate(python) Env
        + create_venv(name, io, options) Env
        + get_system_env() Env
    }

    class VirtualEnv {
        + version_info tuple
        + VirtualEnv(path)
    }

    class IO {
        + write_error_line(message) void
    }

    class Path {
        + is_dir() bool
        + is_file() bool
        + exists() bool
        + unlink() void
    }

    class Env {
    }

    EnvManager --> IO : uses
    EnvManager --> Path : manages in_project_venv
    EnvManager --> VirtualEnv : creates_and_checks
    VirtualEnv --> Env : returns
    Path <|.. Path_venv : instance

    %% Key behavioral changes
    %% - activate now checks in_project_venv.is_dir() instead of exists()
    %% - create_venv treats paths where !is_dir() as invalid
    %% - create_venv checks is_file(), warns via IO, and calls unlink() before creating a new VirtualEnv
Loading

File-Level Changes

Change Details Files
Tighten checks around in-project virtualenv paths and handle the case where .venv is a regular file by warning, deleting it, and proceeding to create a new environment.
  • Change activate to only treat the in-project venv as existing if the path is a directory, avoiding misuse when .venv is a file.
  • Change create_venv to base its logic on is_dir() instead of exists() so non-directory paths are treated as missing virtualenvs.
  • When .venv is a file and a new venv should be created, log a warning via the injected IO and unlink the file before creating the venv.
  • Keep the existing behavior when venv creation is disabled, still falling back to the system environment.
src/poetry/utils/env/env_manager.py
Add test coverage for the .venv-is-a-file scenario and wire EnvManager to use a BufferedIO test fixture so warnings can be asserted.
  • Introduce an io fixture that provides a BufferedIO instance for tests and inject it into EnvManager via an updated manager fixture.
  • Add a test that configures in-project virtualenvs, creates a .venv file, activates the environment, asserts that build_venv is called with the expected arguments, and verifies the warning message is written to error output.
  • Assert that the envs.toml tracking file is not created in this scenario, matching the expected side effects.
tests/utils/env/test_env_manager.py
tests/utils/env/conftest.py

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:

  • When unlinking the unexpected .venv file, consider wrapping venv.unlink() in a try/except for OSError and surfacing a clearer error or fallback behavior in case of permission or race-condition failures.
  • The special-case handling only covers .venv being a regular file; if .venv is a broken symlink or other non-directory path, you might want to treat it consistently (e.g., warn and remove) rather than relying solely on is_dir().
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- When unlinking the unexpected `.venv` file, consider wrapping `venv.unlink()` in a try/except for `OSError` and surfacing a clearer error or fallback behavior in case of permission or race-condition failures.
- The special-case handling only covers `.venv` being a regular file; if `.venv` is a broken symlink or other non-directory path, you might want to treat it consistently (e.g., warn and remove) rather than relying solely on `is_dir()`.

## Individual Comments

### Comment 1
<location path="tests/utils/env/test_env_manager.py" line_range="484" />
<code_context>
+    venv_flags_default: dict[str, bool],
+    mocked_python_register: MockedPythonRegister,
+) -> None:
+    if "VIRTUAL_ENV" in os.environ:
+        del os.environ["VIRTUAL_ENV"]
+
</code_context>
<issue_to_address>
**suggestion (testing):** Use pytest's `monkeypatch` fixture instead of manually mutating `os.environ`.

Directly changing `os.environ` in tests can leak state between tests if an exception occurs before cleanup. Instead, take a `monkeypatch` fixture and use `monkeypatch.delenv("VIRTUAL_ENV", raising=False)` (or `setenv`/`delenv` as appropriate) so the environment is automatically restored and tests remain isolated.

Suggested implementation:

```python
def test_activate_with_in_project_setting_if_venv_is_file(
    manager: EnvManager,
    poetry: Poetry,
    io: BufferedIO,
    config: Config,
    tmp_path: Path,
    mocker: MockerFixture,
    venv_flags_default: dict[str, bool],
    mocked_python_register: MockedPythonRegister,
    monkeypatch,
) -> None:

```

```python
) -> None:
    monkeypatch.delenv("VIRTUAL_ENV", raising=False)

    from cleo.io.buffered_io import BufferedIO
    from pytest import LogCaptureFixture
    from pytest_mock import MockerFixture

```
</issue_to_address>

### Comment 2
<location path="tests/utils/env/test_env_manager.py" line_range="516-518" />
<code_context>
+    envs_file = TOMLFile(tmp_path / "virtualenvs" / "envs.toml")
+    assert not envs_file.exists()
+
+    assert (
+        f"{venv_path} is not a virtual environment but a file. Removing it."
+        in io.fetch_error()
+    )
+
</code_context>
<issue_to_address>
**suggestion (testing):** Also assert that the `.venv` file was actually removed to fully validate the new behavior.

Right now the test only checks the warning and that `build_venv` is called. Please also add an assertion like `assert not venv_path.exists()` after `manager.activate(...)` to confirm the `.venv` file is actually removed.

Suggested implementation:

```python
def test_activate_with_in_project_setting_if_venv_is_file(
    manager: EnvManager,
    poetry: Poetry,

```

```python
    manager.activate(poetry, io)
    # Ensure the .venv file that was masquerading as an environment is removed
    assert not venv_path.exists()

```

Because I can only see part of the file, you may need to adjust the `SEARCH` pattern to match your actual code:

1. Ensure the second `SEARCH` block matches the exact `manager.activate(...)` line in `test_activate_with_in_project_setting_if_venv_is_file`. For example, it might be `manager.activate(poetry, io)` or `manager.activate(poetry, io, ...)`.
2. If the path to the file is not stored in `venv_path`, adapt the assertion accordingly, e.g.:
   ```python
   venv_path = poetry.file.path.parent / ".venv"
   ...
   assert not venv_path.exists()
   ```
3. If `venv_path` is defined earlier in the test with a different name, replace `venv_path` in the new assertion with that variable name.
</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.

@abn
Copy link
Copy Markdown
Member

abn commented Mar 22, 2026

Should we remove the file or simply error out with a better message? Feels wrong to remove something we did not create or manage.

@radoering
Copy link
Copy Markdown
Member Author

If there is a .venv directory that is not a valid virtual environment, we just remove and "recreate" it. I felt it would be consistent to also remove a file. However, I do not have a strong opinion.

@abn
Copy link
Copy Markdown
Member

abn commented Mar 22, 2026

Me neither; I think removing it and recreating it is the opinionated approach. Just feels somehow weird. I guess we can go with it and see what happens. It is unlikely that this is going to make disruptions, unless well .venv is a symlink that we do not correctly detect.

@radoering radoering merged commit 8f6d382 into python-poetry:main Mar 28, 2026
54 checks passed
radoering added a commit that referenced this pull request Mar 28, 2026
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.

2 participants