Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b8d2cde
Fix user configuration error when trying to change global settings
sepehr-rs Aug 11, 2025
46518e1
Add news entry file
sepehr-rs Aug 11, 2025
7614729
Merge branch 'main' into fix-config-error
sepehr-rs Aug 11, 2025
34adca7
Update 13279.bugfix.rst
sepehr-rs Aug 11, 2025
d024fc8
Update configuration.py to avoid pre-commit errors
sepehr-rs Aug 11, 2025
2abf374
Merge branch 'main' into fix-config-error
sepehr-rs Sep 29, 2025
5022272
Merge branch 'main' into fix-config-error
sepehr-rs Oct 24, 2025
ae1b2e3
Merge branch 'main' into fix-config-error
sepehr-rs Oct 26, 2025
ceade38
Merge branch 'main' into fix-config-error
sepehr-rs Feb 24, 2026
3b31ff4
Added a patch for #12706
sepehr-rs Feb 24, 2026
c121eb5
Merge branch 'fix-config-error' of https://github.com/sepehr-rs/pip i…
sepehr-rs Feb 24, 2026
059a21e
Fix precommit
sepehr-rs Feb 24, 2026
fb81f59
Merge branch 'main' into fix-config-error
sepehr-rs Apr 19, 2026
771f386
Merge branch 'main' into fix-config-error
sepehr-rs Apr 24, 2026
df94b6d
Merge branch 'main' into fix-config-error
sepehr-rs Apr 26, 2026
4c92e2a
Merge branch 'main' into fix-config-error
sepehr-rs May 17, 2026
ccd8658
Merge branch 'main' into fix-config-error
sepehr-rs May 30, 2026
4ede1ce
Change logger warning message
sepehr-rs May 30, 2026
6f4237f
Merge branch 'main' into fix-config-error
sepehr-rs May 31, 2026
ca88757
Merge branch 'main' into fix-config-error
sepehr-rs Jun 15, 2026
8aa4aa7
Address reviews
sepehr-rs Jul 5, 2026
706c374
Add tests
sepehr-rs Jul 7, 2026
96f3c97
Merge branch 'main' into fix-config-error
sepehr-rs Jul 25, 2026
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
1 change: 1 addition & 0 deletions news/13279.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed fatal error when using ``PIP_CONFIG_FILE`` environment variable with ``pip config set``
71 changes: 56 additions & 15 deletions src/pip/_internal/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ def get_configuration_files() -> dict[Kind, list[str]]:
}


class _RawConfigParser(configparser.RawConfigParser):
"""RawConfigParser with normalized key names."""

def optionxform(self, optionstr: str) -> str:
return _normalize_name(optionstr)


class Configuration:
"""Handles management of configuration.

Expand Down Expand Up @@ -119,6 +126,7 @@ def __init__(self, isolated: bool, load_only: Kind | None = None) -> None:
variant: {} for variant in OVERRIDE_ORDER
}
self._modified_parsers: list[tuple[str, RawConfigParser]] = []
self._use_env = False

def load(self) -> None:
"""Loads configuration from configuration files and environment"""
Expand All @@ -132,7 +140,7 @@ def get_file_to_edit(self) -> str | None:

try:
return self._get_parser_to_modify()[0]
except IndexError:
except ConfigurationError:
return None

def items(self) -> Iterable[tuple[str, Any]]:
Expand Down Expand Up @@ -183,12 +191,10 @@ def unset_value(self, key: str) -> None:
self._ensure_have_load_only()

assert self.load_only
fname, parser = self._get_parser_to_modify()

if (
key not in self._config[self.load_only][fname]
and key not in self._config[self.load_only]
):
fname, parser = self._get_parser_to_modify(key)
load_config = kinds.ENV if self._use_env else self.load_only
file_config = self._config[load_config].get(fname, {})
if key not in file_config:
raise ConfigurationError(f"No such key - {orig_key}")

if parser is not None:
Expand All @@ -206,9 +212,9 @@ def unset_value(self, key: str) -> None:
parser.remove_section(section)
self._mark_as_modified(fname, parser)
try:
del self._config[self.load_only][fname][key]
del self._config[load_config][fname][key]
except KeyError:
del self._config[self.load_only][key]
del self._config[load_config][key]

def save(self) -> None:
"""Save the current in-memory state."""
Expand Down Expand Up @@ -261,13 +267,20 @@ def _load_config_files(self) -> None:
)
return

env_config_file = os.environ.get("PIP_CONFIG_FILE")
for variant, files in config_files.items():
for fname in files:
# If there's specific variant set in `load_only`, load only
# that variant, not the others.
if self.load_only is not None and variant != self.load_only:
logger.debug("Skipping file '%s' (variant: %s)", fname, variant)
continue
is_env_redirect = (
variant == kinds.ENV
and fname == env_config_file
and self.load_only in (kinds.USER, kinds.GLOBAL)
)
if not is_env_redirect:
logger.debug("Skipping file '%s' (variant: %s)", fname, variant)
continue

parser = self._load_file(variant, fname)

Expand All @@ -278,15 +291,15 @@ def _load_file(self, variant: Kind, fname: str) -> RawConfigParser:
logger.verbose("For variant '%s', will try loading '%s'", variant, fname)
parser = self._construct_parser(fname)

self._config[variant].setdefault(fname, {})
for section in parser.sections():
items = parser.items(section)
self._config[variant].setdefault(fname, {})
self._config[variant][fname].update(self._normalized_keys(section, items))

return parser

def _construct_parser(self, fname: str) -> RawConfigParser:
parser = configparser.RawConfigParser()
parser = _RawConfigParser()
# If there is no such file, don't bother reading it but create the
# parser anyway, to hold the data.
# Doing this is useful when modifying and saving files, where we don't
Expand Down Expand Up @@ -372,16 +385,44 @@ def get_values_in_config(self, variant: Kind) -> dict[str, Any]:
"""Get values present in a config file"""
return self._config[variant]

def _get_parser_to_modify(self) -> tuple[str, RawConfigParser]:
def _get_parser_to_modify(
self, key: str | None = None
) -> tuple[str, RawConfigParser]:
# Determine which parser to modify
assert self.load_only
parsers = self._parsers[self.load_only]
if not parsers:
# This should not happen if everything works correctly.
env_config_file = os.environ.get("PIP_CONFIG_FILE")
Comment thread
sepehr-rs marked this conversation as resolved.
if env_config_file == os.devnull:
raise ConfigurationError(
"Cannot write to PIP_CONFIG_VALUE when it is set to "
f"{env_config_file}"
)
if env_config_file and self.load_only in (kinds.USER, kinds.GLOBAL):
parser = self._construct_parser(env_config_file)
self._parsers[self.load_only].append((env_config_file, parser))
self._use_env = True
logger.warning(
"Because PIP_CONFIG_FILE is set, changes will be applied to "
"'%s', not to '%s'. To restore normal behavior, "
"unset PIP_CONFIG_FILE.",
env_config_file,
self.load_only,
)
return env_config_file, parser

raise ConfigurationError(
"Fatal Internal error [id=2]. Please report as a bug."
)

if key is None:
return parsers[-1]

section, name = _disassemble_key(key)
for fname, parser in reversed(parsers):
if parser.has_section(section) and parser.has_option(section, name):
Comment thread
sepehr-rs marked this conversation as resolved.
return fname, parser

# Use the highest priority parser.
return parsers[-1]

Expand Down
57 changes: 57 additions & 0 deletions tests/functional/test_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,63 @@ def test_env_values(self, script: PipTestEnvironment) -> None:
assert "freeze.timeout: 10" in result.stdout
assert re.search(r"env:\n( .+\n)+", result.stdout)

def test_env_set_values(self, script: PipTestEnvironment) -> None:
"""Test that custom pip configuration using the environment variable
PIP_CONFIG_FILE doesn't fail when setting values."""

config_file = script.scratch_path / "test-pip.cfg"
config_file.write_text("")
script.environ["PIP_CONFIG_FILE"] = str(config_file)

result = script.pip(
"config",
"set",
"global.index-url",
"https://example.com/",
allow_stderr_warning=True,
)
assert "Because PIP_CONFIG_FILE is set, changes" in result.stderr
assert "Writing to" in result.stdout
assert result.returncode == 0

def test_env_unset_values(self, script: PipTestEnvironment) -> None:
"""Test that custom pip configuration using the environment variable
PIP_CONFIG_FILE doesn't fail when unsetting values."""

config_file = script.scratch_path / "test-pip.cfg"
config_file.write_text("")
script.environ["PIP_CONFIG_FILE"] = str(config_file)

script.pip(
"config",
"set",
"global.index-url",
"https://example.com/",
allow_stderr_warning=True,
)
result = script.pip(
"config", "unset", "global.index-url", allow_stderr_warning=True
)
assert "Because PIP_CONFIG_FILE is set, changes" in result.stderr
assert "Writing to" in result.stdout
assert result.returncode == 0

def test_env_raises_error_on_devnull_directory(
self, script: PipTestEnvironment
) -> None:
config_file = os.devnull
script.environ["PIP_CONFIG_FILE"] = str(config_file)

result = script.pip(
"config",
"set",
"global.index-url",
"https://example.com/",
expect_error=True,
)
assert "Cannot write to PIP_CONFIG_VALUE" in result.stderr
assert result.returncode == 1

def test_user_values(self, script: PipTestEnvironment) -> None:
"""Test that the user pip configuration set using --user
is correctly displayed under "user". This configuration takes place
Expand Down
Loading