Repository: pypa/pip

Issue:
Fatal Internal error while trying to unset configuration
**Environment**

* pip version: 20.3.3
* Python version: 3.9.0
* OS: OS/X 10.15.7

I'm using pyenv. I'm not an active Python developer, so please bear with me if I'm missing important details.

**Description**
I try to remove the "global.extra-index-url" key from the pip configuration by:

    `pip config unset global.extra-index-url`

Which then responses with:

    `ERROR: Fatal Internal error [id=1]. Please report as a bug.`

**Expected behavior**

I would expect to either remove the key or to prompt an error message that I could use to investigate into the reason of the failing of the command.

**How to Reproduce**
Sorry, I don't have any clue, how this could be reproduced.

**Output**

```
% pip config unset global.extra-index-url 
ERROR: Fatal Internal error [id=1]. Please report as a bug.
```

Maybe the current value of the configuration gives a clue (some values slightly changed):

```
% pip config list
global.extra-index-url='\nhttps://torsten:keykeykeykey@customer.jfrog.io/customer/api/pypi/    https://customer.jfrog.io/customer/api/pypi/customer-pip3-stable/simple\nhttps://pypi.python.org/simple/'
```

There was a similar issue reported last year: #6940 Unfortunately, the issue was closed without giving and hint to how it was resolved.


Discussion:
cc @dinko-pehar (who opened #6940)
This probably has something to do with the fact that this is a multi-value property. 
My workaround was, to edit `~/.pip/pip.conf` and to remove that line with an editor.
is there a verbose option, you should have tried that
I am unable to reproduce
Ditto. I'm not able to reproduce this either?
I got into the situation by following the instructions of a colleague to edit `~/.pip/pip.conf` and to insert something like this:

    [global]
        extra-index-url =
            https://google.de
            https://pypi.python.org/simple/

If I set the config file to this content, I get reproducible the above error message.

I do reproduce similar issues. I suspect it's related to the legacy configuration path `~/.pip/pip.conf`. Moving the file to the new default path `~/.config/pip/pip/conf` solves the issue.
I also got there.  Not sure how.  

```
$ pip --version
pip 22.0.4 from /private/tmp/v/.venv/lib/python3.10/site-packages/pip (python 3.10)
$ pip config debug
env_var:
env:
global:
  /Library/Application Support/pip/pip.conf, exists: False
site:
  /private/tmp/v/.venv/pip.conf, exists: True
    global.index-url: http://localhost:8080/
user:
  /Users/wim/.pip/pip.conf, exists: False
  /Users/wim/.config/pip/pip.conf, exists: False
$ cat .venv/pip.conf 
[global]
index_url = http://localhost:8080/

$  v pip config unset global.index-url    
ERROR: Fatal Internal error [id=1]. Please report as a bug.
$ pip config unset global.index-url -vvvv
Skipping file '/Library/Application Support/pip/pip.conf' (variant: global)
Skipping file '/Users/wim/.pip/pip.conf' (variant: user)
Skipping file '/Users/wim/.config/pip/pip.conf' (variant: user)
For variant 'site', will try loading '/private/tmp/v/.venv/pip.conf'
Will be working with site variant only
ERROR: Fatal Internal error [id=1]. Please report as a bug.
```
OK, I scratched the itch and have a reproducer:

```
pip config set global.index_url http://localhost:8080/
pip config unset global.index-url
```

note the order of dashes and underscores.



Resolving patch:
diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py
--- a/src/pip/_internal/configuration.py
+++ b/src/pip/_internal/configuration.py
@@ -142,13 +142,19 @@ def items(self) -> Iterable[Tuple[str, Any]]:
 
     def get_value(self, key: str) -> Any:
         """Get a value from the configuration."""
+        orig_key = key
+        key = _normalize_name(key)
         try:
             return self._dictionary[key]
         except KeyError:
-            raise ConfigurationError(f"No such key - {key}")
+            # disassembling triggers a more useful error message than simply
+            # "No such key" in the case that the key isn't in the form command.option
+            _disassemble_key(key)
+            raise ConfigurationError(f"No such key - {orig_key}")
 
     def set_value(self, key: str, value: Any) -> None:
         """Modify a value in the configuration."""
+        key = _normalize_name(key)
         self._ensure_have_load_only()
 
         assert self.load_only
@@ -167,11 +173,13 @@ def set_value(self, key: str, value: Any) -> None:
 
     def unset_value(self, key: str) -> None:
         """Unset a value in the configuration."""
+        orig_key = key
+        key = _normalize_name(key)
         self._ensure_have_load_only()
 
         assert self.load_only
         if key not in self._config[self.load_only]:
-            raise ConfigurationError(f"No such key - {key}")
+            raise ConfigurationError(f"No such key - {orig_key}")
 
         fname, parser = self._get_parser_to_modify()
 


Test patch:
diff --git a/tests/unit/test_configuration.py b/tests/unit/test_configuration.py
--- a/tests/unit/test_configuration.py
+++ b/tests/unit/test_configuration.py
@@ -1,6 +1,7 @@
 """Tests for all things related to the configuration
 """
 
+import re
 from unittest.mock import MagicMock
 
 import pytest
@@ -87,6 +88,25 @@ def test_environment_config_errors_if_malformed(
             err.value
         )
 
+    def test_no_such_key_error_message_no_command(self) -> None:
+        self.configuration.load_only = kinds.GLOBAL
+        self.configuration.load()
+        expected_msg = (
+            "Key does not contain dot separated section and key. "
+            "Perhaps you wanted to use 'global.index-url' instead?"
+        )
+        pat = f"^{re.escape(expected_msg)}$"
+        with pytest.raises(ConfigurationError, match=pat):
+            self.configuration.get_value("index-url")
+
+    def test_no_such_key_error_message_missing_option(self) -> None:
+        self.configuration.load_only = kinds.GLOBAL
+        self.configuration.load()
+        expected_msg = "No such key - global.index-url"
+        pat = f"^{re.escape(expected_msg)}$"
+        with pytest.raises(ConfigurationError, match=pat):
+            self.configuration.get_value("global.index-url")
+
 
 class TestConfigurationPrecedence(ConfigurationMixin):
     # Tests for methods to that determine the order of precedence of
@@ -185,12 +205,8 @@ class TestConfigurationModification(ConfigurationMixin):
     def test_no_specific_given_modification(self) -> None:
         self.configuration.load()
 
-        try:
+        with pytest.raises(ConfigurationError):
             self.configuration.set_value("test.hello", "10")
-        except ConfigurationError:
-            pass
-        else:
-            assert False, "Should have raised an error."
 
     def test_site_modification(self) -> None:
         self.configuration.load_only = kinds.SITE
@@ -241,3 +257,16 @@ def test_global_modification(self) -> None:
         # get the path to user config file
         assert mymock.call_count == 1
         assert mymock.call_args[0][0] == (get_configuration_files()[kinds.GLOBAL][-1])
+
+    def test_normalization(self) -> None:
+        # underscores and dashes can be used interchangeably.
+        # internally, underscores get converted into dashes before reading/writing file
+        self.configuration.load_only = kinds.GLOBAL
+        self.configuration.load()
+        self.configuration.set_value("global.index_url", "example.org")
+        assert self.configuration.get_value("global.index_url") == "example.org"
+        assert self.configuration.get_value("global.index-url") == "example.org"
+        self.configuration.unset_value("global.index-url")
+        pat = r"^No such key - global\.index-url$"
+        with pytest.raises(ConfigurationError, match=pat):
+            self.configuration.get_value("global.index-url")