Skip to content

Commit 9f082bc

Browse files
chrysleuranusjrpradyunsgichard26
authored
Correctly assign configuration to origin files with pip config debug (#12201)
This commit extends the pip configuration dictionary to be nested, allowing the configuration keys to be connected with the origin files. This fixes a bug where pip config debug couldn't tell which file a value came from if the files are at the same "level". Co-authored-by: Tzu-ping Chung <[email protected]> Co-authored-by: Pradyun Gedam <[email protected]> Co-authored-by: Richard Si <[email protected]>
1 parent 8c3a68d commit 9f082bc

File tree

6 files changed

+114
-32
lines changed

6 files changed

+114
-32
lines changed

news/12099.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
``pip config debug`` now correctly separates options as set by the different files
2+
at the same level.

src/pip/_internal/cli/parser.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -188,23 +188,24 @@ def _get_ordered_configuration_items(
188188
section_items: dict[str, list[tuple[str, Any]]] = {
189189
name: [] for name in override_order
190190
}
191-
for section_key, val in self.config.items():
192-
# ignore empty values
193-
if not val:
194-
logger.debug(
195-
"Ignoring configuration key '%s' as it's value is empty.",
196-
section_key,
197-
)
198-
continue
199191

200-
section, key = section_key.split(".", 1)
201-
if section in override_order:
202-
section_items[section].append((key, val))
192+
for _, value in self.config.items(): # noqa: PERF102
193+
for section_key, val in value.items():
194+
# ignore empty values
195+
if not val:
196+
logger.debug(
197+
"Ignoring configuration key '%s' as its value is empty.",
198+
section_key,
199+
)
200+
continue
201+
202+
section, key = section_key.split(".", 1)
203+
if section in override_order:
204+
section_items[section].append((key, val))
203205

204-
# Yield each group in their override order
205-
for section in override_order:
206-
for key, val in section_items[section]:
207-
yield key, val
206+
# Yield each group in their override order
207+
for section in override_order:
208+
yield from section_items[section]
208209

209210
def _update_defaults(self, defaults: dict[str, Any]) -> dict[str, Any]:
210211
"""Updates the given defaults with values from the config files and

src/pip/_internal/commands/configuration.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@ def list_values(self, options: Values, args: list[str]) -> None:
177177
self._get_n_args(args, "list", n=0)
178178

179179
for key, value in sorted(self.configuration.items()):
180-
write_output("%s=%r", key, value)
180+
for key, value in sorted(value.items()):
181+
write_output("%s=%r", key, value)
181182

182183
def get_name(self, options: Values, args: list[str]) -> None:
183184
key = self._get_n_args(args, "get [name]", n=1)
@@ -211,13 +212,15 @@ def list_config_values(self, options: Values, args: list[str]) -> None:
211212
file_exists = os.path.exists(fname)
212213
write_output("%s, exists: %r", fname, file_exists)
213214
if file_exists:
214-
self.print_config_file_values(variant)
215+
self.print_config_file_values(variant, fname)
215216

216-
def print_config_file_values(self, variant: Kind) -> None:
217+
def print_config_file_values(self, variant: Kind, fname: str) -> None:
217218
"""Get key-value pairs from the file of a variant"""
218219
for name, value in self.configuration.get_values_in_config(variant).items():
219220
with indent_log():
220-
write_output("%s: %s", name, value)
221+
if name == fname:
222+
for confname, confvalue in value.items():
223+
write_output("%s: %s", confname, confvalue)
221224

222225
def print_env_var_values(self) -> None:
223226
"""Get key-values pairs present as environment variables"""

src/pip/_internal/configuration.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def __init__(self, isolated: bool, load_only: Kind | None = None) -> None:
117117
self._parsers: dict[Kind, list[tuple[str, RawConfigParser]]] = {
118118
variant: [] for variant in OVERRIDE_ORDER
119119
}
120-
self._config: dict[Kind, dict[str, Any]] = {
120+
self._config: dict[Kind, dict[str, dict[str, Any]]] = {
121121
variant: {} for variant in OVERRIDE_ORDER
122122
}
123123
self._modified_parsers: list[tuple[str, RawConfigParser]] = []
@@ -148,7 +148,10 @@ def get_value(self, key: str) -> Any:
148148
orig_key = key
149149
key = _normalize_name(key)
150150
try:
151-
return self._dictionary[key]
151+
clean_config: dict[str, Any] = {}
152+
for file_values in self._dictionary.values():
153+
clean_config.update(file_values)
154+
return clean_config[key]
152155
except KeyError:
153156
# disassembling triggers a more useful error message than simply
154157
# "No such key" in the case that the key isn't in the form command.option
@@ -171,7 +174,8 @@ def set_value(self, key: str, value: Any) -> None:
171174
parser.add_section(section)
172175
parser.set(section, name, value)
173176

174-
self._config[self.load_only][key] = value
177+
self._config[self.load_only].setdefault(fname, {})
178+
self._config[self.load_only][fname][key] = value
175179
self._mark_as_modified(fname, parser)
176180

177181
def unset_value(self, key: str) -> None:
@@ -181,11 +185,14 @@ def unset_value(self, key: str) -> None:
181185
self._ensure_have_load_only()
182186

183187
assert self.load_only
184-
if key not in self._config[self.load_only]:
185-
raise ConfigurationError(f"No such key - {orig_key}")
186-
187188
fname, parser = self._get_parser_to_modify()
188189

190+
if (
191+
key not in self._config[self.load_only][fname]
192+
and key not in self._config[self.load_only]
193+
):
194+
raise ConfigurationError(f"No such key - {orig_key}")
195+
189196
if parser is not None:
190197
section, name = _disassemble_key(key)
191198
if not (
@@ -200,8 +207,10 @@ def unset_value(self, key: str) -> None:
200207
if not parser.items(section):
201208
parser.remove_section(section)
202209
self._mark_as_modified(fname, parser)
203-
204-
del self._config[self.load_only][key]
210+
try:
211+
del self._config[self.load_only][fname][key]
212+
except KeyError:
213+
del self._config[self.load_only][key]
205214

206215
def save(self) -> None:
207216
"""Save the current in-memory state."""
@@ -233,7 +242,7 @@ def _ensure_have_load_only(self) -> None:
233242
logger.debug("Will be working with %s variant only", self.load_only)
234243

235244
@property
236-
def _dictionary(self) -> dict[str, Any]:
245+
def _dictionary(self) -> dict[str, dict[str, Any]]:
237246
"""A dictionary representing the loaded configuration."""
238247
# NOTE: Dictionaries are not populated if not loaded. So, conditionals
239248
# are not needed here.
@@ -273,7 +282,8 @@ def _load_file(self, variant: Kind, fname: str) -> RawConfigParser:
273282

274283
for section in parser.sections():
275284
items = parser.items(section)
276-
self._config[variant].update(self._normalized_keys(section, items))
285+
self._config[variant].setdefault(fname, {})
286+
self._config[variant][fname].update(self._normalized_keys(section, items))
277287

278288
return parser
279289

@@ -300,7 +310,8 @@ def _construct_parser(self, fname: str) -> RawConfigParser:
300310

301311
def _load_environment_vars(self) -> None:
302312
"""Loads configuration from environment variables"""
303-
self._config[kinds.ENV_VAR].update(
313+
self._config[kinds.ENV_VAR].setdefault(":env:", {})
314+
self._config[kinds.ENV_VAR][":env:"].update(
304315
self._normalized_keys(":env:", self.get_environ_vars())
305316
)
306317

tests/functional/test_configuration.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,43 @@
11
"""Tests for the config command"""
22

3+
from __future__ import annotations
4+
5+
import ast
6+
import os
37
import re
8+
import subprocess
9+
import sys
410
import textwrap
511

612
from pip._internal.cli.status_codes import ERROR
7-
from pip._internal.configuration import CONFIG_BASENAME, get_configuration_files
13+
from pip._internal.configuration import CONFIG_BASENAME, Kind
14+
from pip._internal.configuration import get_configuration_files as _get_config_files
15+
from pip._internal.utils.compat import WINDOWS
816

917
from tests.lib import PipTestEnvironment
1018
from tests.lib.configuration_helpers import ConfigurationMixin, kinds
1119
from tests.lib.venv import VirtualEnvironment
1220

1321

22+
def get_configuration_files() -> dict[Kind, list[str]]:
23+
"""Wrapper over pip._internal.configuration.get_configuration_files()."""
24+
if WINDOWS:
25+
# The user configuration directory is updated in the isolate fixture using the
26+
# APPDATA environment variable. This will only take effect in new subprocesses,
27+
# however. To ensure that get_configuration_files() never returns stale data,
28+
# call it in a subprocess on Windows.
29+
code = (
30+
"from pip._internal.configuration import get_configuration_files; "
31+
"print(get_configuration_files())"
32+
)
33+
proc = subprocess.run(
34+
[sys.executable, "-c", code], capture_output=True, encoding="utf-8"
35+
)
36+
return ast.literal_eval(proc.stdout)
37+
else:
38+
return _get_config_files()
39+
40+
1441
def test_no_options_passed_should_error(script: PipTestEnvironment) -> None:
1542
result = script.pip("config", expect_error=True)
1643
assert result.returncode == ERROR
@@ -148,3 +175,40 @@ def test_editor_does_not_exist(self, script: PipTestEnvironment) -> None:
148175
"config", "edit", "--editor", "notrealeditor", expect_error=True
149176
)
150177
assert "notrealeditor" in result.stderr
178+
179+
def test_config_separated(
180+
self, script: PipTestEnvironment, virtualenv: VirtualEnvironment
181+
) -> None:
182+
"""Test that the pip configuration values in the different config sections
183+
are correctly assigned to their origin files.
184+
"""
185+
186+
# Use new config file
187+
new_config_file = get_configuration_files()[kinds.USER][1]
188+
189+
# Get legacy config file and touch it for testing purposes
190+
legacy_config_file = get_configuration_files()[kinds.USER][0]
191+
os.makedirs(os.path.dirname(legacy_config_file))
192+
open(legacy_config_file, "a").close()
193+
194+
# Site config file
195+
site_config_file = virtualenv.location / CONFIG_BASENAME
196+
197+
script.pip("config", "--user", "set", "global.timeout", "60")
198+
script.pip("config", "--site", "set", "freeze.timeout", "10")
199+
200+
result = script.pip("config", "debug")
201+
202+
assert (
203+
f"{site_config_file}, exists: True\n freeze.timeout: 10" in result.stdout
204+
)
205+
assert (
206+
f"{new_config_file}, exists: True\n global.timeout: 60" in result.stdout
207+
)
208+
assert re.search(
209+
(
210+
rf"{re.escape(legacy_config_file)}, "
211+
rf"exists: True\n( {re.escape(new_config_file)}.+\n)+"
212+
),
213+
result.stdout,
214+
)

tests/lib/configuration_helpers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ def patch_configuration(self, variant: Kind, di: dict[str, Any]) -> None:
2828
@functools.wraps(old)
2929
def overridden() -> None:
3030
# Manual Overload
31-
self.configuration._config[variant].update(di)
31+
self.configuration._config[variant].setdefault("fakefile", {})
32+
self.configuration._config[variant]["fakefile"].update(di)
3233
# Configuration._parsers has type:
3334
# Dict[Kind, List[Tuple[str, RawConfigParser]]].
3435
# As a testing convenience, pass a special value.

0 commit comments

Comments
 (0)