Repository: pypa/pip

Issue:
List configuration files
**What's the problem this feature will solve?**
Easily identify possible configuration files and if they exist.

**Describe the solution you'd like**
Either as a new option of `pip config` or in `pip debug`, it would list the possible configuration files (global, site, user) and whether they actually exist



Discussion:
`pip config --files` is the color I prefer for the bikeshed.

It probably makes sense to also include the final list of files, in `pip debug`.
I would like to work on this issue @xavfernandez 

I see that [pip._internal.configuration. _load_config_files](https://github.com/pypa/pip/blob/master/src/pip/_internal/configuration.py#L279) is where we load files, and the changes will include capturing this info and displaying to the user as part of `pip config --files`. Am I on the right track here?
We'd want to expose `_iter_config_files` as a public attribute of `Configuration` (https://github.com/pypa/pip/blob/97f639057ec0855804c5cb53423ac74394cdefc6/src/pip/_internal/configuration.py#L373), then use that as part of `pip config --files`.

> We'd want to expose `_iter_config_files` as a public attribute of `Configuration`

Thanks for the tip. I will take your suggestion as a starting point and create PR's accordingly, to implement this feature and document it as well.


> `pip config --files` is the color I prefer for the bikeshed.

Since pip config takes subcommand it might be confusing to have `pip config --files list`? Or should the presence of `--files` preclude subcommands? Or should it be a subcommand? `pip config list-files`.

I may have a slight preference for adding this in `pip debug` output.
`pip config list-files` is significantly better than my original suggestion. :)

`pip debug` is explicitly not for programmatic consumption, and we'd want this to be accessible to end users' automation. That said, this should also be included there. :)

FWIW, I don't have strong preferences on what we call this.
IMO,  once we make `_iter_config_files` a class attribute of `Configuration`, we can use it to display the information anywhere, be it `pip debug` or `pip config`

I think if we add it in `pip debug`, we would want to show that info towards the top, since otherwise if we show it in the latter half, we would need `--verbose` to see it. 

Also `pip config list-files` is the better suggestion to keep with the tradition of using a single word subcommand in `pip config`

I can go ahead and start by creating the interface which will print this info, and then it can be used at either, or both places (`debug` and `config`) as per requirement.

Resolving patch:
diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py
--- a/src/pip/_internal/commands/configuration.py
+++ b/src/pip/_internal/commands/configuration.py
@@ -13,6 +13,7 @@
     kinds,
 )
 from pip._internal.exceptions import PipError
+from pip._internal.utils.logging import indent_log
 from pip._internal.utils.misc import get_prog, write_output
 from pip._internal.utils.typing import MYPY_CHECK_RUNNING
 
@@ -34,6 +35,7 @@ class ConfigurationCommand(Command):
     - get: Get the value associated with name
     - set: Set the name=value
     - unset: Unset the value associated with name
+    - debug: List the configuration files and values defined under them
 
     If none of --user, --global and --site are passed, a virtual
     environment configuration file is used if one is active and the file
@@ -49,6 +51,7 @@ class ConfigurationCommand(Command):
         %prog [<file-option>] get name
         %prog [<file-option>] set name value
         %prog [<file-option>] unset name
+        %prog [<file-option>] debug
     """
 
     def __init__(self, name, summary, isolated=False):
@@ -103,7 +106,8 @@ def run(self, options, args):
             "edit": self.open_in_editor,
             "get": self.get_name,
             "set": self.set_name_value,
-            "unset": self.unset_name
+            "unset": self.unset_name,
+            "debug": self.list_config_values,
         }
 
         # Determine action
@@ -190,6 +194,38 @@ def unset_name(self, options, args):
 
         self._save_configuration()
 
+    def list_config_values(self, options, args):
+        """List config key-value pairs across different config files"""
+        self._get_n_args(args, "debug", n=0)
+
+        self.print_env_var_values()
+        # Iterate over config files and print if they exist, and the
+        # key-value pairs present in them if they do
+        for variant, files in sorted(self.configuration.iter_config_files()):
+            write_output("%s:", variant)
+            for fname in files:
+                with indent_log():
+                    file_exists = os.path.exists(fname)
+                    write_output("%s, exists: %r",
+                                 fname, file_exists)
+                    if file_exists:
+                        self.print_config_file_values(variant)
+
+    def print_config_file_values(self, variant):
+        """Get key-value pairs from the file of a variant"""
+        for name, value in self.configuration.\
+                get_values_in_config(variant).items():
+            with indent_log():
+                write_output("%s: %s", name, value)
+
+    def print_env_var_values(self):
+        """Get key-values pairs present as environment variables"""
+        write_output("%s:", 'env_var')
+        with indent_log():
+            for key, value in sorted(self.configuration.get_environ_vars()):
+                env_var = 'PIP_{}'.format(key.upper())
+                write_output("%s=%r", env_var, value)
+
     def open_in_editor(self, options, args):
         editor = self._determine_editor(options)
 
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
@@ -280,7 +280,7 @@ def _load_config_files(self):
         # type: () -> None
         """Loads configuration from configuration files
         """
-        config_files = dict(self._iter_config_files())
+        config_files = dict(self.iter_config_files())
         if config_files[kinds.ENV][0:1] == [os.devnull]:
             logger.debug(
                 "Skipping loading configuration files due to "
@@ -342,7 +342,7 @@ def _load_environment_vars(self):
         """Loads configuration from environment variables
         """
         self._config[kinds.ENV_VAR].update(
-            self._normalized_keys(":env:", self._get_environ_vars())
+            self._normalized_keys(":env:", self.get_environ_vars())
         )
 
     def _normalized_keys(self, section, items):
@@ -358,7 +358,7 @@ def _normalized_keys(self, section, items):
             normalized[key] = val
         return normalized
 
-    def _get_environ_vars(self):
+    def get_environ_vars(self):
         # type: () -> Iterable[Tuple[str, str]]
         """Returns a generator with all environmental vars with prefix PIP_"""
         for key, val in os.environ.items():
@@ -370,7 +370,7 @@ def _get_environ_vars(self):
                 yield key[4:].lower(), val
 
     # XXX: This is patched in the tests.
-    def _iter_config_files(self):
+    def iter_config_files(self):
         # type: () -> Iterable[Tuple[Kind, List[str]]]
         """Yields variant and configuration files associated with it.
 
@@ -401,6 +401,11 @@ def _iter_config_files(self):
         # finally virtualenv configuration first trumping others
         yield kinds.SITE, config_files[kinds.SITE]
 
+    def get_values_in_config(self, variant):
+        # type: (Kind) -> Dict[str, Any]
+        """Get values present in a config file"""
+        return self._config[variant]
+
     def _get_parser_to_modify(self):
         # type: () -> Tuple[str, RawConfigParser]
         # Determine which parser to modify


Test patch:
diff --git a/tests/functional/test_configuration.py b/tests/functional/test_configuration.py
--- a/tests/functional/test_configuration.py
+++ b/tests/functional/test_configuration.py
@@ -1,11 +1,16 @@
 """Tests for the config command
 """
 
+import re
 import textwrap
 
 import pytest
 
 from pip._internal.cli.status_codes import ERROR
+from pip._internal.configuration import (
+    CONFIG_BASENAME,
+    get_configuration_files,
+)
 from tests.lib.configuration_helpers import ConfigurationMixin, kinds
 
 
@@ -62,3 +67,89 @@ def test_forget_section(self, script):
         result = script.pip("config", "set", "isolated", "true",
                             expect_error=True)
         assert "global.isolated" in result.stderr
+
+    def test_env_var_values(self, script):
+        """Test that pip configuration set with environment variables
+        is correctly displayed under "env_var".
+        """
+
+        env_vars = {
+            "PIP_DEFAULT_TIMEOUT": "60",
+            "PIP_FIND_LINKS": "http://mirror.example.com"
+        }
+        script.environ.update(env_vars)
+
+        result = script.pip("config", "debug")
+        assert "PIP_DEFAULT_TIMEOUT='60'" in result.stdout
+        assert "PIP_FIND_LINKS='http://mirror.example.com'" in result.stdout
+        assert re.search(r"env_var:\n(  .+\n)+", result.stdout)
+
+    def test_env_values(self, script):
+        """Test that custom pip configuration using the environment variable
+        PIP_CONFIG_FILE is correctly displayed under "env". This configuration
+        takes place of per-user configuration file displayed under "user".
+        """
+
+        config_file = script.scratch_path / "test-pip.cfg"
+        script.environ['PIP_CONFIG_FILE'] = str(config_file)
+        config_file.write_text(textwrap.dedent("""\
+            [global]
+            timeout = 60
+
+            [freeze]
+            timeout = 10
+            """))
+
+        result = script.pip("config", "debug")
+        assert "{}, exists: True".format(config_file) in result.stdout
+        assert "global.timeout: 60" in result.stdout
+        assert "freeze.timeout: 10" in result.stdout
+        assert re.search(r"env:\n(  .+\n)+", result.stdout)
+
+    def test_user_values(self, script,):
+        """Test that the user pip configuration set using --user
+        is correctly displayed under "user".  This configuration takes place
+        of custom path location using the environment variable PIP_CONFIG_FILE
+        displayed under "env".
+        """
+
+        # Use new config file
+        new_config_file = get_configuration_files()[kinds.USER][1]
+
+        script.pip("config", "--user", "set", "global.timeout", "60")
+        script.pip("config", "--user", "set", "freeze.timeout", "10")
+
+        result = script.pip("config", "debug")
+        assert "{}, exists: True".format(new_config_file) in result.stdout
+        assert "global.timeout: 60" in result.stdout
+        assert "freeze.timeout: 10" in result.stdout
+        assert re.search(r"user:\n(  .+\n)+", result.stdout)
+
+    def test_site_values(self, script, virtualenv):
+        """Test that the current environment configuration set using --site
+        is correctly displayed under "site".
+        """
+
+        # Site config file will be inside the virtualenv
+        site_config_file = virtualenv.location / CONFIG_BASENAME
+
+        script.pip("config", "--site", "set", "global.timeout", "60")
+        script.pip("config", "--site", "set", "freeze.timeout", "10")
+
+        result = script.pip("config", "debug")
+        assert "{}, exists: True".format(site_config_file) in result.stdout
+        assert "global.timeout: 60" in result.stdout
+        assert "freeze.timeout: 10" in result.stdout
+        assert re.search(r"site:\n(  .+\n)+", result.stdout)
+
+    def test_global_config_file(self, script):
+        """Test that the system-wide  configuration can be identified"""
+
+        # We cannot  write to system-wide files which might have permissions
+        # defined in a way that the tox virtualenvcannot write to those
+        # locations. Additionally we cannot patch those paths since pip config
+        # commands runs inside a subprocess.
+        # So we just check if the file can be identified
+        global_config_file = get_configuration_files()[kinds.GLOBAL][0]
+        result = script.pip("config", "debug")
+        assert "{}, exists:".format(global_config_file) in result.stdout
diff --git a/tests/unit/test_options.py b/tests/unit/test_options.py
--- a/tests/unit/test_options.py
+++ b/tests/unit/test_options.py
@@ -387,7 +387,7 @@ def test_venv_config_file_found(self, monkeypatch):
         cp = pip._internal.configuration.Configuration(isolated=False)
 
         files = []
-        for _, val in cp._iter_config_files():
+        for _, val in cp.iter_config_files():
             files.extend(val)
 
         assert len(files) == 4