Repository: python-poetry/poetry

Issue:
Unsolicited messages output to stdout
As requested by @neersighted in #1611, opening a new issue.

1. Install Poetry 1.2.0
2. poetry config virtualenvs.create false
3. Try to run something using `poetry run`. Observe that a message “Skipping virtualenv creation, as specified in config file.” is output to stdout:
```
$ poetry run date 2>/dev/null
Skipping virtualenv creation, as specified in config file.
Tue  6 Sep 16:07:21 UTC 2022
```

Or try `poetry export`:
```
$ poetry export >export.stdout 2>export.stderr
$ cat export.stdout
Skipping virtualenv creation, as specified in config file.
Skipping virtualenv creation, as specified in config file.
Updating dependencies
Resolving dependencies...

Writing lock file
useless-py==1.0.3 ; python_version >= "3.10" and python_version < "4.0" \
    --hash=sha256:27117ffe3044ffeee36af4572c224e445e04878668c45b133cfe606998c73b2a
$ cat export.stderr
The lock file does not exist. Locking.
```

The way `-q` affects this is inconsistent. With `run` it removes the virtualenv message, with `export` it removes everything, making the command useless (unless `-o` is specified). And while it’s a step in the right direction, someone can write a script using, for example, `poetry run scripts/make-some-json | jq ...` and it will work under the default settings but not for someone who has set up the same codebase with virtualenvs.create=false (for example, in a container). You can’t expect developers to add `-q` to all poetry invocations in scripts just in case at a later point poetry decides to add something to the output.

Suggested resolution: whenever there’s a “main” output (the machine-readable output of `poetry export`, or `poetry run` that could be anything) that could be saved into a file, piped etc., don’t add anything to it _unless_ `-v` is present, in which case write it to stderr. (When the entirety of the output is generated by poetry itself, e. g. with `poetry install`, there’s no problem with it printing whatever it likes.)


Discussion:
Some or more of the `io.write_line()` in `/poetry/utils/env.py` should be `io.write_error_line()`, to write to stderr instead of stdout.

MRs welcome, I'm sure.

Resolving patch:
diff --git a/src/poetry/utils/env.py b/src/poetry/utils/env.py
--- a/src/poetry/utils/env.py
+++ b/src/poetry/utils/env.py
@@ -536,15 +536,15 @@ def _detect_active_python(self, io: IO) -> str | None:
         executable = None
 
         try:
-            io.write_line(
+            io.write_error_line(
                 "Trying to detect current active python executable as specified in the"
                 " config.",
                 verbosity=Verbosity.VERBOSE,
             )
             executable = self._full_python_path("python")
-            io.write_line(f"Found: {executable}", verbosity=Verbosity.VERBOSE)
+            io.write_error_line(f"Found: {executable}", verbosity=Verbosity.VERBOSE)
         except CalledProcessError:
-            io.write_line(
+            io.write_error_line(
                 "Unable to detect the current active python executable. Falling back to"
                 " default.",
                 verbosity=Verbosity.VERBOSE,
@@ -651,7 +651,9 @@ def deactivate(self, io: IO) -> None:
             env = envs.get(name)
             if env is not None:
                 venv = venv_path / f"{name}-py{env['minor']}"
-                io.write_line(f"Deactivating virtualenv: <comment>{venv}</comment>")
+                io.write_error_line(
+                    f"Deactivating virtualenv: <comment>{venv}</comment>"
+                )
                 del envs[name]
 
                 envs_file.write(envs)
@@ -911,7 +913,7 @@ def create_venv(
                 python = "python" + python_to_try
 
                 if io.is_debug():
-                    io.write_line(f"<debug>Trying {python}</debug>")
+                    io.write_error_line(f"<debug>Trying {python}</debug>")
 
                 try:
                     python_patch = decode(
@@ -930,7 +932,7 @@ def create_venv(
                     continue
 
                 if supported_python.allows(Version.parse(python_patch)):
-                    io.write_line(f"Using <c1>{python}</c1> ({python_patch})")
+                    io.write_error_line(f"Using <c1>{python}</c1> ({python_patch})")
                     executable = python
                     python_minor = ".".join(python_patch.split(".")[:2])
                     break
@@ -955,7 +957,7 @@ def create_venv(
 
         if not venv.exists():
             if create_venv is False:
-                io.write_line(
+                io.write_error_line(
                     "<fg=black;bg=yellow>"
                     "Skipping virtualenv creation, "
                     "as specified in config file."
@@ -964,7 +966,7 @@ def create_venv(
 
                 return self.get_system_env()
 
-            io.write_line(
+            io.write_error_line(
                 f"Creating virtualenv <c1>{name}</> in"
                 f" {venv_path if not WINDOWS else get_real_windows_path(venv_path)!s}"
             )
@@ -976,11 +978,11 @@ def create_venv(
                         f"<warning>The virtual environment found in {env.path} seems to"
                         " be broken.</warning>"
                     )
-                io.write_line(f"Recreating virtualenv <c1>{name}</> in {venv!s}")
+                io.write_error_line(f"Recreating virtualenv <c1>{name}</> in {venv!s}")
                 self.remove_venv(venv)
                 create_venv = True
             elif io.is_very_verbose():
-                io.write_line(f"Virtualenv <c1>{name}</> already exists.")
+                io.write_error_line(f"Virtualenv <c1>{name}</> already exists.")
 
         if create_venv:
             self.build_venv(
@@ -1917,14 +1919,14 @@ def build_environment(
 
             if io:
                 if not overwrite:
-                    io.write_line("")
+                    io.write_error_line("")
 
                 requires = [
                     f"<c1>{requirement}</c1>"
                     for requirement in poetry.pyproject.build_system.requires
                 ]
 
-                io.overwrite(
+                io.overwrite_error(
                     "<b>Preparing</b> build environment with build-system requirements"
                     f" {', '.join(requires)}"
                 )
@@ -1938,7 +1940,7 @@ def build_environment(
 
             if overwrite:
                 assert io is not None
-                io.write_line("")
+                io.write_error_line("")
 
             yield venv
     else:


Test patch:
diff --git a/tests/console/commands/env/test_use.py b/tests/console/commands/env/test_use.py
--- a/tests/console/commands/env/test_use.py
+++ b/tests/console/commands/env/test_use.py
@@ -86,12 +86,11 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file(
     assert envs[venv_name]["minor"] == "3.7"
     assert envs[venv_name]["patch"] == "3.7.1"
 
-    expected = f"""\
-Creating virtualenv {venv_py37.name} in {venv_py37.parent}
-Using virtualenv: {venv_py37}
-"""
-
-    assert tester.io.fetch_output() == expected
+    assert (
+        tester.io.fetch_error()
+        == f"Creating virtualenv {venv_py37.name} in {venv_py37.parent}\n"
+    )
+    assert tester.io.fetch_output() == f"Using virtualenv: {venv_py37}\n"
 
 
 def test_get_prefers_explicitly_activated_virtualenvs_over_env_var(
@@ -149,9 +148,8 @@ def test_get_prefers_explicitly_activated_non_existing_virtualenvs_over_env_var(
 
     tester.execute(python_minor)
 
-    expected = f"""\
-Creating virtualenv {venv_dir.name} in {venv_dir.parent}
-Using virtualenv: {venv_dir}
-"""
-
-    assert tester.io.fetch_output() == expected
+    assert (
+        tester.io.fetch_error()
+        == f"Creating virtualenv {venv_dir.name} in {venv_dir.parent}\n"
+    )
+    assert tester.io.fetch_output() == f"Using virtualenv: {venv_dir}\n"