Skip to content

Commit bab5bfc

Browse files
authored
Merge pull request #11073 from wimglenn/issue-9330
``pip config`` normalizes names, converting underscores into dashes
2 parents 8d51b83 + ae1c2e3 commit bab5bfc

File tree

4 files changed

+46
-8
lines changed

4 files changed

+46
-8
lines changed

docs/html/development/contributing.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ The contents of this file are reStructuredText formatted text that
8888
will be used as the content of the news file entry. You do not need to
8989
reference the issue or PR numbers in the entry, since ``towncrier``
9090
will automatically add a reference to all of the affected issues when
91-
rendering the NEWS file.
91+
rendering the NEWS file. There must be a newline at the end of the file.
9292

9393
In order to maintain a consistent style in the ``NEWS.rst`` file, it is
9494
preferred to keep the news entry to the point, in sentence case, shorter than

news/9330.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
``pip config`` now normalizes names by converting underscores into dashes.

src/pip/_internal/configuration.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,19 @@ def items(self) -> Iterable[Tuple[str, Any]]:
142142

143143
def get_value(self, key: str) -> Any:
144144
"""Get a value from the configuration."""
145+
orig_key = key
146+
key = _normalize_name(key)
145147
try:
146148
return self._dictionary[key]
147149
except KeyError:
148-
raise ConfigurationError(f"No such key - {key}")
150+
# disassembling triggers a more useful error message than simply
151+
# "No such key" in the case that the key isn't in the form command.option
152+
_disassemble_key(key)
153+
raise ConfigurationError(f"No such key - {orig_key}")
149154

150155
def set_value(self, key: str, value: Any) -> None:
151156
"""Modify a value in the configuration."""
157+
key = _normalize_name(key)
152158
self._ensure_have_load_only()
153159

154160
assert self.load_only
@@ -167,11 +173,13 @@ def set_value(self, key: str, value: Any) -> None:
167173

168174
def unset_value(self, key: str) -> None:
169175
"""Unset a value in the configuration."""
176+
orig_key = key
177+
key = _normalize_name(key)
170178
self._ensure_have_load_only()
171179

172180
assert self.load_only
173181
if key not in self._config[self.load_only]:
174-
raise ConfigurationError(f"No such key - {key}")
182+
raise ConfigurationError(f"No such key - {orig_key}")
175183

176184
fname, parser = self._get_parser_to_modify()
177185

tests/unit/test_configuration.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Tests for all things related to the configuration
22
"""
33

4+
import re
45
from unittest.mock import MagicMock
56

67
import pytest
@@ -87,6 +88,25 @@ def test_environment_config_errors_if_malformed(
8788
err.value
8889
)
8990

91+
def test_no_such_key_error_message_no_command(self) -> None:
92+
self.configuration.load_only = kinds.GLOBAL
93+
self.configuration.load()
94+
expected_msg = (
95+
"Key does not contain dot separated section and key. "
96+
"Perhaps you wanted to use 'global.index-url' instead?"
97+
)
98+
pat = f"^{re.escape(expected_msg)}$"
99+
with pytest.raises(ConfigurationError, match=pat):
100+
self.configuration.get_value("index-url")
101+
102+
def test_no_such_key_error_message_missing_option(self) -> None:
103+
self.configuration.load_only = kinds.GLOBAL
104+
self.configuration.load()
105+
expected_msg = "No such key - global.index-url"
106+
pat = f"^{re.escape(expected_msg)}$"
107+
with pytest.raises(ConfigurationError, match=pat):
108+
self.configuration.get_value("global.index-url")
109+
90110

91111
class TestConfigurationPrecedence(ConfigurationMixin):
92112
# Tests for methods to that determine the order of precedence of
@@ -185,12 +205,8 @@ class TestConfigurationModification(ConfigurationMixin):
185205
def test_no_specific_given_modification(self) -> None:
186206
self.configuration.load()
187207

188-
try:
208+
with pytest.raises(ConfigurationError):
189209
self.configuration.set_value("test.hello", "10")
190-
except ConfigurationError:
191-
pass
192-
else:
193-
assert False, "Should have raised an error."
194210

195211
def test_site_modification(self) -> None:
196212
self.configuration.load_only = kinds.SITE
@@ -241,3 +257,16 @@ def test_global_modification(self) -> None:
241257
# get the path to user config file
242258
assert mymock.call_count == 1
243259
assert mymock.call_args[0][0] == (get_configuration_files()[kinds.GLOBAL][-1])
260+
261+
def test_normalization(self) -> None:
262+
# underscores and dashes can be used interchangeably.
263+
# internally, underscores get converted into dashes before reading/writing file
264+
self.configuration.load_only = kinds.GLOBAL
265+
self.configuration.load()
266+
self.configuration.set_value("global.index_url", "example.org")
267+
assert self.configuration.get_value("global.index_url") == "example.org"
268+
assert self.configuration.get_value("global.index-url") == "example.org"
269+
self.configuration.unset_value("global.index-url")
270+
pat = r"^No such key - global\.index-url$"
271+
with pytest.raises(ConfigurationError, match=pat):
272+
self.configuration.get_value("global.index-url")

0 commit comments

Comments
 (0)