Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/changelog/3230.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
A ``virtualenv.ini`` that fails to parse no longer crashes every invocation with an ``AttributeError``; it is skipped
with the error logged and ``--help`` reports it as ``failed to parse``. A UTF-8 byte order mark, which PowerShell 5 and
older versions of Notepad write, is now tolerated in the config file instead of being treated as a missing section
header - by :user:`darrenhuai`.
4 changes: 3 additions & 1 deletion src/virtualenv/config/ini.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def __init__(self, env: Mapping[str, str] | None = None) -> None:

exception = None
self.has_config_file = None
self.has_virtualenv_section = False
try:
self.has_config_file = self.config_file.exists()
except OSError as exc:
Expand All @@ -50,12 +51,13 @@ def __init__(self, env: Mapping[str, str] | None = None) -> None:
self._load()
self.has_virtualenv_section = self.config_parser.has_section(self.section)
except Exception as exc: # ruff:ignore[blind-except]
self.has_config_file = None # mark it failed to parse, so the config is ignored
exception = exc
if exception is not None:
LOGGER.error("failed to read config file %s because %r", config_file, exception)

def _load(self) -> None:
with self.config_file.open("rt", encoding="utf-8") as file_handler:
with self.config_file.open("rt", encoding="utf-8-sig") as file_handler:
return self.config_parser.read_file(file_handler)

def get(self, key: str, as_type: TypeData) -> tuple[Any, str] | None:
Expand Down
41 changes: 41 additions & 0 deletions tests/unit/config/test_ini.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from __future__ import annotations

import codecs
import logging
import sys
from textwrap import dedent

import pytest

from virtualenv.config.ini import IniConfig
from virtualenv.info import IS_PYPY, IS_WIN, fs_supports_symlink
from virtualenv.run import session_via_cli

Expand Down Expand Up @@ -32,3 +35,41 @@ def test_ini_can_be_overwritten_by_flag(tmp_path, monkeypatch) -> None:

symlinks = result.creator.symlinks
assert symlinks is True


def test_ini_that_fails_to_parse_is_ignored(tmp_path, caplog) -> None:
bad_ini = tmp_path / "conf.ini"
bad_ini.write_text("this line has no section header\n", encoding="utf-8")

config = IniConfig(env={"VIRTUALENV_CONFIG_FILE": str(bad_ini)})

assert bool(config) is False
assert config.has_virtualenv_section is False
assert "failed to parse" in config.epilog
assert any("failed to read config file" in r.message for r in caplog.records if r.levelno == logging.ERROR)


def test_ini_that_fails_to_parse_does_not_break_the_cli(tmp_path, monkeypatch) -> None:
bad_ini = tmp_path / "conf.ini"
bad_ini.write_text("this line has no section header\n", encoding="utf-8")
monkeypatch.setenv("VIRTUALENV_CONFIG_FILE", str(bad_ini))

result = session_via_cli(["venv"]) # the config is ignored, defaults apply

assert result.creator.clear is False


def test_ini_with_utf8_bom_is_read(tmp_path, monkeypatch) -> None:
custom_ini = tmp_path / "conf.ini"
content = dedent(
"""
[virtualenv]
clear = True
""",
)
custom_ini.write_bytes(codecs.BOM_UTF8 + content.encode("utf-8")) # how Notepad and PowerShell 5 write UTF-8
monkeypatch.setenv("VIRTUALENV_CONFIG_FILE", str(custom_ini))

result = session_via_cli(["venv"])

assert result.creator.clear is True
Loading