Repository: pypa/pip

Issue:
Warnings and errors should be printed in stderr instead of stdout
Warning about owner of home directory should be printed in stderr instead of stdout.

```
$ pip --version
pip 6.0.8 from /usr/lib64/python3.4/site-packages (python 3.4)
$ pip completion --bash

# pip bash completion start
_pip_completion()
{
    COMPREPLY=( $( COMP_WORDS="${COMP_WORDS[*]}" \
                   COMP_CWORD=$COMP_CWORD \
                   PIP_AUTO_COMPLETE=1 $1 ) )
}
complete -o default -F _pip_completion pip
# pip bash completion end

$ HOME="/tmp" pip completion --bash
The directory '/tmp/.cache/pip/log' or its parent directory is not owned by the current user and the debug log has been disabled. Please check the permissions and owner of that directory. If executing pip with sudo, you may want the -H flag.

# pip bash completion start
_pip_completion()
{
    COMPREPLY=( $( COMP_WORDS="${COMP_WORDS[*]}" \
                   COMP_CWORD=$COMP_CWORD \
                   PIP_AUTO_COMPLETE=1 $1 ) )
}
complete -o default -F _pip_completion pip
# pip bash completion end

$ HOME="/tmp" pip completion --bash > /dev/null
$
```

(The last command shows redirection of stdout without stderr.)



Discussion:
We can't really do this unless we redirect all of our `logging.WARN` and probably `logging.ERROR` level messages to stderr. That might be a reasonable thing to do though?

It's worth considering. This came up yesterday, because someone was trying to use `pip show --outdated` in a script, and the error messages get mixed with the actual output.

I'd be tempted to say that anything above info should go to stderr.

I agree that all warnings and errors should be printed in stderr instead of stdout.

Second that. I had `alias sup='sudo -H pip'` as a workaround. Forums start to fill up with questions about it because it seems to confuse beginners.


Resolving patch:
diff --git a/pip/basecommand.py b/pip/basecommand.py
--- a/pip/basecommand.py
+++ b/pip/basecommand.py
@@ -38,7 +38,7 @@ class Command(object):
     name = None
     usage = None
     hidden = False
-    log_stream = "ext://sys.stdout"
+    log_streams = ("ext://sys.stdout", "ext://sys.stderr")
 
     def __init__(self, isolated=False):
         parser_kw = {
@@ -124,6 +124,12 @@ def main(self, args):
         logging_dictConfig({
             "version": 1,
             "disable_existing_loggers": False,
+            "filters": {
+                "exclude_warnings": {
+                    "()": "pip.utils.logging.MaxLevelFilter",
+                    "level": logging.WARNING,
+                },
+            },
             "formatters": {
                 "indent": {
                     "()": IndentingFormatter,
@@ -138,7 +144,14 @@ def main(self, args):
                 "console": {
                     "level": level,
                     "class": "pip.utils.logging.ColorizedStreamHandler",
-                    "stream": self.log_stream,
+                    "stream": self.log_streams[0],
+                    "filters": ["exclude_warnings"],
+                    "formatter": "indent",
+                },
+                "console_errors": {
+                    "level": "WARNING",
+                    "class": "pip.utils.logging.ColorizedStreamHandler",
+                    "stream": self.log_streams[1],
                     "formatter": "indent",
                 },
                 "debug_log": {
@@ -162,6 +175,7 @@ def main(self, args):
                 "level": level,
                 "handlers": list(filter(None, [
                     "console",
+                    "console_errors",
                     "debug_log" if write_debug_log else None,
                     "user_log" if options.log else None,
                 ])),
diff --git a/pip/commands/freeze.py b/pip/commands/freeze.py
--- a/pip/commands/freeze.py
+++ b/pip/commands/freeze.py
@@ -16,7 +16,7 @@ class FreezeCommand(Command):
     usage = """
       %prog [options]"""
     summary = 'Output installed packages in requirements format.'
-    log_stream = "ext://sys.stderr"
+    log_streams = ("ext://sys.stderr", "ext://sys.stderr")
 
     def __init__(self, *args, **kw):
         super(FreezeCommand, self).__init__(*args, **kw)
diff --git a/pip/utils/logging.py b/pip/utils/logging.py
--- a/pip/utils/logging.py
+++ b/pip/utils/logging.py
@@ -119,3 +119,12 @@ def _open(self):
             os.makedirs(os.path.dirname(self.baseFilename))
 
         return logging.handlers.RotatingFileHandler._open(self)
+
+
+class MaxLevelFilter(logging.Filter):
+
+    def __init__(self, level):
+        self.level = level
+
+    def filter(self, record):
+        return record.levelno < self.level


Test patch:
diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py
--- a/tests/functional/test_install.py
+++ b/tests/functional/test_install.py
@@ -27,7 +27,7 @@ def test_without_setuptools(script, data):
     )
     assert (
         "setuptools must be installed to install from a source distribution"
-        in result.stdout
+        in result.stderr
     )
 
 
@@ -74,7 +74,7 @@ def test_editable_install(script):
     result = script.pip('install', '-e', 'INITools==0.2', expect_error=True)
     assert (
         "INITools==0.2 should either be a path to a local project or a VCS url"
-        in result.stdout
+        in result.stderr
     )
     assert not result.files_created
     assert not result.files_updated
@@ -205,7 +205,7 @@ def test_bad_install_with_no_download(script):
     )
     assert (
         "perhaps --no-download was used without first running "
-        "an equivalent install with --no-install?" in result.stdout
+        "an equivalent install with --no-install?" in result.stderr
     )
 
 
@@ -310,7 +310,7 @@ def test_install_from_local_directory_with_no_setup_py(script, data):
     """
     result = script.pip('install', data.root, expect_error=True)
     assert not result.files_created
-    assert "is not installable. File 'setup.py' not found." in result.stdout
+    assert "is not installable. File 'setup.py' not found." in result.stderr
 
 
 def test_editable_install_from_local_directory_with_no_setup_py(script, data):
@@ -319,7 +319,7 @@ def test_editable_install_from_local_directory_with_no_setup_py(script, data):
     """
     result = script.pip('install', '-e', data.root, expect_error=True)
     assert not result.files_created
-    assert "is not installable. File 'setup.py' not found." in result.stdout
+    assert "is not installable. File 'setup.py' not found." in result.stderr
 
 
 def test_install_as_egg(script, data):
@@ -508,7 +508,9 @@ def test_install_package_with_target(script):
     )
 
     # Test repeated call without --upgrade, no files should have changed
-    result = script.pip_install_local('-t', target_dir, "simple==1.0")
+    result = script.pip_install_local(
+        '-t', target_dir, "simple==1.0", expect_stderr=True,
+    )
     assert not Path('scratch') / 'target' / 'simple' in result.files_updated
 
     # Test upgrade call, check that new version is installed
@@ -658,6 +660,7 @@ def test_url_incorrect_case_file_index(script, data):
     """
     result = script.pip(
         'install', '--index-url', data.find_links3, "dinner",
+        expect_stderr=True,
     )
 
     # only Upper-2.0.tar.gz should get installed.
diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py
--- a/tests/functional/test_install_cleanup.py
+++ b/tests/functional/test_install_cleanup.py
@@ -80,7 +80,9 @@ def test_no_install_and_download_should_not_leave_build_dir(script):
     script.scratch_path.join("downloaded_packages").mkdir()
     assert not os.path.exists(script.venv_path / 'build')
     result = script.pip(
-        'install', '--no-install', 'INITools==0.2', '-d', 'downloaded_packages'
+        'install', '--no-install', 'INITools==0.2', '-d',
+        'downloaded_packages',
+        expect_stderr=True,
     )
     assert (
         Path('scratch') / 'downloaded_packages/build'
@@ -118,7 +120,10 @@ def test_download_should_not_delete_existing_build_dir(script):
     """
     script.venv_path.join("build").mkdir()
     script.venv_path.join("build", "somefile.txt").write("I am not empty!")
-    script.pip('install', '--no-install', 'INITools==0.2', '-d', '.')
+    script.pip(
+        'install', '--no-install', 'INITools==0.2', '-d', '.',
+        expect_stderr=True,
+    )
     with open(script.venv_path / 'build' / 'somefile.txt') as fp:
         content = fp.read()
     assert os.path.exists(script.venv_path / 'build'), (
@@ -175,5 +180,5 @@ def test_cleanup_prevented_upon_build_dir_exception(script, data):
     )
 
     assert result.returncode == PREVIOUS_BUILD_DIR_ERROR
-    assert "pip can't proceed" in result.stdout, result.stdout
+    assert "pip can't proceed" in result.stderr
     assert exists(build_simple)
diff --git a/tests/functional/test_install_extras.py b/tests/functional/test_install_extras.py
--- a/tests/functional/test_install_extras.py
+++ b/tests/functional/test_install_extras.py
@@ -67,7 +67,7 @@ def test_nonexistent_extra_warns_user_no_wheel(script, data):
     )
     assert (
         "simple 3.0 does not provide the extra 'nonexistent'"
-        in result.stdout
+        in result.stderr
     )
 
 
@@ -85,7 +85,7 @@ def test_nonexistent_extra_warns_user_with_wheel(script, data):
     )
     assert (
         "simplewheel 2.0 does not provide the extra 'nonexistent'"
-        in result.stdout
+        in result.stderr
     )
 
 
@@ -102,4 +102,4 @@ def test_nonexistent_options_listed_in_order(script, data):
         "  simplewheel 2.0 does not provide the extra 'nonexistent'\n"
         "  simplewheel 2.0 does not provide the extra 'nope'"
     )
-    assert msg in result.stdout
+    assert msg in result.stderr
diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py
--- a/tests/functional/test_install_reqs.py
+++ b/tests/functional/test_install_reqs.py
@@ -137,6 +137,7 @@ def test_install_local_editable_with_extras(script, data):
     res = script.pip(
         'install', '-e', to_install + '[bar]', '--process-dependency-links',
         expect_error=False,
+        expect_stderr=True,
     )
     assert script.site_packages / 'easy-install.pth' in res.files_updated, (
         str(res)
diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py
--- a/tests/functional/test_install_upgrade.py
+++ b/tests/functional/test_install_upgrade.py
@@ -293,8 +293,8 @@ def test_upgrade_vcs_req_with_dist_found(script):
             "743aad47656b27"
         )
     )
-    script.pip("install", req)
-    result = script.pip("install", "-U", req)
+    script.pip("install", req, expect_stderr=True)
+    result = script.pip("install", "-U", req, expect_stderr=True)
     assert "pypi.python.org" not in result.stdout, result.stdout
 
 
diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py
--- a/tests/functional/test_install_user.py
+++ b/tests/functional/test_install_user.py
@@ -104,7 +104,7 @@ def test_install_user_venv_nositepkgs_fails(self, script, data):
         )
         assert (
             "Can not perform a '--user' install. User site-packages are not "
-            "visible in this virtualenv." in result.stdout
+            "visible in this virtualenv." in result.stderr
         )
 
     @pytest.mark.network
@@ -288,5 +288,5 @@ def test_install_user_in_global_virtualenv_with_conflict_fails(
         assert (
             "Will not install to the user site because it will lack sys.path "
             "precedence to %s in %s" %
-            ('INITools', dist_location) in result2.stdout
-        ), result2.stdout
+            ('INITools', dist_location) in result2.stderr
+        )
diff --git a/tests/functional/test_install_vcs.py b/tests/functional/test_install_vcs.py
--- a/tests/functional/test_install_vcs.py
+++ b/tests/functional/test_install_vcs.py
@@ -56,7 +56,8 @@ def test_git_with_sha1_revisions(script):
     script.pip(
         'install', '-e',
         '%s@%s#egg=version_pkg' %
-        ('git+file://' + version_pkg_path.abspath.replace('\\', '/'), sha1)
+        ('git+file://' + version_pkg_path.abspath.replace('\\', '/'), sha1),
+        expect_stderr=True
     )
     version = script.run('version_pkg')
     assert '0.1' in version.stdout, version.stdout
diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py
--- a/tests/functional/test_install_wheel.py
+++ b/tests/functional/test_install_wheel.py
@@ -17,7 +17,10 @@ def test_install_from_future_wheel_version(script, data):
                                 editable=False)
 
     package = data.packages.join("futurewheel-1.9-py2.py3-none-any.whl")
-    result = script.pip('install', package, '--no-index', expect_error=False)
+    result = script.pip(
+        'install', package, '--no-index', expect_error=False,
+        expect_stderr=True,
+    )
     result.assert_installed('futurewheel', without_egg_link=True,
                             editable=False)
 
diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py
--- a/tests/functional/test_list.py
+++ b/tests/functional/test_list.py
@@ -55,7 +55,8 @@ def test_uptodate_flag(script, data):
         'git+https://github.com/pypa/pip-test-package.git#egg=pip-test-package'
     )
     result = script.pip(
-        'list', '-f', data.find_links, '--no-index', '--uptodate'
+        'list', '-f', data.find_links, '--no-index', '--uptodate',
+        expect_stderr=True,
     )
     assert 'simple (1.0)' not in result.stdout  # 3.0 is latest
     assert 'pip-test-package' not in result.stdout  # editables excluded
@@ -78,6 +79,7 @@ def test_outdated_flag(script, data):
     )
     result = script.pip(
         'list', '-f', data.find_links, '--no-index', '--outdated',
+        expect_stderr=True,
     )
     assert 'simple (Current: 1.0 Latest: 3.0 [sdist])' in result.stdout
     assert 'simplewheel (Current: 1.0 Latest: 2.0 [wheel])' in result.stdout
diff --git a/tests/functional/test_search.py b/tests/functional/test_search.py
--- a/tests/functional/test_search.py
+++ b/tests/functional/test_search.py
@@ -129,7 +129,7 @@ def test_search_missing_argument(script):
     Test missing required argument for search
     """
     result = script.pip('search', expect_error=True)
-    assert 'ERROR: Missing required argument (search query).' in result.stdout
+    assert 'ERROR: Missing required argument (search query).' in result.stderr
 
 
 @pytest.mark.network
diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py
--- a/tests/functional/test_show.py
+++ b/tests/functional/test_show.py
@@ -67,7 +67,7 @@ def test_missing_argument(script):
     Test show command with no arguments.
     """
     result = script.pip('show', expect_error=True)
-    assert 'ERROR: Please provide a package name or names.' in result.stdout
+    assert 'ERROR: Please provide a package name or names.' in result.stderr
 
 
 def test_find_package_not_found():
diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py
--- a/tests/functional/test_uninstall.py
+++ b/tests/functional/test_uninstall.py
@@ -47,7 +47,7 @@ def test_simple_uninstall_distutils(script):
     result = script.run('python', pkg_path / 'setup.py', 'install')
     result = script.pip('list')
     assert "distutils-install (0.1)" in result.stdout
-    script.pip('uninstall', 'distutils_install', '-y')
+    script.pip('uninstall', 'distutils_install', '-y', expect_stderr=True)
     result2 = script.pip('list')
     assert "distutils-install (0.1)" not in result2.stdout
 
diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py
--- a/tests/functional/test_wheel.py
+++ b/tests/functional/test_wheel.py
@@ -17,7 +17,7 @@ def test_pip_wheel_fails_without_wheel(script, data):
         'wheel', '--no-index', '-f', data.find_links, 'simple==3.0',
         expect_error=True,
     )
-    assert "'pip wheel' requires the 'wheel' package" in result.stdout
+    assert "'pip wheel' requires the 'wheel' package" in result.stderr
 
 
 @pytest.mark.network